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.
- wbcrm/__init__.py +1 -0
- wbcrm/admin/__init__.py +4 -0
- wbcrm/admin/accounts.py +59 -0
- wbcrm/admin/activities.py +101 -0
- wbcrm/admin/groups.py +7 -0
- wbcrm/admin/products.py +8 -0
- wbcrm/apps.py +5 -0
- wbcrm/configurations/__init__.py +1 -0
- wbcrm/configurations/base.py +16 -0
- wbcrm/dynamic_preferences_registry.py +38 -0
- wbcrm/factories/__init__.py +14 -0
- wbcrm/factories/accounts.py +56 -0
- wbcrm/factories/activities.py +125 -0
- wbcrm/factories/groups.py +23 -0
- wbcrm/factories/products.py +10 -0
- wbcrm/filters/__init__.py +10 -0
- wbcrm/filters/accounts.py +67 -0
- wbcrm/filters/activities.py +181 -0
- wbcrm/filters/groups.py +20 -0
- wbcrm/filters/products.py +37 -0
- wbcrm/filters/signals.py +94 -0
- wbcrm/migrations/0001_initial_squashed_squashed_0032_productcompanyrelationship_alter_product_prospects_and_more.py +3948 -0
- wbcrm/migrations/0002_alter_activity_repeat_choice.py +32 -0
- wbcrm/migrations/0003_remove_activity_external_id_and_more.py +63 -0
- wbcrm/migrations/0004_alter_activity_status.py +28 -0
- wbcrm/migrations/0005_account_accountrole_accountroletype_and_more.py +182 -0
- wbcrm/migrations/0006_alter_activity_location.py +17 -0
- wbcrm/migrations/0007_alter_account_status.py +23 -0
- wbcrm/migrations/0008_alter_activity_options.py +16 -0
- wbcrm/migrations/0009_alter_account_is_public.py +19 -0
- wbcrm/migrations/0010_alter_account_reference_id.py +17 -0
- wbcrm/migrations/0011_activity_summary.py +22 -0
- wbcrm/migrations/0012_alter_activity_summary.py +17 -0
- wbcrm/migrations/0013_account_action_plan_account_relationship_status_and_more.py +34 -0
- wbcrm/migrations/0014_alter_account_relationship_status.py +24 -0
- wbcrm/migrations/0015_alter_activity_type.py +23 -0
- wbcrm/migrations/0016_auto_20241205_1015.py +106 -0
- wbcrm/migrations/__init__.py +0 -0
- wbcrm/models/__init__.py +4 -0
- wbcrm/models/accounts.py +637 -0
- wbcrm/models/activities.py +1335 -0
- wbcrm/models/groups.py +118 -0
- wbcrm/models/products.py +83 -0
- wbcrm/models/recurrence.py +279 -0
- wbcrm/preferences.py +14 -0
- wbcrm/serializers/__init__.py +23 -0
- wbcrm/serializers/accounts.py +126 -0
- wbcrm/serializers/activities.py +526 -0
- wbcrm/serializers/groups.py +30 -0
- wbcrm/serializers/products.py +57 -0
- wbcrm/serializers/recurrence.py +90 -0
- wbcrm/serializers/signals.py +70 -0
- wbcrm/synchronization/__init__.py +0 -0
- wbcrm/synchronization/activity/__init__.py +0 -0
- wbcrm/synchronization/activity/admin.py +72 -0
- wbcrm/synchronization/activity/backend.py +207 -0
- wbcrm/synchronization/activity/backends/__init__.py +0 -0
- wbcrm/synchronization/activity/backends/google/__init__.py +2 -0
- wbcrm/synchronization/activity/backends/google/google_calendar_backend.py +399 -0
- wbcrm/synchronization/activity/backends/google/request_utils/__init__.py +16 -0
- wbcrm/synchronization/activity/backends/google/tasks.py +21 -0
- wbcrm/synchronization/activity/backends/google/tests/__init__.py +0 -0
- wbcrm/synchronization/activity/backends/google/tests/conftest.py +1 -0
- wbcrm/synchronization/activity/backends/google/tests/test_data.py +81 -0
- wbcrm/synchronization/activity/backends/google/tests/test_google_backend.py +319 -0
- wbcrm/synchronization/activity/backends/google/tests/test_utils.py +274 -0
- wbcrm/synchronization/activity/backends/google/typing_informations.py +139 -0
- wbcrm/synchronization/activity/backends/google/utils.py +216 -0
- wbcrm/synchronization/activity/backends/outlook/__init__.py +0 -0
- wbcrm/synchronization/activity/backends/outlook/backend.py +576 -0
- wbcrm/synchronization/activity/backends/outlook/msgraph.py +438 -0
- wbcrm/synchronization/activity/backends/outlook/parser.py +423 -0
- wbcrm/synchronization/activity/backends/outlook/tests/__init__.py +0 -0
- wbcrm/synchronization/activity/backends/outlook/tests/conftest.py +1 -0
- wbcrm/synchronization/activity/backends/outlook/tests/fixtures.py +606 -0
- wbcrm/synchronization/activity/backends/outlook/tests/test_admin.py +117 -0
- wbcrm/synchronization/activity/backends/outlook/tests/test_backend.py +269 -0
- wbcrm/synchronization/activity/backends/outlook/tests/test_controller.py +237 -0
- wbcrm/synchronization/activity/backends/outlook/tests/test_parser.py +173 -0
- wbcrm/synchronization/activity/controller.py +545 -0
- wbcrm/synchronization/activity/dynamic_preferences_registry.py +107 -0
- wbcrm/synchronization/activity/preferences.py +21 -0
- wbcrm/synchronization/activity/shortcuts.py +9 -0
- wbcrm/synchronization/activity/signals.py +28 -0
- wbcrm/synchronization/activity/tasks.py +21 -0
- wbcrm/synchronization/activity/urls.py +6 -0
- wbcrm/synchronization/activity/utils.py +46 -0
- wbcrm/synchronization/activity/views.py +37 -0
- wbcrm/synchronization/admin.py +1 -0
- wbcrm/synchronization/apps.py +15 -0
- wbcrm/synchronization/dynamic_preferences_registry.py +1 -0
- wbcrm/synchronization/management.py +36 -0
- wbcrm/synchronization/tasks.py +1 -0
- wbcrm/synchronization/urls.py +5 -0
- wbcrm/tasks.py +312 -0
- wbcrm/tests/__init__.py +0 -0
- wbcrm/tests/accounts/__init__.py +0 -0
- wbcrm/tests/accounts/test_models.py +380 -0
- wbcrm/tests/accounts/test_viewsets.py +87 -0
- wbcrm/tests/conftest.py +76 -0
- wbcrm/tests/disable_signals.py +52 -0
- wbcrm/tests/e2e/__init__.py +1 -0
- wbcrm/tests/e2e/e2e_wbcrm_utility.py +82 -0
- wbcrm/tests/e2e/test_e2e.py +369 -0
- wbcrm/tests/test_assignee_methods.py +39 -0
- wbcrm/tests/test_chartviewsets.py +111 -0
- wbcrm/tests/test_dto.py +63 -0
- wbcrm/tests/test_filters.py +51 -0
- wbcrm/tests/test_models.py +216 -0
- wbcrm/tests/test_recurrence.py +291 -0
- wbcrm/tests/test_report.py +20 -0
- wbcrm/tests/test_serializers.py +170 -0
- wbcrm/tests/test_tasks.py +94 -0
- wbcrm/tests/test_viewsets.py +967 -0
- wbcrm/tests/tests.py +120 -0
- wbcrm/typings.py +107 -0
- wbcrm/urls.py +67 -0
- wbcrm/viewsets/__init__.py +22 -0
- wbcrm/viewsets/accounts.py +121 -0
- wbcrm/viewsets/activities.py +315 -0
- wbcrm/viewsets/buttons/__init__.py +7 -0
- wbcrm/viewsets/buttons/accounts.py +27 -0
- wbcrm/viewsets/buttons/activities.py +68 -0
- wbcrm/viewsets/buttons/signals.py +17 -0
- wbcrm/viewsets/display/__init__.py +12 -0
- wbcrm/viewsets/display/accounts.py +110 -0
- wbcrm/viewsets/display/activities.py +443 -0
- wbcrm/viewsets/display/groups.py +22 -0
- wbcrm/viewsets/display/products.py +105 -0
- wbcrm/viewsets/endpoints/__init__.py +8 -0
- wbcrm/viewsets/endpoints/accounts.py +32 -0
- wbcrm/viewsets/endpoints/activities.py +30 -0
- wbcrm/viewsets/endpoints/groups.py +7 -0
- wbcrm/viewsets/endpoints/products.py +9 -0
- wbcrm/viewsets/groups.py +37 -0
- wbcrm/viewsets/menu/__init__.py +8 -0
- wbcrm/viewsets/menu/accounts.py +18 -0
- wbcrm/viewsets/menu/activities.py +61 -0
- wbcrm/viewsets/menu/groups.py +16 -0
- wbcrm/viewsets/menu/products.py +20 -0
- wbcrm/viewsets/mixins.py +34 -0
- wbcrm/viewsets/previews/__init__.py +1 -0
- wbcrm/viewsets/previews/activities.py +10 -0
- wbcrm/viewsets/products.py +56 -0
- wbcrm/viewsets/recurrence.py +26 -0
- wbcrm/viewsets/titles/__init__.py +13 -0
- wbcrm/viewsets/titles/accounts.py +22 -0
- wbcrm/viewsets/titles/activities.py +61 -0
- wbcrm/viewsets/titles/products.py +13 -0
- wbcrm/viewsets/titles/utils.py +46 -0
- wbcrm/workflows/__init__.py +1 -0
- wbcrm/workflows/assignee_methods.py +25 -0
- wbcrm-2.2.1.dist-info/METADATA +11 -0
- wbcrm-2.2.1.dist-info/RECORD +155 -0
- 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
|