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': if request.method == 'POST':
group_name = request.form['name'] group_name = request.form['name']
player_ids = request.form.getlist('players') 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') flash(f'Group "{group_name}" created successfully.', 'success')
return redirect(url_for('dashboard')) return redirect(url_for('dashboard'))
players = Player.query.all() players = Player.query.all()
@@ -514,7 +515,8 @@ def edit_group(group_id):
if request.method == 'POST': if request.method == 'POST':
name = request.form['name'] name = request.form['name']
player_ids = request.form.getlist('players') 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') flash(f'Group "{name}" updated successfully.', 'success')
return redirect(url_for('dashboard')) return redirect(url_for('dashboard'))
players = Player.query.all() players = Player.query.all()

View File

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

View File

@@ -7,16 +7,7 @@ mkdir -p instance
# Check if database exists # Check if database exists
if [ ! -f instance/dashboard.db ]; then if [ ! -f instance/dashboard.db ]; then
echo "No database found, initializing..." echo "No database found, creating fresh database..."
# 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
# Create admin user if environment variables are set # Create admin user if environment variables are set
if [ -n "$ADMIN_USER" ] && [ -n "$ADMIN_PASSWORD" ]; then 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" echo "Warning: ADMIN_USER or ADMIN_PASSWORD not set, skipping admin creation"
fi fi
else else
echo "Existing database found, applying migrations..." echo "Existing database found, skipping initialization..."
flask db upgrade 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 fi
echo "Starting DigiServer..." 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> </select>
</div> </div>
</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> </div>
<!-- Add this above the player selection --> <!-- Add this above the player selection -->
<div class="alert alert-warning" role="alert"> <div class="alert alert-warning" role="alert">
<strong>Warning:</strong> Adding new players to this group will delete their individual playlists. <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. Removing players from the group will allow them to have their own playlists again.
</div> </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"> <div class="text-center">
<button type="submit" class="btn btn-primary">Save Changes</button> <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> <a href="{{ url_for('dashboard') }}" class="btn btn-secondary mt-3">Back to Dashboard</a>
@@ -62,5 +74,45 @@
</form> </form>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js"></script> <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> </body>
</html> </html>

View File

@@ -189,7 +189,7 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
// Send to server // 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', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@@ -35,15 +35,24 @@ def create_group(name, player_ids, orientation='Landscape'):
log_group_created(name) log_group_created(name)
return new_group 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. Handles locking/unlocking players appropriately.
""" """
group = Group.query.get_or_404(group_id) group = Group.query.get_or_404(group_id)
old_name = group.name # Store old name in case it changes old_name = group.name # Store old name in case it changes
group.name = name 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 # Get current players in the group
current_player_ids = [player.id for player in group.players] current_player_ids = [player.id for player in group.players]