diff --git a/README.md b/README.md index 5b04ad2..d283972 100644 --- a/README.md +++ b/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 @@ -58,15 +60,4 @@ If you change the config.ini file, you either have to restart the container (or * Jinja2 * 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 \ No newline at end of file +* Requests (python) \ No newline at end of file diff --git a/config_readme.md b/config_readme.md index 817436e..8c30c7c 100644 --- a/config_readme.md +++ b/config_readme.md @@ -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 diff --git a/dashmachine/error_pages/routes.py b/dashmachine/error_pages/routes.py index 10e4eb9..be488bd 100755 --- a/dashmachine/error_pages/routes.py +++ b/dashmachine/error_pages/routes.py @@ -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") diff --git a/dashmachine/main/forms.py b/dashmachine/main/forms.py index e69de29..21bc2be 100755 --- a/dashmachine/main/forms.py +++ b/dashmachine/main/forms.py @@ -0,0 +1,6 @@ +from flask_wtf import FlaskForm +from wtforms import SelectField + + +class TagsForm(FlaskForm): + tags = SelectField(choices=[("All tags", "All tags")]) diff --git a/dashmachine/main/models.py b/dashmachine/main/models.py index 7ff36a3..9637d1e 100644 --- a/dashmachine/main/models.py +++ b/dashmachine/main/models.py @@ -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()) diff --git a/dashmachine/main/read_config.py b/dashmachine/main/read_config.py index 0549d86..040e145 100644 --- a/dashmachine/main/read_config.py +++ b/dashmachine/main/read_config.py @@ -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() diff --git a/dashmachine/main/routes.py b/dashmachine/main/routes.py index 811959a..fbb4e0f 100755 --- a/dashmachine/main/routes.py +++ b/dashmachine/main/routes.py @@ -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 diff --git a/dashmachine/settings_system/routes.py b/dashmachine/settings_system/routes.py index aa5b0ef..277f25d 100644 --- a/dashmachine/settings_system/routes.py +++ b/dashmachine/settings_system/routes.py @@ -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}) diff --git a/dashmachine/static/css/global/dashmachine.css b/dashmachine/static/css/global/dashmachine.css index e8c14b2..710877e 100644 --- a/dashmachine/static/css/global/dashmachine.css +++ b/dashmachine/static/css/global/dashmachine.css @@ -28,6 +28,9 @@ .no-vis { visibility: hidden; } +.filtered { + display: none !important; +} .scrollbar { overflow-y: scroll !important; diff --git a/dashmachine/static/css/main/home.css b/dashmachine/static/css/main/home.css new file mode 100644 index 0000000..65d2e61 --- /dev/null +++ b/dashmachine/static/css/main/home.css @@ -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; + } +} \ No newline at end of file diff --git a/dashmachine/static/js/main/home.js b/dashmachine/static/js/main/home.js index 39dc0b9..664916b 100644 --- a/dashmachine/static/js/main/home.js +++ b/dashmachine/static/js/main/home.js @@ -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'); + } + }); + }); }); \ No newline at end of file diff --git a/dashmachine/static/js/settings_system/settings.js b/dashmachine/static/js/settings_system/settings.js index 1204493..99950f8 100644 --- a/dashmachine/static/js/settings_system/settings.js +++ b/dashmachine/static/js/settings_system/settings.js @@ -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'}); + } + } + }); }); }); \ No newline at end of file diff --git a/dashmachine/templates/error_pages/unauthorized.html b/dashmachine/templates/error_pages/unauthorized.html new file mode 100644 index 0000000..d45df2e --- /dev/null +++ b/dashmachine/templates/error_pages/unauthorized.html @@ -0,0 +1,11 @@ +{% extends "main/base.html" %} + + +{% block content %} +
+
+ warning +

Unauthorized

+
+
+{% endblock content %} diff --git a/dashmachine/templates/global_macros.html b/dashmachine/templates/global_macros.html index b5b3d5b..e063677 100644 --- a/dashmachine/templates/global_macros.html +++ b/dashmachine/templates/global_macros.html @@ -169,10 +169,13 @@ col_style=None id='', form_obj=None, size="s12", -label='' +label=None, +class='' ) %}
- {{ form_obj(id=id) }} - + {{ form_obj(id=id, class=class, placeholder="Tags") }} + {% if label %} + + {% endif %}
{% endmacro %} \ No newline at end of file diff --git a/dashmachine/templates/main/home.html b/dashmachine/templates/main/home.html index ed3ab05..ce62a6d 100755 --- a/dashmachine/templates/main/home.html +++ b/dashmachine/templates/main/home.html @@ -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' %} +{% macro FilesTab() %} +
+
Images
+
+
+ + +
+ +
+
+ {{ 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')}) }} +
+
-
-
-
- +
- -
-
-
- +
- - \ No newline at end of file + +{% endmacro %} + +{{ Files(icons, backgrounds) }} \ No newline at end of file diff --git a/dashmachine/templates/settings_system/settings.html b/dashmachine/templates/settings_system/settings.html index 29b253a..2990c23 100644 --- a/dashmachine/templates/settings_system/settings.html +++ b/dashmachine/templates/settings_system/settings.html @@ -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 @@
-
-
Images
-
-
- - -
- -
-
- {{ 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')}) }} -
-
- -
-
{{ files_html|safe }}
-
- + {{ FilesTab() }}
diff --git a/dashmachine/templates/settings_system/user.html b/dashmachine/templates/settings_system/user.html index f4097a3..99992e2 100644 --- a/dashmachine/templates/settings_system/user.html +++ b/dashmachine/templates/settings_system/user.html @@ -61,7 +61,7 @@ -
+
{{ Users(users) }}
@@ -88,11 +88,43 @@ data-role="{{ user.role }}" data-id="{{ user.id }}" data-username="{{ user.username }}">edit - close + close
{% endfor %} + {% endmacro %} {{Users(users)}} \ No newline at end of file diff --git a/dashmachine/user_system/utils.py b/dashmachine/user_system/utils.py index b641955..4855319 100755 --- a/dashmachine/user_system/utils.py +++ b/dashmachine/user_system/utils.py @@ -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 diff --git a/dashmachine/version.py b/dashmachine/version.py index 35520dc..fc8a2fa 100755 --- a/dashmachine/version.py +++ b/dashmachine/version.py @@ -1 +1 @@ -version = "v0.22" +version = "v0.3" diff --git a/default_config.ini b/default_config.ini index c768f39..6654307 100644 --- a/default_config.ini +++ b/default_config.ini @@ -1,4 +1,7 @@ [Settings] theme = light accent = orange -background = None \ No newline at end of file +background = None +roles = admin,user,public_user +home_access_groups = admin_only +settings_access_groups = admin_only \ No newline at end of file diff --git a/migrations/versions/885c5f9b33d5_.py b/migrations/versions/885c5f9b33d5_.py new file mode 100644 index 0000000..a60db60 --- /dev/null +++ b/migrations/versions/885c5f9b33d5_.py @@ -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 ###