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 @@
+
+
+
+
+
+ New Crew Member
+
+
+
+
+ Edit
+ Delete
+
+
+
+
+
+
+
+
+
+ This is a required field.
+
+
+
+
+
+ This is a required field.
+
+
+
+
+
+
+
+
+
+ This is a required field.
+
+
+
+
+
+ This is a required field.
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+ Prop Types
+
+
+
+ New Prop Type
+
+
+
+
+ Edit
+ Delete
+
+
+
+
+
+
+ Props List
+
+
+
+ New Props Item
+
+
+
+ {{ PROP_TYPE_BY_ID(data.item.prop_type_id).name }}
+
+
+
+ Edit
+ Delete
+
+
+
+
+
+
+
+
+
+
+
+ This is a required field.
+
+
+
+
+
+ This is a required field.
+
+
+
+
+
+
+
+
+
+ This is a required field.
+
+
+
+
+
+ This is a required field.
+
+
+
+
+
+
+
+
+
+ This is a required field.
+
+
+
+
+
+ This is a required field.
+
+
+
+
+
+ This is a required field.
+
+
+
+
+
+
+
+
+
+ This is a required field.
+
+
+
+
+
+ This is a required field.
+
+
+
+
+
+ This is a required field.
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+ Scenery Types
+
+
+
+ New Scenery Type
+
+
+
+
+ Edit
+ Delete
+
+
+
+
+
+
+ Scenery List
+
+
+
+ New Scenery Item
+
+
+
+ {{ SCENERY_TYPE_BY_ID(data.item.scenery_type_id).name }}
+
+
+
+ Edit
+ Delete
+
+
+
+
+
+
+
+
+
+
+
+ This is a required field.
+
+
+
+
+
+ This is a required field.
+
+
+
+
+
+
+
+
+
+ This is a required field.
+
+
+
+
+
+ This is a required field.
+
+
+
+
+
+
+
+
+
+ This is a required field.
+
+
+
+
+
+ This is a required field.
+
+
+
+
+
+ This is a required field.
+
+
+
+
+
+
+
+
+
+ This is a required field.
+
+
+
+
+
+ This is a required field.
+
+
+
+
+
+ This is a required field.
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+ Scenery
+
+
+ Props
+
+
+
+
+
+
+
+ This is a required field.
+
+
+
+
+
+
+
+
+ There are no scenes configured for this show.
+
+
+
+
+
+
+
+
+
+
+
+
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: