Merge pull request #5 from Akhdanre/feat/swagger-documentation
swagger and fix issue
This commit is contained in:
commit
a099da34f4
|
@ -1,14 +1,137 @@
|
||||||
# Ignore only __pycache__ inside the app directory
|
# Byte-compiled / optimized / DLL files
|
||||||
app/**/__pycache__/
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# Only ignore __pycache__ inside app
|
||||||
|
app/**/__pycache__/
|
||||||
# Ignore compiled Python files inside app
|
# Ignore compiled Python files inside app
|
||||||
app/**/*.pyc
|
app/**/*.pyc
|
||||||
app/**/*.pyo
|
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
|
.env
|
||||||
|
|
||||||
.venv
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Logs
|
||||||
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
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"yaml.schemas": {
|
||||||
|
"openapi:v3": "file:///mnt/disc1/code/thesis_quiz_project/quiz_maker/docs/rest_api_docs.yaml"
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ from .default import default_blueprint
|
||||||
|
|
||||||
from .auth import auth_blueprint
|
from .auth import auth_blueprint
|
||||||
from .user import user_blueprint
|
from .user import user_blueprint
|
||||||
|
from .swagger import swagger_blueprint
|
||||||
from .quiz import quiz_bp
|
from .quiz import quiz_bp
|
||||||
from .history import history_blueprint
|
from .history import history_blueprint
|
||||||
from .subject import subject_blueprint
|
from .subject import subject_blueprint
|
||||||
|
@ -15,6 +16,7 @@ __all__ = [
|
||||||
"history_blueprint",
|
"history_blueprint",
|
||||||
"subject_blueprint",
|
"subject_blueprint",
|
||||||
"session_bp",
|
"session_bp",
|
||||||
|
"swagger_blueprint",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -17,8 +17,10 @@ def user_history(
|
||||||
@history_blueprint.route("/detail/<answer_id>", methods=["GET"])
|
@history_blueprint.route("/detail/<answer_id>", methods=["GET"])
|
||||||
@inject
|
@inject
|
||||||
def user_detail_history(
|
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)
|
return controller.get_detail_quiz_history(answer_id)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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")
|
|
@ -38,3 +38,11 @@ def get_user(
|
||||||
user_id, user_controller: UserController = Provide[Container.user_controller]
|
user_id, user_controller: UserController = Provide[Container.user_controller]
|
||||||
):
|
):
|
||||||
return user_controller.get_user_by_id(user_id)
|
return user_controller.get_user_by_id(user_id)
|
||||||
|
|
||||||
|
|
||||||
|
@user_blueprint.route("/user/status/<string:user_id>", methods=["GET"])
|
||||||
|
@inject
|
||||||
|
def get_user_stat(
|
||||||
|
user_id, user_controller: UserController = Provide[Container.user_controller]
|
||||||
|
):
|
||||||
|
return user_controller.user_stat(user_id)
|
||||||
|
|
|
@ -9,6 +9,7 @@ class Config:
|
||||||
# Flask Environment Settings
|
# Flask Environment Settings
|
||||||
FLASK_ENV = os.getenv("FLASK_ENV", "development")
|
FLASK_ENV = os.getenv("FLASK_ENV", "development")
|
||||||
DEBUG = os.getenv("DEBUG", "False").lower() in ("true", "1", "t")
|
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")
|
SECRET_KEY = os.getenv("SECRET_KEY", "your_secret_key")
|
||||||
|
|
||||||
# MongoDB Settings
|
# MongoDB Settings
|
||||||
|
|
|
@ -9,9 +9,14 @@ class LoggerConfig:
|
||||||
LOG_DIR = "logs" # Define the log directory
|
LOG_DIR = "logs" # Define the log directory
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def init_logger(app):
|
def init_logger(app, production_mode=False):
|
||||||
"""Initializes separate log files for INFO, ERROR, and WARNING levels."""
|
"""
|
||||||
|
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
|
# Ensure the logs directory exists
|
||||||
if not os.path.exists(LoggerConfig.LOG_DIR):
|
if not os.path.exists(LoggerConfig.LOG_DIR):
|
||||||
os.makedirs(LoggerConfig.LOG_DIR)
|
os.makedirs(LoggerConfig.LOG_DIR)
|
||||||
|
@ -20,23 +25,37 @@ class LoggerConfig:
|
||||||
for handler in app.logger.handlers[:]:
|
for handler in app.logger.handlers[:]:
|
||||||
app.logger.removeHandler(handler)
|
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
|
# Create separate loggers
|
||||||
info_logger = LoggerConfig._setup_logger(
|
info_handler = LoggerConfig._setup_logger(
|
||||||
"info_logger", "info.log", logging.INFO, logging.WARNING
|
"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
|
"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
|
"warning_logger", "warning.log", logging.WARNING, logging.WARNING
|
||||||
)
|
)
|
||||||
|
|
||||||
# Attach handlers to Flask app logger
|
# Attach handlers to Flask app logger
|
||||||
app.logger.addHandler(info_logger)
|
app.logger.addHandler(info_handler)
|
||||||
app.logger.addHandler(error_logger)
|
app.logger.addHandler(error_handler)
|
||||||
app.logger.addHandler(warning_logger)
|
app.logger.addHandler(warning_handler)
|
||||||
|
|
||||||
app.logger.setLevel(logging.DEBUG) # Set lowest level to capture all logs
|
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.")
|
app.logger.info("Logger has been initialized for Flask application.")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -44,11 +63,14 @@ class LoggerConfig:
|
||||||
"""Helper method to configure loggers for specific levels."""
|
"""Helper method to configure loggers for specific levels."""
|
||||||
logger = logging.getLogger(name)
|
logger = logging.getLogger(name)
|
||||||
logger.setLevel(level)
|
logger.setLevel(level)
|
||||||
|
|
||||||
log_file_path = os.path.join(LoggerConfig.LOG_DIR, filename)
|
log_file_path = os.path.join(LoggerConfig.LOG_DIR, filename)
|
||||||
log_handler = RotatingFileHandler(log_file_path, maxBytes=100000, backupCount=3)
|
log_handler = RotatingFileHandler(log_file_path, maxBytes=100000, backupCount=3)
|
||||||
log_handler.setLevel(level)
|
log_handler.setLevel(level)
|
||||||
|
|
||||||
log_formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
|
log_formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
|
||||||
log_handler.setFormatter(log_formatter)
|
log_handler.setFormatter(log_formatter)
|
||||||
log_handler.addFilter(lambda record: level <= record.levelno <= max_level)
|
log_handler.addFilter(lambda record: level <= record.levelno <= max_level)
|
||||||
|
|
||||||
logger.addHandler(log_handler)
|
logger.addHandler(log_handler)
|
||||||
return log_handler
|
return log_handler
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from app.services.subject_service import SubjectService
|
from app.services.subject_service import SubjectService
|
||||||
from app.helpers import make_response, make_error_response
|
from app.helpers import make_response, make_error_response
|
||||||
|
from app.schemas.requests import SubjectCreateRequest
|
||||||
|
|
||||||
|
|
||||||
class SubjectController:
|
class SubjectController:
|
||||||
|
@ -8,7 +9,8 @@ class SubjectController:
|
||||||
|
|
||||||
def create(self, req_body):
|
def create(self, req_body):
|
||||||
try:
|
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})
|
return make_response(message="Subject created", data={"id": new_id})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return make_error_response(e)
|
return make_error_response(e)
|
||||||
|
|
|
@ -114,3 +114,16 @@ class UserController:
|
||||||
message="An internal server error occurred. Please try again later.",
|
message="An internal server error occurred. Please try again later.",
|
||||||
status_code=500,
|
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,
|
||||||
|
)
|
||||||
|
|
|
@ -66,6 +66,8 @@ class Container(containers.DeclarativeContainer):
|
||||||
user_service = providers.Factory(
|
user_service = providers.Factory(
|
||||||
UserService,
|
UserService,
|
||||||
user_repository,
|
user_repository,
|
||||||
|
answer_repository,
|
||||||
|
quiz_repository,
|
||||||
)
|
)
|
||||||
|
|
||||||
quiz_service = providers.Factory(
|
quiz_service = providers.Factory(
|
||||||
|
|
|
@ -16,6 +16,7 @@ from app.blueprints import (
|
||||||
history_blueprint,
|
history_blueprint,
|
||||||
subject_blueprint,
|
subject_blueprint,
|
||||||
session_bp,
|
session_bp,
|
||||||
|
swagger_blueprint,
|
||||||
)
|
)
|
||||||
from app.database import init_db
|
from app.database import init_db
|
||||||
from redis import Redis
|
from redis import Redis
|
||||||
|
@ -24,7 +25,7 @@ from redis import Redis
|
||||||
def createApp() -> tuple[Flask, SocketIO]:
|
def createApp() -> tuple[Flask, SocketIO]:
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config.from_object(Config)
|
app.config.from_object(Config)
|
||||||
LoggerConfig.init_logger(app)
|
LoggerConfig.init_logger(app, not Config.DEBUG)
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
|
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(default_blueprint)
|
||||||
|
app.register_blueprint(swagger_blueprint)
|
||||||
app.register_blueprint(auth_blueprint, url_prefix="/api")
|
app.register_blueprint(auth_blueprint, url_prefix="/api")
|
||||||
app.register_blueprint(user_blueprint, url_prefix="/api")
|
app.register_blueprint(user_blueprint, url_prefix="/api")
|
||||||
app.register_blueprint(quiz_bp, url_prefix="/api/quiz")
|
app.register_blueprint(quiz_bp, url_prefix="/api/quiz")
|
||||||
|
|
|
@ -15,3 +15,7 @@ class ResponseSchema(BaseModel, Generic[T]):
|
||||||
message: str
|
message: str
|
||||||
data: Optional[T] = None
|
data: Optional[T] = None
|
||||||
meta: Optional[MetaSchema] = None
|
meta: Optional[MetaSchema] = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
exclude_none = True
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
class SubjectCreateRequest(BaseModel):
|
class SubjectCreateRequest(BaseModel):
|
||||||
name: str = Field(..., example="Ilmu Pengetahuan ALam")
|
name: str
|
||||||
alias: str = Field(..., examples="IPA", alias="short_name")
|
alias: str
|
||||||
description: Optional[str] = Field(
|
description: Optional[str]
|
||||||
None, example="Pelajaran tentang angka dan logika"
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
from datetime import datetime
|
from app.repositories import UserRepository, UserAnswerRepository, QuizRepository
|
||||||
from app.repositories import UserRepository
|
|
||||||
from app.schemas import RegisterSchema
|
from app.schemas import RegisterSchema
|
||||||
from app.schemas.requests import ProfileUpdateSchema
|
from app.schemas.requests import ProfileUpdateSchema
|
||||||
from app.schemas.response import UserResponseSchema
|
from app.schemas.response import UserResponseSchema
|
||||||
|
from app.models.entities import UserAnswerEntity
|
||||||
from app.mapper import UserMapper
|
from app.mapper import UserMapper
|
||||||
from app.exception import AlreadyExistException, DataNotFoundException
|
from app.exception import AlreadyExistException, DataNotFoundException
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
@ -10,8 +10,15 @@ from app.helpers import DatetimeUtil
|
||||||
|
|
||||||
|
|
||||||
class UserService:
|
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.user_repository = user_repository
|
||||||
|
self.answer_repository = answer_repository
|
||||||
|
self.quiz_repository = quiz_repository
|
||||||
|
|
||||||
def get_all_users(self):
|
def get_all_users(self):
|
||||||
return self.user_repository.get_all_users()
|
return self.user_repository.get_all_users()
|
||||||
|
@ -94,3 +101,24 @@ class UserService:
|
||||||
user_dict["updated_at"] = DatetimeUtil.to_string(user_dict["updated_at"])
|
user_dict["updated_at"] = DatetimeUtil.to_string(user_dict["updated_at"])
|
||||||
|
|
||||||
return UserResponseSchema(**user_dict)
|
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,
|
||||||
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue