diff --git a/backend/api.py b/backend/api.py index 33b9d28..960d814 100644 --- a/backend/api.py +++ b/backend/api.py @@ -1,19 +1,13 @@ -from fastapi import APIRouter, Depends, HTTPException, Body -from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import select, and_, or_ from datetime import datetime, timedelta -import pytz -from typing import List, Union -from backend.database import get_db, Task, Event, WeeklyTaskException -from backend.models import ( - TaskCreate, EventCreate, ScheduleResponse, ScheduleItem, - TaskResponse, EventResponse, UpdateRequest -) -from backend.utils import ( - get_week_range, check_event_overlap, get_weekday_from_date, - materialize_weekly_tasks, format_date_russian -) +import pytz +from fastapi import APIRouter, Depends, HTTPException, Body +from sqlalchemy import and_, select +from sqlalchemy.ext.asyncio import AsyncSession + +from backend.database import get_db, Task, Event, WeeklyTaskException, EventException +from backend.models import TaskCreate, EventCreate, ScheduleResponse, ScheduleItem, UpdateRequest +from backend.utils import check_event_overlap, get_weekday_from_date, materialize_weekly_tasks, materialize_events router = APIRouter() @@ -26,16 +20,18 @@ async def get_schedule( to_date: str, db: AsyncSession = Depends(get_db) ): - """Получить расписание в диапазоне дат""" + """Получить расписание в диапазоне дат.""" try: from_dt = datetime.strptime(from_date, "%Y-%m-%d").date() to_dt = datetime.strptime(to_date, "%Y-%m-%d").date() except ValueError: raise HTTPException(status_code=400, detail="Invalid date format. Use YYYY-MM-DD") - + + if to_dt < from_dt: + raise HTTPException(status_code=400, detail="to_date must be greater than or equal to from_date") + items = [] - - # Получаем разовые tasks + result = await db.execute( select(Task).where( and_( @@ -51,34 +47,14 @@ async def get_schedule( kind="task", id=task.id, date=task.date, + source_date=task.date, title=task.title, repeat_weekly=False )) - - # Получаем events - result = await db.execute( - select(Event).where( - and_( - Event.date >= from_date, - Event.date <= to_date - ) - ) - ) - events = result.scalars().all() - for event in events: - items.append(ScheduleItem( - kind="event", - id=event.id, - date=event.date, - title=event.title, - start_time=event.start_time, - duration_min=event.duration_min - )) - - # Материализуем weekly tasks - weekly_tasks = await materialize_weekly_tasks(db, from_date, to_date) - items.extend(weekly_tasks) - + + items.extend(await materialize_weekly_tasks(db, from_date, to_date)) + items.extend(await materialize_events(db, from_date, to_date)) + return ScheduleResponse(items=items) @@ -88,74 +64,69 @@ async def create_item( data: dict = Body(...), db: AsyncSession = Depends(get_db) ): - """Создать task или event""" + """Создать task или event.""" if kind == "task": try: task = TaskCreate(**data) - except Exception as e: - raise HTTPException(status_code=400, detail=f"Invalid task data: {e}") - - # Создаём основную task + except Exception as exc: + raise HTTPException(status_code=400, detail=f"Invalid task data: {exc}") + new_task = Task( date=task.date, title=task.title, repeat_weekly=task.repeat_weekly ) - if task.repeat_weekly: - weekday = get_weekday_from_date(task.date) - new_task.weekday = weekday - + new_task.weekday = get_weekday_from_date(task.date) + db.add(new_task) await db.commit() await db.refresh(new_task) - - # Копирование на другие дни недели + if task.copy_to_weekdays and not task.repeat_weekly: base_date = datetime.strptime(task.date, "%Y-%m-%d").date() base_weekday = base_date.weekday() for target_weekday in task.copy_to_weekdays: - # Вычисляем дату целевого дня недели в той же неделе - days_diff = (target_weekday - base_weekday) % 7 - if days_diff > 0: # только будущие дни - target_date = base_date + timedelta(days=days_diff) - copy_task = Task( - date=target_date.strftime("%Y-%m-%d"), - title=task.title, - repeat_weekly=False - ) - db.add(copy_task) + days_diff = target_weekday - base_weekday + if days_diff <= 0: + continue + target_date = base_date + timedelta(days=days_diff) + db.add(Task( + date=target_date.strftime("%Y-%m-%d"), + title=task.title, + repeat_weekly=False + )) await db.commit() - + return {"id": new_task.id, "kind": "task"} - - elif kind == "event": + + if kind == "event": try: event = EventCreate(**data) - except Exception as e: - raise HTTPException(status_code=400, detail=f"Invalid event data: {e}") - - # Проверка пересечений - overlap = await check_event_overlap( - db, event.date, event.start_time, event.duration_min, None - ) + except Exception as exc: + raise HTTPException(status_code=400, detail=f"Invalid event data: {exc}") + + overlap = await check_event_overlap(db, event.date, event.start_time, event.duration_min, None) if overlap: raise HTTPException(status_code=400, detail="Нельзя добавить: пересечение по времени") - + new_event = Event( date=event.date, start_time=event.start_time, duration_min=event.duration_min, - title=event.title + title=event.title, + repeat_weekly=event.repeat_weekly ) + if event.repeat_weekly: + new_event.weekday = get_weekday_from_date(event.date) + db.add(new_event) await db.commit() await db.refresh(new_event) - + return {"id": new_event.id, "kind": "event"} - - else: - raise HTTPException(status_code=400, detail="Invalid kind. Use 'task' or 'event'") + + raise HTTPException(status_code=400, detail="Invalid kind. Use 'task' or 'event'") @router.put("/events/{item_id}") @@ -164,130 +135,200 @@ async def update_item( update: UpdateRequest, db: AsyncSession = Depends(get_db) ): - """Обновить task или event""" - # Пробуем найти как task + """Обновить task или event.""" result = await db.execute(select(Task).where(Task.id == item_id)) task = result.scalar_one_or_none() - + if task: if task.repeat_weekly and update.scope == "one_date": - # Создаём исключение для конкретной даты - weekday = get_weekday_from_date(task.date) + occurrence_date = update.occurrence_date or task.date + result = await db.execute( + select(WeeklyTaskException).where( + and_( + WeeklyTaskException.task_id == task.id, + WeeklyTaskException.date == occurrence_date + ) + ) + ) + for existing_exception in result.scalars().all(): + await db.delete(existing_exception) exception = WeeklyTaskException( - weekday=weekday, - date=task.date, - action="replace" if update.title else "delete", - replacement_title=update.title + task_id=task.id, + weekday=get_weekday_from_date(occurrence_date), + date=occurrence_date, + action="replace", + replacement_title=update.title or task.title, + replacement_date=update.date or occurrence_date ) db.add(exception) await db.commit() return {"success": True, "action": "exception_created"} - + if update.title: - if task.repeat_weekly and update.scope == "series": - # Обновляем все weekly tasks с этим weekday - await db.execute( - Task.__table__.update() - .where(Task.weekday == task.weekday) - .values(title=update.title) - ) - else: - task.title = update.title - + task.title = update.title + if update.date: + task.date = update.date + if task.repeat_weekly: + task.weekday = get_weekday_from_date(update.date) + await db.commit() return {"success": True} - - # Пробуем найти как event + result = await db.execute(select(Event).where(Event.id == item_id)) event = result.scalar_one_or_none() - - if event: - new_date = update.date or event.date - new_start_time = update.start_time or event.start_time - new_duration = update.duration_min or event.duration_min - - # Проверка пересечений + if not event: + raise HTTPException(status_code=404, detail="Item not found") + + if event.repeat_weekly and update.scope == "one_date": + occurrence_date = update.occurrence_date or event.date + replacement_date = update.date or occurrence_date + replacement_start_time = update.start_time or event.start_time + replacement_duration = update.duration_min or event.duration_min + overlap = await check_event_overlap( - db, new_date, new_start_time, new_duration, item_id + db, + replacement_date, + replacement_start_time, + replacement_duration, + item_id, + occurrence_date=occurrence_date ) if overlap: raise HTTPException(status_code=400, detail="Нельзя изменить: пересечение по времени") - - if update.title: - event.title = update.title - if update.date: - event.date = update.date - if update.start_time: - event.start_time = update.start_time - if update.duration_min: - event.duration_min = update.duration_min - + + result = await db.execute( + select(EventException).where( + and_( + EventException.event_id == event.id, + EventException.date == occurrence_date + ) + ) + ) + for existing_exception in result.scalars().all(): + await db.delete(existing_exception) + + db.add(EventException( + event_id=event.id, + date=occurrence_date, + action="replace", + replacement_title=update.title or event.title, + replacement_date=replacement_date, + replacement_start_time=replacement_start_time, + replacement_duration_min=replacement_duration + )) await db.commit() - return {"success": True} - - raise HTTPException(status_code=404, detail="Item not found") + return {"success": True, "action": "exception_created"} + + new_date = update.date or event.date + new_start_time = update.start_time or event.start_time + new_duration = update.duration_min or event.duration_min + + overlap = await check_event_overlap(db, new_date, new_start_time, new_duration, item_id) + if overlap: + raise HTTPException(status_code=400, detail="Нельзя изменить: пересечение по времени") + + if update.title: + event.title = update.title + if update.date: + event.date = update.date + if event.repeat_weekly: + event.weekday = get_weekday_from_date(update.date) + if update.start_time: + event.start_time = update.start_time + if update.duration_min: + event.duration_min = update.duration_min + + await db.commit() + return {"success": True} @router.delete("/events/{item_id}") async def delete_item( item_id: int, scope: str = None, + occurrence_date: str = None, db: AsyncSession = Depends(get_db) ): - """Удалить task или event""" - # Пробуем найти как task + """Удалить task или event.""" result = await db.execute(select(Task).where(Task.id == item_id)) task = result.scalar_one_or_none() - + if task: - if task.repeat_weekly: - if scope == "one_date": - # Создаём исключение на удаление - weekday = get_weekday_from_date(task.date) - exception = WeeklyTaskException( - weekday=weekday, - date=task.date, - action="delete" + if task.repeat_weekly and scope == "one_date": + delete_date = occurrence_date or task.date + result = await db.execute( + select(WeeklyTaskException).where( + and_( + WeeklyTaskException.task_id == task.id, + WeeklyTaskException.date == delete_date + ) ) - db.add(exception) - await db.commit() - return {"success": True, "action": "exception_created"} - elif scope == "series": - # Удаляем все weekly tasks с этим weekday - await db.execute( - Task.__table__.delete().where(Task.weekday == task.weekday) - ) - await db.commit() - return {"success": True} - + ) + for existing_exception in result.scalars().all(): + await db.delete(existing_exception) + db.add(WeeklyTaskException( + task_id=task.id, + weekday=get_weekday_from_date(delete_date), + date=delete_date, + action="delete" + )) + await db.commit() + return {"success": True, "action": "exception_created"} + + if task.repeat_weekly and scope == "series": + result = await db.execute(select(WeeklyTaskException).where(WeeklyTaskException.task_id == task.id)) + for exception in result.scalars().all(): + await db.delete(exception) + await db.delete(task) await db.commit() return {"success": True} - - # Пробуем найти как event + result = await db.execute(select(Event).where(Event.id == item_id)) event = result.scalar_one_or_none() - - if event: - await db.delete(event) + if not event: + raise HTTPException(status_code=404, detail="Item not found") + + if event.repeat_weekly and scope == "one_date": + delete_date = occurrence_date or event.date + result = await db.execute( + select(EventException).where( + and_( + EventException.event_id == event.id, + EventException.date == delete_date + ) + ) + ) + for existing_exception in result.scalars().all(): + await db.delete(existing_exception) + db.add(EventException( + event_id=event.id, + date=delete_date, + action="delete" + )) await db.commit() - return {"success": True} - - raise HTTPException(status_code=404, detail="Item not found") + return {"success": True, "action": "exception_created"} + + if event.repeat_weekly and scope == "series": + result = await db.execute(select(EventException).where(EventException.event_id == event.id)) + for exception in result.scalars().all(): + await db.delete(exception) + + await db.delete(event) + await db.commit() + return {"success": True} @router.get("/backup") async def backup_database(): - """Создать backup базы данных""" - import shutil + """Создать backup базы данных.""" import os - from datetime import datetime - + import shutil + db_path = os.getenv("DATABASE_PATH", "data/schedule.db") backup_path = f"{db_path}.backup.{datetime.now().strftime('%Y%m%d_%H%M%S')}" - + if os.path.exists(db_path): shutil.copy2(db_path, backup_path) return {"success": True, "backup_path": backup_path} return {"success": False, "error": "Database not found"} - diff --git a/backend/database.py b/backend/database.py index 5e46855..e1ab0e2 100644 --- a/backend/database.py +++ b/backend/database.py @@ -1,6 +1,6 @@ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker -from sqlalchemy.orm import declarative_base, sessionmaker -from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text +from sqlalchemy.orm import declarative_base +from sqlalchemy import Column, Integer, String, Boolean, text import os Base = declarative_base() @@ -31,16 +31,33 @@ class Event(Base): start_time = Column(String) # HH:MM duration_min = Column(Integer, nullable=False) title = Column(String, nullable=False) + repeat_weekly = Column(Boolean, default=False) + weekday = Column(Integer) # 0=Monday, 6=Sunday, только для weekly events class WeeklyTaskException(Base): __tablename__ = "weekly_task_exceptions" id = Column(Integer, primary_key=True, index=True) + task_id = Column(Integer, index=True) weekday = Column(Integer, nullable=False) # 0=Monday, 6=Sunday date = Column(String, nullable=False) # YYYY-MM-DD action = Column(String, nullable=False) # "delete" или "replace" replacement_title = Column(String) # для action="replace" + replacement_date = Column(String) # для переноса/замены одного вхождения + + +class EventException(Base): + __tablename__ = "event_exceptions" + + id = Column(Integer, primary_key=True, index=True) + event_id = Column(Integer, index=True, nullable=False) + date = Column(String, nullable=False) + action = Column(String, nullable=False) # "delete" или "replace" + replacement_title = Column(String) + replacement_date = Column(String) + replacement_start_time = Column(String) + replacement_duration_min = Column(Integer) class TelegramUser(Base): @@ -53,6 +70,37 @@ class TelegramUser(Base): async def init_db(): async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) + await _run_migrations(conn) + + +async def _run_migrations(conn): + table_columns = await _get_table_columns(conn) + + task_columns = table_columns.get("tasks", set()) + if "weekday" not in task_columns: + await conn.execute(text("ALTER TABLE tasks ADD COLUMN weekday INTEGER")) + + event_columns = table_columns.get("events", set()) + if "repeat_weekly" not in event_columns: + await conn.execute(text("ALTER TABLE events ADD COLUMN repeat_weekly BOOLEAN DEFAULT 0")) + if "weekday" not in event_columns: + await conn.execute(text("ALTER TABLE events ADD COLUMN weekday INTEGER")) + + exception_columns = table_columns.get("weekly_task_exceptions", set()) + if "task_id" not in exception_columns: + await conn.execute(text("ALTER TABLE weekly_task_exceptions ADD COLUMN task_id INTEGER")) + if "replacement_date" not in exception_columns: + await conn.execute(text("ALTER TABLE weekly_task_exceptions ADD COLUMN replacement_date TEXT")) + + +async def _get_table_columns(conn): + table_names = ["tasks", "events", "weekly_task_exceptions", "event_exceptions"] + columns = {} + for table_name in table_names: + result = await conn.exec_driver_sql(f"PRAGMA table_info({table_name})") + rows = result.fetchall() + columns[table_name] = {row[1] for row in rows} + return columns async def get_db(): @@ -61,4 +109,3 @@ async def get_db(): yield session finally: await session.close() - diff --git a/backend/models.py b/backend/models.py index fc9eded..406dcde 100644 --- a/backend/models.py +++ b/backend/models.py @@ -1,7 +1,5 @@ from pydantic import BaseModel from typing import Optional, Literal -from datetime import date - class TaskCreate(BaseModel): date: str # YYYY-MM-DD title: str @@ -13,6 +11,7 @@ class EventCreate(BaseModel): start_time: str # HH:MM duration_min: int title: str + repeat_weekly: bool = False class TaskResponse(BaseModel): id: int @@ -28,11 +27,13 @@ class EventResponse(BaseModel): duration_min: int title: str kind: Literal["event"] = "event" + repeat_weekly: bool = False class ScheduleItem(BaseModel): kind: Literal["task", "event"] id: int date: str + source_date: Optional[str] = None title: str start_time: Optional[str] = None duration_min: Optional[int] = None @@ -46,5 +47,5 @@ class UpdateRequest(BaseModel): date: Optional[str] = None start_time: Optional[str] = None duration_min: Optional[int] = None + occurrence_date: Optional[str] = None scope: Optional[Literal["one_date", "series"]] = None # для weekly tasks - diff --git a/backend/utils.py b/backend/utils.py index 2f4d585..6d39c11 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -3,97 +3,182 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, and_ import pytz -from backend.database import Task, Event, WeeklyTaskException +from backend.database import Task, Event, WeeklyTaskException, EventException from backend.models import ScheduleItem TZ = pytz.timezone("Europe/Moscow") def get_weekday_from_date(date_str: str) -> int: - """Получить день недели (0=Monday, 6=Sunday) из даты""" + """Получить день недели (0=Monday, 6=Sunday) из даты.""" dt = datetime.strptime(date_str, "%Y-%m-%d") return dt.weekday() def get_week_range(date_str: str = None): - """Получить диапазон дат недели (понедельник-воскресенье)""" + """Получить диапазон дат недели (понедельник-воскресенье).""" if date_str: base_date = datetime.strptime(date_str, "%Y-%m-%d").date() else: base_date = datetime.now(TZ).date() - - # Находим понедельник + days_since_monday = base_date.weekday() monday = base_date - timedelta(days=days_since_monday) sunday = monday + timedelta(days=6) - + return monday.strftime("%Y-%m-%d"), sunday.strftime("%Y-%m-%d") def format_date_russian(date_str: str) -> str: - """Форматировать дату в русском формате: 'Вторник, 2 декабря'""" + """Форматировать дату в русском формате: 'Вторник, 2 декабря'.""" dt = datetime.strptime(date_str, "%Y-%m-%d") - + weekdays = ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"] months = [ "января", "февраля", "марта", "апреля", "мая", "июня", "июля", "августа", "сентября", "октября", "ноября", "декабря" ] - + weekday = weekdays[dt.weekday()] month = months[dt.month - 1] - + return f"{weekday}, {dt.day} {month}" +def _time_to_minutes(value: str) -> int: + hour, minute = map(int, value.split(":")) + return hour * 60 + minute + + async def check_event_overlap( db: AsyncSession, date: str, start_time: str, duration_min: int, - exclude_id: int = None + exclude_id: int = None, + occurrence_date: str = None ) -> bool: - """Проверить пересечение по времени с другими events""" - # Парсим время начала - start_hour, start_min = map(int, start_time.split(":")) - start_minutes = start_hour * 60 + start_min + """Проверить пересечение по времени с разовыми и weekly events.""" + start_minutes = _time_to_minutes(start_time) end_minutes = start_minutes + duration_min - - # Получаем все events на эту дату - query = select(Event).where(Event.date == date) - if exclude_id: - query = query.where(Event.id != exclude_id) - - result = await db.execute(query) - events = result.scalars().all() - + + events = await materialize_events(db, date, date) + for event in events: - ev_start_hour, ev_start_min = map(int, event.start_time.split(":")) - ev_start_minutes = ev_start_hour * 60 + ev_start_min - ev_end_minutes = ev_start_minutes + event.duration_min - - # Проверка пересечения интервалов + if exclude_id and event.id == exclude_id and (occurrence_date is None or event.date == occurrence_date): + continue + + ev_start_minutes = _time_to_minutes(event.start_time) + ev_end_minutes = ev_start_minutes + (event.duration_min or 0) if not (end_minutes <= ev_start_minutes or start_minutes >= ev_end_minutes): return True - + return False +async def materialize_events( + db: AsyncSession, + from_date: str, + to_date: str +) -> list[ScheduleItem]: + items = [] + + result = await db.execute( + select(Event).where( + and_( + Event.date >= from_date, + Event.date <= to_date, + Event.repeat_weekly == False + ) + ) + ) + one_off_events = result.scalars().all() + for event in one_off_events: + items.append(ScheduleItem( + kind="event", + id=event.id, + date=event.date, + source_date=event.date, + title=event.title, + start_time=event.start_time, + duration_min=event.duration_min, + repeat_weekly=False + )) + + result = await db.execute(select(Event).where(Event.repeat_weekly == True)) + weekly_events = result.scalars().all() + if not weekly_events: + return items + + result = await db.execute( + select(EventException).where( + and_( + EventException.date >= from_date, + EventException.date <= to_date + ) + ) + ) + exceptions = result.scalars().all() + exception_dict = {(ex.event_id, ex.date): ex for ex in exceptions} + + from_dt = datetime.strptime(from_date, "%Y-%m-%d").date() + to_dt = datetime.strptime(to_date, "%Y-%m-%d").date() + current_date = from_dt + + while current_date <= to_dt: + date_str = current_date.strftime("%Y-%m-%d") + weekday = current_date.weekday() + + for event in weekly_events: + base_date = datetime.strptime(event.date, "%Y-%m-%d").date() + if current_date < base_date or event.weekday != weekday: + continue + + exception = exception_dict.get((event.id, date_str)) + if exception: + if exception.action == "delete": + continue + items.append(ScheduleItem( + kind="event", + id=event.id, + date=exception.replacement_date or date_str, + source_date=date_str, + title=exception.replacement_title or event.title, + start_time=exception.replacement_start_time or event.start_time, + duration_min=exception.replacement_duration_min or event.duration_min, + repeat_weekly=True + )) + continue + + items.append(ScheduleItem( + kind="event", + id=event.id, + date=date_str, + source_date=date_str, + title=event.title, + start_time=event.start_time, + duration_min=event.duration_min, + repeat_weekly=True + )) + + current_date += timedelta(days=1) + + return items + + async def materialize_weekly_tasks( db: AsyncSession, from_date: str, to_date: str ) -> list[ScheduleItem]: - """Материализовать weekly tasks в диапазоне дат""" - from backend.database import Task, WeeklyTaskException - + """Материализовать weekly tasks в диапазоне дат.""" items = [] - - # Получаем все weekly tasks + result = await db.execute(select(Task).where(Task.repeat_weekly == True)) weekly_tasks = result.scalars().all() - - # Получаем все исключения + if not weekly_tasks: + return items + result = await db.execute( select(WeeklyTaskException).where( and_( @@ -103,44 +188,52 @@ async def materialize_weekly_tasks( ) ) exceptions = result.scalars().all() - exception_dict = {(ex.date, ex.weekday): ex for ex in exceptions} - + exception_dict = {} + for ex in exceptions: + if ex.task_id is not None: + exception_dict[(ex.task_id, ex.date)] = ex + else: + exception_dict[(None, ex.date, ex.weekday)] = ex + from_dt = datetime.strptime(from_date, "%Y-%m-%d").date() to_dt = datetime.strptime(to_date, "%Y-%m-%d").date() - + current_date = from_dt while current_date <= to_dt: date_str = current_date.strftime("%Y-%m-%d") weekday = current_date.weekday() - + for task in weekly_tasks: - if task.weekday == weekday: - # Проверяем исключения - ex_key = (date_str, weekday) - if ex_key in exception_dict: - exception = exception_dict[ex_key] - if exception.action == "delete": - continue # Пропускаем эту дату - elif exception.action == "replace": - items.append(ScheduleItem( - kind="task", - id=task.id, - date=date_str, - title=exception.replacement_title, - repeat_weekly=True - )) - continue - - # Обычная weekly task + base_date = datetime.strptime(task.date, "%Y-%m-%d").date() + if current_date < base_date or task.weekday != weekday: + continue + + exception = exception_dict.get((task.id, date_str)) + if not exception: + exception = exception_dict.get((None, date_str, weekday)) + + if exception: + if exception.action == "delete": + continue items.append(ScheduleItem( kind="task", id=task.id, - date=date_str, - title=task.title, + date=exception.replacement_date or date_str, + source_date=date_str, + title=exception.replacement_title or task.title, repeat_weekly=True )) - - current_date += timedelta(days=1) - - return items + continue + items.append(ScheduleItem( + kind="task", + id=task.id, + date=date_str, + source_date=date_str, + title=task.title, + repeat_weekly=True + )) + + current_date += timedelta(days=1) + + return items diff --git a/frontend/admin/index.html b/frontend/admin/index.html index c7fd182..8e51da9 100644 --- a/frontend/admin/index.html +++ b/frontend/admin/index.html @@ -3,37 +3,58 @@
-