Skip to content

Commit 05013ab

Browse files
committed
feat: добавить доменную логику задач
Что сделано: - добавлена модель задачи с UUID и бизнес-полями - добавлены pydantic-схемы для создания, чтения и обновления - реализован репозиторий и сервисный слой для CRUD-операций - добавлена безопасная логика PATCH через exclude_unset
1 parent f773122 commit 05013ab

4 files changed

Lines changed: 184 additions & 0 deletions

File tree

app/models/task.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from __future__ import annotations
2+
3+
import uuid
4+
from datetime import date
5+
from enum import Enum
6+
from typing import TYPE_CHECKING
7+
8+
from sqlalchemy import Date, ForeignKey, String, Text
9+
from sqlalchemy.dialects.postgresql import UUID
10+
from sqlalchemy.orm import Mapped, mapped_column, relationship
11+
12+
from app.database import Base
13+
from app.models.mixins import CreatedAtMixin
14+
15+
if TYPE_CHECKING:
16+
from app.models.tag import Tag
17+
18+
19+
class StatusEnum(str, Enum):
20+
todo = "todo"
21+
in_progress = "in_progress"
22+
done = "done"
23+
24+
25+
class PriorityEnum(str, Enum):
26+
low = "low"
27+
medium = "medium"
28+
high = "high"
29+
30+
31+
class Task(CreatedAtMixin, Base):
32+
__tablename__ = "tasks"
33+
34+
id: Mapped[uuid.UUID] = mapped_column(
35+
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
36+
)
37+
title: Mapped[str] = mapped_column(String(255), nullable=False)
38+
description: Mapped[str | None] = mapped_column(Text(), nullable=True)
39+
status: Mapped[StatusEnum] = mapped_column(
40+
String(20), nullable=False, default=StatusEnum.todo
41+
)
42+
priority: Mapped[PriorityEnum] = mapped_column(
43+
String(10), nullable=False, default=PriorityEnum.medium
44+
)
45+
due_date: Mapped[date | None] = mapped_column(Date(), nullable=True)
46+
user_id: Mapped[uuid.UUID] = mapped_column(
47+
UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
48+
)
49+
tags: Mapped[list[Tag]] = relationship(
50+
secondary="task_tags",
51+
back_populates="tasks",
52+
)

app/repositories/task_repo.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Sequence
4+
from uuid import UUID
5+
6+
from sqlalchemy import select
7+
from sqlalchemy.ext.asyncio import AsyncSession
8+
9+
from app.models.task import Task
10+
from app.models.user import User
11+
12+
13+
async def get_task_by_id(session: AsyncSession, task_id: UUID) -> Task | None:
14+
"""Возвращает задачу по id или None."""
15+
return await session.get(Task, task_id)
16+
17+
18+
async def get_tasks_by_user(session: AsyncSession, user: User) -> Sequence[Task]:
19+
"""Возвращает все задачи пользователя."""
20+
result = await session.execute(select(Task).where(Task.user_id == user.id))
21+
return result.scalars().all()
22+
23+
24+
async def create_task(session: AsyncSession, user: User, **data) -> Task:
25+
"""Создаёт задачу для пользователя."""
26+
task = Task(**data, user_id=user.id)
27+
session.add(task)
28+
await session.commit()
29+
await session.refresh(task)
30+
return task
31+
32+
33+
async def update_task(session: AsyncSession, task: Task, **data) -> Task:
34+
"""Обновляет переданные поля задачи."""
35+
for field, value in data.items():
36+
setattr(task, field, value)
37+
await session.commit()
38+
await session.refresh(task)
39+
return task
40+
41+
42+
async def delete_task(session: AsyncSession, task: Task) -> UUID:
43+
"""Удаляет задачу и возвращает её id."""
44+
task_id = task.id
45+
await session.delete(task)
46+
await session.commit()
47+
return task_id

app/schemas/task.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from __future__ import annotations
2+
3+
from datetime import date, datetime
4+
from uuid import UUID
5+
6+
from pydantic import BaseModel, ConfigDict, Field
7+
8+
from app.models.task import PriorityEnum, StatusEnum
9+
10+
11+
class TaskCreate(BaseModel):
12+
title: str = Field(min_length=1, max_length=255)
13+
description: str | None = Field(None, max_length=2000)
14+
status: StatusEnum = StatusEnum.todo
15+
priority: PriorityEnum = PriorityEnum.medium
16+
due_date: date | None = None
17+
18+
19+
class TaskUpdate(BaseModel):
20+
model_config = ConfigDict(extra="forbid")
21+
22+
title: str | None = Field(None, min_length=1, max_length=255)
23+
description: str | None = Field(None, max_length=2000)
24+
status: StatusEnum | None = None
25+
priority: PriorityEnum | None = None
26+
due_date: date | None = None
27+
28+
29+
class TaskRead(BaseModel):
30+
model_config = ConfigDict(from_attributes=True)
31+
32+
id: UUID
33+
title: str
34+
description: str | None
35+
status: StatusEnum
36+
priority: PriorityEnum
37+
due_date: date | None
38+
user_id: UUID
39+
created_at: datetime
40+
41+
42+
class TaskDeleted(BaseModel):
43+
id: UUID

app/services/task.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Sequence
4+
from datetime import date
5+
from uuid import UUID
6+
7+
from sqlalchemy.ext.asyncio import AsyncSession
8+
9+
from app.models.task import Task
10+
from app.models.user import User
11+
from app.repositories import task_repo
12+
from app.schemas.task import TaskCreate, TaskUpdate
13+
14+
15+
class InvalidDueDate(Exception):
16+
"""Дедлайн задачи не может быть в прошлом."""
17+
18+
19+
async def create_task(session: AsyncSession, user: User, payload: TaskCreate) -> Task:
20+
"""Создаёт задачу. Поднимает InvalidDueDate если дедлайн в прошлом."""
21+
if payload.due_date and payload.due_date < date.today():
22+
raise InvalidDueDate
23+
return await task_repo.create_task(session, user, **payload.model_dump())
24+
25+
26+
async def get_user_tasks(session: AsyncSession, user: User) -> Sequence[Task]:
27+
"""Возвращает все задачи пользователя."""
28+
return await task_repo.get_tasks_by_user(session, user)
29+
30+
31+
async def update_task(session: AsyncSession, task: Task, payload: TaskUpdate) -> Task:
32+
"""Обновляет задачу. Поднимает InvalidDueDate если дедлайн в прошлом."""
33+
updates = payload.model_dump(exclude_unset=True)
34+
due_date = updates.get("due_date")
35+
if due_date is not None and due_date < date.today():
36+
raise InvalidDueDate
37+
return await task_repo.update_task(session, task, **updates)
38+
39+
40+
async def delete_task(session: AsyncSession, task: Task) -> UUID:
41+
"""Удаляет задачу и возвращает её id."""
42+
return await task_repo.delete_task(session, task)

0 commit comments

Comments
 (0)