From 687d31d3f1ea0d4527aea41a52fb4005a20f3940 Mon Sep 17 00:00:00 2001 From: akhdanre Date: Fri, 21 Mar 2025 00:54:44 +0700 Subject: [PATCH 1/7] feat: setup swagger for documentation --- docs/rest_api_docs.yaml | 95 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 docs/rest_api_docs.yaml diff --git a/docs/rest_api_docs.yaml b/docs/rest_api_docs.yaml new file mode 100644 index 0000000..93e5fb1 --- /dev/null +++ b/docs/rest_api_docs.yaml @@ -0,0 +1,95 @@ +openapi: 3.0.4 +info: + title: Quiz Maker API + description: API documentation for Quiz App + version: 1.0.0 + +servers: + - url: http://api.example.com/v1 + description: Optional server description, e.g. Main (production) server + - url: http://127.0.0.1:5000/api + description: local server + +paths: + /login: + post: + summary: Login + description: Login to the application + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + email: + type: string + example: "oukenzeumasio@gmail.com" + password: + type: string + example: "rahasia" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + token: + type: string + example: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6Im91a2VuemV1bWFzaW9AZ21haWwuY29tIiwiZXhwIjoxNjIwNzQwNjY3LCJlbWFpbCI6Im91a2VuemV1bWFzaW9AZ21haWwuY29tIn0.7" + "400": + description: Bad Request + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "Invalid email or password" + /register: + post: + summary: Register + description: Register to the application + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + email: + type: string + example: "oukenzeumasio@gmail.com" + password: + type: string + example: "rahasia" + name: + type: string + example: "Oukenze" + birth_date: + type: string + example: "09-09-1999" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "User created successfully" + "409": + description: Bad Request + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "Email already registered" From 4804495371fe63fcbf17306e1b23e1fed1d8f0cb Mon Sep 17 00:00:00 2001 From: akhdanre Date: Fri, 21 Mar 2025 01:05:19 +0700 Subject: [PATCH 2/7] feat: setup swagger for documentation --- app/blueprints/__init__.py | 3 +- app/blueprints/swagger.py | 23 +++++++++++++ app/main.py | 3 +- docs/rest_api_docs.yaml | 68 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 app/blueprints/swagger.py diff --git a/app/blueprints/__init__.py b/app/blueprints/__init__.py index 9a7a4d3..2400a7a 100644 --- a/app/blueprints/__init__.py +++ b/app/blueprints/__init__.py @@ -2,5 +2,4 @@ from .default import default_blueprint from .auth import auth_blueprint from .user import user_blueprint - -# from .user import user_blueprint +from .swagger import swagger_blueprint diff --git a/app/blueprints/swagger.py b/app/blueprints/swagger.py new file mode 100644 index 0000000..dde6eac --- /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": "Flask 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/main.py b/app/main.py index a1bf450..b959dbe 100644 --- a/app/main.py +++ b/app/main.py @@ -2,7 +2,7 @@ from blueprints import default_blueprint from di_container import Container from configs import Config, LoggerConfig from flask import Flask -from blueprints import auth_blueprint, user_blueprint +from blueprints import auth_blueprint, user_blueprint, swagger_blueprint from database import init_db @@ -24,6 +24,7 @@ def createApp() -> Flask: # Register Blueprints 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") diff --git a/docs/rest_api_docs.yaml b/docs/rest_api_docs.yaml index 93e5fb1..9ae3266 100644 --- a/docs/rest_api_docs.yaml +++ b/docs/rest_api_docs.yaml @@ -10,11 +10,21 @@ servers: - url: http://127.0.0.1:5000/api description: local server +tags: + - name: Auth + description: Authentication endpoints + - name: User + description: User data endpoints + - name: Quiz + description: Quiz endpoints + paths: /login: post: summary: Login description: Login to the application + tags: + - Auth requestBody: required: true content: @@ -49,10 +59,68 @@ paths: message: type: string example: "Invalid email or password" + + /login/google: + post: + summary: Login with Google + description: Login to the application using Google + tags: + - Auth + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + token_id: + type: string + example: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6Im91a2VuemV1bWFzaW9AZ21haWwuY29tIiwiZXhwIjoxNjIwNzQwNjY3LCJlbWFpbCI6Im91a2VuemV1bWFzaW9AZ21haWwuY29tIn0.7" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + token: + type: string + example: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6Im91a2VuemV1bWFzaW9AZ21haWwuY29tIiwiZXhwIjoxNjIwNzQwNjY3LCJlbWFpbCI6Im91a2VuemV1bWFzaW9AZ21haWwuY29tIn0.7" + "400": + description: Bad Request + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "Invalid token" + /logout: + post: + summary: Logout + description: Logout from the application + tags: + - Auth + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "Logout successfully" + /register: post: summary: Register description: Register to the application + tags: + - User requestBody: required: true content: From 71a3091df05c16dd3f2a9951d58c0b4fea7ef5cf Mon Sep 17 00:00:00 2001 From: akhdanre Date: Fri, 21 Mar 2025 01:12:24 +0700 Subject: [PATCH 3/7] fix: prefix on the url path --- app/main.py | 6 ++++-- docs/rest_api_docs.yaml | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/main.py b/app/main.py index b959dbe..b3384bd 100644 --- a/app/main.py +++ b/app/main.py @@ -22,11 +22,13 @@ def createApp() -> Flask: container.wire(modules=["blueprints.auth"]) container.wire(modules=["blueprints.user"]) + prefix = f"/api/{Config.API_VERSION}" + # Register Blueprints 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(auth_blueprint, url_prefix=prefix) + app.register_blueprint(user_blueprint, url_prefix=prefix) return app diff --git a/docs/rest_api_docs.yaml b/docs/rest_api_docs.yaml index 9ae3266..45d0e54 100644 --- a/docs/rest_api_docs.yaml +++ b/docs/rest_api_docs.yaml @@ -5,10 +5,10 @@ info: version: 1.0.0 servers: - - url: http://api.example.com/v1 - description: Optional server description, e.g. Main (production) server - - url: http://127.0.0.1:5000/api + - url: http://127.0.0.1:5000/api/v1 description: local server + - url: http://api.example.com/api/v1 + description: prduction server tags: - name: Auth From 599cd689f6a81c2082e3aed954839c937c3fa88b Mon Sep 17 00:00:00 2001 From: akhdanre Date: Fri, 21 Mar 2025 01:20:32 +0700 Subject: [PATCH 4/7] feat: adding documentation for user --- .vscode/settings.json | 5 +++++ app/configs/config.py | 1 + docs/rest_api_docs.yaml | 50 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 .vscode/settings.json 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/configs/config.py b/app/configs/config.py index 499742d..e27acc7 100644 --- a/app/configs/config.py +++ b/app/configs/config.py @@ -8,6 +8,7 @@ load_dotenv(override=True) class Config: 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") MONGO_URI = os.getenv("MONGO_URI", "mongodb://localhost:27017/yourdb") diff --git a/docs/rest_api_docs.yaml b/docs/rest_api_docs.yaml index 45d0e54..032c653 100644 --- a/docs/rest_api_docs.yaml +++ b/docs/rest_api_docs.yaml @@ -161,3 +161,53 @@ paths: message: type: string example: "Email already registered" + /user: + get: + summary: Get User + description: Get user data + tags: + - User + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + id: + type: integer + example: 1 + name: + type: string + example: "Oukenze" + email: + type: string + example: "oukenzeumasio" + put: + summary: Update User + description: Update user data + tags: + - User + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + email: + type: string + example: "" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "User updated successfully" + From 9c1793088a6e767c1dab120c1661d2d9eb3c7715 Mon Sep 17 00:00:00 2001 From: akhdanre Date: Sat, 24 May 2025 14:13:34 +0700 Subject: [PATCH 5/7] feat: adding docuemntation --- app/blueprints/history.py | 4 +- docs/rest_api_docs.yaml | 945 +++++++++++++++++++++++++++++++++----- 2 files changed, 839 insertions(+), 110 deletions(-) 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/docs/rest_api_docs.yaml b/docs/rest_api_docs.yaml index 032c653..b9dbe6b 100644 --- a/docs/rest_api_docs.yaml +++ b/docs/rest_api_docs.yaml @@ -6,9 +6,9 @@ info: servers: - url: http://127.0.0.1:5000/api/v1 - description: local server + description: Local Server - url: http://api.example.com/api/v1 - description: prduction server + description: Production Server tags: - name: Auth @@ -17,95 +17,117 @@ tags: description: User data endpoints - name: Quiz description: Quiz endpoints + # - name: Answer + - name: History + - name: Subject paths: /login: post: summary: Login - description: Login to the application - tags: - - Auth + tags: [Auth] requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - email: - type: string - example: "oukenzeumasio@gmail.com" - password: - type: string - example: "rahasia" + $ref: "#/components/requestBodies/LoginRequest" responses: "200": - description: OK - content: - application/json: - schema: - type: object - properties: - token: - type: string - example: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6Im91a2VuemV1bWFzaW9AZ21haWwuY29tIiwiZXhwIjoxNjIwNzQwNjY3LCJlbWFpbCI6Im91a2VuemV1bWFzaW9AZ21haWwuY29tIn0.7" + $ref: "#/components/responses/AuthTokenResponse" "400": - description: Bad Request - content: - application/json: - schema: - type: object - properties: - message: - type: string - example: "Invalid email or password" + $ref: "#/components/responses/BadRequestLogin" /login/google: post: summary: Login with Google - description: Login to the application using Google - tags: - - Auth + 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" + + put: + summary: Update User + tags: [User] + requestBody: + $ref: "#/components/requestBodies/UpdateUserRequest" + responses: + "200": + $ref: "#/components/responses/UserUpdated" + + /quiz: + post: + summary: Create a new quiz + tags: [Quiz] requestBody: required: true content: application/json: schema: - type: object - properties: - token_id: - type: string - example: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6Im91a2VuemV1bWFzaW9AZ21haWwuY29tIiwiZXhwIjoxNjIwNzQwNjY3LCJlbWFpbCI6Im91a2VuemV1bWFzaW9AZ21haWwuY29tIn0.7" + $ref: "#/components/schemas/QuizCreateRequest" responses: - "200": - description: OK + "201": + description: Quiz created successfully content: application/json: schema: type: object properties: - token: + message: type: string - example: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6Im91a2VuemV1bWFzaW9AZ21haWwuY29tIiwiZXhwIjoxNjIwNzQwNjY3LCJlbWFpbCI6Im91a2VuemV1bWFzaW9AZ21haWwuY29tIn0.7" + example: Quiz created successfully + quiz_id: + type: string + example: "60f6c2d2e1f1c4567a123abc" "400": description: Bad Request content: application/json: schema: - type: object - properties: - message: - type: string - example: "Invalid token" - /logout: - post: - summary: Logout - description: Logout from the application - tags: - - Auth + $ref: "#/components/schemas/ErrorMessage" + + /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: OK + description: Quiz found content: application/json: schema: @@ -113,14 +135,150 @@ paths: properties: message: type: string - example: "Logout successfully" + example: Quiz Found + data: + $ref: "#/components/schemas/QuizDetails" + meta: + type: object + "404": + description: Quiz not found + content: + application/json: + schema: + $ref: "#/components/schemas/NotFoundResponse" - /register: + /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: + type: object + properties: + total: + type: integer + example: 25 + page: + type: integer + example: 1 + per_page: + type: integer + example: 10 + "404": + description: No quizzes found for user + content: + application/json: + schema: + $ref: "#/components/schemas/NotFoundResponse" + + /quiz/recomendation: + get: + summary: Get recommended quizzes + description: Returns a list of recommended quizzes for the user + tags: [Quiz] + parameters: + - name: page + in: query + required: false + schema: + type: integer + example: 1 + - name: limit + in: query + required: false + schema: + type: integer + example: 4 + 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 + - name: page + in: query + required: false + schema: + type: integer + example: 1 + - name: limit + in: query + required: false + schema: + type: integer + example: 4 + 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: Register - description: Register to the application - tags: - - User + summary: Generate labeling quiz from passage + description: Generate quiz-style labeled data from a given historical passage + tags: [Quiz] requestBody: required: true content: @@ -128,21 +286,13 @@ paths: schema: type: object properties: - email: + sentence: type: string - example: "oukenzeumasio@gmail.com" - password: - type: string - example: "rahasia" - name: - type: string - example: "Oukenze" - birth_date: - type: string - example: "09-09-1999" + example: > + Ratu Sima adalah penguasa di Kerajaan Kalingga. Ia digambarkan sebagai seorang pemimpin wanita yang tegas dan taat terhadap peraturan... responses: "200": - description: OK + description: Successfully labeled the quiz data content: application/json: schema: @@ -150,45 +300,53 @@ paths: properties: message: type: string - example: "User created successfully" - "409": + example: succes labeling + data: + type: array + items: + $ref: "#/components/schemas/LabeledQuestion" + + "400": description: Bad Request content: application/json: schema: - type: object - properties: - message: - type: string - example: "Email already registered" - /user: - get: - summary: Get User - description: Get user data - tags: - - User + $ref: "#/components/schemas/ErrorMessage" + + /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: OK + description: Answers submitted successfully content: application/json: schema: type: object properties: - id: - type: integer - example: 1 - name: + message: type: string - example: "Oukenze" - email: - type: string - example: "oukenzeumasio" - put: - summary: Update User - description: Update user data - tags: - - User + example: Quiz answers submitted successfully + "400": + description: Bad Request + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorMessage" + + /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: @@ -196,12 +354,15 @@ paths: schema: type: object properties: - email: + session_id: type: string - example: "" + example: 682a26b3bedac6c20a215452 + user_id: + type: string + example: 680f0e63180b5c19b3751d42 responses: "200": - description: OK + description: Successfully retrieved the answer content: application/json: schema: @@ -209,5 +370,571 @@ paths: properties: message: type: string - example: "User updated successfully" - + example: Successfully retrieved the answer + data: + $ref: "#/components/schemas/UserAnswerSession" + + "404": + description: Answer session not found + content: + application/json: + schema: + $ref: "#/components/schemas/NotFoundResponse" + + /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": + description: No history found + content: + application/json: + schema: + $ref: "#/components/schemas/NotFoundResponse" + + /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" + +components: + 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 + + 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 + password: + type: string + name: + type: string + birth_date: + type: string + UpdateUser: + type: object + properties: + email: + type: string + example: newemail@example.com + User: + type: object + properties: + id: + type: integer + name: + type: string + email: + type: string + SuccessMessage: + type: object + properties: + message: + type: string + ErrorMessage: + type: object + properties: + message: + type: string + QuizCreateRequest: + type: object + properties: + title: + type: string + description: + type: string + is_public: + type: boolean + author_id: + type: string + subject_id: + type: string + questions: + type: array + items: + $ref: "#/components/schemas/QuizQuestion" + QuizQuestion: + type: object + properties: + index: + type: integer + question: + type: string + target_answer: + oneOf: + - type: string + - type: boolean + - type: integer + duration: + type: integer + type: + type: string + enum: [fill_the_blank, true_false, option] + options: + type: array + items: + type: string + nullable: true + QuizDetails: + type: object + properties: + id: + type: string + author_id: + type: string + subject_id: + type: string + subject_alias: + type: string + title: + type: string + description: + type: string + is_public: + type: boolean + date: + type: string + time: + type: string + total_quiz: + type: integer + limit_duration: + type: integer + question_listings: + type: array + items: + $ref: "#/components/schemas/QuizQuestion" + NotFoundResponse: + type: object + properties: + message: + type: string + example: Quiz not found + + 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 + 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] + options: + type: array + items: + type: string + nullable: true + example: null + answer: + oneOf: + - type: string + - type: boolean + - type: integer + is_correct: + type: boolean + example: true + time_spent: + type: number + format: float + example: 8.0 + + AnswerDetailData: + type: object + properties: + answer_id: + type: string + quiz_id: + type: string + title: + type: string + description: + type: string + author_id: + type: string + answered_at: + type: string + example: 17-May-2025 + total_correct: + type: integer + total_score: + type: integer + total_solve_time: + type: number + question_listings: + type: array + items: + $ref: "#/components/schemas/AnsweredQuestion" + + AnsweredQuestion: + type: object + properties: + index: + type: integer + question: + type: string + type: + type: string + enum: [fill_the_blank, true_false, option] + target_answer: + oneOf: + - type: string + - type: boolean + - type: integer + user_answer: + oneOf: + - type: string + - type: boolean + - type: integer + is_correct: + type: boolean + time_spent: + type: number + options: + type: array + items: + type: string + nullable: true + + 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 From 7ba31325eb4b016d29bbc755d8e0d2eafedd5d3e Mon Sep 17 00:00:00 2001 From: akhdanre Date: Sat, 24 May 2025 15:00:19 +0700 Subject: [PATCH 6/7] fix: subject add data and some documentation --- app/controllers/subject_controller.py | 4 +- .../requests/subject/create_subject_schema.py | 10 +- docs/rest_api_docs.yaml | 326 +++++++++++++----- 3 files changed, 245 insertions(+), 95 deletions(-) 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/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/docs/rest_api_docs.yaml b/docs/rest_api_docs.yaml index b9dbe6b..b67f74a 100644 --- a/docs/rest_api_docs.yaml +++ b/docs/rest_api_docs.yaml @@ -5,9 +5,9 @@ info: version: 1.0.0 servers: - - url: http://127.0.0.1:5000/api/v1 + - url: http://127.0.0.1:5000/api description: Local Server - - url: http://api.example.com/api/v1 + - url: http://api.example.com/api description: Production Server tags: @@ -17,9 +17,12 @@ tags: description: User data endpoints - name: Quiz description: Quiz endpoints - # - name: Answer - name: History + description: Quiz history endpoints - name: Subject + description: Subject management endpoints + - name: Session + description: Session management endpoints paths: /login: @@ -108,11 +111,7 @@ paths: type: string example: "60f6c2d2e1f1c4567a123abc" "400": - description: Bad Request - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorMessage" + $ref: "#/components/responses/BadRequest" /quiz/{id}: get: @@ -141,11 +140,7 @@ paths: meta: type: object "404": - description: Quiz not found - content: - application/json: - schema: - $ref: "#/components/schemas/NotFoundResponse" + $ref: "#/components/responses/NotFound" /quiz/user/{user_id}: get: @@ -181,23 +176,9 @@ paths: items: $ref: "#/components/schemas/QuizDetails" meta: - type: object - properties: - total: - type: integer - example: 25 - page: - type: integer - example: 1 - per_page: - type: integer - example: 10 + $ref: "#/components/schemas/PaginationMeta" "404": - description: No quizzes found for user - content: - application/json: - schema: - $ref: "#/components/schemas/NotFoundResponse" + $ref: "#/components/responses/NotFound" /quiz/recomendation: get: @@ -205,18 +186,8 @@ paths: description: Returns a list of recommended quizzes for the user tags: [Quiz] parameters: - - name: page - in: query - required: false - schema: - type: integer - example: 1 - - name: limit - in: query - required: false - schema: - type: integer - example: 4 + - $ref: "#/components/parameters/PageParam" + - $ref: "#/components/parameters/LimitParam" responses: "200": description: Successfully retrieved recommendation quiz list @@ -245,18 +216,8 @@ paths: schema: type: string example: Sejarah - - name: page - in: query - required: false - schema: - type: integer - example: 1 - - name: limit - in: query - required: false - schema: - type: integer - example: 4 + - $ref: "#/components/parameters/PageParam" + - $ref: "#/components/parameters/LimitParam" responses: "200": description: Search result @@ -274,6 +235,7 @@ paths: $ref: "#/components/schemas/RecommendedQuiz" meta: $ref: "#/components/schemas/SearchMeta" + /quiz/ai: post: summary: Generate labeling quiz from passage @@ -305,13 +267,8 @@ paths: type: array items: $ref: "#/components/schemas/LabeledQuestion" - "400": - description: Bad Request - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorMessage" + $ref: "#/components/responses/BadRequest" /quiz/answer: post: @@ -330,17 +287,9 @@ paths: content: application/json: schema: - type: object - properties: - message: - type: string - example: Quiz answers submitted successfully + $ref: "#/components/schemas/SuccessResponse" "400": - description: Bad Request - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorMessage" + $ref: "#/components/responses/BadRequest" /quiz/answer/session: post: @@ -373,13 +322,8 @@ paths: example: Successfully retrieved the answer data: $ref: "#/components/schemas/UserAnswerSession" - "404": - description: Answer session not found - content: - application/json: - schema: - $ref: "#/components/schemas/NotFoundResponse" + $ref: "#/components/responses/NotFound" /history/{user_id}: get: @@ -408,13 +352,8 @@ paths: type: array items: $ref: "#/components/schemas/AnswerHistoryItem" - "404": - description: No history found - content: - application/json: - schema: - $ref: "#/components/schemas/NotFoundResponse" + $ref: "#/components/responses/NotFound" /history/detail/{answer_id}: get: @@ -441,6 +380,7 @@ paths: example: success retrive detail history data data: $ref: "#/components/schemas/AnswerDetailData" + /subject: get: summary: Get all subjects @@ -462,7 +402,118 @@ paths: 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 @@ -470,18 +521,21 @@ components: 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: @@ -500,6 +554,7 @@ components: token: type: string example: + BadRequestLogin: description: Bad Request content: @@ -508,6 +563,7 @@ components: $ref: "#/components/schemas/ErrorMessage" example: message: Invalid email or password + BadRequestGoogleLogin: description: Bad Request content: @@ -516,6 +572,7 @@ components: $ref: "#/components/schemas/ErrorMessage" example: message: Invalid token + LogoutSuccess: description: OK content: @@ -524,6 +581,7 @@ components: $ref: "#/components/schemas/SuccessMessage" example: message: Logout successfully + UserCreated: description: OK content: @@ -532,6 +590,7 @@ components: $ref: "#/components/schemas/SuccessMessage" example: message: User created successfully + UserExists: description: Conflict content: @@ -540,12 +599,14 @@ components: $ref: "#/components/schemas/ErrorMessage" example: message: Email already registered + UserData: description: OK content: application/json: schema: $ref: "#/components/schemas/User" + UserUpdated: description: OK content: @@ -555,6 +616,22 @@ components: 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 @@ -565,122 +642,175 @@ components: 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: integer + 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" - NotFoundResponse: - type: object - properties: - message: - type: string - example: Quiz not found RecommendedQuiz: type: object @@ -760,6 +890,7 @@ components: date: type: string example: 2025-05-18 19:02:02 + QuizAnswerSubmission: type: object properties: @@ -792,6 +923,7 @@ components: - type: string - type: boolean - type: integer + example: Maulana Malik Ibrahim is_correct: type: boolean example: true @@ -847,6 +979,7 @@ components: type: type: string enum: [fill_the_blank, true_false, option] + example: fill_the_blank options: type: array items: @@ -858,6 +991,7 @@ components: - type: string - type: boolean - type: integer + example: Maulana Malik Ibrahim is_correct: type: boolean example: true @@ -871,23 +1005,31 @@ components: 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: @@ -898,30 +1040,38 @@ components: 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 From 324173ec84214a502f78d7d9c8f7b25c1f0fb270 Mon Sep 17 00:00:00 2001 From: akhdanre Date: Sun, 25 May 2025 12:54:27 +0700 Subject: [PATCH 7/7] feat: adding user stat request --- app/blueprints/swagger.py | 2 +- app/blueprints/user.py | 8 +++++ app/configs/logger_config.py | 40 ++++++++++++++++----- app/controllers/user_controller.py | 13 +++++++ app/di_container.py | 2 ++ app/main.py | 2 +- app/schemas/basic_response_schema.py | 4 +++ app/services/user_service.py | 34 ++++++++++++++++-- docs/rest_api_docs.yaml | 53 +++++++++++++++++++++++++--- 9 files changed, 140 insertions(+), 18 deletions(-) diff --git a/app/blueprints/swagger.py b/app/blueprints/swagger.py index dde6eac..99a3968 100644 --- a/app/blueprints/swagger.py +++ b/app/blueprints/swagger.py @@ -10,7 +10,7 @@ API_URL = "http://127.0.0.1:5000/swagger/docs" swagger_ui_blueprint = get_swaggerui_blueprint( SWAGGER_URL, API_URL, - config={"app_name": "Flask API"}, + config={"app_name": "Quiz Maker API"}, ) swagger_blueprint.register_blueprint(swagger_ui_blueprint) 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/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/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 942ae96..8d4f950 100644 --- a/app/main.py +++ b/app/main.py @@ -25,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" 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/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 index b67f74a..381badf 100644 --- a/docs/rest_api_docs.yaml +++ b/docs/rest_api_docs.yaml @@ -77,14 +77,59 @@ paths: "200": $ref: "#/components/responses/UserData" - put: - summary: Update User + /user/update: + post: + summary: Update user profile tags: [User] requestBody: - $ref: "#/components/requestBodies/UpdateUserRequest" + 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": - $ref: "#/components/responses/UserUpdated" + 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: