feat: refine subject visits and project workflows

Add early termination visit workflow with ordering, non-applicable visit handling, visit window display, and medication adherence support.

Extend monitoring visit issue template fields, site scoping, setup draft project info handling, login security UI, attachment behavior, and related tests/migrations.
This commit is contained in:
Cheng Zhou
2026-05-09 17:10:34 +08:00
parent 74feca4467
commit 917ab7ccf1
41 changed files with 3463 additions and 701 deletions
@@ -0,0 +1,59 @@
"""add template fields to monitoring_visit_issues
Revision ID: 20260509_01
Revises: 20260508_03
Create Date: 2026-05-09 13:45:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "20260509_01"
down_revision: Union[str, None] = "20260508_03"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
bind = op.get_bind()
inspector = sa.inspect(bind)
columns = {col["name"] for col in inspector.get_columns("monitoring_visit_issues")}
if "severity" not in columns:
op.add_column("monitoring_visit_issues", sa.Column("severity", sa.String(length=64), nullable=True))
if "mark" not in columns:
op.add_column("monitoring_visit_issues", sa.Column("mark", sa.String(length=100), nullable=True))
if "visit_cycle" not in columns:
op.add_column("monitoring_visit_issues", sa.Column("visit_cycle", sa.String(length=100), nullable=True))
if "center_query" not in columns:
op.add_column("monitoring_visit_issues", sa.Column("center_query", sa.Text(), nullable=True))
if "center_latest_reply" not in columns:
op.add_column("monitoring_visit_issues", sa.Column("center_latest_reply", sa.Text(), nullable=True))
if "rectification_completed" not in columns:
op.add_column(
"monitoring_visit_issues",
sa.Column("rectification_completed", sa.Boolean(), nullable=False, server_default=sa.text("false")),
)
def downgrade() -> None:
bind = op.get_bind()
inspector = sa.inspect(bind)
columns = {col["name"] for col in inspector.get_columns("monitoring_visit_issues")}
if "rectification_completed" in columns:
op.drop_column("monitoring_visit_issues", "rectification_completed")
if "center_latest_reply" in columns:
op.drop_column("monitoring_visit_issues", "center_latest_reply")
if "center_query" in columns:
op.drop_column("monitoring_visit_issues", "center_query")
if "visit_cycle" in columns:
op.drop_column("monitoring_visit_issues", "visit_cycle")
if "mark" in columns:
op.drop_column("monitoring_visit_issues", "mark")
if "severity" in columns:
op.drop_column("monitoring_visit_issues", "severity")
@@ -0,0 +1,55 @@
"""add site_id to monitoring_visit_issues
Revision ID: 20260509_02
Revises: 20260509_01
Create Date: 2026-05-09 14:30:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = "20260509_02"
down_revision: Union[str, None] = "20260509_01"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
bind = op.get_bind()
inspector = sa.inspect(bind)
columns = {col["name"] for col in inspector.get_columns("monitoring_visit_issues")}
fks = {fk["name"] for fk in inspector.get_foreign_keys("monitoring_visit_issues")}
indexes = {idx["name"] for idx in inspector.get_indexes("monitoring_visit_issues")}
if "site_id" not in columns:
op.add_column("monitoring_visit_issues", sa.Column("site_id", postgresql.UUID(as_uuid=True), nullable=True))
if "fk_monitoring_visit_issues_site_id" not in fks:
op.create_foreign_key(
"fk_monitoring_visit_issues_site_id",
"monitoring_visit_issues",
"sites",
["site_id"],
["id"],
)
if "ix_monitoring_visit_issues_site_id" not in indexes:
op.create_index("ix_monitoring_visit_issues_site_id", "monitoring_visit_issues", ["site_id"])
def downgrade() -> None:
bind = op.get_bind()
inspector = sa.inspect(bind)
columns = {col["name"] for col in inspector.get_columns("monitoring_visit_issues")}
fks = {fk["name"] for fk in inspector.get_foreign_keys("monitoring_visit_issues")}
indexes = {idx["name"] for idx in inspector.get_indexes("monitoring_visit_issues")}
if "ix_monitoring_visit_issues_site_id" in indexes:
op.drop_index("ix_monitoring_visit_issues_site_id", table_name="monitoring_visit_issues")
if "fk_monitoring_visit_issues_site_id" in fks:
op.drop_constraint("fk_monitoring_visit_issues_site_id", "monitoring_visit_issues", type_="foreignkey")
if "site_id" in columns:
op.drop_column("monitoring_visit_issues", "site_id")
@@ -0,0 +1,34 @@
"""add subject actual medication count
Revision ID: 20260509_03
Revises: 20260509_02
Create Date: 2026-05-09 14:58:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "20260509_03"
down_revision: Union[str, None] = "20260509_02"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
bind = op.get_bind()
inspector = sa.inspect(bind)
columns = {col["name"] for col in inspector.get_columns("subjects")}
if "actual_medication_count" not in columns:
op.add_column("subjects", sa.Column("actual_medication_count", sa.Integer(), nullable=True))
def downgrade() -> None:
bind = op.get_bind()
inspector = sa.inspect(bind)
columns = {col["name"] for col in inspector.get_columns("subjects")}
if "actual_medication_count" in columns:
op.drop_column("subjects", "actual_medication_count")