feat: adding user stat request

This commit is contained in:
akhdanre 2025-05-25 12:54:27 +07:00
parent 7ba31325eb
commit 324173ec84
9 changed files with 140 additions and 18 deletions

View File

@ -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)

View File

@ -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/<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)

View File

@ -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

View File

@ -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,
)

View File

@ -66,6 +66,8 @@ class Container(containers.DeclarativeContainer):
user_service = providers.Factory(
UserService,
user_repository,
answer_repository,
quiz_repository,
)
quiz_service = providers.Factory(

View File

@ -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"

View File

@ -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

View File

@ -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,
}

View File

@ -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: