wbcrm 2.2.1__py2.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.

Potentially problematic release.


This version of wbcrm might be problematic. Click here for more details.

Files changed (155) hide show
  1. wbcrm/__init__.py +1 -0
  2. wbcrm/admin/__init__.py +4 -0
  3. wbcrm/admin/accounts.py +59 -0
  4. wbcrm/admin/activities.py +101 -0
  5. wbcrm/admin/groups.py +7 -0
  6. wbcrm/admin/products.py +8 -0
  7. wbcrm/apps.py +5 -0
  8. wbcrm/configurations/__init__.py +1 -0
  9. wbcrm/configurations/base.py +16 -0
  10. wbcrm/dynamic_preferences_registry.py +38 -0
  11. wbcrm/factories/__init__.py +14 -0
  12. wbcrm/factories/accounts.py +56 -0
  13. wbcrm/factories/activities.py +125 -0
  14. wbcrm/factories/groups.py +23 -0
  15. wbcrm/factories/products.py +10 -0
  16. wbcrm/filters/__init__.py +10 -0
  17. wbcrm/filters/accounts.py +67 -0
  18. wbcrm/filters/activities.py +181 -0
  19. wbcrm/filters/groups.py +20 -0
  20. wbcrm/filters/products.py +37 -0
  21. wbcrm/filters/signals.py +94 -0
  22. wbcrm/migrations/0001_initial_squashed_squashed_0032_productcompanyrelationship_alter_product_prospects_and_more.py +3948 -0
  23. wbcrm/migrations/0002_alter_activity_repeat_choice.py +32 -0
  24. wbcrm/migrations/0003_remove_activity_external_id_and_more.py +63 -0
  25. wbcrm/migrations/0004_alter_activity_status.py +28 -0
  26. wbcrm/migrations/0005_account_accountrole_accountroletype_and_more.py +182 -0
  27. wbcrm/migrations/0006_alter_activity_location.py +17 -0
  28. wbcrm/migrations/0007_alter_account_status.py +23 -0
  29. wbcrm/migrations/0008_alter_activity_options.py +16 -0
  30. wbcrm/migrations/0009_alter_account_is_public.py +19 -0
  31. wbcrm/migrations/0010_alter_account_reference_id.py +17 -0
  32. wbcrm/migrations/0011_activity_summary.py +22 -0
  33. wbcrm/migrations/0012_alter_activity_summary.py +17 -0
  34. wbcrm/migrations/0013_account_action_plan_account_relationship_status_and_more.py +34 -0
  35. wbcrm/migrations/0014_alter_account_relationship_status.py +24 -0
  36. wbcrm/migrations/0015_alter_activity_type.py +23 -0
  37. wbcrm/migrations/0016_auto_20241205_1015.py +106 -0
  38. wbcrm/migrations/__init__.py +0 -0
  39. wbcrm/models/__init__.py +4 -0
  40. wbcrm/models/accounts.py +637 -0
  41. wbcrm/models/activities.py +1335 -0
  42. wbcrm/models/groups.py +118 -0
  43. wbcrm/models/products.py +83 -0
  44. wbcrm/models/recurrence.py +279 -0
  45. wbcrm/preferences.py +14 -0
  46. wbcrm/serializers/__init__.py +23 -0
  47. wbcrm/serializers/accounts.py +126 -0
  48. wbcrm/serializers/activities.py +526 -0
  49. wbcrm/serializers/groups.py +30 -0
  50. wbcrm/serializers/products.py +57 -0
  51. wbcrm/serializers/recurrence.py +90 -0
  52. wbcrm/serializers/signals.py +70 -0
  53. wbcrm/synchronization/__init__.py +0 -0
  54. wbcrm/synchronization/activity/__init__.py +0 -0
  55. wbcrm/synchronization/activity/admin.py +72 -0
  56. wbcrm/synchronization/activity/backend.py +207 -0
  57. wbcrm/synchronization/activity/backends/__init__.py +0 -0
  58. wbcrm/synchronization/activity/backends/google/__init__.py +2 -0
  59. wbcrm/synchronization/activity/backends/google/google_calendar_backend.py +399 -0
  60. wbcrm/synchronization/activity/backends/google/request_utils/__init__.py +16 -0
  61. wbcrm/synchronization/activity/backends/google/tasks.py +21 -0
  62. wbcrm/synchronization/activity/backends/google/tests/__init__.py +0 -0
  63. wbcrm/synchronization/activity/backends/google/tests/conftest.py +1 -0
  64. wbcrm/synchronization/activity/backends/google/tests/test_data.py +81 -0
  65. wbcrm/synchronization/activity/backends/google/tests/test_google_backend.py +319 -0
  66. wbcrm/synchronization/activity/backends/google/tests/test_utils.py +274 -0
  67. wbcrm/synchronization/activity/backends/google/typing_informations.py +139 -0
  68. wbcrm/synchronization/activity/backends/google/utils.py +216 -0
  69. wbcrm/synchronization/activity/backends/outlook/__init__.py +0 -0
  70. wbcrm/synchronization/activity/backends/outlook/backend.py +576 -0
  71. wbcrm/synchronization/activity/backends/outlook/msgraph.py +438 -0
  72. wbcrm/synchronization/activity/backends/outlook/parser.py +423 -0
  73. wbcrm/synchronization/activity/backends/outlook/tests/__init__.py +0 -0
  74. wbcrm/synchronization/activity/backends/outlook/tests/conftest.py +1 -0
  75. wbcrm/synchronization/activity/backends/outlook/tests/fixtures.py +606 -0
  76. wbcrm/synchronization/activity/backends/outlook/tests/test_admin.py +117 -0
  77. wbcrm/synchronization/activity/backends/outlook/tests/test_backend.py +269 -0
  78. wbcrm/synchronization/activity/backends/outlook/tests/test_controller.py +237 -0
  79. wbcrm/synchronization/activity/backends/outlook/tests/test_parser.py +173 -0
  80. wbcrm/synchronization/activity/controller.py +545 -0
  81. wbcrm/synchronization/activity/dynamic_preferences_registry.py +107 -0
  82. wbcrm/synchronization/activity/preferences.py +21 -0
  83. wbcrm/synchronization/activity/shortcuts.py +9 -0
  84. wbcrm/synchronization/activity/signals.py +28 -0
  85. wbcrm/synchronization/activity/tasks.py +21 -0
  86. wbcrm/synchronization/activity/urls.py +6 -0
  87. wbcrm/synchronization/activity/utils.py +46 -0
  88. wbcrm/synchronization/activity/views.py +37 -0
  89. wbcrm/synchronization/admin.py +1 -0
  90. wbcrm/synchronization/apps.py +15 -0
  91. wbcrm/synchronization/dynamic_preferences_registry.py +1 -0
  92. wbcrm/synchronization/management.py +36 -0
  93. wbcrm/synchronization/tasks.py +1 -0
  94. wbcrm/synchronization/urls.py +5 -0
  95. wbcrm/tasks.py +312 -0
  96. wbcrm/tests/__init__.py +0 -0
  97. wbcrm/tests/accounts/__init__.py +0 -0
  98. wbcrm/tests/accounts/test_models.py +380 -0
  99. wbcrm/tests/accounts/test_viewsets.py +87 -0
  100. wbcrm/tests/conftest.py +76 -0
  101. wbcrm/tests/disable_signals.py +52 -0
  102. wbcrm/tests/e2e/__init__.py +1 -0
  103. wbcrm/tests/e2e/e2e_wbcrm_utility.py +82 -0
  104. wbcrm/tests/e2e/test_e2e.py +369 -0
  105. wbcrm/tests/test_assignee_methods.py +39 -0
  106. wbcrm/tests/test_chartviewsets.py +111 -0
  107. wbcrm/tests/test_dto.py +63 -0
  108. wbcrm/tests/test_filters.py +51 -0
  109. wbcrm/tests/test_models.py +216 -0
  110. wbcrm/tests/test_recurrence.py +291 -0
  111. wbcrm/tests/test_report.py +20 -0
  112. wbcrm/tests/test_serializers.py +170 -0
  113. wbcrm/tests/test_tasks.py +94 -0
  114. wbcrm/tests/test_viewsets.py +967 -0
  115. wbcrm/tests/tests.py +120 -0
  116. wbcrm/typings.py +107 -0
  117. wbcrm/urls.py +67 -0
  118. wbcrm/viewsets/__init__.py +22 -0
  119. wbcrm/viewsets/accounts.py +121 -0
  120. wbcrm/viewsets/activities.py +315 -0
  121. wbcrm/viewsets/buttons/__init__.py +7 -0
  122. wbcrm/viewsets/buttons/accounts.py +27 -0
  123. wbcrm/viewsets/buttons/activities.py +68 -0
  124. wbcrm/viewsets/buttons/signals.py +17 -0
  125. wbcrm/viewsets/display/__init__.py +12 -0
  126. wbcrm/viewsets/display/accounts.py +110 -0
  127. wbcrm/viewsets/display/activities.py +443 -0
  128. wbcrm/viewsets/display/groups.py +22 -0
  129. wbcrm/viewsets/display/products.py +105 -0
  130. wbcrm/viewsets/endpoints/__init__.py +8 -0
  131. wbcrm/viewsets/endpoints/accounts.py +32 -0
  132. wbcrm/viewsets/endpoints/activities.py +30 -0
  133. wbcrm/viewsets/endpoints/groups.py +7 -0
  134. wbcrm/viewsets/endpoints/products.py +9 -0
  135. wbcrm/viewsets/groups.py +37 -0
  136. wbcrm/viewsets/menu/__init__.py +8 -0
  137. wbcrm/viewsets/menu/accounts.py +18 -0
  138. wbcrm/viewsets/menu/activities.py +61 -0
  139. wbcrm/viewsets/menu/groups.py +16 -0
  140. wbcrm/viewsets/menu/products.py +20 -0
  141. wbcrm/viewsets/mixins.py +34 -0
  142. wbcrm/viewsets/previews/__init__.py +1 -0
  143. wbcrm/viewsets/previews/activities.py +10 -0
  144. wbcrm/viewsets/products.py +56 -0
  145. wbcrm/viewsets/recurrence.py +26 -0
  146. wbcrm/viewsets/titles/__init__.py +13 -0
  147. wbcrm/viewsets/titles/accounts.py +22 -0
  148. wbcrm/viewsets/titles/activities.py +61 -0
  149. wbcrm/viewsets/titles/products.py +13 -0
  150. wbcrm/viewsets/titles/utils.py +46 -0
  151. wbcrm/workflows/__init__.py +1 -0
  152. wbcrm/workflows/assignee_methods.py +25 -0
  153. wbcrm-2.2.1.dist-info/METADATA +11 -0
  154. wbcrm-2.2.1.dist-info/RECORD +155 -0
  155. wbcrm-2.2.1.dist-info/WHEEL +5 -0
@@ -0,0 +1,90 @@
1
+ from django.db.models import Model
2
+ from django.forms import ValidationError
3
+ from django.utils.translation import gettext
4
+ from rest_framework import serializers
5
+ from rest_framework.reverse import reverse
6
+ from wbcore import serializers as wb_serializers
7
+ from wbcrm.models.recurrence import Recurrence
8
+
9
+
10
+ class RecurrenceModelSerializerMixin:
11
+ @wb_serializers.register_only_instance_resource()
12
+ def next_occurrence(self, instance: Model, request, user, **kwargs) -> dict:
13
+ resources = {}
14
+ if next_occurrence := instance.next_occurrence:
15
+ resources["next_occurrence"] = reverse(
16
+ f"{instance.get_endpoint_basename()}-detail", args=[next_occurrence.id], request=request
17
+ )
18
+ return resources
19
+
20
+ @wb_serializers.register_only_instance_resource()
21
+ def previous_occurrence(self, instance: Model, request, user, **kwargs) -> dict:
22
+ resources = {}
23
+ if previous_occurrence := instance.previous_occurrence:
24
+ resources["previous_occurrence"] = reverse(
25
+ f"{instance.get_endpoint_basename()}-detail", args=[previous_occurrence.id], request=request
26
+ )
27
+ return resources
28
+
29
+ @wb_serializers.register_only_instance_resource()
30
+ def get_parent_occurrence(self, instance: Model, request, user, **kwargs) -> dict:
31
+ resources = {}
32
+ if instance.parent_occurrence:
33
+ resources["get_parent_occurrence"] = reverse(
34
+ f"{instance.get_endpoint_basename()}-detail", args=[instance.parent_occurrence.id], request=request
35
+ )
36
+ return resources
37
+
38
+ @wb_serializers.register_only_instance_resource()
39
+ def delete_occurrences(self, instance, request, user, **kwargs):
40
+ resources = dict()
41
+ parent = instance.parent_occurrence if instance.parent_occurrence else instance
42
+ child_occurrences = parent.get_recurrent_valid_children()
43
+ if instance.period and child_occurrences.exists():
44
+ child_occurrences = child_occurrences.filter(period__startswith__gt=instance.period.lower)
45
+ if child_occurrences.exists():
46
+ resources["delete_next_occurrences"] = reverse(
47
+ f"{instance.get_endpoint_basename()}-delete-next-occurrences", args=[instance.id], request=request
48
+ )
49
+ return resources
50
+
51
+ def validate(self, data):
52
+ period = data.get("period", self.instance.period if self.instance else None)
53
+ recurrence_count = data.get("recurrence_count", self.instance.recurrence_count if self.instance else None)
54
+ recurrence_end = data.get("recurrence_end", self.instance.recurrence_end if self.instance else None)
55
+ repeat_choice = data.get("repeat_choice", self.instance.repeat_choice if self.instance else None)
56
+ if not period:
57
+ raise serializers.ValidationError({"period": gettext("Please provide a valid timeframe.")})
58
+
59
+ if recurrence_end and recurrence_count:
60
+ error = gettext("You can only pick either a recurrence count or an end date but not both.")
61
+ raise ValidationError({"recurrence_end": error, "recurrence_count": error})
62
+
63
+ if (
64
+ not self.instance
65
+ and (recurrence_end or recurrence_count)
66
+ and repeat_choice == Recurrence.ReoccuranceChoice.NEVER
67
+ ):
68
+ data["recurrence_end"] = None
69
+ data["recurrence_count"] = None
70
+ if data.get("repeat_choice") and data.get("repeat_choice") != Recurrence.ReoccuranceChoice.NEVER:
71
+ if data.get("recurrence_end") and period.lower.date() >= data.get("recurrence_end"):
72
+ raise ValidationError(
73
+ {
74
+ "recurrence_end": gettext(
75
+ 'The "Repeat Until" date needs to be after the "Recurrence Start" date.'
76
+ )
77
+ }
78
+ )
79
+ if data.get("repeat_choice") == Recurrence.ReoccuranceChoice.BUSINESS_DAILY and period.lower.weekday() > 4:
80
+ raise ValidationError({"period": gettext("Period must correspond to the recurrence 'Business Daily'")})
81
+
82
+ if self.instance and self.instance.period and self.instance.is_recurrent:
83
+ if (
84
+ self.instance.period.lower.date() != period.lower.date()
85
+ or self.instance.period.upper.date() != period.upper.date()
86
+ ):
87
+ raise ValidationError(
88
+ {"period": gettext("It is only possible to change the time of the period of an occurrence.")}
89
+ )
90
+ return super().validate(data)
@@ -0,0 +1,70 @@
1
+ import functools
2
+ from contextlib import suppress
3
+ from urllib.parse import urlencode
4
+
5
+ from django.dispatch import receiver
6
+ from django.utils.translation import gettext as _
7
+ from rest_framework.reverse import reverse
8
+ from wbcore.contrib.directory.models import Entry
9
+ from wbcore.contrib.directory.serializers import (
10
+ CompanyModelSerializer,
11
+ EntryModelSerializer,
12
+ EntryRepresentationSerializer,
13
+ PersonModelListSerializer,
14
+ PersonModelSerializer,
15
+ TelephoneContactSerializer,
16
+ )
17
+ from wbcore.signals import add_additional_resource, add_instance_additional_resource
18
+ from wbcrm.models import ActivityType
19
+
20
+
21
+ @functools.lru_cache()
22
+ def get_call_activity_type() -> int:
23
+ return ActivityType.objects.get_or_create(slugify_title="call", defaults={"title": "Call"})[0].id
24
+
25
+
26
+ @receiver(add_additional_resource, sender=TelephoneContactSerializer)
27
+ def add_telephone_contact_activity_resources(sender, serializer, instance, request, user, **kwargs):
28
+ res = {}
29
+ with suppress(Entry.DoesNotExist):
30
+ if entry := instance.entry:
31
+ activity_reverse_url = reverse("wbcrm:activity-list", args=[], request=request)
32
+
33
+ # Creates the URL for the 'Create New Call Activity'-Button
34
+ query_args = {
35
+ "type": get_call_activity_type(),
36
+ "new_mode": True,
37
+ "participants": [str(request.user.profile.id)],
38
+ "title": _("Call with {name}").format(name=entry.computed_str),
39
+ }
40
+
41
+ if entry.is_company:
42
+ query_args["companies"] = [str(entry.id)]
43
+ activity_reverse_url = f"{activity_reverse_url}?companies={entry.id}"
44
+ else:
45
+ query_args["participants"].append(str(entry.id))
46
+ activity_reverse_url = f"{activity_reverse_url}?participants={entry.id}"
47
+
48
+ query_args["participants"] = ",".join(query_args["participants"])
49
+ if "companies" in query_args:
50
+ query_args["companies"] = ",".join(query_args["companies"])
51
+ res["list_of_activities"] = activity_reverse_url
52
+ res["new_call"] = reverse("wbcrm:activity-list", args=[], request=request) + "?" + urlencode(query_args)
53
+ return res
54
+
55
+
56
+ @receiver(add_instance_additional_resource, sender=CompanyModelSerializer)
57
+ @receiver(add_instance_additional_resource, sender=PersonModelSerializer)
58
+ @receiver(add_instance_additional_resource, sender=EntryModelSerializer)
59
+ @receiver(add_instance_additional_resource, sender=PersonModelListSerializer)
60
+ @receiver(add_instance_additional_resource, sender=EntryRepresentationSerializer)
61
+ def add_entry_additional_resources(sender, serializer, instance, request, user, **kwargs):
62
+ res = {"account": f'{reverse("wbcrm:account-list", args=[], request=request)}?customer={instance.id}'}
63
+ if instance.is_company:
64
+ res["activity"] = f'{reverse("wbcrm:activity-list", request=request)}?companies={instance.id}'
65
+ res["interested_products"] = reverse(
66
+ "wbcrm:company-interestedproduct-list", args=[instance.id], request=request
67
+ )
68
+ else:
69
+ res["activity"] = f'{reverse("wbcrm:activity-list", request=request)}?participants={instance.id}'
70
+ return res
File without changes
File without changes
@@ -0,0 +1,72 @@
1
+ from django.contrib import admin, messages
2
+ from django.contrib.auth import get_user_model
3
+ from django.utils.translation import gettext_lazy as _
4
+ from wbcore.contrib.authentication.admin import UserAdmin
5
+ from wbcrm.admin import ActivityAdmin
6
+ from wbcrm.models import Activity
7
+
8
+ from .shortcuts import get_backend
9
+
10
+ User = get_user_model()
11
+ admin.site.unregister(User)
12
+ admin.site.unregister(Activity)
13
+
14
+
15
+ @admin.register(Activity)
16
+ class ActivitySyncAdmin(ActivityAdmin):
17
+ def delete_queryset(self, request, queryset):
18
+ """Given a queryset, delete it from the database."""
19
+ for obj in queryset.filter(is_active=True):
20
+ obj.delete()
21
+ super().delete_queryset(request, queryset)
22
+
23
+
24
+ @admin.register(User)
25
+ class UserSyncAdmin(UserAdmin):
26
+ def set_web_hook(self, request, queryset):
27
+ try:
28
+ if backend := get_backend():
29
+ for user in queryset:
30
+ backend().set_web_hook(user)
31
+ self.message_user(
32
+ request,
33
+ _("Operation completed, we have set the webhook for {} users.").format(queryset.count()),
34
+ )
35
+ else:
36
+ raise ValueError("No backend set in preferences")
37
+ except Exception as e:
38
+ self.message_user(request, _("Operation Failed, {}").format(e), messages.WARNING)
39
+
40
+ def stop_web_hook(self, request, queryset):
41
+ try:
42
+ if backend := get_backend():
43
+ for user in queryset:
44
+ backend().stop_web_hook(user)
45
+ self.message_user(
46
+ request,
47
+ _("Operation completed, we have stopped the webhook for {} users.").format(queryset.count()),
48
+ )
49
+ else:
50
+ raise ValueError("No backend set in preferences")
51
+ except Exception as e:
52
+ self.message_user(request, _("Operation Failed, {}").format(e), messages.WARNING)
53
+
54
+ def check_web_hook(self, request, queryset):
55
+ try:
56
+ if backend := get_backend():
57
+ for user in queryset:
58
+ backend().check_web_hook(user)
59
+ self.message_user(
60
+ request,
61
+ _("Operation completed, we checked the webhook for {} users.").format(queryset.count()),
62
+ )
63
+ else:
64
+ raise ValueError("No backend set in preferences")
65
+ except Exception as e:
66
+ self.message_user(request, _("Operation Failed, {}").format(e), messages.WARNING)
67
+
68
+ actions = UserAdmin.actions + (
69
+ set_web_hook,
70
+ stop_web_hook,
71
+ check_web_hook,
72
+ )
@@ -0,0 +1,207 @@
1
+ from contextlib import suppress
2
+ from datetime import date
3
+ from typing import Any
4
+
5
+ from django.contrib.auth import get_user_model
6
+ from django.http import HttpRequest, HttpResponse
7
+ from django.utils.translation import gettext_lazy
8
+ from wbcore.contrib.notifications.dispatch import send_notification
9
+ from wbcrm.typings import Activity as ActivityDTO
10
+ from wbcrm.typings import ParticipantStatus as ParticipantStatusDTO
11
+ from wbcrm.typings import User as UserDTO
12
+
13
+ User = get_user_model()
14
+
15
+
16
+ class SyncBackend:
17
+ METADATA_KEY = None
18
+
19
+ def open(self):
20
+ """
21
+ Allows to perform primary operations or to open a communication channel for synchronization,
22
+ such as defining the necessary configurations to send requests
23
+ """
24
+ pass
25
+
26
+ def close(self):
27
+ """
28
+ Close the communication channel and unset configuration
29
+ """
30
+ pass
31
+
32
+ def _validation_response(self, request: HttpRequest) -> HttpResponse:
33
+ """
34
+ send a response to the external calendar if necessary to validate the endpoint
35
+ """
36
+ return None
37
+
38
+ def _is_inbound_request_valid(self, request: HttpRequest) -> bool:
39
+ """
40
+ Valid function to ensure that the request received meets expectations
41
+ """
42
+ raise NotImplementedError
43
+
44
+ def _get_events_from_request(self, request: HttpRequest) -> list[dict[str, Any]]:
45
+ """
46
+ list of events following the notification received
47
+ """
48
+ raise NotImplementedError
49
+
50
+ def _deserialize(self, event: dict[str, Any]) -> tuple[ActivityDTO, bool, UserDTO]:
51
+ """
52
+ convert the dictionary received to a valid format of an activity
53
+ """
54
+ raise NotImplementedError()
55
+
56
+ def _serialize(self, activity_dto: ActivityDTO, created: bool = False) -> dict[str, Any]:
57
+ """
58
+ convert activity data transfer object to event dictionary
59
+ """
60
+ raise NotImplementedError()
61
+
62
+ def _stream_deletion(self, activity_dto: ActivityDTO):
63
+ """
64
+ allow the deletion of the event in the external calendar
65
+ we use the event_id stored in activity_dto's metadata to retrieve the event
66
+ """
67
+ raise NotImplementedError()
68
+
69
+ def _stream_creation(
70
+ self, activity_dto: ActivityDTO, activity_dict: dict[str, Any]
71
+ ) -> tuple[ActivityDTO, dict[str, Any]]:
72
+ """
73
+ allow the creation of the event in the external calendar
74
+ param: activity_dict: dictionary used to create the event
75
+
76
+ we return a tuple of activity, metadata which contains the external id to be store in the activity
77
+ """
78
+ raise NotImplementedError()
79
+
80
+ def _stream_update(
81
+ self,
82
+ activity_dto: ActivityDTO,
83
+ activity_dict: dict[str, Any],
84
+ only_participants_changed: bool = False,
85
+ external_participants: list = [],
86
+ keep_external_description: bool = False,
87
+ ) -> tuple[ActivityDTO, dict[str, Any]]:
88
+ """
89
+ allow to update the event in the external calendar
90
+ param: activity_dict: dictionary used to update the event
91
+ activity_dto: we use the metadata of the activity to retrieve the event
92
+ only_participants_changed: boolean to know if only the participants need to be update
93
+ external_participants: list of external participants, that must be added to the current list of participants to avoid their deletion when the activity is updated
94
+ keep_external_description: boolean to know if the description must be deleted or not before the update of the event
95
+ """
96
+ raise NotImplementedError()
97
+
98
+ def _stream_extension_event(self, activity_dto: ActivityDTO) -> None:
99
+ """
100
+ Extend external event with custom data
101
+ this allows us for example to add additional information to the event to easily identify it for a recurring activities
102
+ """
103
+ pass
104
+
105
+ def _stream_forward(self, activity_dto: ActivityDTO, participants_dto: list[ParticipantStatusDTO]):
106
+ """
107
+ allow to forward an event to a new participant. the external calendar
108
+ send an invitation to all participants and avoid sending an update of the activity to all participants
109
+ """
110
+ raise NotImplementedError()
111
+
112
+ def _stream_participant_change(
113
+ self, participant_dto: ParticipantStatusDTO, is_deleted: bool = False, wait_before_changing: bool = False
114
+ ):
115
+ """
116
+ allow to update the status of an event participant
117
+ """
118
+ raise NotImplementedError()
119
+
120
+ def _set_web_hook(self, user: "User") -> dict[str, dict[str, Any]]:
121
+ """
122
+ allows to activate the webhook for a user
123
+ returns a dictionary that will be stored in the metadata of the ser
124
+ """
125
+ raise NotImplementedError()
126
+
127
+ def _stop_web_hook(self, user: "User") -> dict[str, dict[str, Any]]:
128
+ """
129
+ allows to strop the webhook for a user and deletes the data stored in the metadata
130
+ """
131
+ raise NotImplementedError()
132
+
133
+ def _check_web_hook(self, user: "User") -> bool:
134
+ """
135
+ return a boolean to know if a subscription is activated or not for a user
136
+ """
137
+ raise NotImplementedError()
138
+
139
+ def set_web_hook(self, user: "User"):
140
+ """
141
+ allows to be sure that the metadata are saved by specifying the backend type.
142
+ """
143
+ new_metadata = self._set_web_hook(user)
144
+ user.metadata.setdefault(self.METADATA_KEY, {})
145
+ user.metadata[self.METADATA_KEY] = new_metadata
146
+ user.save()
147
+
148
+ def stop_web_hook(self, user: "User"):
149
+ new_metadata = self._stop_web_hook(user)
150
+ user.metadata.setdefault(self.METADATA_KEY, {})
151
+ user.metadata[self.METADATA_KEY] = new_metadata
152
+ user.save()
153
+
154
+ def check_web_hook(self, user: "User") -> bool:
155
+ try:
156
+ return self._check_web_hook(user)
157
+ except NotImplementedError:
158
+ return False
159
+
160
+ def renew_web_hooks(self) -> None:
161
+ """
162
+ Allows to renew existing webhooks of all users
163
+ """
164
+ pass
165
+
166
+ def _get_webhook_inconsistencies(self) -> str:
167
+ """
168
+ return a message of anomalies that will be notified to the administrator/persons set in the preferences
169
+ """
170
+ raise NotImplementedError()
171
+
172
+ def notify_admins_of_webhook_inconsistencies(self, emails: list) -> None:
173
+ """
174
+ the purpose is to make sure that the authorized persons receive the messages in case a webhook has been deactivated or not renewed correctly.
175
+ """
176
+ with suppress(NotImplementedError):
177
+ if emails and (message := self._get_webhook_inconsistencies()):
178
+ for recipient in User.objects.filter(email__in=emails):
179
+ send_notification(
180
+ code="wbcrm.activity_sync.admin",
181
+ title=gettext_lazy("Notify admins of event webhook inconsistencies - {}").format(date.today()),
182
+ body=f"<ul>{message}</ul>",
183
+ user=recipient,
184
+ )
185
+
186
+ def get_external_event(self, activity_dto: ActivityDTO) -> dict:
187
+ """
188
+ Get an event of external calendar.
189
+ """
190
+ pass
191
+
192
+ def get_external_participants(
193
+ self, activity_dto: ActivityDTO, internal_participants_dto: list[ParticipantStatusDTO]
194
+ ) -> list[str, Any]:
195
+ """
196
+ Get external participants of an external event
197
+ """
198
+ return []
199
+
200
+ def _is_participant_valid(self, user: "User") -> bool:
201
+ return user.is_active and user.is_register
202
+
203
+ def is_valid(self, activity: ActivityDTO) -> bool:
204
+ # Synchronize only if the creator or at least one participant has an active subscription
205
+ participants = [activity.creator.email] if activity.creator else []
206
+ participants.extend(list(map(lambda x: x.person.email, activity.participants)))
207
+ return any([self._is_participant_valid(user) for user in User.objects.filter(email__in=set(participants))])
File without changes
@@ -0,0 +1,2 @@
1
+ from .google_calendar_backend import GoogleCalendarBackend
2
+ from .tasks import google_webhook_resubscription