Merge branch 'develop'
This commit is contained in:
commit
f3cf929cc5
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
|
85
config_readme.md
Normal file
85
config_readme.md
Normal file
@ -0,0 +1,85 @@
|
||||
#### Config.ini Readme
|
||||
|
||||
##### Settings
|
||||
This is the configuration entry for DashMachine's settings. DashMachine will not work if
|
||||
this is missing. As for all config entries, [Settings] can only appear once in the config.
|
||||
If you change the config.ini file, you either have to restart the container
|
||||
(or python script) or click the ‘save’ button in the config section of settings for the
|
||||
config to be applied.
|
||||
```ini
|
||||
[Settings]
|
||||
theme = light
|
||||
accent = orange
|
||||
background = None
|
||||
roles = admin,user,public_user
|
||||
home_access_groups = admin_only
|
||||
settings_access_groups = admin_only
|
||||
```
|
||||
|
||||
| Variable | Required | Description | Options |
|
||||
|------------------------|----------|----------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [Settings] | Yes | Config section name. | [Settings] |
|
||||
| theme | Yes | UI theme. | light, dark |
|
||||
| accent | Yes | UI accent color | orange, red, pink, purple, deepPurple, indigo, blue, lightBlue,cyan, teal, green, lightGreen, lime, yellow, amber, deepOrange, brown, grey, blueGrey |
|
||||
| background | Yes | Background image for the UI | /static/images/backgrounds/yourpicture.png, external link to image, None, random |
|
||||
| roles | No | User roles for access groups. | comma separated string, if not defined, this is set to 'admin,user,public_user'. Note: admin, user, public_user roles are required and will be added automatically if omitted. |
|
||||
| home_access_groups | No | Define which access groups can access the /home page | Groups defined in your config. If not defined, default is admin_only |
|
||||
| settings_access_groups | No | Define which access groups can access the /settings page | Groups defined in your config. If not defined, default is admin_only |
|
||||
|
||||
##### Apps
|
||||
These entries are the cards that you see one the home page, as well as the sidenav. Entries
|
||||
must be unique. They are displayed in the order that they appear in config.ini
|
||||
```ini
|
||||
[App Name]
|
||||
prefix = https://
|
||||
url = your-website.com
|
||||
icon = static/images/apps/default.png
|
||||
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 |
|
||||
|--------------|----------|-------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------|
|
||||
| [App Name] | Yes | The name of your app. | [App Name] |
|
||||
| prefix | Yes | The prefix for the app's url. | web prefix, e.g. http:// or https:// |
|
||||
| url | Yes | The url for your app. | web url, e.g. myapp.com |
|
||||
| open_in | Yes | open the app in the current tab, an iframe or a new tab | iframe, new_tab, this_tab |
|
||||
| icon | No | Icon for the dashboard. | /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 |
|
||||
| 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
|
||||
application can have an access group, if the user's role is not in the group, the app will be hidden.
|
||||
Also, in the settings entry you can specify `home_access_groups` and `settings_access_groups` to control
|
||||
which groups can access /home and /settings
|
||||
```ini
|
||||
[public]
|
||||
roles = admin, user, public_user
|
||||
```
|
||||
|
||||
> **Note:** if no access groups are defined in the config, the application will create a default group called 'admin_only' with 'roles = admin'
|
||||
|
||||
| Variable | Required | Description | Options |
|
||||
|--------------|----------|--------------------------------------------------------------------------------|----------------------------------------------------------------------------------|
|
||||
| [Group Name] | Yes | Name for access group. | [Group Name] |
|
||||
| roles | Yes | A comma separated list of user roles allowed to view apps in this access group | Roles defined in your config. If not defined, defaults are admin and public_user |
|
||||
|
||||
#### Data Source Platforms
|
||||
DashMachine includes several different 'platforms' for displaying data on your dash applications.
|
||||
Platforms are essentially plugins. All data source config entries require the `plaform` variable,
|
||||
which tells DashMachine which platform file in the platform folder to load. **Note:** you are able to
|
||||
load your own plaform files by placing them in the platform folder and referencing them in the config.
|
||||
However currently they will be deleted if you update the application, if you would like to make them
|
||||
permanent, submit a pull request for it to be added by default!
|
||||
|
||||
> To add a data source to your app, add a data source config entry from one of the samples below
|
||||
**above** the application entry in config.ini, then add the following to your app config entry:
|
||||
`data_source = variable_name`
|
@ -6,9 +6,10 @@ from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_bcrypt import Bcrypt
|
||||
from flask_login import LoginManager
|
||||
from flask_restful import Api
|
||||
from dashmachine.paths import user_data_folder
|
||||
|
||||
if not os.path.isdir("dashmachine/user_data"):
|
||||
os.mkdir("dashmachine/user_data")
|
||||
if not os.path.isdir(user_data_folder):
|
||||
os.mkdir(user_data_folder)
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
|
@ -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")])
|
@ -1,5 +1,11 @@
|
||||
from dashmachine import db
|
||||
|
||||
rel_app_data_source = db.Table(
|
||||
"rel_app_data_source",
|
||||
db.Column("data_source_id", db.Integer, db.ForeignKey("data_sources.id")),
|
||||
db.Column("app_id", db.Integer, db.ForeignKey("apps.id")),
|
||||
)
|
||||
|
||||
|
||||
class Files(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@ -20,6 +26,8 @@ class Apps(db.Model):
|
||||
description = db.Column(db.String())
|
||||
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):
|
||||
@ -33,17 +41,31 @@ class TemplateApps(db.Model):
|
||||
open_in = db.Column(db.String())
|
||||
|
||||
|
||||
class ApiCalls(db.Model):
|
||||
class DataSources(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String())
|
||||
resource = db.Column(db.String())
|
||||
method = db.Column(db.String())
|
||||
payload = db.Column(db.String())
|
||||
authentication = db.Column(db.String())
|
||||
username = db.Column(db.String())
|
||||
password = db.Column(db.String())
|
||||
value_template = db.Column(db.String())
|
||||
platform = db.Column(db.String())
|
||||
args = db.relationship("DataSourcesArgs", backref="data_source")
|
||||
apps = db.relationship(
|
||||
"Apps",
|
||||
secondary=rel_app_data_source,
|
||||
backref=db.backref("data_sources", lazy="dynamic"),
|
||||
)
|
||||
|
||||
|
||||
db.create_all()
|
||||
db.session.commit()
|
||||
class DataSourcesArgs(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
key = db.Column(db.String())
|
||||
value = db.Column(db.String())
|
||||
data_source_id = db.Column(db.Integer, db.ForeignKey("data_sources.id"))
|
||||
|
||||
|
||||
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())
|
||||
|
176
dashmachine/main/read_config.py
Normal file
176
dashmachine/main/read_config.py
Normal file
@ -0,0 +1,176 @@
|
||||
import os
|
||||
from configparser import ConfigParser
|
||||
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
|
||||
|
||||
|
||||
def row2dict(row):
|
||||
d = {}
|
||||
for column in row.__table__.columns:
|
||||
d[column.name] = str(getattr(row, column.name))
|
||||
|
||||
return d
|
||||
|
||||
|
||||
def read_config():
|
||||
config = ConfigParser()
|
||||
try:
|
||||
config.read(os.path.join(user_data_folder, "config.ini"))
|
||||
except Exception as e:
|
||||
return {"msg": f"Invalid Config: {e}."}
|
||||
|
||||
ds_list = DataSources.query.all()
|
||||
for ds in ds_list:
|
||||
ds.apps.clear()
|
||||
DataSources.query.delete()
|
||||
DataSourcesArgs.query.delete()
|
||||
Apps.query.delete()
|
||||
Settings.query.delete()
|
||||
Groups.query.delete()
|
||||
Tags.query.delete()
|
||||
|
||||
for section in config.sections():
|
||||
|
||||
# Settings creation
|
||||
if section == "Settings":
|
||||
settings = Settings()
|
||||
if "theme" in config["Settings"]:
|
||||
settings.theme = config["Settings"]["theme"]
|
||||
else:
|
||||
settings.theme = "light"
|
||||
|
||||
if "accent" in config["Settings"]:
|
||||
settings.accent = config["Settings"]["accent"]
|
||||
else:
|
||||
settings.accent = "orange"
|
||||
|
||||
if "background" in config["Settings"]:
|
||||
settings.background = config["Settings"]["background"]
|
||||
else:
|
||||
settings.background = "None"
|
||||
|
||||
if "roles" in config["Settings"]:
|
||||
settings.roles = config["Settings"]["roles"]
|
||||
if "admin" not in settings.roles:
|
||||
settings.roles += ",admin"
|
||||
if "user" not in settings.roles:
|
||||
settings.roles += ",user"
|
||||
if "public_user" not in settings.roles:
|
||||
settings.roles += ",public_user"
|
||||
else:
|
||||
settings.roles = "admin,user,public_user"
|
||||
|
||||
if "home_access_groups" in config["Settings"]:
|
||||
settings.home_access_groups = config["Settings"]["home_access_groups"]
|
||||
else:
|
||||
settings.home_access_groups = "admin_only"
|
||||
|
||||
if "settings_access_groups" in config["Settings"]:
|
||||
settings.settings_access_groups = config["Settings"][
|
||||
"settings_access_groups"
|
||||
]
|
||||
else:
|
||||
settings.settings_access_groups = "admin_only"
|
||||
|
||||
db.session.add(settings)
|
||||
db.session.commit()
|
||||
|
||||
# Groups creation
|
||||
elif "roles" in config[section]:
|
||||
group = Groups()
|
||||
group.name = section
|
||||
group.roles = config[section]["roles"]
|
||||
db.session.add(group)
|
||||
db.session.commit()
|
||||
|
||||
# Data source creation
|
||||
elif "platform" in config[section]:
|
||||
data_source = DataSources()
|
||||
data_source.name = section
|
||||
data_source.platform = config[section]["platform"]
|
||||
db.session.add(data_source)
|
||||
db.session.commit()
|
||||
for key, value in config[section].items():
|
||||
if key not in ["name", "platform"]:
|
||||
arg = DataSourcesArgs()
|
||||
arg.key = key
|
||||
arg.value = value
|
||||
arg.data_source = data_source
|
||||
db.session.add(arg)
|
||||
db.session.commit()
|
||||
|
||||
else:
|
||||
# App creation
|
||||
app = Apps()
|
||||
app.name = section
|
||||
if "prefix" in config[section]:
|
||||
app.prefix = config[section]["prefix"]
|
||||
else:
|
||||
return {"msg": f"Invalid Config: {section} does not contain prefix."}
|
||||
|
||||
if "url" in config[section]:
|
||||
app.url = config[section]["url"]
|
||||
else:
|
||||
return {"msg": f"Invalid Config: {section} does not contain url."}
|
||||
|
||||
if "icon" in config[section]:
|
||||
app.icon = config[section]["icon"]
|
||||
else:
|
||||
app.icon = None
|
||||
|
||||
if "sidebar_icon" in config[section]:
|
||||
app.sidebar_icon = config[section]["sidebar_icon"]
|
||||
else:
|
||||
app.sidebar_icon = app.icon
|
||||
|
||||
if "description" in config[section]:
|
||||
app.description = config[section]["description"]
|
||||
else:
|
||||
app.description = None
|
||||
|
||||
if "open_in" in config[section]:
|
||||
app.open_in = config[section]["open_in"]
|
||||
else:
|
||||
app.open_in = "this_tab"
|
||||
|
||||
if "groups" in config[section]:
|
||||
app.groups = config[section]["groups"]
|
||||
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()
|
||||
|
||||
if "data_sources" in config[section]:
|
||||
for config_ds in config[section]["data_sources"].split(","):
|
||||
db_ds = DataSources.query.filter_by(name=config_ds.strip()).first()
|
||||
if db_ds:
|
||||
app.data_sources.append(db_ds)
|
||||
db.session.merge(app)
|
||||
db.session.commit()
|
||||
else:
|
||||
return {
|
||||
"msg": f"Invalid Config: {section} has a data_source variable that doesn't exist."
|
||||
}
|
||||
|
||||
group = Groups.query.filter_by(name="admin_only").first()
|
||||
if not group:
|
||||
group = Groups()
|
||||
group.name = "admin_only"
|
||||
group.roles = "admin"
|
||||
db.session.add(group)
|
||||
db.session.commit()
|
||||
return {"msg": "success", "settings": row2dict(settings)}
|
@ -4,8 +4,14 @@ 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
|
||||
from dashmachine.main.utils import get_rest_data
|
||||
from dashmachine.main.models import Files, Apps, DataSources, Tags
|
||||
from dashmachine.main.forms import TagsForm
|
||||
from dashmachine.main.utils import (
|
||||
public_route,
|
||||
check_groups,
|
||||
get_data_source,
|
||||
)
|
||||
from dashmachine.settings_system.models import Settings
|
||||
from dashmachine.paths import cache_folder
|
||||
from dashmachine import app, db
|
||||
|
||||
@ -49,21 +55,35 @@ def check_valid_login():
|
||||
# ------------------------------------------------------------------------------
|
||||
# /home
|
||||
# ------------------------------------------------------------------------------
|
||||
@public_route
|
||||
@main.route("/")
|
||||
@main.route("/home", methods=["GET", "POST"])
|
||||
def home():
|
||||
return render_template("main/home.html")
|
||||
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("error_pages.unauthorized"))
|
||||
return render_template("main/home.html", tags_form=tags_form)
|
||||
|
||||
|
||||
@main.route("/app_view?<url>", methods=["GET"])
|
||||
def app_view(url):
|
||||
return render_template("main/app-view.html", url=f"https://{url}")
|
||||
@public_route
|
||||
@main.route("/app_view?<app_id>", methods=["GET"])
|
||||
def app_view(app_id):
|
||||
settings = Settings.query.first()
|
||||
if not check_groups(settings.home_access_groups, current_user):
|
||||
return redirect(url_for("user_system.login"))
|
||||
app_db = Apps.query.filter_by(id=app_id).first()
|
||||
return render_template("main/app-view.html", url=f"{app_db.prefix}{app_db.url}")
|
||||
|
||||
|
||||
@main.route("/load_rest_data", methods=["GET"])
|
||||
def load_rest_data():
|
||||
data_template = get_rest_data(request.args.get("template"))
|
||||
return data_template
|
||||
@main.route("/load_data_source", methods=["GET"])
|
||||
def load_data_source():
|
||||
data_source = DataSources.query.filter_by(id=request.args.get("id")).first()
|
||||
data = get_data_source(data_source)
|
||||
return data
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
|
@ -1,10 +1,12 @@
|
||||
import os
|
||||
import subprocess
|
||||
import importlib
|
||||
from shutil import copyfile
|
||||
from requests import get
|
||||
from configparser import ConfigParser
|
||||
from dashmachine.paths import dashmachine_folder, images_folder, root_folder
|
||||
from dashmachine.main.models import Apps, ApiCalls, TemplateApps
|
||||
from dashmachine.main.models import TemplateApps, Groups
|
||||
from dashmachine.main.read_config import read_config
|
||||
from dashmachine.settings_system.models import Settings
|
||||
from dashmachine.user_system.models import User
|
||||
from dashmachine.user_system.utils import add_edit_user
|
||||
@ -19,119 +21,6 @@ def row2dict(row):
|
||||
return d
|
||||
|
||||
|
||||
def read_config():
|
||||
config = ConfigParser()
|
||||
try:
|
||||
config.read("dashmachine/user_data/config.ini")
|
||||
except Exception as e:
|
||||
return {"msg": f"Invalid Config: {e}."}
|
||||
|
||||
Apps.query.delete()
|
||||
ApiCalls.query.delete()
|
||||
Settings.query.delete()
|
||||
|
||||
try:
|
||||
settings = Settings(
|
||||
theme=config["Settings"]["theme"],
|
||||
accent=config["Settings"]["accent"],
|
||||
background=config["Settings"]["background"],
|
||||
)
|
||||
db.session.add(settings)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
return {"msg": f"Invalid Config: {e}."}
|
||||
|
||||
for section in config.sections():
|
||||
if section != "Settings":
|
||||
|
||||
# API call creation
|
||||
if "platform" in config[section]:
|
||||
api_call = ApiCalls()
|
||||
api_call.name = section
|
||||
if "resource" in config[section]:
|
||||
api_call.resource = config[section]["resource"]
|
||||
else:
|
||||
return {
|
||||
"msg": f"Invalid Config: {section} does not contain resource."
|
||||
}
|
||||
|
||||
if "method" in config[section]:
|
||||
api_call.method = config[section]["method"]
|
||||
else:
|
||||
api_call.method = "GET"
|
||||
|
||||
if "payload" in config[section]:
|
||||
api_call.payload = config[section]["payload"]
|
||||
else:
|
||||
api_call.payload = None
|
||||
|
||||
if "authentication" in config[section]:
|
||||
api_call.authentication = config[section]["authentication"]
|
||||
else:
|
||||
api_call.authentication = None
|
||||
|
||||
if "username" in config[section]:
|
||||
api_call.username = config[section]["username"]
|
||||
else:
|
||||
api_call.username = None
|
||||
|
||||
if "password" in config[section]:
|
||||
api_call.password = config[section]["password"]
|
||||
else:
|
||||
api_call.password = None
|
||||
|
||||
if "value_template" in config[section]:
|
||||
api_call.value_template = config[section]["value_template"]
|
||||
else:
|
||||
api_call.value_template = section
|
||||
|
||||
db.session.add(api_call)
|
||||
db.session.commit()
|
||||
continue
|
||||
|
||||
# App creation
|
||||
app = Apps()
|
||||
app.name = section
|
||||
if "prefix" in config[section]:
|
||||
app.prefix = config[section]["prefix"]
|
||||
else:
|
||||
return {"msg": f"Invalid Config: {section} does not contain prefix."}
|
||||
|
||||
if "url" in config[section]:
|
||||
app.url = config[section]["url"]
|
||||
else:
|
||||
return {"msg": f"Invalid Config: {section} does not contain url."}
|
||||
|
||||
if "icon" in config[section]:
|
||||
app.icon = config[section]["icon"]
|
||||
else:
|
||||
app.icon = None
|
||||
|
||||
if "sidebar_icon" in config[section]:
|
||||
app.sidebar_icon = config[section]["sidebar_icon"]
|
||||
else:
|
||||
app.sidebar_icon = app.icon
|
||||
|
||||
if "description" in config[section]:
|
||||
app.description = config[section]["description"]
|
||||
else:
|
||||
app.description = None
|
||||
|
||||
if "open_in" in config[section]:
|
||||
app.open_in = config[section]["open_in"]
|
||||
else:
|
||||
app.open_in = "this_tab"
|
||||
|
||||
if "data_template" in config[section]:
|
||||
app.data_template = config[section]["data_template"]
|
||||
else:
|
||||
app.data_template = None
|
||||
|
||||
db.session.add(app)
|
||||
db.session.commit()
|
||||
return {"msg": "success", "settings": row2dict(settings)}
|
||||
|
||||
|
||||
def read_template_apps():
|
||||
config = ConfigParser()
|
||||
try:
|
||||
@ -165,13 +54,17 @@ def public_route(decorated_function):
|
||||
|
||||
|
||||
def dashmachine_init():
|
||||
db.create_all()
|
||||
db.session.commit()
|
||||
migrate_cmd = "python " + os.path.join(root_folder, "manage_db.py db stamp head")
|
||||
subprocess.run(migrate_cmd, stderr=subprocess.PIPE, shell=True, encoding="utf-8")
|
||||
|
||||
migrate_cmd = "python " + os.path.join(root_folder, "manage_db.py db migrate")
|
||||
subprocess.run(migrate_cmd, stderr=subprocess.PIPE, shell=True, encoding="utf-8")
|
||||
|
||||
upgrade_cmd = "python " + os.path.join(root_folder, "manage_db.py db upgrade")
|
||||
subprocess.run(upgrade_cmd, stderr=subprocess.PIPE, shell=True, encoding="utf-8")
|
||||
|
||||
read_config()
|
||||
read_template_apps()
|
||||
user_data_folder = os.path.join(dashmachine_folder, "user_data")
|
||||
|
||||
@ -193,28 +86,54 @@ def dashmachine_init():
|
||||
config_file = os.path.join(user_data_folder, "config.ini")
|
||||
if not os.path.exists(config_file):
|
||||
copyfile("default_config.ini", config_file)
|
||||
|
||||
read_config()
|
||||
|
||||
user = User.query.first()
|
||||
if not user:
|
||||
add_edit_user(username="admin", password="adminadmin")
|
||||
settings = Settings.query.first()
|
||||
add_edit_user(
|
||||
username="admin",
|
||||
password="adminadmin",
|
||||
role=settings.roles.split(",")[0].strip(),
|
||||
)
|
||||
|
||||
users = User.query.all()
|
||||
for user in users:
|
||||
if not user.role:
|
||||
user.role = "admin"
|
||||
|
||||
|
||||
def get_rest_data(template):
|
||||
while template and template.find("{{") > -1:
|
||||
start_braces = template.find("{{") + 2
|
||||
end_braces = template.find("}}")
|
||||
key = template[start_braces:end_braces].strip()
|
||||
key_w_braces = template[start_braces - 2 : end_braces + 2]
|
||||
value = do_api_call(key)
|
||||
template = template.replace(key_w_braces, value)
|
||||
return template
|
||||
def check_groups(groups, current_user):
|
||||
if current_user.is_anonymous:
|
||||
current_user.role = "public_user"
|
||||
|
||||
if groups:
|
||||
groups_list = groups.split(",")
|
||||
roles_list = []
|
||||
for group in groups_list:
|
||||
group = Groups.query.filter_by(name=group.strip()).first()
|
||||
for group_role in group.roles.split(","):
|
||||
roles_list.append(group_role.strip())
|
||||
if current_user.role in roles_list:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
if current_user.role == "admin":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def do_api_call(key):
|
||||
api_call = ApiCalls.query.filter_by(name=key).first()
|
||||
if api_call.method.upper() == "GET":
|
||||
value = get(api_call.resource)
|
||||
exec(f"{key} = {value.json()}")
|
||||
value = str(eval(api_call.value_template))
|
||||
return value
|
||||
def get_data_source(data_source):
|
||||
data_source_args = {}
|
||||
for arg in data_source.args:
|
||||
arg = row2dict(arg)
|
||||
data_source_args[arg.get("key")] = arg.get("value")
|
||||
data_source = row2dict(data_source)
|
||||
module = importlib.import_module(
|
||||
f"dashmachine.platform.{data_source['platform']}", "."
|
||||
)
|
||||
platform = module.Platform(data_source, **data_source_args)
|
||||
return platform.process()
|
||||
|
@ -13,6 +13,10 @@ root_folder = get_root_folder()
|
||||
|
||||
dashmachine_folder = os.path.join(root_folder, "dashmachine")
|
||||
|
||||
platform_folder = os.path.join(dashmachine_folder, "platform")
|
||||
|
||||
user_data_folder = os.path.join(dashmachine_folder, "user_data")
|
||||
|
||||
static_folder = os.path.join(dashmachine_folder, "static")
|
||||
|
||||
images_folder = os.path.join(static_folder, "images")
|
||||
|
0
dashmachine/platform/__init__.py
Normal file
0
dashmachine/platform/__init__.py
Normal file
202
dashmachine/platform/pihole.py
Normal file
202
dashmachine/platform/pihole.py
Normal file
@ -0,0 +1,202 @@
|
||||
from flask import render_template_string
|
||||
|
||||
|
||||
import requests
|
||||
import time
|
||||
import hashlib
|
||||
|
||||
|
||||
def inApiLink(ip, endpoint):
|
||||
return "http://" + str(ip) + "/admin/scripts/pi-hole/php/" + str(endpoint) + ".php"
|
||||
|
||||
|
||||
class Auth(object):
|
||||
def __init__(self, password):
|
||||
# PiHole's web token is just a double sha256 hash of the utf8 encoded password
|
||||
self.token = hashlib.sha256(
|
||||
hashlib.sha256(str(password).encode()).hexdigest().encode()
|
||||
).hexdigest()
|
||||
self.auth_timestamp = time.time()
|
||||
|
||||
|
||||
class PiHole(object):
|
||||
# Takes in an ip address of a pihole server
|
||||
def __init__(self, ip_address):
|
||||
self.ip_address = ip_address
|
||||
self.auth_data = None
|
||||
self.refresh()
|
||||
self.pw = None
|
||||
|
||||
def refresh(self):
|
||||
rawdata = requests.get(
|
||||
"http://" + self.ip_address + "/admin/api.php?summary"
|
||||
).json()
|
||||
|
||||
if self.auth_data != None:
|
||||
topdevicedata = requests.get(
|
||||
"http://"
|
||||
+ self.ip_address
|
||||
+ "/admin/api.php?getQuerySources=25&auth="
|
||||
+ self.auth_data.token
|
||||
).json()
|
||||
|
||||
self.top_devices = topdevicedata["top_sources"]
|
||||
|
||||
self.forward_destinations = requests.get(
|
||||
"http://"
|
||||
+ self.ip_address
|
||||
+ "/admin/api.php?getForwardDestinations&auth="
|
||||
+ self.auth_data.token
|
||||
).json()
|
||||
|
||||
self.query_types = requests.get(
|
||||
"http://"
|
||||
+ self.ip_address
|
||||
+ "/admin/api.php?getQueryTypes&auth="
|
||||
+ self.auth_data.token
|
||||
).json()["querytypes"]
|
||||
|
||||
# Data that is returned is now parsed into vars
|
||||
self.status = rawdata["status"]
|
||||
self.domain_count = rawdata["domains_being_blocked"]
|
||||
self.queries = rawdata["dns_queries_today"]
|
||||
self.blocked = rawdata["ads_blocked_today"]
|
||||
self.ads_percentage = rawdata["ads_percentage_today"]
|
||||
self.unique_domains = rawdata["unique_domains"]
|
||||
self.forwarded = rawdata["queries_forwarded"]
|
||||
self.cached = rawdata["queries_cached"]
|
||||
self.total_clients = rawdata["clients_ever_seen"]
|
||||
self.unique_clients = rawdata["unique_clients"]
|
||||
self.total_queries = rawdata["dns_queries_all_types"]
|
||||
self.gravity_last_updated = rawdata["gravity_last_updated"]
|
||||
|
||||
def refreshTop(self, count):
|
||||
if self.auth_data == None:
|
||||
print("Unable to fetch top items. Please authenticate.")
|
||||
exit(1)
|
||||
|
||||
rawdata = requests.get(
|
||||
"http://"
|
||||
+ self.ip_address
|
||||
+ "/admin/api.php?topItems="
|
||||
+ str(count)
|
||||
+ "&auth="
|
||||
+ self.auth_data.token
|
||||
).json()
|
||||
self.top_queries = rawdata["top_queries"]
|
||||
self.top_ads = rawdata["top_ads"]
|
||||
|
||||
def getGraphData(self):
|
||||
rawdata = requests.get(
|
||||
"http://" + self.ip_address + "/admin/api.php?overTimeData10mins"
|
||||
).json()
|
||||
return {
|
||||
"domains": rawdata["domains_over_time"],
|
||||
"ads": rawdata["ads_over_time"],
|
||||
}
|
||||
|
||||
def authenticate(self, password):
|
||||
self.auth_data = Auth(password)
|
||||
self.pw = password
|
||||
|
||||
# print(self.auth_data.token)
|
||||
|
||||
def getAllQueries(self):
|
||||
if self.auth_data == None:
|
||||
print("Unable to get queries. Please authenticate")
|
||||
exit(1)
|
||||
return requests.get(
|
||||
"http://"
|
||||
+ self.ip_address
|
||||
+ "/admin/api.php?getAllQueries&auth="
|
||||
+ self.auth_data.token
|
||||
).json()["data"]
|
||||
|
||||
def enable(self):
|
||||
if self.auth_data == None:
|
||||
print("Unable to enable pihole. Please authenticate")
|
||||
exit(1)
|
||||
requests.get(
|
||||
"http://"
|
||||
+ self.ip_address
|
||||
+ "/admin/api.php?enable&auth="
|
||||
+ self.auth_data.token
|
||||
)
|
||||
|
||||
def disable(self, seconds):
|
||||
if self.auth_data == None:
|
||||
print("Unable to disable pihole. Please authenticate")
|
||||
exit(1)
|
||||
requests.get(
|
||||
"http://"
|
||||
+ self.ip_address
|
||||
+ "/admin/api.php?disable="
|
||||
+ str(seconds)
|
||||
+ "&auth="
|
||||
+ self.auth_data.token
|
||||
)
|
||||
|
||||
def getVersion(self):
|
||||
return requests.get(
|
||||
"http://" + self.ip_address + "/admin/api.php?versions"
|
||||
).json()
|
||||
|
||||
def getDBfilesize(self):
|
||||
if self.auth_data == None:
|
||||
print("Please authenticate")
|
||||
exit(1)
|
||||
return float(
|
||||
requests.get(
|
||||
"http://"
|
||||
+ self.ip_address
|
||||
+ "/admin/api_db.php?getDBfilesize&auth="
|
||||
+ self.auth_data.token
|
||||
).json()["filesize"]
|
||||
)
|
||||
|
||||
def getList(self, list):
|
||||
return requests.get(
|
||||
inApiLink(self.ip_address, "get") + "?list=" + str(list)
|
||||
).json()
|
||||
|
||||
def add(self, list, domain):
|
||||
if self.auth_data == None:
|
||||
print("Please authenticate")
|
||||
exit(1)
|
||||
with requests.session() as s:
|
||||
s.get(
|
||||
"http://" + str(self.ip_address) + "/admin/scripts/pi-hole/php/add.php"
|
||||
)
|
||||
requests.post(
|
||||
"http://" + str(self.ip_address) + "/admin/scripts/pi-hole/php/add.php",
|
||||
data={"list": list, "domain": domain, "pw": self.pw},
|
||||
).text
|
||||
|
||||
def sub(self, list, domain):
|
||||
if self.auth_data == None:
|
||||
print("Please authenticate")
|
||||
exit(1)
|
||||
with requests.session() as s:
|
||||
s.get(
|
||||
"http://" + str(self.ip_address) + "/admin/scripts/pi-hole/php/sub.php"
|
||||
)
|
||||
requests.post(
|
||||
"http://" + str(self.ip_address) + "/admin/scripts/pi-hole/php/sub.php",
|
||||
data={"list": list, "domain": domain, "pw": self.pw},
|
||||
).text
|
||||
|
||||
|
||||
class Platform:
|
||||
def __init__(self, *args, **kwargs):
|
||||
# parse the user's options from the config entries
|
||||
for key, value in kwargs.items():
|
||||
self.__dict__[key] = value
|
||||
|
||||
self.pihole = PiHole(self.host)
|
||||
|
||||
def process(self):
|
||||
self.pihole.refresh()
|
||||
value_template = render_template_string(
|
||||
self.value_template, **self.pihole.__dict__
|
||||
)
|
||||
return value_template
|
41
dashmachine/platform/ping.py
Normal file
41
dashmachine/platform/ping.py
Normal file
@ -0,0 +1,41 @@
|
||||
"""
|
||||
|
||||
##### ping
|
||||
Check if a service is online.
|
||||
```ini
|
||||
[variable_name]
|
||||
platform = ping
|
||||
resource = 192.168.1.1
|
||||
```
|
||||
> **Returns:** a right-aligned colored bullet point on the app card.
|
||||
|
||||
| Variable | Required | Description | Options |
|
||||
|-----------------|----------|-----------------------------------------------------------------|-------------------|
|
||||
| [variable_name] | Yes | Name for the data source. | [variable_name] |
|
||||
| plaform | Yes | Name of the platform. | rest |
|
||||
| resource | Yes | Url of whatever you want to ping | url |
|
||||
|
||||
|
||||
"""
|
||||
|
||||
import platform
|
||||
import subprocess
|
||||
|
||||
|
||||
class Platform:
|
||||
def __init__(self, *args, **kwargs):
|
||||
# parse the user's options from the config entries
|
||||
for key, value in kwargs.items():
|
||||
self.__dict__[key] = value
|
||||
|
||||
def process(self):
|
||||
param = "-n" if platform.system().lower() == "windows" else "-c"
|
||||
command = ["ping", param, "1", self.resource]
|
||||
up = subprocess.call(command) == 0
|
||||
|
||||
if up is True:
|
||||
icon_class = "theme-success-text"
|
||||
else:
|
||||
icon_class = "theme-failure-text"
|
||||
|
||||
return f"<i class='material-icons right {icon_class}'>fiber_manual_record </i>"
|
85
dashmachine/platform/rest.py
Normal file
85
dashmachine/platform/rest.py
Normal file
@ -0,0 +1,85 @@
|
||||
"""
|
||||
|
||||
##### rest
|
||||
Make a call on a REST API and display the results as a jinja formatted string.
|
||||
```ini
|
||||
[variable_name]
|
||||
platform = rest
|
||||
resource = https://your-website.com/api
|
||||
value_template = {{value}}
|
||||
method = post
|
||||
authentication = basic
|
||||
username = my_username
|
||||
password = my_password
|
||||
payload = {"var1": "hi", "var2": 1}
|
||||
```
|
||||
> **Returns:** `value_template` as rendered string
|
||||
|
||||
| Variable | Required | Description | Options |
|
||||
|-----------------|----------|-----------------------------------------------------------------|-------------------|
|
||||
| [variable_name] | Yes | Name for the data source. | [variable_name] |
|
||||
| plaform | Yes | Name of the platform. | rest |
|
||||
| resource | Yes | Url of rest api resource. | url |
|
||||
| value_template | Yes | Jinja template for how the returned data from api is displayed. | jinja template |
|
||||
| method | No | Method for the api call, default is GET | GET,POST |
|
||||
| authentication | No | Authentication for the api call, default is None | None,basic,digest |
|
||||
| username | No | Username to use for auth. | string |
|
||||
| password | No | Password to use for auth. | string |
|
||||
| payload | No | Payload for post request. | json |
|
||||
|
||||
> **Working example:**
|
||||
>```ini
|
||||
>[test]
|
||||
>platform = rest
|
||||
>resource = https://pokeapi.co/api/v2/pokemon
|
||||
>value_template = Pokemon: {{value['count']}}
|
||||
>
|
||||
>[Pokemon]
|
||||
>prefix = https://
|
||||
>url = pokemon.com
|
||||
>icon = static/images/apps/default.png
|
||||
>description = Data sources example
|
||||
>open_in = this_tab
|
||||
>data_sources = test
|
||||
>```
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
from requests import get, post
|
||||
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
|
||||
from flask import render_template_string
|
||||
|
||||
|
||||
class Platform:
|
||||
def __init__(self, *args, **kwargs):
|
||||
# parse the user's options from the config entries
|
||||
for key, value in kwargs.items():
|
||||
self.__dict__[key] = value
|
||||
|
||||
# set defaults for omitted options
|
||||
if not hasattr(self, "method"):
|
||||
self.method = "GET"
|
||||
if not hasattr(self, "authentication"):
|
||||
self.authentication = None
|
||||
|
||||
def process(self):
|
||||
if self.method.upper() == "GET":
|
||||
try:
|
||||
value = get(self.resource).json()
|
||||
except Exception as e:
|
||||
value = f"{e}"
|
||||
|
||||
elif self.method.upper() == "POST":
|
||||
if self.authentication:
|
||||
if self.authentication.lower() == "digest":
|
||||
auth = HTTPDigestAuth(self.username, self.password)
|
||||
else:
|
||||
auth = HTTPBasicAuth(self.username, self.password)
|
||||
else:
|
||||
auth = None
|
||||
|
||||
payload = json.loads(self.payload.replace("'", '"'))
|
||||
value = post(self.resource, data=payload, auth=auth)
|
||||
value_template = render_template_string(self.value_template, value=value)
|
||||
return value_template
|
38
dashmachine/platform/transmission.py
Normal file
38
dashmachine/platform/transmission.py
Normal file
@ -0,0 +1,38 @@
|
||||
import json
|
||||
from flask import render_template_string
|
||||
import transmissionrpc
|
||||
|
||||
|
||||
# from pprint import PrettyPrinter
|
||||
# pp = PrettyPrinter()
|
||||
|
||||
|
||||
class Platform:
|
||||
def __init__(self, *args, **kwargs):
|
||||
# parse the user's options from the config entries
|
||||
for key, value in kwargs.items():
|
||||
self.__dict__[key] = value
|
||||
|
||||
if not hasattr(self, "port"):
|
||||
self.port = 9091
|
||||
if not hasattr(self, "host"):
|
||||
self.host = "localhost"
|
||||
|
||||
self.tc = transmissionrpc.Client(
|
||||
self.host, port=self.port, user=self.user, password=self.password
|
||||
)
|
||||
|
||||
def process(self):
|
||||
|
||||
torrents = len(self.tc.get_torrents())
|
||||
data = {}
|
||||
for key, field in self.tc.session_stats().__dict__["_fields"].items():
|
||||
data[key] = field.value
|
||||
# pp.pprint (data)
|
||||
|
||||
value_template = render_template_string(self.value_template, **data)
|
||||
return value_template
|
||||
|
||||
|
||||
# Testing
|
||||
# test = Platform(host='192.168.1.19', user='', password='').process()
|
@ -1,5 +1,3 @@
|
||||
import os
|
||||
from flask import request
|
||||
from flask_restful import Resource
|
||||
from dashmachine.version import version
|
||||
|
||||
@ -7,15 +5,3 @@ from dashmachine.version import version
|
||||
class GetVersion(Resource):
|
||||
def get(self):
|
||||
return {"Version": version}
|
||||
|
||||
|
||||
class ServerShutdown(Resource):
|
||||
def get(self):
|
||||
os.system("shutdown now")
|
||||
return {"Done"}
|
||||
|
||||
|
||||
class ServerReboot(Resource):
|
||||
def get(self):
|
||||
os.system("reboot")
|
||||
return {"Done"}
|
||||
|
@ -6,7 +6,6 @@ class Settings(db.Model):
|
||||
theme = db.Column(db.String())
|
||||
accent = db.Column(db.String())
|
||||
background = db.Column(db.String())
|
||||
|
||||
|
||||
db.create_all()
|
||||
db.session.commit()
|
||||
roles = db.Column(db.String())
|
||||
home_access_groups = db.Column(db.String())
|
||||
settings_access_groups = db.Column(db.String())
|
||||
|
@ -1,29 +1,46 @@
|
||||
import os
|
||||
from shutil import move
|
||||
from flask import render_template, request, Blueprint, jsonify
|
||||
from dashmachine.settings_system.forms import ConfigForm
|
||||
from flask_login import current_user
|
||||
from flask import render_template, request, Blueprint, jsonify, redirect, url_for
|
||||
from dashmachine.user_system.forms import UserForm
|
||||
from dashmachine.user_system.utils import add_edit_user
|
||||
from dashmachine.main.utils import read_config, row2dict
|
||||
from dashmachine.user_system.models import User
|
||||
from dashmachine.main.utils import row2dict, public_route, check_groups
|
||||
from dashmachine.main.read_config import read_config
|
||||
from dashmachine.main.models import Files, TemplateApps
|
||||
from dashmachine.paths import backgrounds_images_folder, icons_images_folder
|
||||
from dashmachine.settings_system.forms import ConfigForm
|
||||
from dashmachine.settings_system.utils import load_files_html, get_config_html
|
||||
from dashmachine.settings_system.models import Settings
|
||||
from dashmachine.paths import (
|
||||
backgrounds_images_folder,
|
||||
icons_images_folder,
|
||||
user_data_folder,
|
||||
)
|
||||
from dashmachine.version import version
|
||||
from dashmachine.settings_system.utils import load_files_html
|
||||
from dashmachine import db
|
||||
|
||||
settings_system = Blueprint("settings_system", __name__)
|
||||
|
||||
|
||||
@public_route
|
||||
@settings_system.route("/settings", methods=["GET"])
|
||||
def settings():
|
||||
settings_db = Settings.query.first()
|
||||
if not check_groups(settings_db.settings_access_groups, current_user):
|
||||
return redirect(url_for("main.home"))
|
||||
|
||||
config_form = ConfigForm()
|
||||
user_form = UserForm()
|
||||
with open("dashmachine/user_data/config.ini", "r") as config_file:
|
||||
with open(os.path.join(user_data_folder, "config.ini"), "r") as config_file:
|
||||
config_form.config.data = config_file.read()
|
||||
files_html = load_files_html()
|
||||
template_apps = []
|
||||
t_apps = TemplateApps.query.all()
|
||||
for t_app in t_apps:
|
||||
template_apps.append(f"{t_app.name}&&{t_app.icon}")
|
||||
|
||||
users = User.query.all()
|
||||
config_readme = get_config_html()
|
||||
return render_template(
|
||||
"settings_system/settings.html",
|
||||
config_form=config_form,
|
||||
@ -31,12 +48,14 @@ def settings():
|
||||
user_form=user_form,
|
||||
template_apps=",".join(template_apps),
|
||||
version=version,
|
||||
users=users,
|
||||
config_readme=config_readme,
|
||||
)
|
||||
|
||||
|
||||
@settings_system.route("/settings/save_config", methods=["POST"])
|
||||
def save_config():
|
||||
with open("dashmachine/user_data/config.ini", "w") as config_file:
|
||||
with open(os.path.join(user_data_folder, "config.ini"), "w") as config_file:
|
||||
config_file.write(request.form.get("config"))
|
||||
msg = read_config()
|
||||
return jsonify(data=msg)
|
||||
@ -81,7 +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(form.username.data, form.password.data)
|
||||
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():
|
||||
@ -89,4 +115,20 @@ def edit_user():
|
||||
for err in errorMessages:
|
||||
err_str += f"{err} "
|
||||
return jsonify(data={"err": err_str})
|
||||
return jsonify(data={"err": "success"})
|
||||
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})
|
||||
|
@ -1,11 +1,43 @@
|
||||
from dashmachine.paths import backgrounds_images_folder, icons_images_folder
|
||||
import os
|
||||
import importlib
|
||||
from markdown2 import markdown
|
||||
from dashmachine.paths import (
|
||||
backgrounds_images_folder,
|
||||
icons_images_folder,
|
||||
root_folder,
|
||||
platform_folder,
|
||||
)
|
||||
from flask import render_template
|
||||
from os import listdir
|
||||
|
||||
|
||||
def load_files_html():
|
||||
backgrounds = listdir(backgrounds_images_folder)
|
||||
icons = listdir(icons_images_folder)
|
||||
backgrounds = os.listdir(backgrounds_images_folder)
|
||||
icons = os.listdir(icons_images_folder)
|
||||
return render_template(
|
||||
"settings_system/files.html", backgrounds=backgrounds, icons=icons,
|
||||
)
|
||||
|
||||
|
||||
def get_config_html():
|
||||
with open(os.path.join(root_folder, "config_readme.md")) as readme_file:
|
||||
md = readme_file.read()
|
||||
platforms = os.listdir(platform_folder)
|
||||
platforms = sorted(platforms)
|
||||
for platform in platforms:
|
||||
name, extension = os.path.splitext(platform)
|
||||
if extension.lower() == ".py":
|
||||
module = importlib.import_module(f"dashmachine.platform.{name}", ".")
|
||||
if module.__doc__:
|
||||
md += module.__doc__
|
||||
|
||||
config_html = markdown(
|
||||
md,
|
||||
extras=[
|
||||
"tables",
|
||||
"fenced-code-blocks",
|
||||
"break-on-newline",
|
||||
"header-ids",
|
||||
"code-friendly",
|
||||
],
|
||||
)
|
||||
return config_html
|
||||
|
@ -1,8 +1,10 @@
|
||||
import os
|
||||
import random
|
||||
from jsmin import jsmin
|
||||
from flask_login import current_user
|
||||
from dashmachine import app
|
||||
from dashmachine.main.models import Apps
|
||||
from dashmachine.main.utils import check_groups
|
||||
from dashmachine.settings_system.models import Settings
|
||||
from dashmachine.paths import static_folder, backgrounds_images_folder
|
||||
from dashmachine.cssmin import cssmin
|
||||
@ -72,9 +74,19 @@ def process_css_sources(process_bundle=None, src=None, app_global=False):
|
||||
|
||||
@app.context_processor
|
||||
def context_processor():
|
||||
apps = Apps.query.all()
|
||||
apps = []
|
||||
apps_db = Apps.query.all()
|
||||
for app_db in apps_db:
|
||||
if not app_db.groups:
|
||||
app_db.groups = None
|
||||
if check_groups(app_db.groups, current_user):
|
||||
apps.append(app_db)
|
||||
|
||||
settings = Settings.query.first()
|
||||
if settings.background == "random":
|
||||
if len(os.listdir(backgrounds_images_folder)) < 1:
|
||||
settings.background = None
|
||||
else:
|
||||
settings.background = (
|
||||
f"static/images/backgrounds/"
|
||||
f"{random.choice(os.listdir(backgrounds_images_folder))}"
|
||||
|
@ -11,7 +11,9 @@
|
||||
--theme-color-font: #2c2f3a;
|
||||
--theme-color-font-muted: rgba(44, 47, 58, 0.85);
|
||||
--theme-color-font-muted2: rgba(44, 47, 58, 0.65);
|
||||
--theme-warning: #f44336;
|
||||
--theme-failure: #f44336;
|
||||
--theme-warning: #ffae42;
|
||||
--theme-success: #4BB543;
|
||||
--theme-on-primary: #fff;
|
||||
}
|
||||
[data-theme="dark"] {
|
||||
@ -117,12 +119,24 @@
|
||||
.theme-text {
|
||||
color: var(--theme-color-font) !important;
|
||||
}
|
||||
.theme-failure {
|
||||
background-color: var(--theme-failure) !important;
|
||||
}
|
||||
.theme-failure-text {
|
||||
color: var(--theme-failure) !important;
|
||||
}
|
||||
.theme-warning {
|
||||
background-color: var(--theme-warning) !important;
|
||||
}
|
||||
.theme-warning-text {
|
||||
color: var(--theme-warning) !important;
|
||||
}
|
||||
.theme-success {
|
||||
background-color: var(--theme-success) !important;
|
||||
}
|
||||
.theme-success-text {
|
||||
color: var(--theme-success) !important;
|
||||
}
|
||||
.theme-muted-text {
|
||||
color: var(--theme-color-font-muted) !important;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -16,3 +16,39 @@
|
||||
border-top-right-radius: 0px;
|
||||
background: var(--theme-surface-1);
|
||||
}
|
||||
|
||||
#config-readme h5 {
|
||||
color: var(--theme-primary);
|
||||
margin-top: 5%;
|
||||
}
|
||||
#config-readme h4 {
|
||||
color: var(--theme-color-font-muted);
|
||||
margin-top: 5%;
|
||||
}
|
||||
#configini-readme {
|
||||
margin-top: 2% !important;
|
||||
}
|
||||
#config-readme code {
|
||||
-webkit-touch-callout: all;
|
||||
-webkit-user-select: all;
|
||||
-khtml-user-select: all;
|
||||
-moz-user-select: all;
|
||||
-ms-user-select: all;
|
||||
user-select: all;
|
||||
cursor: text;
|
||||
}
|
||||
#config-readme th {
|
||||
color: var(--theme-primary);
|
||||
}
|
||||
#config-readme td {
|
||||
-webkit-touch-callout: text !important;
|
||||
-webkit-user-select: text !important;
|
||||
-khtml-user-select: text !important;
|
||||
-moz-user-select: text !important;
|
||||
-ms-user-select: text !important;
|
||||
user-select: text !important;
|
||||
cursor: text;
|
||||
}
|
||||
#config-readme strong {
|
||||
font-weight: 900;
|
||||
}
|
@ -15,15 +15,26 @@ $( document ).ready(function() {
|
||||
});
|
||||
});
|
||||
|
||||
$(".data-template").each(function(e) {
|
||||
$(".data-source-container").each(function(e) {
|
||||
var el = $(this);
|
||||
$.ajax({
|
||||
url: el.attr('data-url'),
|
||||
type: 'GET',
|
||||
data: {template: el.text()},
|
||||
data: {id: el.attr('data-id')},
|
||||
success: function(data){
|
||||
el.text(data);
|
||||
el.removeClass('hide');
|
||||
el.closest('.col').find('.data-source-loading').addClass('hide');
|
||||
el.html(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -3,7 +3,11 @@ d.className += " active theme-primary";
|
||||
|
||||
$( document ).ready(function() {
|
||||
initTCdrop('#images-tcdrop');
|
||||
$("#config-wiki-modal").modal();
|
||||
$("#user-modal").modal({
|
||||
onCloseEnd: function () {
|
||||
$("#edit-user-form").trigger('reset');
|
||||
}
|
||||
});
|
||||
|
||||
$("#save-config-btn").on('click', function(e) {
|
||||
$.ajax({
|
||||
@ -15,7 +19,7 @@ $( document ).ready(function() {
|
||||
M.toast({html: 'Config applied successfully'});
|
||||
location.reload(true);
|
||||
} else {
|
||||
M.toast({html: data.data.msg, classes: "theme-warning"});
|
||||
M.toast({html: data.data.msg, classes: "theme-failure"});
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -58,18 +62,19 @@ $( document ).ready(function() {
|
||||
}
|
||||
});
|
||||
|
||||
$("#edit-user-btn").on('click', function(e) {
|
||||
$("#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-warning'});
|
||||
M.toast({html: data.data.err, classes: 'theme-failure'});
|
||||
} else {
|
||||
$("#user-form-password").val('');
|
||||
$("#user-form-confirm_password").val('');
|
||||
M.toast({html: 'User updated'});
|
||||
$("#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) }}
|
||||
{{ 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 %}
|
||||
{% 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,31 +28,40 @@
|
||||
<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', url=app.url) }}" 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">
|
||||
<div class="card-content center-align scrollbar" style="max-height: 118px; min-height: 118px; scrollbar-width: none;">
|
||||
{% if app.data_template %}
|
||||
{% if app.data_sources.count() > 0 %}
|
||||
<div class="row">
|
||||
<div class="col s6 center-align">
|
||||
<img src="{{ app.icon }}" height="64px">
|
||||
</div>
|
||||
|
||||
<div class="col s6 left-align">
|
||||
<p class="data-template hide theme-text" data-url="{{ url_for('main.load_rest_data') }}" style="white-space: pre-line">
|
||||
{{ app.data_template|safe }}
|
||||
<span class="data-source-loading">{{ preload_circle() }}</span>
|
||||
{% for data_source in app.data_sources %}
|
||||
<p class="data-source-container"
|
||||
data-url="{{ url_for('main.load_data_source') }}"
|
||||
data-id="{{ data_source.id }}">
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
@ -59,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>
|
||||
|
||||
{% 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>
|
||||
@ -37,7 +44,7 @@
|
||||
{% for app in apps %}
|
||||
<li class="bold">
|
||||
{% if app.open_in == 'iframe' %}
|
||||
<a id="dashboard-sidenav" class="waves-effect waves-cyan" href="{{ url_for('main.app_view', url=app.url) }}">
|
||||
<a id="dashboard-sidenav" class="waves-effect waves-cyan" href="{{ url_for('main.app_view', app_id=app.id) }}">
|
||||
{% elif app.open_in == "this_tab" %}
|
||||
<a id="dashboard-sidenav" class="waves-effect waves-cyan" href="{{ app.prefix }}{{ app.url }}">
|
||||
{% elif app.open_in == "new_tab" %}
|
||||
|
@ -16,8 +16,8 @@
|
||||
{{ preload_circle() }}
|
||||
</li>
|
||||
<li class="tcdrop-error-msg tcdrop-li hide">
|
||||
<i class="tcdrop-delete-file material-icons theme-warning">error</i>
|
||||
<span class="tcdrop-error-msg-txt file-name theme-warning-text"></span>
|
||||
<i class="tcdrop-delete-file material-icons theme-failure">error</i>
|
||||
<span class="tcdrop-error-msg-txt file-name theme-failure-text"></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -1,3 +1,28 @@
|
||||
{% 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 {
|
||||
position: relative;
|
||||
@ -76,3 +101,6 @@
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endmacro %}
|
||||
|
||||
{{ Files(icons, backgrounds) }}
|
@ -1,6 +1,8 @@
|
||||
{% extends "main/layout.html" %}
|
||||
{% from 'global_macros.html' import input, button %}
|
||||
{% 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 %}
|
||||
|
||||
@ -19,234 +21,6 @@
|
||||
{% endblock page_lvl_css %}
|
||||
|
||||
{% block content %}
|
||||
<div id="config-wiki-modal" class="modal full-height-modal">
|
||||
<div class="modal-content">
|
||||
<div class="row">
|
||||
<div class="col s12">
|
||||
<h4>Config.ini Readme
|
||||
<span><i class="material-icons-outlined modal-close right icon-btn">close</i></span>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="config-help-col" class="col s12 theme-surface-1 padding-2 border-radius-10">
|
||||
<h5>Settings Reference</h5>
|
||||
<code class="selectable-all">
|
||||
[Settings]<br>
|
||||
theme = dark<br>
|
||||
accent = orange<br>
|
||||
background = static/images/backgrounds/background.png<br>
|
||||
</code>
|
||||
|
||||
<table class="mt-4 responsive-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Required</th>
|
||||
<th>Description</th>
|
||||
<th>Options</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="selectable">
|
||||
<tr>
|
||||
<td>[Settings]</td>
|
||||
<td>Yes</td>
|
||||
<td>Config section name.</td>
|
||||
<td>string</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>theme</td>
|
||||
<td>Yes</td>
|
||||
<td>UI theme</td>
|
||||
<td>light, dark</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>accent</td>
|
||||
<td>Yes</td>
|
||||
<td>UI accent color</td>
|
||||
<td>
|
||||
orange, red, pink, purple, deepPurple, indigo, blue, lightBlue,
|
||||
cyan, teal, green, lightGreen, lime, yellow, amber, deepOrange, brown, grey, blueGrey
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>background</td>
|
||||
<td>Yes</td>
|
||||
<td>Background image for the UI</td>
|
||||
<td>/static/images/backgrounds/yourpicture.png, external link to image, None, random</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h5>App Reference</h5>
|
||||
<code class="selectable-all">
|
||||
[App Name]<br>
|
||||
prefix = https://<br>
|
||||
url = your-website.com<br>
|
||||
icon = static/images/apps/default.png<br>
|
||||
sidebar_icon = static/images/apps/default.png<br>
|
||||
description = Example description<br>
|
||||
open_in = iframe<br>
|
||||
data_template = None
|
||||
</code>
|
||||
|
||||
<table class="mt-4 responsive-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Required</th>
|
||||
<th>Description</th>
|
||||
<th>Options</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="selectable">
|
||||
<tr>
|
||||
<td>[App Name]</td>
|
||||
<td>Yes</td>
|
||||
<td>The name of your app.</td>
|
||||
<td>string</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>prefix</td>
|
||||
<td>Yes</td>
|
||||
<td>The prefix for the app's url.</td>
|
||||
<td>web prefix, e.g. http:// or https://</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>url</td>
|
||||
<td>Yes</td>
|
||||
<td>The url for your app.</td>
|
||||
<td>web url, e.g. myapp.com</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>icon</td>
|
||||
<td>No</td>
|
||||
<td>Icon for the dashboard.</td>
|
||||
<td>/static/images/icons/yourpicture.png, external link to image</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>sidebar_icon</td>
|
||||
<td>No</td>
|
||||
<td>Icon for the sidenav.</td>
|
||||
<td>/static/images/icons/yourpicture.png, external link to image</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>description</td>
|
||||
<td>No</td>
|
||||
<td>A short description for the app.</td>
|
||||
<td>string</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>open_in</td>
|
||||
<td>Yes</td>
|
||||
<td>open the app in the current tab, an iframe or a new tab</td>
|
||||
<td>iframe, new_tab, this_tab</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>data_template</td>
|
||||
<td>No</td>
|
||||
<td>Template for displaying variable(s) from rest data *Note: you must have a rest data variable set up in the config</td>
|
||||
<td>example: Data: {{ '{{ your_variable }}' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h5>Api Data Reference</h5>
|
||||
<code class="selectable-all">
|
||||
[variable_name]<br>
|
||||
platform = rest<br>
|
||||
resource = your-website.com<br>
|
||||
value_template = variable_name<br>
|
||||
</code>
|
||||
|
||||
<table class="mt-4 responsive-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Required</th>
|
||||
<th>Description</th>
|
||||
<th>Options</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="selectable">
|
||||
<tr>
|
||||
<td>[variable_name]</td>
|
||||
<td>Yes</td>
|
||||
<td>The variable to be made available to apps.</td>
|
||||
<td>variable (python syntax)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>platform</td>
|
||||
<td>Yes</td>
|
||||
<td>Platform for data source</td>
|
||||
<td>rest</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>resource</td>
|
||||
<td>Yes</td>
|
||||
<td>The url for the api call.</td>
|
||||
<td>myapp.com/api/hello</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>value_template</td>
|
||||
<td>No</td>
|
||||
<td>Tranform the data returned by the api call (python syntax)</td>
|
||||
<td>variable_name[0]['info']</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>method</td>
|
||||
<td>NOT IMPLEMENTED</td>
|
||||
<td>NOT IMPLEMENTED</td>
|
||||
<td>NOT IMPLEMENTED</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>payload</td>
|
||||
<td>NOT IMPLEMENTED</td>
|
||||
<td>NOT IMPLEMENTED</td>
|
||||
<td>NOT IMPLEMENTED</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>authentication</td>
|
||||
<td>NOT IMPLEMENTED</td>
|
||||
<td>NOT IMPLEMENTED</td>
|
||||
<td>NOT IMPLEMENTED</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>username</td>
|
||||
<td>NOT IMPLEMENTED</td>
|
||||
<td>NOT IMPLEMENTED</td>
|
||||
<td>NOT IMPLEMENTED</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>password</td>
|
||||
<td>NOT IMPLEMENTED</td>
|
||||
<td>NOT IMPLEMENTED</td>
|
||||
<td>NOT IMPLEMENTED</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h5>Api Data Example</h5>
|
||||
<p>Say we wanted to display how many Pokemon there are using the PokeAPI, we would add the following to the config:</p>
|
||||
<code class="selectable-all">
|
||||
[num_pokemon]<br>
|
||||
platform = rest<br>
|
||||
resource = https://pokeapi.co/api/v2/pokemon<br>
|
||||
value_template = num_pokemon['count']<br>
|
||||
</code>
|
||||
|
||||
<p>Then in the config entry for the app you want to add this to, you would add:</p>
|
||||
|
||||
<code class="selectable-all">
|
||||
data_template = Pokemon: {{ '{{ num_pokemon }}' }}
|
||||
</code>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="main" class="main-full">
|
||||
<div class="container">
|
||||
@ -256,11 +30,7 @@
|
||||
<div class="card-content">
|
||||
<div class="row">
|
||||
<div class="col s12">
|
||||
<h5>Config
|
||||
<a href="#config-wiki-modal" class="modal-trigger">
|
||||
<i class="material-icons-outlined theme-secondary-text icon-btn ml-2 toggle-config-help" style="position: relative; top: 4px;">info</i>
|
||||
</a>
|
||||
</h5>
|
||||
<h5>Config.ini</h5>
|
||||
{{ button(
|
||||
icon="save",
|
||||
id="save-config-btn",
|
||||
@ -292,6 +62,10 @@
|
||||
<div class="row">
|
||||
<div class="col s12 mb-2">
|
||||
<ul class="tabs tabs-fixed-width">
|
||||
<li class="tab col s3"><a href="#config-readme">
|
||||
<i class="material-icons-outlined">info</i>
|
||||
</a></li>
|
||||
|
||||
<li class="tab col s3"><a href="#images">
|
||||
<i class="material-icons-outlined">photo_library</i>
|
||||
</a></li>
|
||||
@ -307,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">
|
||||
@ -350,54 +103,15 @@
|
||||
<div id="template-div" class="selectable-all code"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="config-readme" class="col s12">
|
||||
{{ config_readme|safe }}
|
||||
</div>
|
||||
|
||||
<div id="user" class="col s12">
|
||||
<div class="row">
|
||||
<h5>User</h5>
|
||||
|
||||
<form id="edit-user-form">
|
||||
{{ user_form.hidden_tag() }}
|
||||
|
||||
{{ input(
|
||||
label="Username",
|
||||
id="user-form-username",
|
||||
size="s12",
|
||||
form_obj=user_form.username,
|
||||
val=current_user.username
|
||||
) }}
|
||||
|
||||
{{ input(
|
||||
label="Password",
|
||||
id="user-form-password",
|
||||
form_obj=user_form.password,
|
||||
size="s12"
|
||||
) }}
|
||||
|
||||
{{ input(
|
||||
label="Confirm Password",
|
||||
id="user-form-confirm_password",
|
||||
form_obj=user_form.confirm_password,
|
||||
required='required',
|
||||
size="s12"
|
||||
) }}
|
||||
</form>
|
||||
|
||||
{{ button(
|
||||
icon="save",
|
||||
float="left",
|
||||
id="edit-user-btn",
|
||||
data={'url': url_for('settings_system.edit_user')},
|
||||
text="save"
|
||||
) }}
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<h5>DashMachine</h5>
|
||||
<p class="mb-2">version: {{ version }}</p>
|
||||
|
||||
</div>
|
||||
|
||||
{{ UserTab() }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
130
dashmachine/templates/settings_system/user.html
Normal file
130
dashmachine/templates/settings_system/user.html
Normal file
@ -0,0 +1,130 @@
|
||||
{% macro UserTab() %}
|
||||
<div id="user-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="row mt-2">
|
||||
<div class="col s12">
|
||||
<form id="edit-user-form">
|
||||
{{ user_form.hidden_tag() }}
|
||||
|
||||
{{ select(
|
||||
id='user-form-role',
|
||||
form_obj=user_form.role,
|
||||
size="s12",
|
||||
label='Role'
|
||||
) }}
|
||||
|
||||
{{ input(
|
||||
label="Username",
|
||||
id="user-form-username",
|
||||
size="s12",
|
||||
form_obj=user_form.username
|
||||
) }}
|
||||
|
||||
{{ input(
|
||||
label="Password",
|
||||
id="user-form-password",
|
||||
form_obj=user_form.password,
|
||||
size="s12"
|
||||
) }}
|
||||
|
||||
{{ input(
|
||||
label="Confirm Password",
|
||||
id="user-form-confirm_password",
|
||||
form_obj=user_form.confirm_password,
|
||||
required='required',
|
||||
size="s12"
|
||||
) }}
|
||||
|
||||
{{ user_form.id(class="hide", id="user-form-id") }}
|
||||
|
||||
</form>
|
||||
|
||||
{{ button(
|
||||
icon="save",
|
||||
float="left",
|
||||
id="save-user-btn",
|
||||
class="mb-2",
|
||||
data={'url': url_for('settings_system.edit_user')},
|
||||
text="save"
|
||||
) }}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-2">
|
||||
<div class="col s12">
|
||||
<h5>Users
|
||||
<a href="#user-modal" class="modal-trigger">
|
||||
<i class="material-icons-outlined theme-secondary-text icon-btn ml-2 toggle-config-help" style="position: relative; top: 4px;">add</i>
|
||||
</a>
|
||||
</h5>
|
||||
|
||||
<div id="users-div">
|
||||
{{ Users(users) }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mt-4">
|
||||
<h5>DashMachine</h5>
|
||||
<p class="mb-2">version: {{ version }}</p>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro Users(users) %}
|
||||
{% for user in users %}
|
||||
<div class="card theme-surface-1">
|
||||
<div class="card-content">
|
||||
<span style="font-size: 1.3rem">
|
||||
{{ user.username }}
|
||||
<span class="theme-secondary-text">{{ user.role }}</span>
|
||||
</span>
|
||||
<span class="right pb-2">
|
||||
<i class="material-icons-outlined icon-btn edit-user-btn"
|
||||
data-role="{{ user.role }}"
|
||||
data-id="{{ user.id }}"
|
||||
data-username="{{ user.username }}">edit</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)}}
|
19
dashmachine/user_system/forms.py
Executable file → Normal file
19
dashmachine/user_system/forms.py
Executable file → Normal file
@ -1,10 +1,9 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import (
|
||||
StringField,
|
||||
PasswordField,
|
||||
BooleanField,
|
||||
)
|
||||
from wtforms import StringField, PasswordField, BooleanField, SelectField
|
||||
from wtforms.validators import DataRequired, Length
|
||||
from dashmachine.settings_system.models import Settings
|
||||
|
||||
settings_db = Settings.query.first()
|
||||
|
||||
|
||||
class UserForm(FlaskForm):
|
||||
@ -12,6 +11,16 @@ class UserForm(FlaskForm):
|
||||
|
||||
password = PasswordField(validators=[DataRequired(), Length(min=8, max=120)])
|
||||
|
||||
role = SelectField(choices=[(role, role) for role in settings_db.roles.split(",")])
|
||||
|
||||
id = StringField()
|
||||
|
||||
confirm_password = PasswordField()
|
||||
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
username = StringField(validators=[DataRequired(), Length(min=1, max=120)])
|
||||
|
||||
password = PasswordField(validators=[DataRequired(), Length(min=8, max=120)])
|
||||
|
||||
remember = BooleanField()
|
||||
|
@ -11,7 +11,4 @@ class User(db.Model, UserMixin):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(120), unique=True, nullable=False)
|
||||
password = db.Column(db.String(60), nullable=False)
|
||||
|
||||
|
||||
db.create_all()
|
||||
db.session.commit()
|
||||
role = db.Column(db.String())
|
||||
|
@ -1,6 +1,6 @@
|
||||
from flask import render_template, url_for, redirect, Blueprint
|
||||
from flask_login import login_user, logout_user, current_user
|
||||
from dashmachine.user_system.forms import UserForm
|
||||
from flask_login import login_user, logout_user
|
||||
from dashmachine.user_system.forms import LoginForm
|
||||
from dashmachine.user_system.models import User
|
||||
from dashmachine.user_system.utils import add_edit_user
|
||||
from dashmachine import bcrypt
|
||||
@ -18,10 +18,7 @@ user_system = Blueprint("user_system", __name__)
|
||||
def login():
|
||||
user = User.query.first()
|
||||
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for("main.home"))
|
||||
|
||||
form = UserForm()
|
||||
form = LoginForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
user = User.query.filter_by(username=form.username.data.lower()).first()
|
||||
|
@ -2,16 +2,21 @@ from dashmachine import db, bcrypt
|
||||
from dashmachine.user_system.models import User
|
||||
|
||||
|
||||
def add_edit_user(username, password, user_id=None):
|
||||
def add_edit_user(username, password, user_id=None, role=None):
|
||||
if user_id:
|
||||
user = User.query.filter_by(id=user_id).first()
|
||||
else:
|
||||
user = User.query.first()
|
||||
if not user:
|
||||
user = User()
|
||||
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
|
||||
user.role = role
|
||||
db.session.merge(user)
|
||||
db.session.commit()
|
||||
|
@ -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/01a575cda54d_.py
Normal file
28
migrations/versions/01a575cda54d_.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 01a575cda54d
|
||||
Revises: 598477dd1193
|
||||
Create Date: 2020-02-04 07:39:43.504475
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "01a575cda54d"
|
||||
down_revision = "598477dd1193"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column("apps", sa.Column("groups", sa.String(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("apps", "groups")
|
||||
# ### end Alembic commands ###
|
28
migrations/versions/03663c18575b_.py
Normal file
28
migrations/versions/03663c18575b_.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 03663c18575b
|
||||
Revises: af72304ae017
|
||||
Create Date: 2020-02-04 07:14:23.184567
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "03663c18575b"
|
||||
down_revision = "af72304ae017"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column("user", sa.Column("role", sa.String(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("user", "role")
|
||||
# ### end Alembic commands ###
|
40
migrations/versions/45ebff47af9f_.py
Normal file
40
migrations/versions/45ebff47af9f_.py
Normal file
@ -0,0 +1,40 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 45ebff47af9f
|
||||
Revises: 6bd40f00f2eb
|
||||
Create Date: 2020-02-06 11:48:22.563926
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "45ebff47af9f"
|
||||
down_revision = "6bd40f00f2eb"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table("api_calls")
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"api_calls",
|
||||
sa.Column("id", sa.INTEGER(), nullable=False),
|
||||
sa.Column("name", sa.VARCHAR(), nullable=True),
|
||||
sa.Column("resource", sa.VARCHAR(), nullable=True),
|
||||
sa.Column("method", sa.VARCHAR(), nullable=True),
|
||||
sa.Column("payload", sa.VARCHAR(), nullable=True),
|
||||
sa.Column("authentication", sa.VARCHAR(), nullable=True),
|
||||
sa.Column("username", sa.VARCHAR(), nullable=True),
|
||||
sa.Column("password", sa.VARCHAR(), nullable=True),
|
||||
sa.Column("value_template", sa.VARCHAR(), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
# ### end Alembic commands ###
|
28
migrations/versions/598477dd1193_.py
Normal file
28
migrations/versions/598477dd1193_.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 598477dd1193
|
||||
Revises: 03663c18575b
|
||||
Create Date: 2020-02-04 07:33:25.019173
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "598477dd1193"
|
||||
down_revision = "03663c18575b"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column("settings", sa.Column("roles", sa.String(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("settings", "roles")
|
||||
# ### end Alembic commands ###
|
40
migrations/versions/6bd40f00f2eb_.py
Normal file
40
migrations/versions/6bd40f00f2eb_.py
Normal file
@ -0,0 +1,40 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 6bd40f00f2eb
|
||||
Revises: d87e35114b0b
|
||||
Create Date: 2020-02-05 18:41:57.209232
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "6bd40f00f2eb"
|
||||
down_revision = "d87e35114b0b"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column("apps", sa.Column("groups", sa.String(), nullable=True))
|
||||
op.add_column(
|
||||
"settings", sa.Column("home_access_groups", sa.String(), nullable=True)
|
||||
)
|
||||
op.add_column("settings", sa.Column("roles", sa.String(), nullable=True))
|
||||
op.add_column(
|
||||
"settings", sa.Column("settings_access_groups", sa.String(), nullable=True)
|
||||
)
|
||||
op.add_column("user", sa.Column("role", sa.String(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("user", "role")
|
||||
op.drop_column("settings", "settings_access_groups")
|
||||
op.drop_column("settings", "roles")
|
||||
op.drop_column("settings", "home_access_groups")
|
||||
op.drop_column("apps", "groups")
|
||||
# ### end Alembic commands ###
|
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 ###
|
40
migrations/versions/8f5a046465e8_.py
Normal file
40
migrations/versions/8f5a046465e8_.py
Normal file
@ -0,0 +1,40 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 8f5a046465e8
|
||||
Revises: 45ebff47af9f
|
||||
Create Date: 2020-02-06 19:51:14.594434
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "8f5a046465e8"
|
||||
down_revision = "45ebff47af9f"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table("api_calls")
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"api_calls",
|
||||
sa.Column("id", sa.INTEGER(), nullable=False),
|
||||
sa.Column("name", sa.VARCHAR(), nullable=True),
|
||||
sa.Column("resource", sa.VARCHAR(), nullable=True),
|
||||
sa.Column("method", sa.VARCHAR(), nullable=True),
|
||||
sa.Column("payload", sa.VARCHAR(), nullable=True),
|
||||
sa.Column("authentication", sa.VARCHAR(), nullable=True),
|
||||
sa.Column("username", sa.VARCHAR(), nullable=True),
|
||||
sa.Column("password", sa.VARCHAR(), nullable=True),
|
||||
sa.Column("value_template", sa.VARCHAR(), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
# ### end Alembic commands ###
|
34
migrations/versions/d87e35114b0b_.py
Normal file
34
migrations/versions/d87e35114b0b_.py
Normal file
@ -0,0 +1,34 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: d87e35114b0b
|
||||
Revises: 01a575cda54d
|
||||
Create Date: 2020-02-04 08:13:35.783741
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "d87e35114b0b"
|
||||
down_revision = "01a575cda54d"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column(
|
||||
"settings", sa.Column("home_access_groups", sa.String(), nullable=True)
|
||||
)
|
||||
op.add_column(
|
||||
"settings", sa.Column("settings_access_groups", sa.String(), nullable=True)
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("settings", "settings_access_groups")
|
||||
op.drop_column("settings", "home_access_groups")
|
||||
# ### end Alembic commands ###
|
@ -32,3 +32,5 @@ SQLAlchemy==1.3.13
|
||||
urllib3==1.25.8
|
||||
Werkzeug==0.16.1
|
||||
WTForms==2.2.1
|
||||
transmissionrpc
|
||||
markdown2
|
Loading…
x
Reference in New Issue
Block a user