zou 0.19.15__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 (164) 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} +82 -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/query.py +54 -6
  130. zou/app/utils/redis.py +11 -0
  131. zou/app/utils/saml.py +51 -0
  132. zou/app/utils/string.py +2 -0
  133. zou/app/utils/thumbnail.py +4 -2
  134. zou/cli.py +76 -18
  135. zou/debug.py +4 -2
  136. zou/event_stream.py +122 -165
  137. zou/job_settings.py +1 -0
  138. zou/migrations/env.py +0 -0
  139. zou/migrations/utils/base.py +6 -6
  140. zou/migrations/versions/1bb55759146f_add_table_studio.py +67 -0
  141. zou/migrations/versions/1fab8c420678_add_attachments_to_message_chats.py +56 -0
  142. zou/migrations/versions/23122f290ca2_add_entity_chat_models.py +149 -0
  143. zou/migrations/versions/32f134ff1201_add_is_shared_flag_to_filters.py +33 -0
  144. zou/migrations/versions/57222395f2be_add_statusautomation_import_last_revision.py +41 -0
  145. zou/migrations/versions/59a7445a966c_add_entity_is_shared.py +41 -0
  146. zou/migrations/versions/5b980f0dc365_add_comment_links.py +35 -0
  147. zou/migrations/versions/680c64565f9d_for_searchfiltergroup_is_shared.py +35 -0
  148. zou/migrations/versions/8e67c183bed7_add_preference_fields.py +71 -0
  149. zou/migrations/versions/92b40d79ad3f_allow_message_attachments.py +38 -0
  150. zou/migrations/versions/971dbf5a0faf_add_short_name_for_asset_type_entity_.py +33 -0
  151. zou/migrations/versions/9b85c14fa8a7_add_day_off_new_columns.py +68 -0
  152. zou/migrations/versions/9d3bb33c6fc6_add_department_keys_to_filter_models.py +73 -0
  153. zou/migrations/versions/a252a094e977_add_descriptions_for_entities_tasks_and_.py +40 -0
  154. zou/migrations/versions/be56dc0fb760_for_is_shared_disallow_nullable.py +102 -0
  155. zou/migrations/versions/ca28796a2a62_add_is_done_field_to_the_task_model.py +108 -0
  156. zou/migrations/versions/f344b867a911_for_description_of_entity_task_working_.py +75 -0
  157. zou/remote/config_payload.py +2 -1
  158. zou/utils/movie.py +14 -4
  159. {zou-0.19.15.dist-info → zou-0.20.11.dist-info}/METADATA +75 -69
  160. {zou-0.19.15.dist-info → zou-0.20.11.dist-info}/RECORD +163 -134
  161. {zou-0.19.15.dist-info → zou-0.20.11.dist-info}/WHEEL +1 -1
  162. {zou-0.19.15.dist-info → zou-0.20.11.dist-info}/LICENSE +0 -0
  163. {zou-0.19.15.dist-info → zou-0.20.11.dist-info}/entry_points.txt +0 -0
  164. {zou-0.19.15.dist-info → zou-0.20.11.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,5 @@
1
1
  from sqlalchemy.orm import aliased
2
- from sqlalchemy import func
2
+ from sqlalchemy import func, or_
3
3
 
4
4
  from zou.app import config
5
5
  from zou.app.models.comment import Comment
@@ -9,6 +9,7 @@ from zou.app.models.notification import Notification
9
9
  from zou.app.models.person import Person
10
10
  from zou.app.models.project import Project
11
11
  from zou.app.models.project_status import ProjectStatus
12
+ from zou.app.models.subscription import Subscription
12
13
  from zou.app.models.search_filter import SearchFilter
13
14
  from zou.app.models.search_filter_group import SearchFilterGroup
14
15
  from zou.app.models.task import Task
@@ -32,16 +33,23 @@ from zou.app.services.exception import (
32
33
  SearchFilterNotFoundException,
33
34
  SearchFilterGroupNotFoundException,
34
35
  NotificationNotFoundException,
36
+ WrongParameterException,
35
37
  )
36
38
  from zou.app.utils import cache, fields, permissions
37
39
 
38
40
 
39
- def clear_filter_cache(user_id):
40
- cache.cache.delete_memoized(get_user_filters, user_id)
41
+ def clear_filter_cache(user_id=None):
42
+ if user_id is None:
43
+ cache.cache.delete_memoized(get_user_filters)
44
+ else:
45
+ cache.cache.delete_memoized(get_user_filters, user_id)
41
46
 
42
47
 
43
- def clear_filter_group_cache(user_id):
44
- cache.cache.delete_memoized(get_user_filter_groups, user_id)
48
+ def clear_filter_group_cache(user_id=None):
49
+ if user_id is None:
50
+ cache.cache.delete_memoized(get_user_filter_groups)
51
+ else:
52
+ cache.cache.delete_memoized(get_user_filter_groups, user_id)
45
53
 
46
54
 
47
55
  def clear_project_cache():
@@ -402,20 +410,27 @@ def check_belong_to_project(project_id):
402
410
  if project_id is None:
403
411
  return False
404
412
 
405
- project = projects_service.get_project_with_relations(str(project_id))
413
+ project = projects_service.get_project(str(project_id), relations=True)
406
414
  current_user = persons_service.get_current_user()
407
415
  return current_user["id"] in project["team"]
408
416
 
409
417
 
410
- def check_project_access(project_id):
418
+ def has_project_access(project_id):
411
419
  """
412
420
  Return true if current user is a manager or has a task assigned for this
413
421
  project.
414
422
  """
415
- is_allowed = (
416
- permissions.has_admin_permissions()
417
- or check_belong_to_project(project_id)
423
+ return permissions.has_admin_permissions() or check_belong_to_project(
424
+ project_id
418
425
  )
426
+
427
+
428
+ def check_project_access(project_id):
429
+ """
430
+ Return true if current user is a manager or has a task assigned for this
431
+ project. Raise a PermissionDenied exception if not.
432
+ """
433
+ is_allowed = has_project_access(project_id)
419
434
  if not is_allowed:
420
435
  raise permissions.PermissionDenied
421
436
  return is_allowed
@@ -463,6 +478,16 @@ def check_task_status_access(task_status_id):
463
478
  return True
464
479
 
465
480
 
481
+ def check_task_access(task_id):
482
+ """
483
+ Return true if current user can have access to a task.
484
+ """
485
+ task = tasks_service.get_task(task_id)
486
+ check_project_access(task["project_id"])
487
+ check_entity_access(task["entity_id"])
488
+ return True
489
+
490
+
466
491
  def check_comment_access(comment_id):
467
492
  """
468
493
  Return true if current user can have access to a comment.
@@ -501,15 +526,23 @@ def check_comment_access(comment_id):
501
526
  return True
502
527
 
503
528
 
504
- def check_manager_project_access(project_id):
529
+ def has_manager_project_access(project_id):
505
530
  """
506
531
  Return true if current user is a manager and has a task assigned for this
507
532
  project.
508
533
  """
509
- is_allowed = permissions.has_admin_permissions() or (
534
+ return permissions.has_admin_permissions() or (
510
535
  permissions.has_manager_permissions()
511
536
  and check_belong_to_project(project_id)
512
537
  )
538
+
539
+
540
+ def check_manager_project_access(project_id):
541
+ """
542
+ Return true if current user is a manager and has a task assigned for this
543
+ project. Raise a PermissionDenied exception if not.
544
+ """
545
+ is_allowed = has_manager_project_access(project_id)
513
546
  if not is_allowed:
514
547
  raise permissions.PermissionDenied
515
548
  return is_allowed
@@ -572,7 +605,7 @@ def check_supervisor_task_access(task, new_data={}):
572
605
  # checks that the supervisor only modifies columns
573
606
  # for which he is authorized
574
607
  allowed_columns = set(
575
- ["priority", "start_date", "due_date", "estimation"]
608
+ ["priority", "start_date", "due_date", "estimation", "difficulty"]
576
609
  )
577
610
  if len(set(new_data.keys()) - allowed_columns) == 0:
578
611
  user_departments = persons_service.get_current_user(
@@ -692,7 +725,7 @@ def check_metadata_department_access(entity, new_data={}):
692
725
  return is_allowed
693
726
 
694
727
 
695
- def check_task_departement_access(task_id, person_id):
728
+ def check_task_department_access(task_id, person_id):
696
729
  """
697
730
  Return true if current user is an admin or is a manager and is in team
698
731
  or is a supervisor in the department of the task or is an artist assigning
@@ -744,7 +777,7 @@ def check_person_is_not_bot(person_id):
744
777
  return True
745
778
 
746
779
 
747
- def check_task_departement_access_for_unassign(task_id, person_id=None):
780
+ def check_task_department_access_for_unassign(task_id, person_id=None):
748
781
  """
749
782
  Return true if current user is an admin or is a manager and is in team
750
783
  or is a supervisor in the department of the task or is an artist assigning
@@ -852,43 +885,86 @@ def get_user_filters(current_user_id):
852
885
  result = {}
853
886
 
854
887
  filters = (
855
- SearchFilter.query.join(Project)
856
- .join(ProjectStatus)
857
- .filter(SearchFilter.person_id == current_user_id)
858
- .filter(build_open_project_filter())
888
+ SearchFilter.query.outerjoin(Project)
889
+ .outerjoin(ProjectStatus)
890
+ .filter(
891
+ or_(
892
+ SearchFilter.person_id == current_user_id,
893
+ SearchFilter.is_shared == True,
894
+ )
895
+ )
896
+ .filter(
897
+ or_(build_open_project_filter(), SearchFilter.project_id == None)
898
+ )
859
899
  .all()
860
900
  )
861
901
 
862
- filters = (
863
- filters
864
- + SearchFilter.query.filter(SearchFilter.person_id == current_user_id)
865
- .filter(SearchFilter.project_id == None)
866
- .all()
867
- )
902
+ current_user = persons_service.get_current_user(relations=True)
903
+ is_manager = permissions.has_manager_permissions()
868
904
 
869
905
  for search_filter in filters:
870
- if search_filter.list_type not in result:
871
- result[search_filter.list_type] = {}
872
- subresult = result[search_filter.list_type]
906
+ department_id = search_filter.department_id
907
+ is_in_departments = (
908
+ department_id is not None
909
+ and str(department_id) in current_user["departments"]
910
+ )
873
911
 
874
- if search_filter.project_id is None:
875
- project_id = "all"
876
- else:
877
- project_id = str(search_filter.project_id)
912
+ if department_id is None or is_manager or is_in_departments:
913
+ if search_filter.list_type not in result:
914
+ result[search_filter.list_type] = {}
915
+ subresult = result[search_filter.list_type]
878
916
 
879
- if project_id not in subresult:
880
- subresult[project_id] = []
917
+ if search_filter.project_id is None:
918
+ project_id = "all"
919
+ else:
920
+ project_id = str(search_filter.project_id)
921
+
922
+ if project_id not in subresult:
923
+ subresult[project_id] = []
881
924
 
882
- subresult[project_id].append(search_filter.serialize())
925
+ subresult[project_id].append(search_filter.serialize())
883
926
 
884
927
  return result
885
928
 
886
929
 
887
- def create_filter(list_type, name, query, project_id=None, entity_type=None):
930
+ def create_filter(
931
+ list_type,
932
+ name,
933
+ query,
934
+ project_id=None,
935
+ entity_type=None,
936
+ is_shared=False,
937
+ search_filter_group_id=None,
938
+ department_id=None,
939
+ ):
888
940
  """
889
941
  Add a new search filter to the database.
890
942
  """
891
943
  current_user = persons_service.get_current_user()
944
+
945
+ if project_id is None or (
946
+ project_id is not None and not has_manager_project_access(project_id)
947
+ ):
948
+ is_shared = False
949
+
950
+ if search_filter_group_id is not None:
951
+ search_filter_group = SearchFilterGroup.get_by(
952
+ id=search_filter_group_id
953
+ )
954
+ if search_filter_group is None:
955
+ raise SearchFilterGroupNotFoundException
956
+ if is_shared != search_filter_group.is_shared:
957
+ raise WrongParameterException(
958
+ "A search filter should have the same value for is_shared than its search filter group."
959
+ )
960
+
961
+ if department_id is not None:
962
+ department = tasks_service.get_department(department_id)
963
+ if department is None:
964
+ raise WrongParameterException(
965
+ f"No department found with id: {department_id}"
966
+ )
967
+
892
968
  search_filter = SearchFilter.create(
893
969
  list_type=list_type,
894
970
  name=name,
@@ -896,9 +972,15 @@ def create_filter(list_type, name, query, project_id=None, entity_type=None):
896
972
  project_id=project_id,
897
973
  person_id=current_user["id"],
898
974
  entity_type=entity_type,
975
+ is_shared=is_shared,
976
+ search_filter_group_id=search_filter_group_id,
977
+ department_id=department_id,
899
978
  )
900
979
  search_filter.serialize()
901
- clear_filter_cache(current_user["id"])
980
+ if search_filter.is_shared:
981
+ clear_filter_cache()
982
+ else:
983
+ clear_filter_cache(current_user["id"])
902
984
  return search_filter.serialize()
903
985
 
904
986
 
@@ -910,10 +992,64 @@ def update_filter(search_filter_id, data):
910
992
  search_filter = SearchFilter.get_by(
911
993
  id=search_filter_id, person_id=current_user["id"]
912
994
  )
995
+ if current_user["role"] == "admin" and search_filter is None:
996
+ search_filter = SearchFilter.get_by(id=search_filter_id)
997
+
913
998
  if search_filter is None:
914
999
  raise SearchFilterNotFoundException
1000
+
1001
+ department_id = data.get("department_id", None)
1002
+ if department_id is not None:
1003
+ department = tasks_service.get_department(department_id)
1004
+ if department is None:
1005
+ raise WrongParameterException(
1006
+ f"No department found with id: {department_id}"
1007
+ )
1008
+
1009
+ if (
1010
+ data.get("is_shared", None) is not None
1011
+ and search_filter.is_shared != data["is_shared"]
1012
+ and (
1013
+ data.get("project_id", None) is None
1014
+ or (
1015
+ data["project_id"] is not None
1016
+ and not has_manager_project_access(data["project_id"])
1017
+ )
1018
+ )
1019
+ ):
1020
+ data["is_shared"] = False
1021
+
1022
+ if (
1023
+ search_filter_group_id := data.get(
1024
+ "search_filter_group_id", search_filter.search_filter_group_id
1025
+ )
1026
+ ) is not None:
1027
+ search_filter_group = SearchFilterGroup.get_by(
1028
+ id=search_filter_group_id
1029
+ )
1030
+ if search_filter_group is None:
1031
+ raise SearchFilterGroupNotFoundException
1032
+ if (
1033
+ data.get("is_shared", search_filter.is_shared)
1034
+ != search_filter_group.is_shared
1035
+ ):
1036
+ raise WrongParameterException(
1037
+ "A search filter should have the same value for is_shared than its search filter group."
1038
+ )
1039
+
1040
+ department_id = data.get("department_id", None)
1041
+ if department_id is not None:
1042
+ department = tasks_service.get_department(department_id)
1043
+ if department is None:
1044
+ raise WrongParameterException(
1045
+ f"No department found with id: {department_id}"
1046
+ )
1047
+
915
1048
  search_filter.update(data)
916
- clear_filter_cache(current_user["id"])
1049
+ if search_filter.is_shared:
1050
+ clear_filter_cache()
1051
+ else:
1052
+ clear_filter_cache(current_user["id"])
917
1053
  return search_filter.serialize()
918
1054
 
919
1055
 
@@ -928,7 +1064,10 @@ def remove_filter(search_filter_id):
928
1064
  if search_filter is None:
929
1065
  raise SearchFilterNotFoundException
930
1066
  search_filter.delete()
931
- clear_filter_cache(current_user["id"])
1067
+ if search_filter.is_shared:
1068
+ clear_filter_cache()
1069
+ else:
1070
+ clear_filter_cache(current_user["id"])
932
1071
  return search_filter.serialize()
933
1072
 
934
1073
 
@@ -942,7 +1081,7 @@ def get_filter_groups():
942
1081
  return get_user_filter_groups(current_user["id"])
943
1082
 
944
1083
 
945
- @cache.memoize_function(120)
1084
+ @cache.memoize_function(10)
946
1085
  def get_user_filter_groups(current_user_id):
947
1086
  """
948
1087
  Retrieve search filter groups used for given user. It groups them by
@@ -953,49 +1092,74 @@ def get_user_filter_groups(current_user_id):
953
1092
  result = {}
954
1093
 
955
1094
  filter_groups = (
956
- SearchFilterGroup.query.join(
1095
+ SearchFilterGroup.query.outerjoin(
957
1096
  Project, Project.id == SearchFilterGroup.project_id
958
1097
  )
959
- .join(ProjectStatus, ProjectStatus.id == Project.project_status_id)
960
- .filter(SearchFilterGroup.person_id == current_user_id)
961
- .filter(build_open_project_filter())
962
- .all()
963
- )
964
-
965
- filter_groups = (
966
- filter_groups
967
- + SearchFilterGroup.query.filter(
968
- SearchFilterGroup.person_id == current_user_id
1098
+ .outerjoin(
1099
+ ProjectStatus, ProjectStatus.id == Project.project_status_id
1100
+ )
1101
+ .filter(
1102
+ or_(
1103
+ SearchFilterGroup.person_id == current_user_id,
1104
+ SearchFilterGroup.is_shared == True,
1105
+ )
969
1106
  )
970
- .filter(SearchFilterGroup.project_id == None)
1107
+ .filter(or_(build_open_project_filter(), Project.id == None))
971
1108
  .all()
972
1109
  )
973
1110
 
1111
+ current_user = persons_service.get_current_user(relations=True)
1112
+ is_manager = permissions.has_manager_permissions()
1113
+
974
1114
  for search_filter_group in filter_groups:
975
1115
  if search_filter_group.list_type not in result:
976
1116
  result[search_filter_group.list_type] = {}
977
- subresult = result[search_filter_group.list_type]
978
1117
 
979
- if search_filter_group.project_id is None:
980
- project_id = "all"
981
- else:
982
- project_id = str(search_filter_group.project_id)
1118
+ department_id = search_filter_group.department_id
1119
+ is_in_departments = (
1120
+ department_id is not None
1121
+ and str(department_id) in current_user["departments"]
1122
+ )
1123
+ if department_id is None or is_manager or is_in_departments:
1124
+ subresult = result[search_filter_group.list_type]
983
1125
 
984
- if project_id not in subresult:
985
- subresult[project_id] = []
1126
+ if search_filter_group.project_id is None:
1127
+ project_id = "all"
1128
+ else:
1129
+ project_id = str(search_filter_group.project_id)
986
1130
 
987
- subresult[project_id].append(search_filter_group.serialize())
1131
+ if project_id not in subresult:
1132
+ subresult[project_id] = []
1133
+ subresult[project_id].append(search_filter_group.serialize())
988
1134
 
989
1135
  return result
990
1136
 
991
1137
 
992
1138
  def create_filter_group(
993
- list_type, name, color, project_id=None, entity_type=None
1139
+ list_type,
1140
+ name,
1141
+ color,
1142
+ project_id=None,
1143
+ entity_type=None,
1144
+ is_shared=False,
1145
+ department_id=None,
994
1146
  ):
995
1147
  """
996
1148
  Add a new search filter group to the database.
997
1149
  """
998
1150
  current_user = persons_service.get_current_user()
1151
+ if project_id is None or (
1152
+ project_id is not None and not has_manager_project_access(project_id)
1153
+ ):
1154
+ is_shared = False
1155
+
1156
+ if department_id is not None:
1157
+ department = tasks_service.get_department(department_id)
1158
+ if department is None:
1159
+ raise WrongParameterException(
1160
+ f"No department found with id: {department_id}"
1161
+ )
1162
+
999
1163
  search_filter_group = SearchFilterGroup.create(
1000
1164
  list_type=list_type,
1001
1165
  name=name,
@@ -1003,9 +1167,16 @@ def create_filter_group(
1003
1167
  project_id=project_id,
1004
1168
  person_id=current_user["id"],
1005
1169
  entity_type=entity_type,
1170
+ is_shared=is_shared,
1171
+ department_id=department_id,
1006
1172
  )
1007
1173
  search_filter_group.serialize()
1008
- clear_filter_group_cache(current_user["id"])
1174
+
1175
+ if search_filter_group.is_shared:
1176
+ clear_filter_group_cache()
1177
+ else:
1178
+ clear_filter_group_cache(current_user["id"])
1179
+
1009
1180
  return search_filter_group.serialize()
1010
1181
 
1011
1182
 
@@ -1030,10 +1201,43 @@ def update_filter_group(search_filter_group_id, data):
1030
1201
  search_filter_group = SearchFilterGroup.get_by(
1031
1202
  id=search_filter_group_id, person_id=current_user["id"]
1032
1203
  )
1204
+
1033
1205
  if search_filter_group is None:
1034
1206
  raise SearchFilterGroupNotFoundException
1207
+
1208
+ if (
1209
+ data.get("is_shared", None) is not None
1210
+ and search_filter_group.is_shared != data["is_shared"]
1211
+ and (
1212
+ data.get("project_id", None) is None
1213
+ or (
1214
+ data["project_id"] is not None
1215
+ and not has_manager_project_access(data["project_id"])
1216
+ )
1217
+ )
1218
+ ):
1219
+ data["is_shared"] = False
1220
+
1035
1221
  search_filter_group.update(data)
1036
- clear_filter_group_cache(current_user["id"])
1222
+
1223
+ if (
1224
+ data.get("is_shared", None) is not None
1225
+ and data.get("project_id", None) is not None
1226
+ and has_manager_project_access(data["project_id"])
1227
+ ):
1228
+ if (
1229
+ SearchFilter.query.filter_by(
1230
+ search_filter_group_id=search_filter_group_id
1231
+ ).update({"is_shared": data["is_shared"]})
1232
+ > 0
1233
+ ):
1234
+ SearchFilter.query.session.commit()
1235
+ clear_filter_cache()
1236
+
1237
+ if search_filter_group.is_shared:
1238
+ clear_filter_group_cache()
1239
+ else:
1240
+ clear_filter_group_cache(current_user["id"])
1037
1241
  return search_filter_group.serialize()
1038
1242
 
1039
1243
 
@@ -1047,8 +1251,19 @@ def remove_filter_group(search_filter_group_id):
1047
1251
  )
1048
1252
  if search_filter_group is None:
1049
1253
  raise SearchFilterGroupNotFoundException
1254
+ if (
1255
+ SearchFilter.query.filter_by(
1256
+ search_filter_group_id=search_filter_group_id
1257
+ ).delete()
1258
+ > 0
1259
+ ):
1260
+ SearchFilter.query.session.commit()
1261
+ clear_filter_cache()
1050
1262
  search_filter_group.delete()
1051
- clear_filter_group_cache(current_user["id"])
1263
+ if search_filter_group.is_shared:
1264
+ clear_filter_group_cache()
1265
+ else:
1266
+ clear_filter_group_cache(current_user["id"])
1052
1267
  return search_filter_group.serialize()
1053
1268
 
1054
1269
 
@@ -1064,6 +1279,18 @@ def get_notification(notification_id):
1064
1279
  return notifications[0]
1065
1280
 
1066
1281
 
1282
+ def update_notification(notification_id, read):
1283
+ """
1284
+ Update read status of given notification.
1285
+ """
1286
+ current_user = persons_service.get_current_user()
1287
+ notification = Notification.get_by(
1288
+ id=notification_id, person_id=current_user["id"]
1289
+ )
1290
+ notification.update({"read": read})
1291
+ return notification.serialize()
1292
+
1293
+
1067
1294
  def get_unread_notifications_count(notification_id=None):
1068
1295
  """
1069
1296
  Return the number of unread notifications.
@@ -1074,6 +1301,9 @@ def get_unread_notifications_count(notification_id=None):
1074
1301
  ).count()
1075
1302
 
1076
1303
 
1304
+ from sqlalchemy import and_, func
1305
+
1306
+
1077
1307
  def get_last_notifications(
1078
1308
  notification_id=None,
1079
1309
  after=None,
@@ -1081,6 +1311,8 @@ def get_last_notifications(
1081
1311
  task_type_id=None,
1082
1312
  task_status_id=None,
1083
1313
  notification_type=None,
1314
+ read=None,
1315
+ watching=None,
1084
1316
  ):
1085
1317
  """
1086
1318
  Return last 100 user notifications.
@@ -1095,6 +1327,13 @@ def get_last_notifications(
1095
1327
  .join(Author, Author.id == Notification.author_id)
1096
1328
  .join(Task, Task.id == Notification.task_id)
1097
1329
  .join(Project, Project.id == Task.project_id)
1330
+ .outerjoin(
1331
+ Subscription,
1332
+ and_(
1333
+ Subscription.task_id == Task.id,
1334
+ Subscription.person_id == current_user["id"],
1335
+ ),
1336
+ )
1098
1337
  .outerjoin(Comment, Comment.id == Notification.comment_id)
1099
1338
  .add_columns(
1100
1339
  Project.id,
@@ -1105,6 +1344,7 @@ def get_last_notifications(
1105
1344
  Comment.text,
1106
1345
  Comment.replies,
1107
1346
  Task.entity_id,
1347
+ Subscription.id,
1108
1348
  Author.role,
1109
1349
  )
1110
1350
  )
@@ -1133,6 +1373,15 @@ def get_last_notifications(
1133
1373
  if notification_type is not None:
1134
1374
  query = query.filter(Notification.type == notification_type)
1135
1375
 
1376
+ if read is not None:
1377
+ query = query.filter(Notification.read == read)
1378
+
1379
+ if watching is not None:
1380
+ if watching:
1381
+ query = query.filter(Subscription.id != None)
1382
+ else:
1383
+ query = query.filter(Subscription.id == None)
1384
+
1136
1385
  notifications = query.limit(100).all()
1137
1386
 
1138
1387
  for (
@@ -1145,10 +1394,11 @@ def get_last_notifications(
1145
1394
  comment_text,
1146
1395
  comment_replies,
1147
1396
  task_entity_id,
1397
+ subscription_id,
1148
1398
  role,
1149
1399
  ) in notifications:
1150
- (full_entity_name, episode_id) = names_service.get_full_entity_name(
1151
- task_entity_id
1400
+ (full_entity_name, episode_id, entity_preview_file_id) = (
1401
+ names_service.get_full_entity_name(task_entity_id)
1152
1402
  )
1153
1403
  preview_file_id = None
1154
1404
  mentions = []
@@ -1208,6 +1458,8 @@ def get_last_notifications(
1208
1458
  "change": notification.change,
1209
1459
  "full_entity_name": full_entity_name,
1210
1460
  "episode_id": episode_id,
1461
+ "entity_preview_file_id": entity_preview_file_id,
1462
+ "subscription_id": subscription_id,
1211
1463
  }
1212
1464
  )
1213
1465
  )
@@ -1220,17 +1472,21 @@ def mark_notifications_as_read():
1220
1472
  Mark all recent notifications for current_user as read. It is useful
1221
1473
  to mark a list of notifications as read after an user retrieved them.
1222
1474
  """
1475
+ from sqlalchemy import update
1476
+ from zou.app import db
1477
+
1223
1478
  current_user = persons_service.get_current_user()
1224
- notifications = (
1225
- Notification.query.filter_by(person_id=current_user["id"], read=False)
1226
- .order_by(Notification.created_at)
1227
- .all()
1479
+ update_stmt = (
1480
+ update(Notification)
1481
+ .where(Notification.person_id == current_user["id"])
1482
+ .where(Notification.read == False)
1483
+ .values(read=True)
1228
1484
  )
1229
1485
 
1230
- for notification in notifications:
1231
- notification.update({"read": True})
1486
+ db.session.execute(update_stmt)
1487
+ db.session.commit()
1232
1488
 
1233
- return fields.serialize_list(notifications)
1489
+ return True
1234
1490
 
1235
1491
 
1236
1492
  def has_task_subscription(task_id):
@@ -1313,38 +1569,25 @@ def get_timezone():
1313
1569
 
1314
1570
 
1315
1571
  def get_context():
1316
- if permissions.has_admin_permissions():
1317
- projects = projects_service.open_projects()
1318
- else:
1319
- projects = get_open_projects()
1320
-
1321
- asset_types = assets_service.get_asset_types()
1322
- custom_actions = custom_actions_service.get_custom_actions()
1323
- status_automations = status_automations_service.get_status_automations()
1324
- persons = persons_service.get_persons(
1325
- minimal=not permissions.has_manager_permissions()
1326
- )
1327
- notification_count = get_unread_notifications_count()
1328
- project_status_list = projects_service.get_project_statuses()
1329
- departments = tasks_service.get_departments()
1330
- task_types = tasks_service.get_task_types()
1331
- task_status_list = tasks_service.get_task_statuses()
1332
- search_filters = get_filters()
1333
- search_filter_groups = get_filter_groups()
1334
- preview_background_files = files_service.get_preview_background_files()
1335
-
1336
- return {
1337
- "asset_types": asset_types,
1338
- "custom_actions": custom_actions,
1339
- "status_automations": status_automations,
1340
- "departments": departments,
1341
- "notification_count": notification_count,
1342
- "persons": persons,
1343
- "project_status": project_status_list,
1344
- "projects": projects,
1345
- "task_types": task_types,
1346
- "task_status": task_status_list,
1347
- "search_filters": search_filters,
1348
- "search_filter_groups": search_filter_groups,
1349
- "preview_background_files": preview_background_files,
1572
+ context = {
1573
+ "asset_types": assets_service.get_asset_types(),
1574
+ "custom_actions": custom_actions_service.get_custom_actions(),
1575
+ "status_automations": status_automations_service.get_status_automations(),
1576
+ "departments": tasks_service.get_departments(),
1577
+ "studios": tasks_service.get_studios(),
1578
+ "notification_count": get_unread_notifications_count(),
1579
+ "persons": persons_service.get_persons(
1580
+ minimal=not permissions.has_manager_permissions()
1581
+ ),
1582
+ "project_status": projects_service.get_project_statuses(),
1583
+ "projects": get_open_projects(),
1584
+ "task_types": tasks_service.get_task_types(),
1585
+ "task_status": tasks_service.get_task_statuses(),
1586
+ "search_filters": get_filters(),
1587
+ "search_filter_groups": get_filter_groups(),
1588
+ "preview_background_files": files_service.get_preview_background_files(),
1350
1589
  }
1590
+
1591
+ if permissions.has_admin_permissions():
1592
+ context["user_limit"] = config.USER_LIMIT
1593
+ return context