Files
TimeTracker/migrations/versions/010_enforce_single_active_timer.py
T
Dries Peeters 7486037307 feat: local SQLite test env, UI fixes, and DB migrations
- UI/UX: Refine layouts and responsive styles; fix task and timer views; update
  shared components and dashboard templates
  - Updates across `app/templates/**`, `templates/**`, `app/static/base.css`,
    and `app/static/mobile.css`
- Backend: Route cleanups and minor fixes for admin, auth, invoices, and timer
  - Touches `app/routes/admin.py`, `app/routes/auth.py`, `app/routes/api.py`,
    `app/routes/invoices.py`, `app/routes/timer.py`
- DevOps: Improve Docker setup and add local testing workflow
  - Update `Dockerfile`, `docker/start-fixed.py`
  - Add `docker-compose.local-test.yml`, `.env.local-test`, start scripts
- Docs: Update `README.md` and add `docs/LOCAL_TESTING_WITH_SQLITE.md`
- Utilities: Adjust CLI and PDF generator behavior

Database (Alembic) migrations:
- 005_add_missing_columns.py
- 006_add_logo_and_task_timestamps.py
- 007_add_invoice_and_more_settings_columns.py
- 008_align_invoices_and_settings_more.py
- 009_add_invoice_created_by.py
- 010_enforce_single_active_timer.py

BREAKING CHANGE: Only one active timer per user is now enforced.

Note: Apply database migrations after deploy (e.g., `alembic upgrade head`).
2025-09-10 11:49:49 +02:00

74 lines
2.2 KiB
Python

"""enforce single active timer per user via partial unique index
Revision ID: 010
Revises: 009
Create Date: 2025-09-10 00:00:00
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '010'
down_revision = '009'
branch_labels = None
depends_on = None
def upgrade() -> None:
bind = op.get_bind()
dialect = bind.dialect.name
inspector = sa.inspect(bind)
if 'time_entries' not in inspector.get_table_names():
return
# Best-effort deduplication: close all but the most recent active timer per user
try:
if dialect in ('postgresql', 'sqlite'):
# For backends supporting CTE with window functions in UPDATE
op.execute(
sa.text(
"""
WITH ranked AS (
SELECT id, user_id,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY start_time DESC, id DESC) AS rn
FROM time_entries
WHERE end_time IS NULL
)
UPDATE time_entries
SET end_time = start_time
WHERE id IN (SELECT id FROM ranked WHERE rn > 1)
"""
)
)
except Exception:
# Ignore and proceed; server-side logic already prevents future duplicates
pass
# Create partial unique index to enforce at DB level
if dialect in ('postgresql', 'sqlite'):
try:
op.execute(
sa.text(
"CREATE UNIQUE INDEX IF NOT EXISTS ux_time_entries_one_active_per_user ON time_entries(user_id) WHERE end_time IS NULL"
)
)
except Exception:
# If index exists or backend doesn't support partial indexes, ignore
pass
def downgrade() -> None:
bind = op.get_bind()
dialect = bind.dialect.name
if dialect in ('postgresql', 'sqlite'):
try:
op.execute(sa.text("DROP INDEX IF EXISTS ux_time_entries_one_active_per_user"))
except Exception:
# SQLite may require table-qualified index drop or ignore if absent
pass