wip swagger
This commit is contained in:
parent
46a05cde84
commit
c7fc29ba99
@ -1,38 +1,78 @@
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
from flask import Flask, request, jsonify, abort
|
||||
from werkzeug.utils import secure_filename
|
||||
from src.service.ModelStore import ModelStore
|
||||
from flask import request, abort, jsonify
|
||||
from flask_restx import Resource, Namespace, fields
|
||||
from src.model.entity.Content import Content
|
||||
from src.manager.FolderManager import FolderManager
|
||||
from src.model.enum.ContentType import ContentType
|
||||
from src.model.enum.FolderEntity import FolderEntity, FOLDER_ROOT_PATH
|
||||
from src.interface.ObController import ObController
|
||||
from src.service.ExternalStorageServer import ExternalStorageServer
|
||||
from src.util.utils import str_to_enum, get_optional_string
|
||||
from src.util.UtilFile import randomize_filename
|
||||
from src.util.utils import str_to_enum
|
||||
from plugins.system.CoreApi.exception.ContentPathMissingException import ContentPathMissingException
|
||||
from plugins.system.CoreApi.exception.ContentNotFoundException import ContentNotFoundException
|
||||
from plugins.system.CoreApi.exception.FolderNotFoundException import FolderNotFoundException
|
||||
from plugins.system.CoreApi.exception.FolderNotEmptyException import FolderNotEmptyException
|
||||
|
||||
|
||||
# Namespace for content operations
|
||||
content_ns = Namespace('content', description='Operations related to content management')
|
||||
|
||||
# Input model for adding/updating content
|
||||
content_model = content_ns.model('Content', {
|
||||
'name': fields.String(required=True, description='Name of the content'),
|
||||
'type': fields.String(required=True, description='Type of the content'),
|
||||
'location': fields.String(description='Location of the content (optional)'),
|
||||
'path': fields.String(description='Path of the folder')
|
||||
})
|
||||
|
||||
# Output model for content
|
||||
content_output_model = content_ns.model('ContentOutput', {
|
||||
'id': fields.Integer(readOnly=True, description='Unique identifier of the content'),
|
||||
'name': fields.String(description='Name of the content'),
|
||||
'type': fields.String(description='Type of the content'),
|
||||
'location': fields.String(description='Location of the content'),
|
||||
'folder_id': fields.Integer(description='Folder ID where the content is stored')
|
||||
})
|
||||
|
||||
# Model for folder operations
|
||||
folder_model = content_ns.model('Folder', {
|
||||
'name': fields.String(required=True, description='Name of the folder'),
|
||||
'path': fields.String(description='Path of the folder')
|
||||
})
|
||||
|
||||
# Model for bulk operations
|
||||
bulk_move_model = content_ns.model('BulkMove', {
|
||||
'entity_ids': fields.List(fields.Integer, required=True, description='List of content IDs to move'),
|
||||
'path': fields.String(description='Path of the folder')
|
||||
})
|
||||
|
||||
|
||||
class ContentApiController(ObController):
|
||||
|
||||
def register(self):
|
||||
self._app.add_url_rule('/api/content', 'api_content_list', self.list_content, methods=['GET'])
|
||||
self._app.add_url_rule('/api/content', 'api_content_add', self.add_content, methods=['POST'])
|
||||
self._app.add_url_rule('/api/content/<int:content_id>', 'api_content_get', self.get_content, methods=['GET'])
|
||||
self._app.add_url_rule('/api/content/<int:content_id>', 'api_content_update', self.update_content, methods=['PUT'])
|
||||
self._app.add_url_rule('/api/content/<int:content_id>', 'api_content_delete', self.delete_content, methods=['DELETE'])
|
||||
self._app.add_url_rule('/api/content/location/<int:content_id>', 'api_content_location', self.location_content, methods=['GET'])
|
||||
self._app.add_url_rule('/api/content/upload-bulk', 'api_content_upload_bulk', self.upload_bulk_content, methods=['POST'])
|
||||
self._app.add_url_rule('/api/content/folder/move-bulk', 'api_folder_content_bulk_move', self.move_bulk_content_folder, methods=['POST'])
|
||||
self._app.add_url_rule('/api/content/folder', 'api_folder_add', self.add_folder, methods=['POST'])
|
||||
self._app.add_url_rule('/api/content/folder', 'api_folder_delete', self.delete_folder, methods=['DELETE'])
|
||||
self._app.add_url_rule('/api/content/folder', 'api_folder_update', self.update_folder, methods=['PUT'])
|
||||
# - self._app.add_url_rule('/api/content', 'api_content_list', self.list_content, methods=['GET'])
|
||||
# - self._app.add_url_rule('/api/content', 'api_content_add', self.add_content, methods=['POST'])
|
||||
# - self._app.add_url_rule('/api/content/<int:content_id>', 'api_content_get', self.get_content, methods=['GET'])
|
||||
# - self._app.add_url_rule('/api/content/<int:content_id>', 'api_content_update', self.update_content, methods=['PUT'])
|
||||
# - self._app.add_url_rule('/api/content/<int:content_id>', 'api_content_delete', self.delete_content, methods=['DELETE'])
|
||||
# - self._app.add_url_rule('/api/content/location/<int:content_id>', 'api_content_location', self.location_content, methods=['GET'])
|
||||
# - self._app.add_url_rule('/api/content/upload-bulk', 'api_content_upload_bulk', self.upload_bulk_content, methods=['POST'])
|
||||
# - self._app.add_url_rule('/api/content/folder/move-bulk', 'api_folder_content_bulk_move', self.move_bulk_content_folder, methods=['POST'])
|
||||
# - self._app.add_url_rule('/api/content/folder', 'api_folder_add', self.add_folder, methods=['POST'])
|
||||
# - self._app.add_url_rule('/api/content/folder', 'api_folder_delete', self.delete_folder, methods=['DELETE'])
|
||||
# - self._app.add_url_rule('/api/content/folder', 'api_folder_update', self.update_folder, methods=['PUT'])
|
||||
self.api().add_namespace(content_ns, path='/api/content')
|
||||
content_ns.add_resource(self.create_resource(ContentListResource), '/')
|
||||
content_ns.add_resource(self.create_resource(ContentResource), '/<int:content_id>')
|
||||
content_ns.add_resource(self.create_resource(ContentLocationResource), '/location/<int:content_id>')
|
||||
content_ns.add_resource(self.create_resource(ContentBulkUploadResource), '/upload-bulk')
|
||||
content_ns.add_resource(self.create_resource(FolderBulkMoveResource), '/folder/move-bulk')
|
||||
content_ns.add_resource(self.create_resource(FolderResource), '/folder')
|
||||
|
||||
def create_resource(self, resource_class):
|
||||
# Function to inject dependencies into resources
|
||||
return type(f'{resource_class.__name__}WithDependencies', (resource_class,), {
|
||||
'_model_store': self._model_store,
|
||||
'_controller': self
|
||||
})
|
||||
|
||||
def get_request_data(self):
|
||||
data = {}
|
||||
@ -67,14 +107,19 @@ class ContentApiController(ObController):
|
||||
|
||||
return FOLDER_ROOT_PATH if is_root_drive else path, folder
|
||||
|
||||
def list_content(self):
|
||||
data = self.get_request_data()
|
||||
|
||||
class ContentListResource(Resource):
|
||||
|
||||
@content_ns.marshal_list_with(content_output_model)
|
||||
def get(self):
|
||||
"""List all contents"""
|
||||
data = self._controller.get_request_data()
|
||||
working_folder_path = None
|
||||
working_folder = None
|
||||
folder_id = None
|
||||
|
||||
try:
|
||||
working_folder_path, working_folder = self.get_folder_context()
|
||||
working_folder_path, working_folder = self._controller.get_folder_context()
|
||||
folder_id = data.get('folder_id', 0 if not working_folder else working_folder.id)
|
||||
except ContentPathMissingException:
|
||||
pass
|
||||
@ -85,15 +130,18 @@ class ContentApiController(ObController):
|
||||
)
|
||||
result = [content.to_dict() for content in contents]
|
||||
|
||||
return jsonify({
|
||||
return {
|
||||
'contents': result,
|
||||
'working_folder_path': working_folder_path,
|
||||
'working_folder': working_folder.to_dict() if working_folder else None
|
||||
})
|
||||
}
|
||||
|
||||
def add_content(self):
|
||||
data = self.get_request_data()
|
||||
working_folder_path, working_folder = self.get_folder_context()
|
||||
@content_ns.expect(content_model)
|
||||
@content_ns.marshal_with(content_output_model, code=201)
|
||||
def post(self):
|
||||
"""Add new content"""
|
||||
data = self._controller.get_request_data()
|
||||
working_folder_path, working_folder = self._controller.get_folder_context()
|
||||
|
||||
if 'name' not in data:
|
||||
abort(400, description="Name is required")
|
||||
@ -111,7 +159,7 @@ class ContentApiController(ObController):
|
||||
name=data.get('name'),
|
||||
type=content_type,
|
||||
request_files=request.files,
|
||||
upload_dir=self._app.config['UPLOAD_FOLDER'],
|
||||
upload_dir=self._controller._app.config['UPLOAD_FOLDER'],
|
||||
location=location,
|
||||
folder_id=working_folder.id if working_folder else None
|
||||
)
|
||||
@ -119,17 +167,25 @@ class ContentApiController(ObController):
|
||||
if not content:
|
||||
abort(400, description="Failed to add content")
|
||||
|
||||
return jsonify(content.to_dict()), 201
|
||||
return content.to_dict(), 201
|
||||
|
||||
def get_content(self, content_id: int):
|
||||
|
||||
class ContentResource(Resource):
|
||||
|
||||
@content_ns.marshal_with(content_output_model)
|
||||
def get(self, content_id: int):
|
||||
"""Get content by ID"""
|
||||
content = self._model_store.content().get(content_id)
|
||||
if not content:
|
||||
raise ContentNotFoundException()
|
||||
|
||||
return jsonify(content.to_dict())
|
||||
return content.to_dict()
|
||||
|
||||
def update_content(self, content_id: int):
|
||||
data = self.get_request_data()
|
||||
@content_ns.expect(content_model)
|
||||
@content_ns.marshal_with(content_output_model)
|
||||
def put(self, content_id: int):
|
||||
"""Update existing content"""
|
||||
data = self._controller.get_request_data()
|
||||
content = self._model_store.content().get(content_id)
|
||||
|
||||
if not content:
|
||||
@ -143,11 +199,12 @@ class ContentApiController(ObController):
|
||||
name=data.get('name'),
|
||||
)
|
||||
|
||||
self._post_update()
|
||||
self._controller._post_update()
|
||||
|
||||
return jsonify(content.to_dict())
|
||||
return content.to_dict()
|
||||
|
||||
def delete_content(self, content_id: int):
|
||||
def delete(self, content_id: int):
|
||||
"""Delete content"""
|
||||
content = self._model_store.content().get(content_id)
|
||||
|
||||
if not content:
|
||||
@ -157,11 +214,15 @@ class ContentApiController(ObController):
|
||||
abort(400, description="Content is referenced in slides")
|
||||
|
||||
self._model_store.content().delete(content.id)
|
||||
self._post_update()
|
||||
self._controller._post_update()
|
||||
|
||||
return jsonify({'status': 'ok'}), 204
|
||||
return {'status': 'ok'}, 204
|
||||
|
||||
def location_content(self, content_id: int):
|
||||
|
||||
class ContentLocationResource(Resource):
|
||||
|
||||
def get(self, content_id: int):
|
||||
"""Get content location by ID"""
|
||||
content = self._model_store.content().get(content_id)
|
||||
|
||||
if not content:
|
||||
@ -169,10 +230,14 @@ class ContentApiController(ObController):
|
||||
|
||||
content_location = self._model_store.content().resolve_content_location(content)
|
||||
|
||||
return jsonify({'location': content_location})
|
||||
return {'location': content_location}
|
||||
|
||||
def upload_bulk_content(self):
|
||||
working_folder_path, working_folder = self.get_folder_context()
|
||||
|
||||
class ContentBulkUploadResource(Resource):
|
||||
|
||||
def post(self):
|
||||
"""Upload multiple content files"""
|
||||
working_folder_path, working_folder = self._controller.get_folder_context()
|
||||
|
||||
for key in request.files:
|
||||
files = request.files.getlist(key)
|
||||
@ -185,15 +250,20 @@ class ContentApiController(ObController):
|
||||
name=name,
|
||||
type=content_type,
|
||||
request_files=file,
|
||||
upload_dir=self._app.config['UPLOAD_FOLDER'],
|
||||
upload_dir=self._controller._app.config['UPLOAD_FOLDER'],
|
||||
folder_id=working_folder.id if working_folder else None
|
||||
)
|
||||
|
||||
return jsonify({'status': 'ok'}), 201
|
||||
return {'status': 'ok'}, 201
|
||||
|
||||
def move_bulk_content_folder(self):
|
||||
data = self.get_request_data()
|
||||
working_folder_path, working_folder = self.get_folder_context()
|
||||
|
||||
class FolderBulkMoveResource(Resource):
|
||||
|
||||
@content_ns.expect(bulk_move_model)
|
||||
def post(self):
|
||||
"""Move multiple content to another folder"""
|
||||
data = self._controller.get_request_data()
|
||||
working_folder_path, working_folder = self._controller.get_folder_context()
|
||||
|
||||
if 'entity_ids' not in data:
|
||||
abort(400, description="Content IDs are required under 'entity_ids' field")
|
||||
@ -208,11 +278,17 @@ class ContentApiController(ObController):
|
||||
entity=FolderEntity.CONTENT
|
||||
)
|
||||
|
||||
return jsonify({'status': 'ok'})
|
||||
return {'status': 'ok'}
|
||||
|
||||
def add_folder(self):
|
||||
data = self.get_request_data()
|
||||
working_folder_path, working_folder = self.get_folder_context()
|
||||
|
||||
class FolderResource(Resource):
|
||||
|
||||
@content_ns.expect(folder_model)
|
||||
@content_ns.marshal_with(folder_model, code=201)
|
||||
def post(self):
|
||||
"""Add a new folder"""
|
||||
data = self._controller.get_request_data()
|
||||
working_folder_path, working_folder = self._controller.get_folder_context()
|
||||
|
||||
if 'name' not in data:
|
||||
abort(400, description="Name is required")
|
||||
@ -223,13 +299,14 @@ class ContentApiController(ObController):
|
||||
working_folder_path=working_folder_path
|
||||
)
|
||||
|
||||
return jsonify(folder.to_dict()), 201
|
||||
return folder.to_dict(), 201
|
||||
|
||||
def delete_folder(self):
|
||||
working_folder_path, working_folder = self.get_folder_context()
|
||||
def delete(self):
|
||||
"""Delete a folder"""
|
||||
working_folder_path, working_folder = self._controller.get_folder_context()
|
||||
|
||||
if not working_folder:
|
||||
abort(400, description="You can't update this folder")
|
||||
abort(400, description="You can't delete this folder")
|
||||
|
||||
content_counter = self._model_store.content().count_contents_for_folder(working_folder.id)
|
||||
folder_counter = self._model_store.folder().count_subfolders_for_folder(working_folder.id)
|
||||
@ -238,13 +315,15 @@ class ContentApiController(ObController):
|
||||
raise FolderNotEmptyException()
|
||||
|
||||
self._model_store.folder().delete(id=working_folder.id)
|
||||
self._post_update()
|
||||
self._controller._post_update()
|
||||
|
||||
return jsonify({'status': 'ok'}), 204
|
||||
return {'status': 'ok'}, 204
|
||||
|
||||
def update_folder(self):
|
||||
data = self.get_request_data()
|
||||
working_folder_path, working_folder = self.get_folder_context()
|
||||
@content_ns.expect(folder_model)
|
||||
def put(self):
|
||||
"""Update a folder"""
|
||||
data = self._controller.get_request_data()
|
||||
working_folder_path, working_folder = self._controller.get_folder_context()
|
||||
|
||||
if 'name' not in data:
|
||||
abort(400, description="Name is required")
|
||||
@ -257,7 +336,5 @@ class ContentApiController(ObController):
|
||||
name=data.get('name')
|
||||
)
|
||||
|
||||
return jsonify({'status': 'ok'})
|
||||
return {'status': 'ok'}
|
||||
|
||||
def _post_update(self):
|
||||
self._model_store.variable().update_by_name("last_content_update", time.time())
|
||||
|
||||
@ -1,32 +1,55 @@
|
||||
from flask import Flask, render_template, jsonify, request, abort, make_response
|
||||
|
||||
from flask import request, abort, jsonify
|
||||
from flask_restx import Resource, Namespace, fields
|
||||
from src.model.entity.Playlist import Playlist
|
||||
from src.interface.ObController import ObController
|
||||
|
||||
# Namespace pour les opérations sur les playlists
|
||||
playlist_ns = Namespace('playlists', description='Playlist operations')
|
||||
|
||||
# Modèle d'entrée pour la playlist
|
||||
playlist_model = playlist_ns.model('Playlist', {
|
||||
'name': fields.String(required=True, description='The playlist name'),
|
||||
'enabled': fields.Boolean(default=True, description='Is the playlist enabled?'),
|
||||
'time_sync': fields.Boolean(default=False, description='Is time synchronization enabled?')
|
||||
})
|
||||
|
||||
# Modèle de sortie pour la playlist
|
||||
playlist_output_model = playlist_ns.model('PlaylistOutput', {
|
||||
'id': fields.Integer(readOnly=True, description='The unique identifier of a playlist'),
|
||||
'name': fields.String(required=True, description='The playlist name'),
|
||||
'enabled': fields.Boolean(description='Is the playlist enabled?'),
|
||||
'time_sync': fields.Boolean(description='Is time synchronization enabled?')
|
||||
})
|
||||
|
||||
|
||||
class PlaylistApiController(ObController):
|
||||
|
||||
def register(self):
|
||||
self._app.add_url_rule('/api/playlist', 'api_playlist_list', self.get_playlists, methods=['GET'])
|
||||
self._app.add_url_rule('/api/playlist', 'api_playlist_add', self.add_playlist, methods=['POST'])
|
||||
self._app.add_url_rule('/api/playlist/<int:playlist_id>', 'api_playlist_get', self.get_playlist, methods=['GET'])
|
||||
self._app.add_url_rule('/api/playlist/<int:playlist_id>', 'api_playlist_update', self.update_playlist, methods=['PUT'])
|
||||
self._app.add_url_rule('/api/playlist/<int:playlist_id>', 'api_playlist_delete', self.delete_playlist, methods=['DELETE'])
|
||||
self._app.add_url_rule('/api/playlist/<int:playlist_id>/slides', 'api_playlist_list_slides', self.get_playlists_slides, methods=['GET'])
|
||||
self._app.add_url_rule('/api/playlist/<int:playlist_id>/notifications', 'api_playlist_list_notifications', self.get_playlists_notifications, methods=['GET'])
|
||||
self.api().add_namespace(playlist_ns, path='/api/playlists')
|
||||
playlist_ns.add_resource(self.create_resource(PlaylistResource), '/<int:playlist_id>')
|
||||
playlist_ns.add_resource(self.create_resource(PlaylistListResource), '/')
|
||||
playlist_ns.add_resource(self.create_resource(PlaylistSlidesResource), '/<int:playlist_id>/slides')
|
||||
playlist_ns.add_resource(self.create_resource(PlaylistNotificationsResource), '/<int:playlist_id>/notifications')
|
||||
|
||||
def get_playlists(self):
|
||||
def create_resource(self, resource_class):
|
||||
# Function to inject dependencies into resources
|
||||
return type(f'{resource_class.__name__}WithDependencies', (resource_class,), {
|
||||
'_model_store': self._model_store
|
||||
})
|
||||
|
||||
|
||||
class PlaylistListResource(Resource):
|
||||
@playlist_ns.marshal_list_with(playlist_output_model)
|
||||
def get(self):
|
||||
"""List all playlists"""
|
||||
playlists = self._model_store.playlist().get_all(sort="created_at", ascending=True)
|
||||
result = [playlist.to_dict() for playlist in playlists]
|
||||
return jsonify(result)
|
||||
return result
|
||||
|
||||
def get_playlist(self, playlist_id: int):
|
||||
playlist = self._model_store.playlist().get(playlist_id)
|
||||
if not playlist:
|
||||
abort(404, description="Playlist not found")
|
||||
return jsonify(playlist.to_dict())
|
||||
|
||||
def add_playlist(self):
|
||||
@playlist_ns.expect(playlist_model)
|
||||
@playlist_ns.marshal_with(playlist_output_model, code=201)
|
||||
def post(self):
|
||||
"""Create a new playlist"""
|
||||
data = request.get_json()
|
||||
if not data or 'name' not in data:
|
||||
abort(400, description="Invalid input")
|
||||
@ -42,12 +65,24 @@ class PlaylistApiController(ObController):
|
||||
except Exception as e:
|
||||
abort(409, description=str(e))
|
||||
|
||||
return jsonify(playlist.to_dict()), 201
|
||||
return playlist.to_dict(), 201
|
||||
|
||||
def update_playlist(self, playlist_id: int):
|
||||
|
||||
class PlaylistResource(Resource):
|
||||
|
||||
@playlist_ns.marshal_with(playlist_output_model)
|
||||
def get(self, playlist_id):
|
||||
"""Get a playlist by its ID"""
|
||||
playlist = self._model_store.playlist().get(playlist_id)
|
||||
if not playlist:
|
||||
abort(404, description="Playlist not found")
|
||||
return playlist.to_dict()
|
||||
|
||||
@playlist_ns.expect(playlist_model)
|
||||
@playlist_ns.marshal_with(playlist_output_model)
|
||||
def put(self, playlist_id):
|
||||
"""Update an existing playlist"""
|
||||
data = request.get_json()
|
||||
if not data or 'name' not in data:
|
||||
abort(400, description="Invalid input")
|
||||
|
||||
playlist = self._model_store.playlist().get(playlist_id)
|
||||
if not playlist:
|
||||
@ -60,9 +95,10 @@ class PlaylistApiController(ObController):
|
||||
enabled=data.get('enabled', playlist.enabled)
|
||||
)
|
||||
updated_playlist = self._model_store.playlist().get(playlist_id)
|
||||
return jsonify(updated_playlist.to_dict())
|
||||
return updated_playlist.to_dict()
|
||||
|
||||
def delete_playlist(self, playlist_id: int):
|
||||
def delete(self, playlist_id):
|
||||
"""Delete a playlist"""
|
||||
playlist = self._model_store.playlist().get(playlist_id)
|
||||
if not playlist:
|
||||
abort(404, description="Playlist not found")
|
||||
@ -76,7 +112,11 @@ class PlaylistApiController(ObController):
|
||||
self._model_store.playlist().delete(playlist_id)
|
||||
return '', 204
|
||||
|
||||
def get_playlists_slides(self, playlist_id: int):
|
||||
|
||||
class PlaylistSlidesResource(Resource):
|
||||
|
||||
def get(self, playlist_id):
|
||||
"""Get slides associated with a playlist"""
|
||||
playlist = self._model_store.playlist().get(playlist_id)
|
||||
|
||||
if not playlist:
|
||||
@ -87,7 +127,11 @@ class PlaylistApiController(ObController):
|
||||
result = [slide.to_dict() for slide in slides]
|
||||
return jsonify(result)
|
||||
|
||||
def get_playlists_notifications(self, playlist_id: int):
|
||||
|
||||
class PlaylistNotificationsResource(Resource):
|
||||
|
||||
def get(self, playlist_id):
|
||||
"""Get notifications associated with a playlist"""
|
||||
playlist = self._model_store.playlist().get(playlist_id)
|
||||
|
||||
if not playlist:
|
||||
|
||||
@ -1,43 +1,69 @@
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from flask import Flask, request, jsonify, abort, make_response
|
||||
from werkzeug.utils import secure_filename
|
||||
from src.service.ModelStore import ModelStore
|
||||
from flask import request, abort, jsonify
|
||||
from flask_restx import Resource, Namespace, fields
|
||||
from src.model.entity.Slide import Slide
|
||||
from src.model.enum.ContentType import ContentType
|
||||
from src.interface.ObController import ObController
|
||||
from src.util.utils import str_to_enum, get_optional_string, str_datetime_to_cron, str_weekdaytime_to_cron
|
||||
from src.util.UtilFile import randomize_filename
|
||||
from src.util.utils import str_datetime_to_cron, str_weekdaytime_to_cron
|
||||
import time
|
||||
|
||||
# Namespace for slide operations
|
||||
slide_ns = Namespace('slides', description='Operations on slides')
|
||||
|
||||
# Input model for adding/editing a slide
|
||||
slide_model = slide_ns.model('Slide', {
|
||||
'content_id': fields.Integer(required=True, description='The content ID for the slide'),
|
||||
'playlist_id': fields.Integer(required=True, description='The playlist ID to which the slide belongs'),
|
||||
'enabled': fields.Boolean(default=True, description='Is the slide enabled?'),
|
||||
'delegate_duration': fields.Boolean(default=False, description='Should the duration be delegated?'),
|
||||
'duration': fields.Integer(default=3, description='Duration of the slide'),
|
||||
'position': fields.Integer(default=999, description='Position of the slide'),
|
||||
'scheduling': fields.String(description='Scheduling type: loop, datetime, or inweek'),
|
||||
'datetime_start': fields.String(description='Start datetime for scheduling'),
|
||||
'datetime_end': fields.String(description='End datetime for scheduling'),
|
||||
'day_start': fields.Integer(description='Start day for inweek scheduling'),
|
||||
'time_start': fields.String(description='Start time for inweek scheduling'),
|
||||
'day_end': fields.Integer(description='End day for inweek scheduling'),
|
||||
'time_end': fields.String(description='End time for inweek scheduling'),
|
||||
'cron_start': fields.String(description='Cron expression for scheduling start'),
|
||||
'cron_end': fields.String(description='Cron expression for scheduling end'),
|
||||
})
|
||||
|
||||
# Output model for a slide
|
||||
slide_output_model = slide_ns.model('SlideOutput', {
|
||||
'id': fields.Integer(readOnly=True, description='The unique identifier of a slide'),
|
||||
'content_id': fields.Integer(description='The content ID for the slide'),
|
||||
'playlist_id': fields.Integer(description='The playlist ID to which the slide belongs'),
|
||||
'enabled': fields.Boolean(description='Is the slide enabled?'),
|
||||
'delegate_duration': fields.Boolean(description='Should the duration be delegated?'),
|
||||
'duration': fields.Integer(description='Duration of the slide'),
|
||||
'position': fields.Integer(description='Position of the slide'),
|
||||
'is_notification': fields.Boolean(description='Is the slide a notification?'),
|
||||
'cron_schedule': fields.String(description='Cron expression for scheduling start'),
|
||||
'cron_schedule_end': fields.String(description='Cron expression for scheduling end'),
|
||||
})
|
||||
|
||||
# Input model for updating slide positions
|
||||
positions_model = slide_ns.model('SlidePositions', {
|
||||
'positions': fields.Raw(required=True, description='A dictionary where keys are slide IDs and values are their new positions')
|
||||
})
|
||||
|
||||
|
||||
class SlideApiController(ObController):
|
||||
|
||||
def register(self):
|
||||
self._app.add_url_rule('/api/slide', 'api_slide_add', self.add_slide, methods=['POST'])
|
||||
self._app.add_url_rule('/api/slide/notification', 'api_slide_notification_add', self.add_notification, methods=['POST'])
|
||||
self._app.add_url_rule('/api/slide/<int:slide_id>', 'api_slide_get', self.get_slide, methods=['GET'])
|
||||
self._app.add_url_rule('/api/slide/<int:slide_id>', 'api_slide_edit', self.edit_slide, methods=['PUT'])
|
||||
self._app.add_url_rule('/api/slide/<int:slide_id>', 'api_slide_delete', self.delete_slide, methods=['DELETE'])
|
||||
self._app.add_url_rule('/api/slide/positions', 'api_slide_positions', self.update_slide_positions, methods=['POST'])
|
||||
self.api().add_namespace(slide_ns, path='/api/slides')
|
||||
slide_ns.add_resource(self.create_resource(SlideResource), '/<int:slide_id>')
|
||||
slide_ns.add_resource(self.create_resource(SlideAddResource), '/')
|
||||
slide_ns.add_resource(self.create_resource(SlideAddNotificationResource), '/notifications')
|
||||
slide_ns.add_resource(self.create_resource(SlidePositionResource), '/positions')
|
||||
|
||||
def get_slide(self, slide_id: int):
|
||||
slide = self._model_store.slide().get(slide_id)
|
||||
if not slide:
|
||||
abort(404, description="Slide not found")
|
||||
return jsonify(slide.to_dict())
|
||||
|
||||
def add_slide(self):
|
||||
return self.add_slide_or_notification(is_notification=False)
|
||||
|
||||
def add_notification(self):
|
||||
return self.add_slide_or_notification(is_notification=True)
|
||||
|
||||
def add_slide_or_notification(self, is_notification=False):
|
||||
data = request.get_json()
|
||||
def create_resource(self, resource_class):
|
||||
# Function to inject dependencies into resources
|
||||
return type(f'{resource_class.__name__}WithDependencies', (resource_class,), {
|
||||
'_model_store': self._model_store,
|
||||
'_controller': self
|
||||
})
|
||||
|
||||
def _add_slide_or_notification(self, data, is_notification=False):
|
||||
if not data or 'content_id' not in data:
|
||||
abort(400, description="Valid Content ID is required")
|
||||
|
||||
@ -67,56 +93,7 @@ class SlideApiController(ObController):
|
||||
slide = self._model_store.slide().add_form(slide)
|
||||
self._post_update()
|
||||
|
||||
return jsonify(slide.to_dict()), 201
|
||||
|
||||
def edit_slide(self, slide_id: int):
|
||||
data = request.get_json()
|
||||
if not data or 'content_id' not in data:
|
||||
abort(400, description="Content ID is required")
|
||||
|
||||
slide = self._model_store.slide().get(slide_id)
|
||||
if not slide:
|
||||
abort(404, description="Slide not found")
|
||||
|
||||
cron_schedule_start, cron_schedule_end = self._resolve_scheduling(data, is_notification=slide.is_notification)
|
||||
|
||||
self._model_store.slide().update_form(
|
||||
id=slide_id,
|
||||
content_id=data.get('content_id', slide.content_id),
|
||||
enabled=data.get('enabled', slide.enabled),
|
||||
position=data.get('position', slide.position),
|
||||
delegate_duration=data.get('delegate_duration', slide.delegate_duration),
|
||||
duration=data.get('duration', slide.duration),
|
||||
cron_schedule=cron_schedule_start if 'scheduling' in data else slide.cron_schedule,
|
||||
cron_schedule_end=cron_schedule_end if 'scheduling' in data else slide.cron_schedule_end,
|
||||
)
|
||||
self._post_update()
|
||||
|
||||
updated_slide = self._model_store.slide().get(slide_id)
|
||||
return jsonify(updated_slide.to_dict())
|
||||
|
||||
def delete_slide(self, slide_id: int):
|
||||
slide = self._model_store.slide().get(slide_id)
|
||||
|
||||
if not slide:
|
||||
abort(404, description="Slide not found")
|
||||
|
||||
self._model_store.slide().delete(slide_id)
|
||||
self._post_update()
|
||||
|
||||
return '', 204
|
||||
|
||||
def update_slide_positions(self):
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
abort(400, description="Positions data are required")
|
||||
|
||||
self._model_store.slide().update_positions(data)
|
||||
self._post_update()
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
def _post_update(self):
|
||||
self._model_store.variable().update_by_name("last_slide_update", time.time())
|
||||
return slide.to_dict(), 201
|
||||
|
||||
def _resolve_scheduling(self, data, is_notification=False):
|
||||
try:
|
||||
@ -182,3 +159,100 @@ class SlideApiController(ObController):
|
||||
abort(400, description="Invalid value for notification scheduling. Expected 'datetime' or 'cron'.")
|
||||
|
||||
return cron_schedule_start, cron_schedule_end
|
||||
|
||||
def _post_update(self):
|
||||
self._model_store.variable().update_by_name("last_slide_update", time.time())
|
||||
|
||||
|
||||
class SlideAddResource(Resource):
|
||||
|
||||
@slide_ns.expect(slide_model)
|
||||
@slide_ns.marshal_with(slide_output_model, code=201)
|
||||
def post(self):
|
||||
"""Add a new slide"""
|
||||
data = request.get_json()
|
||||
return self._controller._add_slide_or_notification(data, is_notification=False)
|
||||
|
||||
|
||||
class SlideAddNotificationResource(Resource):
|
||||
|
||||
@slide_ns.expect(slide_model)
|
||||
@slide_ns.marshal_with(slide_output_model, code=201)
|
||||
def post(self):
|
||||
"""Add a new slide"""
|
||||
data = request.get_json()
|
||||
return self._controller._add_slide_or_notification(data, is_notification=True)
|
||||
|
||||
|
||||
class SlideResource(Resource):
|
||||
|
||||
@slide_ns.marshal_with(slide_output_model)
|
||||
def get(self, slide_id):
|
||||
"""Get a slide by its ID"""
|
||||
slide = self._model_store.slide().get(slide_id)
|
||||
if not slide:
|
||||
abort(404, description="Slide not found")
|
||||
return slide.to_dict()
|
||||
|
||||
@slide_ns.expect(slide_model)
|
||||
@slide_ns.marshal_with(slide_output_model)
|
||||
def put(self, slide_id):
|
||||
"""Edit an existing slide"""
|
||||
data = request.get_json()
|
||||
|
||||
slide = self._model_store.slide().get(slide_id)
|
||||
if not slide:
|
||||
abort(404, description="Slide not found")
|
||||
|
||||
cron_schedule_start = slide.cron_schedule
|
||||
cron_schedule_end = slide.cron_schedule_end
|
||||
|
||||
if 'scheduling' in data:
|
||||
cron_schedule_start, cron_schedule_end = self._controller._resolve_scheduling(data, is_notification=slide.is_notification)
|
||||
|
||||
self._model_store.slide().update_form(
|
||||
id=slide_id,
|
||||
content_id=data.get('content_id', slide.content_id),
|
||||
enabled=data.get('enabled', slide.enabled),
|
||||
position=data.get('position', slide.position),
|
||||
delegate_duration=data.get('delegate_duration', slide.delegate_duration),
|
||||
duration=data.get('duration', slide.duration),
|
||||
cron_schedule=cron_schedule_start,
|
||||
cron_schedule_end=cron_schedule_end
|
||||
)
|
||||
self._controller._post_update()
|
||||
|
||||
updated_slide = self._model_store.slide().get(slide_id)
|
||||
return updated_slide.to_dict()
|
||||
|
||||
def delete(self, slide_id):
|
||||
"""Delete a slide"""
|
||||
slide = self._model_store.slide().get(slide_id)
|
||||
|
||||
if not slide:
|
||||
abort(404, description="Slide not found")
|
||||
|
||||
self._model_store.slide().delete(slide_id)
|
||||
self._controller._post_update()
|
||||
|
||||
return '', 204
|
||||
|
||||
|
||||
class SlidePositionResource(Resource):
|
||||
|
||||
@slide_ns.expect(positions_model)
|
||||
def post(self):
|
||||
"""Update positions of multiple slides"""
|
||||
data = request.get_json()
|
||||
positions = data.get('positions', None) if data else None
|
||||
|
||||
if not positions:
|
||||
abort(400, description="Positions data are required")
|
||||
|
||||
# Ensure the input is a dictionary with integer keys and values
|
||||
if not isinstance(data, dict) or not all(isinstance(k, str) and isinstance(v, int) for k, v in positions.items()):
|
||||
abort(400, description="Input must be a dictionary with string keys as slide IDs and integer values as positions")
|
||||
|
||||
self._model_store.slide().update_positions(positions)
|
||||
self._controller._post_update()
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
@ -5,3 +5,4 @@ waitress
|
||||
flask-login
|
||||
pysqlite3
|
||||
psutil
|
||||
flask-restx
|
||||
|
||||
@ -48,3 +48,6 @@ class ObController(abc.ABC):
|
||||
|
||||
def render_view(self, template_file: str, **parameters: dict) -> str:
|
||||
return self._template_renderer.render_view(template_file, self.plugin(), **parameters)
|
||||
|
||||
def api(self):
|
||||
return self._web_server.api
|
||||
|
||||
@ -9,6 +9,7 @@ load_dotenv()
|
||||
|
||||
class ConfigManager:
|
||||
|
||||
APPLICATION_NAME = "Obscreen"
|
||||
DEFAULT_PORT = 5000
|
||||
DEFAULT_PORT_HTTP_EXTERNAL_STORAGE = 5001
|
||||
VERSION_FILE = 'version.txt'
|
||||
@ -16,6 +17,7 @@ class ConfigManager:
|
||||
def __init__(self, replacers: Dict):
|
||||
self._replacers = replacers
|
||||
self._CONFIG = {
|
||||
'application_name': self.APPLICATION_NAME,
|
||||
'version': None,
|
||||
'demo': False,
|
||||
'port_http_external_storage': self.DEFAULT_PORT_HTTP_EXTERNAL_STORAGE,
|
||||
@ -87,7 +89,7 @@ class ConfigManager:
|
||||
if args.log_stdout:
|
||||
self._CONFIG['log_stdout'] = args.log_stdout
|
||||
if args.version:
|
||||
print("Obscreen version v{} (https://github.com/jr-k/obscreen)".format(self._CONFIG['version']))
|
||||
print("{} version v{} (https://github.com/jr-k/obscreen)".format(self.APPLICATION_NAME, self._CONFIG['version']))
|
||||
sys.exit(0)
|
||||
|
||||
def load_from_env(self) -> None:
|
||||
|
||||
@ -165,7 +165,7 @@ GROUP BY playlist_id;
|
||||
return
|
||||
|
||||
form = {
|
||||
"name": name,
|
||||
"name": name if isinstance(name, str) else slide.name,
|
||||
"time_sync": time_sync if isinstance(time_sync, bool) else slide.time_sync,
|
||||
"enabled": enabled if isinstance(enabled, bool) else slide.enabled,
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ from waitress import serve
|
||||
|
||||
from flask import Flask, send_from_directory, redirect, url_for, request, jsonify, make_response
|
||||
from flask_login import LoginManager, current_user
|
||||
from flask_restx import Api
|
||||
|
||||
from src.manager.UserManager import UserManager
|
||||
from src.service.ModelStore import ModelStore
|
||||
@ -27,6 +28,7 @@ class WebServer:
|
||||
|
||||
def __init__(self, kernel, model_store: ModelStore, template_renderer: TemplateRenderer):
|
||||
self._app = None
|
||||
self._api = None
|
||||
self._auth_enabled = False
|
||||
self._login_manager = None
|
||||
self._kernel = kernel
|
||||
@ -35,6 +37,10 @@ class WebServer:
|
||||
self._debug = self._model_store.config().map().get('debug')
|
||||
self.setup()
|
||||
|
||||
@property
|
||||
def api(self) -> Api:
|
||||
return self._api
|
||||
|
||||
def run(self) -> None:
|
||||
serve(
|
||||
self._app,
|
||||
@ -51,6 +57,7 @@ class WebServer:
|
||||
self._setup_web_globals()
|
||||
self._setup_web_errors()
|
||||
self._setup_web_controllers()
|
||||
self._setup_api()
|
||||
|
||||
def get_app(self):
|
||||
return self._app
|
||||
@ -76,6 +83,7 @@ class WebServer:
|
||||
|
||||
self._app.config['UPLOAD_FOLDER'] = "{}/{}".format(WebDirConstant.FOLDER_STATIC, WebDirConstant.FOLDER_STATIC_WEB_UPLOADS)
|
||||
self._app.config['MAX_CONTENT_LENGTH'] = self._model_store.variable().map().get('slide_upload_limit').as_int() * 1024 * 1024
|
||||
self._app.config['ERROR_404_HELP'] = False
|
||||
|
||||
self._setup_flask_login()
|
||||
|
||||
@ -115,6 +123,16 @@ class WebServer:
|
||||
PlaylistController(self._kernel, self, self._app, self.auth_required, self._model_store, self._template_renderer)
|
||||
AuthController(self._kernel, self, self._app, self.auth_required, self._model_store, self._template_renderer)
|
||||
|
||||
def _setup_api(self) -> None:
|
||||
self._api = Api(
|
||||
self._app,
|
||||
version=self._model_store.config().map().get('version'),
|
||||
title="{} {}".format(self._model_store.config().map().get('application_name'), "API"),
|
||||
description='API Documentation with Swagger',
|
||||
endpoint='api',
|
||||
doc='/api'
|
||||
)
|
||||
|
||||
def _setup_web_globals(self) -> None:
|
||||
@self._app.context_processor
|
||||
def inject_global_vars() -> dict:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user