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: