zou 0.19.14__py3-none-any.whl → 0.20.11__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 (165) hide show
  1. zou/__init__.py +1 -1
  2. zou/app/__init__.py +10 -2
  3. zou/app/api.py +2 -0
  4. zou/app/blueprints/assets/__init__.py +22 -0
  5. zou/app/blueprints/assets/resources.py +241 -4
  6. zou/app/blueprints/auth/__init__.py +4 -0
  7. zou/app/blueprints/auth/resources.py +154 -22
  8. zou/app/blueprints/breakdown/resources.py +4 -4
  9. zou/app/blueprints/chats/__init__.py +22 -0
  10. zou/app/blueprints/chats/resources.py +199 -0
  11. zou/app/blueprints/comments/resources.py +36 -19
  12. zou/app/blueprints/crud/__init__.py +12 -0
  13. zou/app/blueprints/crud/attachment_file.py +14 -5
  14. zou/app/blueprints/crud/base.py +29 -28
  15. zou/app/blueprints/crud/chat.py +13 -0
  16. zou/app/blueprints/crud/chat_message.py +13 -0
  17. zou/app/blueprints/crud/comments.py +85 -29
  18. zou/app/blueprints/crud/custom_action.py +1 -1
  19. zou/app/blueprints/crud/day_off.py +47 -9
  20. zou/app/blueprints/crud/department.py +1 -25
  21. zou/app/blueprints/crud/entity.py +46 -5
  22. zou/app/blueprints/crud/entity_type.py +13 -1
  23. zou/app/blueprints/crud/event.py +1 -1
  24. zou/app/blueprints/crud/file_status.py +1 -1
  25. zou/app/blueprints/crud/metadata_descriptor.py +24 -10
  26. zou/app/blueprints/crud/organisation.py +22 -5
  27. zou/app/blueprints/crud/output_file.py +1 -1
  28. zou/app/blueprints/crud/output_type.py +1 -1
  29. zou/app/blueprints/crud/person.py +32 -24
  30. zou/app/blueprints/crud/playlist.py +1 -1
  31. zou/app/blueprints/crud/preview_background_file.py +6 -7
  32. zou/app/blueprints/crud/preview_file.py +1 -1
  33. zou/app/blueprints/crud/project.py +14 -6
  34. zou/app/blueprints/crud/project_status.py +1 -1
  35. zou/app/blueprints/crud/schedule_item.py +4 -2
  36. zou/app/blueprints/crud/software.py +1 -1
  37. zou/app/blueprints/crud/status_automation.py +1 -1
  38. zou/app/blueprints/crud/studio.py +33 -0
  39. zou/app/blueprints/crud/task.py +47 -3
  40. zou/app/blueprints/crud/task_status.py +1 -1
  41. zou/app/blueprints/crud/task_type.py +4 -4
  42. zou/app/blueprints/crud/working_file.py +4 -8
  43. zou/app/blueprints/events/resources.py +13 -12
  44. zou/app/blueprints/export/csv/assets.py +15 -6
  45. zou/app/blueprints/export/csv/edits.py +15 -5
  46. zou/app/blueprints/export/csv/playlists.py +1 -1
  47. zou/app/blueprints/export/csv/shots.py +15 -5
  48. zou/app/blueprints/export/csv/time_spents.py +1 -1
  49. zou/app/blueprints/files/resources.py +22 -23
  50. zou/app/blueprints/index/resources.py +38 -29
  51. zou/app/blueprints/news/resources.py +25 -11
  52. zou/app/blueprints/persons/__init__.py +5 -2
  53. zou/app/blueprints/persons/resources.py +126 -120
  54. zou/app/blueprints/previews/__init__.py +18 -8
  55. zou/app/blueprints/previews/resources.py +569 -328
  56. zou/app/blueprints/projects/resources.py +1 -1
  57. zou/app/blueprints/search/resources.py +18 -6
  58. zou/app/blueprints/shots/__init__.py +5 -0
  59. zou/app/blueprints/shots/resources.py +134 -4
  60. zou/app/blueprints/source/__init__.py +6 -6
  61. zou/app/blueprints/source/csv/assets.py +10 -3
  62. zou/app/blueprints/source/csv/base.py +1 -1
  63. zou/app/blueprints/source/csv/edits.py +10 -3
  64. zou/app/blueprints/source/csv/shots.py +10 -3
  65. zou/app/blueprints/source/{edl.py → otio.py} +84 -41
  66. zou/app/blueprints/tasks/__init__.py +3 -2
  67. zou/app/blueprints/tasks/resources.py +83 -52
  68. zou/app/blueprints/user/__init__.py +9 -0
  69. zou/app/blueprints/user/resources.py +170 -12
  70. zou/app/config.py +10 -0
  71. zou/app/mixin.py +6 -5
  72. zou/app/models/attachment_file.py +10 -4
  73. zou/app/models/base.py +18 -13
  74. zou/app/models/build_job.py +7 -4
  75. zou/app/models/chat.py +44 -0
  76. zou/app/models/chat_message.py +37 -0
  77. zou/app/models/comment.py +1 -0
  78. zou/app/models/day_off.py +3 -0
  79. zou/app/models/entity.py +4 -6
  80. zou/app/models/entity_type.py +2 -0
  81. zou/app/models/organisation.py +14 -15
  82. zou/app/models/person.py +6 -1
  83. zou/app/models/project.py +3 -0
  84. zou/app/models/search_filter.py +11 -0
  85. zou/app/models/search_filter_group.py +10 -0
  86. zou/app/models/serializer.py +17 -17
  87. zou/app/models/status_automation.py +2 -0
  88. zou/app/models/studio.py +13 -0
  89. zou/app/models/subscription.py +2 -2
  90. zou/app/models/task.py +6 -1
  91. zou/app/models/task_status.py +1 -0
  92. zou/app/models/task_type.py +1 -0
  93. zou/app/models/working_file.py +1 -1
  94. zou/app/services/assets_service.py +101 -14
  95. zou/app/services/auth_service.py +17 -44
  96. zou/app/services/breakdown_service.py +37 -5
  97. zou/app/services/chats_service.py +279 -0
  98. zou/app/services/comments_service.py +110 -65
  99. zou/app/services/concepts_service.py +4 -12
  100. zou/app/services/deletion_service.py +43 -30
  101. zou/app/services/edits_service.py +5 -11
  102. zou/app/services/emails_service.py +4 -4
  103. zou/app/services/entities_service.py +17 -2
  104. zou/app/services/events_service.py +12 -4
  105. zou/app/services/exception.py +5 -5
  106. zou/app/services/names_service.py +7 -2
  107. zou/app/services/news_service.py +17 -9
  108. zou/app/services/persons_service.py +38 -21
  109. zou/app/services/playlists_service.py +8 -7
  110. zou/app/services/preview_files_service.py +137 -10
  111. zou/app/services/projects_service.py +5 -14
  112. zou/app/services/shots_service.py +221 -49
  113. zou/app/services/sync_service.py +46 -42
  114. zou/app/services/tasks_service.py +185 -46
  115. zou/app/services/time_spents_service.py +67 -20
  116. zou/app/services/user_service.py +350 -107
  117. zou/app/stores/auth_tokens_store.py +2 -1
  118. zou/app/stores/file_store.py +18 -0
  119. zou/app/stores/publisher_store.py +7 -7
  120. zou/app/stores/queue_store.py +1 -0
  121. zou/app/swagger.py +36 -20
  122. zou/app/utils/cache.py +2 -0
  123. zou/app/utils/commands.py +104 -7
  124. zou/app/utils/csv_utils.py +1 -4
  125. zou/app/utils/date_helpers.py +33 -17
  126. zou/app/utils/dbhelpers.py +14 -1
  127. zou/app/utils/emails.py +2 -2
  128. zou/app/utils/fido.py +22 -0
  129. zou/app/utils/flask.py +1 -0
  130. zou/app/utils/query.py +54 -6
  131. zou/app/utils/redis.py +11 -0
  132. zou/app/utils/saml.py +51 -0
  133. zou/app/utils/string.py +2 -0
  134. zou/app/utils/thumbnail.py +4 -2
  135. zou/cli.py +76 -18
  136. zou/debug.py +4 -2
  137. zou/event_stream.py +122 -165
  138. zou/job_settings.py +1 -0
  139. zou/migrations/env.py +0 -0
  140. zou/migrations/utils/base.py +6 -6
  141. zou/migrations/versions/1bb55759146f_add_table_studio.py +67 -0
  142. zou/migrations/versions/1fab8c420678_add_attachments_to_message_chats.py +56 -0
  143. zou/migrations/versions/23122f290ca2_add_entity_chat_models.py +149 -0
  144. zou/migrations/versions/32f134ff1201_add_is_shared_flag_to_filters.py +33 -0
  145. zou/migrations/versions/57222395f2be_add_statusautomation_import_last_revision.py +41 -0
  146. zou/migrations/versions/59a7445a966c_add_entity_is_shared.py +41 -0
  147. zou/migrations/versions/5b980f0dc365_add_comment_links.py +35 -0
  148. zou/migrations/versions/680c64565f9d_for_searchfiltergroup_is_shared.py +35 -0
  149. zou/migrations/versions/8e67c183bed7_add_preference_fields.py +71 -0
  150. zou/migrations/versions/92b40d79ad3f_allow_message_attachments.py +38 -0
  151. zou/migrations/versions/971dbf5a0faf_add_short_name_for_asset_type_entity_.py +33 -0
  152. zou/migrations/versions/9b85c14fa8a7_add_day_off_new_columns.py +68 -0
  153. zou/migrations/versions/9d3bb33c6fc6_add_department_keys_to_filter_models.py +73 -0
  154. zou/migrations/versions/a252a094e977_add_descriptions_for_entities_tasks_and_.py +40 -0
  155. zou/migrations/versions/be56dc0fb760_for_is_shared_disallow_nullable.py +102 -0
  156. zou/migrations/versions/ca28796a2a62_add_is_done_field_to_the_task_model.py +108 -0
  157. zou/migrations/versions/f344b867a911_for_description_of_entity_task_working_.py +75 -0
  158. zou/remote/config_payload.py +2 -1
  159. zou/utils/movie.py +14 -4
  160. {zou-0.19.14.dist-info → zou-0.20.11.dist-info}/METADATA +75 -69
  161. {zou-0.19.14.dist-info → zou-0.20.11.dist-info}/RECORD +164 -135
  162. {zou-0.19.14.dist-info → zou-0.20.11.dist-info}/WHEEL +1 -1
  163. {zou-0.19.14.dist-info → zou-0.20.11.dist-info}/LICENSE +0 -0
  164. {zou-0.19.14.dist-info → zou-0.20.11.dist-info}/entry_points.txt +0 -0
  165. {zou-0.19.14.dist-info → zou-0.20.11.dist-info}/top_level.txt +0 -0
@@ -42,10 +42,11 @@ from zou.app.models.task import Task
42
42
  from zou.app.models.task_status import TaskStatus
43
43
  from zou.app.models.task_type import TaskType
44
44
  from zou.app.models.time_spent import TimeSpent
45
+ from zou.app.models.studio import Studio
45
46
 
46
47
  from zou.app.services import deletion_service, tasks_service, projects_service
47
48
  from zou.app.stores import file_store
48
- from zou.app.utils import events
49
+ from zou.app.utils import events, date_helpers
49
50
  from zou.app import app, config
50
51
 
51
52
 
@@ -102,6 +103,7 @@ event_name_model_map = {
102
103
  "subscription": Subscription,
103
104
  "search-filter": SearchFilter,
104
105
  "search-filter-group": SearchFilterGroup,
106
+ "studio": Studio,
105
107
  "task": Task,
106
108
  "task-status": TaskStatus,
107
109
  "task-type": TaskType,
@@ -140,6 +142,7 @@ event_name_model_path_map = {
140
142
  "search-filter": "search-filters",
141
143
  "search-filter-group": "search-filter-groups",
142
144
  "subscription": "subscriptions",
145
+ "studio": "studios",
143
146
  "task": "tasks",
144
147
  "task-status": "task-status",
145
148
  "task-type": "task-types",
@@ -169,14 +172,15 @@ project_events = [
169
172
  ]
170
173
 
171
174
  main_events = [
172
- "person",
173
- "organisation",
174
- "project-status",
175
+ "studio",
175
176
  "department",
176
177
  "task-type",
177
178
  "task-status",
178
179
  "custom-action",
180
+ "organisation",
181
+ "project-status",
179
182
  "asset-type",
183
+ "person",
180
184
  "project",
181
185
  ]
182
186
 
@@ -319,14 +323,14 @@ def run_other_sync(project=None, with_events=False):
319
323
  sync_entries("events", ApiEvent, project=project)
320
324
 
321
325
 
322
- def run_last_events_sync(minutes=0, page_size=300):
326
+ def run_last_events_sync(minutes=0, limit=300):
323
327
  """
324
328
  Retrieve last events from source instance and import related data and
325
329
  action.
326
330
  """
327
- path = "events/last?page_size=%s" % page_size
331
+ path = "events/last?limit=%s" % limit
328
332
  if minutes > 0:
329
- now = datetime.datetime.utcnow()
333
+ now = date_helpers.get_utc_now_datetime()
330
334
  min_before = now - datetime.timedelta(minutes=minutes)
331
335
  after = min_before.strftime("%Y-%m-%dT%H:%M:%S")
332
336
  path += "&before=%s" % now.strftime("%Y-%m-%dT%H:%M:%S")
@@ -342,14 +346,14 @@ def run_last_events_sync(minutes=0, page_size=300):
342
346
  pass
343
347
 
344
348
 
345
- def run_last_events_files(minutes=0, page_size=50):
349
+ def run_last_events_files(minutes=0, limit=50):
346
350
  """
347
351
  Retrieve last events from source instance and import related data and
348
352
  action.
349
353
  """
350
- path = "events/last?only_files=true&page_size=%s" % page_size
354
+ path = "events/last?only_files=true&limit=%s" % limit
351
355
  if minutes > 0:
352
- now = datetime.datetime.utcnow()
356
+ now = date_helpers.get_utc_now_datetime()
353
357
  min_before = now - datetime.timedelta(minutes=minutes)
354
358
  after = min_before.strftime("%Y-%m-%dT%H:%M:%S")
355
359
  path += "&before=%s" % now.strftime("%Y-%m-%dT%H:%M:%S")
@@ -404,35 +408,35 @@ def sync_entries(model_name, model, project=None):
404
408
  """
405
409
  instances = []
406
410
 
407
- if model_name in ["organisations", "persons"]:
408
- path = model_name + "?relations=true"
409
- if model_name == "persons":
410
- path += "&with_pass_hash=true"
411
- instances = gazu.client.fetch_all(path)
412
- model.create_from_import_list(instances)
413
- elif project:
411
+ page = 1
412
+ init = True
413
+ results = {"nb_pages": 2}
414
+ params = {
415
+ "relations": "true",
416
+ }
417
+ if model_name == "persons":
418
+ params["with_pass_hash"] = "true"
419
+ if project is not None and model_name in [
420
+ "projects",
421
+ "search-filters",
422
+ "search-filter-groups",
423
+ ]:
414
424
  project = gazu.project.get_project_by_name(project)
415
425
  if model_name == "projects":
416
- instances = [gazu.client.fetch_one(model_name, project.get("id"))]
426
+ params = {"id": project["id"]}
417
427
  elif model_name in ["search-filters", "search-filter-groups"]:
418
- instances = gazu.client.fetch_all(
419
- model_name, params=dict(project_id=project.get("id"))
420
- )
421
- else:
422
- instances = gazu.client.fetch_all(model_name)
423
- model.create_from_import_list(instances)
424
- else:
425
- page = 1
426
- init = True
427
- results = {"nb_pages": 2}
428
- while init or results["nb_pages"] >= page:
429
- results = gazu.client.fetch_all(
430
- "%s?relations=true&page=%d" % (model_name, page)
431
- )
432
- instances += results["data"]
433
- page += 1
434
- init = False
435
- model.create_from_import_list(results["data"])
428
+ params = {"project_id": project["id"]}
429
+ while init or results["nb_pages"] >= page:
430
+ params["page"] = page
431
+ results = gazu.client.fetch_all(model_name, params=params)
432
+ if model_name == "task-status" and results["data"]:
433
+ results["data"] = [
434
+ r for r in results["data"] if not r["for_concept"]
435
+ ]
436
+ instances += results["data"]
437
+ page += 1
438
+ init = False
439
+ model.create_from_import_list(results["data"])
436
440
 
437
441
  logger.info("%s %s synced." % (len(instances), model_name))
438
442
 
@@ -1144,19 +1148,19 @@ def download_preview_background_from_another_instance(
1144
1148
  """
1145
1149
  Download all files link to preview background file entry.
1146
1150
  """
1147
- extension = preview_background.extension
1148
-
1149
1151
  preview_background_file_id = str(preview_background.id)
1150
1152
  for prefix in [
1151
1153
  "thumbnails",
1152
1154
  "preview-backgrounds",
1153
1155
  ]:
1156
+ extension = (
1157
+ "png" if prefix == "thumbnails" else preview_background.extension
1158
+ )
1154
1159
  if prefix == "preview-backgrounds":
1155
1160
  path = f"/pictures/preview-background-files/{preview_background_file_id}.{extension}"
1156
1161
  elif prefix == "thumbnails":
1157
1162
  path = f"/pictures/thumbnails/preview-background-files/{preview_background_file_id}.png"
1158
1163
 
1159
- extension = "png" if prefix == "thumbnails" else extension
1160
1164
  file_path = f"/tmp/{prefix}-{preview_background_file_id}.{extension}"
1161
1165
  download_file_from_another_instance(
1162
1166
  path,
@@ -1169,9 +1173,9 @@ def download_preview_background_from_another_instance(
1169
1173
  force,
1170
1174
  dict_errors,
1171
1175
  )
1172
- logger.info(
1173
- f"{index:0{len(str(total))}}/{total} Preview background file {preview_background_file_id} processed."
1174
- )
1176
+ logger.info(
1177
+ f"{index:0{len(str(total))}}/{total} Preview background file {preview_background_file_id} processed."
1178
+ )
1175
1179
 
1176
1180
 
1177
1181
  def download_attachment_files_from_another_instance(
@@ -1,9 +1,9 @@
1
1
  import collections
2
- import datetime
3
2
  import uuid
4
3
 
5
4
  from sqlalchemy.exc import StatementError, IntegrityError, DataError
6
5
  from sqlalchemy.sql import func
6
+ from sqlalchemy.sql.expression import case
7
7
  from sqlalchemy.orm import aliased
8
8
 
9
9
  from zou.app import app, db
@@ -29,8 +29,15 @@ from zou.app.models.task import Task
29
29
  from zou.app.models.task_type import TaskType
30
30
  from zou.app.models.task_status import TaskStatus
31
31
  from zou.app.models.time_spent import TimeSpent
32
-
33
- from zou.app.utils import cache, fields, query as query_utils, permissions
32
+ from zou.app.models.studio import Studio
33
+
34
+ from zou.app.utils import (
35
+ cache,
36
+ fields,
37
+ query as query_utils,
38
+ permissions,
39
+ date_helpers,
40
+ )
34
41
 
35
42
 
36
43
  from zou.app.services.exception import (
@@ -41,6 +48,7 @@ from zou.app.services.exception import (
41
48
  TaskStatusNotFoundException,
42
49
  TaskTypeNotFoundException,
43
50
  DepartmentNotFoundException,
51
+ StudioNotFoundException,
44
52
  WrongDateFormatException,
45
53
  TimeSpentNotFoundException,
46
54
  )
@@ -75,16 +83,20 @@ def clear_department_cache(department_id):
75
83
  cache.cache.delete_memoized(get_departments)
76
84
 
77
85
 
86
+ def clear_studio_cache(studio_id):
87
+ cache.cache.delete_memoized(get_studio, studio_id)
88
+ cache.cache.delete_memoized(get_studios)
89
+
90
+
78
91
  def clear_task_cache(task_id):
79
92
  cache.cache.delete_memoized(get_task, task_id)
80
93
  cache.cache.delete_memoized(get_task, task_id, True)
81
- cache.cache.delete_memoized(get_task_with_relations, task_id)
82
94
 
83
95
 
84
96
  @cache.memoize_function(120)
85
97
  def clear_comment_cache(comment_id):
86
98
  cache.cache.delete_memoized(get_comment, comment_id)
87
- cache.cache.delete_memoized(get_comment_with_relations, comment_id)
99
+ cache.cache.delete_memoized(get_comment, comment_id, True)
88
100
 
89
101
 
90
102
  @cache.memoize_function(120)
@@ -92,6 +104,11 @@ def get_departments():
92
104
  return fields.serialize_models(Department.get_all())
93
105
 
94
106
 
107
+ @cache.memoize_function(120)
108
+ def get_studios():
109
+ return fields.serialize_models(Studio.get_all())
110
+
111
+
95
112
  @cache.memoize_function(120)
96
113
  def get_task_types():
97
114
  return fields.serialize_models(TaskType.get_all())
@@ -154,6 +171,22 @@ def get_department(department_id):
154
171
  return department.serialize()
155
172
 
156
173
 
174
+ @cache.memoize_function(120)
175
+ def get_studio(studio_id):
176
+ """
177
+ Get studio matching given id as a dictionary.
178
+ """
179
+ try:
180
+ studio = Studio.get(studio_id)
181
+ except StatementError:
182
+ raise StudioNotFoundException
183
+
184
+ if studio is None:
185
+ raise StudioNotFoundException
186
+
187
+ return studio.serialize()
188
+
189
+
157
190
  def get_department_from_task_type(task_type_id):
158
191
  """
159
192
  Get department of given task type as dictionary
@@ -216,14 +249,6 @@ def get_task(task_id, relations=False):
216
249
  return get_task_raw(task_id).serialize(relations=relations)
217
250
 
218
251
 
219
- @cache.memoize_function(120)
220
- def get_task_with_relations(task_id):
221
- """
222
- Get task matching given id as a dictionary.
223
- """
224
- return get_task_raw(task_id).serialize(relations=True)
225
-
226
-
227
252
  def get_task_by_shotgun_id(shotgun_id):
228
253
  """
229
254
  Get task matching given shotgun id as a dictionary.
@@ -362,7 +387,7 @@ def _convert_rows_to_detailed_tasks(rows, relations=False):
362
387
  entity_name,
363
388
  ) = entry
364
389
 
365
- task = get_task_with_relations(str(task_object.id))
390
+ task = get_task(str(task_object.id), relations=relations)
366
391
  task["project_name"] = project_name
367
392
  task["task_type_name"] = task_type_name
368
393
  task["task_status_name"] = task_status_name
@@ -740,21 +765,11 @@ def get_comment_raw(comment_id):
740
765
 
741
766
 
742
767
  @cache.memoize_function(120)
743
- def get_comment(comment_id):
768
+ def get_comment(comment_id, relations=False):
744
769
  """
745
770
  Return comment matching give id as a dict.
746
771
  """
747
- comment = get_comment_raw(comment_id)
748
- return comment.serialize()
749
-
750
-
751
- @cache.memoize_function(120)
752
- def get_comment_with_relations(comment_id):
753
- """
754
- Return comment matching give id as a dict with joins information.
755
- """
756
- comment = get_comment_raw(comment_id)
757
- return comment.serialize(relations=True)
772
+ return get_comment_raw(comment_id).serialize(relations=relations)
758
773
 
759
774
 
760
775
  def get_comment_by_preview_file_id(preview_file_id):
@@ -951,7 +966,7 @@ def get_person_tasks(person_id, projects, is_done=None):
951
966
  except EpisodeNotFoundException:
952
967
  episode_name = "MP"
953
968
 
954
- task_dict = get_task_with_relations(str(task.id))
969
+ task_dict = get_task(str(task.id), relations=True)
955
970
  if entity_type_name == "Sequence" and entity_parent_id is not None:
956
971
  episode_id = entity_parent_id
957
972
  episode = shots_service.get_episode(episode_id)
@@ -1048,7 +1063,7 @@ def get_person_tasks_to_check(project_ids=None, department_ids=None):
1048
1063
  else:
1049
1064
  query = query.filter(user_service.build_open_project_filter())
1050
1065
 
1051
- if department_ids is not None:
1066
+ if department_ids:
1052
1067
  query = query.filter(TaskType.department_id.in_(department_ids))
1053
1068
  tasks = []
1054
1069
  for (
@@ -1083,7 +1098,7 @@ def get_person_tasks_to_check(project_ids=None, department_ids=None):
1083
1098
  if episode_id is None:
1084
1099
  episode_id = entity_source_id
1085
1100
 
1086
- task_dict = get_task_with_relations(str(task.id))
1101
+ task_dict = get_task(str(task.id), relations=True)
1087
1102
  if entity_type_name == "Sequence" and entity_parent_id is not None:
1088
1103
  episode_id = entity_parent_id
1089
1104
  episode = shots_service.get_episode(episode_id)
@@ -1261,7 +1276,10 @@ def update_task(task_id, data):
1261
1276
  task = get_task_raw(task_id)
1262
1277
 
1263
1278
  if is_finished(task, data):
1264
- data["end_date"] = datetime.datetime.utcnow()
1279
+ data["end_date"] = date_helpers.get_utc_now_datetime()
1280
+
1281
+ if is_done(task, data):
1282
+ data["done_date"] = date_helpers.get_utc_now_datetime()
1265
1283
 
1266
1284
  task.update(data)
1267
1285
  clear_task_cache(task_id)
@@ -1456,7 +1474,7 @@ def delete_time_spent(task_id, person_id, date):
1456
1474
 
1457
1475
  def is_finished(task, data):
1458
1476
  """
1459
- Return True if task status is set to done.
1477
+ Return True if task status is set to feedback request.
1460
1478
  """
1461
1479
  if "task_status_id" in data:
1462
1480
  task_status = get_task_status_raw(task.task_status_id)
@@ -1469,6 +1487,18 @@ def is_finished(task, data):
1469
1487
  return False
1470
1488
 
1471
1489
 
1490
+ def is_done(task, data):
1491
+ """
1492
+ Return True if task status is set to done.
1493
+ """
1494
+ if "task_status_id" in data:
1495
+ task_status = get_task_status_raw(task.task_status_id)
1496
+ new_task_status = get_task_status_raw(data["task_status_id"])
1497
+ return new_task_status.id != task_status.id and new_task_status.is_done
1498
+ else:
1499
+ return False
1500
+
1501
+
1472
1502
  def clear_assignation(task_id, person_id=None):
1473
1503
  """
1474
1504
  Clear task assignation and emit a *task:unassign* event.
@@ -1616,20 +1646,14 @@ def update_preview_file_info(preview_file):
1616
1646
  project = projects_service.get_project(task.project_id)
1617
1647
 
1618
1648
  if project["is_set_preview_automated"]:
1619
- entity_id = str(task.entity_id)
1620
1649
  entity = entities_service.update_entity_preview(
1621
- entity_id,
1650
+ task.entity_id,
1622
1651
  preview_file["id"],
1623
1652
  )
1624
- assets_service.clear_asset_cache(entity_id)
1625
- edits_service.clear_edit_cache(entity_id)
1626
- shots_service.clear_shot_cache(entity_id)
1627
- shots_service.clear_episode_cache(entity_id)
1628
- shots_service.clear_sequence_cache(entity_id)
1629
1653
  return entity
1630
1654
 
1631
1655
 
1632
- def get_comments_for_project(project_id, page=0):
1656
+ def get_comments_for_project(project_id, page=0, limit=None):
1633
1657
  """
1634
1658
  Return all comments for given project.
1635
1659
  """
@@ -1638,7 +1662,9 @@ def get_comments_for_project(project_id, page=0):
1638
1662
  .filter(Task.project_id == project_id)
1639
1663
  .order_by(Comment.updated_at.desc())
1640
1664
  )
1641
- return query_utils.get_paginated_results(query, page, relations=True)
1665
+ return query_utils.get_paginated_results(
1666
+ query, page, limit, relations=True
1667
+ )
1642
1668
 
1643
1669
 
1644
1670
  def get_time_spents_for_project(project_id, page=0):
@@ -1649,18 +1675,38 @@ def get_time_spents_for_project(project_id, page=0):
1649
1675
  return query_utils.get_paginated_results(query, page)
1650
1676
 
1651
1677
 
1652
- def get_tasks_for_project(project_id, page=0):
1678
+ def get_tasks_for_project(
1679
+ project_id, page=0, task_type_id=None, episode_id=None
1680
+ ):
1653
1681
  """
1654
1682
  Return all tasks for given project.
1655
1683
  """
1656
1684
  query = Task.query.filter(Task.project_id == project_id).order_by(
1657
1685
  Task.updated_at.desc()
1658
1686
  )
1687
+ if task_type_id is not None:
1688
+ query = query.filter(Task.task_type_id == task_type_id)
1689
+ if episode_id is not None:
1690
+ Sequence = aliased(Entity, name="sequence")
1691
+ query = (
1692
+ query.join(Entity, Entity.id == Task.entity_id)
1693
+ .join(Sequence, Sequence.id == Entity.parent_id)
1694
+ .filter(Sequence.parent_id == episode_id)
1695
+ )
1696
+
1697
+ if permissions.has_vendor_permissions():
1698
+ query = query.filter(user_service.build_assignee_filter())
1699
+ elif not permissions.has_admin_permissions():
1700
+ query = query.join(Project).filter(
1701
+ user_service.build_related_projects_filter()
1702
+ )
1703
+ return query
1704
+
1659
1705
  return query_utils.get_paginated_results(query, page, relations=True)
1660
1706
 
1661
1707
 
1662
1708
  def get_full_task(task_id, user_id):
1663
- task = get_task_with_relations(task_id)
1709
+ task = get_task(task_id, relations=True)
1664
1710
  task_type = get_task_type(task["task_type_id"])
1665
1711
  project = projects_service.get_project(task["project_id"])
1666
1712
  task_status = get_task_status(task["task_status_id"])
@@ -1724,6 +1770,7 @@ def reset_task_data(task_id):
1724
1770
  real_start_date = None
1725
1771
  last_comment_date = None
1726
1772
  end_date = None
1773
+ done_date = None
1727
1774
  entity = entities_service.get_entity(task.entity_id)
1728
1775
  task_status_id = get_default_status(
1729
1776
  for_concept=entity["entity_type_id"]
@@ -1736,6 +1783,7 @@ def reset_task_data(task_id):
1736
1783
  .add_columns(
1737
1784
  TaskStatus.is_retake,
1738
1785
  TaskStatus.is_feedback_request,
1786
+ TaskStatus.is_done,
1739
1787
  TaskStatus.short_name,
1740
1788
  )
1741
1789
  .all()
@@ -1746,6 +1794,7 @@ def reset_task_data(task_id):
1746
1794
  comment,
1747
1795
  task_status_is_retake,
1748
1796
  task_status_is_feedback_request,
1797
+ task_status_is_done,
1749
1798
  task_status_short_name,
1750
1799
  ) in comments:
1751
1800
  if task_status_is_retake and not previous_is_retake:
@@ -1758,6 +1807,11 @@ def reset_task_data(task_id):
1758
1807
  if task_status_is_feedback_request:
1759
1808
  end_date = comment.created_at
1760
1809
 
1810
+ print("ok", task_status_is_done)
1811
+ if task_status_is_done:
1812
+ done_date = comment.created_at
1813
+ print(done_date)
1814
+
1761
1815
  task_status_id = comment.task_status_id
1762
1816
  last_comment_date = comment.created_at
1763
1817
 
@@ -1773,12 +1827,13 @@ def reset_task_data(task_id):
1773
1827
  "real_start_date": real_start_date,
1774
1828
  "last_comment_date": last_comment_date,
1775
1829
  "end_date": end_date,
1830
+ "done_date": done_date,
1776
1831
  "task_status_id": task_status_id,
1777
1832
  }
1778
1833
  )
1779
1834
  project_id = str(task.project_id)
1780
1835
  events.emit("task:update", {"task_id": task.id}, project_id)
1781
- return task.serialize()
1836
+ return task.serialize(relations=True)
1782
1837
 
1783
1838
 
1784
1839
  def get_persons_tasks_dates():
@@ -1907,14 +1962,20 @@ def get_open_tasks(
1907
1962
  query_stats = query_stats.filter(TaskStatus.id == task_status_id)
1908
1963
 
1909
1964
  if person_id is not None:
1910
- query = query.filter(Task.assignees.any(id=person_id))
1911
- query_stats = query_stats.filter(Task.assignees.any(id=person_id))
1965
+ if person_id == "unassigned":
1966
+ query = query.filter(Task.assignees == None)
1967
+ query_stats = query_stats.filter(Task.assignees == None)
1968
+ else:
1969
+ query = query.filter(Task.assignees.any(id=person_id))
1970
+ query_stats = query_stats.filter(Task.assignees.any(id=person_id))
1912
1971
 
1913
1972
  if start_date is not None:
1973
+ start_date = func.cast(start_date, Task.start_date.type)
1914
1974
  query = query.filter(Task.start_date >= start_date)
1915
1975
  query_stats = query_stats.filter(Task.start_date >= start_date)
1916
1976
 
1917
1977
  if due_date is not None:
1978
+ due_date = func.cast(due_date, Task.due_date.type)
1918
1979
  query = query.filter(Task.due_date <= due_date)
1919
1980
  query_stats = query_stats.filter(Task.due_date <= due_date)
1920
1981
 
@@ -1972,7 +2033,7 @@ def get_open_tasks(
1972
2033
  except EpisodeNotFoundException:
1973
2034
  episode_name = "MP"
1974
2035
 
1975
- task_dict = get_task_with_relations(str(task.id))
2036
+ task_dict = get_task(str(task.id), relations=True)
1976
2037
  if entity_type_name == "Sequence" and entity_parent_id is not None:
1977
2038
  episode_id = entity_parent_id
1978
2039
  episode = shots_service.get_episode(episode_id)
@@ -1998,6 +2059,7 @@ def get_open_tasks(
1998
2059
  "duration": task.duration,
1999
2060
  "start_date": fields.serialize_value(task.start_date),
2000
2061
  "due_date": fields.serialize_value(task.due_date),
2062
+ "done_date": fields.serialize_value(task.done_date),
2001
2063
  "type_name": task_type_name,
2002
2064
  "task_type_for_entity": task_type_for_entity,
2003
2065
  "status_name": task_status_name,
@@ -2043,3 +2105,80 @@ def get_open_tasks(
2043
2105
  "page": page or 1,
2044
2106
  }
2045
2107
  return result
2108
+
2109
+
2110
+ def get_open_tasks_stats():
2111
+ """
2112
+ Return the amount of tasks, done tasks, estimation, and duration for each
2113
+ status in open projects. Aggregate the amounts for each project.
2114
+ """
2115
+ Sequence = aliased(Entity, name="sequence")
2116
+ Episode = aliased(Entity, name="episode")
2117
+
2118
+ from zou.app import db
2119
+
2120
+ query_stats = (
2121
+ db.session.query(
2122
+ func.count().label("amount"),
2123
+ func.count(case({TaskStatus.is_done: Task.id})).label(
2124
+ "amount_done"
2125
+ ),
2126
+ func.sum(Task.duration).label("total_duration"),
2127
+ func.sum(Task.estimation).label("total_estimation"),
2128
+ )
2129
+ .join(TaskType, Task.task_type_id == TaskType.id)
2130
+ .join(TaskStatus, Task.task_status_id == TaskStatus.id)
2131
+ .join(Entity, Entity.id == Task.entity_id)
2132
+ .join(EntityType, EntityType.id == Entity.entity_type_id)
2133
+ .join(Project, Project.id == Task.project_id)
2134
+ .join(ProjectStatus, ProjectStatus.id == Project.project_status_id)
2135
+ .filter(TaskType.for_entity != "Concept")
2136
+ .group_by(Project.id, TaskType.id, TaskStatus.id)
2137
+ .add_columns(
2138
+ Project.id.label("project_id"),
2139
+ TaskType.id.label("task_type_id"),
2140
+ TaskStatus.id.label("task_status_id"),
2141
+ )
2142
+ )
2143
+
2144
+ if permissions.has_admin_permissions():
2145
+ query_stats = query_stats.filter(ProjectStatus.name == "Open")
2146
+ else:
2147
+ query_stats = query_stats.filter(
2148
+ user_service.build_related_projects_filter()
2149
+ )
2150
+
2151
+ stats_status = query_stats.all()
2152
+
2153
+ statuses_stats = [
2154
+ {
2155
+ "task_status_id": stat.task_status_id,
2156
+ "task_type_id": stat.task_type_id,
2157
+ "project_id": stat.project_id,
2158
+ "amount": stat.amount,
2159
+ "amount_done": stat.amount_done,
2160
+ "total_duration": stat.total_duration,
2161
+ "total_estimation": stat.total_estimation,
2162
+ }
2163
+ for stat in stats_status
2164
+ ]
2165
+
2166
+ stats_map = {}
2167
+ for stat in statuses_stats:
2168
+ project_id = stat["project_id"]
2169
+ if project_id not in stats_map:
2170
+ stats_map[project_id] = {
2171
+ "amount": 0,
2172
+ "amount_done": 0,
2173
+ "total_duration": 0,
2174
+ "total_estimation": 0,
2175
+ "task_types": [],
2176
+ }
2177
+ project_stats = stats_map[project_id]
2178
+ project_stats["amount"] += stat["amount"]
2179
+ project_stats["amount_done"] += stat["amount_done"]
2180
+ project_stats["total_duration"] += stat["total_duration"]
2181
+ project_stats["total_estimation"] += stat["total_estimation"]
2182
+ project_stats["task_types"].append(stat)
2183
+
2184
+ return stats_map