fix(core): stabilize editor lifecycle, transactional versions, and runtime config
This commit is contained in:
@@ -8,6 +8,49 @@ from app.models.scheme_group import SchemeGroupRecord
|
||||
from app.models.scheme_seat import SchemeSeatRecord
|
||||
|
||||
|
||||
def _conflict(message: str) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail={
|
||||
"code": "group_uniqueness_violation",
|
||||
"message": message,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def _ensure_group_uniqueness(
|
||||
*,
|
||||
session,
|
||||
scheme_version_id: str,
|
||||
group_id: str | None,
|
||||
element_id: str | None,
|
||||
exclude_group_record_id: str | None = None,
|
||||
) -> None:
|
||||
if group_id:
|
||||
stmt = select(SchemeGroupRecord).where(
|
||||
SchemeGroupRecord.scheme_version_id == scheme_version_id,
|
||||
SchemeGroupRecord.group_id == group_id,
|
||||
)
|
||||
if exclude_group_record_id:
|
||||
stmt = stmt.where(SchemeGroupRecord.group_record_id != exclude_group_record_id)
|
||||
|
||||
existing = (await session.execute(stmt)).scalar_one_or_none()
|
||||
if existing is not None:
|
||||
raise _conflict(f"Group with group_id='{group_id}' already exists in current draft version")
|
||||
|
||||
if element_id:
|
||||
stmt = select(SchemeGroupRecord).where(
|
||||
SchemeGroupRecord.scheme_version_id == scheme_version_id,
|
||||
SchemeGroupRecord.element_id == element_id,
|
||||
)
|
||||
if exclude_group_record_id:
|
||||
stmt = stmt.where(SchemeGroupRecord.group_record_id != exclude_group_record_id)
|
||||
|
||||
existing = (await session.execute(stmt)).scalar_one_or_none()
|
||||
if existing is not None:
|
||||
raise _conflict(f"Group with element_id='{element_id}' already exists in current draft version")
|
||||
|
||||
|
||||
async def replace_scheme_version_groups(
|
||||
*,
|
||||
scheme_id: str,
|
||||
@@ -23,13 +66,29 @@ async def replace_scheme_version_groups(
|
||||
for row in existing_rows:
|
||||
await session.delete(row)
|
||||
|
||||
seen_group_ids: set[str] = set()
|
||||
seen_element_ids: set[str] = set()
|
||||
|
||||
for item in groups:
|
||||
group_id = item.get("group_id")
|
||||
element_id = item.get("id")
|
||||
|
||||
if group_id:
|
||||
if group_id in seen_group_ids:
|
||||
raise _conflict(f"Duplicate group_id='{group_id}' in replacement payload")
|
||||
seen_group_ids.add(group_id)
|
||||
|
||||
if element_id:
|
||||
if element_id in seen_element_ids:
|
||||
raise _conflict(f"Duplicate element_id='{element_id}' in replacement payload")
|
||||
seen_element_ids.add(element_id)
|
||||
|
||||
row = SchemeGroupRecord(
|
||||
group_record_id=item["group_record_id"] if "group_record_id" in item and item["group_record_id"] else uuid4().hex,
|
||||
scheme_id=scheme_id,
|
||||
scheme_version_id=scheme_version_id,
|
||||
element_id=item.get("id"),
|
||||
group_id=item.get("group_id"),
|
||||
element_id=element_id,
|
||||
group_id=group_id,
|
||||
name=item.get("group_id"),
|
||||
classes_raw=str(item.get("classes")),
|
||||
)
|
||||
@@ -44,26 +103,51 @@ async def clone_scheme_version_groups(
|
||||
target_scheme_version_id: str,
|
||||
) -> None:
|
||||
async with AsyncSessionLocal() as session:
|
||||
result = await session.execute(
|
||||
select(SchemeGroupRecord).where(SchemeGroupRecord.scheme_version_id == source_scheme_version_id)
|
||||
await clone_scheme_version_groups_in_session(
|
||||
session=session,
|
||||
source_scheme_version_id=source_scheme_version_id,
|
||||
target_scheme_version_id=target_scheme_version_id,
|
||||
)
|
||||
rows = list(result.scalars().all())
|
||||
|
||||
for row in rows:
|
||||
cloned = SchemeGroupRecord(
|
||||
group_record_id=uuid4().hex,
|
||||
scheme_id=row.scheme_id,
|
||||
scheme_version_id=target_scheme_version_id,
|
||||
element_id=row.element_id,
|
||||
group_id=row.group_id,
|
||||
name=row.name,
|
||||
classes_raw=row.classes_raw,
|
||||
)
|
||||
session.add(cloned)
|
||||
|
||||
await session.commit()
|
||||
|
||||
|
||||
async def clone_scheme_version_groups_in_session(
|
||||
*,
|
||||
session,
|
||||
source_scheme_version_id: str,
|
||||
target_scheme_version_id: str,
|
||||
) -> None:
|
||||
result = await session.execute(
|
||||
select(SchemeGroupRecord).where(SchemeGroupRecord.scheme_version_id == source_scheme_version_id)
|
||||
)
|
||||
rows = list(result.scalars().all())
|
||||
|
||||
seen_group_ids: set[str] = set()
|
||||
seen_element_ids: set[str] = set()
|
||||
|
||||
for row in rows:
|
||||
if row.group_id:
|
||||
if row.group_id in seen_group_ids:
|
||||
raise _conflict(f"Duplicate group_id='{row.group_id}' while cloning draft")
|
||||
seen_group_ids.add(row.group_id)
|
||||
|
||||
if row.element_id:
|
||||
if row.element_id in seen_element_ids:
|
||||
raise _conflict(f"Duplicate element_id='{row.element_id}' while cloning draft")
|
||||
seen_element_ids.add(row.element_id)
|
||||
|
||||
cloned = SchemeGroupRecord(
|
||||
group_record_id=uuid4().hex,
|
||||
scheme_id=row.scheme_id,
|
||||
scheme_version_id=target_scheme_version_id,
|
||||
element_id=row.element_id,
|
||||
group_id=row.group_id,
|
||||
name=row.name,
|
||||
classes_raw=row.classes_raw,
|
||||
)
|
||||
session.add(cloned)
|
||||
|
||||
|
||||
async def list_scheme_version_groups(scheme_version_id: str) -> list[SchemeGroupRecord]:
|
||||
async with AsyncSessionLocal() as session:
|
||||
result = await session.execute(
|
||||
@@ -78,8 +162,7 @@ async def update_scheme_version_group_by_record_id(
|
||||
*,
|
||||
scheme_version_id: str,
|
||||
group_record_id: str,
|
||||
group_id: str | None,
|
||||
name: str | None,
|
||||
**update_data,
|
||||
) -> tuple[SchemeGroupRecord, str | None]:
|
||||
async with AsyncSessionLocal() as session:
|
||||
result = await session.execute(
|
||||
@@ -96,9 +179,20 @@ async def update_scheme_version_group_by_record_id(
|
||||
detail="Group record not found in current draft version",
|
||||
)
|
||||
|
||||
if "group_id" in update_data:
|
||||
await _ensure_group_uniqueness(
|
||||
session=session,
|
||||
scheme_version_id=scheme_version_id,
|
||||
group_id=update_data["group_id"],
|
||||
element_id=row.element_id,
|
||||
exclude_group_record_id=group_record_id,
|
||||
)
|
||||
|
||||
old_group_id = row.group_id
|
||||
row.group_id = group_id
|
||||
row.name = name
|
||||
if "group_id" in update_data:
|
||||
row.group_id = update_data["group_id"]
|
||||
if "name" in update_data:
|
||||
row.name = update_data["name"]
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(row)
|
||||
@@ -115,6 +209,13 @@ async def create_scheme_version_group(
|
||||
classes_raw: str | None,
|
||||
) -> SchemeGroupRecord:
|
||||
async with AsyncSessionLocal() as session:
|
||||
await _ensure_group_uniqueness(
|
||||
session=session,
|
||||
scheme_version_id=scheme_version_id,
|
||||
group_id=group_id,
|
||||
element_id=element_id,
|
||||
)
|
||||
|
||||
row = SchemeGroupRecord(
|
||||
group_record_id=uuid4().hex,
|
||||
scheme_id=scheme_id,
|
||||
|
||||
@@ -51,36 +51,48 @@ async def clone_scheme_version_seats(
|
||||
target_scheme_version_id: str,
|
||||
) -> None:
|
||||
async with AsyncSessionLocal() as session:
|
||||
result = await session.execute(
|
||||
select(SchemeSeatRecord).where(SchemeSeatRecord.scheme_version_id == source_scheme_version_id)
|
||||
await clone_scheme_version_seats_in_session(
|
||||
session=session,
|
||||
source_scheme_version_id=source_scheme_version_id,
|
||||
target_scheme_version_id=target_scheme_version_id,
|
||||
)
|
||||
rows = list(result.scalars().all())
|
||||
|
||||
for row in rows:
|
||||
cloned = SchemeSeatRecord(
|
||||
seat_record_id=__import__("uuid").uuid4().hex,
|
||||
scheme_id=row.scheme_id,
|
||||
scheme_version_id=target_scheme_version_id,
|
||||
element_id=row.element_id,
|
||||
seat_id=row.seat_id,
|
||||
sector_id=row.sector_id,
|
||||
group_id=row.group_id,
|
||||
row_label=row.row_label,
|
||||
seat_number=row.seat_number,
|
||||
tag=row.tag,
|
||||
classes_raw=row.classes_raw,
|
||||
x=row.x,
|
||||
y=row.y,
|
||||
cx=row.cx,
|
||||
cy=row.cy,
|
||||
width=row.width,
|
||||
height=row.height,
|
||||
)
|
||||
session.add(cloned)
|
||||
|
||||
await session.commit()
|
||||
|
||||
|
||||
async def clone_scheme_version_seats_in_session(
|
||||
*,
|
||||
session,
|
||||
source_scheme_version_id: str,
|
||||
target_scheme_version_id: str,
|
||||
) -> None:
|
||||
result = await session.execute(
|
||||
select(SchemeSeatRecord).where(SchemeSeatRecord.scheme_version_id == source_scheme_version_id)
|
||||
)
|
||||
rows = list(result.scalars().all())
|
||||
|
||||
for row in rows:
|
||||
cloned = SchemeSeatRecord(
|
||||
seat_record_id=__import__("uuid").uuid4().hex,
|
||||
scheme_id=row.scheme_id,
|
||||
scheme_version_id=target_scheme_version_id,
|
||||
element_id=row.element_id,
|
||||
seat_id=row.seat_id,
|
||||
sector_id=row.sector_id,
|
||||
group_id=row.group_id,
|
||||
row_label=row.row_label,
|
||||
seat_number=row.seat_number,
|
||||
tag=row.tag,
|
||||
classes_raw=row.classes_raw,
|
||||
x=row.x,
|
||||
y=row.y,
|
||||
cx=row.cx,
|
||||
cy=row.cy,
|
||||
width=row.width,
|
||||
height=row.height,
|
||||
)
|
||||
session.add(cloned)
|
||||
|
||||
|
||||
async def list_scheme_version_seats(scheme_version_id: str) -> list[SchemeSeatRecord]:
|
||||
async with AsyncSessionLocal() as session:
|
||||
result = await session.execute(
|
||||
@@ -141,11 +153,7 @@ async def update_scheme_version_seat_by_record_id(
|
||||
*,
|
||||
scheme_version_id: str,
|
||||
seat_record_id: str,
|
||||
seat_id: str | None,
|
||||
sector_id: str | None,
|
||||
group_id: str | None,
|
||||
row_label: str | None,
|
||||
seat_number: str | None,
|
||||
**update_data,
|
||||
) -> SchemeSeatRecord:
|
||||
async with AsyncSessionLocal() as session:
|
||||
result = await session.execute(
|
||||
@@ -162,11 +170,16 @@ async def update_scheme_version_seat_by_record_id(
|
||||
detail="Seat record not found in current draft version",
|
||||
)
|
||||
|
||||
row.seat_id = seat_id
|
||||
row.sector_id = sector_id
|
||||
row.group_id = group_id
|
||||
row.row_label = row_label
|
||||
row.seat_number = seat_number
|
||||
if "seat_id" in update_data:
|
||||
row.seat_id = update_data["seat_id"]
|
||||
if "sector_id" in update_data:
|
||||
row.sector_id = update_data["sector_id"]
|
||||
if "group_id" in update_data:
|
||||
row.group_id = update_data["group_id"]
|
||||
if "row_label" in update_data:
|
||||
row.row_label = update_data["row_label"]
|
||||
if "seat_number" in update_data:
|
||||
row.seat_number = update_data["seat_number"]
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(row)
|
||||
@@ -196,11 +209,16 @@ async def bulk_update_scheme_version_seats_by_record_id(
|
||||
detail=f"Seat record not found in current draft version: {item['seat_record_id']}",
|
||||
)
|
||||
|
||||
row.seat_id = item.get("seat_id")
|
||||
row.sector_id = item.get("sector_id")
|
||||
row.group_id = item.get("group_id")
|
||||
row.row_label = item.get("row_label")
|
||||
row.seat_number = item.get("seat_number")
|
||||
if "seat_id" in item:
|
||||
row.seat_id = item["seat_id"]
|
||||
if "sector_id" in item:
|
||||
row.sector_id = item["sector_id"]
|
||||
if "group_id" in item:
|
||||
row.group_id = item["group_id"]
|
||||
if "row_label" in item:
|
||||
row.row_label = item["row_label"]
|
||||
if "seat_number" in item:
|
||||
row.seat_number = item["seat_number"]
|
||||
updated_rows.append(row)
|
||||
|
||||
await session.commit()
|
||||
|
||||
@@ -8,6 +8,49 @@ from app.models.scheme_sector import SchemeSectorRecord
|
||||
from app.models.scheme_seat import SchemeSeatRecord
|
||||
|
||||
|
||||
def _conflict(message: str) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail={
|
||||
"code": "sector_uniqueness_violation",
|
||||
"message": message,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def _ensure_sector_uniqueness(
|
||||
*,
|
||||
session,
|
||||
scheme_version_id: str,
|
||||
sector_id: str | None,
|
||||
element_id: str | None,
|
||||
exclude_sector_record_id: str | None = None,
|
||||
) -> None:
|
||||
if sector_id:
|
||||
stmt = select(SchemeSectorRecord).where(
|
||||
SchemeSectorRecord.scheme_version_id == scheme_version_id,
|
||||
SchemeSectorRecord.sector_id == sector_id,
|
||||
)
|
||||
if exclude_sector_record_id:
|
||||
stmt = stmt.where(SchemeSectorRecord.sector_record_id != exclude_sector_record_id)
|
||||
|
||||
existing = (await session.execute(stmt)).scalar_one_or_none()
|
||||
if existing is not None:
|
||||
raise _conflict(f"Sector with sector_id='{sector_id}' already exists in current draft version")
|
||||
|
||||
if element_id:
|
||||
stmt = select(SchemeSectorRecord).where(
|
||||
SchemeSectorRecord.scheme_version_id == scheme_version_id,
|
||||
SchemeSectorRecord.element_id == element_id,
|
||||
)
|
||||
if exclude_sector_record_id:
|
||||
stmt = stmt.where(SchemeSectorRecord.sector_record_id != exclude_sector_record_id)
|
||||
|
||||
existing = (await session.execute(stmt)).scalar_one_or_none()
|
||||
if existing is not None:
|
||||
raise _conflict(f"Sector with element_id='{element_id}' already exists in current draft version")
|
||||
|
||||
|
||||
async def replace_scheme_version_sectors(
|
||||
*,
|
||||
scheme_id: str,
|
||||
@@ -23,13 +66,29 @@ async def replace_scheme_version_sectors(
|
||||
for row in existing_rows:
|
||||
await session.delete(row)
|
||||
|
||||
seen_sector_ids: set[str] = set()
|
||||
seen_element_ids: set[str] = set()
|
||||
|
||||
for item in sectors:
|
||||
sector_id = item.get("sector_id")
|
||||
element_id = item.get("id")
|
||||
|
||||
if sector_id:
|
||||
if sector_id in seen_sector_ids:
|
||||
raise _conflict(f"Duplicate sector_id='{sector_id}' in replacement payload")
|
||||
seen_sector_ids.add(sector_id)
|
||||
|
||||
if element_id:
|
||||
if element_id in seen_element_ids:
|
||||
raise _conflict(f"Duplicate element_id='{element_id}' in replacement payload")
|
||||
seen_element_ids.add(element_id)
|
||||
|
||||
row = SchemeSectorRecord(
|
||||
sector_record_id=item["sector_record_id"] if "sector_record_id" in item and item["sector_record_id"] else uuid4().hex,
|
||||
scheme_id=scheme_id,
|
||||
scheme_version_id=scheme_version_id,
|
||||
element_id=item.get("id"),
|
||||
sector_id=item.get("sector_id"),
|
||||
element_id=element_id,
|
||||
sector_id=sector_id,
|
||||
name=item.get("sector_id"),
|
||||
classes_raw=str(item.get("classes")),
|
||||
)
|
||||
@@ -44,26 +103,51 @@ async def clone_scheme_version_sectors(
|
||||
target_scheme_version_id: str,
|
||||
) -> None:
|
||||
async with AsyncSessionLocal() as session:
|
||||
result = await session.execute(
|
||||
select(SchemeSectorRecord).where(SchemeSectorRecord.scheme_version_id == source_scheme_version_id)
|
||||
await clone_scheme_version_sectors_in_session(
|
||||
session=session,
|
||||
source_scheme_version_id=source_scheme_version_id,
|
||||
target_scheme_version_id=target_scheme_version_id,
|
||||
)
|
||||
rows = list(result.scalars().all())
|
||||
|
||||
for row in rows:
|
||||
cloned = SchemeSectorRecord(
|
||||
sector_record_id=uuid4().hex,
|
||||
scheme_id=row.scheme_id,
|
||||
scheme_version_id=target_scheme_version_id,
|
||||
element_id=row.element_id,
|
||||
sector_id=row.sector_id,
|
||||
name=row.name,
|
||||
classes_raw=row.classes_raw,
|
||||
)
|
||||
session.add(cloned)
|
||||
|
||||
await session.commit()
|
||||
|
||||
|
||||
async def clone_scheme_version_sectors_in_session(
|
||||
*,
|
||||
session,
|
||||
source_scheme_version_id: str,
|
||||
target_scheme_version_id: str,
|
||||
) -> None:
|
||||
result = await session.execute(
|
||||
select(SchemeSectorRecord).where(SchemeSectorRecord.scheme_version_id == source_scheme_version_id)
|
||||
)
|
||||
rows = list(result.scalars().all())
|
||||
|
||||
seen_sector_ids: set[str] = set()
|
||||
seen_element_ids: set[str] = set()
|
||||
|
||||
for row in rows:
|
||||
if row.sector_id:
|
||||
if row.sector_id in seen_sector_ids:
|
||||
raise _conflict(f"Duplicate sector_id='{row.sector_id}' while cloning draft")
|
||||
seen_sector_ids.add(row.sector_id)
|
||||
|
||||
if row.element_id:
|
||||
if row.element_id in seen_element_ids:
|
||||
raise _conflict(f"Duplicate element_id='{row.element_id}' while cloning draft")
|
||||
seen_element_ids.add(row.element_id)
|
||||
|
||||
cloned = SchemeSectorRecord(
|
||||
sector_record_id=uuid4().hex,
|
||||
scheme_id=row.scheme_id,
|
||||
scheme_version_id=target_scheme_version_id,
|
||||
element_id=row.element_id,
|
||||
sector_id=row.sector_id,
|
||||
name=row.name,
|
||||
classes_raw=row.classes_raw,
|
||||
)
|
||||
session.add(cloned)
|
||||
|
||||
|
||||
async def list_scheme_version_sectors(scheme_version_id: str) -> list[SchemeSectorRecord]:
|
||||
async with AsyncSessionLocal() as session:
|
||||
result = await session.execute(
|
||||
@@ -78,8 +162,7 @@ async def update_scheme_version_sector_by_record_id(
|
||||
*,
|
||||
scheme_version_id: str,
|
||||
sector_record_id: str,
|
||||
sector_id: str | None,
|
||||
name: str | None,
|
||||
**update_data,
|
||||
) -> tuple[SchemeSectorRecord, str | None]:
|
||||
async with AsyncSessionLocal() as session:
|
||||
result = await session.execute(
|
||||
@@ -96,9 +179,20 @@ async def update_scheme_version_sector_by_record_id(
|
||||
detail="Sector record not found in current draft version",
|
||||
)
|
||||
|
||||
if "sector_id" in update_data:
|
||||
await _ensure_sector_uniqueness(
|
||||
session=session,
|
||||
scheme_version_id=scheme_version_id,
|
||||
sector_id=update_data["sector_id"],
|
||||
element_id=row.element_id,
|
||||
exclude_sector_record_id=sector_record_id,
|
||||
)
|
||||
|
||||
old_sector_id = row.sector_id
|
||||
row.sector_id = sector_id
|
||||
row.name = name
|
||||
if "sector_id" in update_data:
|
||||
row.sector_id = update_data["sector_id"]
|
||||
if "name" in update_data:
|
||||
row.name = update_data["name"]
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(row)
|
||||
@@ -115,6 +209,13 @@ async def create_scheme_version_sector(
|
||||
classes_raw: str | None,
|
||||
) -> SchemeSectorRecord:
|
||||
async with AsyncSessionLocal() as session:
|
||||
await _ensure_sector_uniqueness(
|
||||
session=session,
|
||||
scheme_version_id=scheme_version_id,
|
||||
sector_id=sector_id,
|
||||
element_id=element_id,
|
||||
)
|
||||
|
||||
row = SchemeSectorRecord(
|
||||
sector_record_id=uuid4().hex,
|
||||
scheme_id=scheme_id,
|
||||
|
||||
@@ -7,6 +7,125 @@ from sqlalchemy import asc, desc, func, select
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.scheme import SchemeRecord
|
||||
from app.models.scheme_version import SchemeVersionRecord
|
||||
from app.repositories.scheme_groups import clone_scheme_version_groups_in_session
|
||||
from app.repositories.scheme_seats import clone_scheme_version_seats_in_session
|
||||
from app.repositories.scheme_sectors import clone_scheme_version_sectors_in_session
|
||||
from app.services.api_errors import raise_conflict
|
||||
|
||||
|
||||
def _raise_current_version_inconsistent(*, scheme_id: str, current_version_number: int) -> None:
|
||||
raise_conflict(
|
||||
code="current_version_inconsistent",
|
||||
message="Scheme current version pointer is inconsistent with scheme_versions state.",
|
||||
details={
|
||||
"scheme_id": scheme_id,
|
||||
"current_version_number": current_version_number,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _raise_stale_current_version(*, expected_scheme_version_id: str, actual_scheme_version_id: str) -> None:
|
||||
raise_conflict(
|
||||
code="stale_current_version",
|
||||
message="Current scheme version changed. Reload scheme state before creating a new version.",
|
||||
details={
|
||||
"expected_scheme_version_id": expected_scheme_version_id,
|
||||
"actual_scheme_version_id": actual_scheme_version_id,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def _get_scheme_for_update(session, scheme_id: str) -> SchemeRecord:
|
||||
scheme_result = await session.execute(
|
||||
select(SchemeRecord)
|
||||
.where(SchemeRecord.scheme_id == scheme_id)
|
||||
.with_for_update()
|
||||
)
|
||||
scheme = scheme_result.scalar_one_or_none()
|
||||
if scheme is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Scheme not found",
|
||||
)
|
||||
return scheme
|
||||
|
||||
|
||||
async def _get_current_scheme_version_for_update(
|
||||
session,
|
||||
*,
|
||||
scheme_id: str,
|
||||
current_version_number: int,
|
||||
) -> SchemeVersionRecord:
|
||||
current_result = await session.execute(
|
||||
select(SchemeVersionRecord)
|
||||
.where(
|
||||
SchemeVersionRecord.scheme_id == scheme_id,
|
||||
SchemeVersionRecord.version_number == current_version_number,
|
||||
)
|
||||
.with_for_update()
|
||||
)
|
||||
current_version = current_result.scalar_one_or_none()
|
||||
if current_version is None:
|
||||
_raise_current_version_inconsistent(
|
||||
scheme_id=scheme_id,
|
||||
current_version_number=current_version_number,
|
||||
)
|
||||
return current_version
|
||||
|
||||
|
||||
async def _build_next_draft_version(
|
||||
session,
|
||||
*,
|
||||
scheme: SchemeRecord,
|
||||
source_version: SchemeVersionRecord,
|
||||
) -> SchemeVersionRecord:
|
||||
max_version_result = await session.execute(
|
||||
select(func.coalesce(func.max(SchemeVersionRecord.version_number), 0)).where(
|
||||
SchemeVersionRecord.scheme_id == scheme.scheme_id
|
||||
)
|
||||
)
|
||||
next_version_number = int(max_version_result.scalar_one()) + 1
|
||||
new_version = SchemeVersionRecord(
|
||||
scheme_version_id=uuid4().hex,
|
||||
scheme_id=scheme.scheme_id,
|
||||
version_number=next_version_number,
|
||||
status="draft",
|
||||
normalized_storage_path=source_version.normalized_storage_path,
|
||||
normalized_elements_count=source_version.normalized_elements_count,
|
||||
normalized_seats_count=source_version.normalized_seats_count,
|
||||
normalized_groups_count=source_version.normalized_groups_count,
|
||||
normalized_sectors_count=source_version.normalized_sectors_count,
|
||||
display_svg_storage_path=source_version.display_svg_storage_path,
|
||||
display_svg_status=source_version.display_svg_status,
|
||||
display_svg_generated_at=source_version.display_svg_generated_at,
|
||||
)
|
||||
session.add(new_version)
|
||||
await session.flush()
|
||||
|
||||
await clone_scheme_version_sectors_in_session(
|
||||
session=session,
|
||||
source_scheme_version_id=source_version.scheme_version_id,
|
||||
target_scheme_version_id=new_version.scheme_version_id,
|
||||
)
|
||||
await clone_scheme_version_groups_in_session(
|
||||
session=session,
|
||||
source_scheme_version_id=source_version.scheme_version_id,
|
||||
target_scheme_version_id=new_version.scheme_version_id,
|
||||
)
|
||||
await clone_scheme_version_seats_in_session(
|
||||
session=session,
|
||||
source_scheme_version_id=source_version.scheme_version_id,
|
||||
target_scheme_version_id=new_version.scheme_version_id,
|
||||
)
|
||||
|
||||
scheme.current_version_number = new_version.version_number
|
||||
scheme.status = "draft"
|
||||
scheme.published_at = None
|
||||
scheme.normalized_elements_count = source_version.normalized_elements_count
|
||||
scheme.normalized_seats_count = source_version.normalized_seats_count
|
||||
scheme.normalized_groups_count = source_version.normalized_groups_count
|
||||
scheme.normalized_sectors_count = source_version.normalized_sectors_count
|
||||
return new_version
|
||||
|
||||
|
||||
async def create_initial_scheme_version(
|
||||
@@ -75,9 +194,9 @@ async def get_current_scheme_version(scheme_id: str, current_version_number: int
|
||||
row = result.scalar_one_or_none()
|
||||
|
||||
if row is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Current scheme version not found",
|
||||
_raise_current_version_inconsistent(
|
||||
scheme_id=scheme_id,
|
||||
current_version_number=current_version_number,
|
||||
)
|
||||
|
||||
return row
|
||||
@@ -113,57 +232,87 @@ async def update_scheme_version_display_artifact(
|
||||
|
||||
async def create_next_scheme_version_from_current(scheme_id: str) -> SchemeVersionRecord:
|
||||
async with AsyncSessionLocal() as session:
|
||||
scheme_result = await session.execute(
|
||||
select(SchemeRecord).where(SchemeRecord.scheme_id == scheme_id)
|
||||
)
|
||||
scheme = scheme_result.scalar_one_or_none()
|
||||
|
||||
if scheme is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Scheme not found",
|
||||
async with session.begin():
|
||||
scheme = await _get_scheme_for_update(session, scheme_id)
|
||||
current_version = await _get_current_scheme_version_for_update(
|
||||
session,
|
||||
scheme_id=scheme.scheme_id,
|
||||
current_version_number=scheme.current_version_number,
|
||||
)
|
||||
new_version = await _build_next_draft_version(
|
||||
session,
|
||||
scheme=scheme,
|
||||
source_version=current_version,
|
||||
)
|
||||
|
||||
current_result = await session.execute(
|
||||
select(SchemeVersionRecord).where(
|
||||
SchemeVersionRecord.scheme_id == scheme.scheme_id,
|
||||
SchemeVersionRecord.version_number == scheme.current_version_number,
|
||||
)
|
||||
)
|
||||
current_version = current_result.scalar_one_or_none()
|
||||
|
||||
if current_version is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Current scheme version not found",
|
||||
)
|
||||
|
||||
next_version_number = current_version.version_number + 1
|
||||
new_version = SchemeVersionRecord(
|
||||
scheme_version_id=uuid4().hex,
|
||||
scheme_id=scheme.scheme_id,
|
||||
version_number=next_version_number,
|
||||
status="draft",
|
||||
normalized_storage_path=current_version.normalized_storage_path,
|
||||
normalized_elements_count=current_version.normalized_elements_count,
|
||||
normalized_seats_count=current_version.normalized_seats_count,
|
||||
normalized_groups_count=current_version.normalized_groups_count,
|
||||
normalized_sectors_count=current_version.normalized_sectors_count,
|
||||
display_svg_storage_path=current_version.display_svg_storage_path,
|
||||
display_svg_status=current_version.display_svg_status,
|
||||
display_svg_generated_at=current_version.display_svg_generated_at,
|
||||
)
|
||||
session.add(new_version)
|
||||
|
||||
scheme.current_version_number = next_version_number
|
||||
scheme.status = "draft"
|
||||
scheme.published_at = None
|
||||
scheme.normalized_elements_count = current_version.normalized_elements_count
|
||||
scheme.normalized_seats_count = current_version.normalized_seats_count
|
||||
scheme.normalized_groups_count = current_version.normalized_groups_count
|
||||
scheme.normalized_sectors_count = current_version.normalized_sectors_count
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(new_version)
|
||||
|
||||
return new_version
|
||||
|
||||
|
||||
async def create_next_scheme_version_from_current_checked(
|
||||
*,
|
||||
scheme_id: str,
|
||||
expected_current_scheme_version_id: str | None = None,
|
||||
) -> tuple[SchemeVersionRecord, SchemeVersionRecord]:
|
||||
async with AsyncSessionLocal() as session:
|
||||
async with session.begin():
|
||||
scheme = await _get_scheme_for_update(session, scheme_id)
|
||||
current_version = await _get_current_scheme_version_for_update(
|
||||
session,
|
||||
scheme_id=scheme.scheme_id,
|
||||
current_version_number=scheme.current_version_number,
|
||||
)
|
||||
if (
|
||||
expected_current_scheme_version_id
|
||||
and expected_current_scheme_version_id != current_version.scheme_version_id
|
||||
):
|
||||
_raise_stale_current_version(
|
||||
expected_scheme_version_id=expected_current_scheme_version_id,
|
||||
actual_scheme_version_id=current_version.scheme_version_id,
|
||||
)
|
||||
new_version = await _build_next_draft_version(
|
||||
session,
|
||||
scheme=scheme,
|
||||
source_version=current_version,
|
||||
)
|
||||
|
||||
await session.refresh(current_version)
|
||||
await session.refresh(new_version)
|
||||
return current_version, new_version
|
||||
|
||||
|
||||
async def ensure_draft_scheme_version_consistent(
|
||||
*,
|
||||
scheme_id: str,
|
||||
expected_current_scheme_version_id: str | None = None,
|
||||
) -> tuple[SchemeVersionRecord, bool, str | None]:
|
||||
async with AsyncSessionLocal() as session:
|
||||
async with session.begin():
|
||||
scheme = await _get_scheme_for_update(session, scheme_id)
|
||||
current_version = await _get_current_scheme_version_for_update(
|
||||
session,
|
||||
scheme_id=scheme.scheme_id,
|
||||
current_version_number=scheme.current_version_number,
|
||||
)
|
||||
if (
|
||||
expected_current_scheme_version_id
|
||||
and expected_current_scheme_version_id != current_version.scheme_version_id
|
||||
):
|
||||
_raise_stale_current_version(
|
||||
expected_scheme_version_id=expected_current_scheme_version_id,
|
||||
actual_scheme_version_id=current_version.scheme_version_id,
|
||||
)
|
||||
|
||||
if scheme.status == "draft" and current_version.status == "draft":
|
||||
await session.refresh(current_version)
|
||||
return current_version, False, None
|
||||
|
||||
new_version = await _build_next_draft_version(
|
||||
session,
|
||||
scheme=scheme,
|
||||
source_version=current_version,
|
||||
)
|
||||
source_scheme_version_id = current_version.scheme_version_id
|
||||
|
||||
await session.refresh(new_version)
|
||||
return new_version, True, source_scheme_version_id
|
||||
|
||||
@@ -6,6 +6,51 @@ from sqlalchemy import desc, func, select
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.scheme import SchemeRecord
|
||||
from app.models.scheme_version import SchemeVersionRecord
|
||||
from app.services.api_errors import raise_conflict
|
||||
|
||||
|
||||
def _raise_current_version_inconsistent(*, scheme_id: str, current_version_number: int) -> None:
|
||||
raise_conflict(
|
||||
code="current_version_inconsistent",
|
||||
message="Scheme current version pointer is inconsistent with scheme_versions state.",
|
||||
details={
|
||||
"scheme_id": scheme_id,
|
||||
"current_version_number": current_version_number,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def _get_scheme_for_update(session, scheme_id: str) -> SchemeRecord:
|
||||
scheme_result = await session.execute(
|
||||
select(SchemeRecord)
|
||||
.where(SchemeRecord.scheme_id == scheme_id)
|
||||
.with_for_update()
|
||||
)
|
||||
scheme = scheme_result.scalar_one_or_none()
|
||||
if scheme is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Scheme not found",
|
||||
)
|
||||
return scheme
|
||||
|
||||
|
||||
async def _get_current_version_for_scheme(session, scheme: SchemeRecord) -> SchemeVersionRecord:
|
||||
version_result = await session.execute(
|
||||
select(SchemeVersionRecord)
|
||||
.where(
|
||||
SchemeVersionRecord.scheme_id == scheme.scheme_id,
|
||||
SchemeVersionRecord.version_number == scheme.current_version_number,
|
||||
)
|
||||
.with_for_update()
|
||||
)
|
||||
version = version_result.scalar_one_or_none()
|
||||
if version is None:
|
||||
_raise_current_version_inconsistent(
|
||||
scheme_id=scheme.scheme_id,
|
||||
current_version_number=scheme.current_version_number,
|
||||
)
|
||||
return version
|
||||
|
||||
|
||||
async def create_scheme_from_upload(
|
||||
@@ -37,6 +82,55 @@ async def create_scheme_from_upload(
|
||||
return scheme_id
|
||||
|
||||
|
||||
async def create_scheme_from_upload_with_initial_version(
|
||||
*,
|
||||
source_upload_id: str,
|
||||
name: str,
|
||||
normalized_storage_path: str,
|
||||
normalized_elements_count: int,
|
||||
normalized_seats_count: int,
|
||||
normalized_groups_count: int,
|
||||
normalized_sectors_count: int,
|
||||
display_svg_storage_path: str | None = None,
|
||||
display_svg_status: str = "pending",
|
||||
display_svg_generated_at=None,
|
||||
) -> tuple[str, str]:
|
||||
scheme_id = uuid4().hex
|
||||
scheme_version_id = uuid4().hex
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
scheme = SchemeRecord(
|
||||
scheme_id=scheme_id,
|
||||
source_upload_id=source_upload_id,
|
||||
name=name,
|
||||
status="draft",
|
||||
current_version_number=1,
|
||||
normalized_elements_count=normalized_elements_count,
|
||||
normalized_seats_count=normalized_seats_count,
|
||||
normalized_groups_count=normalized_groups_count,
|
||||
normalized_sectors_count=normalized_sectors_count,
|
||||
)
|
||||
version = SchemeVersionRecord(
|
||||
scheme_version_id=scheme_version_id,
|
||||
scheme_id=scheme_id,
|
||||
version_number=1,
|
||||
status="draft",
|
||||
normalized_storage_path=normalized_storage_path,
|
||||
normalized_elements_count=normalized_elements_count,
|
||||
normalized_seats_count=normalized_seats_count,
|
||||
normalized_groups_count=normalized_groups_count,
|
||||
normalized_sectors_count=normalized_sectors_count,
|
||||
display_svg_storage_path=display_svg_storage_path,
|
||||
display_svg_status=display_svg_status,
|
||||
display_svg_generated_at=display_svg_generated_at,
|
||||
)
|
||||
session.add(scheme)
|
||||
session.add(version)
|
||||
await session.commit()
|
||||
|
||||
return scheme_id, scheme_version_id
|
||||
|
||||
|
||||
async def list_scheme_records(limit: int = 50, offset: int = 0) -> list[SchemeRecord]:
|
||||
async with AsyncSessionLocal() as session:
|
||||
result = await session.execute(
|
||||
@@ -72,127 +166,60 @@ async def get_scheme_record_by_scheme_id(scheme_id: str) -> SchemeRecord:
|
||||
|
||||
async def publish_scheme(scheme_id: str) -> SchemeRecord:
|
||||
async with AsyncSessionLocal() as session:
|
||||
scheme_result = await session.execute(
|
||||
select(SchemeRecord).where(SchemeRecord.scheme_id == scheme_id)
|
||||
)
|
||||
scheme = scheme_result.scalar_one_or_none()
|
||||
async with session.begin():
|
||||
scheme = await _get_scheme_for_update(session, scheme_id)
|
||||
version = await _get_current_version_for_scheme(session, scheme)
|
||||
scheme.status = "published"
|
||||
scheme.published_at = func.now()
|
||||
version.status = "published"
|
||||
|
||||
if scheme is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Scheme not found",
|
||||
)
|
||||
|
||||
version_result = await session.execute(
|
||||
select(SchemeVersionRecord).where(
|
||||
SchemeVersionRecord.scheme_id == scheme.scheme_id,
|
||||
SchemeVersionRecord.version_number == scheme.current_version_number,
|
||||
)
|
||||
)
|
||||
version = version_result.scalar_one_or_none()
|
||||
|
||||
if version is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Current scheme version not found",
|
||||
)
|
||||
|
||||
scheme.status = "published"
|
||||
scheme.published_at = func.now()
|
||||
version.status = "published"
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(scheme)
|
||||
|
||||
return scheme
|
||||
|
||||
|
||||
async def unpublish_scheme(scheme_id: str) -> SchemeRecord:
|
||||
async with AsyncSessionLocal() as session:
|
||||
scheme_result = await session.execute(
|
||||
select(SchemeRecord).where(SchemeRecord.scheme_id == scheme_id)
|
||||
)
|
||||
scheme = scheme_result.scalar_one_or_none()
|
||||
async with session.begin():
|
||||
scheme = await _get_scheme_for_update(session, scheme_id)
|
||||
version = await _get_current_version_for_scheme(session, scheme)
|
||||
scheme.status = "draft"
|
||||
scheme.published_at = None
|
||||
version.status = "draft"
|
||||
|
||||
if scheme is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Scheme not found",
|
||||
)
|
||||
|
||||
version_result = await session.execute(
|
||||
select(SchemeVersionRecord).where(
|
||||
SchemeVersionRecord.scheme_id == scheme.scheme_id,
|
||||
SchemeVersionRecord.version_number == scheme.current_version_number,
|
||||
)
|
||||
)
|
||||
version = version_result.scalar_one_or_none()
|
||||
|
||||
if version is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Current scheme version not found",
|
||||
)
|
||||
|
||||
scheme.status = "draft"
|
||||
scheme.published_at = None
|
||||
version.status = "draft"
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(scheme)
|
||||
|
||||
return scheme
|
||||
|
||||
|
||||
async def rollback_scheme_to_version(scheme_id: str, target_version_number: int) -> SchemeRecord:
|
||||
async with AsyncSessionLocal() as session:
|
||||
scheme_result = await session.execute(
|
||||
select(SchemeRecord).where(SchemeRecord.scheme_id == scheme_id)
|
||||
)
|
||||
scheme = scheme_result.scalar_one_or_none()
|
||||
async with session.begin():
|
||||
scheme = await _get_scheme_for_update(session, scheme_id)
|
||||
current_version = await _get_current_version_for_scheme(session, scheme)
|
||||
|
||||
if scheme is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Scheme not found",
|
||||
target_result = await session.execute(
|
||||
select(SchemeVersionRecord).where(
|
||||
SchemeVersionRecord.scheme_id == scheme.scheme_id,
|
||||
SchemeVersionRecord.version_number == target_version_number,
|
||||
)
|
||||
)
|
||||
target_version = target_result.scalar_one_or_none()
|
||||
|
||||
target_result = await session.execute(
|
||||
select(SchemeVersionRecord).where(
|
||||
SchemeVersionRecord.scheme_id == scheme.scheme_id,
|
||||
SchemeVersionRecord.version_number == target_version_number,
|
||||
)
|
||||
)
|
||||
target_version = target_result.scalar_one_or_none()
|
||||
if target_version is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Target scheme version not found",
|
||||
)
|
||||
|
||||
if target_version is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Target scheme version not found",
|
||||
)
|
||||
|
||||
current_result = await session.execute(
|
||||
select(SchemeVersionRecord).where(
|
||||
SchemeVersionRecord.scheme_id == scheme.scheme_id,
|
||||
SchemeVersionRecord.version_number == scheme.current_version_number,
|
||||
)
|
||||
)
|
||||
current_version = current_result.scalar_one_or_none()
|
||||
|
||||
if current_version is not None:
|
||||
current_version.status = "draft"
|
||||
target_version.status = "draft"
|
||||
scheme.current_version_number = target_version.version_number
|
||||
scheme.status = "draft"
|
||||
scheme.published_at = None
|
||||
|
||||
target_version.status = "draft"
|
||||
scheme.current_version_number = target_version.version_number
|
||||
scheme.status = "draft"
|
||||
scheme.published_at = None
|
||||
scheme.normalized_elements_count = target_version.normalized_elements_count
|
||||
scheme.normalized_seats_count = target_version.normalized_seats_count
|
||||
scheme.normalized_groups_count = target_version.normalized_groups_count
|
||||
scheme.normalized_sectors_count = target_version.normalized_sectors_count
|
||||
|
||||
scheme.normalized_elements_count = target_version.normalized_elements_count
|
||||
scheme.normalized_seats_count = target_version.normalized_seats_count
|
||||
scheme.normalized_groups_count = target_version.normalized_groups_count
|
||||
scheme.normalized_sectors_count = target_version.normalized_sectors_count
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(scheme)
|
||||
|
||||
return scheme
|
||||
|
||||
Reference in New Issue
Block a user