mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-04-29 00:40:06 -05:00
7486037307
- 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`).
74 lines
2.2 KiB
Python
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
|
|
|
|
|