From 9dccbfc532161f41d9935baf15114da2c590090b Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Sat, 19 Apr 2025 11:46:28 +0100 Subject: [PATCH 1/7] Create user settings registry and model for user settings --- .../a39ac9e9f085_add_user_settings.py | 56 +++++++ server/models/script.py | 12 ++ server/models/user.py | 67 ++++++++- server/registry/__init__.py | 0 server/registry/user_settings.py | 138 ++++++++++++++++++ 5 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 server/alembic_config/versions/a39ac9e9f085_add_user_settings.py create mode 100644 server/registry/__init__.py create mode 100644 server/registry/user_settings.py diff --git a/server/alembic_config/versions/a39ac9e9f085_add_user_settings.py b/server/alembic_config/versions/a39ac9e9f085_add_user_settings.py new file mode 100644 index 00000000..065bd76d --- /dev/null +++ b/server/alembic_config/versions/a39ac9e9f085_add_user_settings.py @@ -0,0 +1,56 @@ +"""Add user settings + +Revision ID: a39ac9e9f085 +Revises: a44e01459595 +Create Date: 2025-04-19 11:37:43.556499 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "a39ac9e9f085" +down_revision: Union[str, None] = "a44e01459595" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "user_settings", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("user_id", sa.Integer(), nullable=True), + sa.Column("settings_type", sa.String(), nullable=True), + sa.Column("settings", sa.Text(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("updated_at", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint( + ["user_id"], ["user.id"], name=op.f("fk_user_settings_user_id_user") + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_user_settings")), + ) + with op.batch_alter_table("user_settings", schema=None) as batch_op: + batch_op.create_index( + batch_op.f("ix_user_settings_settings_type"), + ["settings_type"], + unique=False, + ) + batch_op.create_index( + batch_op.f("ix_user_settings_user_id"), ["user_id"], unique=False + ) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("user_settings", schema=None) as batch_op: + batch_op.drop_index(batch_op.f("ix_user_settings_user_id")) + batch_op.drop_index(batch_op.f("ix_user_settings_settings_type")) + + op.drop_table("user_settings") + # ### end Alembic commands ### diff --git a/server/models/script.py b/server/models/script.py index 39d33324..14d98a88 100644 --- a/server/models/script.py +++ b/server/models/script.py @@ -2,6 +2,7 @@ from sqlalchemy.orm import backref, relationship from models.models import db +from registry.user_settings import UserSettingsRegistry from utils.database import DeleteMixin @@ -139,6 +140,17 @@ class ScriptCuts(db.Model): ) +@UserSettingsRegistry.register( + settings_fields=[ + "bold", + "italic", + "underline", + "text_format", + "text_colour", + "enable_background_colour", + "background_colour", + ] +) class StageDirectionStyle(db.Model): __tablename__ = "stage_direction_styles" diff --git a/server/models/user.py b/server/models/user.py index 45ae8c69..6446de59 100644 --- a/server/models/user.py +++ b/server/models/user.py @@ -1,6 +1,11 @@ -from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String +import datetime +import json +from functools import partial + +from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Text from models.models import db +from registry.user_settings import UserSettingsRegistry class User(db.Model): @@ -12,3 +17,63 @@ class User(db.Model): show_id = Column(Integer(), ForeignKey("shows.id"), index=True) is_admin = Column(Boolean()) last_login = Column(DateTime()) + + +class UserSettings(db.Model): + __tablename__ = "user_settings" + + id = Column(Integer, primary_key=True, autoincrement=True) + user_id = Column(Integer, ForeignKey("user.id"), index=True) + + settings_type = Column(String, index=True) + settings = Column(Text) + + created_at = Column( + DateTime, default=partial(datetime.datetime.now, tz=datetime.timezone.utc) + ) + updated_at = Column( + DateTime, + default=partial(datetime.datetime.now, tz=datetime.timezone.utc), + onupdate=partial(datetime.datetime.now, tz=datetime.timezone.utc), + ) + + @property + def settings_dict(self): + """Return settings as a Python dictionary""" + return json.loads(self.settings) if self.settings else {} + + def update_settings(self, new_settings): + """Update settings with validation""" + # Validate the complete set of settings that would result from this update + current = self.settings_dict + merged = current.copy() + merged.update(new_settings) + + errors = UserSettingsRegistry.validate(self.settings_type, merged) + if errors: + raise ValueError(f"Invalid settings: {', '.join(errors)}") + + # Apply the update + self.settings = json.dumps(merged) + self.updated_at = datetime.datetime.now(tz=datetime.timezone.utc) + + @classmethod + def create_for_user(cls, user_id, settings_type, settings_data): + """Create settings with validation""" + errors = UserSettingsRegistry.validate(settings_type, settings_data) + if errors: + raise ValueError(f"Invalid settings: {', '.join(errors)}") + + settings = cls( + user_id=user_id, + settings_type=settings_type, + settings=json.dumps(settings_data), + ) + + return settings + + @classmethod + def get_default_settings(cls, settings_type): + """Get default settings from registered model""" + model_class = UserSettingsRegistry._registry[settings_type]["model"] + return model_class.to_settings_dict() diff --git a/server/registry/__init__.py b/server/registry/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/registry/user_settings.py b/server/registry/user_settings.py new file mode 100644 index 00000000..7c0542b3 --- /dev/null +++ b/server/registry/user_settings.py @@ -0,0 +1,138 @@ +from sqlalchemy import Boolean, DateTime, Float, Integer, String, Text + + +class UserSettingsRegistry: + _registry = {} + + # Type mapping for SQLAlchemy column types to Python types + _type_map = { + String: {"name": "String", "python_type": str}, + Text: {"name": "String", "python_type": str}, + Integer: {"name": "Integer", "python_type": int}, + Boolean: {"name": "Boolean", "python_type": bool}, + Float: {"name": "Float", "python_type": float}, + DateTime: {"name": "DateTime", "python_type": str}, + } + + @classmethod + def register(cls, settings_fields=None): + def decorator(model_class): + # Use the model's __tablename__ as the settings type identifier + settings_type = model_class.__tablename__ + + # Raise an exception if settings_fields is None + if settings_fields is None: + raise ValueError( + f"settings_fields must be provided for {model_class.__name__}" + ) + + # Get primary key fields + primary_key_fields = [ + c.name for c in model_class.__table__.columns if c.primary_key + ] + + # Extract schema + schema = cls._extract_schema( + model_class, primary_key_fields, settings_fields + ) + + # Store schema in registry + cls._registry[settings_type] = { + "model": model_class, + "schema": schema, + "primary_key_fields": primary_key_fields, + "settings_fields": settings_fields, + } + + # Add class method for conversion to settings + @classmethod + def to_settings_dict(model_cls, instance=None): + result = {} + + # Include all fields from schema + for field_name in cls._registry[settings_type]["schema"].keys(): + if instance and hasattr(instance, field_name): + # Get the value from the instance + result[field_name] = getattr(instance, field_name) + else: + # Get default from schema + schema_info = cls._registry[settings_type]["schema"][field_name] + result[field_name] = schema_info["default"] + return result + + # Add the method to the model class + model_class.to_settings_dict = to_settings_dict + + return model_class + + return decorator + + @classmethod + def _get_column_type_info(cls, column): + column_type = type(column.type) + + # Use the actual column type, if this fails then we need to add + # support for that column type + return cls._type_map[column_type] + + @classmethod + def _extract_schema(cls, model_class, primary_key_fields, additional_fields): + schema = {} + + # Include primary key fields and additional fields + fields_to_include = primary_key_fields + additional_fields + + for column_name, column in model_class.__table__.columns.items(): + if column_name in fields_to_include: + type_info = cls._get_column_type_info(column) + + schema[column_name] = { + "type_name": type_info["name"], + "python_type": type_info["python_type"], + "nullable": column.nullable, + "default": ( + column.default.arg if column.default is not None else None + ), + "primary_key": column.primary_key, + } + + # Make sure we have the same number of fields in the schema as we do fields requested by + # the registration + if len(schema) != len(fields_to_include): + raise ValueError( + f"Schema mismatch for {model_class.__name__}. Not all fields included." + ) + + return schema + + @classmethod + def get_schema(cls, settings_type): + if settings_type not in cls._registry: + raise ValueError(f"Settings type '{settings_type}' not registered") + return cls._registry[settings_type]["schema"] + + @classmethod + def validate(cls, settings_type, data): + if settings_type not in cls._registry: + raise ValueError(f"Settings type '{settings_type}' not registered") + + schema = cls._registry[settings_type]["schema"] + errors = [] + + # Check that all fields are present + for field in schema: + if field not in data: + errors.append(f"Missing required field: {field}") + + # Validate field types + for field, value in data.items(): + if field in schema: + expected_type = schema[field]["python_type"] + if not isinstance(value, expected_type): + errors.append( + f"Field {field} must be a {schema[field]['type_name'].lower()}" + ) + else: + errors.append(f"Unknown field: {field}") + + return errors From 29259aa51c70f6aa6f367840182ebb89b766d9ea Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Sat, 19 Apr 2025 22:14:25 +0100 Subject: [PATCH 2/7] Pin marshmallow version --- server/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/server/requirements.txt b/server/requirements.txt index 39eb177a..4d0050b5 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -8,3 +8,4 @@ tornado-prometheus==0.1.2 bcrypt==4.3.0 anytree==2.13.0 alembic==1.15.2 +marshmallow<4 \ No newline at end of file From b3d909eea5a8d6c6d8f3459277f6cd4845a8919b Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Sat, 19 Apr 2025 22:31:29 +0100 Subject: [PATCH 3/7] Add barebones user settings page and routing --- client/src/App.vue | 3 +++ client/src/router/index.js | 7 ++++++- client/src/views/{ => user}/LoginView.vue | 0 client/src/views/user/Settings.vue | 14 ++++++++++++++ 4 files changed, 23 insertions(+), 1 deletion(-) rename client/src/views/{ => user}/LoginView.vue (100%) create mode 100644 client/src/views/user/Settings.vue diff --git a/client/src/App.vue b/client/src/App.vue index f9cb80a4..317bc10d 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -80,6 +80,9 @@ + + Settings + Sign Out diff --git a/client/src/router/index.js b/client/src/router/index.js index 1e0275f8..73e94d12 100644 --- a/client/src/router/index.js +++ b/client/src/router/index.js @@ -84,7 +84,12 @@ const routes = [ { path: '/login', name: 'login', - component: () => import('../views/LoginView.vue'), + component: () => import('../views/user/LoginView.vue'), + }, + { + path: '/me', + name: 'user_settings', + component: () => import('../views/user/Settings.vue'), }, { path: '*', diff --git a/client/src/views/LoginView.vue b/client/src/views/user/LoginView.vue similarity index 100% rename from client/src/views/LoginView.vue rename to client/src/views/user/LoginView.vue diff --git a/client/src/views/user/Settings.vue b/client/src/views/user/Settings.vue new file mode 100644 index 00000000..999e21ed --- /dev/null +++ b/client/src/views/user/Settings.vue @@ -0,0 +1,14 @@ + + + From be418626fcff279907e3c793a7df5f751569cd49 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Sat, 19 Apr 2025 23:41:44 +0100 Subject: [PATCH 4/7] Add simple about user table --- client/src/views/user/Settings.vue | 14 +++++++ .../user/settings/AboutUser.vue | 40 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 client/src/vue_components/user/settings/AboutUser.vue diff --git a/client/src/views/user/Settings.vue b/client/src/views/user/Settings.vue index 999e21ed..4216001b 100644 --- a/client/src/views/user/Settings.vue +++ b/client/src/views/user/Settings.vue @@ -4,11 +4,25 @@ class="mx-0" >

User Settings

+ + + + + diff --git a/client/src/vue_components/user/settings/AboutUser.vue b/client/src/vue_components/user/settings/AboutUser.vue new file mode 100644 index 00000000..f12c2959 --- /dev/null +++ b/client/src/vue_components/user/settings/AboutUser.vue @@ -0,0 +1,40 @@ + + + From 3d826a8517511af88889048dec5613124a31fe5b Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Sun, 20 Apr 2025 17:47:09 +0100 Subject: [PATCH 5/7] Create/delete new stage direction user override --- client/src/store/modules/user/settings.js | 68 ++++ client/src/store/modules/{ => user}/user.js | 4 + client/src/store/store.js | 2 +- client/src/views/user/Settings.vue | 8 +- .../config/script/StageDirectionStyles.vue | 2 +- .../user/settings/StageDirectionStyles.vue | 371 ++++++++++++++++++ server/controllers/api/user/__init__.py | 0 server/controllers/api/user/settings.py | 126 ++++++ server/digi_server/app_server.py | 10 +- server/models/user.py | 14 + server/registry/user_settings.py | 8 + 11 files changed, 609 insertions(+), 4 deletions(-) create mode 100644 client/src/store/modules/user/settings.js rename client/src/store/modules/{ => user}/user.js (98%) create mode 100644 client/src/vue_components/user/settings/StageDirectionStyles.vue create mode 100644 server/controllers/api/user/__init__.py create mode 100644 server/controllers/api/user/settings.py diff --git a/client/src/store/modules/user/settings.js b/client/src/store/modules/user/settings.js new file mode 100644 index 00000000..70c051f7 --- /dev/null +++ b/client/src/store/modules/user/settings.js @@ -0,0 +1,68 @@ +import Vue from 'vue'; +import log from 'loglevel'; + +import { makeURL } from '@/js/utils'; + +export default { + state: { + stageDirectionStyleOverrides: [], + }, + mutations: { + SET_STAGE_DIRECTION_STYLE_OVERRIDES(state, overrides) { + state.stageDirectionStyleOverrides = overrides; + }, + }, + actions: { + async GET_STAGE_DIRECTION_STYLE_OVERRIDES(context) { + const response = await fetch(`${makeURL('/api/v1/user/settings/stage_direction_overrides')}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + if (response.ok) { + const respJson = await response.json(); + context.commit('SET_STAGE_DIRECTION_STYLE_OVERRIDES', respJson.overrides); + } else { + log.error('Unable to load stage direction style overrides'); + } + }, + async ADD_STAGE_DIRECTION_STYLE_OVERRIDE(context, style) { + const response = await fetch(`${makeURL('/api/v1/user/settings/stage_direction_overrides')}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(style), + }); + if (response.ok) { + context.dispatch('GET_STAGE_DIRECTION_STYLE_OVERRIDES'); + Vue.$toast.success('Added new stage direction style override!'); + } else { + log.error('Unable to add new stage direction style override'); + Vue.$toast.error('Unable to add new stage direction style override'); + } + }, + async DELETE_STAGE_DIRECTION_STYLE_OVERRIDE(context, styleId) { + const response = await fetch(`${makeURL('/api/v1/user/settings/stage_direction_overrides')}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ id: styleId }), + }); + if (response.ok) { + context.dispatch('GET_STAGE_DIRECTION_STYLE_OVERRIDES'); + Vue.$toast.success('Deleted stage direction style override!'); + } else { + log.error('Unable to delete stage direction style override'); + Vue.$toast.error('Unable to delete stage direction style override'); + } + }, + }, + getters: { + STAGE_DIRECTION_STYLE_OVERRIDES(state) { + return state.stageDirectionStyleOverrides; + }, + }, +}; diff --git a/client/src/store/modules/user.js b/client/src/store/modules/user/user.js similarity index 98% rename from client/src/store/modules/user.js rename to client/src/store/modules/user/user.js index 84d2f698..9d69bf28 100644 --- a/client/src/store/modules/user.js +++ b/client/src/store/modules/user/user.js @@ -3,6 +3,7 @@ import log from 'loglevel'; import { makeURL } from '@/js/utils'; import router from '@/router'; +import settings from './settings'; export default { state: { @@ -129,4 +130,7 @@ export default { return state.currentRbac; }, }, + modules: { + settings, + }, }; diff --git a/client/src/store/store.js b/client/src/store/store.js index 5d3ba719..05fadbc8 100644 --- a/client/src/store/store.js +++ b/client/src/store/store.js @@ -4,7 +4,7 @@ import createPersistedState from 'vuex-persistedstate'; import log from 'loglevel'; import { makeURL } from '@/js/utils'; -import user from '@/store/modules/user'; +import user from '@/store/modules/user/user'; import websocket from './modules/websocket'; import system from './modules/system'; import show from './modules/show'; diff --git a/client/src/views/user/Settings.vue b/client/src/views/user/Settings.vue index 4216001b..205728a2 100644 --- a/client/src/views/user/Settings.vue +++ b/client/src/views/user/Settings.vue @@ -14,15 +14,21 @@ > + + + diff --git a/client/src/vue_components/show/config/script/StageDirectionStyles.vue b/client/src/vue_components/show/config/script/StageDirectionStyles.vue index 8e9f7d53..659fa2e7 100644 --- a/client/src/vue_components/show/config/script/StageDirectionStyles.vue +++ b/client/src/vue_components/show/config/script/StageDirectionStyles.vue @@ -513,7 +513,7 @@ export default { event.preventDefault(); } else { await this.ADD_STAGE_DIRECTION_STYLE(this.createPayload); - this.resetNewCueTypeForm(); + this.resetNewFormState(); } }, async onSubmitEditStyle(event) { diff --git a/client/src/vue_components/user/settings/StageDirectionStyles.vue b/client/src/vue_components/user/settings/StageDirectionStyles.vue new file mode 100644 index 00000000..b11c410f --- /dev/null +++ b/client/src/vue_components/user/settings/StageDirectionStyles.vue @@ -0,0 +1,371 @@ + + + diff --git a/server/controllers/api/user/__init__.py b/server/controllers/api/user/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/controllers/api/user/settings.py b/server/controllers/api/user/settings.py new file mode 100644 index 00000000..b13fffc7 --- /dev/null +++ b/server/controllers/api/user/settings.py @@ -0,0 +1,126 @@ +from typing import List + +from tornado import escape, web + +from models.script import StageDirectionStyle +from models.user import UserSettings +from utils.web.base_controller import BaseAPIController +from utils.web.route import ApiRoute, ApiVersion + + +@ApiRoute("user/settings/stage_direction_overrides", ApiVersion.V1) +class UserStageDirectionOverridesController(BaseAPIController): + @web.authenticated + def get(self): + with self.make_session() as session: + overrides: List[UserSettings] = UserSettings.get_by_type( + self.current_user["id"], StageDirectionStyle, session + ) + self.set_status(200) + self.finish( + { + "overrides": [ + {"id": override.id, "settings": override.settings_dict} + for override in overrides + ] + } + ) + + @web.authenticated + async def post(self): + data = escape.json_decode(self.request.body) + style_id: str = data.get("styleId", None) + if not style_id: + self.set_status(400) + await self.finish({"message": "Style ID missing"}) + return + + bold: bool = data.get("bold", False) + italic: bool = data.get("italic", False) + underline: bool = data.get("underline", False) + + text_format: str = data.get("textFormat", None) + if not text_format or text_format not in ["default", "upper", "lower"]: + self.set_status(400) + await self.finish({"message": "Text format missing or invalid"}) + return + + text_colour: str = data.get("textColour", None) + if not text_colour: + self.set_status(400) + await self.finish({"message": "Text colour missing"}) + return + + enable_background_colour: bool = data.get("enableBackgroundColour", False) + background_colour: str = data.get("backgroundColour", None) + if enable_background_colour and not background_colour: + self.set_status(400) + await self.finish({"message": "Background colour missing"}) + return + + with self.make_session() as session: + style_to_override = session.query(StageDirectionStyle).get(style_id) + if not style_to_override: + self.set_status(404) + await self.finish({"message": "Stage direction style not found"}) + return + + user_style = UserSettings.create_for_user( + user_id=self.current_user["id"], + settings_type="stage_direction_styles", + settings_data={ + "id": style_id, + "bold": bold, + "italic": italic, + "underline": underline, + "text_format": text_format, + "text_colour": text_colour, + "enable_background_colour": enable_background_colour, + "background_colour": background_colour, + }, + ) + session.add(user_style) + session.commit() + + await self.application.ws_send_to_user( + self.current_user["id"], + "NOOP", + "GET_STAGE_DIRECTION_STYLE_OVERRIDES", + {}, + ) + + @web.authenticated + async def delete(self): + data = escape.json_decode(self.request.body) + with self.make_session() as session: + settings_id = data.get("id", None) + if not settings_id: + self.set_status(400) + await self.finish({"message": "ID missing"}) + return + + entry: UserSettings = session.get(UserSettings, settings_id) + if entry.user_id != self.current_user["id"]: + self.set_status(403) + await self.finish() + + if entry: + session.delete(entry) + session.commit() + + self.set_status(200) + await self.finish( + {"message": "Successfully deleted stage direction style override"} + ) + + await self.application.ws_send_to_user( + self.current_user["id"], + "NOOP", + "GET_STAGE_DIRECTION_STYLE_OVERRIDES", + {}, + ) + else: + self.set_status(404) + await self.finish( + {"message": "Stage direction style override not found"} + ) diff --git a/server/digi_server/app_server.py b/server/digi_server/app_server.py index b3ae4186..39867d32 100644 --- a/server/digi_server/app_server.py +++ b/server/digi_server/app_server.py @@ -289,7 +289,7 @@ async def _show_changed(self): def get_db(self) -> DigiSQLAlchemy: return self._db - def get_all_ws(self, user_id) -> List[WebSocketController]: + def get_all_ws(self, user_id: int) -> List[WebSocketController]: sockets = [] for client in self.clients: c_user_id = client.get_secure_cookie("digiscript_user_id") @@ -308,3 +308,11 @@ async def ws_send_to_all(self, ws_op: str, ws_action: str, ws_data: dict): await client.write_message( {"OP": ws_op, "DATA": ws_data, "ACTION": ws_action} ) + + async def ws_send_to_user( + self, user_id: int, ws_op: str, ws_action: str, ws_data: dict + ): + for client in self.get_all_ws(user_id): + await client.write_message( + {"OP": ws_op, "DATA": ws_data, "ACTION": ws_action} + ) diff --git a/server/models/user.py b/server/models/user.py index 6446de59..0ba61c75 100644 --- a/server/models/user.py +++ b/server/models/user.py @@ -1,6 +1,7 @@ import datetime import json from functools import partial +from typing import Union from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Text @@ -57,6 +58,19 @@ def update_settings(self, new_settings): self.settings = json.dumps(merged) self.updated_at = datetime.datetime.now(tz=datetime.timezone.utc) + @classmethod + def get_by_type(cls, user_id, settings_type: Union[db.Model, str], session): + if issubclass(settings_type, db.Model): + settings_type = settings_type.__tablename__ + if not UserSettingsRegistry.is_registered(settings_type): + return [] + return ( + session.query(UserSettings) + .filter_by(user_id=user_id) + .filter_by(settings_type=settings_type) + .all() + ) + @classmethod def create_for_user(cls, user_id, settings_type, settings_data): """Create settings with validation""" diff --git a/server/registry/user_settings.py b/server/registry/user_settings.py index 7c0542b3..7be76cfd 100644 --- a/server/registry/user_settings.py +++ b/server/registry/user_settings.py @@ -1,5 +1,7 @@ from sqlalchemy import Boolean, DateTime, Float, Integer, String, Text +from models.models import db + class UserSettingsRegistry: _registry = {} @@ -136,3 +138,9 @@ def validate(cls, settings_type, data): errors.append(f"Unknown field: {field}") return errors + + @classmethod + def is_registered(cls, settings_type): + if isinstance(settings_type, db.Model): + settings_type = settings_type.__tablename__ + return settings_type in cls._registry From ccbe8e391bbab49a5776529ee9f199bccde4e519 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Sun, 20 Apr 2025 21:29:37 +0100 Subject: [PATCH 6/7] Edit stage direction override, fix other controller logic --- client/src/store/modules/user/settings.js | 16 ++ .../user/settings/StageDirectionStyles.vue | 224 +++++++++++++++++- server/controllers/api/user/settings.py | 54 ++++- server/models/user.py | 2 +- server/registry/user_settings.py | 6 +- 5 files changed, 293 insertions(+), 9 deletions(-) diff --git a/client/src/store/modules/user/settings.js b/client/src/store/modules/user/settings.js index 70c051f7..d4555c6d 100644 --- a/client/src/store/modules/user/settings.js +++ b/client/src/store/modules/user/settings.js @@ -59,6 +59,22 @@ export default { Vue.$toast.error('Unable to delete stage direction style override'); } }, + async UPDATE_STAGE_DIRECTION_STYLE_OVERRIDE(context, style) { + const response = await fetch(`${makeURL('/api/v1/user/settings/stage_direction_overrides')}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(style), + }); + if (response.ok) { + context.dispatch('GET_STAGE_DIRECTION_STYLE_OVERRIDES'); + Vue.$toast.success('Updated stage direction style override!'); + } else { + log.error('Unable to edit stage direction style override'); + Vue.$toast.error('Unable to edit stage direction style override'); + } + }, }, getters: { STAGE_DIRECTION_STYLE_OVERRIDES(state) { diff --git a/client/src/vue_components/user/settings/StageDirectionStyles.vue b/client/src/vue_components/user/settings/StageDirectionStyles.vue index b11c410f..249f50ae 100644 --- a/client/src/vue_components/user/settings/StageDirectionStyles.vue +++ b/client/src/vue_components/user/settings/StageDirectionStyles.vue @@ -149,6 +149,124 @@ + +
+

Example Stage Direction

+ + + + + +
+
+

Configuration Options

+ + + + + {{ btn.caption }} + + + + + + + Default + + + Uppercase + + + Lowercase + + + + + + + This is a required field. + + + + + + + + + +
+