|
| 1 | +from typing import List |
| 2 | +from dataclasses import dataclass |
| 3 | +from django.core.paginator import Paginator, EmptyPage |
| 4 | +from django.core.exceptions import ValidationError |
| 5 | +from django.urls import reverse_lazy |
| 6 | +from urllib.parse import urlencode |
| 7 | + |
| 8 | +from todo.dto.label_dto import LabelDTO |
| 9 | +from todo.dto.task_dto import TaskDTO |
| 10 | +from todo.dto.user_dto import UserDTO |
| 11 | +from todo.dto.responses.get_tasks_response import GetTasksResponse |
| 12 | +from todo.dto.responses.paginated_response import LinksData |
| 13 | +from todo.models.task import TaskModel |
| 14 | +from todo.repositories.task_repository import TaskRepository |
| 15 | +from todo.repositories.label_repository import LabelRepository |
| 16 | +from django.conf import settings |
| 17 | + |
| 18 | + |
| 19 | +@dataclass |
| 20 | +class PaginationConfig: |
| 21 | + DEFAULT_PAGE: int = 1 |
| 22 | + DEFAULT_LIMIT: int = settings.REST_FRAMEWORK["DEFAULT_PAGINATION_SETTINGS"]["DEFAULT_PAGE_LIMIT"] |
| 23 | + MAX_LIMIT: int = settings.REST_FRAMEWORK["DEFAULT_PAGINATION_SETTINGS"]["MAX_PAGE_LIMIT"] |
| 24 | + |
| 25 | + |
| 26 | +class TaskService: |
| 27 | + @classmethod |
| 28 | + def get_tasks( |
| 29 | + cls, page: int = PaginationConfig.DEFAULT_PAGE, limit: int = PaginationConfig.DEFAULT_LIMIT |
| 30 | + ) -> GetTasksResponse: |
| 31 | + try: |
| 32 | + cls._validate_pagination_params(page, limit) |
| 33 | + |
| 34 | + tasks = TaskRepository.get_all() |
| 35 | + |
| 36 | + if not tasks: |
| 37 | + return GetTasksResponse(tasks=[], links=None) |
| 38 | + |
| 39 | + paginator = Paginator(tasks, limit) |
| 40 | + |
| 41 | + try: |
| 42 | + current_page = paginator.page(page) |
| 43 | + |
| 44 | + task_dtos = [cls.prepare_task_dto(task) for task in current_page.object_list] |
| 45 | + |
| 46 | + links = cls._prepare_pagination_links(current_page=current_page, page=page, limit=limit) |
| 47 | + |
| 48 | + return GetTasksResponse(tasks=task_dtos, links=links) |
| 49 | + |
| 50 | + except EmptyPage: |
| 51 | + return GetTasksResponse( |
| 52 | + tasks=[], |
| 53 | + links=None, |
| 54 | + error={"message": "Requested page exceeds available results", "code": "PAGE_NOT_FOUND"}, |
| 55 | + ) |
| 56 | + |
| 57 | + except ValidationError as e: |
| 58 | + return GetTasksResponse(tasks=[], links=None, error={"message": str(e), "code": "VALIDATION_ERROR"}) |
| 59 | + |
| 60 | + except Exception: |
| 61 | + return GetTasksResponse( |
| 62 | + tasks=[], links=None, error={"message": "An unexpected error occurred", "code": "INTERNAL_ERROR"} |
| 63 | + ) |
| 64 | + |
| 65 | + @classmethod |
| 66 | + def _validate_pagination_params(cls, page: int, limit: int) -> None: |
| 67 | + if page < 1: |
| 68 | + raise ValidationError("Page must be a positive integer") |
| 69 | + |
| 70 | + if limit < 1: |
| 71 | + raise ValidationError("Limit must be a positive integer") |
| 72 | + |
| 73 | + if limit > PaginationConfig.MAX_LIMIT: |
| 74 | + raise ValidationError(f"Maximum limit of {PaginationConfig.MAX_LIMIT} exceeded") |
| 75 | + |
| 76 | + @classmethod |
| 77 | + def _prepare_pagination_links(cls, current_page, page: int, limit: int) -> LinksData: |
| 78 | + next_link = None |
| 79 | + prev_link = None |
| 80 | + |
| 81 | + if current_page.has_next(): |
| 82 | + next_page = current_page.next_page_number() |
| 83 | + next_link = cls.build_page_url(next_page, limit) |
| 84 | + |
| 85 | + if current_page.has_previous(): |
| 86 | + prev_page = current_page.previous_page_number() |
| 87 | + prev_link = cls.build_page_url(prev_page, limit) |
| 88 | + |
| 89 | + return LinksData(next=next_link, prev=prev_link) |
| 90 | + |
| 91 | + @classmethod |
| 92 | + def build_page_url(cls, page: int, limit: int) -> str: |
| 93 | + base_url = reverse_lazy("tasks") |
| 94 | + query_params = urlencode({"page": page, "limit": limit}) |
| 95 | + return f"{base_url}?{query_params}" |
| 96 | + |
| 97 | + @classmethod |
| 98 | + def prepare_task_dto(cls, task_model: TaskModel) -> TaskDTO: |
| 99 | + label_dtos = cls._prepare_label_dtos(task_model.labels) if task_model.labels else [] |
| 100 | + |
| 101 | + assignee = cls.prepare_user_dto(task_model.assignee) if task_model.assignee else None |
| 102 | + created_by = cls.prepare_user_dto(task_model.createdBy) |
| 103 | + updated_by = cls.prepare_user_dto(task_model.updatedBy) if task_model.updatedBy else None |
| 104 | + |
| 105 | + return TaskDTO( |
| 106 | + id=str(task_model.id), |
| 107 | + displayId=task_model.displayId, |
| 108 | + title=task_model.title, |
| 109 | + description=task_model.description, |
| 110 | + assignee=assignee, |
| 111 | + isAcknowledged=task_model.isAcknowledged, |
| 112 | + labels=label_dtos, |
| 113 | + startedAt=task_model.startedAt, |
| 114 | + dueAt=task_model.dueAt, |
| 115 | + status=task_model.status, |
| 116 | + priority=task_model.priority, |
| 117 | + createdAt=task_model.createdAt, |
| 118 | + updatedAt=task_model.updatedAt, |
| 119 | + createdBy=created_by, |
| 120 | + updatedBy=updated_by, |
| 121 | + ) |
| 122 | + |
| 123 | + @classmethod |
| 124 | + def _prepare_label_dtos(cls, label_ids: List[str]) -> List[LabelDTO]: |
| 125 | + label_models = LabelRepository.list_by_ids(label_ids) |
| 126 | + |
| 127 | + return [ |
| 128 | + LabelDTO( |
| 129 | + name=label_model.name, |
| 130 | + color=label_model.color, |
| 131 | + createdAt=label_model.createdAt, |
| 132 | + updatedAt=label_model.updatedAt if hasattr(label_model, "updatedAt") else None, |
| 133 | + createdBy=cls.prepare_user_dto(label_model.createdBy), |
| 134 | + updatedBy=cls.prepare_user_dto(label_model.updatedBy) |
| 135 | + if hasattr(label_model, "updatedBy") and label_model.updatedBy |
| 136 | + else None, |
| 137 | + ) |
| 138 | + for label_model in label_models |
| 139 | + ] |
| 140 | + |
| 141 | + @classmethod |
| 142 | + def prepare_user_dto(cls, user_id: str) -> UserDTO: |
| 143 | + return UserDTO(id=user_id, name="SYSTEM") |
0 commit comments