diff --git a/app/blueprints/__init__.py b/app/blueprints/__init__.py index 5d6f278..7b887f5 100644 --- a/app/blueprints/__init__.py +++ b/app/blueprints/__init__.py @@ -4,6 +4,7 @@ from .auth import auth_blueprint from .user import user_blueprint from .quiz import quiz_bp from .history import history_blueprint +from .subject import subject_blueprint __all__ = [ "default_blueprint", @@ -11,6 +12,7 @@ __all__ = [ "user_blueprint", "quiz_bp", "history_blueprint", + "subject_blueprint", ] diff --git a/app/blueprints/subject.py b/app/blueprints/subject.py new file mode 100644 index 0000000..3e01644 --- /dev/null +++ b/app/blueprints/subject.py @@ -0,0 +1,50 @@ +from flask import Blueprint, request +from di_container import Container +from dependency_injector.wiring import inject, Provide +from controllers import SubjectController + + +subject_blueprint = Blueprint("subject", __name__) + + +@subject_blueprint.route("", methods=["POST"]) +@inject +def create_subject( + controller: SubjectController = Provide[Container.subject_controller], +): + return controller.create(request.get_json()) + + +@subject_blueprint.route("", methods=["GET"]) +@inject +def get_all_subjects( + controller: SubjectController = Provide[Container.subject_controller], +): + return controller.get_all() + + +@subject_blueprint.route("/", methods=["GET"]) +@inject +def get_subject( + subject_id: str, + controller: SubjectController = Provide[Container.subject_controller], +): + return controller.get_by_id(subject_id) + + +@subject_blueprint.route("/", methods=["PUT"]) +@inject +def update_subject( + subject_id: str, + controller: SubjectController = Provide[Container.subject_controller], +): + return controller.update(subject_id, request.get_json()) + + +@subject_blueprint.route("/", methods=["DELETE"]) +@inject +def delete_subject( + subject_id: str, + controller: SubjectController = Provide[Container.subject_controller], +): + return controller.delete(subject_id) diff --git a/app/controllers/__init__.py b/app/controllers/__init__.py index 2491620..02844fe 100644 --- a/app/controllers/__init__.py +++ b/app/controllers/__init__.py @@ -2,11 +2,12 @@ from .auth_controller import AuthController from .user_controller import UserController from .quiz_controller import QuizController from .history_controller import HistoryController - +from .subject_controller import SubjectController __all__ = [ "AuthController", "UserController", "QuizController", "HistoryController", + "SubjectController", ] diff --git a/app/controllers/subject_controller.py b/app/controllers/subject_controller.py new file mode 100644 index 0000000..a67e388 --- /dev/null +++ b/app/controllers/subject_controller.py @@ -0,0 +1,48 @@ +from services.subject_service import SubjectService +from helpers import make_response, make_error_response + + +class SubjectController: + def __init__(self, service: SubjectService): + self.service = service + + def create(self, req_body): + try: + new_id = self.service.create_subject(req_body) + return make_response(message="Subject created", data={"id": new_id}) + except Exception as e: + return make_error_response(e) + + def get_all(self): + try: + subjects = self.service.get_all_subjects() + return make_response(message="success retrieve subject", data=subjects) + except Exception as e: + return make_error_response(e) + + def get_by_id(self, subject_id: str): + try: + subject = self.service.get_subject_by_id(subject_id) + if not subject: + return make_response(message="Subject not found", status_code=404) + return make_response(data=subject.model_dump()) + except Exception as e: + return make_error_response(e) + + def update(self, subject_id: str, req_body): + try: + updated = self.service.update_subject(subject_id, req_body) + if not updated: + return make_response(message="No subject updated", status_code=404) + return make_response(message="Subject updated") + except Exception as e: + return make_error_response(e) + + def delete(self, subject_id: str): + try: + deleted = self.service.delete_subject(subject_id) + if not deleted: + return make_response(message="No subject deleted", status_code=404) + return make_response(message="Subject deleted") + except Exception as e: + return make_error_response(e) diff --git a/app/database/db.py b/app/database/db.py index fe50adb..519bc4b 100644 --- a/app/database/db.py +++ b/app/database/db.py @@ -1,5 +1,6 @@ from flask_pymongo import PyMongo from flask import Flask, current_app +from .seed.subject_seed import seed_subjects def init_db(app: Flask) -> PyMongo: @@ -8,8 +9,8 @@ def init_db(app: Flask) -> PyMongo: mongo.cx.server_info() app.logger.info("MongoDB connection established") + seed_subjects(mongo) return mongo - except Exception as e: app.logger.error(f"MongoDB connection failed: {e}") - return None # Handle failure gracefully + return None diff --git a/app/database/seed/subject_seed.py b/app/database/seed/subject_seed.py new file mode 100644 index 0000000..fd1d9b1 --- /dev/null +++ b/app/database/seed/subject_seed.py @@ -0,0 +1,42 @@ +from flask_pymongo import PyMongo + + +def seed_subjects(mongo: PyMongo): + subject_collection = mongo.db.subjects + + base_subjects = [ + { + "name": "Ilmu Pengetahuan Alam", + "short_name": "IPA", + "description": "Pelajaran tentang sains dan alam", + }, + { + "name": "Ilmu Pengetahuan Sosial", + "short_name": "IPS", + "description": "Pelajaran tentang masyarakat dan geografi", + }, + { + "name": "Sejarah", + "short_name": "Sejarah", + "description": "Pelajaran mengenai sejarah di indonesia", + }, + { + "name": "Matematika", + "short_name": "Matematika", + "description": "Pelajaran tentang angka dan logika", + }, + { + "name": "Bahasa Indonesia", + "short_name": "B.Indonesia", + "description": "Pelajaran tentang bahasa nasional", + }, + { + "name": "Sejarah", + "short_name": "Sejarah", + "description": "Pelajaran sejarah Indonesia", + }, + ] + + for subject in base_subjects: + if not subject_collection.find_one({"name": subject["name"]}): + subject_collection.insert_one(subject) diff --git a/app/di_container.py b/app/di_container.py index 566e7ea..af4088c 100644 --- a/app/di_container.py +++ b/app/di_container.py @@ -1,17 +1,26 @@ from dependency_injector import containers, providers -from controllers import ( - UserController, - AuthController, - QuizController, - HistoryController, +from repositories import ( + UserRepository, + QuizRepository, + UserAnswerRepository, + SubjectRepository, ) -from repositories import UserRepository, QuizRepository, UserAnswerRepository + from services import ( UserService, AuthService, QuizService, AnswerService, HistoryService, + SubjectService, +) + +from controllers import ( + UserController, + AuthController, + QuizController, + HistoryController, + SubjectController, ) @@ -24,10 +33,19 @@ class Container(containers.DeclarativeContainer): user_repository = providers.Factory(UserRepository, mongo.provided.db) quiz_repository = providers.Factory(QuizRepository, mongo.provided.db) answer_repository = providers.Factory(UserAnswerRepository, mongo.provided.db) + subject_repository = providers.Factory(SubjectRepository, mongo.provided.db) # services - auth_service = providers.Factory(AuthService, user_repository) - user_service = providers.Factory(UserService, user_repository) + auth_service = providers.Factory( + AuthService, + user_repository, + ) + + user_service = providers.Factory( + UserService, + user_repository, + ) + quiz_service = providers.Factory( QuizService, quiz_repository, @@ -46,8 +64,14 @@ class Container(containers.DeclarativeContainer): answer_repository, ) + subject_service = providers.Factory( + SubjectService, + subject_repository, + ) + # controllers auth_controller = providers.Factory(AuthController, user_service, auth_service) user_controller = providers.Factory(UserController, user_service) quiz_controller = providers.Factory(QuizController, quiz_service, answer_service) history_controller = providers.Factory(HistoryController, history_service) + subject_controller = providers.Factory(SubjectController, subject_service) diff --git a/app/main.py b/app/main.py index 32a3940..6ce5145 100644 --- a/app/main.py +++ b/app/main.py @@ -12,6 +12,7 @@ from blueprints import ( quiz_bp, default_blueprint, history_blueprint, + subject_blueprint, ) from database import init_db import logging @@ -34,16 +35,13 @@ def createApp() -> Flask: if mongo is not None: container.mongo.override(mongo) - # container.wire(modules=["blueprints.auth"]) - # container.wire(modules=["blueprints.user"]) - # container.wire(modules=["blueprints.quiz"]) - container.wire( modules=[ "blueprints.auth", "blueprints.user", "blueprints.quiz", "blueprints.history", + "blueprints.subject", ] ) @@ -53,6 +51,7 @@ def createApp() -> Flask: app.register_blueprint(user_blueprint, url_prefix="/api") app.register_blueprint(quiz_bp, url_prefix="/api/quiz") app.register_blueprint(history_blueprint, url_prefix="/api/history") + app.register_blueprint(subject_blueprint, url_prefix="/api/subject") # for rule in app.url_map.iter_rules(): # print(f"Route: {rule} -> Methods: {rule.methods}") diff --git a/app/models/entities/__init__.py b/app/models/entities/__init__.py index 2bf5cb6..9ede322 100644 --- a/app/models/entities/__init__.py +++ b/app/models/entities/__init__.py @@ -4,6 +4,7 @@ from .quiz_entity import QuizEntity from .question_item_entity import QuestionItemEntity from .user_answer_entity import UserAnswerEntity from .answer_item import AnswerItemEntity +from .subject_entity import SubjectEntity __all__ = [ "UserEntity", @@ -11,4 +12,6 @@ __all__ = [ "QuizEntity", "QuestionItemEntity", "UserAnswerEntity", + "AnswerItemEntity", + "SubjectEntity", ] diff --git a/app/models/entities/quiz_entity.py b/app/models/entities/quiz_entity.py index 639998f..93a3ac3 100644 --- a/app/models/entities/quiz_entity.py +++ b/app/models/entities/quiz_entity.py @@ -10,6 +10,7 @@ class QuizEntity(BaseModel): author_id: Optional[str] = None title: str description: Optional[str] = None + subject: str is_public: bool = False date: Optional[datetime] = None total_quiz: Optional[int] = 0 diff --git a/app/models/entities/subject_entity.py b/app/models/entities/subject_entity.py new file mode 100644 index 0000000..2dfd9bd --- /dev/null +++ b/app/models/entities/subject_entity.py @@ -0,0 +1,24 @@ +from typing import Optional +from bson import ObjectId +from pydantic import BaseModel, Field +from models.entities import PyObjectId + + +class SubjectEntity(BaseModel): + id: Optional[PyObjectId] = Field(default=None, alias="_id") + name: str + short_name: str + description: Optional[str] = None + icon: Optional[str] = None + + class Config: + populate_by_name = True + json_encoders = {ObjectId: str} + json_schema_extra = { + "example": { + "_id": "sejarah", + "name": "Sejarah", + "description": "Kuis tentang sejarah Indonesia", + "icon": "http://", + } + } diff --git a/app/repositories/__init__.py b/app/repositories/__init__.py index ba00511..dac7659 100644 --- a/app/repositories/__init__.py +++ b/app/repositories/__init__.py @@ -1,9 +1,11 @@ from .user_repository import UserRepository from .quiz_repositroy import QuizRepository from .answer_repository import UserAnswerRepository +from .subject_repository import SubjectRepository __all__ = [ "UserRepository", "QuizRepository", "UserAnswerRepository", + "SubjectRepository", ] diff --git a/app/repositories/subject_repository.py b/app/repositories/subject_repository.py new file mode 100644 index 0000000..f9cd236 --- /dev/null +++ b/app/repositories/subject_repository.py @@ -0,0 +1,35 @@ +from typing import List, Optional +from pymongo.database import Database +from pymongo.collection import Collection +from models.entities import SubjectEntity +from bson import ObjectId + + +class SubjectRepository: + def __init__(self, db: Database): + self.collection: Collection = db.subjects + + def create(self, subject: SubjectEntity) -> str: + subject_dict = subject.dict(by_alias=True, exclude_none=True) + result = self.collection.insert_one(subject_dict) + return str(result.inserted_id) + + def get_all(self) -> List[SubjectEntity]: + cursor = self.collection.find({}) + return [SubjectEntity(**doc) for doc in cursor] + + def get_by_id(self, subject_id: str) -> Optional[SubjectEntity]: + doc = self.collection.find_one({"_id": ObjectId(subject_id)}) + if doc: + return SubjectEntity(**doc) + return None + + def update(self, subject_id: str, update_data: dict) -> bool: + result = self.collection.update_one( + {"_id": ObjectId(subject_id)}, {"$set": update_data} + ) + return result.modified_count > 0 + + def delete(self, subject_id: str) -> bool: + result = self.collection.delete_one({"_id": ObjectId(subject_id)}) + return result.deleted_count > 0 diff --git a/app/schemas/requests/__init__.py b/app/schemas/requests/__init__.py index 1a3f8d3..7007bbb 100644 --- a/app/schemas/requests/__init__.py +++ b/app/schemas/requests/__init__.py @@ -8,6 +8,9 @@ from .quiz import ( from .answer.answer_request_schema import UserAnswerSchema from .answer.answer_item_request_schema import AnswerItemSchema +from .subject.create_subject_schema import SubjectCreateRequest +from .subject.update_subject_schema import SubjectUpdateRequest + __all__ = [ "RegisterSchema", @@ -15,4 +18,6 @@ __all__ = [ "QuizCreateSchema", "UserAnswerSchema", "AnswerItemSchema", + "SubjectCreateRequest", + "SubjectUpdateRequest", ] diff --git a/app/schemas/requests/subject/create_subject_schema.py b/app/schemas/requests/subject/create_subject_schema.py new file mode 100644 index 0000000..628ab33 --- /dev/null +++ b/app/schemas/requests/subject/create_subject_schema.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel, Field +from typing import Optional + + +class SubjectCreateRequest(BaseModel): + name: str = Field(..., example="Ilmu Pengetahuan ALam") + alias: str = Field(..., examples="IPA", alias="short_name") + description: Optional[str] = Field( + None, example="Pelajaran tentang angka dan logika" + ) diff --git a/app/schemas/requests/subject/update_subject_schema.py b/app/schemas/requests/subject/update_subject_schema.py new file mode 100644 index 0000000..caaa73c --- /dev/null +++ b/app/schemas/requests/subject/update_subject_schema.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel, Field +from typing import Optional + + +class SubjectUpdateRequest(BaseModel): + name: Optional[str] = Field(None, example="Fisika") + description: Optional[str] = Field(None, example="Pelajaran tentang hukum alam") diff --git a/app/schemas/response/__init__.py b/app/schemas/response/__init__.py index 9bd8c7c..be1b130 100644 --- a/app/schemas/response/__init__.py +++ b/app/schemas/response/__init__.py @@ -5,6 +5,7 @@ from .quiz.quiz_data_rsp_schema import UserQuizListResponse from .history.history_response import HistoryResultSchema from .history.detail_history_response import QuizHistoryResponse, QuestionResult from .recomendation.recomendation_response_schema import RecomendationResponse +from .subject.get_subject_schema import GetSubjectResponse __all__ = [ "QuizCreationResponse", @@ -15,4 +16,5 @@ __all__ = [ "QuizHistoryResponse", "QuestionResult", "RecomendationResponse", + "GetSubjectResponse", ] diff --git a/app/schemas/response/subject/get_subject_schema.py b/app/schemas/response/subject/get_subject_schema.py new file mode 100644 index 0000000..5384666 --- /dev/null +++ b/app/schemas/response/subject/get_subject_schema.py @@ -0,0 +1,13 @@ +from pydantic import BaseModel, Field +from typing import Optional + + +class GetSubjectResponse(BaseModel): + id: str + name: str + alias: str + description: Optional[str] + + class Config: + orm_mode = True + allow_population_by_field_name = True diff --git a/app/services/__init__.py b/app/services/__init__.py index f011aca..4b5b657 100644 --- a/app/services/__init__.py +++ b/app/services/__init__.py @@ -3,6 +3,7 @@ from .user_service import UserService from .quiz_service import QuizService from .answer_service import AnswerService from .history_service import HistoryService +from .subject_service import SubjectService __all__ = [ "AuthService", @@ -10,4 +11,5 @@ __all__ = [ "QuizService", "AnswerService", "HistoryService", + "SubjectService", ] diff --git a/app/services/subject_service.py b/app/services/subject_service.py new file mode 100644 index 0000000..8cf2f35 --- /dev/null +++ b/app/services/subject_service.py @@ -0,0 +1,44 @@ +from typing import List, Optional +from models.entities import SubjectEntity +from schemas.requests import SubjectCreateRequest, SubjectUpdateRequest +from schemas.response import GetSubjectResponse +from repositories import SubjectRepository + + +class SubjectService: + def __init__(self, repository: SubjectRepository): + self.repository = repository + + def create_subject(self, request: SubjectCreateRequest) -> str: + subject = SubjectEntity(**request) + return self.repository.create(subject) + + def get_all_subjects(self) -> List[GetSubjectResponse]: + subjects = self.repository.get_all() + return [ + GetSubjectResponse( + id=str(subject.id), + name=subject.name, + alias=subject.short_name, + description=subject.description, + ) + for subject in subjects + ] + + def get_subject_by_id(self, subject_id: str) -> Optional[GetSubjectResponse]: + subject = self.repository.get_by_id(subject_id) + if subject: + return GetSubjectResponse( + id=str(subject.id), + name=subject.name, + alias=subject.short_name, + description=subject.description, + ) + return None + + def update_subject(self, subject_id: str, request: SubjectUpdateRequest) -> bool: + update_data = request.dict(exclude_unset=True) + return self.repository.update(subject_id, update_data) + + def delete_subject(self, subject_id: str) -> bool: + return self.repository.delete(subject_id)