add user tracking on slides + add favicon/manifest root urls

This commit is contained in:
jr-k 2024-05-13 01:14:41 +02:00
parent 785c4c04ef
commit 173a4d2417
15 changed files with 374 additions and 89 deletions

View File

@ -106,7 +106,36 @@ header nav ul {
header nav ul li { header nav ul li {
margin: 0 15px; margin: 0 15px;
}
header nav ul li.user-menu {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
border: 1px solid #0bc44e;
padding: 5px 15px;
border-radius: 4px;
background: rgba(14, 239, 95, .2);
}
header nav ul li.user-menu .logout {
color: rgb(255, 255, 255);
}
header nav ul li.user-menu .username {
margin-right: 20px;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
font-weight: bold;
color: #0bc44e;
}
header nav ul li.user-menu .username i {
margin-right: 5px;
font-size: 12px;
} }
header nav ul li a { header nav ul li a {
@ -657,3 +686,20 @@ footer .version {
font-size: 36px; font-size: 36px;
margin: 0 0 40px 0; margin: 0 0 40px 0;
} }
.badge {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 5px 5px;
border-radius: 4px;
font-size: 12px;
background: rgba(255, 255, 255, .1);
color: #ffffff;
}
.panel-inactive .badge {
background: rgba(200, 200, 200, .1);
color: #999999;
}

View File

@ -0,0 +1,22 @@
from flask import Flask, send_file, render_template_string, jsonify
from src.interface.ObController import ObController
class CoreController(ObController):
def register(self):
self._app.add_url_rule('/manifest.json', 'manifest', self.manifest, methods=['GET'])
self._app.add_url_rule('/favicon.ico', 'favicon', self.favicon, methods=['GET'])
def manifest(self):
with open("{}/manifest.jinja.json".format(self.get_template_folder()), 'r') as file:
template_content = file.read()
rendered_content = render_template_string(template_content)
return self._app.response_class(rendered_content, mimetype='application/json')
def favicon(self):
return send_file("{}/favicon.ico".format(self.get_web_folder()), mimetype='image/x-icon')

View File

@ -27,6 +27,12 @@ class ObController(abc.ABC):
return self._plugin return self._plugin
def get_template_folder(self):
return self._web_server.get_template_folder()
def get_web_folder(self):
return self._web_server.get_web_folder()
def reload_web_server(self): def reload_web_server(self):
self._web_server.reload() self._web_server.reload()

View File

@ -4,6 +4,7 @@ from typing import Dict, Optional, List, Tuple, Union
from src.model.entity.Screen import Screen from src.model.entity.Screen import Screen
from src.manager.DatabaseManager import DatabaseManager from src.manager.DatabaseManager import DatabaseManager
from src.manager.LangManager import LangManager from src.manager.LangManager import LangManager
from src.manager.UserManager import UserManager
from src.service.ModelManager import ModelManager from src.service.ModelManager import ModelManager
@ -18,24 +19,21 @@ class ScreenManager(ModelManager):
"port" "port"
] ]
def __init__(self, lang_manager: LangManager, database_manager: DatabaseManager): def __init__(self, lang_manager: LangManager, database_manager: DatabaseManager, user_manager: UserManager):
super().__init__(lang_manager, database_manager) super().__init__(lang_manager, database_manager, user_manager)
self._db = database_manager.open(self.TABLE_NAME, self.TABLE_MODEL) self._db = database_manager.open(self.TABLE_NAME, self.TABLE_MODEL)
@staticmethod def hydrate_object(self, raw_screen: dict, id: Optional[str] = None) -> Screen:
def hydrate_object(raw_screen: dict, id: Optional[str] = None) -> Screen:
if id: if id:
raw_screen['id'] = id raw_screen['id'] = id
return Screen(**raw_screen) return Screen(**raw_screen)
@staticmethod def hydrate_dict(self, raw_screens: dict) -> List[Screen]:
def hydrate_dict(raw_screens: dict) -> List[Screen]: return [self.hydrate_object(raw_screen, raw_id) for raw_id, raw_screen in raw_screens.items()]
return [ScreenManager.hydrate_object(raw_screen, raw_id) for raw_id, raw_screen in raw_screens.items()]
@staticmethod def hydrate_list(self, raw_screens: list) -> List[Screen]:
def hydrate_list(raw_screens: list) -> List[Screen]: return [self.hydrate_object(raw_screen) for raw_screen in raw_screens]
return [ScreenManager.hydrate_object(raw_screen) for raw_screen in raw_screens]
def get(self, id: str) -> Optional[Screen]: def get(self, id: str) -> Optional[Screen]:
try: try:
@ -59,10 +57,10 @@ class ScreenManager(ModelManager):
if isinstance(raw_screens, dict): if isinstance(raw_screens, dict):
if sort: if sort:
return sorted(ScreenManager.hydrate_dict(raw_screens), key=lambda x: x.position) return sorted(self.hydrate_dict(raw_screens), key=lambda x: x.position)
return ScreenManager.hydrate_dict(raw_screens) return self.hydrate_dict(raw_screens)
return ScreenManager.hydrate_list(sorted(raw_screens, key=lambda x: x['position']) if sort else raw_screens) return self.hydrate_list(sorted(raw_screens, key=lambda x: x['position']) if sort else raw_screens)
def get_enabled_screens(self) -> List[Screen]: def get_enabled_screens(self) -> List[Screen]:
return [screen for screen in self.get_all(sort=True) if screen.enabled] return [screen for screen in self.get_all(sort=True) if screen.enabled]

View File

@ -8,6 +8,7 @@ from src.model.enum.SlideType import SlideType
from src.utils import get_optional_string, get_yt_video_id from src.utils import get_optional_string, get_yt_video_id
from src.manager.DatabaseManager import DatabaseManager from src.manager.DatabaseManager import DatabaseManager
from src.manager.LangManager import LangManager from src.manager.LangManager import LangManager
from src.manager.UserManager import UserManager
from src.service.ModelManager import ModelManager from src.service.ModelManager import ModelManager
@ -22,27 +23,33 @@ class SlideManager(ModelManager):
"position", "position",
"location", "location",
"cron_schedule", "cron_schedule",
"cron_schedule_end" "cron_schedule_end",
"created_by",
"updated_by",
"created_at",
"updated_at"
] ]
def __init__(self, lang_manager: LangManager, database_manager: DatabaseManager): def __init__(self, lang_manager: LangManager, database_manager: DatabaseManager, user_manager: UserManager):
super().__init__(lang_manager, database_manager) super().__init__(lang_manager, database_manager, user_manager)
self._db = database_manager.open(self.TABLE_NAME, self.TABLE_MODEL) self._db = database_manager.open(self.TABLE_NAME, self.TABLE_MODEL)
@staticmethod def hydrate_object(self, raw_slide: dict, id: str = None) -> Slide:
def hydrate_object(raw_slide: dict, id: str = None) -> Slide:
if id: if id:
raw_slide['id'] = 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)
return Slide(**raw_slide) return Slide(**raw_slide)
@staticmethod def hydrate_dict(self, raw_slides: dict) -> List[Slide]:
def hydrate_dict(raw_slides: dict) -> List[Slide]: return [self.hydrate_object(raw_slide, raw_id) for raw_id, raw_slide in raw_slides.items()]
return [SlideManager.hydrate_object(raw_slide, raw_id) for raw_id, raw_slide in raw_slides.items()]
@staticmethod def hydrate_list(self, raw_slides: list) -> List[Slide]:
def hydrate_list(raw_slides: list) -> List[Slide]: return [self.hydrate_object(raw_slide) for raw_slide in raw_slides]
return [SlideManager.hydrate_object(raw_slide) for raw_slide in raw_slides]
def get(self, id: str) -> Optional[Slide]: def get(self, id: str) -> Optional[Slide]:
try: try:
@ -66,10 +73,10 @@ class SlideManager(ModelManager):
if isinstance(raw_slides, dict): if isinstance(raw_slides, dict):
if sort: if sort:
return sorted(SlideManager.hydrate_dict(raw_slides), key=lambda x: x.position) return sorted(self.hydrate_dict(raw_slides), key=lambda x: x.position)
return SlideManager.hydrate_dict(raw_slides) return self.hydrate_dict(raw_slides)
return SlideManager.hydrate_list(sorted(raw_slides, key=lambda x: x['position']) if sort else raw_slides) return self.hydrate_list(sorted(raw_slides, key=lambda x: x['position']) if sort else raw_slides)
def get_enabled_slides(self) -> List[Slide]: def get_enabled_slides(self) -> List[Slide]:
return [slide for slide in self.get_all(sort=True) if slide.enabled] return [slide for slide in self.get_all(sort=True) if slide.enabled]
@ -77,8 +84,33 @@ class SlideManager(ModelManager):
def get_disabled_slides(self) -> List[Slide]: def get_disabled_slides(self) -> List[Slide]:
return [slide for slide in self.get_all(sort=True) if not slide.enabled] return [slide for slide in self.get_all(sort=True) if not slide.enabled]
def pre_add(self, slide: Dict) -> Dict:
self.user_manager.track_user_on_create(slide)
self.user_manager.track_user_on_update(slide)
return slide
def pre_update(self, slide: Dict) -> Dict:
self.user_manager.track_user_on_update(slide)
return slide
def pre_delete(self, slide_id: str) -> str:
return slide_id
def post_add(self, slide_id: str) -> str:
return slide_id
def post_update(self, slide_id: str) -> str:
return slide_id
def post_updates(self):
pass
def post_delete(self, slide_id: str) -> str:
return slide_id
def update_enabled(self, id: str, enabled: bool) -> None: def update_enabled(self, id: str, enabled: bool) -> None:
self._db.update_by_id(id, {"enabled": enabled, "position": 999}) self._db.update_by_id(id, self.pre_update({"enabled": enabled, "position": 999}))
self.post_update(id)
def update_positions(self, positions: list) -> None: def update_positions(self, positions: list) -> None:
for slide_id, slide_position in positions.items(): for slide_id, slide_position in positions.items():
@ -103,7 +135,8 @@ class SlideManager(ModelManager):
if slide.type == SlideType.YOUTUBE: if slide.type == SlideType.YOUTUBE:
form['location'] = get_yt_video_id(form['location']) form['location'] = get_yt_video_id(form['location'])
self._db.update_by_id(id, form) self._db.update_by_id(id, self.pre_update(form))
self.post_update(id)
def add_form(self, slide: Union[Slide, Dict]) -> None: def add_form(self, slide: Union[Slide, Dict]) -> None:
form = slide form = slide
@ -115,7 +148,8 @@ class SlideManager(ModelManager):
if form['type'] == SlideType.YOUTUBE: if form['type'] == SlideType.YOUTUBE:
form['location'] = get_yt_video_id(form['location']) form['location'] = get_yt_video_id(form['location'])
self._db.add(form) self._db.add(self.pre_add(form))
self.post_add(slide.id)
def delete(self, id: str) -> None: def delete(self, id: str) -> None:
slide = self.get(id) slide = self.get(id)
@ -127,7 +161,9 @@ class SlideManager(ModelManager):
except FileNotFoundError: except FileNotFoundError:
pass pass
self.pre_delete(id)
self._db.delete_by_id(id) self._db.delete_by_id(id)
self.post_delete(id)
def to_dict(self, slides: List[Slide]) -> List[Dict]: def to_dict(self, slides: List[Slide]) -> List[Dict]:
return [slide.to_dict() for slide in slides] return [slide.to_dict() for slide in slides]

View File

@ -1,14 +1,15 @@
import hashlib import hashlib
import time
from pysondb.errors import IdDoesNotExistError from pysondb.errors import IdDoesNotExistError
from typing import Dict, Optional, List, Tuple, Union from typing import Dict, Optional, List, Tuple, Union
from flask_login import current_user
from src.model.entity.User import User from src.model.entity.User import User
from src.manager.DatabaseManager import DatabaseManager from src.manager.DatabaseManager import DatabaseManager
from src.manager.LangManager import LangManager from src.manager.LangManager import LangManager
from src.service.ModelManager import ModelManager
class UserManager(ModelManager): class UserManager:
TABLE_NAME = "user" TABLE_NAME = "user"
TABLE_MODEL = [ TABLE_MODEL = [
@ -17,24 +18,40 @@ class UserManager(ModelManager):
"enabled" "enabled"
] ]
def __init__(self, lang_manager: LangManager, database_manager: DatabaseManager): def __init__(self, database_manager: DatabaseManager):
super().__init__(lang_manager, database_manager)
self._db = database_manager.open(self.TABLE_NAME, self.TABLE_MODEL) self._db = database_manager.open(self.TABLE_NAME, self.TABLE_MODEL)
self._user_map = {}
self.reload()
def reload(self) -> None:
self._user_map = self.prepare_map()
def map(self) -> dict:
return self._user_map
def prepare_map(self) -> Dict[str, User]:
return self.list_to_map(self.get_all())
@staticmethod @staticmethod
def hydrate_object(raw_user: dict, id: Optional[str] = None) -> User: def list_to_map(list: List[User]) -> Dict[str, User]:
user_map = {}
for user in list:
user_map[user.id] = user
return user_map
def hydrate_object(self, raw_user: dict, id: Optional[str] = None) -> User:
if id: if id:
raw_user['id'] = id raw_user['id'] = id
return User(**raw_user) return User(**raw_user)
@staticmethod def hydrate_dict(self, raw_users: dict) -> List[User]:
def hydrate_dict(raw_users: dict) -> List[User]: return [self.hydrate_object(raw_user, raw_id) for raw_id, raw_user in raw_users.items()]
return [UserManager.hydrate_object(raw_user, raw_id) for raw_id, raw_user in raw_users.items()]
@staticmethod def hydrate_list(self, raw_users: list) -> List[User]:
def hydrate_list(raw_users: list) -> List[User]: return [self.hydrate_object(raw_user) for raw_user in raw_users]
return [UserManager.hydrate_object(raw_user) for raw_user in raw_users]
def get(self, id: str) -> Optional[User]: def get(self, id: str) -> Optional[User]:
try: try:
@ -59,15 +76,33 @@ class UserManager(ModelManager):
def count_all_enabled(self): def count_all_enabled(self):
return len(self.get_enabled_users()) return len(self.get_enabled_users())
def track_user_created(self, id_or_entity: Optional[str]) -> User:
return self.track_user_action(id_or_entity, 'created_by')
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):
id_or_entity = getattr(id_or_entity, attribute)
id_or_entity = str(id_or_entity)
user_map = self.map()
if id_or_entity in user_map:
return user_map[id_or_entity]
return User(username="")
def get_all(self, sort: bool = False) -> List[User]: def get_all(self, sort: bool = False) -> List[User]:
raw_users = self._db.get_all() raw_users = self._db.get_all()
if isinstance(raw_users, dict): if isinstance(raw_users, dict):
if sort: if sort:
return sorted(UserManager.hydrate_dict(raw_users), key=lambda x: x.username) return sorted(self.hydrate_dict(raw_users), key=lambda x: x.username)
return UserManager.hydrate_dict(raw_users) return self.hydrate_dict(raw_users)
return UserManager.hydrate_list(sorted(raw_users, key=lambda x: x['username']) if sort else raw_users) return self.hydrate_list(sorted(raw_users, key=lambda x: x['username']) if sort else raw_users)
def get_enabled_users(self) -> List[User]: def get_enabled_users(self) -> List[User]:
return [user for user in self.get_all(sort=True) if user.enabled] return [user for user in self.get_all(sort=True) if user.enabled]
@ -75,8 +110,30 @@ class UserManager(ModelManager):
def get_disabled_users(self) -> List[User]: def get_disabled_users(self) -> List[User]:
return [user for user in self.get_all(sort=True) if not user.enabled] return [user for user in self.get_all(sort=True) if not user.enabled]
def pre_add(self, user: Dict) -> Dict:
return user
def pre_update(self, user: Dict) -> Dict:
return user
def pre_delete(self, user_id: str) -> str:
return user_id
def post_add(self, user_id: str) -> str:
self.reload()
return user_id
def post_update(self, user_id: str) -> str:
self.reload()
return user_id
def post_delete(self, user_id: str) -> str:
self.reload()
return user_id
def update_enabled(self, id: str, enabled: bool) -> None: def update_enabled(self, id: str, enabled: bool) -> None:
self._db.update_by_id(id, {"enabled": enabled}) self._db.update_by_id(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: str, username: str, password: Optional[str]) -> None:
form = {"username": username} form = {"username": username}
@ -84,7 +141,8 @@ class UserManager(ModelManager):
if password is not None and password: if password is not None and password:
form['password'] = self.encode_password(password) form['password'] = self.encode_password(password)
self._db.update_by_id(id, form) self._db.update_by_id(id, self.pre_update(form))
self.post_update(id)
def add_form(self, user: Union[User, Dict]) -> None: def add_form(self, user: Union[User, Dict]) -> None:
form = user form = user
@ -95,13 +153,57 @@ class UserManager(ModelManager):
form['password'] = self.encode_password(form['password']) form['password'] = self.encode_password(form['password'])
self._db.add(form) self._db.add(self.pre_add(form))
self.post_add(user.id)
def delete(self, id: str) -> None: def delete(self, id: str) -> None:
self.pre_delete(id)
self._db.delete_by_id(id) self._db.delete_by_id(id)
self.post_delete(id)
def to_dict(self, users: List[User]) -> List[Dict]: def to_dict(self, users: List[User]) -> List[Dict]:
return [user.to_dict() for user in users] return [user.to_dict() for user in users]
def encode_password(self, password: str) -> str: def encode_password(self, password: str) -> str:
return hashlib.sha256(password.encode()).hexdigest() return hashlib.sha256(password.encode()).hexdigest()
def initialize_user_trackers(self, object: dict) -> dict:
edits = {}
user_id = self.get_logged_user("id")
now = time.time()
if 'created_by' not in object or not object['created_by']:
object["created_by"] = user_id
edits['created_by'] = object['created_by']
if 'updated_by' not in object or not object['updated_by']:
object["updated_by"] = user_id
edits['updated_by'] = object['updated_by']
if 'created_at' not in object or not object['created_at']:
object["created_at"] = now
edits['created_at'] = object['created_at']
if 'updated_at' not in object or not object['updated_at']:
object["updated_at"] = now
edits['updated_at'] = object['updated_at']
return [object, edits]
def track_user_on_create(self, object: dict) -> dict:
object["created_at"] = time.time()
object["created_by"] = self.get_logged_user("id")
return object
def track_user_on_update(self, object: dict) -> dict:
object["updated_at"] = time.time()
object["updated_by"] = self.get_logged_user("id")
return object
def get_logged_user(self, attribute: Optional[str]) -> Union[User, None, str]:
if current_user and current_user.is_authenticated:
if attribute:
return getattr(current_user, attribute)
return current_user
return None

View File

@ -4,6 +4,7 @@ from pysondb.errors import IdDoesNotExistError
from src.manager.DatabaseManager import DatabaseManager from src.manager.DatabaseManager import DatabaseManager
from src.manager.LangManager import LangManager from src.manager.LangManager import LangManager
from src.manager.UserManager import UserManager
from src.service.ModelManager import ModelManager from src.service.ModelManager import ModelManager
from src.model.entity.Variable import Variable from src.model.entity.Variable import Variable
from src.model.entity.Selectable import Selectable from src.model.entity.Selectable import Selectable
@ -36,8 +37,8 @@ class VariableManager(ModelManager):
"value" "value"
] ]
def __init__(self, lang_manager: LangManager, database_manager: DatabaseManager): def __init__(self, lang_manager: LangManager, database_manager: DatabaseManager, user_manager: UserManager):
super().__init__(lang_manager, database_manager) super().__init__(lang_manager, database_manager, user_manager)
self._db = database_manager.open(self.TABLE_NAME, self.TABLE_MODEL) self._db = database_manager.open(self.TABLE_NAME, self.TABLE_MODEL)
self._var_map = {} self._var_map = {}
self.reload() self.reload()
@ -126,12 +127,12 @@ class VariableManager(ModelManager):
for default_var in default_vars: for default_var in default_vars:
self.set_variable(**default_var) self.set_variable(**default_var)
self._var_map = self.prepare_variable_map() self._var_map = self.prepare_map()
def map(self) -> dict: def map(self) -> dict:
return self._var_map return self._var_map
def prepare_variable_map(self) -> Dict[str, Variable]: def prepare_map(self) -> Dict[str, Variable]:
return self.list_to_map(self.get_all()) return self.list_to_map(self.get_all())
@staticmethod @staticmethod
@ -143,8 +144,7 @@ class VariableManager(ModelManager):
return var_map return var_map
@staticmethod def hydrate_object(self, raw_variable: dict, id: Optional[str] = None) -> Variable:
def hydrate_object(raw_variable: dict, id: Optional[str] = None) -> Variable:
if id: if id:
raw_variable['id'] = id raw_variable['id'] = id
@ -153,13 +153,11 @@ class VariableManager(ModelManager):
return Variable(**raw_variable) return Variable(**raw_variable)
@staticmethod def hydrate_dict(self, raw_variables: dict) -> List[Variable]:
def hydrate_dict(raw_variables: dict) -> List[Variable]: return [self.hydrate_object(raw_variable, raw_id) for raw_id, raw_variable in raw_variables.items()]
return [VariableManager.hydrate_object(raw_variable, raw_id) for raw_id, raw_variable in raw_variables.items()]
@staticmethod def hydrate_list(self, raw_variables: list) -> List[Variable]:
def hydrate_list(raw_variables: list) -> List[Variable]: return [self.hydrate_object(raw_variable) for raw_variable in raw_variables]
return [VariableManager.hydrate_object(raw_variable) for raw_variable in raw_variables]
def get(self, id: str) -> Optional[Variable]: def get(self, id: str) -> Optional[Variable]:
try: try:
@ -192,9 +190,9 @@ class VariableManager(ModelManager):
raw_variables = self._db.get_all() raw_variables = self._db.get_all()
if isinstance(raw_variables, dict): if isinstance(raw_variables, dict):
return VariableManager.hydrate_dict(raw_variables) return self.hydrate_dict(raw_variables)
return VariableManager.hydrate_list(raw_variables) return self.hydrate_list(raw_variables)
def get_editable_variables(self, plugin: bool = True, sort: Optional[str] = None) -> List[Variable]: 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)) query = lambda v: (not plugin and not isinstance(v['plugin'], str)) or (plugin and isinstance(v['plugin'], str))

View File

@ -1,4 +1,5 @@
import json import json
import time
from typing import Optional, Union from typing import Optional, Union
from src.model.enum.SlideType import SlideType, SlideInputType from src.model.enum.SlideType import SlideType, SlideInputType
@ -7,7 +8,7 @@ from src.utils import str_to_enum
class Slide: 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): 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):
self._id = id if id else None self._id = id if id else None
self._location = location self._location = location
self._duration = duration self._duration = duration
@ -17,6 +18,10 @@ class Slide:
self._position = position self._position = position
self._cron_schedule = cron_schedule self._cron_schedule = cron_schedule
self._cron_schedule_end = cron_schedule_end self._cron_schedule_end = cron_schedule_end
self._created_by = int(created_by if created_by else time.time())
self._updated_by = updated_by
self._created_at = int(created_at if created_at else time.time())
self._updated_at = int(updated_at if updated_at else time.time())
@property @property
def id(self) -> Optional[str]: def id(self) -> Optional[str]:
@ -30,6 +35,38 @@ class Slide:
def location(self, value: str): def location(self, value: str):
self._location = value self._location = value
@property
def created_by(self) -> str:
return self._created_by
@created_by.setter
def created_by(self, value: str):
self._created_by = value
@property
def updated_by(self) -> str:
return self._updated_by
@updated_by.setter
def updated_by(self, value: str):
self._updated_by = value
@property
def created_at(self) -> int:
return self._created_at
@created_at.setter
def created_at(self, value: int):
self._created_at = value
@property
def updated_at(self) -> int:
return self._updated_at
@updated_at.setter
def updated_at(self, value: int):
self._updated_at = value
@property @property
def type(self) -> SlideType: def type(self) -> SlideType:
return self._type return self._type
@ -95,6 +132,10 @@ class Slide:
f"duration='{self.duration}',\n" \ f"duration='{self.duration}',\n" \
f"position='{self.position}',\n" \ f"position='{self.position}',\n" \
f"location='{self.location}',\n" \ f"location='{self.location}',\n" \
f"created_by='{self.created_by}',\n" \
f"updated_by='{self.updated_by}',\n" \
f"created_at='{self.created_at}',\n" \
f"updated_at='{self.updated_at}',\n" \
f"cron_schedule='{self.cron_schedule}',\n" \ f"cron_schedule='{self.cron_schedule}',\n" \
f"cron_schedule_end='{self.cron_schedule_end}',\n" \ f"cron_schedule_end='{self.cron_schedule_end}',\n" \
f")" f")"
@ -111,6 +152,10 @@ class Slide:
"type": self.type.value, "type": self.type.value,
"duration": self.duration, "duration": self.duration,
"location": self.location, "location": self.location,
"created_by": self.created_by,
"updated_by": self.updated_by,
"created_at": self.created_at,
"updated_at": self.updated_at,
"cron_schedule": self.cron_schedule, "cron_schedule": self.cron_schedule,
"cron_schedule_end": self.cron_schedule_end, "cron_schedule_end": self.cron_schedule_end,
} }

View File

@ -1,15 +1,18 @@
from enum import Enum from enum import Enum
from typing import Union, Dict from typing import Union, Dict, Optional
from src.model.entity.User import User
from src.manager.LangManager import LangManager from src.manager.LangManager import LangManager
from src.manager.UserManager import UserManager
from src.manager.DatabaseManager import DatabaseManager from src.manager.DatabaseManager import DatabaseManager
class ModelManager: class ModelManager:
def __init__(self, lang_manager: LangManager, database_manager: DatabaseManager): def __init__(self, lang_manager: LangManager, database_manager: DatabaseManager, user_manager: UserManager):
self._lang_manager = lang_manager self._lang_manager = lang_manager
self._database_manager = database_manager self._database_manager = database_manager
self._user_manager = user_manager
def t(self, token) -> Union[Dict, str]: def t(self, token) -> Union[Dict, str]:
return self.lang_manager.translate(token) return self.lang_manager.translate(token)
@ -21,3 +24,9 @@ class ModelManager:
@property @property
def database_manager(self) -> DatabaseManager: def database_manager(self) -> DatabaseManager:
return self._database_manager return self._database_manager
@property
def user_manager(self) -> UserManager:
return self._user_manager

View File

@ -16,7 +16,8 @@ class ModelStore:
self._database_manager = DatabaseManager() self._database_manager = DatabaseManager()
# Dynamics # Dynamics
self._variable_manager = VariableManager(lang_manager=self._lang_manager, database_manager=self._database_manager) self._user_manager = UserManager(database_manager=self._database_manager)
self._variable_manager = VariableManager(lang_manager=self._lang_manager, database_manager=self._database_manager, user_manager=self._user_manager)
self._lang_manager.set_lang(self.variable().map().get('lang').as_string()) self._lang_manager.set_lang(self.variable().map().get('lang').as_string())
# Core # Core
@ -24,9 +25,8 @@ class ModelStore:
self._logging_manager = LoggingManager(config_manager=self._config_manager) self._logging_manager = LoggingManager(config_manager=self._config_manager)
# Model # Model
self._user_manager = UserManager(lang_manager=self._lang_manager, database_manager=self._database_manager) self._screen_manager = ScreenManager(lang_manager=self._lang_manager, database_manager=self._database_manager, user_manager=self._user_manager)
self._screen_manager = ScreenManager(lang_manager=self._lang_manager, database_manager=self._database_manager) self._slide_manager = SlideManager(lang_manager=self._lang_manager, database_manager=self._database_manager, user_manager=self._user_manager)
self._slide_manager = SlideManager(lang_manager=self._lang_manager, database_manager=self._database_manager)
self._variable_manager.reload() self._variable_manager.reload()
def logging(self) -> LoggingManager: def logging(self) -> LoggingManager:

View File

@ -27,6 +27,8 @@ class TemplateRenderer:
STATIC_PREFIX="/{}/{}/".format(WebDirConstant.FOLDER_STATIC, WebDirConstant.FOLDER_STATIC_WEB_ASSETS), STATIC_PREFIX="/{}/{}/".format(WebDirConstant.FOLDER_STATIC, WebDirConstant.FOLDER_STATIC_WEB_ASSETS),
FLEET_ENABLED=self._model_store.variable().map().get('fleet_enabled').as_bool(), FLEET_ENABLED=self._model_store.variable().map().get('fleet_enabled').as_bool(),
AUTH_ENABLED=self._model_store.variable().map().get('auth_enabled').as_bool(), AUTH_ENABLED=self._model_store.variable().map().get('auth_enabled').as_bool(),
track_created=self._model_store.user().track_user_created,
track_updated=self._model_store.user().track_user_updated,
VERSION=self._model_store.config().map().get('version'), VERSION=self._model_store.config().map().get('version'),
LANG=self._model_store.variable().map().get('lang').as_string(), LANG=self._model_store.variable().map().get('lang').as_string(),
HOOK=self._render_hook, HOOK=self._render_hook,

View File

@ -15,6 +15,7 @@ from src.controller.FleetController import FleetController
from src.controller.AuthController import AuthController from src.controller.AuthController import AuthController
from src.controller.SysinfoController import SysinfoController from src.controller.SysinfoController import SysinfoController
from src.controller.SettingsController import SettingsController from src.controller.SettingsController import SettingsController
from src.controller.CoreController import CoreController
from src.constant.WebDirConstant import WebDirConstant from src.constant.WebDirConstant import WebDirConstant
@ -50,17 +51,20 @@ class WebServer:
def get_app(self): def get_app(self):
return self._app return self._app
def _get_template_folder(self) -> str: def get_template_folder(self) -> str:
return "{}/{}".format(self._project_dir, WebDirConstant.FOLDER_TEMPLATES) return "{}/{}".format(self._project_dir, WebDirConstant.FOLDER_TEMPLATES)
def _get_static_folder(self) -> str: def get_static_folder(self) -> str:
return "{}/{}".format(self._project_dir, WebDirConstant.FOLDER_STATIC) return "{}/{}".format(self._project_dir, WebDirConstant.FOLDER_STATIC)
def get_web_folder(self) -> str:
return "{}/{}/{}".format(self._project_dir, WebDirConstant.FOLDER_STATIC, WebDirConstant.FOLDER_STATIC_WEB_ASSETS)
def _setup_flask_app(self) -> None: def _setup_flask_app(self) -> None:
self._app = Flask( self._app = Flask(
__name__, __name__,
template_folder=self._get_template_folder(), template_folder=self.get_template_folder(),
static_folder=self._get_static_folder(), static_folder=self.get_static_folder(),
) )
self._app.config['UPLOAD_FOLDER'] = "{}/{}".format(WebDirConstant.FOLDER_STATIC, WebDirConstant.FOLDER_STATIC_WEB_UPLOADS) self._app.config['UPLOAD_FOLDER'] = "{}/{}".format(WebDirConstant.FOLDER_STATIC, WebDirConstant.FOLDER_STATIC_WEB_UPLOADS)
@ -96,6 +100,7 @@ class WebServer:
return decorated_function return decorated_function
CoreController(self, self._app, auth_required, self._model_store, self._template_renderer)
PlayerController(self, self._app, auth_required, self._model_store, self._template_renderer) PlayerController(self, self._app, auth_required, self._model_store, self._template_renderer)
SlideshowController(self, self._app, auth_required, self._model_store, self._template_renderer) SlideshowController(self, self._app, auth_required, self._model_store, self._template_renderer)
SettingsController(self, self._app, auth_required, self._model_store, self._template_renderer) SettingsController(self, self._app, auth_required, self._model_store, self._template_renderer)
@ -111,5 +116,5 @@ class WebServer:
def _setup_web_errors(self) -> None: def _setup_web_errors(self) -> None:
@self._app.errorhandler(404) @self._app.errorhandler(404)
def not_found(e): def not_found(e):
return send_from_directory(self._get_template_folder(), 'core/error404.html'), 404 return send_from_directory(self.get_template_folder(), 'core/error404.html'), 404

View File

@ -6,7 +6,7 @@
</title> </title>
<meta name="robots" content="noindex, nofollow"> <meta name="robots" content="noindex, nofollow">
<meta name="google" content="notranslate"> <meta name="google" content="notranslate">
<link rel="shortcut icon" href="{{ STATIC_PREFIX }}/favicon.ico"> <link rel="shortcut icon" href="/favicon.ico">
<link rel="apple-touch-icon" sizes="57x57" href="{{ STATIC_PREFIX }}favicon/apple-icon-57x57.png"> <link rel="apple-touch-icon" sizes="57x57" href="{{ STATIC_PREFIX }}favicon/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="{{ STATIC_PREFIX }}favicon/apple-icon-60x60.png"> <link rel="apple-touch-icon" sizes="60x60" href="{{ STATIC_PREFIX }}favicon/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="{{ STATIC_PREFIX }}favicon/apple-icon-72x72.png"> <link rel="apple-touch-icon" sizes="72x72" href="{{ STATIC_PREFIX }}favicon/apple-icon-72x72.png">
@ -20,7 +20,7 @@
<link rel="icon" type="image/png" sizes="32x32" href="{{ STATIC_PREFIX }}favicon/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="{{ STATIC_PREFIX }}favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="{{ STATIC_PREFIX }}favicon/favicon-96x96.png"> <link rel="icon" type="image/png" sizes="96x96" href="{{ STATIC_PREFIX }}favicon/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ STATIC_PREFIX }}favicon/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="{{ STATIC_PREFIX }}favicon/favicon-16x16.png">
<link rel="manifest" href="{{ STATIC_PREFIX }}/manifest.json"> <link rel="manifest" href="/manifest.json">
<meta name="msapplication-TileColor" content="#692fbd"> <meta name="msapplication-TileColor" content="#692fbd">
<meta name="msapplication-TileImage" content="{{ STATIC_PREFIX }}favicon/ms-icon-144x144.png"> <meta name="msapplication-TileImage" content="{{ STATIC_PREFIX }}favicon/ms-icon-144x144.png">
<meta name="theme-color" content="#692fbd"> <meta name="theme-color" content="#692fbd">
@ -81,8 +81,12 @@
</a> </a>
</li> </li>
{% if AUTH_ENABLED %} {% if AUTH_ENABLED %}
<li> <li class="user-menu">
<a href="{{ url_for('logout') }}" title="{{ l.logout }}"> <div class="username">
<i class="fa fa-user"></i>
{{ current_user.username }}
</div>
<a href="{{ url_for('logout') }}" title="{{ l.logout }}" class="logout">
<i class="fa fa-right-from-bracket"></i> <i class="fa fa-right-from-bracket"></i>
</a> </a>
</li> </li>

View File

@ -1,38 +1,38 @@
{ {
"name": "App", "name": "Obscreen",
"icons": [ "icons": [
{ {
"src": "\/favicon/android-icon-36x36.png", "src": "{{ STATIC_PREFIX }}favicon/android-icon-36x36.png",
"sizes": "36x36", "sizes": "36x36",
"type": "image\/png", "type": "image\/png",
"density": "0.75" "density": "0.75"
}, },
{ {
"src": "\/favicon/android-icon-48x48.png", "src": "{{ STATIC_PREFIX }}favicon/android-icon-48x48.png",
"sizes": "48x48", "sizes": "48x48",
"type": "image\/png", "type": "image\/png",
"density": "1.0" "density": "1.0"
}, },
{ {
"src": "\/favicon/android-icon-72x72.png", "src": "{{ STATIC_PREFIX }}favicon/android-icon-72x72.png",
"sizes": "72x72", "sizes": "72x72",
"type": "image\/png", "type": "image\/png",
"density": "1.5" "density": "1.5"
}, },
{ {
"src": "\/favicon/android-icon-96x96.png", "src": "{{ STATIC_PREFIX }}favicon/android-icon-96x96.png",
"sizes": "96x96", "sizes": "96x96",
"type": "image\/png", "type": "image\/png",
"density": "2.0" "density": "2.0"
}, },
{ {
"src": "\/favicon/android-icon-144x144.png", "src": "{{ STATIC_PREFIX }}favicon/android-icon-144x144.png",
"sizes": "144x144", "sizes": "144x144",
"type": "image\/png", "type": "image\/png",
"density": "3.0" "density": "3.0"
}, },
{ {
"src": "\/favicon/android-icon-192x192.png", "src": "{{ STATIC_PREFIX }}favicon/android-icon-192x192.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image\/png", "type": "image\/png",
"density": "4.0" "density": "4.0"

View File

@ -2,9 +2,14 @@
<thead> <thead>
<tr> <tr>
<th>{{ l.slideshow_slide_panel_th_name }}</th> <th>{{ l.slideshow_slide_panel_th_name }}</th>
<th class="tac">{{ l.slideshow_slide_panel_th_duration }}</th> {% if AUTH_ENABLED %}
<th class="tac">
<i class="fa fa-user"></i>
</th>
{% endif %}
<th class="tac">{{ l.slideshow_slide_panel_th_enabled }}</th> <th class="tac">{{ l.slideshow_slide_panel_th_enabled }}</th>
<th class="">{{ l.slideshow_slide_panel_th_cron_scheduled }}</th> <th class="">{{ l.slideshow_slide_panel_th_cron_scheduled }}</th>
<th class="tac">{{ l.slideshow_slide_panel_th_duration }}</th>
<th class="tac">{{ l.slideshow_slide_panel_th_activity }}</th> <th class="tac">{{ l.slideshow_slide_panel_th_activity }}</th>
</tr> </tr>
</thead> </thead>
@ -35,9 +40,13 @@
{{ slide.name }} {{ slide.name }}
</div> </div>
</td> </td>
<td class="tac"> {% if AUTH_ENABLED %}
{{ slide.duration }} {{ l.slideshow_slide_panel_th_duration_unit }} <td class="tac">
</td> <div class="badge">
{{ track_updated(slide).username }}
</div>
</td>
{% endif %}
<td class="tac"> <td class="tac">
<label class="pure-material-switch"> <label class="pure-material-switch">
<input type="checkbox" {% if slide.enabled %}checked="checked"{% endif %}><span></span> <input type="checkbox" {% if slide.enabled %}checked="checked"{% endif %}><span></span>
@ -59,6 +68,9 @@
🔄 {{ l.slideshow_slide_panel_td_cron_scheduled_loop }} 🔄 {{ l.slideshow_slide_panel_td_cron_scheduled_loop }}
{% endif %} {% endif %}
</td> </td>
<td class="tac">
{{ slide.duration }} {{ l.slideshow_slide_panel_th_duration_unit }}
</td>
<td class="actions tac"> <td class="actions tac">
<a href="javascript:void(0);" class="item-edit slide-edit"> <a href="javascript:void(0);" class="item-edit slide-edit">
<i class="fa fa-pencil"></i> <i class="fa fa-pencil"></i>