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
@@ -0,0 +1,279 @@
1
+ import random
2
+ import string
3
+
4
+ from flask import current_app
5
+ from sqlalchemy.exc import IntegrityError
6
+
7
+ from zou.app.models.attachment_file import AttachmentFile
8
+ from zou.app.models.chat import Chat
9
+ from zou.app.models.chat_message import ChatMessage
10
+ from zou.app.models.entity import Entity
11
+ from zou.app.models.project import Project
12
+ from zou.app.models.project_status import ProjectStatus
13
+
14
+ from zou.app.utils import cache, events, fs, thumbnail
15
+
16
+ from zou.app.stores import file_store
17
+
18
+ from zou.app.services import names_service, persons_service
19
+
20
+
21
+ def clear_chat_message_cache(chat_message_id):
22
+ cache.cache.delete_memoized(get_chat_message, chat_message_id)
23
+
24
+
25
+ def get_chat_raw(entity_id):
26
+ """
27
+ Return chat corresponding to given entity ID.
28
+ """
29
+ chat = Chat.get_by(object_id=entity_id)
30
+ if chat is None:
31
+ chat = Chat.create(object_id=entity_id)
32
+ return chat
33
+
34
+
35
+ def get_chat(entity_id):
36
+ """
37
+ Return chat corresponding to given entity ID.
38
+ """
39
+ chat = get_chat_raw(entity_id)
40
+ return chat.serialize(relations=True)
41
+
42
+
43
+ def get_chat_by_id(chat_id):
44
+ """
45
+ Return chat corresponding to given entity ID.
46
+ """
47
+ chat = Chat.get(chat_id)
48
+ return chat.serialize(relations=True)
49
+
50
+
51
+ def get_chat_message_raw(chat_message_id):
52
+ """
53
+ Return chat message corresponding to given chat message ID.
54
+ """
55
+ return ChatMessage.get(chat_message_id)
56
+
57
+
58
+ @cache.memoize_function(1200)
59
+ def get_chat_message(chat_message_id):
60
+ """
61
+ Return chat message corresponding to given chat message ID.
62
+ """
63
+ message = get_chat_message_raw(chat_message_id)
64
+ serialized_message = message.serialize()
65
+ serialized_message["attachment_files"] = []
66
+ for attachment_file in message.attachment_files:
67
+ serialized_message["attachment_files"].append(
68
+ {
69
+ "id": attachment_file.id,
70
+ "name": attachment_file.name,
71
+ "extension": attachment_file.extension,
72
+ }
73
+ )
74
+ return serialized_message
75
+
76
+
77
+ def join_chat(entity_id, person_id):
78
+ """
79
+ Join chat for given entity ID.
80
+ """
81
+ chat = get_chat_raw(entity_id)
82
+ person = persons_service.get_person_raw(person_id)
83
+ chat.participants.append(person)
84
+ chat.save()
85
+ events.emit(
86
+ "chat:joined",
87
+ data={"chat_id": chat.id, "person_id": person.id},
88
+ persist=False,
89
+ )
90
+ return chat.serialize()
91
+
92
+
93
+ def leave_chat(entity_id, person_id):
94
+ """
95
+ Leave chat for given entity ID.
96
+ """
97
+ chat = get_chat_raw(entity_id)
98
+ person = persons_service.get_person_raw(person_id)
99
+ chat.participants.remove(person)
100
+ chat.save()
101
+ events.emit(
102
+ "chat:left",
103
+ data={"chat_id": chat.id, "person_id": person.id},
104
+ persist=False,
105
+ )
106
+ return chat.serialize()
107
+
108
+
109
+ def get_chat_messages(chat_id):
110
+ """
111
+ Return chat messages for given chat ID.
112
+ """
113
+ result = []
114
+ messages = (
115
+ ChatMessage.query.filter(ChatMessage.chat_id == chat_id)
116
+ .order_by(ChatMessage.created_at)
117
+ .all()
118
+ )
119
+ for message in messages:
120
+ serialized_message = message.serialize()
121
+ serialized_message["attachment_files"] = []
122
+ for attachment_file in message.attachment_files:
123
+ serialized_message["attachment_files"].append(
124
+ {
125
+ "id": attachment_file.id,
126
+ "name": attachment_file.name,
127
+ "extension": attachment_file.extension,
128
+ }
129
+ )
130
+ result.append(serialized_message)
131
+ return result
132
+
133
+
134
+ def get_chat_messages_for_entity(entity_id):
135
+ """
136
+ Return chat messages for given entity ID.
137
+ """
138
+ chat = get_chat_raw(entity_id)
139
+ return get_chat_messages(chat.id)
140
+
141
+
142
+ def create_chat_message(chat_id, person_id, message, files=None):
143
+ """
144
+ Create a new chat message.
145
+ """
146
+ chat = Chat.get(chat_id)
147
+ message = ChatMessage.create(
148
+ chat_id=chat_id, person_id=person_id, text=message
149
+ )
150
+ chat.update({"last_message": message.created_at})
151
+ serialized_message = message.serialize()
152
+ if files:
153
+ _add_attachments_to_message(serialized_message, files)
154
+ events.emit(
155
+ "chat:new-message",
156
+ data={
157
+ "chat_id": chat_id,
158
+ "chat_message_id": serialized_message["id"],
159
+ "last_message": serialized_message["created_at"],
160
+ },
161
+ persist=False,
162
+ )
163
+ return serialized_message
164
+
165
+
166
+ def delete_chat_message(chat_message_id):
167
+ """
168
+ Delete chat message.
169
+ """
170
+ message = get_chat_message_raw(chat_message_id)
171
+
172
+ for attachment in message.attachment_files:
173
+ attachment_file_id = str(attachment.id)
174
+ file_store.remove_file("attachments", attachment_file_id)
175
+ file_store.remove_picture("thumbnails", attachment_file_id)
176
+ attachment.delete()
177
+
178
+ message.delete()
179
+ events.emit(
180
+ "chat:deleted-message",
181
+ data={
182
+ "chat_id": str(message.chat_id),
183
+ "chat_message_id": chat_message_id,
184
+ },
185
+ persist=False,
186
+ )
187
+ clear_chat_message_cache(chat_message_id)
188
+ return message.serialize()
189
+
190
+
191
+ def build_participant_filter(person_id):
192
+ """
193
+ Query filter for returning chats for current user.
194
+ """
195
+ person = persons_service.get_person_raw(person_id)
196
+ return Chat.participants.contains(person)
197
+
198
+
199
+ def get_chats_for_person(person_id):
200
+ """
201
+ Return chats for current user.
202
+ """
203
+ chats = (
204
+ Chat.query.join(Entity, Chat.object_id == Entity.id)
205
+ .join(Project, Entity.project_id == Project.id)
206
+ .join(ProjectStatus, ProjectStatus.id == Project.project_status_id)
207
+ .add_columns(Entity.project_id, Entity.preview_file_id)
208
+ .filter(build_participant_filter(person_id))
209
+ .filter(ProjectStatus.name == "Open")
210
+ .all()
211
+ )
212
+
213
+ result = []
214
+ for chat_model, project_id, preview_file_id in chats:
215
+ chat = chat_model.present()
216
+ chat["entity_name"], _, _ = names_service.get_full_entity_name(
217
+ chat["object_id"]
218
+ )
219
+ chat["project_id"] = project_id
220
+ chat["preview_file_id"] = preview_file_id
221
+ result.append(chat)
222
+ return result
223
+
224
+
225
+ def _add_attachments_to_message(message, files):
226
+ """
227
+ Create an attachment entry and for each given uploaded files and tie it
228
+ to given message.
229
+ """
230
+ message["attachment_files"] = []
231
+ for uploaded_file in files.values():
232
+ try:
233
+ attachment_file = _create_attachment(message, uploaded_file)
234
+ message["attachment_files"].append(attachment_file)
235
+ except IntegrityError:
236
+ attachment_file = _create_attachment(
237
+ message, uploaded_file, randomize=True
238
+ )
239
+ message["attachment_files"].append(attachment_file)
240
+ return message
241
+
242
+
243
+ def _create_attachment(message, uploaded_file, randomize=False):
244
+ tmp_folder = current_app.config["TMP_DIR"]
245
+
246
+ # Prepare file name and create db entry
247
+ filename = uploaded_file.filename
248
+ mimetype = uploaded_file.mimetype
249
+ extension = fs.get_file_extension(filename)
250
+ if randomize:
251
+ letters = string.ascii_lowercase
252
+ random_str = "".join(random.choice(letters) for i in range(8))
253
+ filename = f"{filename[:len(filename) - len(extension) - 1]}"
254
+ filename += f"-{random_str}.{extension}"
255
+ attachment_file = AttachmentFile.create(
256
+ name=filename,
257
+ size=0,
258
+ extension=extension,
259
+ mimetype=mimetype,
260
+ chat_message_id=message["id"],
261
+ )
262
+
263
+ # Store attachment file
264
+ attachment_file_id = str(attachment_file.id)
265
+ tmp_file_path = fs.save_file(tmp_folder, attachment_file_id, uploaded_file)
266
+ size = fs.get_file_size(tmp_file_path)
267
+ attachment_file.update({"size": size})
268
+ file_store.add_file("attachments", attachment_file_id, tmp_file_path)
269
+
270
+ # Create thumbnail for pictures
271
+ if "png" in mimetype or "jpg" in mimetype:
272
+ image_path = tmp_file_path
273
+ if "jpg" in mimetype:
274
+ image_path = thumbnail.convert_jpg_to_png(tmp_file_path)
275
+ thumbnail.resize(image_path, (150, 150), True)
276
+ file_store.add_picture("thumbnails", attachment_file_id, image_path)
277
+ fs.rm_file(image_path)
278
+ fs.rm_file(tmp_file_path)
279
+ return attachment_file.present()
@@ -26,6 +26,7 @@ from zou.app.services import (
26
26
  projects_service,
27
27
  tasks_service,
28
28
  concepts_service,
29
+ preview_files_service,
29
30
  )
30
31
  from zou.app.services.exception import (
31
32
  AttachmentFileNotFoundException,
@@ -33,7 +34,7 @@ from zou.app.services.exception import (
33
34
  AssetNotFoundException,
34
35
  )
35
36
 
36
- from zou.app.utils import cache, date_helpers, events, fs, fields
37
+ from zou.app.utils import cache, date_helpers, events, fs, fields, date_helpers
37
38
  from zou.app.stores import file_store
38
39
  from zou.app import config
39
40
 
@@ -69,12 +70,19 @@ def get_attachment_file_path(attachment_file):
69
70
 
70
71
 
71
72
  def create_comment(
72
- person_id, task_id, task_status_id, text, checklist, files, created_at
73
+ person_id,
74
+ task_id,
75
+ task_status_id,
76
+ text,
77
+ checklist=[],
78
+ files={},
79
+ created_at="",
80
+ links=[],
73
81
  ):
74
82
  """
75
83
  Create a new comment and related: news, notifications and events.
76
84
  """
77
- task = tasks_service.get_task_with_relations(task_id)
85
+ task = tasks_service.get_task(task_id, relations=True)
78
86
  task_status = tasks_service.get_task_status(task_status_id)
79
87
  author = _get_comment_author(person_id)
80
88
  _check_retake_capping(task_status, task)
@@ -87,6 +95,7 @@ def create_comment(
87
95
  text=text,
88
96
  checklist=checklist,
89
97
  created_at=created_at,
98
+ links=links,
90
99
  )
91
100
  task, status_changed = _manage_status_change(task_status, task, comment)
92
101
  _manage_subscriptions(task, comment, status_changed)
@@ -127,41 +136,51 @@ def _get_comment_author(person_id):
127
136
 
128
137
 
129
138
  def _manage_status_change(task_status, task, comment):
130
- status_changed = task_status["id"] != task["task_status_id"]
131
- new_data = {
132
- "task_status_id": task_status["id"],
133
- "last_comment_date": comment["created_at"],
134
- }
135
- if status_changed:
136
- if task_status["is_retake"]:
137
- retake_count = task["retake_count"]
138
- if retake_count is None or retake_count == "NoneType":
139
- retake_count = 0
140
- new_data["retake_count"] = retake_count + 1
141
-
142
- if task_status["is_feedback_request"]:
143
- new_data["end_date"] = datetime.datetime.utcnow()
144
-
145
- if (
146
- task_status["short_name"] == "wip"
147
- and task["real_start_date"] is None
148
- ):
149
- new_data["real_start_date"] = datetime.datetime.utcnow()
150
-
151
- tasks_service.update_task(task["id"], new_data)
152
-
153
- if status_changed:
154
- events.emit(
155
- "task:status-changed",
156
- {
157
- "task_id": task["id"],
158
- "new_task_status_id": new_data["task_status_id"],
159
- "previous_task_status_id": task["task_status_id"],
160
- "person_id": comment["person_id"],
161
- },
162
- project_id=task["project_id"],
163
- )
164
- task.update(new_data)
139
+ is_last_comment = (
140
+ task["last_comment_date"] is None
141
+ or task["last_comment_date"] <= comment["created_at"]
142
+ )
143
+ if not is_last_comment:
144
+ status_changed = False
145
+ task = tasks_service.reset_task_data(task["id"])
146
+ else:
147
+ status_changed = task_status["id"] != task["task_status_id"]
148
+ new_data = {
149
+ "task_status_id": task_status["id"],
150
+ "last_comment_date": comment["created_at"],
151
+ }
152
+ if status_changed:
153
+ if task_status["is_retake"]:
154
+ retake_count = task["retake_count"]
155
+ if retake_count is None or retake_count == "NoneType":
156
+ retake_count = 0
157
+ new_data["retake_count"] = retake_count + 1
158
+
159
+ if task_status["is_feedback_request"]:
160
+ new_data["end_date"] = date_helpers.get_utc_now_datetime()
161
+
162
+ if (
163
+ task_status["short_name"] == "wip"
164
+ and task["real_start_date"] is None
165
+ ):
166
+ new_data["real_start_date"] = datetime.datetime.now(
167
+ datetime.timezone.utc
168
+ )
169
+
170
+ tasks_service.update_task(task["id"], new_data)
171
+
172
+ if status_changed:
173
+ events.emit(
174
+ "task:status-changed",
175
+ {
176
+ "task_id": task["id"],
177
+ "new_task_status_id": new_data["task_status_id"],
178
+ "previous_task_status_id": task["task_status_id"],
179
+ "person_id": comment["person_id"],
180
+ },
181
+ project_id=task["project_id"],
182
+ )
183
+ task.update(new_data)
165
184
  return task, status_changed
166
185
 
167
186
 
@@ -182,12 +201,11 @@ def _manage_subscriptions(task, comment, status_changed):
182
201
 
183
202
 
184
203
  def _run_status_automation(automation, task, person_id):
185
- is_automation_to_run = (
186
- not automation["archived"]
187
- and task["task_type_id"] == automation["in_task_type_id"]
188
- and task["task_status_id"] == automation["in_task_status_id"]
189
- )
190
- if not is_automation_to_run:
204
+ if (
205
+ automation["archived"]
206
+ or task["task_type_id"] != automation["in_task_type_id"]
207
+ or task["task_status_id"] != automation["in_task_status_id"]
208
+ ):
191
209
  return
192
210
 
193
211
  priorities = projects_service.get_task_type_priority_map(
@@ -215,7 +233,7 @@ def _run_status_automation(automation, task, person_id):
215
233
  task_status = tasks_service.get_task_status(
216
234
  automation["in_task_status_id"]
217
235
  )
218
- create_comment(
236
+ new_comment = create_comment(
219
237
  person_id,
220
238
  task_to_update["id"],
221
239
  automation["out_task_status_id"],
@@ -228,6 +246,31 @@ def _run_status_automation(automation, task, person_id):
228
246
  {},
229
247
  None,
230
248
  )
249
+ if automation["import_last_revision"]:
250
+ preview_file = (
251
+ preview_files_service.get_last_preview_file_for_task(
252
+ task["id"]
253
+ )
254
+ )
255
+ preview_files = (
256
+ preview_files_service.get_preview_files_for_revision(
257
+ preview_file["task_id"], preview_file["revision"]
258
+ )
259
+ )
260
+
261
+ for preview_file in preview_files:
262
+ new_preview_file = (
263
+ tasks_service.add_preview_file_to_comment(
264
+ new_comment["id"],
265
+ new_comment["person_id"],
266
+ task_to_update["id"],
267
+ )
268
+ )
269
+
270
+ preview_files_service.copy_preview_file_in_another_one(
271
+ preview_file["id"], new_preview_file["id"]
272
+ )
273
+
231
274
  elif automation["out_field_type"] == "ready_for":
232
275
  try:
233
276
  asset = assets_service.update_asset(
@@ -248,6 +291,7 @@ def new_comment(
248
291
  files={},
249
292
  checklist=[],
250
293
  created_at="",
294
+ links=[],
251
295
  ):
252
296
  """
253
297
  Create a new comment for given object (by default, it considers this object
@@ -262,8 +306,8 @@ def new_comment(
262
306
  )
263
307
  except ValueError:
264
308
  try:
265
- created_at_date = fields.get_date_object(
266
- created_at, date_format="%Y-%m-%dT%H:%M:%S"
309
+ created_at_date = date_helpers.get_datetime_from_string(
310
+ created_at
267
311
  )
268
312
  except ValueError:
269
313
  pass
@@ -278,6 +322,7 @@ def new_comment(
278
322
  checklist=checklist,
279
323
  text=text,
280
324
  created_at=created_at_date,
325
+ links=links,
281
326
  )
282
327
 
283
328
  comment = comment.serialize(relations=True)
@@ -290,24 +335,6 @@ def new_comment(
290
335
  return comment
291
336
 
292
337
 
293
- def add_attachments_to_comment(comment, files):
294
- """
295
- Create an attachment entry and for each given uploaded files and tie it
296
- to given comment.
297
- """
298
- comment["attachment_files"] = []
299
- for uploaded_file in files.values():
300
- try:
301
- attachment_file = create_attachment(comment, uploaded_file)
302
- comment["attachment_files"].append(attachment_file)
303
- except IntegrityError:
304
- attachment_file = create_attachment(
305
- comment, uploaded_file, randomize=True
306
- )
307
- comment["attachment_files"].append(attachment_file)
308
- return comment
309
-
310
-
311
338
  def reset_mentions(comment):
312
339
  task = tasks_service.get_task(comment["object_id"])
313
340
  mentions = get_comment_mentions(task["project_id"], comment["text"])
@@ -536,3 +563,21 @@ def get_comment_department_mention_ids(project_id, text):
536
563
  str(mention.id)
537
564
  for mention in get_comment_department_mentions(project_id, text)
538
565
  ]
566
+
567
+
568
+ def add_attachments_to_comment(comment, files):
569
+ """
570
+ Create an attachment entry and for each given uploaded files and tie it
571
+ to given comment.
572
+ """
573
+ comment["attachment_files"] = []
574
+ for uploaded_file in files.values():
575
+ try:
576
+ attachment_file = create_attachment(comment, uploaded_file)
577
+ comment["attachment_files"].append(attachment_file)
578
+ except IntegrityError:
579
+ attachment_file = create_attachment(
580
+ comment, uploaded_file, randomize=True
581
+ )
582
+ comment["attachment_files"].append(attachment_file)
583
+ return comment
@@ -33,7 +33,7 @@ from zou.app.services.exception import (
33
33
 
34
34
  def clear_concept_cache(concept_id):
35
35
  cache.cache.delete_memoized(get_concept, concept_id)
36
- cache.cache.delete_memoized(get_concept_with_relations, concept_id)
36
+ cache.cache.delete_memoized(get_concept, concept_id, True)
37
37
  cache.cache.delete_memoized(get_full_concept, concept_id)
38
38
 
39
39
 
@@ -61,23 +61,15 @@ def get_concept_raw(concept_id):
61
61
 
62
62
 
63
63
  @cache.memoize_function(120)
64
- def get_concept_with_relations(concept_id):
64
+ def get_concept(concept_id, relations=False):
65
65
  """
66
66
  Return given concept as a dictionary.
67
67
  """
68
68
  return get_concept_raw(concept_id).serialize(
69
- obj_type="Concept", relations=True
69
+ obj_type="Concept", relations=relations
70
70
  )
71
71
 
72
72
 
73
- @cache.memoize_function(120)
74
- def get_concept(concept_id):
75
- """
76
- Return given concept as a dictionary.
77
- """
78
- return get_concept_raw(concept_id).serialize(obj_type="Concept")
79
-
80
-
81
73
  @cache.memoize_function(120)
82
74
  def get_full_concept(concept_id):
83
75
  """
@@ -86,7 +78,7 @@ def get_full_concept(concept_id):
86
78
  concepts = get_concepts_and_tasks({"id": concept_id})
87
79
  if len(concepts) > 0:
88
80
  concept = concepts[0]
89
- concept.update(get_concept_with_relations(concept_id))
81
+ concept.update(get_concept(concept_id, relations=True))
90
82
  return concept
91
83
  else:
92
84
  raise ConceptNotFoundException