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
|
* hideable sidebar with dragable reveal button
|
||||||
* user login system
|
* user login system
|
||||||
* 'app templates' which are sample config entries for popular self hosted apps
|
* '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
|
## Installation
|
||||||
### Docker
|
### Docker
|
||||||
@ -59,14 +61,3 @@ If you change the config.ini file, you either have to restart the container (or
|
|||||||
* Materialize css
|
* Materialize css
|
||||||
* JavaScript/jQuery/jQueryUI
|
* JavaScript/jQuery/jQueryUI
|
||||||
* Requests (python)
|
* 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.
|
config to be applied.
|
||||||
```ini
|
```ini
|
||||||
[Settings]
|
[Settings]
|
||||||
theme = dark
|
theme = light
|
||||||
accent = orange
|
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 |
|
| Variable | Required | Description | Options |
|
||||||
@ -35,6 +38,8 @@ sidebar_icon = static/images/apps/default.png
|
|||||||
description = Example description
|
description = Example description
|
||||||
open_in = iframe
|
open_in = iframe
|
||||||
data_sources = None
|
data_sources = None
|
||||||
|
tags = Example Tag
|
||||||
|
groups = admin_only
|
||||||
```
|
```
|
||||||
|
|
||||||
| Variable | Required | Description | Options |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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
|
##### Access Groups
|
||||||
You can create access groups to control what user roles can access parts of the ui. Each
|
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)
|
@error_pages.app_errorhandler(500)
|
||||||
def error_500(error):
|
def error_500(error):
|
||||||
return render_template("/error_pages/500.html"), 500
|
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())
|
open_in = db.Column(db.String())
|
||||||
data_template = db.Column(db.String())
|
data_template = db.Column(db.String())
|
||||||
groups = db.Column(db.String())
|
groups = db.Column(db.String())
|
||||||
|
tags = db.Column(db.String())
|
||||||
|
|
||||||
|
|
||||||
class TemplateApps(db.Model):
|
class TemplateApps(db.Model):
|
||||||
@ -63,3 +64,8 @@ class Groups(db.Model):
|
|||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
name = db.Column(db.String())
|
name = db.Column(db.String())
|
||||||
roles = 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
|
import os
|
||||||
from configparser import ConfigParser
|
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.settings_system.models import Settings
|
||||||
from dashmachine.paths import user_data_folder
|
from dashmachine.paths import user_data_folder
|
||||||
from dashmachine import db
|
from dashmachine import db
|
||||||
@ -29,6 +29,7 @@ def read_config():
|
|||||||
Apps.query.delete()
|
Apps.query.delete()
|
||||||
Settings.query.delete()
|
Settings.query.delete()
|
||||||
Groups.query.delete()
|
Groups.query.delete()
|
||||||
|
Tags.query.delete()
|
||||||
|
|
||||||
for section in config.sections():
|
for section in config.sections():
|
||||||
|
|
||||||
@ -139,6 +140,17 @@ def read_config():
|
|||||||
else:
|
else:
|
||||||
app.groups = None
|
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.add(app)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
@ -4,7 +4,8 @@ from secrets import token_hex
|
|||||||
from htmlmin.main import minify
|
from htmlmin.main import minify
|
||||||
from flask import render_template, url_for, redirect, request, Blueprint, jsonify
|
from flask import render_template, url_for, redirect, request, Blueprint, jsonify
|
||||||
from flask_login import current_user
|
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 (
|
from dashmachine.main.utils import (
|
||||||
public_route,
|
public_route,
|
||||||
check_groups,
|
check_groups,
|
||||||
@ -58,10 +59,14 @@ def check_valid_login():
|
|||||||
@main.route("/")
|
@main.route("/")
|
||||||
@main.route("/home", methods=["GET", "POST"])
|
@main.route("/home", methods=["GET", "POST"])
|
||||||
def home():
|
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()
|
settings = Settings.query.first()
|
||||||
if not check_groups(settings.home_access_groups, current_user):
|
if not check_groups(settings.home_access_groups, current_user):
|
||||||
return redirect(url_for("user_system.login"))
|
return redirect(url_for("error_pages.unauthorized"))
|
||||||
return render_template("main/home.html")
|
return render_template("main/home.html", tags_form=tags_form)
|
||||||
|
|
||||||
|
|
||||||
@public_route
|
@public_route
|
||||||
|
@ -17,6 +17,7 @@ from dashmachine.paths import (
|
|||||||
user_data_folder,
|
user_data_folder,
|
||||||
)
|
)
|
||||||
from dashmachine.version import version
|
from dashmachine.version import version
|
||||||
|
from dashmachine import db
|
||||||
|
|
||||||
settings_system = Blueprint("settings_system", __name__)
|
settings_system = Blueprint("settings_system", __name__)
|
||||||
|
|
||||||
@ -99,12 +100,14 @@ def edit_user():
|
|||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
if form.password.data != form.confirm_password.data:
|
if form.password.data != form.confirm_password.data:
|
||||||
return jsonify(data={"err": "Passwords don't match"})
|
return jsonify(data={"err": "Passwords don't match"})
|
||||||
add_edit_user(
|
err = add_edit_user(
|
||||||
form.username.data,
|
form.username.data,
|
||||||
form.password.data,
|
form.password.data,
|
||||||
user_id=form.id.data,
|
user_id=form.id.data,
|
||||||
role=form.role.data,
|
role=form.role.data,
|
||||||
)
|
)
|
||||||
|
if err:
|
||||||
|
return jsonify(data={"err": err})
|
||||||
else:
|
else:
|
||||||
err_str = ""
|
err_str = ""
|
||||||
for fieldName, errorMessages in form.errors.items():
|
for fieldName, errorMessages in form.errors.items():
|
||||||
@ -115,3 +118,17 @@ def edit_user():
|
|||||||
users = User.query.all()
|
users = User.query.all()
|
||||||
html = render_template("settings_system/user.html", users=users)
|
html = render_template("settings_system/user.html", users=users)
|
||||||
return jsonify(data={"err": "success", "html": html})
|
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 {
|
.no-vis {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
.filtered {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.scrollbar {
|
.scrollbar {
|
||||||
overflow-y: scroll !important;
|
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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
@ -73,19 +73,11 @@ $( document ).ready(function() {
|
|||||||
} else {
|
} else {
|
||||||
$("#users-div").empty();
|
$("#users-div").empty();
|
||||||
$("#users-div").append(data.data.html);
|
$("#users-div").append(data.data.html);
|
||||||
$("#edit-user-modal").modal('close');
|
$("#user-modal").modal('close');
|
||||||
M.toast({html: 'User saved'});
|
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();
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
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='',
|
id='',
|
||||||
form_obj=None,
|
form_obj=None,
|
||||||
size="s12",
|
size="s12",
|
||||||
label=''
|
label=None,
|
||||||
|
class=''
|
||||||
) %}
|
) %}
|
||||||
<div class="input-field col {{size}}">
|
<div class="input-field col {{size}}">
|
||||||
{{ form_obj(id=id) }}
|
{{ form_obj(id=id, class=class, placeholder="Tags") }}
|
||||||
|
{% if label %}
|
||||||
<label>{{ label }}</label>
|
<label>{{ label }}</label>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
@ -1,10 +1,11 @@
|
|||||||
{% extends "main/layout.html" %}
|
{% 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 %}
|
{% block page_vendor_css %}
|
||||||
{% endblock page_vendor_css %}
|
{% endblock page_vendor_css %}
|
||||||
|
|
||||||
{% block page_lvl_css %}
|
{% block page_lvl_css %}
|
||||||
|
{{ process_css_sources(src="main/home.css")|safe }}
|
||||||
{% if settings.background and settings.background != 'None' %}
|
{% if settings.background and settings.background != 'None' %}
|
||||||
<style>
|
<style>
|
||||||
#main {
|
#main {
|
||||||
@ -27,17 +28,22 @@
|
|||||||
<input type="text" id="apps-filter" class="card-filter theme-surface-transparent" placeholder="Search apps">
|
<input type="text" id="apps-filter" class="card-filter theme-surface-transparent" placeholder="Search apps">
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{% if apps %}
|
{% if apps %}
|
||||||
|
|
||||||
{% for app in apps %}
|
{% for app in apps %}
|
||||||
{% if app.open_in == 'iframe' %}
|
{% 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' %}
|
{% 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" %}
|
{% 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 %}
|
{% endif %}
|
||||||
<div class="col s12 m6 l3">
|
<div class="col s12 m6 l3">
|
||||||
<div class="card theme-surface-transparent app-card">
|
<div class="card theme-surface-transparent app-card">
|
||||||
@ -63,7 +69,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-action center-align scrollbar" style="max-height: 127px; min-height: 127px; scrollbar-width: none;">
|
<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>
|
<span class="theme-secondary-text">{{ app.description }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -24,10 +24,17 @@
|
|||||||
<span class="menu-title" data-i18n="">Settings</span>
|
<span class="menu-title" data-i18n="">Settings</span>
|
||||||
</a></li>
|
</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') }}">
|
<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>
|
<i class="material-icons-outlined">exit_to_app</i>
|
||||||
<span class="menu-title" data-i18n="">Logout</span>
|
<span class="menu-title" data-i18n="">Logout</span>
|
||||||
</a></li>
|
</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="#">
|
<li class="bold"><a id="hide-sidenav" class="waves-effect waves-cyan" href="#">
|
||||||
<i class="material-icons-outlined">menu_open</i>
|
<i class="material-icons-outlined">menu_open</i>
|
||||||
|
@ -1,12 +1,37 @@
|
|||||||
<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="row">
|
||||||
|
<div id="files-div">{{ files_html|safe }}</div>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro Files(icons, backgrounds) %}
|
||||||
|
<style>
|
||||||
.file-title {
|
.file-title {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: .6em;
|
top: .6em;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col s12">
|
<div class="col s12">
|
||||||
<ul class="collection with-header">
|
<ul class="collection with-header">
|
||||||
<li class="collection-header theme-primary theme-on-primary-text">Backgrounds</li>
|
<li class="collection-header theme-primary theme-on-primary-text">Backgrounds</li>
|
||||||
@ -31,9 +56,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col s12">
|
<div class="col s12">
|
||||||
<ul class="collection with-header">
|
<ul class="collection with-header">
|
||||||
<li class="collection-header theme-primary theme-on-primary-text">Icons</li>
|
<li class="collection-header theme-primary theme-on-primary-text">Icons</li>
|
||||||
@ -58,9 +83,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$( document ).ready(function() {
|
$( document ).ready(function() {
|
||||||
init_copy_btn(".collection-item");
|
init_copy_btn(".collection-item");
|
||||||
$(".delete-file-btn").on('click', function(e) {
|
$(".delete-file-btn").on('click', function(e) {
|
||||||
@ -75,4 +100,7 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{{ Files(icons, backgrounds) }}
|
@ -2,6 +2,7 @@
|
|||||||
{% from 'global_macros.html' import input, button, select %}
|
{% from 'global_macros.html' import input, button, select %}
|
||||||
{% from 'main/tcdrop.html' import tcdrop %}
|
{% from 'main/tcdrop.html' import tcdrop %}
|
||||||
{% from 'settings_system/user.html' import UserTab with context %}
|
{% from 'settings_system/user.html' import UserTab with context %}
|
||||||
|
{% from 'settings_system/files.html' import FilesTab with context %}
|
||||||
{% block page_vendor_css %}
|
{% block page_vendor_css %}
|
||||||
{% endblock page_vendor_css %}
|
{% endblock page_vendor_css %}
|
||||||
|
|
||||||
@ -80,28 +81,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="images" class="col s12">
|
<div id="images" class="col s12">
|
||||||
<div class="row">
|
{{ FilesTab() }}
|
||||||
<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>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="apps" class="col s12">
|
<div id="apps" class="col s12">
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</h5>
|
</h5>
|
||||||
|
|
||||||
<div class="users-div">
|
<div id="users-div">
|
||||||
{{ Users(users) }}
|
{{ Users(users) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -88,11 +88,43 @@
|
|||||||
data-role="{{ user.role }}"
|
data-role="{{ user.role }}"
|
||||||
data-id="{{ user.id }}"
|
data-id="{{ user.id }}"
|
||||||
data-username="{{ user.username }}">edit</i>
|
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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% 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 %}
|
{% endmacro %}
|
||||||
|
|
||||||
{{Users(users)}}
|
{{Users(users)}}
|
@ -10,6 +10,10 @@ def add_edit_user(username, password, user_id=None, role=None):
|
|||||||
else:
|
else:
|
||||||
user = User()
|
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")
|
hashed_password = bcrypt.generate_password_hash(password).decode("utf-8")
|
||||||
user.username = username
|
user.username = username
|
||||||
user.password = hashed_password
|
user.password = hashed_password
|
||||||
|
@ -1 +1 @@
|
|||||||
version = "v0.22"
|
version = "v0.3"
|
||||||
|
@ -2,3 +2,6 @@
|
|||||||
theme = light
|
theme = light
|
||||||
accent = orange
|
accent = orange
|
||||||
background = None
|
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