File size: 5,934 Bytes
69be42f
 
dc3879e
 
69be42f
dc3879e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69be42f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dc3879e
 
 
 
 
 
 
 
 
 
 
 
69be42f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dc3879e
 
 
69be42f
 
dc3879e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69be42f
 
 
 
 
 
 
 
dc3879e
 
 
69be42f
 
dc3879e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69be42f
 
 
 
 
 
 
 
 
 
dc3879e
 
 
69be42f
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
"""Task model and related I/O classes."""
import uuid
from datetime import datetime, timezone
from enum import Enum
from typing import Optional
from sqlmodel import Field, SQLModel, Column
from pydantic import field_validator
from sqlalchemy import ARRAY, String


class PriorityLevel(str, Enum):
    """Task priority levels.

    Defines the three priority levels for tasks:
    - HIGH: Urgent tasks that need immediate attention
    - MEDIUM: Default priority for normal tasks
    - LOW: Optional tasks that can be done whenever
    """
    HIGH = "HIGH"
    MEDIUM = "MEDIUM"
    LOW = "LOW"


class Task(SQLModel, table=True):
    """Database table model for Task entity."""

    __tablename__ = "tasks"

    id: uuid.UUID = Field(
        default_factory=uuid.uuid4,
        primary_key=True,
        index=True
    )
    user_id: uuid.UUID = Field(
        foreign_key="users.id",
        index=True
    )
    title: str = Field(max_length=255)
    description: Optional[str] = Field(
        default=None,
        max_length=2000
    )
    priority: PriorityLevel = Field(
        default=PriorityLevel.MEDIUM,
        max_length=10
    )
    tags: list[str] = Field(
        default=[],
        sa_column=Column(ARRAY(String), nullable=False),  # PostgreSQL TEXT[] type
    )
    due_date: Optional[datetime] = Field(
        default=None,
        index=True
    )
    completed: bool = Field(default=False)
    created_at: datetime = Field(
        default_factory=datetime.utcnow
    )
    updated_at: datetime = Field(
        default_factory=datetime.utcnow
    )


class TaskCreate(SQLModel):
    """Request model for creating a task.

    Validates input data when creating a new task.
    """
    title: str = Field(min_length=1, max_length=255)
    description: Optional[str] = Field(default=None, max_length=2000)
    priority: PriorityLevel = Field(default=PriorityLevel.MEDIUM)
    tags: list[str] = Field(default=[])
    due_date: Optional[datetime] = None
    completed: bool = False

    @field_validator('tags')
    @classmethod
    def validate_tags(cls, v: list[str]) -> list[str]:
        """Validate tags: max 50 characters per tag, remove duplicates."""
        validated = []
        seen = set()
        for tag in v:
            if len(tag) > 50:
                raise ValueError(f"Tag '{tag[:20]}...' exceeds maximum length of 50 characters")
            # Normalize tag: lowercase and strip whitespace
            normalized = tag.strip().lower()
            if not normalized:
                continue
            if normalized not in seen:
                seen.add(normalized)
                validated.append(normalized)
        return validated

    @field_validator('due_date')
    @classmethod
    def validate_due_date(cls, v: Optional[datetime]) -> Optional[datetime]:
        """Validate due date is not more than 10 years in the past."""
        if v is not None:
            # Normalize to UTC timezone-aware datetime for comparison
            now = datetime.now(timezone.utc)
            if v.tzinfo is None:
                # If input is naive, assume it's UTC
                v = v.replace(tzinfo=timezone.utc)
            else:
                # Convert to UTC
                v = v.astimezone(timezone.utc)

            # Allow dates up to 10 years in the past (for historical tasks)
            min_date = now.replace(year=now.year - 10)
            if v < min_date:
                raise ValueError("Due date cannot be more than 10 years in the past")
        return v


class TaskUpdate(SQLModel):
    """Request model for updating a task.

    All fields are optional - only provided fields will be updated.
    """
    title: Optional[str] = Field(default=None, min_length=1, max_length=255)
    description: Optional[str] = Field(default=None, max_length=2000)
    priority: Optional[PriorityLevel] = None
    tags: Optional[list[str]] = None
    due_date: Optional[datetime] = None
    completed: Optional[bool] = None

    @field_validator('tags')
    @classmethod
    def validate_tags(cls, v: Optional[list[str]]) -> Optional[list[str]]:
        """Validate tags: max 50 characters per tag, remove duplicates."""
        if v is None:
            return v
        validated = []
        seen = set()
        for tag in v:
            if len(tag) > 50:
                raise ValueError(f"Tag '{tag[:20]}...' exceeds maximum length of 50 characters")
            # Normalize tag: lowercase and strip whitespace
            normalized = tag.strip().lower()
            if not normalized:
                continue
            if normalized not in seen:
                seen.add(normalized)
                validated.append(normalized)
        return validated

    @field_validator('due_date')
    @classmethod
    def validate_due_date(cls, v: Optional[datetime]) -> Optional[datetime]:
        """Validate due date is not more than 10 years in the past."""
        if v is not None:
            # Normalize to UTC timezone-aware datetime for comparison
            now = datetime.now(timezone.utc)
            if v.tzinfo is None:
                # If input is naive, assume it's UTC
                v = v.replace(tzinfo=timezone.utc)
            else:
                # Convert to UTC
                v = v.astimezone(timezone.utc)

            # Allow dates up to 10 years in the past (for historical tasks)
            min_date = now.replace(year=now.year - 10)
            if v < min_date:
                raise ValueError("Due date cannot be more than 10 years in the past")
        return v


class TaskRead(SQLModel):
    """Response model for task data.

    Used for serializing task data in API responses.
    """
    id: uuid.UUID
    user_id: uuid.UUID
    title: str
    description: Optional[str] | None
    priority: PriorityLevel
    tags: list[str]
    due_date: Optional[datetime] | None
    completed: bool
    created_at: datetime
    updated_at: datetime