Merge pull request #63 from jr-k/develop

Release v1.15
This commit is contained in:
JRK 2024-05-18 20:43:56 +02:00 committed by GitHub
commit 260366fb01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 392 additions and 321 deletions

View File

@ -6,7 +6,6 @@ out/
data/uploads/*
!data/uploads/sample.jpg
data/db/*
!data/db/slideshow.json.dist
/plugins/user/*
!/plugins/user/.gitkeep
*.lock

1
.gitignore vendored
View File

@ -6,7 +6,6 @@ out/
data/uploads/*
!data/uploads/sample.jpg
data/db/*
!data/db/slideshow.json.dist
/plugins/user/*
!/plugins/user/.gitkeep
*.lock

View File

@ -1,9 +1,11 @@
FROM python:3.9.17-alpine3.17
RUN apk add --no-cache --virtual .build-deps gcc musl-dev sqlite-dev
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
RUN pip install -r requirements.txt && apk del .build-deps gcc musl-dev sqlite-dev
ENTRYPOINT ["python", "/app/obscreen.py"]

View File

@ -15,7 +15,7 @@ Use a RaspberryPi (Lite OS) to show a full-screen slideshow (Kiosk-mode)
- Clear GUI
- Fleet view to manage many devices easily
- Very few dependencies
- JSON database files
- SQLite database
- Plugin system
- No stupid pricing plan
- No cloud

View File

@ -1,37 +0,0 @@
{
"version": 2,
"keys": [
"duration",
"enabled",
"location",
"name",
"position",
"type",
"cron_schedule",
"cron_schedule_end",
"created_by",
"updated_by",
"created_at",
"updated_at"
],
"data": {
"0": {
"location": "data/uploads/sample.jpg",
"duration": 10,
"type": "picture",
"enabled": true,
"name": "Picture Sample",
"position": 0,
"cron_schedule": null
},
"1": {
"location": "https://unix.org",
"duration": 20,
"type": "url",
"enabled": true,
"name": "URL Sample",
"position": 1,
"cron_schedule": null
}
}
}

View File

@ -9,7 +9,7 @@ jQuery(document).ready(function ($) {
if (confirm(l.js_sysinfo_restart_confirmation)) {
$('body').html(l.js_sysinfo_restart_loading).css({margin:200});
$.ajax({
url: '/sysinfo/restart',
url: '/sysinfo/restart?secret_key='+secret_key,
headers: {'Content-Type': 'application/json'},
data: '',
method: 'POST',

View File

@ -53,10 +53,10 @@ cd ~ && git clone https://github.com/jr-k/obscreen.git && cd obscreen
# Install application dependencies
python3 -m venv venv
source ./venv/bin/activate
pip install -r requirements.txt
# Add some sample data
cp data/db/slideshow.json.dist data/db/slideshow.json
# 🚨For MacOS users, requirements installation may cause an error but it's ok if only for pysqlite3 package
# you'll need to install brew and execute command `brew install sqlite3`
pip install -r requirements.txt
# Customize server default values
cp .env.dist .env

View File

@ -72,9 +72,6 @@ python3 -m venv venv
source ./venv/bin/activate
pip install -r requirements.txt
# Add some sample data
cp data/db/slideshow.json.dist data/db/slideshow.json
# Customize server default values
cp .env.dist .env
```

View File

@ -1,6 +1,6 @@
flask==2.3.3
pysondb-v2==2.1.0
python-dotenv
cron-descriptor
waitress
flask-login
pysqlite3

View File

@ -18,8 +18,9 @@ class Application:
self._model_store = ModelStore()
self._template_renderer = TemplateRenderer(project_dir=project_dir, model_store=self._model_store, render_hook=self.render_hook)
self._web_server = WebServer(project_dir=project_dir, model_store=self._model_store, template_renderer=self._template_renderer)
self._plugin_store = PluginStore(project_dir=project_dir, model_store=self._model_store, template_renderer=self._template_renderer, web_server=self._web_server)
logging.info("[Obscreen] Starting...")
self._plugin_store = PluginStore(project_dir=project_dir, model_store=self._model_store, template_renderer=self._template_renderer, web_server=self._web_server)
signal.signal(signal.SIGINT, self.signal_handler)
def start(self) -> None:
@ -27,6 +28,7 @@ class Application:
def signal_handler(self, signal, frame) -> None:
logging.info("Shutting down...")
self._model_store.database().close()
self._stop_event.set()
sys.exit(0)

View File

@ -24,6 +24,9 @@ class AuthController(ObController):
if current_user.is_authenticated:
return redirect(url_for('slideshow_slide_list'))
if not self._model_store.variable().map().get('auth_enabled').as_bool():
return redirect(url_for('slideshow_slide_list'))
if len(request.form):
user = self._model_store.user().get_one_by_username(request.form['username'], enabled=True)
if user:
@ -42,6 +45,13 @@ class AuthController(ObController):
def logout(self):
logout_user()
if request.args.get('restart'):
return redirect(url_for(
'sysinfo_restart',
secret_key=self._model_store.config().map().get('secret_key')
))
return redirect(url_for('login'))
def auth_user_list(self):

View File

@ -24,7 +24,7 @@ class SettingsController(ObController):
forward = self._post_update(request.form['id'])
return forward if forward is not None else redirect(url_for('settings_variable_list'))
def _post_update(self, id: str):
def _post_update(self, id: int):
variable = self._model_store.variable().get(id)
if variable.refresh_player:
@ -39,7 +39,10 @@ class SettingsController(ObController):
if variable.name == 'auth_enabled':
self.reload_web_server()
if variable.as_bool():
return redirect(url_for('logout'))
return redirect(url_for(
'logout',
restart=1
))
if variable.name == 'lang':
self._model_store.lang().set_lang(variable.value)

View File

@ -2,21 +2,23 @@ import os
import sys
import platform
import subprocess
import threading
import time
from flask import Flask, render_template, jsonify
from flask import Flask, render_template, jsonify, request, url_for, redirect
from src.manager.VariableManager import VariableManager
from src.manager.ConfigManager import ConfigManager
from src.service.ModelStore import ModelStore
from src.interface.ObController import ObController
from src.utils import get_ip_address
from src.utils import get_ip_address, am_i_in_docker
class SysinfoController(ObController):
def register(self):
self._app.add_url_rule('/sysinfo', 'sysinfo_attribute_list', self._auth(self.sysinfo), methods=['GET'])
self._app.add_url_rule('/sysinfo/restart', 'sysinfo_restart', self._auth(self.sysinfo_restart), methods=['POST'])
self._app.add_url_rule('/sysinfo/restart', 'sysinfo_restart', self.sysinfo_restart, methods=['GET', 'POST'])
self._app.add_url_rule('/sysinfo/restart/needed', 'sysinfo_restart_needed', self._auth(self.sysinfo_restart_needed), methods=['GET'])
def sysinfo(self):
@ -29,20 +31,13 @@ class SysinfoController(ObController):
)
def sysinfo_restart(self):
if platform.system().lower() == 'darwin':
if self._model_store.config().map().get('debug'):
python = sys.executable
os.execl(python, python, *sys.argv)
else:
try:
subprocess.run(["sudo", "systemctl", "restart", 'obscreen'], check=True, timeout=10, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
pass
except subprocess.TimeoutExpired:
pass
except subprocess.CalledProcessError:
pass
secret = self._model_store.config().map().get('secret_key')
challenge = request.args.get('secret_key')
thread = threading.Thread(target=self.restart, args=(secret, challenge))
thread.daemon = True
thread.start()
return jsonify({'status': 'ok'})
return redirect(url_for('manage'))
def sysinfo_restart_needed(self):
var_last_slide_update = self._model_store.variable().get_one_by_name('last_slide_update')
@ -53,3 +48,24 @@ class SysinfoController(ObController):
return jsonify({'status': True})
def restart(self, secret: str, challenge: str) -> None:
time.sleep(1)
if secret != challenge:
return jsonify({'status': 'error'})
if platform.system().lower() == 'darwin':
if self._model_store.config().map().get('debug'):
python = sys.executable
os.execl(python, python, *sys.argv)
elif am_i_in_docker:
python = sys.executable
os.execl(python, python, *sys.argv)
else:
try:
subprocess.run(["sudo", "systemctl", "restart", 'obscreen-manager'], check=True, timeout=10, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
pass
except subprocess.TimeoutExpired:
pass
except subprocess.CalledProcessError:
pass

View File

@ -1,33 +1,144 @@
import os
import json
import sys
from pysondb import PysonDB
from typing import Optional
import sqlite3
import logging
from sqlite3 import Cursor
from src.utils import wrap_if, is_wrapped_by
from typing import Optional, Dict
class DatabaseManager:
DB_DIR = 'data/db'
DB_FILE: str = "data/db/obscreen.db"
def __init__(self):
pass
self._conn = None
self._enabled = True
self.init()
def open(self, table_name: str, table_model: list) -> PysonDB:
db_file = "{}/{}.json".format(self.DB_DIR, table_name)
db = PysonDB(db_file)
db = self._update_model(db_file, table_model)
return db
def init(self):
logging.info('Using DB engine {}'.format(self.__class__.__name__))
self._open()
def _open(self, flush: bool = False) -> None:
if flush and os.path.isfile(self.DB_FILE):
os.unlink(self.DB_FILE)
self._conn = sqlite3.connect(self.DB_FILE, check_same_thread=False)
self._conn.row_factory = sqlite3.Row
def open(self, table_name: str, table_model: list):
self.execute_write_query('''CREATE TABLE IF NOT EXISTS {} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
{}
)'''.format(table_name, ", ".join(table_model)))
return self
def close(self) -> None:
self._conn.close()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
def get_connection(self):
return self._conn
def execute_write_query(self, query, params=()) -> None:
logging.debug(query)
cur = None
sanitized_params = []
for param in params:
if isinstance(param, bool):
sanitized_params.append(int(param))
elif isinstance(param, dict) or isinstance(param, list):
sanitized_params.append(json.dumps(param))
else:
sanitized_params.append(param)
@staticmethod
def _update_model(db_file: str, table_model: list) -> Optional[PysonDB]:
try:
with open(db_file, 'r') as file:
db_model = file.read()
db_model = json.loads(db_model)
db_model['keys'] = table_model
with open(db_file, 'w') as file:
file.write(json.dumps(db_model, indent=4))
return PysonDB(db_file)
except FileNotFoundError:
logging.error("Database file {} not found".format(db_file))
return None
with self._conn:
cur = self._conn.cursor()
cur.execute(query, tuple(sanitized_params))
except sqlite3.Error as e:
logging.error("SQL query execution error while writing '{}': {}".format(query, e))
self._conn.rollback()
finally:
if cur is not None:
cur.close()
def execute_read_query(self, query, params=()) -> list:
logging.debug(query)
cur = None
try:
with self._conn:
cur = self._conn.cursor()
cur.execute(query, params)
rows = cur.fetchall()
result = [dict(row) for row in rows]
except sqlite3.Error as e:
logging.error("SQL query execution error while reading '{}': {}".format(query, e))
result = []
finally:
if cur is not None:
cur.close()
return result
def get_all(self, table_name: str, sort: Optional[str] = None) -> list:
return self.execute_read_query(
query="select * from {} {}".format(table_name, "ORDER BY {} ASC".format(sort) if sort else "")
)
def get_by_query(self, table_name: str, query: str = "1=1", sort: Optional[str] = None) -> list:
return self.execute_read_query(
query="select * from {} where {} {}".format(
table_name,
query,
"ORDER BY {} ASC".format(sort) if sort else ""
)
)
def get_one_by_query(self, table_name: str, query: str = "1=1", sort: Optional[str] = None) -> list:
query = "select * from {} where {} {}".format(table_name, query, "ORDER BY {} ASC".format(sort) if sort else "")
lines = self.execute_read_query(query=query)
count = len(lines)
if count > 1:
raise Error("More than one line returned by query '{}'".format(query))
return lines[0] if count == 1 else None
def update_by_query(self, table_name: str, query: str = "1=1", values: dict = {}) -> list:
return self.execute_write_query(
query="UPDATE {} SET {} where {}".format(
table_name,
" , ".join(["{} = ?".format(k, v) for k, v in values.items()]),
query
),
params=tuple(v for v in values.values())
)
def update_by_id(self, table_name: str, id: int, values: dict = {}) -> list:
return self.update_by_query(table_name, "id = {}".format(id), values)
def get_by_id(self, table_name: str, id: int) -> Optional[Dict]:
return self.get_one_by_query(table_name, "id = {}".format(id))
def add(self, table_name: str, values: dict) -> None:
self.execute_write_query(
query="INSERT INTO {} ({}) VALUES ({})".format(
table_name,
", ".join(["{}".format(key) for key in values.keys()]),
", ".join(["?" for _ in values.keys()]),
),
params=tuple(v for v in values.values())
)
def delete_by_id(self, table_name: str, id: int) -> None:
self.execute_write_query("DELETE FROM {} WHERE id = ?".format(table_name), params=(id,))

View File

@ -1,4 +1,3 @@
from pysondb.errors import IdDoesNotExistError
from typing import Dict, Optional, List, Tuple, Union
from src.model.entity.Screen import Screen
@ -12,71 +11,59 @@ class ScreenManager(ModelManager):
TABLE_NAME = "fleet"
TABLE_MODEL = [
"name",
"enabled",
"position",
"host",
"port"
"name CHAR(255)",
"enabled INTEGER",
"position INTEGER",
"host CHAR(255)",
"port INTEGER"
]
def __init__(self, lang_manager: LangManager, database_manager: DatabaseManager, user_manager: UserManager):
super().__init__(lang_manager, database_manager, user_manager)
self._db = database_manager.open(self.TABLE_NAME, self.TABLE_MODEL)
def hydrate_object(self, raw_screen: dict, id: Optional[str] = None) -> Screen:
def hydrate_object(self, raw_screen: dict, id: Optional[int] = None) -> Screen:
if id:
raw_screen['id'] = id
return Screen(**raw_screen)
def hydrate_dict(self, raw_screens: dict) -> List[Screen]:
return [self.hydrate_object(raw_screen, raw_id) for raw_id, raw_screen in raw_screens.items()]
def hydrate_list(self, raw_screens: list) -> List[Screen]:
return [self.hydrate_object(raw_screen) for raw_screen in raw_screens]
def get(self, id: str) -> Optional[Screen]:
try:
return self.hydrate_object(self._db.get_by_id(id), id)
except IdDoesNotExistError:
return None
def get(self, id: int) -> Optional[Screen]:
object = self._db.get_by_id(self.TABLE_NAME, id)
return self.hydrate_object(object, id) if object else None
def get_by(self, query) -> List[Screen]:
return self.hydrate_dict(self._db.get_by_query(query=query))
def get_by(self, query, sort: Optional[str] = None) -> List[Screen]:
return self.hydrate_list(self._db.get_by_query(self.TABLE_NAME, query=query, sort=sort))
def get_one_by(self, query) -> Optional[Screen]:
screens = self.hydrate_dict(self._db.get_by_query(query=query))
if len(screens) == 1:
return screens[0]
elif len(screens) > 1:
raise Error("More than one result for query")
object = self._db.get_one_by_query(self.TABLE_NAME, query=query)
if not object:
return None
return self.hydrate_object(object)
def get_all(self, sort: bool = False) -> List[Screen]:
raw_screens = self._db.get_all()
if isinstance(raw_screens, dict):
if sort:
return sorted(self.hydrate_dict(raw_screens), key=lambda x: x.position)
return self.hydrate_dict(raw_screens)
return self.hydrate_list(sorted(raw_screens, key=lambda x: x['position']) if sort else raw_screens)
return self.hydrate_list(self._db.get_all(self.TABLE_NAME, "position" if sort else None))
def get_enabled_screens(self) -> List[Screen]:
return [screen for screen in self.get_all(sort=True) if screen.enabled]
return self.get_by(query="enabled = 1", sort="position")
def get_disabled_screens(self) -> List[Screen]:
return [screen for screen in self.get_all(sort=True) if not screen.enabled]
return self.get_by(query="enabled = 0", sort="position")
def update_enabled(self, id: str, enabled: bool) -> None:
self._db.update_by_id(id, {"enabled": enabled, "position": 999})
def update_enabled(self, id: int, enabled: bool) -> None:
self._db.update_by_id(self.TABLE_NAME, id, {"enabled": enabled, "position": 999})
def update_positions(self, positions: list) -> None:
for screen_id, screen_position in positions.items():
self._db.update_by_id(screen_id, {"position": screen_position})
self._db.update_by_id(self.TABLE_NAME, screen_id, {"position": screen_position})
def update_form(self, id: str, name: str, host: str, port: int) -> None:
self._db.update_by_id(id, {"name": name, "host": host, "port": port})
def update_form(self, id: int, name: str, host: str, port: int) -> None:
self._db.update_by_id(self.TABLE_NAME, id, {"name": name, "host": host, "port": port})
def add_form(self, screen: Union[Screen, Dict]) -> None:
form = screen
@ -85,10 +72,10 @@ class ScreenManager(ModelManager):
form = screen.to_dict()
del form['id']
self._db.add(form)
self._db.add(self.TABLE_NAME, form)
def delete(self, id: str) -> None:
self._db.delete_by_id(id)
def delete(self, id: int) -> None:
self._db.delete_by_id(self.TABLE_NAME, id)
def to_dict(self, screens: List[Screen]) -> List[Dict]:
return [screen.to_dict() for screen in screens]

View File

@ -1,7 +1,6 @@
import os
from typing import Dict, Optional, List, Tuple, Union
from pysondb.errors import IdDoesNotExistError
from src.model.entity.Slide import Slide
from src.model.enum.SlideType import SlideType
@ -16,80 +15,68 @@ class SlideManager(ModelManager):
TABLE_NAME = "slideshow"
TABLE_MODEL = [
"name",
"type",
"enabled",
"duration",
"position",
"location",
"cron_schedule",
"cron_schedule_end",
"created_by",
"updated_by",
"created_at",
"updated_at"
"name CHAR(255)",
"type CHAR(30)",
"enabled INTEGER",
"duration INTEGER",
"position INTEGER",
"location TEXT",
"cron_schedule CHAR(255)",
"cron_schedule_end CHAR(255)",
"created_by CHAR(255)",
"updated_by CHAR(255)",
"created_at INTEGER",
"updated_at INTEGER"
]
def __init__(self, lang_manager: LangManager, database_manager: DatabaseManager, user_manager: UserManager):
super().__init__(lang_manager, database_manager, user_manager)
self._db = database_manager.open(self.TABLE_NAME, self.TABLE_MODEL)
def hydrate_object(self, raw_slide: dict, id: str = None) -> Slide:
def hydrate_object(self, raw_slide: dict, id: int = None) -> Slide:
if id:
raw_slide['id'] = id
[raw_slide, user_tracker_edits] = self.user_manager.initialize_user_trackers(raw_slide)
if len(user_tracker_edits) > 0:
self._db.update_by_id(raw_slide['id'], user_tracker_edits)
self._db.update_by_id(self.TABLE_NAME, raw_slide['id'], user_tracker_edits)
return Slide(**raw_slide)
def hydrate_dict(self, raw_slides: dict) -> List[Slide]:
return [self.hydrate_object(raw_slide, raw_id) for raw_id, raw_slide in raw_slides.items()]
def hydrate_list(self, raw_slides: list) -> List[Slide]:
return [self.hydrate_object(raw_slide) for raw_slide in raw_slides]
def get(self, id: str) -> Optional[Slide]:
try:
return self.hydrate_object(self._db.get_by_id(id), id)
except IdDoesNotExistError:
return None
def get(self, id: int) -> Optional[Slide]:
object = self._db.get_by_id(self.TABLE_NAME, id)
return self.hydrate_object(object, id) if object else None
def get_by(self, query) -> List[Slide]:
return self.hydrate_dict(self._db.get_by_query(query=query))
def get_by(self, query, sort: Optional[str] = None) -> List[Slide]:
return self.hydrate_list(self._db.get_by_query(self.TABLE_NAME, query=query, sort=sort))
def get_one_by(self, query) -> Optional[Slide]:
slides = self.hydrate_dict(self._db.get_by_query(query=query))
if len(slides) == 1:
return slides[0]
elif len(slides) > 1:
raise Error("More than one result for query")
object = self._db.get_one_by_query(self.TABLE_NAME, query=query)
if not object:
return None
return self.hydrate_object(object)
def get_all(self, sort: bool = False) -> List[Slide]:
raw_slides = self._db.get_all()
return self.hydrate_list(self._db.get_all(self.TABLE_NAME, sort="position" if sort else None))
if isinstance(raw_slides, dict):
if sort:
return sorted(self.hydrate_dict(raw_slides), key=lambda x: x.position)
return self.hydrate_dict(raw_slides)
return self.hydrate_list(sorted(raw_slides, key=lambda x: x['position']) if sort else raw_slides)
def forget_user(self, user_id: str):
slides = self.hydrate_dict(self._db.get_by_query(query=lambda s: s['created_by'] == user_id or s['updated_by'] == user_id))
def forget_user(self, user_id: int):
slides = self.get_by("created_by = '{}' or updated_by = '{}'".format(user_id, user_id))
edits_slides = self.user_manager.forget_user(slides, user_id)
for slide_id, edits in edits_slides.items():
self._db.update_by_id(slide_id, edits)
self._db.update_by_id(self.TABLE_NAME, slide_id, edits)
def get_enabled_slides(self) -> List[Slide]:
return [slide for slide in self.get_all(sort=True) if slide.enabled]
return self.get_by(query="enabled = 1", sort="position")
def get_disabled_slides(self) -> List[Slide]:
return [slide for slide in self.get_all(sort=True) if not slide.enabled]
return self.get_by(query="enabled = 0", sort="position")
def pre_add(self, slide: Dict) -> Dict:
self.user_manager.track_user_on_create(slide)
@ -115,15 +102,15 @@ class SlideManager(ModelManager):
def post_delete(self, slide_id: str) -> str:
return slide_id
def update_enabled(self, id: str, enabled: bool) -> None:
self._db.update_by_id(id, self.pre_update({"enabled": enabled, "position": 999}))
def update_enabled(self, id: int, enabled: bool) -> None:
self._db.update_by_id(self.TABLE_NAME, id, self.pre_update({"enabled": enabled, "position": 999}))
self.post_update(id)
def update_positions(self, positions: list) -> None:
for slide_id, slide_position in positions.items():
self._db.update_by_id(slide_id, {"position": slide_position})
self._db.update_by_id(self.TABLE_NAME, slide_id, {"position": slide_position})
def update_form(self, id: str, name: str, duration: int, cron_schedule: Optional[str] = '', cron_schedule_end: Optional[str] = '', location: Optional[str] = None) -> None:
def update_form(self, id: int, name: str, duration: int, cron_schedule: Optional[str] = '', cron_schedule_end: Optional[str] = '', location: Optional[str] = None) -> None:
slide = self.get(id)
if not slide:
@ -142,7 +129,7 @@ class SlideManager(ModelManager):
if slide.type == SlideType.YOUTUBE:
form['location'] = get_yt_video_id(form['location'])
self._db.update_by_id(id, self.pre_update(form))
self._db.update_by_id(self.TABLE_NAME, id, self.pre_update(form))
self.post_update(id)
def add_form(self, slide: Union[Slide, Dict]) -> None:
@ -155,10 +142,10 @@ class SlideManager(ModelManager):
if form['type'] == SlideType.YOUTUBE.value:
form['location'] = get_yt_video_id(form['location'])
self._db.add(self.pre_add(form))
self._db.add(self.TABLE_NAME, self.pre_add(form))
self.post_add(slide.id)
def delete(self, id: str) -> None:
def delete(self, id: int) -> None:
slide = self.get(id)
if slide:
@ -169,7 +156,7 @@ class SlideManager(ModelManager):
pass
self.pre_delete(id)
self._db.delete_by_id(id)
self._db.delete_by_id(self.TABLE_NAME, id)
self.post_delete(id)
def to_dict(self, slides: List[Slide]) -> List[Dict]:

View File

@ -1,6 +1,5 @@
import hashlib
import time
from pysondb.errors import IdDoesNotExistError
from typing import Dict, Optional, List, Tuple, Union
from flask_login import current_user
@ -14,9 +13,9 @@ class UserManager:
TABLE_NAME = "user"
TABLE_MODEL = [
"username",
"password",
"enabled"
"username CHAR(255)",
"password CHAR(255)",
"enabled INTEGER"
]
def __init__(self, lang_manager: LangManager, database_manager: DatabaseManager, on_user_delete):
@ -44,37 +43,32 @@ class UserManager:
return user_map
def hydrate_object(self, raw_user: dict, id: Optional[str] = None) -> User:
def hydrate_object(self, raw_user: dict, id: Optional[int] = None) -> User:
if id:
raw_user['id'] = id
return User(**raw_user)
def hydrate_dict(self, raw_users: dict) -> List[User]:
return [self.hydrate_object(raw_user, raw_id) for raw_id, raw_user in raw_users.items()]
def hydrate_list(self, raw_users: list) -> List[User]:
return [self.hydrate_object(raw_user) for raw_user in raw_users]
def get(self, id: str) -> Optional[User]:
try:
return self.hydrate_object(self._db.get_by_id(id), id)
except IdDoesNotExistError:
return None
def get(self, id: int) -> Optional[User]:
object = self._db.get_by_id(self.TABLE_NAME, id)
return self.hydrate_object(object, id) if object else None
def get_by(self, query) -> List[User]:
return self.hydrate_dict(self._db.get_by_query(query=query))
def get_by(self, query, sort: Optional[str] = None) -> List[User]:
return self.hydrate_list(self._db.get_by_query(self.TABLE_NAME, query=query, sort=sort))
def get_one_by(self, query) -> Optional[User]:
users = self.hydrate_dict(self._db.get_by_query(query=query))
if len(users) == 1:
return users[0]
elif len(users) > 1:
raise Error("More than one result for query")
object = self._db.get_one_by_query(self.TABLE_NAME, query=query)
if not object:
return None
return self.hydrate_object(object)
def get_one_by_username(self, username: str, enabled: bool = None) -> Optional[User]:
return self.get_one_by(query=lambda v: v['username'] == username and (enabled is None or v['enabled'] == enabled))
return self.get_one_by("username = '{}' and (enabled is null or enabled = {})".format(username, int(enabled)))
def count_all_enabled(self):
return len(self.get_enabled_users())
@ -85,12 +79,16 @@ class UserManager:
def track_user_updated(self, id_or_entity: Optional[str]) -> User:
return self.track_user_action(id_or_entity, 'updated_by')
def track_user_action(self, id_or_entity: Optional[str], attribute: Optional[str] = 'created_by') -> User:
if not isinstance(id_or_entity, str):
def track_user_action(self, id_or_entity: Optional[int], attribute: Optional[str] = 'created_by') -> User:
if not isinstance(id_or_entity, int):
id_or_entity = getattr(id_or_entity, attribute)
id_or_entity = str(id_or_entity)
user_map = self.map()
try:
id_or_entity = int(id_or_entity)
except ValueError:
return User(username=id_or_entity, enabled=False)
user_map = self.prepare_map()
if id_or_entity in user_map:
return user_map[id_or_entity]
@ -101,20 +99,13 @@ class UserManager:
return User(username=self._lang_manager.translate('anonymous'), enabled=False)
def get_all(self, sort: bool = False) -> List[User]:
raw_users = self._db.get_all()
if isinstance(raw_users, dict):
if sort:
return sorted(self.hydrate_dict(raw_users), key=lambda x: x.username)
return self.hydrate_dict(raw_users)
return self.hydrate_list(sorted(raw_users, key=lambda x: x['username']) if sort else raw_users)
return self.hydrate_list(self._db.get_all(self.TABLE_NAME, "username" if sort else None))
def get_enabled_users(self) -> List[User]:
return [user for user in self.get_all(sort=True) if user.enabled]
return self.get_by(query="enabled = 1", sort="username")
def get_disabled_users(self) -> List[User]:
return [user for user in self.get_all(sort=True) if not user.enabled]
return self.get_by(query="enabled = 0", sort="username")
def pre_add(self, user: Dict) -> Dict:
return user
@ -122,33 +113,33 @@ class UserManager:
def pre_update(self, user: Dict) -> Dict:
return user
def pre_delete(self, user_id: str) -> str:
def pre_delete(self, user_id: int) -> int:
self._on_user_delete(user_id)
return user_id
def post_add(self, user_id: str) -> str:
def post_add(self, user_id: int) -> int:
self.reload()
return user_id
def post_update(self, user_id: str) -> str:
def post_update(self, user_id: int) -> int:
self.reload()
return user_id
def post_delete(self, user_id: str) -> str:
def post_delete(self, user_id: int) -> int:
self.reload()
return user_id
def update_enabled(self, id: str, enabled: bool) -> None:
self._db.update_by_id(id, self.pre_update({"enabled": enabled}))
def update_enabled(self, id: int, enabled: bool) -> None:
self._db.update_by_id(self.TABLE_NAME, id, self.pre_update({"enabled": enabled}))
self.post_update(id)
def update_form(self, id: str, username: str, password: Optional[str]) -> None:
def update_form(self, id: int, username: str, password: Optional[str]) -> None:
form = {"username": username}
if password is not None and password:
form['password'] = self.encode_password(password)
self._db.update_by_id(id, self.pre_update(form))
self._db.update_by_id(self.TABLE_NAME, id, self.pre_update(form))
self.post_update(id)
def add_form(self, user: Union[User, Dict]) -> None:
@ -160,12 +151,12 @@ class UserManager:
form['password'] = self.encode_password(form['password'])
self._db.add(self.pre_add(form))
self._db.add(self.TABLE_NAME, self.pre_add(form))
self.post_add(user.id)
def delete(self, id: str) -> None:
def delete(self, id: int) -> None:
self.pre_delete(id)
self._db.delete_by_id(id)
self._db.delete_by_id(self.TABLE_NAME, id)
self.post_delete(id)
def to_dict(self, users: List[User]) -> List[Dict]:
@ -215,18 +206,18 @@ class UserManager:
return None
def forget_user(self, objects: List, user_id: str) -> Dict:
user_map = self.map()
user_id = str(user_id)
def forget_user(self, objects: List, user_id: int) -> Dict:
user_map = self.prepare_map()
user_id = int(user_id)
edits = {}
for object in objects:
edits = {object.id: {}}
edits[object.id] = {}
if str(object.created_by) == user_id and user_id in user_map:
if int(object.created_by) == user_id and user_id in user_map:
edits[object.id]['created_by'] = user_map[user_id].username
if str(object.updated_by) == user_id and user_id in user_map:
if int(object.updated_by) == user_id and user_id in user_map:
edits[object.id]['updated_by'] = user_map[user_id].username
return edits

View File

@ -1,6 +1,6 @@
import json
import time
from typing import Dict, Optional, List, Tuple, Union
from pysondb.errors import IdDoesNotExistError
from src.manager.DatabaseManager import DatabaseManager
from src.manager.LangManager import LangManager
@ -24,17 +24,17 @@ class VariableManager(ModelManager):
TABLE_NAME = "settings"
TABLE_MODEL = [
"description",
"description_edition",
"editable",
"name",
"section",
"plugin",
"selectables",
"type",
"unit",
"refresh_player",
"value"
"description TEXT",
"description_edition TEXT",
"editable INTEGER",
"name CHAR(255)",
"section CHAR(255)",
"plugin CHAR(255)",
"selectables TEXT",
"type CHAR(255)",
"unit CHAR(255)",
"refresh_player INTEGER",
"value TEXT"
]
def __init__(self, lang_manager: LangManager, database_manager: DatabaseManager, user_manager: UserManager):
@ -75,25 +75,25 @@ class VariableManager(ModelManager):
same_selectables_label = get_keys(default_var, 'selectables', 'label') == get_keys(variable, 'selectables', 'label')
if variable.description != default_var['description']:
self._db.update_by_id(variable.id, {"description": default_var['description']})
self._db.update_by_id(self.TABLE_NAME, variable.id, {"description": default_var['description']})
if variable.description_edition != default_var['description_edition']:
self._db.update_by_id(variable.id, {"description_edition": default_var['description_edition']})
self._db.update_by_id(self.TABLE_NAME, variable.id, {"description_edition": default_var['description_edition']})
if variable.unit != default_var['unit']:
self._db.update_by_id(variable.id, {"unit": default_var['unit']})
self._db.update_by_id(self.TABLE_NAME, variable.id, {"unit": default_var['unit']})
if variable.section != default_var['section']:
self._db.update_by_id(variable.id, {"section": default_var['section']})
self._db.update_by_id(self.TABLE_NAME, variable.id, {"section": default_var['section']})
if variable.refresh_player != default_var['refresh_player']:
self._db.update_by_id(variable.id, {"refresh_player": default_var['refresh_player']})
self._db.update_by_id(self.TABLE_NAME, variable.id, {"refresh_player": default_var['refresh_player']})
if not same_selectables_keys or not same_selectables_label:
self._db.update_by_id(variable.id, {"selectables": default_var['selectables']})
self._db.update_by_id(self.TABLE_NAME, variable.id, {"selectables": default_var['selectables']})
if variable.name == 'last_restart':
self._db.update_by_id(variable.id, {"value": time.time()})
self._db.update_by_id(self.TABLE_NAME, variable.id, {"value": time.time()})
return variable
@ -144,75 +144,60 @@ class VariableManager(ModelManager):
return var_map
def hydrate_object(self, raw_variable: dict, id: Optional[str] = None) -> Variable:
def hydrate_object(self, raw_variable: dict, id: Optional[int] = None) -> Variable:
if id:
raw_variable['id'] = id
if 'selectables' in raw_variable and raw_variable['selectables']:
raw_variable['selectables'] = [Selectable(**selectable) for selectable in raw_variable['selectables']]
raw_variable['selectables'] = [Selectable(**selectable) for selectable in json.loads(raw_variable['selectables'])]
return Variable(**raw_variable)
def hydrate_dict(self, raw_variables: dict) -> List[Variable]:
return [self.hydrate_object(raw_variable, raw_id) for raw_id, raw_variable in raw_variables.items()]
def hydrate_list(self, raw_variables: list) -> List[Variable]:
return [self.hydrate_object(raw_variable) for raw_variable in raw_variables]
def get(self, id: str) -> Optional[Variable]:
try:
return self.hydrate_object(self._db.get_by_id(id), id)
except IdDoesNotExistError:
return None
def get(self, id: int) -> Optional[Variable]:
object = self._db.get_by_id(self.TABLE_NAME, id)
return self.hydrate_object(object, id) if object else None
def get_by(self, query) -> List[Variable]:
return self.hydrate_dict(self._db.get_by_query(query=query))
def get_by(self, query, sort: Optional[str] = None) -> List[Variable]:
return self.hydrate_list(self._db.get_by_query(self.TABLE_NAME, query=query, sort=sort))
def get_by_prefix(self, prefix: str) -> List[Variable]:
return self.hydrate_dict(self._db.get_by_query(query=lambda v: v['name'].startswith(prefix)))
return self.get_by(query="name like '{}%'".format(prefix))
def get_by_plugin(self, plugin: str) -> List[Variable]:
return self.hydrate_dict(self._db.get_by_query(query=lambda v: v['plugin'] == plugin))
return self.get_by(query="plugin = '{}'".format(plugin))
def get_one_by_name(self, name: str) -> Optional[Variable]:
return self.get_one_by(query=lambda v: v['name'] == name)
return self.get_one_by("name = '{}'".format(name))
def get_one_by(self, query) -> Optional[Variable]:
object = self._db.get_by_query(query=query)
variables = self.hydrate_dict(object)
if len(variables) == 1:
return variables[0]
elif len(variables) > 1:
raise Error("More than one result for query")
object = self._db.get_one_by_query(self.TABLE_NAME, query=query)
if not object:
return None
return self.hydrate_object(object)
def get_all(self) -> List[Variable]:
raw_variables = self._db.get_all()
if isinstance(raw_variables, dict):
return self.hydrate_dict(raw_variables)
return self.hydrate_list(raw_variables)
return self.hydrate_list(self._db.get_all(self.TABLE_NAME))
def get_editable_variables(self, plugin: bool = True, sort: Optional[str] = None) -> List[Variable]:
query = lambda v: (not plugin and not isinstance(v['plugin'], str)) or (plugin and isinstance(v['plugin'], str))
variables = [variable for variable in self.get_by(query=query) if variable.editable]
if sort is not None and sort:
return sorted(variables, key=lambda x: getattr(x, sort))
return variables
query = "plugin is null and editable = 1" if not plugin else "plugin is not null and length(plugin) > 0 and editable = 1"
return self.get_by(query=query, sort=sort)
def get_readonly_variables(self) -> List[Variable]:
return [variable for variable in self.get_all() if not variable.editable]
return self.get_by(query="editable = 0", sort="name")
def update_form(self, id: str, value: Union[int, bool, str]) -> None:
var_dict = self._db.update_by_id(id, {"value": value})
var = self.hydrate_object(var_dict, id)
def update_form(self, id: int, value: Union[int, bool, str]) -> None:
self._db.update_by_id(self.TABLE_NAME, id, {"value": value})
var = self.get_one_by("id = {}".format(id))
self._var_map[var.name] = var
def update_by_name(self, name: str, value) -> Optional[Variable]:
[var_id] = self._db.update_by_query(query=lambda v: v['name'] == name, new_data={"value": value})
var_dict = self._db.get_by_id(var_id)
var = self.hydrate_object(var_dict, id)
self._db.update_by_query(self.TABLE_NAME, query="name = '{}'".format(name), values={"value": value})
var = self.get_one_by_name(name)
self._var_map[name] = var
def add_form(self, variable: Union[Variable, Dict]) -> None:
@ -222,10 +207,10 @@ class VariableManager(ModelManager):
form = variable.to_dict()
del form['id']
self._db.add(form)
self._db.add(self.TABLE_NAME, form)
def delete(self, id: str) -> None:
self._db.delete_by_id(id)
def delete(self, id: int) -> None:
self._db.delete_by_id(self.TABLE_NAME, id)
def to_dict(self, variables: List[Variable]) -> List[Dict]:
return [variable.to_dict() for variable in variables]

View File

@ -5,7 +5,7 @@ from typing import Optional, Union
class Screen:
def __init__(self, host: str = '', port: int = 5000, enabled: bool = False, name: str = 'Untitled', position: int = 999, id: Optional[str] = None):
def __init__(self, host: str = '', port: int = 5000, enabled: bool = False, name: str = 'Untitled', position: int = 999, id: Optional[int] = None):
self._id = id if id else None
self._host = host
self._port = port
@ -14,7 +14,7 @@ class Screen:
self._position = position
@property
def id(self) -> Union[int, str]:
def id(self) -> Optional[int]:
return self._id
@property
@ -35,11 +35,11 @@ class Screen:
@property
def enabled(self) -> bool:
return self._enabled
return bool(self._enabled)
@enabled.setter
def enabled(self, value: bool):
self._enabled = value
self._enabled = bool(value)
@property
def name(self) -> str:

View File

@ -8,7 +8,7 @@ from src.utils import str_to_enum
class Slide:
def __init__(self, location: str = '', duration: int = 3, type: Union[SlideType, str] = SlideType.URL, enabled: bool = False, name: str = 'Untitled', position: int = 999, id: Optional[str] = None, cron_schedule: Optional[str] = None, cron_schedule_end: Optional[str] = None, created_by: Optional[str] = None, updated_by: Optional[str] = None, created_at: Optional[int] = None, updated_at: Optional[int] = None):
def __init__(self, location: str = '', duration: int = 3, type: Union[SlideType, str] = SlideType.URL, enabled: bool = False, name: str = 'Untitled', position: int = 999, id: Optional[int] = None, cron_schedule: Optional[str] = None, cron_schedule_end: Optional[str] = None, created_by: Optional[str] = None, updated_by: Optional[str] = None, created_at: Optional[int] = None, updated_at: Optional[int] = None):
self._id = id if id else None
self._location = location
self._duration = duration
@ -24,7 +24,7 @@ class Slide:
self._updated_at = int(updated_at if updated_at else time.time())
@property
def id(self) -> Optional[str]:
def id(self) -> Optional[int]:
return self._id
@property
@ -101,11 +101,11 @@ class Slide:
@property
def enabled(self) -> bool:
return self._enabled
return bool(self._enabled)
@enabled.setter
def enabled(self, value: bool):
self._enabled = value
self._enabled = bool(value)
@property
def name(self) -> str:

View File

@ -5,14 +5,14 @@ from typing import Optional, Union
class User:
def __init__(self, username: str = '', password: str = '', enabled: bool = True, id: Optional[str] = None):
def __init__(self, username: str = '', password: str = '', enabled: bool = True, id: Optional[int] = None):
self._id = id if id else None
self._username = username
self._password = password
self._enabled = enabled
@property
def id(self) -> Union[int, str]:
def id(self) -> Optional[int]:
return self._id
@property
@ -33,11 +33,11 @@ class User:
@property
def enabled(self) -> bool:
return self._enabled
return bool(self._enabled)
@enabled.setter
def enabled(self, value: bool):
self._enabled = value
self._enabled = bool(value)
def __str__(self) -> str:
return f"User(" \

View File

@ -11,7 +11,7 @@ from src.utils import str_to_enum
class Variable:
def __init__(self, name: str = '', section: str = '', description: str = '', description_edition: str = '', type: Union[VariableType, str] = VariableType.STRING,
value: Union[int, bool, str] = '', editable: bool = True, id: Optional[str] = None,
value: Union[int, bool, str] = '', editable: bool = True, id: Optional[int] = None,
plugin: Optional[str] = None, selectables: Optional[List[Selectable]] = None, unit: Optional[VariableUnit] = None,
refresh_player: bool = False):
self._id = id if id else None
@ -32,7 +32,7 @@ class Variable:
self._unit = None
@property
def id(self) -> Union[int, str]:
def id(self) -> Optional[int]:
return self._id
@property
@ -96,19 +96,19 @@ class Variable:
@property
def editable(self) -> bool:
return self._editable
return bool(self._editable)
@editable.setter
def editable(self, value: bool):
self._editable = value
self._editable = bool(value)
@property
def refresh_player(self) -> bool:
return self._refresh_player
return bool(self._refresh_player)
@refresh_player.setter
def refresh_player(self, value: bool):
self._refresh_player = value
self._refresh_player = bool(value)
@property
def value(self) -> Union[int, bool, str]:
@ -171,10 +171,10 @@ class Variable:
return str(self._value)
def as_int(self) -> int:
return int(self._value)
return int(float(self._value))
def as_ctime(self) -> int:
return time.ctime(self._value)
return time.ctime(int(float(self._value)))
def display(self) -> Union[int, bool, str]:
value = self.eval()

View File

@ -53,5 +53,5 @@ class ModelStore:
def user(self) -> UserManager:
return self._user_manager
def on_user_delete(self, user_id: str):
def on_user_delete(self, user_id: int) -> None:
self._slide_manager.forget_user(user_id)

View File

@ -156,6 +156,9 @@ class PluginStore:
def is_plugin_enabled(self, plugin: ObPlugin) -> bool:
var = self._model_store.variable().get_one_by_name(plugin.get_plugin_variable_name(self.DEFAULT_PLUGIN_ENABLED_VARIABLE))
if var.as_bool:
logging.info("[Plugin] {} enabled".format(plugin.use_title()))
return var.as_bool() if var else False

View File

@ -25,6 +25,7 @@ class TemplateRenderer:
def get_view_globals(self) -> dict:
globals = dict(
STATIC_PREFIX="/{}/{}/".format(WebDirConstant.FOLDER_STATIC, WebDirConstant.FOLDER_STATIC_WEB_ASSETS),
SECRET_KEY=self._model_store.config().map().get('secret_key'),
FLEET_ENABLED=self._model_store.variable().map().get('fleet_enabled').as_bool(),
AUTH_ENABLED=self._model_store.variable().map().get('auth_enabled').as_bool(),
track_created=self._model_store.user().track_user_created,

View File

@ -13,6 +13,20 @@ from cron_descriptor.Exception import FormatException, WrongArgumentException, M
CAMEL_CASE_TO_SNAKE_CASE_PATTERN = re.compile(r'(?<!^)(?=[A-Z])')
def is_wrapped_by(s: str, head: str = '', tail: str = '') -> bool:
return s[0] == head and s[-1] == tail if len(s) > 0 else None
def wrap_if(s: str, condition: bool = True, quote_type: str = "'") -> str:
if not condition or is_wrapped_by(s, quote_type, quote_type):
return s
return "{}{}{}".format(
quote_type,
s,
quote_type
)
def am_i_in_docker():
docker_env = os.path.exists('/.dockerenv')
docker_cgroup = False

View File

@ -1 +1 @@
1.14
1.15

View File

@ -113,6 +113,7 @@
{% endblock %}
</div>
<script>
var secret_key = '{{ SECRET_KEY }}';
var l = {
'js_slideshow_slide_delete_confirmation': '{{ l.slideshow_slide_delete_confirmation }}',
'js_fleet_screen_delete_confirmation': '{{ l.js_fleet_screen_delete_confirmation }}',