Implement duty schedule import functionality and enhance user management
- Added a new command `/import_duty_schedule` for importing duty schedules via JSON, restricted to admin users. - Introduced a two-step import process: specifying handover time and uploading a JSON file. - Updated the database schema to allow `telegram_user_id` to be nullable for user creation by full name. - Implemented repository functions for user management, including `get_or_create_user_by_full_name` and `delete_duties_in_range`. - Enhanced README documentation with details on the new import command and JSON format requirements. - Added comprehensive tests for the duty schedule parser and integration tests for the import functionality.
This commit is contained in:
1
importers/__init__.py
Normal file
1
importers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Importers for duty data (e.g. duty-schedule JSON)."""
|
||||
89
importers/duty_schedule.py
Normal file
89
importers/duty_schedule.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""Parser for duty-schedule JSON format. No DB access."""
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, timedelta
|
||||
|
||||
# Символы, обозначающие день дежурства в ячейке duty (CSV с разделителем ;)
|
||||
DUTY_MARKERS = frozenset({"б", "Б", "в", "Н", "О"})
|
||||
|
||||
|
||||
@dataclass
|
||||
class DutyScheduleResult:
|
||||
"""Parsed duty schedule: start_date, end_date, and per-person duty dates."""
|
||||
|
||||
start_date: date
|
||||
end_date: date
|
||||
entries: list[tuple[str, list[date]]] # (full_name, list of duty dates)
|
||||
|
||||
|
||||
class DutyScheduleParseError(Exception):
|
||||
"""Invalid or missing fields in duty-schedule JSON."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def parse_duty_schedule(raw_bytes: bytes) -> DutyScheduleResult:
|
||||
"""Parse duty-schedule JSON. Returns start_date, end_date, and list of (full_name, duty_dates).
|
||||
|
||||
- meta.start_date (YYYY-MM-DD) and schedule (array) required.
|
||||
- meta.weeks optional; number of days from max duty string length (split by ';').
|
||||
- For each schedule item: name (required), duty = CSV with ';'; index i = start_date + i days.
|
||||
- Cell value after strip in DUTY_MARKERS => duty day.
|
||||
"""
|
||||
try:
|
||||
data = json.loads(raw_bytes.decode("utf-8"))
|
||||
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
||||
raise DutyScheduleParseError(f"Invalid JSON or encoding: {e}") from e
|
||||
|
||||
meta = data.get("meta")
|
||||
if not meta or not isinstance(meta, dict):
|
||||
raise DutyScheduleParseError("Missing or invalid 'meta'")
|
||||
|
||||
start_str = meta.get("start_date")
|
||||
if not start_str or not isinstance(start_str, str):
|
||||
raise DutyScheduleParseError("Missing or invalid meta.start_date")
|
||||
try:
|
||||
start_date = date.fromisoformat(start_str.strip())
|
||||
except ValueError as e:
|
||||
raise DutyScheduleParseError(f"Invalid meta.start_date: {start_str}") from e
|
||||
|
||||
schedule = data.get("schedule")
|
||||
if not isinstance(schedule, list):
|
||||
raise DutyScheduleParseError("Missing or invalid 'schedule' (must be array)")
|
||||
|
||||
max_days = 0
|
||||
entries: list[tuple[str, list[date]]] = []
|
||||
|
||||
for row in schedule:
|
||||
if not isinstance(row, dict):
|
||||
raise DutyScheduleParseError("schedule item must be an object")
|
||||
name = row.get("name")
|
||||
if name is None or not isinstance(name, str):
|
||||
raise DutyScheduleParseError("schedule item must have 'name' (string)")
|
||||
full_name = name.strip()
|
||||
if not full_name:
|
||||
raise DutyScheduleParseError("schedule item 'name' cannot be empty")
|
||||
|
||||
duty_str = row.get("duty")
|
||||
if duty_str is None:
|
||||
duty_str = ""
|
||||
if not isinstance(duty_str, str):
|
||||
raise DutyScheduleParseError("schedule item 'duty' must be string")
|
||||
|
||||
cells = [c.strip() for c in duty_str.split(";")]
|
||||
max_days = max(max_days, len(cells))
|
||||
|
||||
duty_dates: list[date] = []
|
||||
for i, cell in enumerate(cells):
|
||||
if cell in DUTY_MARKERS:
|
||||
d = start_date + timedelta(days=i)
|
||||
duty_dates.append(d)
|
||||
entries.append((full_name, duty_dates))
|
||||
|
||||
if max_days == 0:
|
||||
end_date = start_date
|
||||
else:
|
||||
end_date = start_date + timedelta(days=max_days - 1)
|
||||
|
||||
return DutyScheduleResult(start_date=start_date, end_date=end_date, entries=entries)
|
||||
Reference in New Issue
Block a user