From c9ff799281626792b62bc6bbbeeb633dfa446af3 Mon Sep 17 00:00:00 2001 From: Ajat Darojat Date: Mon, 15 Jun 2026 14:16:03 +0700 Subject: [PATCH 1/8] Fix: expire database session before retrieving simulation by ID --- app/services/simulation_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/services/simulation_service.py b/app/services/simulation_service.py index ea3d8c5..975018a 100644 --- a/app/services/simulation_service.py +++ b/app/services/simulation_service.py @@ -135,6 +135,7 @@ def delete_simulation_run(simulation_run_id): def get_simulation_by_id(simulation_id): + db.session.expire_all() simulation = Simulation.query.filter_by(id=simulation_id).first() if not simulation: logger.error("Simulation with id " + str(simulation_id) + " does not exist!") From e64092b908aa55356cf80ccfac7eb7bfabaf31b8 Mon Sep 17 00:00:00 2001 From: Ajat Darojat Date: Wed, 17 Jun 2026 15:10:44 +0700 Subject: [PATCH 2/8] feat: add UserPreference model and initial JSON data --- app/models/UserPreference.py | 14 ++++++++++++++ app/models/__init__.py | 1 + app/models/data/user_preferences.json | 7 +++++++ 3 files changed, 22 insertions(+) create mode 100644 app/models/UserPreference.py create mode 100644 app/models/data/user_preferences.json diff --git a/app/models/UserPreference.py b/app/models/UserPreference.py new file mode 100644 index 0000000..bc2ed81 --- /dev/null +++ b/app/models/UserPreference.py @@ -0,0 +1,14 @@ +from datetime import datetime + +from sqlalchemy import JSON + +from app.db import db +from app.types import Setting, Status, ResourceType + +class UserPreference(db.Model): + __tablename__ = "user_preferences" + + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + settings = db.Column(JSON, nullable=False) + createdAt = db.Column(db.String(), default=datetime.now()) + updatedAt = db.Column(db.String(), default=datetime.now()) diff --git a/app/models/__init__.py b/app/models/__init__.py index ee51907..0db76af 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -8,3 +8,4 @@ from app.models.Simulation import Simulation from app.models.SimulationRun import SimulationRun from app.models.Task import Task +from app.models.UserPreference import UserPreference diff --git a/app/models/data/user_preferences.json b/app/models/data/user_preferences.json new file mode 100644 index 0000000..44541c6 --- /dev/null +++ b/app/models/data/user_preferences.json @@ -0,0 +1,7 @@ +[ + { + "settings": { + "hideSimulationSettingErrors": false + } + } +] \ No newline at end of file From 7d143a15671d00a41fe795750d14a3fcc0467040 Mon Sep 17 00:00:00 2001 From: Ajat Darojat Date: Wed, 17 Jun 2026 15:11:05 +0700 Subject: [PATCH 3/8] feat: add UserPreference and UserPreferenceUpdateBody schemas --- app/schemas/user_preference_schema.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 app/schemas/user_preference_schema.py diff --git a/app/schemas/user_preference_schema.py b/app/schemas/user_preference_schema.py new file mode 100644 index 0000000..54dee05 --- /dev/null +++ b/app/schemas/user_preference_schema.py @@ -0,0 +1,13 @@ +from marshmallow import Schema, fields + + +class UserPreferenceSchema(Schema): + id = fields.Integer() + settings = fields.Dict() + createdAt = fields.String() + updatedAt = fields.String() + + +class UserPreferenceUpdateBodySchema(Schema): + settings = fields.Dict() + From 55a51ec676b1b8c96fdce1cb0db6df30fd4f360a Mon Sep 17 00:00:00 2001 From: Ajat Darojat Date: Wed, 17 Jun 2026 15:11:10 +0700 Subject: [PATCH 4/8] feat: implement user preference service with CRUD operations and initial data insertion --- app/services/user_preference_service.py | 84 +++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 app/services/user_preference_service.py diff --git a/app/services/user_preference_service.py b/app/services/user_preference_service.py new file mode 100644 index 0000000..aae5676 --- /dev/null +++ b/app/services/user_preference_service.py @@ -0,0 +1,84 @@ +import json +import logging +import os + +from flask_smorest import abort +from sqlalchemy import asc + +from app.db import db +from app.models import UserPreference +from config import app_dir +from datetime import datetime + +# Create logger for this module +logger = logging.getLogger(__name__) + + +def get_all_user_preferences(): + return UserPreference.query.order_by(asc(UserPreference.id)).all() + + +def create_new_user_preference(user_preference_data): + new_user_preference = UserPreference(**user_preference_data) + + try: + db.session.add(new_user_preference) + db.session.commit() + + except Exception as ex: + db.session.rollback() + logger.error(f"Can not create a new user preference: {ex}") + abort(400, f"Can not create a new user preference: {ex}") + + return new_user_preference + +def update_user_preference(user_preference_id, user_preference_data): + user_preference = UserPreference.query.filter_by(id=user_preference_id).first() + if not user_preference: + abort(404, message="User preference doesn't exist, cannot update!") + + try: + user_preference.settings = user_preference_data["settings"] + user_preference.updatedAt = datetime.now() + db.session.commit() + except Exception as ex: + db.session.rollback() + logger.error(f"Can not update! Error: {ex}") + abort(400, message=f"Can not update! Error: {ex}") + + return user_preference + + +def get_user_preference_by_id(user_preference_id): + user_preference = UserPreference.query.filter_by(id=user_preference_id).first() + if not user_preference: + logger.error("User preference with id " + str(user_preference_id) + " does not exists!") + abort(400, "User preference doesn't exists!") + return user_preference + + +def insert_initial_user_preferences(): + user_preferences = get_all_user_preferences() + if len(user_preferences): + return + logger.info("Inserting initial user preferences") + with open(os.path.join(app_dir, "models", "data", "user_preferences.json")) as json_user_preferences: + initial_user_preferences = json.load(json_user_preferences) + try: + new_user_preferences = [] + for user_preference in initial_user_preferences: + new_user_preferences.append( + UserPreference( + settings=user_preference["settings"], + ) + ) + + db.session.add_all(new_user_preferences) + db.session.commit() + + except Exception as ex: + db.session.rollback() + logger.error(f"Can not insert initial user preferences! Error: {ex}") + abort(400, f"Can not insert initial user preferences! Error: {ex}") + + return {"message": "Initial user preferences added successfully!"} From 5d31b545ceb23cdb2021a3092c1f9ef2e6d07335 Mon Sep 17 00:00:00 2001 From: Ajat Darojat Date: Wed, 17 Jun 2026 15:11:20 +0700 Subject: [PATCH 5/8] feat: add user preference API with GET and PUT endpoints --- app/routes/user_preference.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 app/routes/user_preference.py diff --git a/app/routes/user_preference.py b/app/routes/user_preference.py new file mode 100644 index 0000000..f5bb5c8 --- /dev/null +++ b/app/routes/user_preference.py @@ -0,0 +1,24 @@ +from flask.views import MethodView +from flask_smorest import Blueprint + +from app.schemas.user_preference_schema import ( + UserPreferenceSchema, + UserPreferenceUpdateBodySchema, +) +from app.services import user_preference_service + +blp = Blueprint("User Preference", __name__, description="User Preference API") + +@blp.route("/user-preferences") +class UserPreference(MethodView): + @blp.response(200, UserPreferenceSchema(many=True)) + def get(self): + return user_preference_service.get_all_user_preferences() + +@blp.route("/user-preferences/") +class UserPreferenceDetail(MethodView): + @blp.arguments(UserPreferenceUpdateBodySchema) + @blp.response(200, UserPreferenceSchema) + def put(self, body_data, user_preference_id): + result = user_preference_service.update_user_preference(user_preference_id, body_data) + return result From bf6c082b21bda454c38ccd16551385613d3e7be0 Mon Sep 17 00:00:00 2001 From: Ajat Darojat Date: Wed, 17 Jun 2026 15:11:27 +0700 Subject: [PATCH 6/8] feat: register user preference blueprint in routing --- app/blueprint.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/blueprint.py b/app/blueprint.py index 199c037..36af7cb 100644 --- a/app/blueprint.py +++ b/app/blueprint.py @@ -11,6 +11,7 @@ from app.routes.setting import blp as setting_blueprint from app.routes.simulation import blp as simulation_blueprint from app.routes.receive import blp as receive_blueprint +from app.routes.user_preference import blp as user_preference_blueprint # Register Blueprint @@ -27,3 +28,4 @@ def register_routing(app): api.register_blueprint(auralization_blueprint) api.register_blueprint(setting_blueprint) api.register_blueprint(receive_blueprint) + api.register_blueprint(user_preference_blueprint) From 5bae9b4620a99ae7fe85c4cc7bb17fd06a6c235b Mon Sep 17 00:00:00 2001 From: Ajat Darojat Date: Wed, 17 Jun 2026 15:11:37 +0700 Subject: [PATCH 7/8] feat: include user preference service in database initialization functions --- manage.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/manage.py b/manage.py index 6db6685..26067b2 100644 --- a/manage.py +++ b/manage.py @@ -6,7 +6,7 @@ from passlib.hash import pbkdf2_sha256 from app.db import db -from app.services import auralization_service, material_service, setting_service +from app.services import auralization_service, material_service, setting_service, user_preference_service from config import DefaultConfig @@ -85,6 +85,7 @@ def create_db(): material_service.insert_initial_materials() auralization_service.insert_initial_audios_examples() setting_service.insert_initial_settings() + user_preference_service.insert_initial_user_preferences() db.session.commit() @@ -97,6 +98,7 @@ def reset_db(): material_service.insert_initial_materials() auralization_service.insert_initial_audios_examples() setting_service.insert_initial_settings() + user_preference_service.insert_initial_user_preferences() db.session.commit() From 3fdd89abafc0220dac9b1ca5e1164a4cac465077 Mon Sep 17 00:00:00 2001 From: Ajat Darojat Date: Tue, 23 Jun 2026 16:43:30 +0700 Subject: [PATCH 8/8] chore: add numpy style docstrings --- app/models/UserPreference.py | 16 ++++- app/routes/user_preference.py | 33 +++++++++- app/schemas/user_preference_schema.py | 32 ++++++++- app/services/user_preference_service.py | 86 +++++++++++++++++++++++++ 4 files changed, 164 insertions(+), 3 deletions(-) diff --git a/app/models/UserPreference.py b/app/models/UserPreference.py index bc2ed81..46c99ae 100644 --- a/app/models/UserPreference.py +++ b/app/models/UserPreference.py @@ -6,9 +6,23 @@ from app.types import Setting, Status, ResourceType class UserPreference(db.Model): + """ + Represents the database model for storing user-specific configurations. + + Attributes + ---------- + id : int + The primary key for the preference record. Automatically incremented. + settings : dict + A JSON object containing various configuration settings for the user. + createdAt : str + The timestamp indicating when the preference record was created. + updatedAt : str + The timestamp indicating when the preference record was last updated. + """ __tablename__ = "user_preferences" id = db.Column(db.Integer, primary_key=True, autoincrement=True) settings = db.Column(JSON, nullable=False) createdAt = db.Column(db.String(), default=datetime.now()) - updatedAt = db.Column(db.String(), default=datetime.now()) + updatedAt = db.Column(db.String(), default=datetime.now()) \ No newline at end of file diff --git a/app/routes/user_preference.py b/app/routes/user_preference.py index f5bb5c8..88104f2 100644 --- a/app/routes/user_preference.py +++ b/app/routes/user_preference.py @@ -11,14 +11,45 @@ @blp.route("/user-preferences") class UserPreference(MethodView): + """ + HTTP methods for handling collections of user preferences. + """ + @blp.response(200, UserPreferenceSchema(many=True)) def get(self): + """ + Retrieve a list of all user preferences. + + Returns + ------- + list of UserPreference + A list of user preference database objects matching the schema. + """ return user_preference_service.get_all_user_preferences() @blp.route("/user-preferences/") class UserPreferenceDetail(MethodView): + """ + HTTP methods for handling operations on specific user preference records. + """ + @blp.arguments(UserPreferenceUpdateBodySchema) @blp.response(200, UserPreferenceSchema) def put(self, body_data, user_preference_id): + """ + Update an existing user preference configuration by its unique ID. + + Parameters + ---------- + body_data : dict + The validated request body containing updated user preference data. + user_preference_id : int + The unique identifier of the user preference record to modify. + + Returns + ------- + UserPreference + The updated user preference database object matching the schema. + """ result = user_preference_service.update_user_preference(user_preference_id, body_data) - return result + return result \ No newline at end of file diff --git a/app/schemas/user_preference_schema.py b/app/schemas/user_preference_schema.py index 54dee05..872990d 100644 --- a/app/schemas/user_preference_schema.py +++ b/app/schemas/user_preference_schema.py @@ -2,6 +2,24 @@ class UserPreferenceSchema(Schema): + """ + Serialization and deserialization schema for the complete UserPreference model. + + This schema is used to validate and format the full user preference data + when transferring it across application layers or API endpoints. + + Attributes + ---------- + id : int + The unique identifier for the preference record. + settings : dict + The dictionary containing user-specific configuration settings. + createdAt : str + The timestamp indicating when the preference record was created. + updatedAt : str + The timestamp indicating when the preference record was last updated. + """ + id = fields.Integer() settings = fields.Dict() createdAt = fields.String() @@ -9,5 +27,17 @@ class UserPreferenceSchema(Schema): class UserPreferenceUpdateBodySchema(Schema): - settings = fields.Dict() + """ + Deserialization schema for validating user preference update payloads. + + This schema is specifically used to validate incoming request bodies + when a user updates their configuration settings, ensuring only permissible + fields are processed. + + Attributes + ---------- + settings : dict + The dictionary containing the updated user-specific configuration settings. + """ + settings = fields.Dict() \ No newline at end of file diff --git a/app/services/user_preference_service.py b/app/services/user_preference_service.py index aae5676..711eef1 100644 --- a/app/services/user_preference_service.py +++ b/app/services/user_preference_service.py @@ -15,10 +15,37 @@ def get_all_user_preferences(): + """ + Retrieve all user preference records from the database ordered by ID. + + Returns + ------- + list of UserPreference + A list of all user preference database model objects ordered ascending by ID. + """ return UserPreference.query.order_by(asc(UserPreference.id)).all() def create_new_user_preference(user_preference_data): + """ + Create and persist a new user preference record in the database. + + Parameters + ---------- + user_preference_data : dict + A dictionary containing the field values required to instantiate + a new UserPreference model instance. + + Returns + ------- + UserPreference + The newly created and committed UserPreference database object. + + Raises + ------ + HTTPException + Aborts with a 400 status code if a database transaction error occurs. + """ new_user_preference = UserPreference(**user_preference_data) try: @@ -32,7 +59,30 @@ def create_new_user_preference(user_preference_data): return new_user_preference + def update_user_preference(user_preference_id, user_preference_data): + """ + Update the settings and timestamp of an existing user preference record. + + Parameters + ---------- + user_preference_id : int + The unique identifier of the user preference record to update. + user_preference_data : dict + A dictionary containing the updated configuration data, specifically + expecting a "settings" key. + + Returns + ------- + UserPreference + The updated and committed UserPreference database object. + + Raises + ------ + HTTPException + Aborts with a 404 status code if the record is not found, or a 400 + status code if a database transaction error occurs. + """ user_preference = UserPreference.query.filter_by(id=user_preference_id).first() if not user_preference: abort(404, message="User preference doesn't exist, cannot update!") @@ -50,6 +100,24 @@ def update_user_preference(user_preference_id, user_preference_data): def get_user_preference_by_id(user_preference_id): + """ + Retrieve a specific user preference record by its unique ID. + + Parameters + ---------- + user_preference_id : int + The unique identifier of the user preference record to fetch. + + Returns + ------- + UserPreference + The matching UserPreference database object. + + Raises + ------ + HTTPException + Aborts with a 400 status code if no record matches the given ID. + """ user_preference = UserPreference.query.filter_by(id=user_preference_id).first() if not user_preference: logger.error("User preference with id " + str(user_preference_id) + " does not exists!") @@ -58,6 +126,23 @@ def get_user_preference_by_id(user_preference_id): def insert_initial_user_preferences(): + """ + Seed the database with initial user preference data from a JSON file. + + If any records already exist in the user preferences table, the seeding + process is skipped entirely to prevent duplication. + + Returns + ------- + dict or None + A success message dictionary if initialization is performed, + or None if skipped. + + Raises + ------ + HTTPException + Aborts with a 400 status code if a database or file system error occurs. + """ user_preferences = get_all_user_preferences() if len(user_preferences): return @@ -82,3 +167,4 @@ def insert_initial_user_preferences(): abort(400, f"Can not insert initial user preferences! Error: {ex}") return {"message": "Initial user preferences added successfully!"} + \ No newline at end of file