From cdaa17803a3ca2178e54a5a1a30a988027507679 Mon Sep 17 00:00:00 2001 From: ske087 Date: Tue, 21 Jul 2026 10:42:31 +0300 Subject: [PATCH] feat(digiserver-v2): port edited media report and deploy badge features from standalone - Added edited_media_report route to players blueprint with tabular report view - Created edited_media_report.html template with summary stats, dark mode, print/PDF support - Created _deploy_badge.html partial template for deployment status display - Added deployment status column and badge CSS to players_list.html - Added 'View Report' button to manage_player.html next to 'View All Edited Media' - Fixed api.py receive_edited_media with robust content lookup: - Fallback content lookup by regex path matching (edited_media//) - Fallback via PlayerEdit record if direct lookup fails - First-edit original file preservation (moves original to versionized folder) Enterprise-specific features (portal SSO, internal sync, playlist permissions, WAL mode) are preserved unchanged. --- digiserver-v2/app/blueprints/api.py | 30 +- digiserver-v2/app/blueprints/players.py | 34 ++ .../app/templates/players/_deploy_badge.html | 25 ++ .../players/edited_media_report.html | 342 ++++++++++++++++++ .../app/templates/players/manage_player.html | 7 + .../app/templates/players/players_list.html | 73 ++++ 6 files changed, 509 insertions(+), 2 deletions(-) create mode 100644 digiserver-v2/app/templates/players/_deploy_badge.html create mode 100644 digiserver-v2/app/templates/players/edited_media_report.html diff --git a/digiserver-v2/app/blueprints/api.py b/digiserver-v2/app/blueprints/api.py index f9893da..25f242b 100644 --- a/digiserver-v2/app/blueprints/api.py +++ b/digiserver-v2/app/blueprints/api.py @@ -765,6 +765,8 @@ def receive_edited_media(): # Import required modules import os + import shutil + import re from werkzeug.utils import secure_filename from app.models.player_edit import PlayerEdit @@ -772,6 +774,19 @@ def receive_edited_media(): original_name = metadata['original_name'] content = Content.query.filter_by(filename=original_name).first() + if not content: + # Try to extract content_id from a path like "edited_media//file" + m = re.match(r'edited_media/(\d+)/', original_name) + if m: + content = db.session.get(Content, int(m.group(1))) + if not content: + # Last resort – look up the most recent PlayerEdit for this + # content and use its content_id. + fallback_edit = PlayerEdit.query.filter_by(new_name=original_name)\ + .order_by(PlayerEdit.created_at.desc()).first() + if fallback_edit: + content = db.session.get(Content, fallback_edit.content_id) + if not content: log_action('warning', f'Player {player.name} tried to edit non-existent content: {original_name}') return jsonify({'error': f'Original content not found: {original_name}'}), 404 @@ -781,20 +796,31 @@ def receive_edited_media(): edited_media_dir = os.path.join(base_upload_dir, 'edited_media', str(content.id)) os.makedirs(edited_media_dir, exist_ok=True) + # On the very first edit (v1) move the original file into the + # versionized folder so it is never orphaned. + is_first_edit = PlayerEdit.query.filter_by(content_id=content.id).count() == 0 + if is_first_edit: + orig_upload = os.path.join(base_upload_dir, content.filename) + # content.filename might already be an edited_media/ path if + # this is a re-process; only move if it's a plain filename. + if os.path.isfile(orig_upload) and not content.filename.startswith('edited_media/'): + orig_stored = f"original_{content.filename}" + shutil.move(orig_upload, os.path.join(edited_media_dir, orig_stored)) + log_action('info', f'Moved original file "{content.filename}" to versionized folder as "{orig_stored}"') + # Save the edited file with version suffix version = metadata['version'] new_filename = metadata['new_name'] edited_file_path = os.path.join(edited_media_dir, new_filename) file.save(edited_file_path) - # Save metadata JSON file + # Save metadata JSON side-car file metadata_filename = f"{os.path.splitext(new_filename)[0]}_metadata.json" metadata_path = os.path.join(edited_media_dir, metadata_filename) with open(metadata_path, 'w') as f: json.dump(metadata, f, indent=2) # Update the content record to reference the edited version path - # Keep original filename unchanged, point to edited_media folder old_filename = content.filename content.filename = f"edited_media/{content.id}/{new_filename}" diff --git a/digiserver-v2/app/blueprints/players.py b/digiserver-v2/app/blueprints/players.py index 02b14c1..7cf0855 100644 --- a/digiserver-v2/app/blueprints/players.py +++ b/digiserver-v2/app/blueprints/players.py @@ -460,6 +460,40 @@ def edited_media(player_id: int): return redirect(url_for('players.manage_player', player_id=player_id)) +@players_bp.route('//edited-media-report') +@login_required +def edited_media_report(player_id: int): + """Display a tabular report of all edited media from this player.""" + try: + player = Player.query.get_or_404(player_id) + + from app.models.player_edit import PlayerEdit + from app.models.player_user import PlayerUser + + edited_media = PlayerEdit.query.filter_by(player_id=player_id)\ + .order_by(PlayerEdit.created_at.desc())\ + .all() + + # Build user display name mapping + user_mappings = {} + for edit in edited_media: + if edit.user and edit.user not in user_mappings: + player_user = PlayerUser.query.filter_by(user_code=edit.user).first() + if player_user and player_user.user_name: + user_mappings[edit.user] = player_user.user_name + else: + user_mappings[edit.user] = edit.user + + return render_template('players/edited_media_report.html', + player=player, + edited_media=edited_media, + user_mappings=user_mappings) + except Exception as e: + log_action('error', f'Error loading edited media report for player {player_id}: {str(e)}') + flash('Error loading edited media report.', 'danger') + return redirect(url_for('players.manage_player', player_id=player_id)) + + @players_bp.route('//fullscreen') def player_fullscreen(player_id: int): """Display player fullscreen view (no authentication required for players).""" diff --git a/digiserver-v2/app/templates/players/_deploy_badge.html b/digiserver-v2/app/templates/players/_deploy_badge.html new file mode 100644 index 0000000..0b3dc85 --- /dev/null +++ b/digiserver-v2/app/templates/players/_deploy_badge.html @@ -0,0 +1,25 @@ +{% if player.deployment_status == 'deploying' %} + + Deploying... + +{% elif player.deployment_status == 'deployed' %} + + ✅ Deployed + {% if player.last_deployment_at %} + {{ player.last_deployment_at | localtime }} + {% endif %} + +{% elif player.deployment_status == 'failed' %} + + ❌ Failed + {% if player.last_deployment_at %} + {{ player.last_deployment_at | localtime }} + {% endif %} + +{% elif player.deployment_status == 'pending' %} + + ⏳ Pending + +{% else %} + +{% endif %} diff --git a/digiserver-v2/app/templates/players/edited_media_report.html b/digiserver-v2/app/templates/players/edited_media_report.html new file mode 100644 index 0000000..eca432c --- /dev/null +++ b/digiserver-v2/app/templates/players/edited_media_report.html @@ -0,0 +1,342 @@ +{% extends "base.html" %} + +{% block title %}Edited Media Report - {{ player.name }}{% endblock %} + +{% block content %} + + +
+
+ + ← Back to Player + +

+ + Edited Media Report — {{ player.name }} +

+ +
+ + {% if edited_media %} + {% set total_edits = edited_media|length %} + {% set unique_files = edited_media|map(attribute='content_id')|unique|list|length %} + {% set unique_users = edited_media|selectattr('user')|map(attribute='user')|unique|list|length %} + +
+
+ {{ total_edits }} + Total Edits +
+
+ {{ unique_files }} + Files Edited +
+
+ {{ unique_users }} + Editors +
+
+ +
+ + + + + + + + + + + + {% for edit in edited_media %} + + + + + + + + {% endfor %} + +
UserEdited FileVersionDateLink
+ {% if edit.user %} + {% set display_name = user_mappings.get(edit.user, edit.user) %} + 👤 {{ display_name }} + {% else %} + + {% endif %} + + 📄 {{ edit.new_name }} + + v{{ edit.version }} + + {% if edit.time_of_modification %} + {{ edit.time_of_modification | localtime('%Y-%m-%d %H:%M') }} + {% elif edit.created_at %} + {{ edit.created_at | localtime('%Y-%m-%d %H:%M') }} + {% else %} + + {% endif %} +
+
+ {% else %} +
+
📋
+

No edited media found

+

This player has not submitted any edited media yet.

+
+ {% endif %} +
+{% endblock %} diff --git a/digiserver-v2/app/templates/players/manage_player.html b/digiserver-v2/app/templates/players/manage_player.html index 9d683be..ec4b2c1 100644 --- a/digiserver-v2/app/templates/players/manage_player.html +++ b/digiserver-v2/app/templates/players/manage_player.html @@ -653,6 +653,13 @@ document.addEventListener('keydown', function(event) { onmouseout="this.style.background='#7c3aed'"> 📋 View All Edited Media + + 📊 View Report + {% endif %}

Latest 3 edited files with their most recent versions

diff --git a/digiserver-v2/app/templates/players/players_list.html b/digiserver-v2/app/templates/players/players_list.html index 7d4e537..1e46385 100644 --- a/digiserver-v2/app/templates/players/players_list.html +++ b/digiserver-v2/app/templates/players/players_list.html @@ -98,6 +98,75 @@ .text-muted { color: #6c757d; } + + /* Deployment status */ + .deploy-badge { + display: inline-flex; + align-items: center; + gap: 0.3rem; + padding: 2px 8px; + border-radius: 3px; + font-size: 11px; + font-weight: 600; + white-space: nowrap; + } + .deploy-badge.pending { + background: #fff3cd; + color: #856404; + } + body.dark-mode .deploy-badge.pending { + background: #2d2a0e; + color: #f0d45e; + } + .deploy-badge.deployed { + background: #d4edda; + color: #155724; + } + body.dark-mode .deploy-badge.deployed { + background: #0e2d1a; + color: #68d98b; + } + .deploy-badge.failed { + background: #f8d7da; + color: #721c24; + } + body.dark-mode .deploy-badge.failed { + background: #2d0e11; + color: #f05a6a; + } + .deploy-badge.deploying { + background: #cce5ff; + color: #004085; + } + body.dark-mode .deploy-badge.deploying { + background: #0e1d2d; + color: #5aadf0; + } + .deploy-badge .spinner { + display: inline-block; + width: 10px; + height: 10px; + border: 2px solid #004085; + border-top-color: transparent; + border-radius: 50%; + animation: deploy-spin 0.8s linear infinite; + } + body.dark-mode .deploy-badge .spinner { + border-color: #5aadf0; + border-top-color: transparent; + } + @keyframes deploy-spin { to { transform: rotate(360deg); } } + .deploy-timestamp { + font-size: 10px; + opacity: 0.75; + display: block; + } + body.dark-mode .deploy-timestamp { + color: #9ca3af; + } + .deploy-tooltip { + cursor: help; + } body.dark-mode .text-muted { color: #718096; @@ -142,6 +211,7 @@ Location Orientation Status + Deployment Last Seen Actions @@ -168,6 +238,9 @@ Offline {% endif %} + + {% include "players/_deploy_badge.html" %} + {% if player.last_seen %} {{ player.last_seen | localtime }}