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.
- zou/__init__.py +1 -1
- zou/app/api.py +32 -41
- zou/app/blueprints/crud/__init__.py +6 -0
- zou/app/blueprints/crud/budget.py +21 -0
- zou/app/blueprints/crud/budget_entry.py +15 -0
- zou/app/blueprints/crud/plugin.py +13 -0
- zou/app/blueprints/crud/salary_scale.py +73 -0
- zou/app/blueprints/playlists/resources.py +15 -0
- zou/app/blueprints/projects/__init__.py +17 -0
- zou/app/blueprints/projects/resources.py +402 -7
- zou/app/blueprints/shots/resources.py +1 -0
- zou/app/mixin.py +12 -1
- zou/app/models/budget.py +39 -0
- zou/app/models/budget_entry.py +65 -0
- zou/app/models/person.py +17 -1
- zou/app/models/plugin.py +21 -0
- zou/app/models/salary_scale.py +28 -0
- zou/app/services/budget_service.py +195 -0
- zou/app/services/comments_service.py +1 -1
- zou/app/services/exception.py +8 -0
- zou/app/services/plugin_service.py +195 -0
- zou/app/services/user_service.py +1 -3
- zou/app/utils/fields.py +10 -0
- zou/cli.py +109 -1
- zou/event_stream.py +23 -5
- zou/migrations/versions/2762a797f1f9_add_people_salary_information.py +52 -0
- zou/migrations/versions/45f739ef962a_add_people_salary_scale_table.py +70 -0
- zou/migrations/versions/4aab1f84ad72_introduce_plugin_table.py +68 -0
- zou/migrations/versions/7a16258f2fab_add_currency_field_to_budgets.py +33 -0
- zou/migrations/versions/83e2f33a9b14_add_project_bugdet_table.py +57 -0
- zou/migrations/versions/8ab98c178903_add_budget_entry_table.py +123 -0
- zou/migrations/versions/d25118cddcaa_modify_salary_scale_model.py +133 -0
- zou/plugin_template/__init__.py +39 -0
- zou/plugin_template/routes.py +6 -0
- {zou-0.20.38.dist-info → zou-0.20.39.dist-info}/METADATA +6 -3
- {zou-0.20.38.dist-info → zou-0.20.39.dist-info}/RECORD +40 -21
- {zou-0.20.38.dist-info → zou-0.20.39.dist-info}/WHEEL +1 -1
- {zou-0.20.38.dist-info → zou-0.20.39.dist-info}/entry_points.txt +0 -0
- {zou-0.20.38.dist-info → zou-0.20.39.dist-info}/licenses/LICENSE +0 -0
- {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()
|
zou/app/services/exception.py
CHANGED
|
@@ -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
|
zou/app/services/user_service.py
CHANGED
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
|
-
|
|
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(
|
|
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
|
|
213
|
+
room_id = data.get("playlist_id", "")
|
|
203
214
|
_leave_room(room_id, user_id)
|
|
204
215
|
|
|
205
216
|
|
|
206
|
-
@socketio.on("preview-room:
|
|
217
|
+
@socketio.on("preview-room:room-updated", namespace="/events")
|
|
207
218
|
@jwt_required()
|
|
208
|
-
def
|
|
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 ###
|