0.3 ready for docker testing
This commit is contained in:
parent
91c5350330
commit
838831b857
15
README.md
15
README.md
@ -16,7 +16,9 @@
|
||||
* hideable sidebar with dragable reveal button
|
||||
* user login system
|
||||
* 'app templates' which are sample config entries for popular self hosted apps
|
||||
* ability to display rest api data on application cards
|
||||
* powerful plugin system for adding data from various sources to display on cards
|
||||
* multiple users, access groups, access settings
|
||||
* tagging system
|
||||
|
||||
## Installation
|
||||
### Docker
|
||||
@ -59,14 +61,3 @@ If you change the config.ini file, you either have to restart the container (or
|
||||
* Materialize css
|
||||
* JavaScript/jQuery/jQueryUI
|
||||
* Requests (python)
|
||||
|
||||
## Version 1.0 TODO list
|
||||
- [ ] finish rest api data functions (post requests, auth)
|
||||
- [ ] finish rest api data display (make it look prettier)
|
||||
- [ ] include nginx & gunicorn in docker container
|
||||
- [ ] tag/folder system & support for services without web redirection
|
||||
- [ ] add more template apps from popular self hosted apps
|
||||
- [ ] pull request template for adding template apps
|
||||
- [ ] rest api data examples for template apps
|
||||
- [ ] find a way to mirror this repo on GitHub for exposure
|
||||
- [ ] support multiple users
|
@ -8,9 +8,12 @@ If you change the config.ini file, you either have to restart the container
|
||||
config to be applied.
|
||||
```ini
|
||||
[Settings]
|
||||
theme = dark
|
||||
theme = light
|
||||
accent = orange
|
||||
background = static/images/backgrounds/background.png
|
||||
background = None
|
||||
roles = admin,user,public_user
|
||||
home_access_groups = admin_only
|
||||
settings_access_groups = admin_only
|
||||
```
|
||||
|
||||
| Variable | Required | Description | Options |
|
||||
@ -35,6 +38,8 @@ sidebar_icon = static/images/apps/default.png
|
||||
description = Example description
|
||||
open_in = iframe
|
||||
data_sources = None
|
||||
tags = Example Tag
|
||||
groups = admin_only
|
||||
```
|
||||
|
||||
| Variable | Required | Description | Options |
|
||||
@ -47,6 +52,8 @@ data_sources = None
|
||||
| sidebar_icon | No | Icon for the sidenav. | /static/images/icons/yourpicture.png, external link to image |
|
||||
| description | No | A short description for the app. | string |
|
||||
| data_sources | No | Data sources to be included on the app's card.*Note: you must have a data source set up in the config above this application entry. | comma separated string |
|
||||
| tags | No | Optionally specify tags for organization on /home | comma separated string |
|
||||
| groups | No | Optionally the access groups that can see this app. | comma separated string |
|
||||
|
||||
##### Access Groups
|
||||
You can create access groups to control what user roles can access parts of the ui. Each
|
||||
|
@ -19,3 +19,8 @@ def error_403(error):
|
||||
@error_pages.app_errorhandler(500)
|
||||
def error_500(error):
|
||||
return render_template("/error_pages/500.html"), 500
|
||||
|
||||
|
||||
@error_pages.route("/unauthorized")
|
||||
def unauthorized():
|
||||
return render_template("/error_pages/unauthorized.html")
|
||||
|
@ -0,0 +1,6 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import SelectField
|
||||
|
||||
|
||||
class TagsForm(FlaskForm):
|
||||
tags = SelectField(choices=[("All tags", "All tags")])
|
@ -27,6 +27,7 @@ class Apps(db.Model):
|
||||
open_in = db.Column(db.String())
|
||||
data_template = db.Column(db.String())
|
||||
groups = db.Column(db.String())
|
||||
tags = db.Column(db.String())
|
||||
|
||||
|
||||
class TemplateApps(db.Model):
|
||||
@ -63,3 +64,8 @@ class Groups(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String())
|
||||
roles = db.Column(db.String())
|
||||
|
||||
|
||||
class Tags(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String())
|
||||
|
@ -1,6 +1,6 @@
|
||||
import os
|
||||
from configparser import ConfigParser
|
||||
from dashmachine.main.models import Apps, Groups, DataSources, DataSourcesArgs
|
||||
from dashmachine.main.models import Apps, Groups, DataSources, DataSourcesArgs, Tags
|
||||
from dashmachine.settings_system.models import Settings
|
||||
from dashmachine.paths import user_data_folder
|
||||
from dashmachine import db
|
||||
@ -29,6 +29,7 @@ def read_config():
|
||||
Apps.query.delete()
|
||||
Settings.query.delete()
|
||||
Groups.query.delete()
|
||||
Tags.query.delete()
|
||||
|
||||
for section in config.sections():
|
||||
|
||||
@ -139,6 +140,17 @@ def read_config():
|
||||
else:
|
||||
app.groups = None
|
||||
|
||||
if "tags" in config[section]:
|
||||
app.tags = config[section]["tags"].title()
|
||||
for tag in app.tags.split(","):
|
||||
tag = tag.strip().title()
|
||||
if not Tags.query.filter_by(name=tag).first():
|
||||
tag_db = Tags(name=tag)
|
||||
db.session.add(tag_db)
|
||||
db.session.commit()
|
||||
else:
|
||||
app.tags = None
|
||||
|
||||
db.session.add(app)
|
||||
db.session.commit()
|
||||
|
||||
|
@ -4,7 +4,8 @@ from secrets import token_hex
|
||||
from htmlmin.main import minify
|
||||
from flask import render_template, url_for, redirect, request, Blueprint, jsonify
|
||||
from flask_login import current_user
|
||||
from dashmachine.main.models import Files, Apps, DataSources
|
||||
from dashmachine.main.models import Files, Apps, DataSources, Tags
|
||||
from dashmachine.main.forms import TagsForm
|
||||
from dashmachine.main.utils import (
|
||||
public_route,
|
||||
check_groups,
|
||||
@ -58,10 +59,14 @@ def check_valid_login():
|
||||
@main.route("/")
|
||||
@main.route("/home", methods=["GET", "POST"])
|
||||
def home():
|
||||
tags_form = TagsForm()
|
||||
tags_form.tags.choices += [
|
||||
(tag.name, tag.name) for tag in Tags.query.order_by(Tags.name).all()
|
||||
]
|
||||
settings = Settings.query.first()
|
||||
if not check_groups(settings.home_access_groups, current_user):
|
||||
return redirect(url_for("user_system.login"))
|
||||
return render_template("main/home.html")
|
||||
return redirect(url_for("error_pages.unauthorized"))
|
||||
return render_template("main/home.html", tags_form=tags_form)
|
||||
|
||||
|
||||
@public_route
|
||||
|
@ -17,6 +17,7 @@ from dashmachine.paths import (
|
||||
user_data_folder,
|
||||
)
|
||||
from dashmachine.version import version
|
||||
from dashmachine import db
|
||||
|
||||
settings_system = Blueprint("settings_system", __name__)
|
||||
|
||||
@ -99,12 +100,14 @@ def edit_user():
|
||||
if form.validate_on_submit():
|
||||
if form.password.data != form.confirm_password.data:
|
||||
return jsonify(data={"err": "Passwords don't match"})
|
||||
add_edit_user(
|
||||
err = add_edit_user(
|
||||
form.username.data,
|
||||
form.password.data,
|
||||
user_id=form.id.data,
|
||||
role=form.role.data,
|
||||
)
|
||||
if err:
|
||||
return jsonify(data={"err": err})
|
||||
else:
|
||||
err_str = ""
|
||||
for fieldName, errorMessages in form.errors.items():
|
||||
@ -115,3 +118,17 @@ def edit_user():
|
||||
users = User.query.all()
|
||||
html = render_template("settings_system/user.html", users=users)
|
||||
return jsonify(data={"err": "success", "html": html})
|
||||
|
||||
|
||||
@settings_system.route("/settings/delete_user", methods=["GET"])
|
||||
def delete_user():
|
||||
admin_users = User.query.filter_by(role="admin").all()
|
||||
user = User.query.filter_by(id=request.args.get("id")).first()
|
||||
if len(admin_users) < 2 and user.role == "admin":
|
||||
return jsonify(data={"err": "You must have at least one admin user"})
|
||||
else:
|
||||
User.query.filter_by(id=request.args.get("id")).delete()
|
||||
db.session.commit()
|
||||
users = User.query.all()
|
||||
html = render_template("settings_system/user.html", users=users)
|
||||
return jsonify(data={"err": "success", "html": html})
|
||||
|
@ -28,6 +28,9 @@
|
||||
.no-vis {
|
||||
visibility: hidden;
|
||||
}
|
||||
.filtered {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
overflow-y: scroll !important;
|
||||
|
23
dashmachine/static/css/main/home.css
Normal file
23
dashmachine/static/css/main/home.css
Normal file
@ -0,0 +1,23 @@
|
||||
|
||||
.tags-select-col {
|
||||
position: relative;
|
||||
top: 15px;
|
||||
margin: 0;
|
||||
border-radius: .4rem;
|
||||
height: 45px;
|
||||
-webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 1px 3px 0 rgba(0, 0, 0, 0.12);
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 1px 3px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.tags-select-col .select-wrapper {
|
||||
top: 5px;
|
||||
}
|
||||
.tags-select-col .select-wrapper input {
|
||||
color: var(--theme-secondary);
|
||||
}
|
||||
@media screen and (max-width: 992px) {
|
||||
.tags-select-col {
|
||||
top: 0;
|
||||
width: calc(100vw - 45px) !important;
|
||||
margin-left: 15px !important;
|
||||
}
|
||||
}
|
@ -27,4 +27,15 @@ $( document ).ready(function() {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#tags-select").on('change', function(e) {
|
||||
var value = $(this).val();
|
||||
$(".app-a").each(function(i, e) {
|
||||
if ($(this).attr("data-tags").indexOf(value) > -1 || value === "All tags") {
|
||||
$(this).removeClass('filtered');
|
||||
} else {
|
||||
$(this).addClass('filtered');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
@ -63,29 +63,21 @@ $( document ).ready(function() {
|
||||
});
|
||||
|
||||
$("#save-user-btn").on('click', function(e) {
|
||||
$.ajax({
|
||||
url: $(this).attr('data-url'),
|
||||
type: 'POST',
|
||||
data: $("#edit-user-form").serialize(),
|
||||
success: function(data){
|
||||
if (data.data.err !== 'success'){
|
||||
M.toast({html: data.data.err, classes: 'theme-failure'});
|
||||
} else {
|
||||
$("#users-div").empty();
|
||||
$("#users-div").append(data.data.html);
|
||||
$("#edit-user-modal").modal('close');
|
||||
M.toast({html: 'User saved'});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(".edit-user-btn").on('click', function(e) {
|
||||
$("#user-modal").modal('open');
|
||||
$("#user-form-username").val($(this).attr("data-username"));
|
||||
$("#user-form-role").val($(this).attr("data-role"));
|
||||
$("#user-form-id").val($(this).attr("data-id"));
|
||||
M.updateTextFields();
|
||||
$.ajax({
|
||||
url: $(this).attr('data-url'),
|
||||
type: 'POST',
|
||||
data: $("#edit-user-form").serialize(),
|
||||
success: function(data){
|
||||
if (data.data.err !== 'success'){
|
||||
M.toast({html: data.data.err, classes: 'theme-failure'});
|
||||
} else {
|
||||
$("#users-div").empty();
|
||||
$("#users-div").append(data.data.html);
|
||||
$("#user-modal").modal('close');
|
||||
M.toast({html: 'User saved'});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
11
dashmachine/templates/error_pages/unauthorized.html
Normal file
11
dashmachine/templates/error_pages/unauthorized.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends "main/base.html" %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="row center-align mt-10">
|
||||
<div class="col s12">
|
||||
<i class="material-icons-outlined theme-failure-text" style="font-size: 8rem">warning</i>
|
||||
<h4>Unauthorized</h4>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
@ -169,10 +169,13 @@ col_style=None
|
||||
id='',
|
||||
form_obj=None,
|
||||
size="s12",
|
||||
label=''
|
||||
label=None,
|
||||
class=''
|
||||
) %}
|
||||
<div class="input-field col {{size}}">
|
||||
{{ form_obj(id=id) }}
|
||||
<label>{{ label }}</label>
|
||||
{{ form_obj(id=id, class=class, placeholder="Tags") }}
|
||||
{% if label %}
|
||||
<label>{{ label }}</label>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
@ -1,10 +1,11 @@
|
||||
{% extends "main/layout.html" %}
|
||||
{% from 'global_macros.html' import data, preload_circle %}
|
||||
{% from 'global_macros.html' import data, preload_circle, select %}
|
||||
|
||||
{% block page_vendor_css %}
|
||||
{% endblock page_vendor_css %}
|
||||
|
||||
{% block page_lvl_css %}
|
||||
{{ process_css_sources(src="main/home.css")|safe }}
|
||||
{% if settings.background and settings.background != 'None' %}
|
||||
<style>
|
||||
#main {
|
||||
@ -27,17 +28,22 @@
|
||||
<input type="text" id="apps-filter" class="card-filter theme-surface-transparent" placeholder="Search apps">
|
||||
</span>
|
||||
</div>
|
||||
{% if tags_form.tags.choices|count > 1 %}
|
||||
<div class="input-field col s12 l2 tags-select-col theme-surface-transparent">
|
||||
{{ tags_form.tags(id='tags-select') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="row">
|
||||
{% if apps %}
|
||||
|
||||
{% for app in apps %}
|
||||
{% if app.open_in == 'iframe' %}
|
||||
<a href="{{ url_for('main.app_view', app_id=app.id) }}" class="app-a" data-name="{{ app.name }}" data-description="{{ app.description }}">
|
||||
<a href="{{ url_for('main.app_view', app_id=app.id) }}" class="app-a" data-name="{{ app.name }}" data-description="{{ app.description }}" data-tags="{{ app.tags }}">
|
||||
{% elif app.open_in == 'this_tab' %}
|
||||
<a href="{{ app.prefix }}{{ app.url }}" class="app-a" data-name="{{ app.name }}" data-description="{{ app.description }}">
|
||||
<a href="{{ app.prefix }}{{ app.url }}" class="app-a" data-name="{{ app.name }}" data-description="{{ app.description }}" data-tags="{{ app.tags }}">
|
||||
{% elif app.open_in == "new_tab" %}
|
||||
<a href="{{ app.prefix }}{{ app.url }}" target="_blank" class="app-a" data-name="{{ app.name }}" data-description="{{ app.description }}">
|
||||
<a href="{{ app.prefix }}{{ app.url }}" target="_blank" class="app-a" data-name="{{ app.name }}" data-description="{{ app.description }}" data-tags="{{ app.tags }}">
|
||||
{% endif %}
|
||||
<div class="col s12 m6 l3">
|
||||
<div class="card theme-surface-transparent app-card">
|
||||
@ -63,7 +69,8 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-action center-align scrollbar" style="max-height: 127px; min-height: 127px; scrollbar-width: none;">
|
||||
<h5>{{ app.name }}</h5>
|
||||
<h5>{{ app.name }}
|
||||
</h5>
|
||||
<span class="theme-secondary-text">{{ app.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -24,10 +24,17 @@
|
||||
<span class="menu-title" data-i18n="">Settings</span>
|
||||
</a></li>
|
||||
|
||||
<li class="bold"><a id="logout-sidenav" class="waves-effect waves-cyan" href="{{ url_for('user_system.logout') }}">
|
||||
<i class="material-icons-outlined">exit_to_app</i>
|
||||
<span class="menu-title" data-i18n="">Logout</span>
|
||||
</a></li>
|
||||
{% if current_user.is_authenticated %}
|
||||
<li class="bold"><a id="logout-sidenav" class="waves-effect waves-cyan" href="{{ url_for('user_system.logout') }}">
|
||||
<i class="material-icons-outlined">exit_to_app</i>
|
||||
<span class="menu-title" data-i18n="">Logout</span>
|
||||
</a></li>
|
||||
{% else %}
|
||||
<li class="bold"><a id="logout-sidenav" class="waves-effect waves-cyan" href="{{ url_for('user_system.login') }}">
|
||||
<i class="material-icons-outlined">account_circle</i>
|
||||
<span class="menu-title" data-i18n="">Login</span>
|
||||
</a></li>
|
||||
{% endif %}
|
||||
|
||||
<li class="bold"><a id="hide-sidenav" class="waves-effect waves-cyan" href="#">
|
||||
<i class="material-icons-outlined">menu_open</i>
|
||||
|
@ -1,78 +1,106 @@
|
||||
<style>
|
||||
.file-title {
|
||||
position: relative;
|
||||
top: .6em;
|
||||
}
|
||||
</style>
|
||||
{% macro FilesTab() %}
|
||||
<div class="row">
|
||||
<h5>Images</h5>
|
||||
<form id="add-images-form">
|
||||
<div class="input-field col s12 mt-4">
|
||||
<select name="folder">
|
||||
<option value="icons">Icons</option>
|
||||
<option value="backgrounds">Backgrounds</option>
|
||||
</select>
|
||||
<label>Folder</label>
|
||||
</div>
|
||||
<input name="files" id="add-images-input" class="hide">
|
||||
</form>
|
||||
<div class="col s12">
|
||||
{{ tcdrop(allowed_types='jpg,jpeg,png,gif', id="images-tcdrop", max_files="30") }}
|
||||
{{ button(text="save", icon="save", id="save-images-btn", float="left", data={"url": url_for('settings_system.add_images')}) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
<div class="row">
|
||||
<div class="col s12">
|
||||
<ul class="collection with-header">
|
||||
<li class="collection-header theme-primary theme-on-primary-text">Backgrounds</li>
|
||||
{% if backgrounds %}
|
||||
{% for background in backgrounds %}
|
||||
<li class="collection-item pt-2 pb-2 avatar">
|
||||
<a href="static/images/backgrounds/{{ background }}" target="_blank">
|
||||
<img src="static/images/backgrounds/{{ background }}" alt="" class="circle">
|
||||
</a>
|
||||
<span class="selectable-all copy-target file-title">static/images/backgrounds/{{ background }}</span>
|
||||
<span class="secondary-content">
|
||||
<div class="row">
|
||||
<div id="files-div">{{ files_html|safe }}</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro Files(icons, backgrounds) %}
|
||||
<style>
|
||||
.file-title {
|
||||
position: relative;
|
||||
top: .6em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="divider"></div>
|
||||
<div class="row">
|
||||
<div class="col s12">
|
||||
<ul class="collection with-header">
|
||||
<li class="collection-header theme-primary theme-on-primary-text">Backgrounds</li>
|
||||
{% if backgrounds %}
|
||||
{% for background in backgrounds %}
|
||||
<li class="collection-item pt-2 pb-2 avatar">
|
||||
<a href="static/images/backgrounds/{{ background }}" target="_blank">
|
||||
<img src="static/images/backgrounds/{{ background }}" alt="" class="circle">
|
||||
</a>
|
||||
<span class="selectable-all copy-target file-title">static/images/backgrounds/{{ background }}</span>
|
||||
<span class="secondary-content">
|
||||
<i class="material-icons-outlined icon-btn delete-file-btn"
|
||||
data-url="{{ url_for('settings_system.delete_file') }}"
|
||||
data-folder="backgrounds"
|
||||
data-file="{{ background }}">close</i>
|
||||
</span>
|
||||
<span class="secondary-content mr-4"><i class="material-icons-outlined icon-btn copy-btn">filter_none</i></span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<li class="collection-item">No files yet</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<span class="secondary-content mr-4"><i class="material-icons-outlined icon-btn copy-btn">filter_none</i></span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<li class="collection-item">No files yet</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="row">
|
||||
<div class="col s12">
|
||||
<ul class="collection with-header">
|
||||
<li class="collection-header theme-primary theme-on-primary-text">Icons</li>
|
||||
{% if icons %}
|
||||
{% for icon in icons %}
|
||||
<li class="collection-item pt-2 pb-2 avatar">
|
||||
<a href="static/images/icons/{{ icon }}" target="_blank">
|
||||
<img src="static/images/icons/{{ icon }}" alt="" class="circle">
|
||||
</a>
|
||||
<span class="selectable-all copy-target file-title">static/images/icons/{{ icon }}</span>
|
||||
<span class="secondary-content">
|
||||
<div class="divider"></div>
|
||||
<div class="row">
|
||||
<div class="col s12">
|
||||
<ul class="collection with-header">
|
||||
<li class="collection-header theme-primary theme-on-primary-text">Icons</li>
|
||||
{% if icons %}
|
||||
{% for icon in icons %}
|
||||
<li class="collection-item pt-2 pb-2 avatar">
|
||||
<a href="static/images/icons/{{ icon }}" target="_blank">
|
||||
<img src="static/images/icons/{{ icon }}" alt="" class="circle">
|
||||
</a>
|
||||
<span class="selectable-all copy-target file-title">static/images/icons/{{ icon }}</span>
|
||||
<span class="secondary-content">
|
||||
<i class="material-icons-outlined icon-btn delete-file-btn"
|
||||
data-url="{{ url_for('settings_system.delete_file') }}"
|
||||
data-folder="icons"
|
||||
data-file="{{ icon }}">close</i>
|
||||
</span>
|
||||
<span class="secondary-content mr-4"><i class="material-icons-outlined icon-btn copy-btn">filter_none</i></span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<li class="collection-item">No files yet</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<span class="secondary-content mr-4"><i class="material-icons-outlined icon-btn copy-btn">filter_none</i></span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<li class="collection-item">No files yet</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$( document ).ready(function() {
|
||||
init_copy_btn(".collection-item");
|
||||
$(".delete-file-btn").on('click', function(e) {
|
||||
$.ajax({
|
||||
url: $(this).attr('data-url'),
|
||||
type: 'GET',
|
||||
data: {folder: $(this).attr("data-folder"), file: $(this).attr("data-file")},
|
||||
success: function(data){
|
||||
$("#files-div").empty();
|
||||
$("#files-div").append(data);
|
||||
}
|
||||
});
|
||||
<script>
|
||||
$( document ).ready(function() {
|
||||
init_copy_btn(".collection-item");
|
||||
$(".delete-file-btn").on('click', function(e) {
|
||||
$.ajax({
|
||||
url: $(this).attr('data-url'),
|
||||
type: 'GET',
|
||||
data: {folder: $(this).attr("data-folder"), file: $(this).attr("data-file")},
|
||||
success: function(data){
|
||||
$("#files-div").empty();
|
||||
$("#files-div").append(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
{% endmacro %}
|
||||
|
||||
{{ Files(icons, backgrounds) }}
|
@ -2,6 +2,7 @@
|
||||
{% from 'global_macros.html' import input, button, select %}
|
||||
{% from 'main/tcdrop.html' import tcdrop %}
|
||||
{% from 'settings_system/user.html' import UserTab with context %}
|
||||
{% from 'settings_system/files.html' import FilesTab with context %}
|
||||
{% block page_vendor_css %}
|
||||
{% endblock page_vendor_css %}
|
||||
|
||||
@ -80,28 +81,7 @@
|
||||
</div>
|
||||
|
||||
<div id="images" class="col s12">
|
||||
<div class="row">
|
||||
<h5>Images</h5>
|
||||
<form id="add-images-form">
|
||||
<div class="input-field col s12 mt-4">
|
||||
<select name="folder">
|
||||
<option value="icons">Icons</option>
|
||||
<option value="backgrounds">Backgrounds</option>
|
||||
</select>
|
||||
<label>Folder</label>
|
||||
</div>
|
||||
<input name="files" id="add-images-input" class="hide">
|
||||
</form>
|
||||
<div class="col s12">
|
||||
{{ tcdrop(allowed_types='jpg,jpeg,png,gif', id="images-tcdrop", max_files="30") }}
|
||||
{{ button(text="save", icon="save", id="save-images-btn", float="left", data={"url": url_for('settings_system.add_images')}) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div id="files-div">{{ files_html|safe }}</div>
|
||||
</div>
|
||||
|
||||
{{ FilesTab() }}
|
||||
</div>
|
||||
|
||||
<div id="apps" class="col s12">
|
||||
|
@ -61,7 +61,7 @@
|
||||
</a>
|
||||
</h5>
|
||||
|
||||
<div class="users-div">
|
||||
<div id="users-div">
|
||||
{{ Users(users) }}
|
||||
</div>
|
||||
|
||||
@ -88,11 +88,43 @@
|
||||
data-role="{{ user.role }}"
|
||||
data-id="{{ user.id }}"
|
||||
data-username="{{ user.username }}">edit</i>
|
||||
<i class="material-icons-outlined icon-btn">close</i>
|
||||
<i class="material-icons-outlined icon-btn delete-user-btn"
|
||||
data-id="{{ user.id }}"
|
||||
data-url="{{ url_for('settings_system.delete_user') }}">close</i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<script>
|
||||
$(".edit-user-btn").on('click', function(e) {
|
||||
$("#user-modal").modal('open');
|
||||
$("#user-form-username").val($(this).attr("data-username"));
|
||||
$("#user-form-role").val($(this).attr("data-role"));
|
||||
$("#user-form-id").val($(this).attr("data-id"));
|
||||
M.updateTextFields();
|
||||
});
|
||||
|
||||
$(".delete-user-btn").on('click', function(e) {
|
||||
var r = confirm("Are you sure?");
|
||||
if (r == true) {
|
||||
$.ajax({
|
||||
url: $(this).attr('data-url'),
|
||||
type: 'GET',
|
||||
data: {id: $(this).attr('data-id')},
|
||||
success: function(data){
|
||||
if (data.data.err !== 'success') {
|
||||
M.toast({html: data.data.err, classes: 'theme-failure'});
|
||||
} else {
|
||||
$("#users-div").empty();
|
||||
$("#users-div").append(data.data.html);
|
||||
$("#edit-user-modal").modal('close');
|
||||
M.toast({html: 'User deleted'});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endmacro %}
|
||||
|
||||
{{Users(users)}}
|
@ -10,6 +10,10 @@ def add_edit_user(username, password, user_id=None, role=None):
|
||||
else:
|
||||
user = User()
|
||||
|
||||
admin_users = User.query.filter_by(role="admin").all()
|
||||
if user_id and role != "admin" and len(admin_users) < 2:
|
||||
return "You must have at least one admin user"
|
||||
|
||||
hashed_password = bcrypt.generate_password_hash(password).decode("utf-8")
|
||||
user.username = username
|
||||
user.password = hashed_password
|
||||
|
@ -1 +1 @@
|
||||
version = "v0.22"
|
||||
version = "v0.3"
|
||||
|
@ -2,3 +2,6 @@
|
||||
theme = light
|
||||
accent = orange
|
||||
background = None
|
||||
roles = admin,user,public_user
|
||||
home_access_groups = admin_only
|
||||
settings_access_groups = admin_only
|
28
migrations/versions/885c5f9b33d5_.py
Normal file
28
migrations/versions/885c5f9b33d5_.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 885c5f9b33d5
|
||||
Revises: 8f5a046465e8
|
||||
Create Date: 2020-02-08 13:30:01.632487
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "885c5f9b33d5"
|
||||
down_revision = "8f5a046465e8"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column("apps", sa.Column("tags", sa.String(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("apps", "tags")
|
||||
# ### end Alembic commands ###
|
Loading…
x
Reference in New Issue
Block a user