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
@@ -9,6 +9,7 @@ try:
9
9
  host=config.KEY_VALUE_STORE["host"],
10
10
  port=config.KEY_VALUE_STORE["port"],
11
11
  db=config.AUTH_TOKEN_BLACKLIST_KV_INDEX,
12
+ password=config.KEY_VALUE_STORE["password"],
12
13
  decode_responses=True,
13
14
  )
14
15
  revoked_tokens_store.ping()
@@ -58,4 +59,4 @@ def is_revoked(jti):
58
59
  """
59
60
  Tell if a stored auth token is revoked or not.
60
61
  """
61
- return get(jti) in [None, "true"]
62
+ return get(jti) == "true"
@@ -104,6 +104,12 @@ def get_local_picture_path(prefix, id):
104
104
  return path(pictures, make_key(prefix, id))
105
105
 
106
106
 
107
+ def copy_picture(prefix, id, new_prefix, new_id):
108
+ key = make_key(prefix, id)
109
+ target = make_key(new_prefix, new_id)
110
+ return pictures.copy(key, target)
111
+
112
+
107
113
  def add_movie(prefix, id, path):
108
114
  key = make_key(prefix, id)
109
115
  with open(path, "rb") as fd:
@@ -139,6 +145,12 @@ def get_local_movie_path(prefix, id):
139
145
  return path(movies, make_key(prefix, id))
140
146
 
141
147
 
148
+ def copy_movie(prefix, id, new_prefix, new_id):
149
+ key = make_key(prefix, id)
150
+ target = make_key(new_prefix, new_id)
151
+ return movies.copy(key, target)
152
+
153
+
142
154
  def add_file(prefix, id, path):
143
155
  key = make_key(prefix, id)
144
156
  with open(path, "rb") as fd:
@@ -172,3 +184,9 @@ def remove_file(prefix, id):
172
184
 
173
185
  def get_local_file_path(prefix, id):
174
186
  return path(files, make_key(prefix, id))
187
+
188
+
189
+ def copy_file(prefix, id, new_prefix, new_id):
190
+ key = make_key(prefix, id)
191
+ target = make_key(new_prefix, new_id)
192
+ return files.copy(key, target)
@@ -3,11 +3,7 @@ import redis
3
3
  from flask_socketio import SocketIO
4
4
 
5
5
  from zou.app import config
6
-
7
- host = config.KEY_VALUE_STORE["host"]
8
- port = config.KEY_VALUE_STORE["port"]
9
- redis_db = config.KV_EVENTS_DB_INDEX
10
- redis_url = "redis://%s:%s/%s" % (host, port, redis_db)
6
+ from zou.app.utils.redis import get_redis_url
11
7
 
12
8
  socketio = None
13
9
 
@@ -27,11 +23,15 @@ def init():
27
23
 
28
24
  try:
29
25
  publisher_store = redis.StrictRedis(
30
- host=host, port=port, db=redis_db, decode_responses=True
26
+ host=config.KEY_VALUE_STORE["host"],
27
+ port=config.KEY_VALUE_STORE["port"],
28
+ db=config.KV_EVENTS_DB_INDEX,
29
+ password=config.KEY_VALUE_STORE["password"],
30
+ decode_responses=True,
31
31
  )
32
32
  publisher_store.get("test")
33
33
  socketio = SocketIO(
34
- message_queue=redis_url,
34
+ message_queue=get_redis_url(config.KV_EVENTS_DB_INDEX),
35
35
  cors_allowed_origins=[],
36
36
  cors_credentials=False,
37
37
  )
@@ -11,6 +11,7 @@ try:
11
11
  host=config.KEY_VALUE_STORE["host"],
12
12
  port=config.KEY_VALUE_STORE["port"],
13
13
  db=config.KV_JOB_DB_INDEX,
14
+ password=config.KEY_VALUE_STORE["password"],
14
15
  decode_responses=True,
15
16
  )
16
17
  queue_store.get("test")
zou/app/swagger.py CHANGED
@@ -14,6 +14,7 @@ swagger_config = {
14
14
  "static_url_path": "/docs",
15
15
  "swagger_ui": True,
16
16
  "specs_route": "/apidocs/",
17
+ "openapi": "3.0.2",
17
18
  }
18
19
 
19
20
 
@@ -30,26 +31,18 @@ An easy to use Python client to access this API is available:
30
31
 
31
32
  <p>Before you can use any of the endpoints outline below,
32
33
  you will have to get a JWT token to authorize your requests.
34
+ </p>
33
35
 
34
- You can get a authorization token using a (form-encoded) POST request to ```/auth/login```.
35
- With curl this would look something like ```curl -X POST <server_address>/auth/login -d "email=<youremail>&password=<yourpassword>```.
36
+ <p>
37
+ You will find the information to retrieve it in the
38
+ <a href="#tag/Authentication">Zou documentation</a>.
39
+ </p>
36
40
 
37
- The response is a JSON object, specifically you'll need to provide the ```access_token``` for your future requests.
38
-
39
- Here is a complete authentication process as an example (again using curl):
40
- ```
41
- $ curl -X POST <server_address>/api/auth/login -d "email=<youremail>&password=<yourpassword>"'
42
- {{"login": true", "access_token": "eyJ0e...", ...}}
43
- $ jwt=eyJ0e... # Store the access token for easier use
44
- $ curl -H "Authorization: Bearer $jwt" <server_address>/api/data/projects
45
- [{{...}},
46
- {{...}}]
47
- ```
48
41
  [OpenAPI definition](/openapi.json)
49
42
  """
50
43
 
51
44
  swagger_template = {
52
- "openapi": "3.1",
45
+ "openapi": "3.0.2",
53
46
  "info": {
54
47
  "title": "Kitsu API",
55
48
  "description": description,
@@ -70,12 +63,14 @@ swagger_template = {
70
63
  "host": "localhost:8080",
71
64
  "basePath": "/api",
72
65
  "schemes": ["http", "https"],
73
- "securityDefinitions": {
74
- "JWT Authorization": {
75
- "name": "Authorization",
76
- "in": "header",
77
- "type": "apiKey",
78
- "description": "Format in header: **Authorization: Bearer {token}**. \n\n Value example: Bearer xxxxx.yyyyy.zzzzz",
66
+ "components": {
67
+ "securitySchemes": {
68
+ "JWT Authorization": {
69
+ "name": "Authorization",
70
+ "in": "header",
71
+ "type": "apiKey",
72
+ "description": "Format in header: **Authorization: Bearer {token}**. \n\n Value example: Bearer xxxxx.yyyyy.zzzzz",
73
+ }
79
74
  }
80
75
  },
81
76
  "security": [{"JWT Authorization": []}],
@@ -345,6 +340,10 @@ swagger_template = {
345
340
  "type": "string",
346
341
  "description": "Color of department",
347
342
  },
343
+ "archived": {
344
+ "type": "boolean",
345
+ "description": "True if the department is archived else False",
346
+ },
348
347
  },
349
348
  },
350
349
  "DesktopLoginLog": {
@@ -1189,6 +1188,23 @@ swagger_template = {
1189
1188
  },
1190
1189
  },
1191
1190
  },
1191
+ "Studio": {
1192
+ "type": "object",
1193
+ "properties": {
1194
+ "name": {
1195
+ "type": "string",
1196
+ "description": "Name of studio",
1197
+ },
1198
+ "color": {
1199
+ "type": "string",
1200
+ "description": "Color of studio",
1201
+ },
1202
+ "archived": {
1203
+ "type": "boolean",
1204
+ "description": "True if the studio is archived else False",
1205
+ },
1206
+ },
1207
+ },
1192
1208
  "Task": {
1193
1209
  "type": "object",
1194
1210
  "properties": {
zou/app/utils/cache.py CHANGED
@@ -17,6 +17,7 @@ try:
17
17
  host=config.KEY_VALUE_STORE["host"],
18
18
  port=config.KEY_VALUE_STORE["port"],
19
19
  db=config.MEMOIZE_DB_INDEX,
20
+ password=config.KEY_VALUE_STORE["password"],
20
21
  decode_responses=True,
21
22
  )
22
23
  redis_cache.get("test")
@@ -26,6 +27,7 @@ try:
26
27
  "CACHE_REDIS_HOST": config.KEY_VALUE_STORE["host"],
27
28
  "CACHE_REDIS_PORT": config.KEY_VALUE_STORE["port"],
28
29
  "CACHE_REDIS_DB": config.MEMOIZE_DB_INDEX,
30
+ "CACHE_REDIS_PASSWORD": config.KEY_VALUE_STORE["password"],
29
31
  }
30
32
  )
31
33
 
zou/app/utils/commands.py CHANGED
@@ -3,11 +3,13 @@
3
3
  import os
4
4
  import datetime
5
5
  import tempfile
6
+ import sys
7
+ import shutil
6
8
 
7
9
 
8
10
  from ldap3 import Server, Connection, ALL, NTLM, SIMPLE
9
11
  from zou.app.utils import thumbnail as thumbnail_utils, auth
10
- from zou.app.stores import auth_tokens_store, file_store
12
+ from zou.app.stores import auth_tokens_store, file_store, queue_store
11
13
  from zou.app.services import (
12
14
  assets_service,
13
15
  backup_service,
@@ -23,6 +25,8 @@ from zou.app.services import (
23
25
  tasks_service,
24
26
  )
25
27
  from zou.app.models.person import Person
28
+ from zou.app.models.preview_file import PreviewFile
29
+ from zou.app.models.task import Task
26
30
  from sqlalchemy.sql.expression import not_
27
31
 
28
32
  from zou.app.services.exception import (
@@ -390,7 +394,9 @@ def sync_with_ldap_server():
390
394
  )
391
395
  .all()
392
396
  ):
393
- persons_service.update_person(person.id, {"active": False})
397
+ persons_service.update_person(
398
+ person.id, {"active": False}, bypass_protected_accounts=True
399
+ )
394
400
  print(
395
401
  "User %s disabled (not found in LDAP)." % person.desktop_login
396
402
  )
@@ -414,6 +420,7 @@ def sync_with_ldap_server():
414
420
  "desktop_login": user["desktop_login"],
415
421
  "ldap_uid": user["ldap_uid"],
416
422
  },
423
+ bypass_protected_accounts=True,
417
424
  )
418
425
  print(f"User {user['desktop_login']} updated.")
419
426
  except IsUserLimitReachedException:
@@ -466,7 +473,9 @@ def sync_with_ldap_server():
466
473
  )
467
474
  file_store.add_picture("thumbnails", person["id"], thumbnail_png_path)
468
475
  os.remove(thumbnail_png_path)
469
- persons_service.update_person(person["id"], {"has_avatar": True})
476
+ persons_service.update_person(
477
+ person["id"], {"has_avatar": True}, bypass_protected_accounts=True
478
+ )
470
479
 
471
480
  ldap_users = get_ldap_users()
472
481
  update_person_list_with_ldap_users(ldap_users)
@@ -529,7 +538,7 @@ def run_sync_file_change_daemon(
529
538
 
530
539
 
531
540
  def import_last_changes_from_another_instance(
532
- source, login, password, minutes=0, page_size=300
541
+ source, login, password, minutes=0, limit=300
533
542
  ):
534
543
  """
535
544
  Retrieve and save all the data related to most recent events from another
@@ -538,12 +547,12 @@ def import_last_changes_from_another_instance(
538
547
  with app.app_context():
539
548
  sync_service.init(source, login, password)
540
549
  print("Last events syncing started.")
541
- sync_service.run_last_events_sync(minutes=minutes, page_size=300)
550
+ sync_service.run_last_events_sync(minutes=minutes, limit=300)
542
551
  print("Last events syncing ended.")
543
552
 
544
553
 
545
554
  def import_last_file_changes_from_another_instance(
546
- source, login, password, minutes=20, page_size=50, force=False
555
+ source, login, password, minutes=20, limit=50, force=False
547
556
  ):
548
557
  """
549
558
  Retrieve and save all the data related most to recent file events
@@ -553,7 +562,7 @@ def import_last_file_changes_from_another_instance(
553
562
  with app.app_context():
554
563
  sync_service.init(source, login, password)
555
564
  print("Last files syncing started.")
556
- sync_service.run_last_events_files(minutes=minutes, page_size=50)
565
+ sync_service.run_last_events_files(minutes=minutes, limit=50)
557
566
  print("Last files syncing ended.")
558
567
 
559
568
 
@@ -717,3 +726,91 @@ def create_bot(
717
726
  is_bot=True,
718
727
  )
719
728
  print(bot["access_token"])
729
+
730
+
731
+ def renormalize_movie_preview_files(
732
+ preview_file_id=None, project_id=None, all_broken=None, all_processing=None
733
+ ):
734
+ with app.app_context():
735
+ if preview_file_id is None and not all_broken and not all_processing:
736
+ print("You must specify at least one option.")
737
+ sys.exit(1)
738
+ else:
739
+ query = PreviewFile.query.filter(
740
+ PreviewFile.extension == "mp4"
741
+ ).order_by(PreviewFile.created_at.asc())
742
+
743
+ if preview_file_id is not None:
744
+ query = query.filter(PreviewFile.id == preview_file_id)
745
+
746
+ if project_id is not None:
747
+ query = query.join(Task).filter(
748
+ PreviewFile.project_id == project_id
749
+ )
750
+
751
+ if all_broken and all_processing:
752
+ query = query.filter(
753
+ PreviewFile.status.in_(("broken", "processing"))
754
+ )
755
+ elif all_broken:
756
+ query = query.filter(PreviewFile.status == "broken")
757
+ elif all_processing:
758
+ query = query.filter(PreviewFile.status == "processing")
759
+
760
+ preview_files = query.all()
761
+ len_preview_files = len(preview_files)
762
+ if len_preview_files == 0:
763
+ print("No preview files found.")
764
+ sys.exit(1)
765
+ else:
766
+ for i, preview_file in enumerate(preview_files):
767
+ try:
768
+ preview_file_id = str(preview_file.id)
769
+ print(
770
+ f"Renormalizing preview file {preview_file_id} ({i+1}/{len_preview_files})."
771
+ )
772
+ extension = preview_file.extension
773
+ uploaded_movie_path = os.path.join(
774
+ config.TMP_DIR,
775
+ f"{preview_file_id}.{extension}.tmp",
776
+ )
777
+ try:
778
+ if config.FS_BACKEND == "local":
779
+ shutil.copyfile(
780
+ file_store.get_local_movie_path(
781
+ "source", preview_file_id
782
+ ),
783
+ uploaded_movie_path,
784
+ )
785
+ else:
786
+ sync_service.download_file(
787
+ uploaded_movie_path,
788
+ "source",
789
+ file_store.open_movie,
790
+ str(preview_file_id),
791
+ )
792
+ except:
793
+ pass
794
+ if config.ENABLE_JOB_QUEUE:
795
+ queue_store.job_queue.enqueue(
796
+ preview_files_service.prepare_and_store_movie,
797
+ args=(
798
+ preview_file_id,
799
+ uploaded_movie_path,
800
+ True,
801
+ False,
802
+ ),
803
+ job_timeout=int(config.JOB_QUEUE_TIMEOUT),
804
+ )
805
+ else:
806
+ preview_files_service.prepare_and_store_movie(
807
+ preview_file_id,
808
+ uploaded_movie_path,
809
+ normalize=True,
810
+ add_source_to_file_store=False,
811
+ )
812
+ except Exception as e:
813
+ print(
814
+ f"Renormalization of preview file {preview_file_id} failed: {e}"
815
+ )
816
+ continue
@@ -1,9 +1,6 @@
1
- try:
2
- from StringIO import StringIO
3
- except ImportError:
4
- from io import StringIO
5
1
  import csv
6
2
 
3
+ from io import StringIO
7
4
  from flask import make_response
8
5
  from slugify import slugify
9
6
 
@@ -1,17 +1,21 @@
1
1
  import isoweek
2
+ import datetime
2
3
 
3
4
  from babel.dates import format_datetime
4
- from datetime import date, datetime, timedelta
5
5
  from dateutil import relativedelta
6
6
  from zou.app.services.exception import WrongDateFormatException
7
7
 
8
8
 
9
9
  def get_now():
10
- return get_string_with_timezone_from_date(datetime.utcnow(), "UTC")
10
+ return get_string_with_timezone_from_date(get_utc_now_datetime(), "UTC")
11
+
12
+
13
+ def get_utc_now_datetime():
14
+ return datetime.datetime.now(tz=datetime.timezone.utc).replace(tzinfo=None)
11
15
 
12
16
 
13
17
  def get_date_from_now(nb_days):
14
- return date.today() - timedelta(days=nb_days)
18
+ return datetime.date.today() - datetime.timedelta(days=nb_days)
15
19
 
16
20
 
17
21
  def get_date_diff(date_a, date_b):
@@ -29,7 +33,7 @@ def get_date_string_with_timezone(date_string, timezone):
29
33
  """
30
34
  Apply given timezone to given date and return it as a string.
31
35
  """
32
- date_obj = datetime.strptime(date_string, "%Y-%m-%dT%H:%M:%S")
36
+ date_obj = datetime.datetime.strptime(date_string, "%Y-%m-%dT%H:%M:%S")
33
37
  return format_datetime(date_obj, "yyyy-MM-ddTHH:mm:ss", tzinfo=timezone)
34
38
 
35
39
 
@@ -51,14 +55,16 @@ def get_today_string_with_timezone(timezone):
51
55
  """
52
56
  Get today date in string format with timezone applied.
53
57
  """
54
- return get_simple_string_with_timezone_from_date(date.today(), timezone)
58
+ return get_simple_string_with_timezone_from_date(
59
+ datetime.date.today(), timezone
60
+ )
55
61
 
56
62
 
57
63
  def get_date_from_string(date_str):
58
64
  """
59
65
  Parse a date string and returns a date object.
60
66
  """
61
- return datetime.strptime(date_str, "%Y-%m-%d")
67
+ return datetime.datetime.strptime(date_str, "%Y-%m-%d")
62
68
 
63
69
 
64
70
  def get_datetime_from_string(date_str, milliseconds=False):
@@ -66,9 +72,9 @@ def get_datetime_from_string(date_str, milliseconds=False):
66
72
  Parse a datetime string and returns a datetime object.
67
73
  """
68
74
  if milliseconds:
69
- return datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S.%f")
75
+ return datetime.datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S.%f")
70
76
  else:
71
- return datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S")
77
+ return datetime.datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S")
72
78
 
73
79
 
74
80
  def get_year_interval(year):
@@ -76,10 +82,10 @@ def get_year_interval(year):
76
82
  Get a tuple containing start date and end date for given year.
77
83
  """
78
84
  year = int(year)
79
- if year > datetime.utcnow().year or year < 2010:
85
+ if year > get_utc_now_datetime().year or year < 2010:
80
86
  raise WrongDateFormatException
81
87
 
82
- start = datetime(year, 1, 1)
88
+ start = datetime.datetime(year, 1, 1)
83
89
  end = start + relativedelta.relativedelta(years=1)
84
90
  return start, end
85
91
 
@@ -90,10 +96,15 @@ def get_month_interval(year, month):
90
96
  """
91
97
  year = int(year)
92
98
  month = int(month)
93
- if year > datetime.utcnow().year or year < 2010 or month < 1 or month > 12:
99
+ if (
100
+ year > get_utc_now_datetime().year
101
+ or year < 2010
102
+ or month < 1
103
+ or month > 12
104
+ ):
94
105
  raise WrongDateFormatException
95
106
 
96
- start = datetime(year, month, 1)
107
+ start = datetime.datetime(year, month, 1)
97
108
  end = start + relativedelta.relativedelta(months=1)
98
109
  return start, end
99
110
 
@@ -104,7 +115,12 @@ def get_week_interval(year, week):
104
115
  """
105
116
  year = int(year)
106
117
  week = int(week)
107
- if year > datetime.utcnow().year or year < 2010 or week < 1 or week > 52:
118
+ if (
119
+ year > get_utc_now_datetime().year
120
+ or year < 2010
121
+ or week < 1
122
+ or week > 52
123
+ ):
108
124
  raise WrongDateFormatException
109
125
  start = isoweek.Week(year, week).monday()
110
126
  end = start + relativedelta.relativedelta(days=7)
@@ -119,7 +135,7 @@ def get_day_interval(year, month, day):
119
135
  month = int(month)
120
136
  day = int(day)
121
137
  if (
122
- year > datetime.utcnow().year
138
+ year > get_utc_now_datetime().year
123
139
  or year < 2010
124
140
  or month < 1
125
141
  or month > 12
@@ -127,7 +143,7 @@ def get_day_interval(year, month, day):
127
143
  or day > 31
128
144
  ):
129
145
  raise WrongDateFormatException
130
- start = datetime(year, month, day)
146
+ start = datetime.datetime(year, month, day)
131
147
  end = start + relativedelta.relativedelta(days=1)
132
148
  return start, end
133
149
 
@@ -147,14 +163,14 @@ def get_business_days(start, end):
147
163
  Returns the number of business days between two dates.
148
164
  """
149
165
  daygenerator = (
150
- start + timedelta(x + 1) for x in range((end - start).days)
166
+ start + datetime.timedelta(x + 1) for x in range((end - start).days)
151
167
  )
152
168
  return sum(1 for day in daygenerator if day.weekday() < 5)
153
169
 
154
170
 
155
171
  def add_business_days_to_date(date, nb_days):
156
172
  while nb_days > 0:
157
- date += timedelta(days=1)
173
+ date += datetime.timedelta(days=1)
158
174
  if date.weekday() < 5:
159
175
  nb_days -= 1
160
176
  return date
@@ -1,4 +1,4 @@
1
- from sqlalchemy import create_engine
1
+ from sqlalchemy import create_engine, inspect
2
2
  from sqlalchemy_utils import database_exists, create_database
3
3
  from sqlalchemy.engine.url import URL
4
4
  from sqlalchemy.orm import close_all_sessions
@@ -39,3 +39,16 @@ def drop_all():
39
39
  db.session.flush()
40
40
  close_all_sessions()
41
41
  return db.drop_all()
42
+
43
+
44
+ def is_init():
45
+ """
46
+ Check if database is initialized.
47
+ """
48
+ from zou.app import db
49
+ from zou.app.models.project_status import ProjectStatus
50
+
51
+ return (
52
+ inspect(db.engine).has_table("person")
53
+ and db.session.query(ProjectStatus).count() == 2
54
+ )
zou/app/utils/emails.py CHANGED
@@ -12,9 +12,9 @@ def send_email(subject, html, recipient_email, body=None):
12
12
  """
13
13
  if body is None:
14
14
  body = strip_html_tags(html)
15
- if app.config["MAIL_DEBUG"]:
15
+ if app.config["MAIL_DEBUG_BODY"]:
16
16
  print(body)
17
- elif app.config["MAIL_ENABLED"]:
17
+ if app.config["MAIL_ENABLED"]:
18
18
  with app.app_context():
19
19
  try:
20
20
  mail_default_sender = app.config["MAIL_DEFAULT_SENDER"]
zou/app/utils/fido.py ADDED
@@ -0,0 +1,22 @@
1
+ import fido2.features
2
+
3
+ from fido2.webauthn import (
4
+ PublicKeyCredentialRpEntity,
5
+ )
6
+ from fido2.server import Fido2Server
7
+ from urllib.parse import urlparse
8
+
9
+ from zou.app import config
10
+
11
+ fido2.features.webauthn_json_mapping.enabled = True
12
+
13
+
14
+ def get_fido_server():
15
+ return Fido2Server(
16
+ PublicKeyCredentialRpEntity(
17
+ name="Kitsu", id=urlparse(f"https://{config.DOMAIN_NAME}").hostname
18
+ ),
19
+ verify_origin=(
20
+ None if config.DOMAIN_NAME != "localhost:8080" else lambda a: True
21
+ ),
22
+ )
zou/app/utils/flask.py CHANGED
@@ -57,6 +57,7 @@ def is_from_browser(user_agent):
57
57
  "Brave",
58
58
  "Chrome",
59
59
  "Chrome Mobile",
60
+ "Chrome Mobile iOS",
60
61
  "Edge",
61
62
  "Firefox",
62
63
  "Mobile Safari",
zou/app/utils/query.py CHANGED
@@ -1,8 +1,11 @@
1
1
  import math
2
+ import orjson as json
3
+ import sqlalchemy.orm as orm
2
4
 
3
5
  from zou.app import app
4
6
  from zou.app.utils import fields, string
5
7
  from sqlalchemy import func
8
+ from sqlalchemy.inspection import inspect
6
9
 
7
10
 
8
11
  def get_query_criterions_from_request(request):
@@ -21,12 +24,57 @@ def apply_criterions_to_db_query(model, db_query, criterions):
21
24
  """
22
25
  Apply criterions given in HTTP request to the sqlachemy db query object.
23
26
  """
24
- if "name" in criterions and hasattr(model, "name"):
25
- value = criterions["name"]
26
- db_query = db_query.filter(model.name.ilike(value))
27
- del criterions["name"]
28
27
 
29
- return db_query.filter_by(**criterions)
28
+ many_join_filter = []
29
+ in_filter = []
30
+ name_filter = []
31
+ filters = {}
32
+
33
+ column_names = inspect(model).all_orm_descriptors.keys()
34
+ for key, value in criterions.items():
35
+ if key not in ["page", "relations"] and key in column_names:
36
+ field_key = getattr(model, key)
37
+
38
+ is_many_to_many_field = hasattr(
39
+ field_key, "property"
40
+ ) and isinstance(
41
+ field_key.property, orm.properties.RelationshipProperty
42
+ )
43
+ value_is_list = (
44
+ hasattr(value, "__len__")
45
+ and len(value) > 0
46
+ and value[0] == "["
47
+ )
48
+
49
+ if key == "name" and field_key is not None:
50
+ name_filter.append(value)
51
+
52
+ elif is_many_to_many_field:
53
+ many_join_filter.append((key, value))
54
+
55
+ elif value_is_list:
56
+ value_array = json.loads(value)
57
+ in_filter.append(
58
+ field_key.in_(
59
+ [cast_value(value, field_key) for value in value_array]
60
+ )
61
+ )
62
+ else:
63
+ filters[key] = cast_value(value, field_key)
64
+
65
+ if filters:
66
+ db_query = db_query.filter_by(**filters)
67
+
68
+ for value in name_filter:
69
+ db_query = db_query.filter(model.name.ilike(value))
70
+
71
+ for id_filter in in_filter:
72
+ db_query = db_query.filter(id_filter)
73
+
74
+ for key, value in many_join_filter:
75
+ db_query = db_query.filter(getattr(model, key).any(id=value))
76
+
77
+ return db_query
30
78
 
31
79
 
32
80
  def get_paginated_results(query, page, limit=None, relations=False):
@@ -79,7 +127,7 @@ def get_cursor_results(
79
127
  total = query.count()
80
128
  query = (
81
129
  query.filter(model.created_at > cursor_created_at)
82
- .order_by(model.created_at)
130
+ .order_by(model.created_at, model.updated_at, model.id)
83
131
  .limit(limit)
84
132
  )
85
133
  models = fields.serialize_models(