diff --git a/client/src/router/index.js b/client/src/router/index.js index 0050a491..5bf1ac6d 100644 --- a/client/src/router/index.js +++ b/client/src/router/index.js @@ -39,6 +39,12 @@ const routes = [ component: () => import('../views/show/config/ConfigCast.vue'), meta: { requiresAuth: true, requiresShowAccess: true }, }, + { + name: 'show-config-stage', + path: 'stage', + component: () => import('../views/show/config/ConfigStage.vue'), + meta: { requiresAuth: true, requiresShowAccess: true }, + }, { name: 'show-config-characters', path: 'characters', diff --git a/client/src/store/modules/stage.js b/client/src/store/modules/stage.js new file mode 100644 index 00000000..9b18216f --- /dev/null +++ b/client/src/store/modules/stage.js @@ -0,0 +1,376 @@ +import Vue from 'vue'; +import log from 'loglevel'; + +import { makeURL } from '@/js/utils'; + +export default { + state: { + crewList: [], + sceneryTypes: [], + sceneryList: [], + propTypes: [], + propsList: [], + }, + mutations: { + SET_CREW_LIST(state, crewList) { + state.crewList = crewList; + }, + SET_SCENERY_TYPES(state, sceneryTypes) { + state.sceneryTypes = sceneryTypes; + }, + SET_SCENERY_LIST(state, sceneryList) { + state.sceneryList = sceneryList; + }, + SET_PROP_TYPES(state, propTypes) { + state.propTypes = propTypes; + }, + SET_PROPS_LIST(state, propsList) { + state.propsList = propsList; + }, + }, + actions: { + async GET_CREW_LIST(context) { + const response = await fetch(`${makeURL('/api/v1/show/stage/crew')}`); + if (response.ok) { + const crew = await response.json(); + context.commit('SET_CREW_LIST', crew.crew); + } else { + log.error('Unable to get crew list'); + } + }, + async ADD_CREW_MEMBER(context, crewMember) { + const response = await fetch(`${makeURL('/api/v1/show/stage/crew')}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(crewMember), + }); + if (response.ok) { + context.dispatch('GET_CREW_LIST'); + Vue.$toast.success('Added new crew member!'); + } else { + log.error('Unable to add new crew member'); + Vue.$toast.error('Unable to add new crew member'); + } + }, + async DELETE_CREW_MEMBER(context, crewId) { + const response = await fetch(`${makeURL('/api/v1/show/stage/crew')}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ id: crewId }), + }); + if (response.ok) { + context.dispatch('GET_CREW_LIST'); + Vue.$toast.success('Deleted crew member!'); + } else { + log.error('Unable to delete crew member'); + Vue.$toast.error('Unable to delete crew member'); + } + }, + async UPDATE_CREW_MEMBER(context, crewMember) { + const response = await fetch(`${makeURL('/api/v1/show/stage/crew')}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(crewMember), + }); + if (response.ok) { + context.dispatch('GET_CREW_LIST'); + Vue.$toast.success('Updated crew member!'); + } else { + log.error('Unable to edit crew member'); + Vue.$toast.error('Unable to edit crew member'); + } + }, + async GET_SCENERY_TYPES(context) { + const response = await fetch(`${makeURL('/api/v1/show/stage/scenery/types')}`); + if (response.ok) { + const scenery = await response.json(); + context.commit('SET_SCENERY_TYPES', scenery.scenery_types); + } else { + log.error('Unable to get scenery types list'); + } + }, + async ADD_SCENERY_TYPE(context, sceneryType) { + const response = await fetch(`${makeURL('/api/v1/show/stage/scenery/types')}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(sceneryType), + }); + if (response.ok) { + context.dispatch('GET_SCENERY_TYPES'); + Vue.$toast.success('Added new scenery type!'); + } else { + log.error('Unable to add new scenery type'); + Vue.$toast.error('Unable to add new scenery type'); + } + }, + async DELETE_SCENERY_TYPE(context, sceneryTypeId) { + const searchParams = new URLSearchParams({ + id: sceneryTypeId, + }); + const response = await fetch( + `${makeURL('/api/v1/show/stage/scenery/types')}?${searchParams}`, + { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + } + ); + if (response.ok) { + context.dispatch('GET_SCENERY_TYPES'); + context.dispatch('GET_SCENERY_LIST'); + Vue.$toast.success('Deleted scenery type!'); + } else { + log.error('Unable to delete scenery type'); + Vue.$toast.error('Unable to delete scenery type'); + } + }, + async UPDATE_SCENERY_TYPE(context, sceneryType) { + const response = await fetch(`${makeURL('/api/v1/show/stage/scenery/types')}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(sceneryType), + }); + if (response.ok) { + context.dispatch('GET_SCENERY_TYPES'); + Vue.$toast.success('Updated scenery type!'); + } else { + log.error('Unable to edit scenery type'); + Vue.$toast.error('Unable to edit scenery type'); + } + }, + async GET_SCENERY_LIST(context) { + const response = await fetch(`${makeURL('/api/v1/show/stage/scenery')}`); + if (response.ok) { + const scenery = await response.json(); + context.commit('SET_SCENERY_LIST', scenery.scenery); + } else { + log.error('Unable to get scenery list'); + } + }, + async ADD_SCENERY(context, sceneryMember) { + const response = await fetch(`${makeURL('/api/v1/show/stage/scenery')}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(sceneryMember), + }); + if (response.ok) { + context.dispatch('GET_SCENERY_LIST'); + Vue.$toast.success('Added new scenery member!'); + } else { + log.error('Unable to add new scenery member'); + Vue.$toast.error('Unable to add new scenery member'); + } + }, + async DELETE_SCENERY(context, sceneryId) { + const searchParams = new URLSearchParams({ + id: sceneryId, + }); + const response = await fetch(`${makeURL('/api/v1/show/stage/scenery')}?${searchParams}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + }); + if (response.ok) { + context.dispatch('GET_SCENERY_LIST'); + Vue.$toast.success('Deleted scenery member!'); + } else { + log.error('Unable to delete scenery member'); + Vue.$toast.error('Unable to delete scenery member'); + } + }, + async UPDATE_SCENERY(context, sceneryMember) { + const response = await fetch(`${makeURL('/api/v1/show/stage/scenery')}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(sceneryMember), + }); + if (response.ok) { + context.dispatch('GET_SCENERY_LIST'); + Vue.$toast.success('Updated scenery member!'); + } else { + log.error('Unable to edit scenery member'); + Vue.$toast.error('Unable to edit scenery member'); + } + }, + async GET_PROP_TYPES(context) { + const response = await fetch(`${makeURL('/api/v1/show/stage/props/types')}`); + if (response.ok) { + const props = await response.json(); + context.commit('SET_PROP_TYPES', props.prop_types); + } else { + log.error('Unable to get prop types list'); + } + }, + async ADD_PROP_TYPE(context, propType) { + const response = await fetch(`${makeURL('/api/v1/show/stage/props/types')}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(propType), + }); + if (response.ok) { + context.dispatch('GET_PROP_TYPES'); + Vue.$toast.success('Added new prop type!'); + } else { + log.error('Unable to add new prop type'); + Vue.$toast.error('Unable to add new prop type'); + } + }, + async DELETE_PROP_TYPE(context, propTypeId) { + const searchParams = new URLSearchParams({ + id: propTypeId, + }); + const response = await fetch(`${makeURL('/api/v1/show/stage/props/types')}?${searchParams}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + }); + if (response.ok) { + context.dispatch('GET_PROP_TYPES'); + context.dispatch('GET_PROPS_LIST'); + Vue.$toast.success('Deleted prop type!'); + } else { + log.error('Unable to delete prop type'); + Vue.$toast.error('Unable to delete prop type'); + } + }, + async UPDATE_PROP_TYPE(context, propType) { + const response = await fetch(`${makeURL('/api/v1/show/stage/props/types')}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(propType), + }); + if (response.ok) { + context.dispatch('GET_PROP_TYPES'); + Vue.$toast.success('Updated prop type!'); + } else { + log.error('Unable to edit prop type'); + Vue.$toast.error('Unable to edit prop type'); + } + }, + async GET_PROPS_LIST(context) { + const response = await fetch(`${makeURL('/api/v1/show/stage/props')}`); + if (response.ok) { + const props = await response.json(); + context.commit('SET_PROPS_LIST', props.props); + } else { + log.error('Unable to get props list'); + } + }, + async ADD_PROP(context, propsMember) { + const response = await fetch(`${makeURL('/api/v1/show/stage/props')}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(propsMember), + }); + if (response.ok) { + context.dispatch('GET_PROPS_LIST'); + Vue.$toast.success('Added new props member!'); + } else { + log.error('Unable to add new props member'); + Vue.$toast.error('Unable to add new props member'); + } + }, + async DELETE_PROP(context, propsId) { + const searchParams = new URLSearchParams({ + id: propsId, + }); + const response = await fetch(`${makeURL('/api/v1/show/stage/props')}?${searchParams}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + }); + if (response.ok) { + context.dispatch('GET_PROPS_LIST'); + Vue.$toast.success('Deleted props member!'); + } else { + log.error('Unable to delete props member'); + Vue.$toast.error('Unable to delete props member'); + } + }, + async UPDATE_PROP(context, propsMember) { + const response = await fetch(`${makeURL('/api/v1/show/stage/props')}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(propsMember), + }); + if (response.ok) { + context.dispatch('GET_PROPS_LIST'); + Vue.$toast.success('Updated props member!'); + } else { + log.error('Unable to edit props member'); + Vue.$toast.error('Unable to edit props member'); + } + }, + }, + getters: { + CREW_LIST(state) { + return state.crewList; + }, + SCENERY_TYPES(state) { + return state.sceneryTypes; + }, + SCENERY_TYPES_DICT(state) { + return Object.fromEntries( + state.sceneryTypes.map((sceneryType) => [sceneryType.id, sceneryType]) + ); + }, + SCENERY_TYPE_BY_ID: (state, getters) => (sceneryTypeId) => { + if (sceneryTypeId == null) { + return null; + } + const sceneryTypeStr = sceneryTypeId.toString(); + if (Object.keys(getters.SCENERY_TYPES_DICT).includes(sceneryTypeStr)) { + return getters.SCENERY_TYPES_DICT[sceneryTypeStr]; + } + return null; + }, + SCENERY_LIST(state) { + return state.sceneryList; + }, + PROP_TYPES(state) { + return state.propTypes; + }, + PROP_TYPES_DICT(state) { + return Object.fromEntries(state.propTypes.map((propType) => [propType.id, propType])); + }, + PROP_TYPE_BY_ID: (state, getters) => (propTypeId) => { + if (propTypeId == null) { + return null; + } + const propTypeStr = propTypeId.toString(); + if (Object.keys(getters.PROP_TYPES_DICT).includes(propTypeStr)) { + return getters.PROP_TYPES_DICT[propTypeStr]; + } + return null; + }, + PROPS_LIST(state) { + return state.propsList; + }, + }, +}; diff --git a/client/src/store/store.js b/client/src/store/store.js index 68263842..41d53200 100644 --- a/client/src/store/store.js +++ b/client/src/store/store.js @@ -11,6 +11,7 @@ import show from './modules/show'; import script from './modules/script'; import scriptConfig from './modules/scriptConfig'; import help from './modules/help'; +import stage from './modules/stage'; Vue.use(Vuex); @@ -198,6 +199,7 @@ export default new Vuex.Store({ websocket, system, show, + stage, script, scriptConfig, user, diff --git a/client/src/views/show/ShowConfigView.vue b/client/src/views/show/ShowConfigView.vue index f6449248..8e80bb2a 100644 --- a/client/src/views/show/ShowConfigView.vue +++ b/client/src/views/show/ShowConfigView.vue @@ -14,6 +14,15 @@ > Show + + Staging + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/src/vue_components/show/config/stage/CrewList.vue b/client/src/vue_components/show/config/stage/CrewList.vue new file mode 100644 index 00000000..d1c0b566 --- /dev/null +++ b/client/src/vue_components/show/config/stage/CrewList.vue @@ -0,0 +1,220 @@ + + + diff --git a/client/src/vue_components/show/config/stage/PropsList.vue b/client/src/vue_components/show/config/stage/PropsList.vue new file mode 100644 index 00000000..ef0b82bf --- /dev/null +++ b/client/src/vue_components/show/config/stage/PropsList.vue @@ -0,0 +1,489 @@ + + + diff --git a/client/src/vue_components/show/config/stage/SceneryList.vue b/client/src/vue_components/show/config/stage/SceneryList.vue new file mode 100644 index 00000000..1ca44dab --- /dev/null +++ b/client/src/vue_components/show/config/stage/SceneryList.vue @@ -0,0 +1,497 @@ + + + diff --git a/client/src/vue_components/show/config/stage/StageManager.vue b/client/src/vue_components/show/config/stage/StageManager.vue new file mode 100644 index 00000000..be138e43 --- /dev/null +++ b/client/src/vue_components/show/config/stage/StageManager.vue @@ -0,0 +1,212 @@ + + + + + diff --git a/server/alembic_config/versions/9849eb6d381a_add_prop_and_scenery_types.py b/server/alembic_config/versions/9849eb6d381a_add_prop_and_scenery_types.py new file mode 100644 index 00000000..22f8f34c --- /dev/null +++ b/server/alembic_config/versions/9849eb6d381a_add_prop_and_scenery_types.py @@ -0,0 +1,93 @@ +"""Add prop and scenery types + +Revision ID: 9849eb6d381a +Revises: fa27b233d26c +Create Date: 2026-01-14 01:01:31.586812 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + + +# revision identifiers, used by Alembic. +revision: str = "9849eb6d381a" +down_revision: Union[str, None] = "fa27b233d26c" +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( + "prop_type", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("show_id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["show_id"], ["shows.id"], name=op.f("fk_prop_type_show_id_shows") + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_prop_type")), + ) + op.create_table( + "scenery_type", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("show_id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["show_id"], ["shows.id"], name=op.f("fk_scenery_type_show_id_shows") + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_scenery_type")), + ) + with op.batch_alter_table("crew", schema=None) as batch_op: + batch_op.alter_column("first_name", existing_type=sa.String(), nullable=False) + + with op.batch_alter_table("props", schema=None) as batch_op: + batch_op.add_column(sa.Column("prop_type_id", sa.Integer(), nullable=False)) + batch_op.alter_column("name", existing_type=sa.String(), nullable=False) + batch_op.create_foreign_key( + batch_op.f("fk_props_prop_type_id_prop_type"), + "prop_type", + ["prop_type_id"], + ["id"], + ) + + with op.batch_alter_table("scenery", schema=None) as batch_op: + batch_op.add_column(sa.Column("scenery_type_id", sa.Integer(), nullable=False)) + batch_op.alter_column("name", existing_type=sa.String(), nullable=False) + batch_op.create_foreign_key( + batch_op.f("fk_scenery_scenery_type_id_scenery_type"), + "scenery_type", + ["scenery_type_id"], + ["id"], + ) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("scenery", schema=None) as batch_op: + batch_op.drop_constraint( + batch_op.f("fk_scenery_scenery_type_id_scenery_type"), type_="foreignkey" + ) + batch_op.alter_column("name", existing_type=sa.String(), nullable=True) + batch_op.drop_column("scenery_type_id") + + with op.batch_alter_table("props", schema=None) as batch_op: + batch_op.drop_constraint( + batch_op.f("fk_props_prop_type_id_prop_type"), type_="foreignkey" + ) + batch_op.alter_column("name", existing_type=sa.String(), nullable=True) + batch_op.drop_column("prop_type_id") + + with op.batch_alter_table("crew", schema=None) as batch_op: + batch_op.alter_column("first_name", existing_type=sa.String(), nullable=True) + + op.drop_table("scenery_type") + op.drop_table("prop_type") + # ### end Alembic commands ### diff --git a/server/alembic_config/versions/fa27b233d26c_add_crew_props_and_scenery_tables_and_.py b/server/alembic_config/versions/fa27b233d26c_add_crew_props_and_scenery_tables_and_.py new file mode 100644 index 00000000..920b08ba --- /dev/null +++ b/server/alembic_config/versions/fa27b233d26c_add_crew_props_and_scenery_tables_and_.py @@ -0,0 +1,105 @@ +"""Add crew, props and scenery tables and allocations + +Revision ID: fa27b233d26c +Revises: 01fb1d6c6b08 +Create Date: 2026-01-14 00:38:40.210710 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + + +# revision identifiers, used by Alembic. +revision: str = "fa27b233d26c" +down_revision: Union[str, None] = "01fb1d6c6b08" +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( + "crew", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("show_id", sa.Integer(), nullable=False), + sa.Column("first_name", sa.String(), nullable=True), + sa.Column("last_name", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["show_id"], ["shows.id"], name=op.f("fk_crew_show_id_shows") + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_crew")), + ) + op.create_table( + "props", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("show_id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(), nullable=True), + sa.Column("description", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["show_id"], ["shows.id"], name=op.f("fk_props_show_id_shows") + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_props")), + ) + op.create_table( + "scenery", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("show_id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(), nullable=True), + sa.Column("description", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["show_id"], ["shows.id"], name=op.f("fk_scenery_show_id_shows") + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_scenery")), + ) + op.create_table( + "props_allocation", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("props_id", sa.Integer(), nullable=False), + sa.Column("scene_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["props_id"], + ["props.id"], + name=op.f("fk_props_allocation_props_id_props"), + ondelete="CASCADE", + ), + sa.ForeignKeyConstraint( + ["scene_id"], + ["scene.id"], + name=op.f("fk_props_allocation_scene_id_scene"), + ondelete="CASCADE", + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_props_allocation")), + ) + op.create_table( + "scenery_allocation", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("scenery_id", sa.Integer(), nullable=False), + sa.Column("scene_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["scene_id"], + ["scene.id"], + name=op.f("fk_scenery_allocation_scene_id_scene"), + ondelete="CASCADE", + ), + sa.ForeignKeyConstraint( + ["scenery_id"], + ["scenery.id"], + name=op.f("fk_scenery_allocation_scenery_id_scenery"), + ondelete="CASCADE", + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_scenery_allocation")), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("scenery_allocation") + op.drop_table("props_allocation") + op.drop_table("scenery") + op.drop_table("props") + op.drop_table("crew") + # ### end Alembic commands ### diff --git a/server/controllers/api/show/stage/__init__.py b/server/controllers/api/show/stage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/controllers/api/show/stage/crew.py b/server/controllers/api/show/stage/crew.py new file mode 100644 index 00000000..361df252 --- /dev/null +++ b/server/controllers/api/show/stage/crew.py @@ -0,0 +1,156 @@ +from tornado import escape + +from models.show import Show +from models.stage import Crew +from rbac.role import Role +from schemas.schemas import CrewSchema +from utils.web.base_controller import BaseAPIController +from utils.web.route import ApiRoute, ApiVersion +from utils.web.web_decorators import no_live_session, requires_show + + +@ApiRoute("show/stage/crew", ApiVersion.V1) +class CrewController(BaseAPIController): + @requires_show + def get(self): + current_show = self.get_current_show() + show_id = current_show["id"] + crew_schema = CrewSchema() + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + crew = [crew_schema.dump(c) for c in show.crew_list] + self.set_status(200) + self.finish({"crew": crew}) + else: + self.set_status(404) + self.finish({"message": "404 show not found"}) + + @requires_show + @no_live_session + async def post(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + self.requires_role(show, Role.WRITE) + data = escape.json_decode(self.request.body) + + first_name = data.get("firstName", None) + if not first_name: + self.set_status(400) + await self.finish({"message": "First name missing"}) + return + + last_name = data.get("lastName", None) + if not last_name: + self.set_status(400) + await self.finish({"message": "Last name missing"}) + return + + new_crew = Crew( + show_id=show.id, first_name=first_name, last_name=last_name + ) + session.add(new_crew) + session.commit() + + self.set_status(200) + await self.finish( + {"id": new_crew.id, "message": "Successfully added crew member"} + ) + + await self.application.ws_send_to_all( + "GET_CREW_LIST", "GET_CREW_LIST", {} + ) + else: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + + @requires_show + @no_live_session + async def patch(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + self.requires_role(show, Role.WRITE) + data = escape.json_decode(self.request.body) + + crew_id = data.get("id", None) + if not crew_id: + self.set_status(400) + await self.finish({"message": "ID missing"}) + return + + entry: Crew = session.get(Crew, crew_id) + if entry: + first_name = data.get("firstName", None) + if not first_name: + self.set_status(400) + await self.finish({"message": "First name missing"}) + return + entry.first_name = first_name + + last_name = data.get("lastName", None) + if not last_name: + self.set_status(400) + await self.finish({"message": "Last name missing"}) + return + entry.last_name = last_name + + session.commit() + + self.set_status(200) + await self.finish({"message": "Successfully updated crew member"}) + + await self.application.ws_send_to_all( + "GET_CREW_LIST", "GET_CREW_LIST", {} + ) + else: + self.set_status(404) + await self.finish({"message": "404 cast member not found"}) + return + else: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + + @requires_show + @no_live_session + async def delete(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + self.requires_role(show, Role.WRITE) + data = escape.json_decode(self.request.body) + + crew_id = data.get("id", None) + if not crew_id: + self.set_status(400) + await self.finish({"message": "ID missing"}) + return + + entry = session.get(Crew, crew_id) + if entry: + session.delete(entry) + session.commit() + + self.set_status(200) + await self.finish({"message": "Successfully deleted crew member"}) + + await self.application.ws_send_to_all( + "GET_CREW_LIST", "GET_CREW_LIST", {} + ) + else: + self.set_status(404) + await self.finish({"message": "404 crew member not found"}) + else: + self.set_status(404) + await self.finish({"message": "404 show not found"}) diff --git a/server/controllers/api/show/stage/props.py b/server/controllers/api/show/stage/props.py new file mode 100644 index 00000000..1a579d39 --- /dev/null +++ b/server/controllers/api/show/stage/props.py @@ -0,0 +1,334 @@ +from tornado import escape + +from models.show import Show +from models.stage import Props, PropType +from rbac.role import Role +from schemas.schemas import PropsSchema, PropTypeSchema +from utils.web.base_controller import BaseAPIController +from utils.web.route import ApiRoute, ApiVersion +from utils.web.web_decorators import no_live_session, requires_show + + +@ApiRoute("show/stage/props/types", ApiVersion.V1) +class PropsTypesController(BaseAPIController): + @requires_show + def get(self): + current_show = self.get_current_show() + show_id = current_show["id"] + prop_type_schema = PropTypeSchema() + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + prop_types = [prop_type_schema.dump(c) for c in show.prop_types] + self.set_status(200) + self.finish({"prop_types": prop_types}) + else: + self.set_status(404) + self.finish({"message": "404 show not found"}) + + @requires_show + @no_live_session + async def post(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if not show: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + return + self.requires_role(show, Role.WRITE) + data = escape.json_decode(self.request.body) + + name = data.get("name", None) + if not name: + self.set_status(400) + await self.finish({"message": "Name missing"}) + return + + description = data.get("description", "") + + new_prop_type = PropType( + show_id=show.id, name=name, description=description + ) + session.add(new_prop_type) + session.commit() + + self.set_status(200) + await self.finish( + {"id": new_prop_type.id, "message": "Successfully added prop type"} + ) + + await self.application.ws_send_to_all("NOOP", "GET_PROP_TYPES", {}) + + @requires_show + @no_live_session + async def patch(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if not show: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + return + self.requires_role(show, Role.WRITE) + data = escape.json_decode(self.request.body) + + prop_type = data.get("id", None) + if not prop_type: + self.set_status(400) + await self.finish({"message": "ID missing"}) + return + + entry: PropType = session.get(PropType, prop_type) + if not entry: + self.set_status(404) + await self.finish({"message": "404 prop type not found"}) + return + + name = data.get("name", None) + description = data.get("description", "") + if not name: + self.set_status(400) + await self.finish({"message": "Name missing"}) + return + + entry.name = name + entry.description = description + session.commit() + + self.set_status(200) + await self.finish({"message": "Successfully updated prop type"}) + + await self.application.ws_send_to_all("NOOP", "GET_PROP_TYPES", {}) + + @requires_show + @no_live_session + async def delete(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if not show: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + return + self.requires_role(show, Role.WRITE) + + prop_type_id_str = self.get_argument("id", None) + if not prop_type_id_str: + self.set_status(400) + await self.finish({"message": "ID missing"}) + return + + try: + prop_type_id = int(prop_type_id_str) + except ValueError: + self.set_status(400) + await self.finish({"message": "Invalid ID"}) + return + + entry = session.get(PropType, prop_type_id) + if not entry: + self.set_status(404) + await self.finish({"message": "404 prop type not found"}) + return + + session.delete(entry) + session.commit() + + self.set_status(200) + await self.finish({"message": "Successfully deleted prop type"}) + + await self.application.ws_send_to_all("NOOP", "GET_PROP_TYPES", {}) + await self.application.ws_send_to_all("NOOP", "GET_PROPS_LIST", {}) + + +@ApiRoute("show/stage/props", ApiVersion.V1) +class PropsController(BaseAPIController): + @requires_show + def get(self): + current_show = self.get_current_show() + show_id = current_show["id"] + props_schema = PropsSchema() + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + props = [props_schema.dump(c) for c in show.props_list] + self.set_status(200) + self.finish({"props": props}) + else: + self.set_status(404) + self.finish({"message": "404 show not found"}) + + @requires_show + @no_live_session + async def post(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + self.requires_role(show, Role.WRITE) + data = escape.json_decode(self.request.body) + + name = data.get("name", None) + if not name: + self.set_status(400) + await self.finish({"message": "Name missing"}) + return + + prop_type_id = data.get("prop_type_id", None) + if not prop_type_id: + self.set_status(400) + await self.finish({"message": "Prop type ID missing"}) + return + try: + prop_type_id = int(prop_type_id) + except ValueError: + self.set_status(400) + await self.finish({"message": "Invalid prop type ID"}) + return + prop_type: PropType = session.get(PropType, prop_type_id) + if not prop_type: + self.set_status(404) + await self.finish({"message": "Prop type not found"}) + return + if prop_type.show_id != show.id: + self.set_status(400) + await self.finish({"message": "Invalid prop type for show"}) + return + + description = data.get("description", "") + + new_props = Props( + show_id=show.id, + name=name, + description=description, + prop_type_id=prop_type.id, + ) + session.add(new_props) + session.commit() + + self.set_status(200) + await self.finish( + {"id": new_props.id, "message": "Successfully added props"} + ) + + await self.application.ws_send_to_all("NOOP", "GET_PROPS_LIST", {}) + else: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + + @requires_show + @no_live_session + async def patch(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + self.requires_role(show, Role.WRITE) + data = escape.json_decode(self.request.body) + + props = data.get("id", None) + if not props: + self.set_status(400) + await self.finish({"message": "ID missing"}) + return + + entry: Props = session.get(Props, props) + if entry: + name = data.get("name", None) + if not name: + self.set_status(400) + await self.finish({"message": "Name missing"}) + return + entry.name = name + + prop_type_id = data.get("prop_type_id", None) + if not prop_type_id: + self.set_status(400) + await self.finish({"message": "Prop type ID missing"}) + return + try: + prop_type_id = int(prop_type_id) + except ValueError: + self.set_status(400) + await self.finish({"message": "Invalid prop type ID"}) + return + prop_type: PropType = session.get(PropType, prop_type_id) + if not prop_type: + self.set_status(404) + await self.finish({"message": "Prop type not found"}) + return + if prop_type.show_id != show.id: + self.set_status(400) + await self.finish({"message": "Invalid prop type for show"}) + return + entry.prop_type_id = prop_type.id + + description = data.get("description", "") + entry.description = description + + session.commit() + + self.set_status(200) + await self.finish({"message": "Successfully updated props"}) + + await self.application.ws_send_to_all("NOOP", "GET_PROPS_LIST", {}) + else: + self.set_status(404) + await self.finish({"message": "404 prop not found"}) + return + else: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + + @requires_show + @no_live_session + async def delete(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + self.requires_role(show, Role.WRITE) + + props_id_str = self.get_argument("id", None) + if not props_id_str: + self.set_status(400) + await self.finish({"message": "ID missing"}) + return + + try: + props_id = int(props_id_str) + except ValueError: + self.set_status(400) + await self.finish({"message": "Invalid ID"}) + return + + entry = session.get(Props, props_id) + if entry: + session.delete(entry) + session.commit() + + self.set_status(200) + await self.finish({"message": "Successfully deleted props"}) + + await self.application.ws_send_to_all("NOOP", "GET_PROPS_LIST", {}) + else: + self.set_status(404) + await self.finish({"message": "404 props not found"}) + else: + self.set_status(404) + await self.finish({"message": "404 show not found"}) diff --git a/server/controllers/api/show/stage/scenery.py b/server/controllers/api/show/stage/scenery.py new file mode 100644 index 00000000..343d2f30 --- /dev/null +++ b/server/controllers/api/show/stage/scenery.py @@ -0,0 +1,345 @@ +from tornado import escape + +from models.show import Show +from models.stage import Scenery, SceneryType +from rbac.role import Role +from schemas.schemas import ScenerySchema, SceneryTypeSchema +from utils.web.base_controller import BaseAPIController +from utils.web.route import ApiRoute, ApiVersion +from utils.web.web_decorators import no_live_session, requires_show + + +@ApiRoute("show/stage/scenery/types", ApiVersion.V1) +class PropsTypesController(BaseAPIController): + @requires_show + def get(self): + current_show = self.get_current_show() + show_id = current_show["id"] + scenery_type_schema = SceneryTypeSchema() + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + scenery_types = [ + scenery_type_schema.dump(c) for c in show.scenery_types + ] + self.set_status(200) + self.finish({"scenery_types": scenery_types}) + else: + self.set_status(404) + self.finish({"message": "404 show not found"}) + + @requires_show + @no_live_session + async def post(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if not show: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + return + self.requires_role(show, Role.WRITE) + data = escape.json_decode(self.request.body) + + name = data.get("name", None) + if not name: + self.set_status(400) + await self.finish({"message": "Name missing"}) + return + + description = data.get("description", "") + + new_scenery_type = SceneryType( + show_id=show.id, name=name, description=description + ) + session.add(new_scenery_type) + session.commit() + + self.set_status(200) + await self.finish( + { + "id": new_scenery_type.id, + "message": "Successfully added scenery type", + } + ) + + await self.application.ws_send_to_all("NOOP", "GET_SCENERY_TYPES", {}) + + @requires_show + @no_live_session + async def patch(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if not show: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + return + self.requires_role(show, Role.WRITE) + data = escape.json_decode(self.request.body) + + prop_type = data.get("id", None) + if not prop_type: + self.set_status(400) + await self.finish({"message": "ID missing"}) + return + + entry: SceneryType = session.get(SceneryType, prop_type) + if not entry: + self.set_status(404) + await self.finish({"message": "404 scenery type not found"}) + return + + name = data.get("name", None) + description = data.get("description", "") + if not name: + self.set_status(400) + await self.finish({"message": "Name missing"}) + return + + entry.name = name + entry.description = description + session.commit() + + self.set_status(200) + await self.finish({"message": "Successfully updated scenery type"}) + + await self.application.ws_send_to_all("NOOP", "GET_SCENERY_TYPES", {}) + + @requires_show + @no_live_session + async def delete(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if not show: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + return + self.requires_role(show, Role.WRITE) + + scenery_type_id_str = self.get_argument("id", None) + if not scenery_type_id_str: + self.set_status(400) + await self.finish({"message": "ID missing"}) + return + + try: + scenery_type_id = int(scenery_type_id_str) + except ValueError: + self.set_status(400) + await self.finish({"message": "Invalid ID"}) + return + + entry = session.get(SceneryType, scenery_type_id) + if not entry: + self.set_status(404) + await self.finish({"message": "404 scenery type not found"}) + return + + session.delete(entry) + session.commit() + + self.set_status(200) + await self.finish({"message": "Successfully deleted scenery type"}) + + await self.application.ws_send_to_all("NOOP", "GET_SCENERY_TYPES", {}) + await self.application.ws_send_to_all("NOOP", "GET_SCENERY_LIST", {}) + + +@ApiRoute("show/stage/scenery", ApiVersion.V1) +class SceneryController(BaseAPIController): + @requires_show + def get(self): + current_show = self.get_current_show() + show_id = current_show["id"] + scenery_schema = ScenerySchema() + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + scenery = [scenery_schema.dump(c) for c in show.scenery_list] + self.set_status(200) + self.finish({"scenery": scenery}) + else: + self.set_status(404) + self.finish({"message": "404 show not found"}) + + @requires_show + @no_live_session + async def post(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + self.requires_role(show, Role.WRITE) + data = escape.json_decode(self.request.body) + + name = data.get("name", None) + if not name: + self.set_status(400) + await self.finish({"message": "Name missing"}) + return + + scenery_type_id = data.get("scenery_type_id", None) + if not scenery_type_id: + self.set_status(400) + await self.finish({"message": "Scenery type ID missing"}) + return + try: + scenery_type_id = int(scenery_type_id) + except ValueError: + self.set_status(400) + await self.finish({"message": "Scenery prop type ID"}) + return + scenery_type: SceneryType = session.get(SceneryType, scenery_type_id) + if not scenery_type: + self.set_status(404) + await self.finish({"message": "Scenery type not found"}) + return + if scenery_type.show_id != show.id: + self.set_status(400) + await self.finish({"message": "Invalid scenery type for show"}) + return + + description = data.get("description", "") + + new_scenery = Scenery( + show_id=show.id, + name=name, + description=description, + scenery_type_id=scenery_type.id, + ) + session.add(new_scenery) + session.commit() + + self.set_status(200) + await self.finish( + {"id": new_scenery.id, "message": "Successfully added scenery"} + ) + + await self.application.ws_send_to_all("NOOP", "GET_SCENERY_LIST", {}) + else: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + + @requires_show + @no_live_session + async def patch(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + self.requires_role(show, Role.WRITE) + data = escape.json_decode(self.request.body) + + scenery = data.get("id", None) + if not scenery: + self.set_status(400) + await self.finish({"message": "ID missing"}) + return + + entry: Scenery = session.get(Scenery, scenery) + if entry: + name = data.get("name", None) + if not name: + self.set_status(400) + await self.finish({"message": "Name missing"}) + return + entry.name = name + + scenery_type_id = data.get("scenery_type_id", None) + if not scenery_type_id: + self.set_status(400) + await self.finish({"message": "Scenery type ID missing"}) + return + try: + scenery_type_id = int(scenery_type_id) + except ValueError: + self.set_status(400) + await self.finish({"message": "Scenery prop type ID"}) + return + scenery_type: SceneryType = session.get( + SceneryType, scenery_type_id + ) + if not scenery_type: + self.set_status(404) + await self.finish({"message": "Scenery type not found"}) + return + if scenery_type.show_id != show.id: + self.set_status(400) + await self.finish({"message": "Invalid scenery type for show"}) + return + entry.scenery_type_id = scenery_type.id + + description = data.get("description", "") + entry.description = description + + session.commit() + + self.set_status(200) + await self.finish({"message": "Successfully updated scenery"}) + + await self.application.ws_send_to_all( + "NOOP", "GET_SCENERY_LIST", {} + ) + else: + self.set_status(404) + await self.finish({"message": "404 cast member not found"}) + return + else: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + + @requires_show + @no_live_session + async def delete(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + self.requires_role(show, Role.WRITE) + + scenery_id_str = self.get_argument("id", None) + if not scenery_id_str: + self.set_status(400) + await self.finish({"message": "ID missing"}) + return + + try: + scenery_id = int(scenery_id_str) + except ValueError: + self.set_status(400) + await self.finish({"message": "Invalid ID"}) + return + + entry = session.get(Scenery, scenery_id) + if entry: + session.delete(entry) + session.commit() + + self.set_status(200) + await self.finish({"message": "Successfully deleted scenery"}) + + await self.application.ws_send_to_all( + "NOOP", "GET_SCENERY_LIST", {} + ) + else: + self.set_status(404) + await self.finish({"message": "404 scenery not found"}) + else: + self.set_status(404) + await self.finish({"message": "404 show not found"}) diff --git a/server/models/show.py b/server/models/show.py index 5c115844..fdd1b486 100644 --- a/server/models/show.py +++ b/server/models/show.py @@ -13,6 +13,15 @@ from models.mics import MicrophoneAllocation from models.script import ScriptLine from models.session import ShowSession + from models.stage import ( + Crew, + Props, + PropsAllocation, + PropType, + Scenery, + SceneryAllocation, + SceneryType, + ) class ShowScriptType(enum.IntEnum): @@ -66,6 +75,26 @@ class Show(db.Model): ) cast_list: Mapped[List["Cast"]] = relationship(cascade="all, delete-orphan") + crew_list: Mapped[List["Crew"]] = relationship( + back_populates="show", + cascade="all, delete-orphan", + ) + scenery_types: Mapped[List["SceneryType"]] = relationship( + back_populates="show", + cascade="all, delete-orphan", + ) + scenery_list: Mapped[List["Scenery"]] = relationship( + back_populates="show", + cascade="all, delete-orphan", + ) + prop_types: Mapped[List["PropType"]] = relationship( + back_populates="show", + cascade="all, delete-orphan", + ) + props_list: Mapped[List["Props"]] = relationship( + back_populates="show", + cascade="all, delete-orphan", + ) character_list: Mapped[List["Character"]] = relationship( cascade="all, delete-orphan" ) @@ -194,3 +223,11 @@ class Scene(db.Model): mic_allocations: Mapped[List["MicrophoneAllocation"]] = relationship( cascade="all, delete-orphan", back_populates="scene" ) + scenery_allocations: Mapped[List["SceneryAllocation"]] = relationship( + back_populates="scene", + cascade="all, delete-orphan", + ) + props_allocations: Mapped[List["PropsAllocation"]] = relationship( + back_populates="scene", + cascade="all, delete-orphan", + ) diff --git a/server/models/stage.py b/server/models/stage.py new file mode 100644 index 00000000..26c216b1 --- /dev/null +++ b/server/models/stage.py @@ -0,0 +1,118 @@ +from typing import List + +from sqlalchemy import ForeignKey +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from models.models import db +from models.show import Scene, Show + + +class Crew(db.Model): + __tablename__ = "crew" + + id: Mapped[int] = mapped_column(primary_key=True) + show_id: Mapped[int] = mapped_column(ForeignKey("shows.id")) + first_name: Mapped[str] = mapped_column() + last_name: Mapped[str | None] = mapped_column() + + show: Mapped["Show"] = relationship(back_populates="crew_list") + + +class SceneryAllocation(db.Model): + __tablename__ = "scenery_allocation" + + id: Mapped[int] = mapped_column(primary_key=True) + scenery_id: Mapped[int] = mapped_column( + ForeignKey("scenery.id", ondelete="CASCADE") + ) + scene_id: Mapped[int] = mapped_column(ForeignKey("scene.id", ondelete="CASCADE")) + + scenery: Mapped["Scenery"] = relationship( + back_populates="scene_allocations", + foreign_keys=[scenery_id], + ) + scene: Mapped["Scene"] = relationship( + back_populates="scenery_allocations", + foreign_keys=[scene_id], + ) + + +class PropsAllocation(db.Model): + __tablename__ = "props_allocation" + + id: Mapped[int] = mapped_column(primary_key=True) + props_id: Mapped[int] = mapped_column(ForeignKey("props.id", ondelete="CASCADE")) + scene_id: Mapped[int] = mapped_column(ForeignKey("scene.id", ondelete="CASCADE")) + + prop: Mapped["Props"] = relationship( + back_populates="scene_allocations", + foreign_keys=[props_id], + ) + scene: Mapped["Scene"] = relationship( + back_populates="props_allocations", + foreign_keys=[scene_id], + ) + + +class SceneryType(db.Model): + __tablename__ = "scenery_type" + + id: Mapped[int] = mapped_column(primary_key=True) + show_id: Mapped[int] = mapped_column(ForeignKey("shows.id")) + name: Mapped[str] = mapped_column() + description: Mapped[str | None] = mapped_column() + + show: Mapped["Show"] = relationship(back_populates="scenery_types") + scenery_items: Mapped[list["Scenery"]] = relationship( + back_populates="scenery_type", + cascade="all, delete-orphan", + ) + + +class Scenery(db.Model): + __tablename__ = "scenery" + + id: Mapped[int] = mapped_column(primary_key=True) + show_id: Mapped[int] = mapped_column(ForeignKey("shows.id")) + scenery_type_id: Mapped[int] = mapped_column(ForeignKey("scenery_type.id")) + name: Mapped[str] = mapped_column() + description: Mapped[str | None] = mapped_column() + + show: Mapped["Show"] = relationship(back_populates="scenery_list") + scenery_type: Mapped["SceneryType"] = relationship(back_populates="scenery_items") + scene_allocations: Mapped[List["SceneryAllocation"]] = relationship( + back_populates="scenery", + cascade="all, delete-orphan", + ) + + +class PropType(db.Model): + __tablename__ = "prop_type" + + id: Mapped[int] = mapped_column(primary_key=True) + show_id: Mapped[int] = mapped_column(ForeignKey("shows.id")) + name: Mapped[str] = mapped_column() + description: Mapped[str | None] = mapped_column() + + show: Mapped["Show"] = relationship(back_populates="prop_types") + prop_items: Mapped[List["Props"]] = relationship( + back_populates="prop_type", + cascade="all, delete-orphan", + ) + + +class Props(db.Model): + __tablename__ = "props" + + id: Mapped[int] = mapped_column(primary_key=True) + show_id: Mapped[int] = mapped_column(ForeignKey("shows.id")) + prop_type_id: Mapped[int] = mapped_column(ForeignKey("prop_type.id")) + name: Mapped[str] = mapped_column() + description: Mapped[str | None] = mapped_column() + + show: Mapped["Show"] = relationship(back_populates="props_list") + prop_type: Mapped["PropType"] = relationship(back_populates="prop_items") + scene_allocations: Mapped[list["PropsAllocation"]] = relationship( + back_populates="prop", + cascade="all, delete-orphan", + ) diff --git a/server/schemas/schemas.py b/server/schemas/schemas.py index 84b4a383..7f1aa4ea 100644 --- a/server/schemas/schemas.py +++ b/server/schemas/schemas.py @@ -14,6 +14,7 @@ ) from models.session import Interval, Session, SessionTag, ShowSession from models.show import Act, Cast, Character, CharacterGroup, Scene, Show +from models.stage import Crew, Props, PropType, Scenery, SceneryType from models.user import User, UserSettings from registry.schema import get_registry @@ -72,6 +73,46 @@ class Meta: ) +@schema +class CrewSchema(SQLAlchemyAutoSchema): + class Meta: + model = Crew + load_instance = True + include_fk = True + + +@schema +class SceneryTypeSchema(SQLAlchemyAutoSchema): + class Meta: + model = SceneryType + load_instance = True + include_fk = True + + +@schema +class ScenerySchema(SQLAlchemyAutoSchema): + class Meta: + model = Scenery + load_instance = True + include_fk = True + + +@schema +class PropsSchema(SQLAlchemyAutoSchema): + class Meta: + model = Props + load_instance = True + include_fk = True + + +@schema +class PropTypeSchema(SQLAlchemyAutoSchema): + class Meta: + model = PropType + load_instance = True + include_fk = True + + @schema class CharacterSchema(SQLAlchemyAutoSchema): class Meta: