diff --git a/.gitignore b/.gitignore index 3f3c67f..e54c97b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,137 @@ -# Ignore only __pycache__ inside the app directory -app/**/__pycache__/ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class +# Only ignore __pycache__ inside app +app/**/__pycache__/ # Ignore compiled Python files inside app app/**/*.pyc app/**/*.pyo +# C extensions +*.so +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Environments .env - .venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ - +# Logs logs/ +*.log + +# Django stuff: +*.sqlite3 +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre +.pyre/ + +# pytype +.pytype/ + +# Cython debug symbols +cython_debug/ + +# VS Code +.vscode/ + +# JetBrains IDEs +.idea/ +*.iml + +# MacOS +.DS_Store + +# Thumbs.db (Windows) +Thumbs.db +ehthumbs.db + +# Others +*.bak +*.swp +*.swo +*~ + +# Local dev files +local_settings.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..26e7cad --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "yaml.schemas": { + "openapi:v3": "file:///mnt/disc1/code/thesis_quiz_project/quiz_maker/docs/rest_api_docs.yaml" + } +} \ No newline at end of file diff --git a/app/blueprints/__init__.py b/app/blueprints/__init__.py index d5186f1..756745c 100644 --- a/app/blueprints/__init__.py +++ b/app/blueprints/__init__.py @@ -2,6 +2,7 @@ from .default import default_blueprint from .auth import auth_blueprint from .user import user_blueprint +from .swagger import swagger_blueprint from .quiz import quiz_bp from .history import history_blueprint from .subject import subject_blueprint @@ -15,6 +16,7 @@ __all__ = [ "history_blueprint", "subject_blueprint", "session_bp", + "swagger_blueprint", ] diff --git a/app/blueprints/history.py b/app/blueprints/history.py index b711025..9d3d381 100644 --- a/app/blueprints/history.py +++ b/app/blueprints/history.py @@ -17,8 +17,10 @@ def user_history( @history_blueprint.route("/detail/", methods=["GET"]) @inject def user_detail_history( - answer_id, controller: HistoryController = Provide[Container.history_controller] + answer_id: str, + controller: HistoryController = Provide[Container.history_controller], ): + print(answer_id) return controller.get_detail_quiz_history(answer_id) diff --git a/app/blueprints/swagger.py b/app/blueprints/swagger.py new file mode 100644 index 0000000..99a3968 --- /dev/null +++ b/app/blueprints/swagger.py @@ -0,0 +1,23 @@ +from flask import Blueprint, jsonify, send_file +from flask_swagger_ui import get_swaggerui_blueprint +import os + +swagger_blueprint = Blueprint("swagger", __name__) + +SWAGGER_URL = "/swagger" +API_URL = "http://127.0.0.1:5000/swagger/docs" + +swagger_ui_blueprint = get_swaggerui_blueprint( + SWAGGER_URL, + API_URL, + config={"app_name": "Quiz Maker API"}, +) + +swagger_blueprint.register_blueprint(swagger_ui_blueprint) + + +@swagger_blueprint.route("/swagger/docs") +def serve_openapi(): + """Serve the OpenAPI spec from a file.""" + docs_path = os.path.abspath("docs/rest_api_docs.yaml") + return send_file(docs_path, mimetype="application/yaml") diff --git a/app/blueprints/user.py b/app/blueprints/user.py index cd7968f..69366ea 100644 --- a/app/blueprints/user.py +++ b/app/blueprints/user.py @@ -38,3 +38,11 @@ def get_user( user_id, user_controller: UserController = Provide[Container.user_controller] ): return user_controller.get_user_by_id(user_id) + + +@user_blueprint.route("/user/status/", methods=["GET"]) +@inject +def get_user_stat( + user_id, user_controller: UserController = Provide[Container.user_controller] +): + return user_controller.user_stat(user_id) diff --git a/app/configs/config.py b/app/configs/config.py index a3a3220..850623b 100644 --- a/app/configs/config.py +++ b/app/configs/config.py @@ -9,6 +9,7 @@ class Config: # Flask Environment Settings FLASK_ENV = os.getenv("FLASK_ENV", "development") DEBUG = os.getenv("DEBUG", "False").lower() in ("true", "1", "t") + API_VERSION = os.getenv("API_VERSION", "v1") SECRET_KEY = os.getenv("SECRET_KEY", "your_secret_key") # MongoDB Settings diff --git a/app/configs/logger_config.py b/app/configs/logger_config.py index 087a56f..f977829 100644 --- a/app/configs/logger_config.py +++ b/app/configs/logger_config.py @@ -9,9 +9,14 @@ class LoggerConfig: LOG_DIR = "logs" # Define the log directory @staticmethod - def init_logger(app): - """Initializes separate log files for INFO, ERROR, and WARNING levels.""" + def init_logger(app, production_mode=False): + """ + Initializes separate log files for INFO, ERROR, and WARNING levels. + Args: + app: Flask application instance + production_mode (bool): If True, disables console output and only logs to files + """ # Ensure the logs directory exists if not os.path.exists(LoggerConfig.LOG_DIR): os.makedirs(LoggerConfig.LOG_DIR) @@ -20,23 +25,37 @@ class LoggerConfig: for handler in app.logger.handlers[:]: app.logger.removeHandler(handler) + # Disable propagation to root logger to prevent console output in production + if production_mode: + app.logger.propagate = False + # Create separate loggers - info_logger = LoggerConfig._setup_logger( + info_handler = LoggerConfig._setup_logger( "info_logger", "info.log", logging.INFO, logging.WARNING ) - error_logger = LoggerConfig._setup_logger( + error_handler = LoggerConfig._setup_logger( "error_logger", "error.log", logging.ERROR, logging.CRITICAL ) - warning_logger = LoggerConfig._setup_logger( + warning_handler = LoggerConfig._setup_logger( "warning_logger", "warning.log", logging.WARNING, logging.WARNING ) # Attach handlers to Flask app logger - app.logger.addHandler(info_logger) - app.logger.addHandler(error_logger) - app.logger.addHandler(warning_logger) - + app.logger.addHandler(info_handler) + app.logger.addHandler(error_handler) + app.logger.addHandler(warning_handler) app.logger.setLevel(logging.DEBUG) # Set lowest level to capture all logs + + # Add console handler only in development mode + if not production_mode: + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.DEBUG) + console_formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + console_handler.setFormatter(console_formatter) + app.logger.addHandler(console_handler) + app.logger.info("Logger has been initialized for Flask application.") @staticmethod @@ -44,11 +63,14 @@ class LoggerConfig: """Helper method to configure loggers for specific levels.""" logger = logging.getLogger(name) logger.setLevel(level) + log_file_path = os.path.join(LoggerConfig.LOG_DIR, filename) log_handler = RotatingFileHandler(log_file_path, maxBytes=100000, backupCount=3) log_handler.setLevel(level) + log_formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") log_handler.setFormatter(log_formatter) log_handler.addFilter(lambda record: level <= record.levelno <= max_level) + logger.addHandler(log_handler) return log_handler diff --git a/app/controllers/subject_controller.py b/app/controllers/subject_controller.py index 6f080e4..2dbc17f 100644 --- a/app/controllers/subject_controller.py +++ b/app/controllers/subject_controller.py @@ -1,5 +1,6 @@ from app.services.subject_service import SubjectService from app.helpers import make_response, make_error_response +from app.schemas.requests import SubjectCreateRequest class SubjectController: @@ -8,7 +9,8 @@ class SubjectController: def create(self, req_body): try: - new_id = self.service.create_subject(req_body) + data = SubjectCreateRequest(**req_body) + new_id = self.service.create_subject(data) return make_response(message="Subject created", data={"id": new_id}) except Exception as e: return make_error_response(e) diff --git a/app/controllers/user_controller.py b/app/controllers/user_controller.py index 4ef57b3..73807d8 100644 --- a/app/controllers/user_controller.py +++ b/app/controllers/user_controller.py @@ -114,3 +114,16 @@ class UserController: message="An internal server error occurred. Please try again later.", status_code=500, ) + + def user_stat(self, user_id): + try: + response = self.user_service.get_user_status(user_id) + return make_response(message="Success retrive user stat", data=response) + except Exception as e: + current_app.logger.error( + f"Error while changing password: {str(e)}", exc_info=True + ) + return make_response( + message="An internal server error occurred. Please try again later.", + status_code=500, + ) diff --git a/app/di_container.py b/app/di_container.py index 7e91f9e..fb0ac4f 100644 --- a/app/di_container.py +++ b/app/di_container.py @@ -66,6 +66,8 @@ class Container(containers.DeclarativeContainer): user_service = providers.Factory( UserService, user_repository, + answer_repository, + quiz_repository, ) quiz_service = providers.Factory( diff --git a/app/main.py b/app/main.py index 6a97a6e..8d4f950 100644 --- a/app/main.py +++ b/app/main.py @@ -16,6 +16,7 @@ from app.blueprints import ( history_blueprint, subject_blueprint, session_bp, + swagger_blueprint, ) from app.database import init_db from redis import Redis @@ -24,7 +25,7 @@ from redis import Redis def createApp() -> tuple[Flask, SocketIO]: app = Flask(__name__) app.config.from_object(Config) - LoggerConfig.init_logger(app) + LoggerConfig.init_logger(app, not Config.DEBUG) logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s" @@ -65,6 +66,7 @@ def createApp() -> tuple[Flask, SocketIO]: ) app.register_blueprint(default_blueprint) + app.register_blueprint(swagger_blueprint) app.register_blueprint(auth_blueprint, url_prefix="/api") app.register_blueprint(user_blueprint, url_prefix="/api") app.register_blueprint(quiz_bp, url_prefix="/api/quiz") diff --git a/app/schemas/basic_response_schema.py b/app/schemas/basic_response_schema.py index d5cf112..4eb8bfc 100644 --- a/app/schemas/basic_response_schema.py +++ b/app/schemas/basic_response_schema.py @@ -15,3 +15,7 @@ class ResponseSchema(BaseModel, Generic[T]): message: str data: Optional[T] = None meta: Optional[MetaSchema] = None + + class Config: + from_attributes = True + exclude_none = True diff --git a/app/schemas/requests/subject/create_subject_schema.py b/app/schemas/requests/subject/create_subject_schema.py index 628ab33..9ed643e 100644 --- a/app/schemas/requests/subject/create_subject_schema.py +++ b/app/schemas/requests/subject/create_subject_schema.py @@ -1,10 +1,8 @@ -from pydantic import BaseModel, Field +from pydantic import BaseModel 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" - ) + name: str + alias: str + description: Optional[str] diff --git a/app/services/user_service.py b/app/services/user_service.py index 099e1c9..0ad8d93 100644 --- a/app/services/user_service.py +++ b/app/services/user_service.py @@ -1,8 +1,8 @@ -from datetime import datetime -from app.repositories import UserRepository +from app.repositories import UserRepository, UserAnswerRepository, QuizRepository from app.schemas import RegisterSchema from app.schemas.requests import ProfileUpdateSchema from app.schemas.response import UserResponseSchema +from app.models.entities import UserAnswerEntity from app.mapper import UserMapper from app.exception import AlreadyExistException, DataNotFoundException from werkzeug.security import generate_password_hash, check_password_hash @@ -10,8 +10,15 @@ from app.helpers import DatetimeUtil class UserService: - def __init__(self, user_repository: UserRepository): + def __init__( + self, + user_repository: UserRepository, + answer_repository: UserAnswerRepository, + quiz_repository: QuizRepository, + ): self.user_repository = user_repository + self.answer_repository = answer_repository + self.quiz_repository = quiz_repository def get_all_users(self): return self.user_repository.get_all_users() @@ -94,3 +101,24 @@ class UserService: user_dict["updated_at"] = DatetimeUtil.to_string(user_dict["updated_at"]) return UserResponseSchema(**user_dict) + + def get_user_status(self, user_id): + user_answers: list[UserAnswerEntity] = self.answer_repository.get_by_user( + user_id + ) + total_quiz = self.quiz_repository.count_by_user_id(user_id) + + if not user_answers: + return None + + total_score = sum(answer.total_score for answer in user_answers) + + total_questions = len(user_answers) + + percentage = total_score / total_questions + + return { + "avg_score": round(percentage, 2), + "total_solve": total_questions, + "total_quiz": total_quiz, + } diff --git a/docs/rest_api_docs.yaml b/docs/rest_api_docs.yaml new file mode 100644 index 0000000..381badf --- /dev/null +++ b/docs/rest_api_docs.yaml @@ -0,0 +1,1135 @@ +openapi: 3.0.4 +info: + title: Quiz Maker API + description: API documentation for Quiz App + version: 1.0.0 + +servers: + - url: http://127.0.0.1:5000/api + description: Local Server + - url: http://api.example.com/api + description: Production Server + +tags: + - name: Auth + description: Authentication endpoints + - name: User + description: User data endpoints + - name: Quiz + description: Quiz endpoints + - name: History + description: Quiz history endpoints + - name: Subject + description: Subject management endpoints + - name: Session + description: Session management endpoints + +paths: + /login: + post: + summary: Login + tags: [Auth] + requestBody: + $ref: "#/components/requestBodies/LoginRequest" + responses: + "200": + $ref: "#/components/responses/AuthTokenResponse" + "400": + $ref: "#/components/responses/BadRequestLogin" + + /login/google: + post: + summary: Login with Google + tags: [Auth] + requestBody: + $ref: "#/components/requestBodies/GoogleLoginRequest" + responses: + "200": + $ref: "#/components/responses/AuthTokenResponse" + "400": + $ref: "#/components/responses/BadRequestGoogleLogin" + + /logout: + post: + summary: Logout + tags: [Auth] + responses: + "200": + $ref: "#/components/responses/LogoutSuccess" + + /register: + post: + summary: Register + tags: [User] + requestBody: + $ref: "#/components/requestBodies/RegisterRequest" + responses: + "200": + $ref: "#/components/responses/UserCreated" + "409": + $ref: "#/components/responses/UserExists" + + /user: + get: + summary: Get User + tags: [User] + responses: + "200": + $ref: "#/components/responses/UserData" + + /user/update: + post: + summary: Update user profile + tags: [User] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + id: + type: string + description: Unique identifier of the user + example: "123abc" + name: + type: string + nullable: true + example: "John Doe" + birth_date: + type: string + format: date + nullable: true + example: "1990-01-01" + locale: + type: string + nullable: true + example: "en-US" + phone: + type: string + nullable: true + example: "+628123456789" + required: + - id + responses: + "200": + description: Profile updated successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: User profile updated successfully. + data: + type: object + nullable: true + example: null + meta: + type: object + nullable: true + example: null + + /quiz: + post: + summary: Create a new quiz + tags: [Quiz] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/QuizCreateRequest" + responses: + "201": + description: Quiz created successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Quiz created successfully + quiz_id: + type: string + example: "60f6c2d2e1f1c4567a123abc" + "400": + $ref: "#/components/responses/BadRequest" + + /quiz/{id}: + get: + summary: Get quiz by ID + tags: [Quiz] + parameters: + - name: id + in: path + required: true + schema: + type: string + example: 68283dc9806020760d14e963 + responses: + "200": + description: Quiz found + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Quiz Found + data: + $ref: "#/components/schemas/QuizDetails" + meta: + type: object + "404": + $ref: "#/components/responses/NotFound" + + /quiz/user/{user_id}: + get: + summary: Get quizzes by user ID + description: Returns a list of quizzes created by a specific user + tags: [Quiz] + parameters: + - name: user_id + in: path + required: true + schema: + type: string + example: 680f0e63180b5c19b3751d42 + - name: page + in: query + required: false + schema: + type: integer + example: 1 + responses: + "200": + description: List of quizzes + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Quiz list retrieved successfully + data: + type: array + items: + $ref: "#/components/schemas/QuizDetails" + meta: + $ref: "#/components/schemas/PaginationMeta" + "404": + $ref: "#/components/responses/NotFound" + + /quiz/recomendation: + get: + summary: Get recommended quizzes + description: Returns a list of recommended quizzes for the user + tags: [Quiz] + parameters: + - $ref: "#/components/parameters/PageParam" + - $ref: "#/components/parameters/LimitParam" + responses: + "200": + description: Successfully retrieved recommendation quiz list + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: success retrieve recommendation quiz + data: + type: array + items: + $ref: "#/components/schemas/RecommendedQuiz" + + /quiz/search: + get: + summary: Search quizzes by keyword + description: Returns a list of quizzes matching the search keyword + tags: [Quiz] + parameters: + - name: keyword + in: query + required: true + schema: + type: string + example: Sejarah + - $ref: "#/components/parameters/PageParam" + - $ref: "#/components/parameters/LimitParam" + responses: + "200": + description: Search result + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: success + data: + type: array + items: + $ref: "#/components/schemas/RecommendedQuiz" + meta: + $ref: "#/components/schemas/SearchMeta" + + /quiz/ai: + post: + summary: Generate labeling quiz from passage + description: Generate quiz-style labeled data from a given historical passage + tags: [Quiz] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + sentence: + type: string + example: > + Ratu Sima adalah penguasa di Kerajaan Kalingga. Ia digambarkan sebagai seorang pemimpin wanita yang tegas dan taat terhadap peraturan... + responses: + "200": + description: Successfully labeled the quiz data + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: succes labeling + data: + type: array + items: + $ref: "#/components/schemas/LabeledQuestion" + "400": + $ref: "#/components/responses/BadRequest" + + /quiz/answer: + post: + summary: Submit quiz answers + description: Submit answers for a specific quiz session + tags: [Quiz] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/QuizAnswerSubmission" + responses: + "200": + description: Answers submitted successfully + content: + application/json: + schema: + $ref: "#/components/schemas/SuccessResponse" + "400": + $ref: "#/components/responses/BadRequest" + + /quiz/answer/session: + post: + summary: Get submitted quiz answers by session + description: Retrieve user's quiz answers using session ID and user ID + tags: [Quiz] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + session_id: + type: string + example: 682a26b3bedac6c20a215452 + user_id: + type: string + example: 680f0e63180b5c19b3751d42 + responses: + "200": + description: Successfully retrieved the answer + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Successfully retrieved the answer + data: + $ref: "#/components/schemas/UserAnswerSession" + "404": + $ref: "#/components/responses/NotFound" + + /history/{user_id}: + get: + summary: Get quiz answer history by user + description: Retrieve a list of all quiz history for a given user + tags: [History] + parameters: + - name: user_id + in: path + required: true + schema: + type: string + example: 680f0e63180b5c19b3751d42 + responses: + "200": + description: Successfully retrieved history data + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: retrive history data + data: + type: array + items: + $ref: "#/components/schemas/AnswerHistoryItem" + "404": + $ref: "#/components/responses/NotFound" + + /history/detail/{answer_id}: + get: + summary: Get detail quiz history + description: Retrieve detailed quiz answer history for a specific answer ID + tags: [History] + parameters: + - name: answer_id + in: path + required: true + schema: + type: string + example: 6828bcddb5418bf21ab424b2 + responses: + "200": + description: success retrive detail history data + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: success retrive detail history data + data: + $ref: "#/components/schemas/AnswerDetailData" + + /subject: + get: + summary: Get all subjects + description: Retrieve a list of all available subjects + tags: [Subject] + responses: + "200": + description: success retrieve subject + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: success retrieve subject + data: + type: array + items: + $ref: "#/components/schemas/Subject" + + post: + summary: Create a new subject + tags: [Subject] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + example: Teknik Kimia + alias: + type: string + example: KIM + description: + type: string + example: basic Kimia + required: + - name + - alias + - description + responses: + "200": + description: Subject successfully created + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Subject created + data: + type: object + properties: + id: + type: string + example: 683178ffac911e8a9d04bb0e + meta: + type: object + nullable: true + example: null + + /session: + post: + summary: Create a new quiz session + tags: [Session] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + quiz_id: + type: string + example: 6815da9f37a1ce472ba72819 + host_id: + type: string + example: 680f0e63180b5c19b3751d42 + limit_participan: + type: integer + example: 2 + required: + - quiz_id + - host_id + - limit_participan + responses: + "200": + description: Successfully created a session + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: succes create room + data: + type: object + properties: + session_id: + type: string + example: 68317a6a48d97464ec3aaf1c + session_code: + type: string + example: AE5AFE + meta: + type: object + nullable: true + example: null + +components: + parameters: + PageParam: + name: page + in: query + required: false + schema: + type: integer + example: 1 + + LimitParam: + name: limit + in: query + required: false + schema: + type: integer + example: 4 + + requestBodies: + LoginRequest: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Login" + + GoogleLoginRequest: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/GoogleLogin" + + RegisterRequest: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Register" + + UpdateUserRequest: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateUser" + + responses: + AuthTokenResponse: + description: OK + content: + application/json: + schema: + type: object + properties: + token: + type: string + example: + + BadRequestLogin: + description: Bad Request + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorMessage" + example: + message: Invalid email or password + + BadRequestGoogleLogin: + description: Bad Request + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorMessage" + example: + message: Invalid token + + LogoutSuccess: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/SuccessMessage" + example: + message: Logout successfully + + UserCreated: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/SuccessMessage" + example: + message: User created successfully + + UserExists: + description: Conflict + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorMessage" + example: + message: Email already registered + + UserData: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/User" + + UserUpdated: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/SuccessMessage" + example: + message: User updated successfully + + BadRequest: + description: Bad Request + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorMessage" + + NotFound: + description: Not Found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorMessage" + example: + message: Resource not found + + schemas: + Login: + type: object + properties: + email: + type: string + example: user@example.com + password: + type: string + example: rahasia + + GoogleLogin: + type: object + properties: + token_id: + type: string + example: + + Register: + type: object + properties: + email: + type: string + example: user@example.com + password: + type: string + example: secure_password + name: + type: string + example: John Doe + birth_date: + type: string + example: 1990-01-01 + + UpdateUser: + type: object + properties: + email: + type: string + example: newemail@example.com + + User: + type: object + properties: + id: + type: string + example: 680f0e63180b5c19b3751d42 + name: + type: string + example: John Doe + email: + type: string + example: user@example.com + + SuccessMessage: + type: object + properties: + message: + type: string + + SuccessResponse: + type: object + properties: + message: + type: string + example: Operation completed successfully + + ErrorMessage: + type: object + properties: + message: + type: string + example: An error occurred + + PaginationMeta: + type: object + properties: + total: + type: integer + example: 25 + page: + type: integer + example: 1 + per_page: + type: integer + example: 10 + + QuizCreateRequest: + type: object + properties: + title: + type: string + example: Sejarah Indonesia + description: + type: string + example: Kuis tentang sejarah Indonesia + is_public: + type: boolean + example: true + author_id: + type: string + example: 680f0e63180b5c19b3751d42 + subject_id: + type: string + example: 68131eac43a09ed7dbb2cf44 + questions: + type: array + items: + $ref: "#/components/schemas/QuizQuestion" + + QuizQuestion: + type: object + properties: + index: + type: integer + example: 1 + question: + type: string + example: Siapakah ketua Wali Songo yang juga dikenal sebagai Sunan Gresik? + target_answer: + oneOf: + - type: string + - type: boolean + - type: integer + example: Maulana Malik Ibrahim + duration: + type: integer + example: 30 + type: + type: string + enum: [fill_the_blank, true_false, option] + example: fill_the_blank + options: + type: array + items: + type: string + nullable: true + example: null + + QuizDetails: + type: object + properties: + id: + type: string + example: 68283dc9806020760d14e963 + author_id: + type: string + example: 680f0e63180b5c19b3751d42 + subject_id: + type: string + example: 68131eac43a09ed7dbb2cf44 + subject_alias: + type: string + example: IPA + title: + type: string + example: Sejarah Indonesia - Proklamasi dan Kemerdekaan + description: + type: string + example: Kuis ini membahas peristiwa penting seputar proklamasi dan kemerdekaan Indonesia serta tokoh-tokoh terkait. + is_public: + type: boolean + example: true + date: + type: string + example: 17-May-2025 + time: + type: string + example: 14:30 + total_quiz: + type: integer + example: 10 + limit_duration: + type: integer + example: 300 + question_listings: + type: array + items: + $ref: "#/components/schemas/QuizQuestion" + + RecommendedQuiz: + type: object + properties: + quiz_id: + type: string + example: 68283dc9806020760d14e963 + author_id: + type: string + example: 680e5a6d2f480bd75db17a09 + author_name: + type: string + example: robbani + title: + type: string + example: Sejarah Indonesia - Proklamasi dan Kemerdekaan + description: + type: string + example: Kuis ini membahas peristiwa penting seputar proklamasi dan kemerdekaan Indonesia serta tokoh-tokoh terkait. + date: + type: string + example: 17-May-2025 + total_quiz: + type: integer + example: 10 + duration: + type: integer + example: 300 + + SearchMeta: + type: object + properties: + total_page: + type: integer + example: 3 + current_page: + type: integer + example: 2 + total_data: + type: integer + example: 4 + total_all_data: + type: integer + example: 9 + + LabeledQuestion: + type: object + properties: + qustion: + type: string + example: kerajaan kalingga disebutkan di di sungai + answer: + type: string + example: true + + AnswerHistoryItem: + type: object + properties: + quiz_id: + type: string + example: 68283dc9806020760d14e963 + answer_id: + type: string + example: 6828bcddb5418bf21ab424b2 + title: + type: string + example: Sejarah Indonesia - Proklamasi dan Kemerdekaan + description: + type: string + example: Kuis ini membahas peristiwa penting seputar proklamasi dan kemerdekaan Indonesia serta tokoh-tokoh terkait. + total_correct: + type: integer + example: 8 + total_question: + type: integer + example: 10 + date: + type: string + example: 2025-05-18 19:02:02 + + QuizAnswerSubmission: + type: object + properties: + session_id: + type: string + example: abc123-session-id + quiz_id: + type: string + example: 6815da9f37a1ce472ba72819 + user_id: + type: string + example: 68163d981f2241b7d8210c21 + answered_at: + type: string + format: date-time + example: 2025-04-25T10:15:00Z + answers: + type: array + items: + $ref: "#/components/schemas/QuizAnswerItem" + + QuizAnswerItem: + type: object + properties: + question_index: + type: integer + example: 1 + answer: + oneOf: + - type: string + - type: boolean + - type: integer + example: Maulana Malik Ibrahim + is_correct: + type: boolean + example: true + time_spent: + type: number + format: float + example: 6.5 + + UserAnswerSession: + type: object + properties: + id: + type: string + example: 682a26e6bedac6c20a215453 + session_id: + type: string + example: 682a26b3bedac6c20a215452 + quiz_id: + type: string + example: 682a120f18339f4cc31318e4 + user_id: + type: string + example: 680f0e63180b5c19b3751d42 + answered_at: + type: string + example: 2025-05-19 01:28:22 + answers: + type: array + items: + $ref: "#/components/schemas/AnswerDetail" + total_score: + type: integer + example: 100 + total_correct: + type: integer + example: 1 + + AnswerDetail: + type: object + properties: + index: + type: integer + example: 1 + question: + type: string + example: Siapakah ketua Wali Songo yang juga dikenal sebagai Sunan Gresik? + target_answer: + type: string + example: Maulana Malik Ibrahim + duration: + type: integer + example: 30 + type: + type: string + enum: [fill_the_blank, true_false, option] + example: fill_the_blank + options: + type: array + items: + type: string + nullable: true + example: null + answer: + oneOf: + - type: string + - type: boolean + - type: integer + example: Maulana Malik Ibrahim + is_correct: + type: boolean + example: true + time_spent: + type: number + format: float + example: 8.0 + + AnswerDetailData: + type: object + properties: + answer_id: + type: string + example: 6828bcddb5418bf21ab424b2 + quiz_id: + type: string + example: 68283dc9806020760d14e963 + title: + type: string + example: Sejarah Indonesia - Proklamasi dan Kemerdekaan + description: + type: string + example: Kuis ini membahas peristiwa penting seputar proklamasi dan kemerdekaan Indonesia serta tokoh-tokoh terkait. + author_id: + type: string + example: 680f0e63180b5c19b3751d42 + answered_at: + type: string + example: 17-May-2025 + total_correct: + type: integer + example: 8 + total_score: + type: integer + example: 80 + total_solve_time: + type: number + example: 240.5 + question_listings: + type: array + items: + $ref: "#/components/schemas/AnsweredQuestion" + + AnsweredQuestion: + type: object + properties: + index: + type: integer + example: 1 + question: + type: string + example: Siapakah ketua Wali Songo yang juga dikenal sebagai Sunan Gresik? + type: + type: string + enum: [fill_the_blank, true_false, option] + example: fill_the_blank + target_answer: + oneOf: + - type: string + - type: boolean + - type: integer + example: Maulana Malik Ibrahim + user_answer: + oneOf: + - type: string + - type: boolean + - type: integer + example: Maulana Malik Ibrahim + is_correct: + type: boolean + example: true + time_spent: + type: number + example: 25.3 + options: + type: array + items: + type: string + nullable: true + example: null + + Subject: + type: object + properties: + id: + type: string + example: 68131eac43a09ed7dbb2cf44 + name: + type: string + example: Ilmu Pengetahuan Alam + alias: + type: string + example: IPA + description: + type: string + example: Pelajaran tentang sains dan alam