zou 1.0.4__py3-none-any.whl → 1.0.6__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/blueprints/crud/person.py +1 -0
- zou/app/blueprints/crud/task_type.py +10 -0
- zou/app/blueprints/files/resources.py +2 -0
- zou/app/blueprints/index/resources.py +76 -66
- zou/app/blueprints/news/resources.py +2 -2
- zou/app/blueprints/persons/resources.py +7 -5
- zou/app/blueprints/playlists/resources.py +8 -2
- zou/app/blueprints/previews/resources.py +11 -2
- zou/app/blueprints/source/csv/base.py +4 -3
- zou/app/config.py +2 -0
- zou/app/file_trees/default.json +6 -0
- zou/app/file_trees/simple.json +6 -0
- zou/app/mixin.py +9 -0
- zou/app/models/plugin.py +21 -2
- zou/app/models/preview_file.py +2 -0
- zou/app/services/comments_service.py +2 -1
- zou/app/services/deletion_service.py +17 -5
- zou/app/services/file_tree_service.py +10 -5
- zou/app/services/notifications_service.py +11 -2
- zou/app/services/persons_service.py +1 -0
- zou/app/services/plugins_service.py +83 -11
- zou/app/services/schedule_service.py +2 -1
- zou/app/services/tasks_service.py +1 -1
- zou/app/services/time_spents_service.py +4 -1
- zou/app/services/user_service.py +5 -0
- zou/app/stores/file_store.py +29 -2
- zou/app/utils/commands.py +1 -1
- zou/app/utils/fs.py +27 -5
- zou/app/utils/plugins.py +122 -11
- zou/cli.py +15 -5
- zou/migrations/versions/12208e50bf18_add_json_data_field_to_preview_files.py +38 -0
- zou/migrations/versions/35ebb38695cd_add_icon_field_to_plugins.py +36 -0
- zou/migrations/versions/9a9df20ea5a7_add_frontend_options_to_plugins.py +40 -0
- zou/migrations/versions/a0f668430352_add_created_by_field_to_playlists.py +16 -8
- zou/plugin_template/__init__.py +2 -13
- zou/plugin_template/migrations/env.py +9 -0
- zou/plugin_template/models.py +2 -2
- zou/plugin_template/{routes.py → resources.py} +4 -0
- {zou-1.0.4.dist-info → zou-1.0.6.dist-info}/METADATA +1 -1
- {zou-1.0.4.dist-info → zou-1.0.6.dist-info}/RECORD +45 -42
- {zou-1.0.4.dist-info → zou-1.0.6.dist-info}/WHEEL +1 -1
- {zou-1.0.4.dist-info → zou-1.0.6.dist-info}/entry_points.txt +0 -0
- {zou-1.0.4.dist-info → zou-1.0.6.dist-info}/licenses/LICENSE +0 -0
- {zou-1.0.4.dist-info → zou-1.0.6.dist-info}/top_level.txt +0 -0
zou/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.0.
|
|
1
|
+
__version__ = "1.0.6"
|
|
@@ -604,6 +604,7 @@ class PersonResource(BaseModelResource, ArgsMixin):
|
|
|
604
604
|
if (
|
|
605
605
|
instance_dict["email"] in config.PROTECTED_ACCOUNTS
|
|
606
606
|
and instance_dict["id"] != persons_service.get_current_user()["id"]
|
|
607
|
+
and instance_dict.get("is_bot", False) == False
|
|
607
608
|
):
|
|
608
609
|
message = None
|
|
609
610
|
if data.get("active") is False:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from flask_jwt_extended import jwt_required
|
|
2
2
|
|
|
3
3
|
from zou.app.models.task_type import TaskType
|
|
4
|
+
from zou.app.models.schedule_item import ScheduleItem
|
|
4
5
|
from zou.app.services.exception import WrongParameterException
|
|
5
6
|
from zou.app.services import tasks_service
|
|
6
7
|
|
|
@@ -317,6 +318,15 @@ class TaskTypeResource(BaseModelResource):
|
|
|
317
318
|
"""
|
|
318
319
|
return super().delete(instance_id)
|
|
319
320
|
|
|
321
|
+
def pre_delete(self, instance_dict):
|
|
322
|
+
"""
|
|
323
|
+
Delete related ScheduleItems before deleting the TaskType.
|
|
324
|
+
This runs AFTER check_delete_permissions, preventing foreign key
|
|
325
|
+
constraint violations while maintaining security.
|
|
326
|
+
"""
|
|
327
|
+
ScheduleItem.query.filter_by(task_type_id=instance_dict["id"]).delete()
|
|
328
|
+
return instance_dict
|
|
329
|
+
|
|
320
330
|
def update_data(self, data, instance_id):
|
|
321
331
|
data = super().update_data(data, instance_id)
|
|
322
332
|
name = data.get("name", None)
|
|
@@ -1303,6 +1303,8 @@ class NewEntityOutputFileResource(Resource, ArgsMixin):
|
|
|
1303
1303
|
return {"error": "Cannot find given person."}, 400
|
|
1304
1304
|
except EntryAlreadyExistsException:
|
|
1305
1305
|
return {"error": "The given output file already exists."}, 400
|
|
1306
|
+
except MalformedFileTreeException as exception:
|
|
1307
|
+
return {"error": str(exception)}, 400
|
|
1306
1308
|
|
|
1307
1309
|
return output_file_dict, 201
|
|
1308
1310
|
|
|
@@ -1,16 +1,21 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
|
|
1
3
|
import psutil
|
|
2
4
|
import redis
|
|
3
5
|
import requests
|
|
4
|
-
import datetime
|
|
5
|
-
|
|
6
6
|
from flask import Response, abort
|
|
7
|
+
from flask_jwt_extended import jwt_required
|
|
7
8
|
from flask_restful import Resource
|
|
8
|
-
from zou import __version__
|
|
9
9
|
|
|
10
|
+
from zou import __version__
|
|
10
11
|
from zou.app import app, config
|
|
11
|
-
from zou.app.
|
|
12
|
-
from zou.app.services import
|
|
13
|
-
|
|
12
|
+
from zou.app.indexer import indexing
|
|
13
|
+
from zou.app.services import (
|
|
14
|
+
persons_service,
|
|
15
|
+
projects_service,
|
|
16
|
+
stats_service,
|
|
17
|
+
)
|
|
18
|
+
from zou.app.utils import date_helpers, permissions, shell
|
|
14
19
|
from zou.app.utils.redis import get_redis_url
|
|
15
20
|
|
|
16
21
|
|
|
@@ -40,14 +45,32 @@ class IndexResource(Resource):
|
|
|
40
45
|
|
|
41
46
|
|
|
42
47
|
class BaseStatusResource(Resource):
|
|
48
|
+
|
|
43
49
|
def get_status(self):
|
|
44
|
-
is_db_up =
|
|
50
|
+
is_db_up = self._check_database()
|
|
51
|
+
is_kv_up = self._check_key_value_store()
|
|
52
|
+
is_es_up = self._check_event_stream()
|
|
53
|
+
is_jq_up = self._check_job_queue()
|
|
54
|
+
is_indexer_up = self._check_indexer()
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
config.APP_NAME,
|
|
58
|
+
__version__,
|
|
59
|
+
is_db_up,
|
|
60
|
+
is_kv_up,
|
|
61
|
+
is_es_up,
|
|
62
|
+
is_jq_up,
|
|
63
|
+
is_indexer_up,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def _check_database(self):
|
|
45
67
|
try:
|
|
46
68
|
projects_service.get_or_create_status("Open")
|
|
69
|
+
return True
|
|
47
70
|
except Exception:
|
|
48
|
-
|
|
71
|
+
return False
|
|
49
72
|
|
|
50
|
-
|
|
73
|
+
def _check_key_value_store(self):
|
|
51
74
|
try:
|
|
52
75
|
store = redis.StrictRedis(
|
|
53
76
|
host=config.KEY_VALUE_STORE["host"],
|
|
@@ -57,18 +80,21 @@ class BaseStatusResource(Resource):
|
|
|
57
80
|
decode_responses=True,
|
|
58
81
|
)
|
|
59
82
|
store.get("test")
|
|
83
|
+
return True
|
|
60
84
|
except redis.ConnectionError:
|
|
61
|
-
|
|
85
|
+
return False
|
|
62
86
|
|
|
63
|
-
|
|
87
|
+
def _check_event_stream(self):
|
|
64
88
|
try:
|
|
65
89
|
requests.get(
|
|
66
|
-
f"http://{config.EVENT_STREAM_HOST}:{config.EVENT_STREAM_PORT}"
|
|
90
|
+
f"http://{config.EVENT_STREAM_HOST}:{config.EVENT_STREAM_PORT}",
|
|
91
|
+
timeout=5,
|
|
67
92
|
)
|
|
93
|
+
return True
|
|
68
94
|
except Exception:
|
|
69
|
-
|
|
95
|
+
return False
|
|
70
96
|
|
|
71
|
-
|
|
97
|
+
def _check_job_queue(self):
|
|
72
98
|
try:
|
|
73
99
|
args = [
|
|
74
100
|
"rq",
|
|
@@ -77,30 +103,20 @@ class BaseStatusResource(Resource):
|
|
|
77
103
|
get_redis_url(config.KV_JOB_DB_INDEX),
|
|
78
104
|
]
|
|
79
105
|
out = shell.run_command(args)
|
|
80
|
-
|
|
106
|
+
return b"0 workers" not in out
|
|
81
107
|
except Exception:
|
|
82
108
|
app.logger.error("Job queue is not accessible", exc_info=1)
|
|
83
|
-
|
|
109
|
+
return False
|
|
84
110
|
|
|
85
|
-
|
|
111
|
+
def _check_indexer(self):
|
|
86
112
|
try:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
113
|
+
client = indexing.get_client()
|
|
114
|
+
client.get_indexes()
|
|
115
|
+
return True
|
|
116
|
+
except indexing.IndexerNotInitializedError:
|
|
117
|
+
return False
|
|
90
118
|
except Exception:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
version = __version__
|
|
94
|
-
|
|
95
|
-
return (
|
|
96
|
-
config.APP_NAME,
|
|
97
|
-
version,
|
|
98
|
-
is_db_up,
|
|
99
|
-
is_kv_up,
|
|
100
|
-
is_es_up,
|
|
101
|
-
is_jq_up,
|
|
102
|
-
is_indexer_up,
|
|
103
|
-
)
|
|
119
|
+
return False
|
|
104
120
|
|
|
105
121
|
|
|
106
122
|
class StatusResource(BaseStatusResource):
|
|
@@ -224,9 +240,16 @@ class StatusResourcesResource(BaseStatusResource):
|
|
|
224
240
|
type: integer
|
|
225
241
|
example: 3
|
|
226
242
|
"""
|
|
227
|
-
|
|
243
|
+
return {
|
|
244
|
+
"date": datetime.datetime.now().isoformat(),
|
|
245
|
+
"cpu": self._get_cpu_stats(),
|
|
246
|
+
"memory": self._get_memory_stats(),
|
|
247
|
+
"jobs": self._get_job_stats(),
|
|
248
|
+
}
|
|
228
249
|
|
|
229
|
-
|
|
250
|
+
def _get_cpu_stats(self):
|
|
251
|
+
loadavg = list(psutil.getloadavg())
|
|
252
|
+
return {
|
|
230
253
|
"percent": psutil.cpu_percent(interval=1, percpu=True),
|
|
231
254
|
"loadavg": {
|
|
232
255
|
"last 1 min": loadavg[0],
|
|
@@ -235,29 +258,23 @@ class StatusResourcesResource(BaseStatusResource):
|
|
|
235
258
|
},
|
|
236
259
|
}
|
|
237
260
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
"
|
|
242
|
-
"
|
|
261
|
+
def _get_memory_stats(self):
|
|
262
|
+
memory = psutil.virtual_memory()
|
|
263
|
+
return {
|
|
264
|
+
"total": memory.total,
|
|
265
|
+
"used": memory.used,
|
|
266
|
+
"available": memory.available,
|
|
267
|
+
"percent": memory.percent,
|
|
243
268
|
}
|
|
244
269
|
|
|
270
|
+
def _get_job_stats(self):
|
|
245
271
|
nb_jobs = 0
|
|
246
272
|
if config.ENABLE_JOB_QUEUE:
|
|
247
273
|
from zou.app.stores.queue_store import job_queue
|
|
248
274
|
|
|
249
275
|
registry = job_queue.started_job_registry
|
|
250
276
|
nb_jobs = registry.count
|
|
251
|
-
|
|
252
|
-
"running_jobs": nb_jobs,
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return {
|
|
256
|
-
"date": datetime.datetime.now().isoformat(),
|
|
257
|
-
"cpu": cpu_stats,
|
|
258
|
-
"memory": memory_stats,
|
|
259
|
-
"jobs": job_stats,
|
|
260
|
-
}
|
|
277
|
+
return {"running_jobs": nb_jobs}
|
|
261
278
|
|
|
262
279
|
|
|
263
280
|
class TxtStatusResource(BaseStatusResource):
|
|
@@ -294,22 +311,14 @@ class TxtStatusResource(BaseStatusResource):
|
|
|
294
311
|
is_indexer_up,
|
|
295
312
|
) = self.get_status()
|
|
296
313
|
|
|
297
|
-
text = """name:
|
|
298
|
-
version:
|
|
299
|
-
database-up:
|
|
300
|
-
event-stream-up:
|
|
301
|
-
key-value-store-up:
|
|
302
|
-
job-queue-up:
|
|
303
|
-
indexer-up:
|
|
304
|
-
"""
|
|
305
|
-
api_name,
|
|
306
|
-
version,
|
|
307
|
-
"up" if is_db_up else "down",
|
|
308
|
-
"up" if is_kv_up else "down",
|
|
309
|
-
"up" if is_es_up else "down",
|
|
310
|
-
"up" if is_jq_up else "down",
|
|
311
|
-
"up" if is_indexer_up else "down",
|
|
312
|
-
)
|
|
314
|
+
text = f"""name: {api_name}
|
|
315
|
+
version: {version}
|
|
316
|
+
database-up: {"up" if is_db_up else "down"}
|
|
317
|
+
event-stream-up: {"up" if is_es_up else "down"}
|
|
318
|
+
key-value-store-up: {"up" if is_kv_up else "down"}
|
|
319
|
+
job-queue-up: {"up" if is_jq_up else "down"}
|
|
320
|
+
indexer-up: {"up" if is_indexer_up else "down"}
|
|
321
|
+
"""
|
|
313
322
|
return Response(text, mimetype="text")
|
|
314
323
|
|
|
315
324
|
|
|
@@ -469,6 +478,7 @@ class ConfigResource(Resource):
|
|
|
469
478
|
"saml_idp_name": config.SAML_IDP_NAME,
|
|
470
479
|
"default_locale": config.DEFAULT_LOCALE,
|
|
471
480
|
"default_timezone": config.DEFAULT_TIMEZONE,
|
|
481
|
+
"enforce_2fa": config.ENFORCE_2FA,
|
|
472
482
|
}
|
|
473
483
|
if config.SENTRY_KITSU_ENABLED:
|
|
474
484
|
conf["sentry"] = {
|
|
@@ -228,7 +228,7 @@ class NewsResource(Resource, NewsMixin, ArgsMixin):
|
|
|
228
228
|
"""
|
|
229
229
|
Get open projects news
|
|
230
230
|
---
|
|
231
|
-
description: Returns the latest news and activity feed from all
|
|
231
|
+
description: Returns the latest news and activity feed from all
|
|
232
232
|
projects the user has access to.
|
|
233
233
|
tags:
|
|
234
234
|
- News
|
|
@@ -348,7 +348,7 @@ class ProjectSingleNewsResource(Resource):
|
|
|
348
348
|
"""
|
|
349
349
|
Get news item
|
|
350
350
|
---
|
|
351
|
-
description: Retrieves detailed information about a specific news item
|
|
351
|
+
description: Retrieves detailed information about a specific news item
|
|
352
352
|
from a givenproject.
|
|
353
353
|
tags:
|
|
354
354
|
- News
|
|
@@ -672,7 +672,7 @@ class PersonMonthAllTimeSpentsResource(Resource):
|
|
|
672
672
|
)
|
|
673
673
|
return fields.serialize_list(timespents)
|
|
674
674
|
except WrongDateFormatException:
|
|
675
|
-
|
|
675
|
+
raise WrongParameterException("Invalid month or year.")
|
|
676
676
|
|
|
677
677
|
|
|
678
678
|
class PersonWeekTimeSpentsResource(PersonDurationTimeSpentsResource):
|
|
@@ -1160,10 +1160,12 @@ class TimeSpentMonthResource(TimeSpentDurationResource):
|
|
|
1160
1160
|
schema:
|
|
1161
1161
|
type: object
|
|
1162
1162
|
"""
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1163
|
+
try:
|
|
1164
|
+
return time_spents_service.get_day_table(
|
|
1165
|
+
year, month, **self.get_person_project_department_arguments()
|
|
1166
|
+
)
|
|
1167
|
+
except WrongDateFormatException:
|
|
1168
|
+
raise WrongParameterException("Invalid month or year.")
|
|
1167
1169
|
|
|
1168
1170
|
|
|
1169
1171
|
class TimeSpentYearsResource(TimeSpentDurationResource):
|
|
@@ -835,6 +835,11 @@ class NotifyClientsResource(Resource, ArgsMixin):
|
|
|
835
835
|
format: uuid
|
|
836
836
|
description: Studio unique identifier to notify
|
|
837
837
|
example: b35b7fb5-df86-5776-b181-68564193d36
|
|
838
|
+
department_id:
|
|
839
|
+
type: string
|
|
840
|
+
format: uuid
|
|
841
|
+
description: Department unique identifier to notify
|
|
842
|
+
example: c46c8gc6-eg97-6887-c292-79675204e47
|
|
838
843
|
responses:
|
|
839
844
|
200:
|
|
840
845
|
description: Clients notified successfully
|
|
@@ -848,11 +853,12 @@ class NotifyClientsResource(Resource, ArgsMixin):
|
|
|
848
853
|
description: Notification status
|
|
849
854
|
example: "success"
|
|
850
855
|
"""
|
|
851
|
-
studio_id =
|
|
856
|
+
studio_id = self.get_id_parameter("studio_id", None)
|
|
857
|
+
department_id = self.get_id_parameter("department_id", None)
|
|
852
858
|
playlist = playlists_service.get_playlist(playlist_id)
|
|
853
859
|
project_id = playlist["project_id"]
|
|
854
860
|
user_service.check_manager_project_access(project_id)
|
|
855
861
|
notifications_service.notify_clients_playlist_ready(
|
|
856
|
-
playlist, studio_id
|
|
862
|
+
playlist, studio_id, department_id
|
|
857
863
|
)
|
|
858
864
|
return {"status": "success"}
|
|
@@ -248,15 +248,24 @@ class BaseNewPreviewFilePicture:
|
|
|
248
248
|
uploaded_movie_path = movie.save_file(
|
|
249
249
|
tmp_folder, preview_file_id, uploaded_file
|
|
250
250
|
)
|
|
251
|
+
save_source_file = config.PREVIEW_SAVE_SOURCE_FILE
|
|
251
252
|
if normalize and config.ENABLE_JOB_QUEUE and not no_job:
|
|
252
253
|
queue_store.job_queue.enqueue(
|
|
253
254
|
preview_files_service.prepare_and_store_movie,
|
|
254
|
-
args=(
|
|
255
|
+
args=(
|
|
256
|
+
preview_file_id,
|
|
257
|
+
uploaded_movie_path,
|
|
258
|
+
True,
|
|
259
|
+
save_source_file,
|
|
260
|
+
),
|
|
255
261
|
job_timeout=int(config.JOB_QUEUE_TIMEOUT),
|
|
256
262
|
)
|
|
257
263
|
else:
|
|
258
264
|
preview_files_service.prepare_and_store_movie(
|
|
259
|
-
preview_file_id,
|
|
265
|
+
preview_file_id,
|
|
266
|
+
uploaded_movie_path,
|
|
267
|
+
normalize=normalize,
|
|
268
|
+
add_source_to_file_store=save_source_file,
|
|
260
269
|
)
|
|
261
270
|
return preview_file_id
|
|
262
271
|
|
|
@@ -68,7 +68,7 @@ class BaseCsvImportResource(Resource, ArgsMixin):
|
|
|
68
68
|
result = []
|
|
69
69
|
self.check_permissions(*args)
|
|
70
70
|
self.prepare_import(*args)
|
|
71
|
-
with open(file_path) as csvfile:
|
|
71
|
+
with open(file_path, newline="", encoding="utf-8") as csvfile:
|
|
72
72
|
reader = csv.DictReader(csvfile, dialect=self.get_dialect(csvfile))
|
|
73
73
|
line_number = 1
|
|
74
74
|
for row in reader:
|
|
@@ -94,9 +94,10 @@ class BaseCsvImportResource(Resource, ArgsMixin):
|
|
|
94
94
|
|
|
95
95
|
def get_dialect(self, csvfile):
|
|
96
96
|
sniffer = csv.Sniffer()
|
|
97
|
-
|
|
98
|
-
dialect.doublequote = True
|
|
97
|
+
sample = csvfile.read(8192)
|
|
99
98
|
csvfile.seek(0)
|
|
99
|
+
dialect = sniffer.sniff(sample)
|
|
100
|
+
dialect.doublequote = True
|
|
100
101
|
return dialect
|
|
101
102
|
|
|
102
103
|
def prepare_import(self, *args):
|
zou/app/config.py
CHANGED
|
@@ -68,6 +68,7 @@ PREVIEW_FOLDER = os.getenv(
|
|
|
68
68
|
"PREVIEW_FOLDER",
|
|
69
69
|
os.getenv("THUMBNAIL_FOLDER", os.path.join(os.getcwd(), "previews")),
|
|
70
70
|
)
|
|
71
|
+
PREVIEW_SAVE_SOURCE_FILE = envtobool("PREVIEW_SAVE_SOURCE_FILE", False)
|
|
71
72
|
TMP_DIR = os.getenv("TMP_DIR", os.path.join(tempfile.gettempdir(), "zou"))
|
|
72
73
|
|
|
73
74
|
EVENT_STREAM_HOST = os.getenv("EVENT_STREAM_HOST", "localhost")
|
|
@@ -165,6 +166,7 @@ DEFAULT_LOCALE = os.getenv("DEFAULT_LOCALE", "en_US")
|
|
|
165
166
|
USER_LIMIT = int(os.getenv("USER_LIMIT", "100"))
|
|
166
167
|
MIN_PASSWORD_LENGTH = int(os.getenv("MIN_PASSWORD_LENGTH", 8))
|
|
167
168
|
PROTECTED_ACCOUNTS = env_with_semicolon_to_list("PROTECTED_ACCOUNTS")
|
|
169
|
+
ENFORCE_2FA = envtobool("ENFORCE_2FA", False)
|
|
168
170
|
|
|
169
171
|
TELEMETRY_URL = os.getenv(
|
|
170
172
|
"TELEMETRY_URL",
|
zou/app/file_trees/default.json
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
"asset": "<Project>/assets/<AssetType>/<Asset>/<TaskType>",
|
|
8
8
|
"sequence": "<Project>/sequences/<Sequence>/<TaskType>",
|
|
9
9
|
"scene": "<Project>/scenes/<Sequence>/<Scene>/<TaskType>",
|
|
10
|
+
"episode": "<Project>/episodes/<Episode>/<TaskType>",
|
|
10
11
|
"style": "lowercase"
|
|
11
12
|
},
|
|
12
13
|
"file_name": {
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
"asset": "<Project>_<AssetType>_<Asset>_<TaskType>",
|
|
15
16
|
"sequence": "<Project>_<Sequence>_<TaskType>",
|
|
16
17
|
"scene": "<Project>_<Sequence>_<Scene>_<TaskType>",
|
|
18
|
+
"episode": "<Project>_<Episode>_<TaskType>",
|
|
17
19
|
"style": "lowercase"
|
|
18
20
|
}
|
|
19
21
|
},
|
|
@@ -25,6 +27,7 @@
|
|
|
25
27
|
"asset": "<Project>/assets/<AssetType>/<Asset>/<OutputType>",
|
|
26
28
|
"sequence": "<Project>/sequences/<Sequence>/<OutputType>",
|
|
27
29
|
"scene": "<Project>/scenes/<Sequence>/<Scene>/<OutputType>",
|
|
30
|
+
"episode": "<Project>/episodes/<Episode>/<OutputType>",
|
|
28
31
|
"style": "lowercase"
|
|
29
32
|
},
|
|
30
33
|
"file_name": {
|
|
@@ -32,6 +35,7 @@
|
|
|
32
35
|
"asset": "<Project>_<AssetType>_<Asset>_<OutputType>_<OutputFile>",
|
|
33
36
|
"sequence": "<Project>_<Sequence>_<OuputType>_<OutputFile>",
|
|
34
37
|
"scene": "<Project>_<Sequence>_<Scene>_<OutputType>_<OutputFile>",
|
|
38
|
+
"episode": "<Project>_<Episode>_<OutputType>_<OutputFile>",
|
|
35
39
|
"style": "lowercase"
|
|
36
40
|
}
|
|
37
41
|
},
|
|
@@ -43,6 +47,7 @@
|
|
|
43
47
|
"asset": "<Project>/assets/<AssetType>/<Asset>/<TaskType>",
|
|
44
48
|
"sequence": "<Project>/sequences/<Sequence>/<TaskType>",
|
|
45
49
|
"scene": "<Project>/scene/<Scene>/<TaskType>",
|
|
50
|
+
"episode": "<Project>/episodes/<Episode>/<TaskType>",
|
|
46
51
|
"style": "lowercase"
|
|
47
52
|
},
|
|
48
53
|
"file_name": {
|
|
@@ -50,6 +55,7 @@
|
|
|
50
55
|
"asset": "<Project>_<AssetType>_<Asset>_<TaskType>",
|
|
51
56
|
"sequence": "<Project>_<Sequence>_<TaskType>",
|
|
52
57
|
"scene": "<Project>_<Scene>_<TaskType>",
|
|
58
|
+
"episode": "<Project>_<Episode>_<TaskType>",
|
|
53
59
|
"style": "lowercase"
|
|
54
60
|
}
|
|
55
61
|
}
|
zou/app/file_trees/simple.json
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
"asset": "<Project>/assets/<AssetType>/<Asset>/<TaskType>/<Software>",
|
|
8
8
|
"sequence": "<Project>/sequences/<Sequence>/<TaskType>/<Software>",
|
|
9
9
|
"scene": "<Project>/scenes/<Sequence>/<Scene>/<TaskType>/<Software>",
|
|
10
|
+
"episode": "<Project>/episodes/<Episode>/<TaskType>/<Software>",
|
|
10
11
|
"style": "lowercase"
|
|
11
12
|
},
|
|
12
13
|
"file_name": {
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
"asset": "<Project>_<AssetType>_<Asset>_<TaskType>_<Name>_v<Version>",
|
|
15
16
|
"sequence": "<Project>_<Sequence>_<TaskType>_<Name>_v<Version>",
|
|
16
17
|
"scene": "<Project>_<Scene>_<TaskType>_<Name>_v<Version>",
|
|
18
|
+
"episode": "<Project>_<Episode>_<TaskType>_<Name>_v<Version>",
|
|
17
19
|
"style": "lowercase"
|
|
18
20
|
}
|
|
19
21
|
},
|
|
@@ -25,6 +27,7 @@
|
|
|
25
27
|
"asset": "<Project>/assets/<AssetType>/<Asset>/<TaskType>/<OutputType>",
|
|
26
28
|
"sequence": "<Project>/sequences/<Sequence>/<TaskType>/<OutputType>",
|
|
27
29
|
"scene": "<Project>/scenes/<Sequence>/<Scene>/<TaskType>/<OutputType>",
|
|
30
|
+
"episode": "<Project>/episodes/<Episode>/<TaskType>/<OutputType>",
|
|
28
31
|
"instance_asset": "<Project>/assets/<TemporalEntityType>/<TemporalEntity>/<TaskType>/<OutputType>/<AssetType>/<Asset>/instance_<Instance.number>/<Representation>",
|
|
29
32
|
"instance": "<Project>/<TemporalEntityType>/<Sequence>/<TemporalEntity>/<TaskType>/<OutputType>/<AssetType>/<Asset>/instance_<Instance.number>/<Representation>",
|
|
30
33
|
"style": "lowercase"
|
|
@@ -34,6 +37,7 @@
|
|
|
34
37
|
"asset": "<Project>_<AssetType>_<Asset>_<TaskType>_<OutputType>_<Name>_v<Version>",
|
|
35
38
|
"sequence": "<Project>_<Sequence>_<TaskType>_<OutputType>_<Name>_v<Version>",
|
|
36
39
|
"scene": "<Project>_<Scene>_<TaskType>_<OutputType>_<Name>_v<Version>",
|
|
40
|
+
"episode": "<Project>_<Episode>_<TaskType>_<OutputType>_<Name>_v<Version>",
|
|
37
41
|
"instance_asset": "<Project>_<TemporalEntityType>_<TemporalEntity>_<TaskType>_<OutputType>_<Name>_<Instance.name>_v<Version>",
|
|
38
42
|
"instance": "<Project>_<Sequence>_<TemporalEntity>_<TaskType>_<OutputType>_<Name>_<Instance.name>_v<Version>",
|
|
39
43
|
"style": "lowercase"
|
|
@@ -47,6 +51,7 @@
|
|
|
47
51
|
"asset": "<Project>/assets/<AssetType>/<Asset>/<TaskType>",
|
|
48
52
|
"sequence": "<Project>/sequences/<Sequence>/<TaskType>",
|
|
49
53
|
"scene": "<Project>/scenes/<Sequence>/<Scene>/<TaskType>",
|
|
54
|
+
"episode": "<Project>/episodes/<Episode>/<TaskType>",
|
|
50
55
|
"style": "lowercase"
|
|
51
56
|
},
|
|
52
57
|
"file_name": {
|
|
@@ -54,6 +59,7 @@
|
|
|
54
59
|
"asset": "<Project>_<AssetType>_<Asset>_<TaskType>",
|
|
55
60
|
"sequence": "<Project>_<Sequence>_<TaskType>",
|
|
56
61
|
"scene": "<Project>_<Scene>_<TaskType>",
|
|
62
|
+
"episode": "<Project>_<Episode>_<TaskType>",
|
|
57
63
|
"style": "lowercase"
|
|
58
64
|
}
|
|
59
65
|
}
|
zou/app/mixin.py
CHANGED
|
@@ -168,3 +168,12 @@ class ArgsMixin(object):
|
|
|
168
168
|
if not fields.is_valid_id(uuid):
|
|
169
169
|
raise WrongParameterException("Wrong UUID format.")
|
|
170
170
|
return True
|
|
171
|
+
|
|
172
|
+
def get_id_parameter(self, field_name, default=None):
|
|
173
|
+
"""
|
|
174
|
+
Returns ID parameter value matching `field_name`.
|
|
175
|
+
"""
|
|
176
|
+
entity_id = self.get_text_parameter(field_name + "_id", default)
|
|
177
|
+
if entity_id is not None and entity_id != "":
|
|
178
|
+
self.check_id_parameter(entity_id)
|
|
179
|
+
return entity_id
|
zou/app/models/plugin.py
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
from sqlalchemy_utils import EmailType, URLType
|
|
2
|
+
|
|
1
3
|
from zou.app import db
|
|
2
4
|
from zou.app.models.serializer import SerializerMixin
|
|
3
5
|
from zou.app.models.base import BaseMixin
|
|
4
|
-
from sqlalchemy_utils import EmailType, URLType
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class Plugin(db.Model, BaseMixin, SerializerMixin):
|
|
8
9
|
"""
|
|
9
|
-
|
|
10
|
+
A plugin is a module used to extend the functionality of Zou.
|
|
11
|
+
You can extend the REST API routes and the database models.
|
|
10
12
|
"""
|
|
11
13
|
|
|
12
14
|
plugin_id = db.Column(
|
|
@@ -20,3 +22,20 @@ class Plugin(db.Model, BaseMixin, SerializerMixin):
|
|
|
20
22
|
website = db.Column(URLType)
|
|
21
23
|
license = db.Column(db.String(80), nullable=False)
|
|
22
24
|
revision = db.Column(db.String(12), nullable=True)
|
|
25
|
+
frontend_project_enabled = db.Column(db.Boolean(), default=False)
|
|
26
|
+
frontend_studio_enabled = db.Column(db.Boolean(), default=False)
|
|
27
|
+
icon = db.Column(db.String(255), nullable=True) # lucide-vue icon name
|
|
28
|
+
|
|
29
|
+
def present(self):
|
|
30
|
+
return {
|
|
31
|
+
"id": self.id,
|
|
32
|
+
"plugin_id": self.plugin_id,
|
|
33
|
+
"name": self.name,
|
|
34
|
+
"description": self.description,
|
|
35
|
+
"version": self.version,
|
|
36
|
+
"maintainer_name": self.maintainer_name,
|
|
37
|
+
"maintainer_email": self.maintainer_email,
|
|
38
|
+
"frontend_project_enabled": self.frontend_project_enabled,
|
|
39
|
+
"frontend_studio_enabled": self.frontend_studio_enabled,
|
|
40
|
+
"icon": self.icon,
|
|
41
|
+
}
|
zou/app/models/preview_file.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from sqlalchemy_utils import UUIDType, ChoiceType
|
|
2
|
+
from sqlalchemy.dialects.postgresql import JSONB
|
|
2
3
|
|
|
3
4
|
from zou.app import db
|
|
4
5
|
from zou.app.models.serializer import SerializerMixin
|
|
@@ -45,6 +46,7 @@ class PreviewFile(db.Model, BaseMixin, SerializerMixin):
|
|
|
45
46
|
width = db.Column(db.Integer(), default=0)
|
|
46
47
|
height = db.Column(db.Integer(), default=0)
|
|
47
48
|
duration = db.Column(db.Float, default=0)
|
|
49
|
+
data = db.Column(JSONB)
|
|
48
50
|
|
|
49
51
|
task_id = db.Column(
|
|
50
52
|
UUIDType(binary=False), db.ForeignKey("task.id"), index=True
|
|
@@ -188,7 +188,8 @@ def _manage_status_change(task_status, task, comment):
|
|
|
188
188
|
new_data["retake_count"] = retake_count + 1
|
|
189
189
|
|
|
190
190
|
if task_status["is_feedback_request"]:
|
|
191
|
-
|
|
191
|
+
if task.get("end_date") is None:
|
|
192
|
+
new_data["end_date"] = date_helpers.get_utc_now_datetime()
|
|
192
193
|
|
|
193
194
|
if task_status["is_wip"] and task["real_start_date"] is None:
|
|
194
195
|
new_data["real_start_date"] = datetime.datetime.now(
|
|
@@ -42,6 +42,7 @@ from zou.app.services.exception import (
|
|
|
42
42
|
CommentNotFoundException,
|
|
43
43
|
ModelWithRelationsDeletionException,
|
|
44
44
|
PersonInProtectedAccounts,
|
|
45
|
+
PreviewFileNotFoundException,
|
|
45
46
|
)
|
|
46
47
|
|
|
47
48
|
|
|
@@ -157,6 +158,8 @@ def remove_task(task_id, force=False):
|
|
|
157
158
|
|
|
158
159
|
def remove_preview_file_by_id(preview_file_id, force=False):
|
|
159
160
|
preview_file = PreviewFile.get(preview_file_id)
|
|
161
|
+
if preview_file is None:
|
|
162
|
+
raise PreviewFileNotFoundException
|
|
160
163
|
return remove_preview_file(preview_file, force=force)
|
|
161
164
|
|
|
162
165
|
|
|
@@ -291,11 +294,12 @@ def clear_movie_files(preview_file_id):
|
|
|
291
294
|
Remove all files related to given preview file, supposing the original file
|
|
292
295
|
was a movie.
|
|
293
296
|
"""
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
297
|
+
for movie_type in ["previews", "lowdef", "source"]:
|
|
298
|
+
try:
|
|
299
|
+
file_store.remove_movie(movie_type, preview_file_id)
|
|
300
|
+
except BaseException:
|
|
301
|
+
pass
|
|
302
|
+
for image_type in ["thumbnails", "thumbnails-square", "previews", "tiles"]:
|
|
299
303
|
try:
|
|
300
304
|
file_store.remove_picture(image_type, preview_file_id)
|
|
301
305
|
except BaseException:
|
|
@@ -344,6 +348,14 @@ def remove_tasks_for_project_and_task_type(project_id, task_type_id):
|
|
|
344
348
|
def remove_project(project_id):
|
|
345
349
|
from zou.app.services import playlists_service
|
|
346
350
|
|
|
351
|
+
preview_files = (
|
|
352
|
+
PreviewFile.query.join(Task)
|
|
353
|
+
.filter(Task.project_id == project_id)
|
|
354
|
+
.all()
|
|
355
|
+
)
|
|
356
|
+
for preview_file in preview_files:
|
|
357
|
+
remove_preview_file(preview_file, force=True)
|
|
358
|
+
|
|
347
359
|
tasks = Task.query.filter_by(project_id=project_id)
|
|
348
360
|
for task in tasks:
|
|
349
361
|
remove_task(task.id, force=True)
|
|
@@ -595,13 +595,18 @@ def get_folder_from_sequence(entity, field="name"):
|
|
|
595
595
|
|
|
596
596
|
|
|
597
597
|
def get_folder_from_episode(entity, field="name"):
|
|
598
|
-
|
|
599
|
-
sequence = shots_service.get_sequence_from_shot(entity)
|
|
600
|
-
elif shots_service.is_sequence(entity):
|
|
601
|
-
sequence = entity
|
|
598
|
+
episode = None
|
|
602
599
|
|
|
603
|
-
|
|
600
|
+
if shots_service.is_episode(entity):
|
|
601
|
+
episode = entity
|
|
602
|
+
else:
|
|
603
|
+
if shots_service.is_shot(entity) or shots_service.is_scene(entity):
|
|
604
|
+
sequence = shots_service.get_sequence_from_shot(entity)
|
|
605
|
+
elif shots_service.is_sequence(entity):
|
|
606
|
+
sequence = entity
|
|
604
607
|
episode = shots_service.get_episode_from_sequence(sequence)
|
|
608
|
+
|
|
609
|
+
try:
|
|
605
610
|
episode_name = episode[field]
|
|
606
611
|
except BaseException:
|
|
607
612
|
episode_name = "e001"
|