"""Groups blueprint for group management and player assignments.""" from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify from flask_login import login_required from typing import List, Dict from app.extensions import db, cache from app.models import Group, Player, Content from app.utils.logger import log_action from app.utils.group_player_management import get_player_status_info, get_group_statistics groups_bp = Blueprint('groups', __name__, url_prefix='/groups') @groups_bp.route('/') @login_required def groups_list(): """Display list of all groups.""" try: groups = Group.query.order_by(Group.name).all() # Get statistics for each group group_stats = {} for group in groups: stats = get_group_statistics(group.id) group_stats[group.id] = stats return render_template('groups/groups_list.html', groups=groups, group_stats=group_stats) except Exception as e: log_action('error', f'Error loading groups list: {str(e)}') flash('Error loading groups list.', 'danger') return redirect(url_for('main.dashboard')) @groups_bp.route('/create', methods=['GET', 'POST']) @login_required def create_group(): """Create a new group.""" if request.method == 'GET': available_content = Content.query.order_by(Content.filename).all() return render_template('groups/create_group.html', available_content=available_content) try: name = request.form.get('name', '').strip() description = request.form.get('description', '').strip() content_ids = request.form.getlist('content_ids') # Validation if not name or len(name) < 3: flash('Group name must be at least 3 characters long.', 'warning') return redirect(url_for('groups.create_group')) # Check if group name exists existing_group = Group.query.filter_by(name=name).first() if existing_group: flash(f'Group "{name}" already exists.', 'warning') return redirect(url_for('groups.create_group')) # Create group new_group = Group( name=name, description=description or None ) # Add content to group if content_ids: for content_id in content_ids: content = Content.query.get(int(content_id)) if content: new_group.contents.append(content) db.session.add(new_group) db.session.commit() log_action('info', f'Group "{name}" created with {len(content_ids)} content items') flash(f'Group "{name}" created successfully.', 'success') return redirect(url_for('groups.groups_list')) except Exception as e: db.session.rollback() log_action('error', f'Error creating group: {str(e)}') flash('Error creating group. Please try again.', 'danger') return redirect(url_for('groups.create_group')) @groups_bp.route('//edit', methods=['GET', 'POST']) @login_required def edit_group(group_id: int): """Edit group details.""" group = Group.query.get_or_404(group_id) if request.method == 'GET': available_content = Content.query.order_by(Content.filename).all() return render_template('groups/edit_group.html', group=group, available_content=available_content) try: name = request.form.get('name', '').strip() description = request.form.get('description', '').strip() content_ids = request.form.getlist('content_ids') # Validation if not name or len(name) < 3: flash('Group name must be at least 3 characters long.', 'warning') return redirect(url_for('groups.edit_group', group_id=group_id)) # Check if group name exists (excluding current group) existing_group = Group.query.filter(Group.name == name, Group.id != group_id).first() if existing_group: flash(f'Group name "{name}" is already in use.', 'warning') return redirect(url_for('groups.edit_group', group_id=group_id)) # Update group group.name = name group.description = description or None # Update content group.contents = [] if content_ids: for content_id in content_ids: content = Content.query.get(int(content_id)) if content: group.contents.append(content) db.session.commit() # Clear cache for all players in this group for player in group.players: cache.delete_memoized('get_player_playlist', player.id) log_action('info', f'Group "{name}" (ID: {group_id}) updated') flash(f'Group "{name}" updated successfully.', 'success') return redirect(url_for('groups.groups_list')) except Exception as e: db.session.rollback() log_action('error', f'Error updating group: {str(e)}') flash('Error updating group. Please try again.', 'danger') return redirect(url_for('groups.edit_group', group_id=group_id)) @groups_bp.route('//delete', methods=['POST']) @login_required def delete_group(group_id: int): """Delete a group.""" try: group = Group.query.get_or_404(group_id) group_name = group.name # Unassign players from group for player in group.players: player.group_id = None cache.delete_memoized('get_player_playlist', player.id) db.session.delete(group) db.session.commit() log_action('info', f'Group "{group_name}" (ID: {group_id}) deleted') flash(f'Group "{group_name}" deleted successfully.', 'success') except Exception as e: db.session.rollback() log_action('error', f'Error deleting group: {str(e)}') flash('Error deleting group. Please try again.', 'danger') return redirect(url_for('groups.groups_list')) @groups_bp.route('//manage') @login_required def manage_group(group_id: int): """Manage group with player status cards and content.""" try: group = Group.query.get_or_404(group_id) # Get all players in this group players = group.players.order_by(Player.name).all() # Get player status for each player player_statuses = {} for player in players: status_info = get_player_status_info(player.id) player_statuses[player.id] = status_info # Get group content contents = group.contents.order_by(Content.position).all() # Get available players (not in this group) available_players = Player.query.filter( (Player.group_id == None) | (Player.group_id != group_id) ).order_by(Player.name).all() # Get available content (not in this group) all_content = Content.query.order_by(Content.filename).all() return render_template('groups/manage_group.html', group=group, players=players, player_statuses=player_statuses, contents=contents, available_players=available_players, all_content=all_content) except Exception as e: log_action('error', f'Error loading manage group page: {str(e)}') flash('Error loading manage group page.', 'danger') return redirect(url_for('groups.groups_list')) @groups_bp.route('//fullscreen') def group_fullscreen(group_id: int): """Display group fullscreen view with all player status cards.""" try: group = Group.query.get_or_404(group_id) # Get all players in this group players = group.players.order_by(Player.name).all() # Get player status for each player player_statuses = {} for player in players: status_info = get_player_status_info(player.id) player_statuses[player.id] = status_info return render_template('groups/group_fullscreen.html', group=group, players=players, player_statuses=player_statuses) except Exception as e: log_action('error', f'Error loading group fullscreen: {str(e)}') return "Error loading group fullscreen", 500 @groups_bp.route('//add-player', methods=['POST']) @login_required def add_player_to_group(group_id: int): """Add a player to a group.""" try: group = Group.query.get_or_404(group_id) player_id = request.form.get('player_id') if not player_id: flash('No player selected.', 'warning') return redirect(url_for('groups.manage_group', group_id=group_id)) player = Player.query.get_or_404(int(player_id)) player.group_id = group_id db.session.commit() # Clear cache cache.delete_memoized('get_player_playlist', player.id) log_action('info', f'Player "{player.name}" added to group "{group.name}"') flash(f'Player "{player.name}" added to group successfully.', 'success') except Exception as e: db.session.rollback() log_action('error', f'Error adding player to group: {str(e)}') flash('Error adding player to group. Please try again.', 'danger') return redirect(url_for('groups.manage_group', group_id=group_id)) @groups_bp.route('//remove-player/', methods=['POST']) @login_required def remove_player_from_group(group_id: int, player_id: int): """Remove a player from a group.""" try: player = Player.query.get_or_404(player_id) if player.group_id != group_id: flash('Player is not in this group.', 'warning') return redirect(url_for('groups.manage_group', group_id=group_id)) player_name = player.name player.group_id = None db.session.commit() # Clear cache cache.delete_memoized('get_player_playlist', player_id) log_action('info', f'Player "{player_name}" removed from group {group_id}') flash(f'Player "{player_name}" removed from group successfully.', 'success') except Exception as e: db.session.rollback() log_action('error', f'Error removing player from group: {str(e)}') flash('Error removing player from group. Please try again.', 'danger') return redirect(url_for('groups.manage_group', group_id=group_id)) @groups_bp.route('//add-content', methods=['POST']) @login_required def add_content_to_group(group_id: int): """Add content to a group.""" try: group = Group.query.get_or_404(group_id) content_ids = request.form.getlist('content_ids') if not content_ids: flash('No content selected.', 'warning') return redirect(url_for('groups.manage_group', group_id=group_id)) # Add content added_count = 0 for content_id in content_ids: content = Content.query.get(int(content_id)) if content and content not in group.contents: group.contents.append(content) added_count += 1 db.session.commit() # Clear cache for all players in this group for player in group.players: cache.delete_memoized('get_player_playlist', player.id) log_action('info', f'{added_count} content items added to group "{group.name}"') flash(f'{added_count} content items added successfully.', 'success') except Exception as e: db.session.rollback() log_action('error', f'Error adding content to group: {str(e)}') flash('Error adding content to group. Please try again.', 'danger') return redirect(url_for('groups.manage_group', group_id=group_id)) @groups_bp.route('//remove-content/', methods=['POST']) @login_required def remove_content_from_group(group_id: int, content_id: int): """Remove content from a group.""" try: group = Group.query.get_or_404(group_id) content = Content.query.get_or_404(content_id) if content not in group.contents: flash('Content is not in this group.', 'warning') return redirect(url_for('groups.manage_group', group_id=group_id)) group.contents.remove(content) db.session.commit() # Clear cache for all players in this group for player in group.players: cache.delete_memoized('get_player_playlist', player.id) log_action('info', f'Content "{content.filename}" removed from group "{group.name}"') flash('Content removed from group successfully.', 'success') except Exception as e: db.session.rollback() log_action('error', f'Error removing content from group: {str(e)}') flash('Error removing content from group. Please try again.', 'danger') return redirect(url_for('groups.manage_group', group_id=group_id)) @groups_bp.route('//reorder-content', methods=['POST']) @login_required def reorder_group_content(group_id: int): """Reorder content within a group.""" try: group = Group.query.get_or_404(group_id) content_order = request.json.get('order', []) # Update positions for idx, content_id in enumerate(content_order): content = Content.query.get(content_id) if content and content in group.contents: content.position = idx db.session.commit() # Clear cache for all players in this group for player in group.players: cache.delete_memoized('get_player_playlist', player.id) log_action('info', f'Content reordered for group "{group.name}"') return jsonify({'success': True}) except Exception as e: db.session.rollback() log_action('error', f'Error reordering group content: {str(e)}') return jsonify({'success': False, 'error': str(e)}), 500 @groups_bp.route('//stats') @login_required def group_stats(group_id: int): """Get group statistics as JSON.""" try: stats = get_group_statistics(group_id) return jsonify(stats) except Exception as e: log_action('error', f'Error getting group stats: {str(e)}') return jsonify({'error': str(e)}), 500