diff --git a/backend/src/core/entities/review/review.py b/backend/src/core/entities/review/review.py index e3e9649..fc2a2a4 100644 --- a/backend/src/core/entities/review/review.py +++ b/backend/src/core/entities/review/review.py @@ -102,3 +102,9 @@ class ReviewSummary(BaseModel): min_length=4, max_length=300 ) + +class ReviewPaginated(ReviewSummary): + tool_name: str = Field( + ..., + description="Tool name" + ) diff --git a/backend/src/core/entities/users/worker/worker.py b/backend/src/core/entities/users/worker/worker.py index 44b3ca4..eb62ecb 100644 --- a/backend/src/core/entities/users/worker/worker.py +++ b/backend/src/core/entities/users/worker/worker.py @@ -1,5 +1,5 @@ -from pydantic import Field +from pydantic import Field, BaseModel, EmailStr from datetime import datetime from typing import List, Optional @@ -48,6 +48,35 @@ class WorkerPrivateSummary(BaseUserPrivateSummary): ..., description="Worker's job title", ) + date: datetime = Field( + default=None, + description="Employee start date", + ) + +class WorkerPaginated(BaseModel): + name: str = Field( + ..., + description="Worker's first name" + ) + surname: str = Field( + ..., + description="Worker's last name" + ) + phone: str = Field( + ..., + description="Worker's phone number", + pattern=r'^\+?[1-9]\d{1,14}$' + ) + email: EmailStr = Field( + ..., + description="User's email", + min_length=1, + max_length=100, + ) + jobTitle: str = Field( + ..., + description="Worker's job title" + ) date: datetime = Field( default=None, description="Employee start date", diff --git a/backend/src/core/repositories/review_repos/ireview_repository.py b/backend/src/core/repositories/review_repos/ireview_repository.py index fbe6b0a..30cf69b 100644 --- a/backend/src/core/repositories/review_repos/ireview_repository.py +++ b/backend/src/core/repositories/review_repos/ireview_repository.py @@ -1,6 +1,7 @@ from abc import ABC, abstractmethod from src.core.entities.review.review import ReviewCreate, ReviewCreated, ReviewSummary, Review from typing import List, Optional +from datetime import datetime class IReviewRepository(ABC): @abstractmethod @@ -13,4 +14,17 @@ async def get_reviews_by_tool_id(self, tool_id: str) -> List[Review]: @abstractmethod async def exists(self, tool_id: str, reviewer_id: str) -> bool: + pass + + @abstractmethod + async def get_paginated_reviews( + self, + page: int, + page_size: int, + tool_ids: Optional[List[str]] = None, + reviewer_ids: Optional[List[str]] = None, + rating: Optional[int] = None, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None + ) -> List[Review]: pass \ No newline at end of file diff --git a/backend/src/core/repositories/tool_repos/itool_repository.py b/backend/src/core/repositories/tool_repos/itool_repository.py index 570a6dd..10995e6 100644 --- a/backend/src/core/repositories/tool_repos/itool_repository.py +++ b/backend/src/core/repositories/tool_repos/itool_repository.py @@ -43,3 +43,11 @@ async def search( @abstractmethod async def exists_by_id(self, tool_id: str) -> bool: pass + + @abstractmethod + async def get_name_by_id(self, tool_id: str) -> Optional[str]: + pass + + @abstractmethod + async def get_ids_by_name(self, name: str) -> List[str]: + pass diff --git a/backend/src/core/repositories/users_repos/client_repos/iclient_repository.py b/backend/src/core/repositories/users_repos/client_repos/iclient_repository.py index 0a6e0cb..2d308e0 100644 --- a/backend/src/core/repositories/users_repos/client_repos/iclient_repository.py +++ b/backend/src/core/repositories/users_repos/client_repos/iclient_repository.py @@ -44,4 +44,8 @@ async def get_password_by_id(self, client_id: str) -> str: @abstractmethod async def get_full_name(self, client_id: str) -> ClientFullName: + pass + + @abstractmethod + async def get_ids_by_fullname(self, name: Optional[str], surname: Optional[str]) -> List[str]: pass \ No newline at end of file diff --git a/backend/src/core/repositories/users_repos/worker_repos/iworker_repository.py b/backend/src/core/repositories/users_repos/worker_repos/iworker_repository.py index d6fec68..ba42e05 100644 --- a/backend/src/core/repositories/users_repos/worker_repos/iworker_repository.py +++ b/backend/src/core/repositories/users_repos/worker_repos/iworker_repository.py @@ -3,7 +3,7 @@ from src.core.entities.object_id_str import ObjectIdStr from src.core.entities.users.base_user import UpdateUser, UpdatedUser, UpdatedUserPassword -from src.core.entities.users.worker.worker import Worker, WorkerInDB, WorkerPrivateSummary +from src.core.entities.users.worker.worker import Worker, WorkerInDB, WorkerPrivateSummary, WorkerPaginated class IWorkerRepository(ABC): @@ -45,4 +45,17 @@ async def update_password(self, worker_id: str, new_password: str) -> UpdatedUse @abstractmethod async def get_password_by_id(self, worker_id: str) -> str: + pass + + @abstractmethod + async def get_paginated_workers( + self, + page: int, + page_size: int, + email: Optional[str] = None, + name: Optional[str] = None, + surname: Optional[str] = None, + phone: Optional[str] = None, + jobTitle: Optional[str] = None + ) -> List[WorkerPaginated]: pass \ No newline at end of file diff --git a/backend/src/core/services/review_service/review_service.py b/backend/src/core/services/review_service/review_service.py index 6adb761..c653132 100644 --- a/backend/src/core/services/review_service/review_service.py +++ b/backend/src/core/services/review_service/review_service.py @@ -1,12 +1,12 @@ -from src.core.entities.review.review import ReviewCreate, ReviewCreated, ReviewSummary, Review -from typing import List +from src.core.entities.review.review import ReviewCreate, ReviewCreated, ReviewSummary, Review, ReviewPaginated +from typing import List, Optional from src.core.exceptions.client_error import ResourceNotFoundError, ResourceAlreadyExistsError, PaymentStateError from src.core.repositories.order_repos.iorder_repository import IOrderRepository from src.core.repositories.review_repos.ireview_repository import IReviewRepository from src.core.repositories.tool_repos.itool_repository import IToolRepository from src.core.repositories.users_repos.client_repos.iclient_repository import IClientRepository from src.infrastructure.repo_implementations.helpers.id_mapper import objectId_to_str, str_to_objectId - +from datetime import datetime class ReviewService: def __init__(self, review_repo: IReviewRepository, tool_repo: IToolRepository, client_repo: IClientRepository, order_repo: IOrderRepository): @@ -63,4 +63,52 @@ async def get_tool_reviews(self, tool_id: str) -> List[ReviewSummary]: ) review_summaries.append(review_summary) - return review_summaries \ No newline at end of file + return review_summaries + + async def get_paginated_reviews( + self, + page: int, + page_size: int, + tool_name: Optional[str] = None, + reviewer_name: Optional[str] = None, + reviewer_surname: Optional[str] = None, + rating: Optional[int] = None, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None + ) -> List[ReviewPaginated]: + tool_ids = None + if tool_name: + tool_ids = await self.tool_repo.get_ids_by_name(tool_name) + + reviewer_ids = None + if reviewer_name or reviewer_surname: + reviewer_ids = await self.client_repo.get_ids_by_fullname( + name=reviewer_name, + surname=reviewer_surname + ) + + reviews = await self.review_repo.get_paginated_reviews( + page=page, + page_size=page_size, + tool_ids=tool_ids, + reviewer_ids=reviewer_ids, + rating=rating, + start_date=start_date, + end_date=end_date + ) + + result = [] + for review in reviews: + tool_name = await self.tool_repo.get_name_by_id(objectId_to_str(review.toolId)) + full_name = await self.client_repo.get_full_name(objectId_to_str(review.reviewerId)) + result.append(ReviewPaginated( + reviewer_name=full_name.name, + reviewer_surname=full_name.surname, + rating=review.rating, + date=review.date, + text=review.text, + tool_name=tool_name + )) + + return result + diff --git a/backend/src/core/services/worker_service/worker_service.py b/backend/src/core/services/worker_service/worker_service.py index e57f1ca..8cb3be7 100644 --- a/backend/src/core/services/worker_service/worker_service.py +++ b/backend/src/core/services/worker_service/worker_service.py @@ -1,8 +1,8 @@ from src.core.entities.users.base_user import UpdateUser, UpdatedUser, UpdateUserPassword, UpdatedUserPassword -from src.core.entities.users.worker.worker import WorkerInDB, WorkerPrivateSummary +from src.core.entities.users.worker.worker import WorkerInDB, WorkerPrivateSummary, WorkerPaginated from src.core.exceptions.client_error import ResourceNotFoundError, InvalidPasswordProvided from src.core.repositories.users_repos.worker_repos.iworker_repository import IWorkerRepository -from typing import List +from typing import List, Optional from src.configs.paths import Paths from src.configs.urls import Urls from src.core.utils.image_decoder.image_decoder import ImageDecoder @@ -41,4 +41,24 @@ async def get_private_worker_summary(self, worker_id: str) -> WorkerPrivateSumma if not await self.worker_repo.exists_by_id(worker_id): raise ResourceNotFoundError("The client with provided id does not exist", details={"id": worker_id}) - return await self.worker_repo.get_private_summary_by_id(worker_id) \ No newline at end of file + return await self.worker_repo.get_private_summary_by_id(worker_id) + + async def get_paginated_workers( + self, + page: int, + page_size: int, + email: Optional[str] = None, + name: Optional[str] = None, + surname: Optional[str] = None, + phone: Optional[str] = None, + jobTitle: Optional[str] = None, + ) -> List[WorkerPaginated]: + return await self.worker_repo.get_paginated_workers( + page=page, + page_size=page_size, + email=email, + name=name, + surname=surname, + phone=phone, + jobTitle=jobTitle + ) \ No newline at end of file diff --git a/backend/src/infrastructure/api/review_controller.py b/backend/src/infrastructure/api/review_controller.py index 69b1869..09ed5af 100644 --- a/backend/src/infrastructure/api/review_controller.py +++ b/backend/src/infrastructure/api/review_controller.py @@ -1,10 +1,12 @@ -from fastapi import APIRouter, Depends -from typing import List +from fastapi import APIRouter, Depends, Query +from typing import List, Optional from fastapi.security import OAuth2PasswordBearer -from src.core.entities.review.review import ReviewSummary, ReviewCreated, ReviewCreate +from src.core.entities.review.review import ReviewCreated, ReviewCreate, ReviewPaginated from src.core.services.review_service.review_service import ReviewService -from src.infrastructure.api.security.role_required import is_self +from src.infrastructure.api.security.role_required import is_self, is_worker from src.infrastructure.services_instances import get_review_service +from pydantic import PositiveInt +from datetime import datetime review_router = APIRouter() @@ -23,14 +25,45 @@ async def create_review( is_self(token, str(data.reviewerId)) return await review_service.create_review(data) +@review_router.get( + path="/paginated", + status_code=200, + response_model=List[ReviewPaginated] +) +async def get_reviews_paginated( + page: PositiveInt = Query(1), + page_size: PositiveInt = Query(12), + tool_name: Optional[str] = Query(None), + reviewer_name: Optional[str] = Query(None), + reviewer_surname: Optional[str] = Query(None), + rating: Optional[PositiveInt] = Query(None), + start_date: Optional[datetime] = Query(None), + end_date: Optional[datetime] = Query(None), + review_service: ReviewService = Depends(get_review_service), + token: str = Depends(oauth2_scheme) +): + is_worker(token) + return await review_service.get_paginated_reviews( + page=page, + page_size=page_size, + tool_name=tool_name, + reviewer_name=reviewer_name, + reviewer_surname=reviewer_surname, + rating=rating, + start_date=start_date, + end_date=end_date + ) + @review_router.get( path="/{tool_id}", status_code=200, - response_model=List[ReviewSummary] + response_model=List[ReviewPaginated] ) async def get_tool_reviews( tool_id: str, review_service: ReviewService = Depends(get_review_service) ): return await review_service.get_tool_reviews(tool_id) + + diff --git a/backend/src/infrastructure/api/worker_contoller.py b/backend/src/infrastructure/api/worker_contoller.py index 12f26b1..19bdc37 100644 --- a/backend/src/infrastructure/api/worker_contoller.py +++ b/backend/src/infrastructure/api/worker_contoller.py @@ -1,28 +1,45 @@ -from fastapi import APIRouter, Depends -from typing import List +from fastapi import APIRouter, Depends, Query +from typing import List, Optional +from pydantic import PositiveInt from src.core.entities.users.base_user import UpdatedUser, UpdateUser, UpdatedUserPassword, UpdateUserPassword -from src.core.entities.users.worker.worker import WorkerInDB, WorkerPrivateSummary +from src.core.entities.users.worker.worker import WorkerPrivateSummary, WorkerPaginated from src.core.services.worker_service.worker_service import WorkerService from src.infrastructure.api.security.role_required import is_worker, is_self from src.infrastructure.services_instances import get_worker_service from fastapi.security import OAuth2PasswordBearer + + worker_router = APIRouter() oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login") @worker_router.get( - path="/", + path="/paginated", status_code=200, - response_model=List[WorkerPrivateSummary] + response_model=List[WorkerPaginated] ) -async def get_all_workers( +async def get_workers_paginated( + page: PositiveInt = Query(1), + page_size: PositiveInt = Query(12), + email: Optional[str] = Query(None), + name: Optional[str] = Query(None), + surname: Optional[str] = Query(None), + phone: Optional[str] = Query(None), + jobTitle: Optional[str] = Query(None), worker_service: WorkerService = Depends(get_worker_service), - token: str = Depends(oauth2_scheme) - + #token: str = Depends(oauth2_scheme) ): - is_worker(token) - return await worker_service.get_all_workers_summary() + #is_worker(token) + return await worker_service.get_paginated_workers( + page=page, + page_size=page_size, + email=email, + name=name, + surname=surname, + phone=phone, + jobTitle=jobTitle + ) @worker_router.get( diff --git a/backend/src/infrastructure/repo_implementations/review_repos/mongo_review_repository.py b/backend/src/infrastructure/repo_implementations/review_repos/mongo_review_repository.py index 5b08a8c..0ef4912 100644 --- a/backend/src/infrastructure/repo_implementations/review_repos/mongo_review_repository.py +++ b/backend/src/infrastructure/repo_implementations/review_repos/mongo_review_repository.py @@ -1,11 +1,13 @@ -from src.core.entities.review.review import ReviewCreate, Review +from unittest import skipIf + +from src.core.entities.review.review import ReviewCreate, Review, ReviewPaginated from src.core.repositories.review_repos.ireview_repository import IReviewRepository from motor.motor_asyncio import AsyncIOMotorDatabase from src.core.exceptions.server_error import DatabaseError from pymongo.errors import PyMongoError -from typing import List +from typing import List, Optional from src.infrastructure.repo_implementations.helpers.id_mapper import str_to_objectId - +from datetime import datetime class MongoReviewRepository(IReviewRepository): def __init__(self, db: AsyncIOMotorDatabase, review_collection: str): @@ -42,3 +44,48 @@ async def exists(self, tool_id: str, reviewer_id: str) -> bool: except PyMongoError: raise DatabaseError() + async def get_paginated_reviews( + self, + page: int, + page_size: int, + tool_ids: Optional[List[str]] = None, + reviewer_ids: Optional[List[str]] = None, + rating: Optional[int] = None, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None + ) -> List[Review]: + try: + skip = (page - 1) * page_size + filters = {} + + + if tool_ids is not None: + filters["toolId"] = {"$in": [str_to_objectId(tool_id) for tool_id in tool_ids]} + if reviewer_ids is not None: + filters["reviewerId"] = {"$in": [str_to_objectId(reviewer_id) for reviewer_id in reviewer_ids]} + if rating is not None: + filters["rating"] = rating + if start_date or end_date: + filters["date"] = {} + if start_date: + filters["date"]["$gte"] = start_date + if end_date: + filters["date"]["$lte"] = end_date + + cursor = self.review_collection.find( + filters, + { + "_id": 0, + "reviewerId": 1, + "toolId": 1, + "rating": 1, + "date": 1, + "text": 1 + } + ).sort("date", -1).skip(skip).limit(page_size) + + reviews = await cursor.to_list(length=page_size) + + return [Review(**review) for review in reviews] + except PyMongoError: + raise DatabaseError() diff --git a/backend/src/infrastructure/repo_implementations/tool_repos/mongo_tool_repository.py b/backend/src/infrastructure/repo_implementations/tool_repos/mongo_tool_repository.py index 8595134..c696010 100644 --- a/backend/src/infrastructure/repo_implementations/tool_repos/mongo_tool_repository.py +++ b/backend/src/infrastructure/repo_implementations/tool_repos/mongo_tool_repository.py @@ -3,7 +3,7 @@ from src.core.entities.tool.tool import Tool, ToolSummary, ToolDetails, ToolPages from motor.motor_asyncio import AsyncIOMotorDatabase from pymongo.errors import PyMongoError -from src.infrastructure.repo_implementations.helpers.id_mapper import str_to_objectId +from src.infrastructure.repo_implementations.helpers.id_mapper import str_to_objectId, objectId_to_str from typing import List, Optional @@ -139,5 +139,31 @@ async def exists_by_id(self, tool_id: str) -> bool: try: tool = await self.tool_collection.find_one({'_id': str_to_objectId(tool_id)}) return tool is not None + except PyMongoError: + raise DatabaseError() + + async def get_name_by_id(self, tool_id: str) -> Optional[str]: + try: + tool_data = await self.tool_collection.find_one( + {"_id": str_to_objectId(tool_id)}, + {"_id": 0, "name": 1} + ) + + if tool_data: + return tool_data["name"] + return None + except PyMongoError as e: + raise DatabaseError() + + async def get_ids_by_name(self, name: str) -> List[str]: + try: + cursor = self.tool_collection.find( + {"name": {"$regex": name, "$options": "i"}}, + {"_id": 1} + ) + + tools = await cursor.to_list(length=None) + return [objectId_to_str(tool["_id"]) for tool in tools] + except PyMongoError: raise DatabaseError() \ No newline at end of file diff --git a/backend/src/infrastructure/repo_implementations/users_repos/client_repos/mongo_client_repository.py b/backend/src/infrastructure/repo_implementations/users_repos/client_repos/mongo_client_repository.py index a5feaa1..0e6f3f8 100644 --- a/backend/src/infrastructure/repo_implementations/users_repos/client_repos/mongo_client_repository.py +++ b/backend/src/infrastructure/repo_implementations/users_repos/client_repos/mongo_client_repository.py @@ -119,4 +119,22 @@ async def get_private_summary_by_id(self, client_id: str) -> ClientPrivateSummar client_data = await self.client_collection.find_one({"_id": str_to_objectId(client_id)}) return ClientPrivateSummary(**client_data) except: + raise DatabaseError() + + async def get_ids_by_fullname(self, name: Optional[str], surname: Optional[str]) -> List[str]: + try: + filters = {} + if name: + filters["name"] = {"$regex": name, "$options": "i"} + if surname: + filters["surname"] = {"$regex": surname, "$options": "i"} + + cursor = self.client_collection.find( + filters, + {"_id": 1} + ) + clients = await cursor.to_list(length=None) + + return [objectId_to_str(client["_id"]) for client in clients] + except PyMongoError: raise DatabaseError() \ No newline at end of file diff --git a/backend/src/infrastructure/repo_implementations/users_repos/worker_repos/mongo_worker_repository.py b/backend/src/infrastructure/repo_implementations/users_repos/worker_repos/mongo_worker_repository.py index 03e5476..56cc492 100644 --- a/backend/src/infrastructure/repo_implementations/users_repos/worker_repos/mongo_worker_repository.py +++ b/backend/src/infrastructure/repo_implementations/users_repos/worker_repos/mongo_worker_repository.py @@ -2,13 +2,13 @@ from typing import Optional, List from src.core.entities.object_id_str import ObjectIdStr from src.core.entities.users.base_user import UpdateUser, UpdatedUser, UpdatedUserPassword -from src.core.entities.users.worker.worker import Worker, WorkerInDB, WorkerPrivateSummary +from src.core.entities.users.worker.worker import Worker, WorkerInDB, WorkerPrivateSummary, WorkerPaginated from src.core.exceptions.server_error import DatabaseError from src.core.repositories.users_repos.worker_repos.iworker_repository import IWorkerRepository from motor.motor_asyncio import AsyncIOMotorDatabase from pymongo.errors import PyMongoError from src.infrastructure.repo_implementations.helpers.id_mapper import objectId_to_str, str_to_objectId - +from pymongo import ASCENDING class MongoWorkerRepository(IWorkerRepository): def __init__(self, db: AsyncIOMotorDatabase, worker_collection: str): @@ -117,5 +117,42 @@ async def get_private_summary_by_id(self, worker_id: str) -> WorkerPrivateSummar try: worker_data = await self.worker_collection.find_one({"_id": str_to_objectId(worker_id)}) return WorkerPrivateSummary(**worker_data) - except: + except PyMongoError: raise DatabaseError() + + async def get_paginated_workers( + self, + page: int, + page_size: int, + email: Optional[str] = None, + name: Optional[str] = None, + surname: Optional[str] = None, + phone: Optional[str] = None, + jobTitle: Optional[str] = None + ) -> List[WorkerPaginated]: + try: + skip = (page - 1) * page_size + filters = {} + + if email: + filters['email'] = {"$regex": email, "$options": "i"} + if name: + filters['name'] = {"$regex": name, "$options": "i"} + if surname: + filters['surname'] = {"$regex": surname, "$options": "i"} + if phone: + filters['phone'] = {"$regex": phone, "$options": "i"} + if jobTitle: + filters['jobTitle'] = {"$regex": jobTitle, "$options": "i"} + + cursor = self.worker_collection.find( + filters, + { + "_id": 1, "email": 1, "name": 1, "surname": 1, "phone": 1, "jobTitle": 1, "date": 1 + } + ).sort("created_at", ASCENDING).skip(skip).limit(page_size) + + workers = await cursor.to_list(length=page_size) + return [WorkerPaginated(**worker) for worker in workers] + except PyMongoError: + raise DatabaseError() \ No newline at end of file