0.3 ready for docker testing

This commit is contained in:
Ross Mountjoy 2020-02-08 17:05:48 -05:00
parent 91c5350330
commit 838831b857
23 changed files with 325 additions and 144 deletions

View File

@ -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
* Requests (python)

View File

@ -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

View File

@ -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")

View File

@ -0,0 +1,6 @@
from flask_wtf import FlaskForm
from wtforms import SelectField
class TagsForm(FlaskForm):
tags = SelectField(choices=[("All tags", "All tags")])

View File

@ -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())

View File

@ -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()

View File

@ -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

View File

@ -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})

View File

@ -28,6 +28,9 @@
.no-vis {
visibility: hidden;
}
.filtered {
display: none !important;
}
.scrollbar {
overflow-y: scroll !important;

View 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;
}
}

View File

@ -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');
}
});
});
});

View File

@ -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'});
}
}
});
});
});

View 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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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>

View File

@ -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) }}

View File

@ -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">

View File

@ -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)}}

View File

@ -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

View File

@ -1 +1 @@
version = "v0.22"
version = "v0.3"

View File

@ -1,4 +1,7 @@
[Settings]
theme = light
accent = orange
background = None
background = None
roles = admin,user,public_user
home_access_groups = admin_only
settings_access_groups = admin_only

View 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 ###