zou 0.20.38__py3-none-any.whl → 0.20.39__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. zou/__init__.py +1 -1
  2. zou/app/api.py +32 -41
  3. zou/app/blueprints/crud/__init__.py +6 -0
  4. zou/app/blueprints/crud/budget.py +21 -0
  5. zou/app/blueprints/crud/budget_entry.py +15 -0
  6. zou/app/blueprints/crud/plugin.py +13 -0
  7. zou/app/blueprints/crud/salary_scale.py +73 -0
  8. zou/app/blueprints/playlists/resources.py +15 -0
  9. zou/app/blueprints/projects/__init__.py +17 -0
  10. zou/app/blueprints/projects/resources.py +402 -7
  11. zou/app/blueprints/shots/resources.py +1 -0
  12. zou/app/mixin.py +12 -1
  13. zou/app/models/budget.py +39 -0
  14. zou/app/models/budget_entry.py +65 -0
  15. zou/app/models/person.py +17 -1
  16. zou/app/models/plugin.py +21 -0
  17. zou/app/models/salary_scale.py +28 -0
  18. zou/app/services/budget_service.py +195 -0
  19. zou/app/services/comments_service.py +1 -1
  20. zou/app/services/exception.py +8 -0
  21. zou/app/services/plugin_service.py +195 -0
  22. zou/app/services/user_service.py +1 -3
  23. zou/app/utils/fields.py +10 -0
  24. zou/cli.py +109 -1
  25. zou/event_stream.py +23 -5
  26. zou/migrations/versions/2762a797f1f9_add_people_salary_information.py +52 -0
  27. zou/migrations/versions/45f739ef962a_add_people_salary_scale_table.py +70 -0
  28. zou/migrations/versions/4aab1f84ad72_introduce_plugin_table.py +68 -0
  29. zou/migrations/versions/7a16258f2fab_add_currency_field_to_budgets.py +33 -0
  30. zou/migrations/versions/83e2f33a9b14_add_project_bugdet_table.py +57 -0
  31. zou/migrations/versions/8ab98c178903_add_budget_entry_table.py +123 -0
  32. zou/migrations/versions/d25118cddcaa_modify_salary_scale_model.py +133 -0
  33. zou/plugin_template/__init__.py +39 -0
  34. zou/plugin_template/routes.py +6 -0
  35. {zou-0.20.38.dist-info → zou-0.20.39.dist-info}/METADATA +6 -3
  36. {zou-0.20.38.dist-info → zou-0.20.39.dist-info}/RECORD +40 -21
  37. {zou-0.20.38.dist-info → zou-0.20.39.dist-info}/WHEEL +1 -1
  38. {zou-0.20.38.dist-info → zou-0.20.39.dist-info}/entry_points.txt +0 -0
  39. {zou-0.20.38.dist-info → zou-0.20.39.dist-info}/licenses/LICENSE +0 -0
  40. {zou-0.20.38.dist-info → zou-0.20.39.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,195 @@
1
+ from zou.app.models.budget import Budget
2
+ from zou.app.models.budget_entry import BudgetEntry
3
+
4
+ from zou.app.services.exception import (
5
+ BudgetNotFoundException,
6
+ BudgetEntryNotFoundException,
7
+ )
8
+
9
+ from zou.app.utils import events
10
+ from zou.app.utils import fields
11
+
12
+ from sqlalchemy.exc import StatementError
13
+
14
+
15
+ def get_budget_raw(budget_id):
16
+ """
17
+ Return budget corresponding to given budget ID.
18
+ """
19
+ if budget_id is None:
20
+ raise BudgetNotFoundException()
21
+
22
+ try:
23
+ budget = Budget.get(budget_id)
24
+ except StatementError:
25
+ raise BudgetNotFoundException()
26
+
27
+ if budget is None:
28
+ raise BudgetNotFoundException()
29
+ return budget
30
+
31
+
32
+ def get_budget(budget_id):
33
+ """
34
+ Return budget corresponding to given budget ID as a dictionary.
35
+ """
36
+ return get_budget_raw(budget_id).serialize(relations=True)
37
+
38
+
39
+ def get_budgets(project_id):
40
+ """
41
+ Return all budgets for given project ID.
42
+ """
43
+ budgets = Budget.get_all_by(project_id=project_id)
44
+ return fields.present_models(budgets)
45
+
46
+
47
+ def create_budget(project_id, name, currency=None):
48
+ """
49
+ Create a new budget for given project ID.
50
+ """
51
+ last_budget = (
52
+ Budget.query.filter_by(project_id=project_id)
53
+ .order_by(Budget.revision.desc())
54
+ .first()
55
+ )
56
+ last_revision = 1
57
+ if last_budget is not None:
58
+ last_revision = last_budget.revision + 1
59
+ budget = Budget(
60
+ project_id=project_id,
61
+ name=name,
62
+ currency=currency,
63
+ revision=last_revision,
64
+ )
65
+ budget.save()
66
+ events.emit(
67
+ "budget:create",
68
+ {"budget_id": str(budget.id)},
69
+ project_id=project_id,
70
+ )
71
+ return budget.serialize()
72
+
73
+
74
+ def update_budget(budget_id, name=None, currency=None):
75
+ """
76
+ Update budget corresponding to given budget ID.
77
+ """
78
+ budget = get_budget_raw(budget_id)
79
+ if name is not None:
80
+ budget.name = name
81
+ if currency is not None:
82
+ budget.currency = currency
83
+ budget.save()
84
+ events.emit(
85
+ "budget:update",
86
+ {"budget_id": str(budget.id)},
87
+ project_id=str(budget.project_id),
88
+ )
89
+ return budget.serialize()
90
+
91
+
92
+ def delete_budget(budget_id):
93
+ """
94
+ Delete budget corresponding to given budget ID.
95
+ """
96
+ budget = get_budget_raw(budget_id)
97
+ budget_entries = BudgetEntry.delete_all_by(budget_id=budget_id)
98
+ budget.delete()
99
+ events.emit(
100
+ "budget:delete",
101
+ {"budget_id": budget_id},
102
+ project_id=str(budget.project_id),
103
+ )
104
+ return budget.serialize()
105
+
106
+
107
+ def get_budget_entries(budget_id):
108
+ """
109
+ Return all budget entries for given budget ID.
110
+ """
111
+ budget_entries = BudgetEntry.get_all_by(budget_id=budget_id)
112
+ return fields.present_models(budget_entries)
113
+
114
+
115
+ def get_budget_entry_raw(budget_entry_id):
116
+ """
117
+ Return budget entry corresponding to given budget entry ID.
118
+ """
119
+ try:
120
+ budget_entry = BudgetEntry.get(budget_entry_id)
121
+ except StatementError:
122
+ raise BudgetEntryNotFoundException()
123
+
124
+ if budget_entry is None:
125
+ raise BudgetEntryNotFoundException()
126
+ return budget_entry
127
+
128
+
129
+ def get_budget_entry(budget_entry_id):
130
+ """
131
+ Return budget entry corresponding to given budget entry ID as a dictionary.
132
+ """
133
+ return get_budget_entry_raw(budget_entry_id).serialize()
134
+
135
+
136
+ def create_budget_entry(
137
+ budget_id,
138
+ department_id,
139
+ start_date,
140
+ months_duration,
141
+ daily_salary,
142
+ position,
143
+ seniority,
144
+ person_id=None,
145
+ ):
146
+ """
147
+ Create a new budget entry for given budget ID.
148
+ """
149
+ budget = get_budget_raw(budget_id)
150
+ budget_entry = BudgetEntry.create(
151
+ budget_id=budget_id,
152
+ department_id=department_id,
153
+ person_id=person_id,
154
+ start_date=start_date,
155
+ months_duration=months_duration,
156
+ daily_salary=daily_salary,
157
+ position=position,
158
+ seniority=seniority,
159
+ )
160
+ events.emit(
161
+ "budget-entry:create",
162
+ {"budget_id": str(budget_id), "budget_entry_id": str(budget_entry.id)},
163
+ project_id=str(budget.project_id),
164
+ )
165
+ return budget_entry.serialize()
166
+
167
+
168
+ def update_budget_entry(budget_entry_id, data):
169
+ """
170
+ Update budget entry corresponding to given budget entry ID.
171
+ """
172
+ budget_entry = get_budget_entry_raw(budget_entry_id)
173
+ budget = get_budget_raw(str(budget_entry.budget_id))
174
+ budget_entry.update(data)
175
+ events.emit(
176
+ "budget-entry:update",
177
+ {"budget_id": str(budget.id), "budget_entry_id": str(budget_entry.id)},
178
+ project_id=str(budget.project_id),
179
+ )
180
+ return budget_entry.serialize()
181
+
182
+
183
+ def delete_budget_entry(budget_entry_id):
184
+ """
185
+ Delete budget entry corresponding to given budget entry ID.
186
+ """
187
+ budget_entry = get_budget_entry_raw(budget_entry_id)
188
+ budget = get_budget_raw(str(budget_entry.budget_id))
189
+ budget_entry.delete()
190
+ events.emit(
191
+ "budget-entry:delete",
192
+ {"budget_id": str(budget.id), "budget_entry_id": budget_entry_id},
193
+ project_id=str(budget.project_id),
194
+ )
195
+ return budget_entry.serialize()
@@ -333,7 +333,7 @@ def new_comment(
333
333
  {
334
334
  "comment_id": comment["id"],
335
335
  "task_id": task_id,
336
- "task_status_id": task_status_id
336
+ "task_status_id": task_status_id,
337
337
  },
338
338
  project_id=task["project_id"],
339
339
  )
@@ -149,6 +149,14 @@ class MetadataDescriptorNotFoundException(NotFound):
149
149
  pass
150
150
 
151
151
 
152
+ class BudgetNotFoundException(NotFound):
153
+ pass
154
+
155
+
156
+ class BudgetEntryNotFoundException(NotFound):
157
+ pass
158
+
159
+
152
160
  class MalformedFileTreeException(Exception):
153
161
  pass
154
162
 
@@ -0,0 +1,195 @@
1
+ import zipfile
2
+ import semver
3
+ import spdx_license_list
4
+ import shutil
5
+ import email.utils
6
+ import tomlkit
7
+
8
+ from pathlib import Path
9
+
10
+ from zou.app import config, db
11
+ from zou.app.models.plugin import Plugin
12
+
13
+
14
+ def install_plugin(path, force=False):
15
+ """
16
+ Install a plugin.
17
+ """
18
+ path = Path(path)
19
+ if not path.exists():
20
+ raise FileNotFoundError(f"Plugin path '{path}' does not exist.")
21
+
22
+ try:
23
+ manifest_file = None
24
+ if path.is_dir():
25
+ manifest_file = open(path.joinpath("manifest.toml"), "rb")
26
+ elif zipfile.is_zipfile(path):
27
+ with zipfile.ZipFile(path) as zip_file:
28
+ manifest_file = zip_file.open("manifest.toml", "rb")
29
+ else:
30
+ raise ValueError(
31
+ f"Plugin path '{path}' is not a valid zip file or a directory."
32
+ )
33
+
34
+ # Read the plugin metadatas
35
+ manifest = tomlkit.load(manifest_file)
36
+ finally:
37
+ if manifest_file is not None:
38
+ manifest_file.close()
39
+
40
+ version = str(semver.Version.parse(manifest["version"]))
41
+ spdx_license_list.LICENSES[manifest["license"]]
42
+ if manifest.get("maintainer") is not None:
43
+ manifest["maintainer_name"], manifest["maintainer_email"] = (
44
+ email.utils.parseaddr(manifest["maintainer"])
45
+ )
46
+
47
+ new_plugin_info = {
48
+ "plugin_id": manifest["id"],
49
+ "name": manifest["name"],
50
+ "description": manifest.get("description"),
51
+ "version": version,
52
+ "maintainer_name": manifest["maintainer_name"],
53
+ "maintainer_email": manifest.get("maintainer_email"),
54
+ "website": manifest.get("website"),
55
+ "license": manifest["license"],
56
+ }
57
+
58
+ # Check if the plugin is already installed
59
+ plugin = Plugin.query.filter_by(
60
+ plugin_id=new_plugin_info["plugin_id"]
61
+ ).one_or_none()
62
+
63
+ already_installed = False
64
+ try:
65
+ if plugin is not None:
66
+ existing_plugin_version = semver.Version.parse(plugin.version)
67
+
68
+ if not force:
69
+ if existing_plugin_version == version:
70
+ raise ValueError(
71
+ f"Plugin '{manifest['name']}' version {version} is already installed."
72
+ )
73
+ elif existing_plugin_version > version:
74
+ raise ValueError(
75
+ f"Plugin '{manifest['name']}' version {version} is older than the installed version {existing_plugin_version}."
76
+ )
77
+ already_installed = True
78
+ plugin.update_no_commit(new_plugin_info)
79
+ else:
80
+ plugin = Plugin.create_no_commit(**new_plugin_info)
81
+
82
+ install_plugin_files(
83
+ new_plugin_info["plugin_id"],
84
+ path,
85
+ already_installed=already_installed,
86
+ )
87
+ except:
88
+ db.session.rollback()
89
+ db.session.remove()
90
+ raise
91
+ Plugin.commit()
92
+
93
+ return plugin.serialize()
94
+
95
+
96
+ def install_plugin_files(plugin_id, path, already_installed=False):
97
+ """
98
+ Install the plugin files.
99
+ """
100
+ path = Path(path)
101
+ plugin_path = Path(config.PLUGIN_FOLDER).joinpath(plugin_id)
102
+ if already_installed:
103
+ shutil.rmtree(plugin_path)
104
+
105
+ plugin_path.mkdir(parents=True, exist_ok=True)
106
+
107
+ if path.is_dir():
108
+ shutil.copytree(path, plugin_path, dirs_exist_ok=True)
109
+ elif zipfile.is_zipfile(path):
110
+ shutil.unpack_archive(path, plugin_path, format="zip")
111
+ else:
112
+ raise ValueError(
113
+ f"Plugin path '{path}' is not a valid zip file or a directory."
114
+ )
115
+
116
+ return plugin_path
117
+
118
+
119
+ def uninstall_plugin_files(plugin_id):
120
+ """
121
+ Uninstall the plugin files.
122
+ """
123
+ plugin_path = Path(config.PLUGIN_FOLDER).joinpath(plugin_id)
124
+ if plugin_path.exists():
125
+ shutil.rmtree(plugin_path)
126
+ return True
127
+ return False
128
+
129
+
130
+ def uninstall_plugin(plugin_id):
131
+ """
132
+ Uninstall a plugin.
133
+ """
134
+ installed = uninstall_plugin_files(plugin_id)
135
+ plugin = Plugin.query.filter_by(plugin_id=plugin_id).one_or_none()
136
+ if plugin is not None:
137
+ installed = True
138
+ plugin.delete()
139
+
140
+ if not installed:
141
+ raise ValueError(f"Plugin '{plugin_id}' is not installed.")
142
+ return True
143
+
144
+
145
+ def create_plugin_skeleton(
146
+ path,
147
+ id,
148
+ name,
149
+ description=None,
150
+ version=None,
151
+ maintainer=None,
152
+ website=None,
153
+ license=None,
154
+ force=False,
155
+ ):
156
+ """
157
+ Create a plugin skeleton.
158
+ """
159
+ plugin_template_path = Path(__file__).parent.parent.parent.joinpath(
160
+ "plugin_template"
161
+ )
162
+ plugin_path = Path(path).joinpath(id)
163
+ if plugin_path.exists():
164
+ if force:
165
+ shutil.rmtree(plugin_path)
166
+ else:
167
+ raise ValueError(f"Plugin '{id}' already exists in {plugin_path}.")
168
+
169
+ shutil.copytree(plugin_template_path, plugin_path)
170
+
171
+ manifest_path = plugin_path.joinpath("manifest.toml")
172
+ with open(manifest_path, "r") as manifest_file:
173
+ manifest = tomlkit.load(manifest_file)
174
+
175
+ manifest["id"] = id
176
+ if name is not None:
177
+ manifest["name"] = name
178
+ if description is not None:
179
+ manifest["description"] = description
180
+ if version is not None:
181
+ manifest["version"] = version
182
+ semver.Version.parse(manifest["version"])
183
+ if maintainer is not None:
184
+ manifest["maintainer"] = maintainer
185
+ email.utils.parseaddr(manifest["maintainer"])
186
+ if website is not None:
187
+ manifest["website"] = website
188
+ if license is not None:
189
+ manifest["license"] = license
190
+ spdx_license_list.LICENSES[manifest["license"]]
191
+
192
+ with open(manifest_path, "w") as manifest_file:
193
+ tomlkit.dump(manifest, manifest_file)
194
+
195
+ return plugin_path
@@ -1460,9 +1460,7 @@ def get_last_notifications(
1460
1460
  )
1461
1461
  else:
1462
1462
  reply_mentions = []
1463
- reply_department_mentions = (
1464
- []
1465
- )
1463
+ reply_department_mentions = []
1466
1464
 
1467
1465
  if role == "client" and is_current_user_artist:
1468
1466
  comment_text = ""
zou/app/utils/fields.py CHANGED
@@ -92,6 +92,13 @@ def serialize_models(models, relations=False, milliseconds=False):
92
92
  ]
93
93
 
94
94
 
95
+ def present_models(models):
96
+ """
97
+ Present a list of models (useful for json dumping)
98
+ """
99
+ return [model.present() for model in models if model is not None]
100
+
101
+
95
102
  def gen_uuid():
96
103
  """
97
104
  Generate a unique identifier (useful for json dumping).
@@ -123,6 +130,9 @@ def get_default_date_object(date_string):
123
130
 
124
131
 
125
132
  def is_valid_id(uuid):
133
+ """
134
+ Check if a given string is a valid UUID.
135
+ """
126
136
  _UUID_RE = re.compile(
127
137
  "([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}){1}"
128
138
  )
zou/cli.py CHANGED
@@ -9,7 +9,7 @@ import traceback
9
9
  from sqlalchemy.exc import IntegrityError
10
10
 
11
11
  from zou.app.utils import dbhelpers, auth, commands
12
- from zou.app.services import persons_service, auth_service
12
+ from zou.app.services import persons_service, auth_service, plugin_service
13
13
  from zou.app.services.exception import (
14
14
  IsUserLimitReachedException,
15
15
  PersonNotFoundException,
@@ -643,5 +643,113 @@ def renormalize_movie_preview_files(
643
643
  )
644
644
 
645
645
 
646
+ @cli.command()
647
+ @click.option(
648
+ "--path",
649
+ required=True,
650
+ )
651
+ @click.option(
652
+ "--force",
653
+ is_flag=True,
654
+ default=False,
655
+ show_default=True,
656
+ )
657
+ def install_plugin(path, force=False):
658
+ """
659
+ Install a plugin.
660
+ """
661
+ with app.app_context():
662
+ plugin_service.install_plugin(path, force)
663
+ print(f"Plugin {path} installed. Restart the server to apply changes.")
664
+
665
+
666
+ @cli.command()
667
+ @click.option(
668
+ "--id",
669
+ required=True,
670
+ )
671
+ def uninstall_plugin(id):
672
+ """
673
+ Uninstall a plugin.
674
+ """
675
+ with app.app_context():
676
+ plugin_service.uninstall_plugin(id)
677
+ print(f"Plugin {id} uninstalled.")
678
+
679
+
680
+ @cli.command()
681
+ @click.option(
682
+ "--path",
683
+ required=True,
684
+ )
685
+ @click.option(
686
+ "--id",
687
+ help="Plugin ID (must be unique).",
688
+ required=True,
689
+ )
690
+ @click.option(
691
+ "--name", help="Plugin name.", default="MyPlugin", show_default=True
692
+ )
693
+ @click.option(
694
+ "--description",
695
+ help="Plugin description.",
696
+ default="My plugin description.",
697
+ show_default=True,
698
+ )
699
+ @click.option(
700
+ "--version", help="Plugin version.", default="0.1.0", show_default=True
701
+ )
702
+ @click.option(
703
+ "--maintainer",
704
+ help="Plugin maintainer.",
705
+ default="Author <author@author.com>",
706
+ show_default=True,
707
+ )
708
+ @click.option(
709
+ "--website",
710
+ help="Plugin website.",
711
+ default="mywebsite.com",
712
+ show_default=True,
713
+ )
714
+ @click.option(
715
+ "--license",
716
+ help="Plugin license.",
717
+ default="GPL-3.0-only",
718
+ show_default=True,
719
+ )
720
+ @click.option(
721
+ "--force",
722
+ is_flag=True,
723
+ default=False,
724
+ show_default=True,
725
+ )
726
+ def create_plugin_skeleton(
727
+ path,
728
+ id,
729
+ name,
730
+ description,
731
+ version,
732
+ maintainer,
733
+ website,
734
+ license,
735
+ force=False,
736
+ ):
737
+ """
738
+ Create a plugin skeleton.
739
+ """
740
+ plugin_path = plugin_service.create_plugin_skeleton(
741
+ path,
742
+ id,
743
+ name,
744
+ description,
745
+ version,
746
+ maintainer,
747
+ website,
748
+ license,
749
+ force,
750
+ )
751
+ print(f"Plugin skeleton created in '{plugin_path}'.")
752
+
753
+
646
754
  if __name__ == "__main__":
647
755
  cli()
zou/event_stream.py CHANGED
@@ -35,6 +35,9 @@ def _get_empty_room(current_frame=0):
35
35
  "current_preview_file_index": None,
36
36
  "current_frame": current_frame,
37
37
  "is_repeating": None,
38
+ "is_annotations_displayed": False,
39
+ "is_zoom_enabled": False,
40
+ "is_waveform_displayed": False,
38
41
  "is_laser_mode": None,
39
42
  "handle_in": None,
40
43
  "handle_out": None,
@@ -59,7 +62,7 @@ def _leave_room(room_id, user_id):
59
62
  room["people"] = list(set(room["people"]) - {user_id})
60
63
  if len(room["people"]) > 0:
61
64
  rooms_data[room_id] = room
62
- else:
65
+ elif room_id in rooms_data:
63
66
  del rooms_data[room_id]
64
67
  _emit_people_updated(room_id, room["people"])
65
68
  return room
@@ -81,10 +84,17 @@ def _update_room_playing_status(data, room):
81
84
  room["is_playing"] = data.get("is_playing", False)
82
85
  room["is_repeating"] = data.get("is_repeating", False)
83
86
  room["is_laser_mode"] = data.get("is_laser_mode", False)
87
+ room["is_annotations_displayed"] = data.get(
88
+ "is_annotations_displayed", False
89
+ )
90
+ room["is_zoom_enabled"] = data.get("is_zoom_enabled", False)
91
+ room["is_waveform_displayed"] = data.get("is_waveform_displayed", False)
84
92
  room["current_entity_id"] = data.get("current_entity_id", None)
85
93
  room["current_entity_index"] = data.get("current_entity_index", None)
86
94
  room["current_preview_file_id"] = data.get("current_preview_file_id", None)
87
- room["current_preview_file_index"] = data.get("current_preview_file_index", None)
95
+ room["current_preview_file_index"] = data.get(
96
+ "current_preview_file_index", None
97
+ )
88
98
  room["handle_in"] = data.get("handle_in", None)
89
99
  room["handle_out"] = data.get("handle_out", None)
90
100
  if "current_frame" in data:
@@ -193,19 +203,20 @@ def on_join(data):
193
203
  room["playlist_id"] = room_id
194
204
  rooms_data[room_id] = room
195
205
  _emit_people_updated(room_id, room["people"])
206
+ emit("preview-room:room-updated", room, room=room_id)
196
207
 
197
208
 
198
209
  @socketio.on("preview-room:leave", namespace="/events")
199
210
  @jwt_required()
200
211
  def on_leave(data):
201
212
  user_id = get_jwt_identity()
202
- room_id = data["playlist_id"]
213
+ room_id = data.get("playlist_id", "")
203
214
  _leave_room(room_id, user_id)
204
215
 
205
216
 
206
- @socketio.on("preview-room:update-playing-status", namespace="/events")
217
+ @socketio.on("preview-room:room-updated", namespace="/events")
207
218
  @jwt_required()
208
- def on_playing_status_updated(data, only_newcomer=False):
219
+ def on_room_updated(data, only_newcomer=False):
209
220
  room, room_id = _get_room_from_data(data)
210
221
  rooms_data[room_id] = _update_room_playing_status(data, room)
211
222
  event_data = {"only_newcomer": only_newcomer, **rooms_data[room_id]}
@@ -240,6 +251,13 @@ def on_change_version(data):
240
251
  emit("preview-room:change-version", data, room=room_id)
241
252
 
242
253
 
254
+ @socketio.on("preview-room:panzoom-changed", namespace="/events")
255
+ @jwt_required()
256
+ def on_change_version(data):
257
+ room_id = data["playlist_id"]
258
+ emit("preview-room:panzoom-changed", data, room=room_id)
259
+
260
+
243
261
  if __name__ == "__main__":
244
262
  socketio.run(
245
263
  app,
@@ -0,0 +1,52 @@
1
+ """add people salary information
2
+
3
+ Revision ID: 2762a797f1f9
4
+ Revises: 307edd8c639d
5
+ Create Date: 2025-04-01 18:02:39.343857
6
+
7
+ """
8
+
9
+ from alembic import op
10
+ import sqlalchemy as sa
11
+ import sqlalchemy_utils
12
+
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision = "2762a797f1f9"
16
+ down_revision = "307edd8c639d"
17
+ branch_labels = None
18
+ depends_on = None
19
+
20
+ from zou.app.models.person import POSITION_TYPES, SENIORITY_TYPES
21
+
22
+
23
+ def upgrade():
24
+ with op.batch_alter_table("person", schema=None) as batch_op:
25
+ batch_op.add_column(
26
+ sa.Column(
27
+ "position",
28
+ sqlalchemy_utils.types.choice.ChoiceType(POSITION_TYPES),
29
+ nullable=True,
30
+ )
31
+ )
32
+ batch_op.add_column(
33
+ sa.Column(
34
+ "seniority",
35
+ sqlalchemy_utils.types.choice.ChoiceType(SENIORITY_TYPES),
36
+ nullable=True,
37
+ )
38
+ )
39
+ batch_op.add_column(
40
+ sa.Column("daily_salary", sa.Integer(), nullable=True)
41
+ )
42
+
43
+ # ### end Alembic commands ###
44
+
45
+
46
+ def downgrade():
47
+ # ### commands auto generated by Alembic - please adjust! ###
48
+ with op.batch_alter_table("person", schema=None) as batch_op:
49
+ batch_op.drop_column("daily_salary")
50
+ batch_op.drop_column("seniority")
51
+ batch_op.drop_column("position")
52
+ # ### end Alembic commands ###