Compare commits

..

2 Commits

Author SHA1 Message Date
318f783de3 Fix orientation parameter handling and template URL endpoints
- Add orientation parameter support to create_group and edit_group functions
- Fix manage_group.html template URL endpoint from 'update_group_content_order' to 'update_group_content_order_route'
- Add orientation field and filtering to edit_group.html template with JavaScript functionality
- Update group_player_management.py to handle orientation validation in create and edit operations
- Fix docker-compose.yml to include build directive and correct volume paths
- Update entrypoint.sh to handle fresh deployments without migrations
- Ensure orientation consistency across group and player management

These changes resolve:
- Internal Server Error on manage_group page
- Missing orientation parameter in group creation/editing
- Template URL endpoint mismatches
- Docker deployment issues with fresh installations
2025-08-01 15:15:59 -04:00
70d76f45e7 updated 2025-08-01 13:22:37 -04:00
16 changed files with 76 additions and 314 deletions

6
app.py
View File

@@ -488,7 +488,8 @@ def create_group():
if request.method == 'POST':
group_name = request.form['name']
player_ids = request.form.getlist('players')
create_group_util(group_name, player_ids)
orientation = request.form.get('orientation', 'Landscape')
create_group_util(group_name, player_ids, orientation)
flash(f'Group "{group_name}" created successfully.', 'success')
return redirect(url_for('dashboard'))
players = Player.query.all()
@@ -514,7 +515,8 @@ def edit_group(group_id):
if request.method == 'POST':
name = request.form['name']
player_ids = request.form.getlist('players')
edit_group_util(group_id, name, player_ids)
orientation = request.form.get('orientation', group.orientation)
edit_group_util(group_id, name, player_ids, orientation)
flash(f'Group "{name}" updated successfully.', 'success')
return redirect(url_for('dashboard'))
players = Player.query.all()

View File

@@ -1,6 +1,7 @@
#version: '"1.1.0"'
services:
web:
build: .
image: digiserver:latest
ports:
- "8880:5000"
@@ -12,5 +13,5 @@ services:
- SECRET_KEY=Ma_Duc_Dupa_Merele_Lui_Ana
volumes:
- /opt/digi-s/instance:/app/instance
- /opt/digi-s/static/uploads:/app/static/uploads
- /opt/digi-s/uploads:/app/static/uploads
restart: unless-stopped

View File

@@ -7,16 +7,7 @@ mkdir -p instance
# Check if database exists
if [ ! -f instance/dashboard.db ]; then
echo "No database found, initializing..."
# Remove and recreate migrations directory to ensure clean state
rm -rf migrations
mkdir -p migrations
# Initialize the database
flask db init
flask db migrate -m "Initial migration"
flask db upgrade
echo "No database found, creating fresh database..."
# Create admin user if environment variables are set
if [ -n "$ADMIN_USER" ] && [ -n "$ADMIN_PASSWORD" ]; then
@@ -26,8 +17,11 @@ if [ ! -f instance/dashboard.db ]; then
echo "Warning: ADMIN_USER or ADMIN_PASSWORD not set, skipping admin creation"
fi
else
echo "Existing database found, applying migrations..."
flask db upgrade
echo "Existing database found, skipping initialization..."
echo "Creating admin user if needed..."
if [ -n "$ADMIN_USER" ] && [ -n "$ADMIN_PASSWORD" ]; then
flask create-admin --username "$ADMIN_USER" --password "$ADMIN_PASSWORD" 2>/dev/null || echo "Default user '$ADMIN_USER' already exists."
fi
fi
echo "Starting DigiServer..."

Binary file not shown.

View File

@@ -1 +0,0 @@
Single-database configuration for Flask.

View File

@@ -1,50 +0,0 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic,flask_migrate
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[logger_flask_migrate]
level = INFO
handlers =
qualname = flask_migrate
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View File

@@ -1,113 +0,0 @@
import logging
from logging.config import fileConfig
from flask import current_app
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
def get_engine():
try:
# this works with Flask-SQLAlchemy<3 and Alchemical
return current_app.extensions['migrate'].db.get_engine()
except (TypeError, AttributeError):
# this works with Flask-SQLAlchemy>=3
return current_app.extensions['migrate'].db.engine
def get_engine_url():
try:
return get_engine().url.render_as_string(hide_password=False).replace(
'%', '%%')
except AttributeError:
return str(get_engine().url).replace('%', '%%')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
config.set_main_option('sqlalchemy.url', get_engine_url())
target_db = current_app.extensions['migrate'].db
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def get_metadata():
if hasattr(target_db, 'metadatas'):
return target_db.metadatas[None]
return target_db.metadata
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=get_metadata(), literal_binds=True
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
conf_args = current_app.extensions['migrate'].configure_args
if conf_args.get("process_revision_directives") is None:
conf_args["process_revision_directives"] = process_revision_directives
connectable = get_engine()
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=get_metadata(),
**conf_args
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@@ -1,24 +0,0 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@@ -1,46 +0,0 @@
"""Add position field to Content model
Revision ID: 54d8ece92767
Revises:
Create Date: 2025-06-29 15:32:30.794390
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '54d8ece92767'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('content', schema=None) as batch_op:
batch_op.add_column(sa.Column('position', sa.Integer(), nullable=True))
batch_op.alter_column('file_name',
existing_type=sa.VARCHAR(length=120),
type_=sa.String(length=255),
existing_nullable=False)
batch_op.alter_column('player_id',
existing_type=sa.INTEGER(),
nullable=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('content', schema=None) as batch_op:
batch_op.alter_column('player_id',
existing_type=sa.INTEGER(),
nullable=True)
batch_op.alter_column('file_name',
existing_type=sa.String(length=255),
type_=sa.VARCHAR(length=120),
existing_nullable=False)
batch_op.drop_column('position')
# ### end Alembic commands ###

View File

@@ -1,62 +0,0 @@
"""Add position field to Content model
Revision ID: c2aad6547472
Revises: 54d8ece92767
Create Date: 2025-06-29 15:58:57.678396
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'c2aad6547472'
down_revision = '54d8ece92767'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('player', schema=None) as batch_op:
batch_op.alter_column('username',
existing_type=sa.VARCHAR(length=100),
type_=sa.String(length=255),
existing_nullable=False)
batch_op.alter_column('hostname',
existing_type=sa.VARCHAR(length=100),
type_=sa.String(length=255),
existing_nullable=False)
batch_op.alter_column('password',
existing_type=sa.VARCHAR(length=200),
type_=sa.String(length=255),
existing_nullable=False)
batch_op.alter_column('quickconnect_password',
existing_type=sa.VARCHAR(length=200),
type_=sa.String(length=255),
existing_nullable=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('player', schema=None) as batch_op:
batch_op.alter_column('quickconnect_password',
existing_type=sa.String(length=255),
type_=sa.VARCHAR(length=200),
existing_nullable=True)
batch_op.alter_column('password',
existing_type=sa.String(length=255),
type_=sa.VARCHAR(length=200),
existing_nullable=False)
batch_op.alter_column('hostname',
existing_type=sa.String(length=255),
type_=sa.VARCHAR(length=100),
existing_nullable=False)
batch_op.alter_column('username',
existing_type=sa.String(length=255),
type_=sa.VARCHAR(length=100),
existing_nullable=False)
# ### end Alembic commands ###

View File

@@ -49,12 +49,24 @@
</select>
</div>
</div>
<div class="col-md-6 col-12">
<div class="mb-3">
<label for="orientation" class="form-label">Group Orientation</label>
<select class="form-control {{ 'dark-mode' if theme == 'dark' else '' }}" id="orientation" name="orientation" required>
<option value="Landscape" {% if group.orientation == 'Landscape' %}selected{% endif %}>Landscape</option>
<option value="Portret" {% if group.orientation == 'Portret' %}selected{% endif %}>Portret</option>
</select>
</div>
</div>
</div>
<!-- Add this above the player selection -->
<div class="alert alert-warning" role="alert">
<strong>Warning:</strong> Adding new players to this group will delete their individual playlists.
Removing players from the group will allow them to have their own playlists again.
</div>
<div id="orientation-warning" class="alert alert-danger d-none" role="alert">
No players with the selected orientation are available.
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary">Save Changes</button>
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary mt-3">Back to Dashboard</a>
@@ -62,5 +74,45 @@
</form>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Get all players and their orientations from the backend
const players = [
{% for player in players %}
{id: {{ player.id }}, username: "{{ player.username }}", orientation: "{{ player.orientation }}", inGroup: {% if player in group.players %}true{% else %}false{% endif %}},
{% endfor %}
];
const orientationSelect = document.getElementById('orientation');
const playersSelect = document.getElementById('players');
const orientationWarning = document.getElementById('orientation-warning');
function filterPlayers() {
const selectedOrientation = orientationSelect.value;
const currentSelection = Array.from(playersSelect.selectedOptions).map(option => option.value);
playersSelect.innerHTML = '';
let compatibleCount = 0;
players.forEach(player => {
if (player.orientation === selectedOrientation) {
const option = document.createElement('option');
option.value = player.id;
option.textContent = player.username;
// Re-select if it was previously selected
if (currentSelection.includes(player.id.toString()) || player.inGroup) {
option.selected = true;
}
playersSelect.appendChild(option);
compatibleCount++;
}
});
orientationWarning.classList.toggle('d-none', compatibleCount > 0);
}
orientationSelect.addEventListener('change', filterPlayers);
// Initial filter on page load
filterPlayers();
</script>
</body>
</html>

View File

@@ -189,7 +189,7 @@ document.addEventListener('DOMContentLoaded', function() {
});
// Send to server
fetch('{{ url_for("update_group_content_order", group_id=group.id) }}', {
fetch('{{ url_for("update_group_content_order_route", group_id=group.id) }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View File

@@ -35,15 +35,24 @@ def create_group(name, player_ids, orientation='Landscape'):
log_group_created(name)
return new_group
def edit_group(group_id, name, player_ids):
def edit_group(group_id, name, player_ids, orientation=None):
"""
Edit an existing group, updating its name and players.
Edit an existing group, updating its name, orientation, and players.
Handles locking/unlocking players appropriately.
"""
group = Group.query.get_or_404(group_id)
old_name = group.name # Store old name in case it changes
group.name = name
# Update orientation if provided
if orientation:
group.orientation = orientation
# Validate that all selected players have the matching orientation
for player_id in player_ids:
player = Player.query.get(player_id)
if player and player.orientation != orientation:
raise ValueError(f"Player '{player.username}' has orientation '{player.orientation}', which does not match group orientation '{orientation}'.")
# Get current players in the group
current_player_ids = [player.id for player in group.players]