Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""add_reference_url_to_checklist_items

Revision ID: 74ae746f13e6
Revises: b465db6a13ac
Create Date: 2026-02-25 22:21:23.947814

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '74ae746f13e6'
down_revision: Union[str, None] = 'b465db6a13ac'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('checklist_items', schema=None) as batch_op:
batch_op.add_column(sa.Column('reference_url', sa.String(length=500), nullable=True))

# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('checklist_items', schema=None) as batch_op:
batch_op.drop_column('reference_url')

# ### end Alembic commands ###
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""add_sop_fields_to_checklist_items

Revision ID: b465db6a13ac
Revises: ffbd6edaf13b
Create Date: 2026-02-25 21:59:55.302075

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = 'b465db6a13ac'
down_revision: Union[str, None] = 'ffbd6edaf13b'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('checklist_items', schema=None) as batch_op:
batch_op.add_column(sa.Column('description', sa.Text(), nullable=True))
batch_op.add_column(sa.Column('is_mandatory', sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column('responsible_role', sa.String(length=100), nullable=True))
batch_op.add_column(sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True))

with op.batch_alter_table('checklist_template_items', schema=None) as batch_op:
batch_op.add_column(sa.Column('is_mandatory', sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column('responsible_role', sa.String(length=100), nullable=True))
batch_op.add_column(sa.Column('deadline_offset_days', sa.Integer(), nullable=True))
batch_op.add_column(sa.Column('estimated_hours', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('reference_url', sa.String(length=500), nullable=True))

# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('checklist_template_items', schema=None) as batch_op:
batch_op.drop_column('reference_url')
batch_op.drop_column('estimated_hours')
batch_op.drop_column('deadline_offset_days')
batch_op.drop_column('responsible_role')
batch_op.drop_column('is_mandatory')

with op.batch_alter_table('checklist_items', schema=None) as batch_op:
batch_op.drop_column('completed_at')
batch_op.drop_column('responsible_role')
batch_op.drop_column('is_mandatory')
batch_op.drop_column('description')

# ### end Alembic commands ###
106 changes: 103 additions & 3 deletions backend/app/api/event_templates.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import or_
from sqlalchemy.orm import Session

from app.core.dependencies import get_current_user
from app.database import get_db
from app.models import User
from app.models.event import ChecklistTemplateItem, EventTemplate
from app.schemas.event import EventTemplateCreate, EventTemplateListOut, EventTemplateOut, EventTemplateUpdate
from app.schemas.event import (
ChecklistTemplateItemCreate,
ChecklistTemplateItemOut,
ChecklistTemplateItemUpdate,
EventTemplateCreate,
EventTemplateListOut,
EventTemplateOut,
EventTemplateUpdate,
)

router = APIRouter()

Expand All @@ -17,7 +26,12 @@ def list_templates(
):
return (
db.query(EventTemplate)
.filter(EventTemplate.is_public == True) # noqa: E712
.filter(
or_(
EventTemplate.is_public == True, # noqa: E712
EventTemplate.created_by_id == current_user.id,
)
)
.order_by(EventTemplate.created_at.desc())
.all()
)
Expand Down Expand Up @@ -58,7 +72,7 @@ def get_template(
template = db.query(EventTemplate).filter(EventTemplate.id == template_id).first()
if not template:
raise HTTPException(404, "模板不存在")
if not template.is_public:
if not template.is_public and template.created_by_id != current_user.id:
raise HTTPException(403, "无权访问此模板")
return template

Expand All @@ -73,8 +87,94 @@ def update_template(
template = db.query(EventTemplate).filter(EventTemplate.id == template_id).first()
if not template:
raise HTTPException(404, "模板不存在")
if template.created_by_id != current_user.id and not current_user.is_superuser:
raise HTTPException(403, "无权修改此模板")
for key, value in data.model_dump(exclude_unset=True).items():
setattr(template, key, value)
db.commit()
db.refresh(template)
return template


@router.delete("/{template_id}", status_code=204)
def delete_template(
template_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
template = db.query(EventTemplate).filter(EventTemplate.id == template_id).first()
if not template:
raise HTTPException(404, "模板不存在")
if template.created_by_id != current_user.id and not current_user.is_superuser:
raise HTTPException(403, "无权删除此模板")
db.delete(template)
db.commit()


# ─── Template Checklist Item CRUD ─────────────────────────────────────────────

@router.post("/{template_id}/items", response_model=ChecklistTemplateItemOut, status_code=201)
def add_template_item(
template_id: int,
data: ChecklistTemplateItemCreate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
template = db.query(EventTemplate).filter(EventTemplate.id == template_id).first()
if not template:
raise HTTPException(404, "模板不存在")
if template.created_by_id != current_user.id and not current_user.is_superuser:
raise HTTPException(403, "无权修改此模板")
item = ChecklistTemplateItem(template_id=template_id, **data.model_dump())
db.add(item)
db.commit()
db.refresh(item)
return item


@router.patch("/{template_id}/items/{item_id}", response_model=ChecklistTemplateItemOut)
def update_template_item(
template_id: int,
item_id: int,
data: ChecklistTemplateItemUpdate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
template = db.query(EventTemplate).filter(EventTemplate.id == template_id).first()
if not template:
raise HTTPException(404, "模板不存在")
if template.created_by_id != current_user.id and not current_user.is_superuser:
raise HTTPException(403, "无权修改此模板")
item = db.query(ChecklistTemplateItem).filter(
ChecklistTemplateItem.id == item_id,
ChecklistTemplateItem.template_id == template_id,
).first()
if not item:
raise HTTPException(404, "条目不存在")
for key, value in data.model_dump(exclude_unset=True).items():
setattr(item, key, value)
db.commit()
db.refresh(item)
return item


@router.delete("/{template_id}/items/{item_id}", status_code=204)
def delete_template_item(
template_id: int,
item_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
template = db.query(EventTemplate).filter(EventTemplate.id == template_id).first()
if not template:
raise HTTPException(404, "模板不存在")
if template.created_by_id != current_user.id and not current_user.is_superuser:
raise HTTPException(403, "无权修改此模板")
item = db.query(ChecklistTemplateItem).filter(
ChecklistTemplateItem.id == item_id,
ChecklistTemplateItem.template_id == template_id,
).first()
if not item:
raise HTTPException(404, "条目不存在")
db.delete(item)
db.commit()
49 changes: 49 additions & 0 deletions backend/app/api/events.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from datetime import timedelta

from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session

from app.core.dependencies import get_current_user
from app.core.timezone import utc_now
from app.database import get_db
from app.models import User
from app.models.community import Community
Expand All @@ -16,6 +19,7 @@
IssueLink,
)
from app.schemas.event import (
ChecklistItemCreate,
ChecklistItemOut,
ChecklistItemUpdate,
EventCreate,
Expand Down Expand Up @@ -99,10 +103,18 @@ def create_event(
template = db.query(EventTemplate).filter(EventTemplate.id == data.template_id).first()
if template:
for titem in template.checklist_items:
due = None
if titem.deadline_offset_days is not None and event.planned_at:
due = (event.planned_at + timedelta(days=titem.deadline_offset_days)).date()
db.add(ChecklistItem(
event_id=event.id,
phase=titem.phase,
title=titem.title,
description=titem.description,
is_mandatory=titem.is_mandatory,
responsible_role=titem.responsible_role,
reference_url=titem.reference_url,
due_date=due,
order=titem.order,
))

Expand Down Expand Up @@ -206,11 +218,48 @@ def update_checklist_item(
raise HTTPException(404, "检查项不存在")
for key, value in data.model_dump(exclude_unset=True).items():
setattr(item, key, value)
if data.status == "done" and item.completed_at is None:
item.completed_at = utc_now()
elif data.status in ("pending", "skipped"):
item.completed_at = None
db.commit()
db.refresh(item)
return item


@router.post("/{event_id}/checklist", response_model=ChecklistItemOut, status_code=201)
def create_checklist_item(
event_id: int,
data: ChecklistItemCreate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
event = db.query(Event).filter(Event.id == event_id).first()
if not event:
raise HTTPException(404, "活动不存在")
item = ChecklistItem(event_id=event_id, **data.model_dump())
db.add(item)
db.commit()
db.refresh(item)
return item


@router.delete("/{event_id}/checklist/{item_id}", status_code=204)
def delete_checklist_item(
event_id: int,
item_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
item = db.query(ChecklistItem).filter(
ChecklistItem.id == item_id, ChecklistItem.event_id == event_id
).first()
if not item:
raise HTTPException(404, "检查项不存在")
db.delete(item)
db.commit()


# ─── Personnel ────────────────────────────────────────────────────────────────

@router.get("/{event_id}/personnel", response_model=list[EventPersonnelOut])
Expand Down
10 changes: 10 additions & 0 deletions backend/app/models/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ class ChecklistTemplateItem(Base):
)
title = Column(String(300), nullable=False)
description = Column(Text, nullable=True)
is_mandatory = Column(Boolean, default=False)
responsible_role = Column(String(100), nullable=True)
deadline_offset_days = Column(Integer, nullable=True)
estimated_hours = Column(Float, nullable=True)
reference_url = Column(String(500), nullable=True)
order = Column(Integer, default=0)

template = relationship("EventTemplate", back_populates="checklist_items")
Expand Down Expand Up @@ -127,12 +132,17 @@ class ChecklistItem(Base):
SAEnum("pre", "during", "post", name="checklist_item_phase_enum"), nullable=False
)
title = Column(String(300), nullable=False)
description = Column(Text, nullable=True)
is_mandatory = Column(Boolean, default=False)
responsible_role = Column(String(100), nullable=True)
reference_url = Column(String(500), nullable=True)
status = Column(
SAEnum("pending", "done", "skipped", name="checklist_status_enum"), default="pending"
)
assignee_id = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
due_date = Column(Date, nullable=True)
notes = Column(Text, nullable=True)
completed_at = Column(DateTime(timezone=True), nullable=True)
order = Column(Integer, default=0)

event = relationship("Event", back_populates="checklist_items")
Expand Down
Loading
Loading