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,576 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
import time
|
|
4
|
+
from datetime import date, timedelta
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from dateutil import parser
|
|
8
|
+
from dateutil.relativedelta import relativedelta
|
|
9
|
+
from django.contrib.auth import get_user_model
|
|
10
|
+
from django.http import HttpRequest, HttpResponse
|
|
11
|
+
from django.utils import timezone
|
|
12
|
+
from django.utils.translation import gettext, gettext_lazy
|
|
13
|
+
from dynamic_preferences.registries import global_preferences_registry
|
|
14
|
+
from psycopg.types.range import TimestamptzRange
|
|
15
|
+
from wbcrm.synchronization.activity.backend import SyncBackend
|
|
16
|
+
from wbcrm.synchronization.activity.utils import flattened_dict_into_nested_dict
|
|
17
|
+
from wbcrm.typings import Activity as ActivityDTO
|
|
18
|
+
from wbcrm.typings import ParticipantStatus as ParticipantStatusDTO
|
|
19
|
+
from wbcrm.typings import User as UserDTO
|
|
20
|
+
|
|
21
|
+
from .msgraph import MicrosoftGraphAPI
|
|
22
|
+
from .parser import OutlookParser, parse
|
|
23
|
+
|
|
24
|
+
User = get_user_model()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OutlookSyncBackend(SyncBackend):
|
|
28
|
+
METADATA_KEY = "outlook"
|
|
29
|
+
|
|
30
|
+
def open(self):
|
|
31
|
+
self.msgraph = MicrosoftGraphAPI()
|
|
32
|
+
|
|
33
|
+
def close(self):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
def _validation_response(self, request: HttpRequest) -> HttpResponse:
|
|
37
|
+
# handle validation
|
|
38
|
+
if token := request.GET.get("validationToken"):
|
|
39
|
+
return HttpResponse(token, content_type="text/plain")
|
|
40
|
+
# handle callback consent permision
|
|
41
|
+
if _consent := request.GET.get("admin_consent"):
|
|
42
|
+
return HttpResponse(_consent, content_type="text/plain")
|
|
43
|
+
return super()._validation_response(request)
|
|
44
|
+
|
|
45
|
+
def _is_inbound_request_valid(self, request: HttpRequest) -> bool:
|
|
46
|
+
is_valid = False
|
|
47
|
+
try:
|
|
48
|
+
if request.body and (json_body := json.loads(request.body)) and (notifications := json_body.get("value")):
|
|
49
|
+
# When many changes occur, MSGraph may send multiple notifications that correspond to different subscriptions in the same POST request.
|
|
50
|
+
for notification in parse(notifications):
|
|
51
|
+
if (
|
|
52
|
+
notification.get("client_state")
|
|
53
|
+
== global_preferences_registry.manager()["wbactivity_sync__outlook_sync_client_state"]
|
|
54
|
+
and (resource_data := notification.get("resource_data"))
|
|
55
|
+
and (event_type := resource_data.get("@odata.type", "").lower())
|
|
56
|
+
and (event_type == "#microsoft.graph.event")
|
|
57
|
+
):
|
|
58
|
+
is_valid = True
|
|
59
|
+
else:
|
|
60
|
+
return False
|
|
61
|
+
except json.decoder.JSONDecodeError:
|
|
62
|
+
return False
|
|
63
|
+
return is_valid
|
|
64
|
+
|
|
65
|
+
def _get_events_from_request(self, request: HttpRequest) -> list[dict[str, Any]]:
|
|
66
|
+
events = []
|
|
67
|
+
for notification in parse(json.loads(request.body)["value"]):
|
|
68
|
+
event = {
|
|
69
|
+
"change_type": notification["change_type"],
|
|
70
|
+
"resource": notification["resource"],
|
|
71
|
+
"subscription_id": notification["subscription_id"],
|
|
72
|
+
}
|
|
73
|
+
if data := self.msgraph.get_event_by_resource(notification["resource"]):
|
|
74
|
+
# Try to get the organizer's event in case if it's an invitation event
|
|
75
|
+
if data["is_organizer"] is True:
|
|
76
|
+
event["organizer_resource"] = notification["resource"]
|
|
77
|
+
elif (
|
|
78
|
+
(tenant_id := self.msgraph.get_tenant_id(data["organizer.email_address.address"]))
|
|
79
|
+
and data["is_organizer"] is False
|
|
80
|
+
and (organizer_data := self.msgraph.get_event_by_uid(tenant_id, data["uid"]))
|
|
81
|
+
):
|
|
82
|
+
event["organizer_resource"] = f"Users/{tenant_id}/Events/{organizer_data['id']}"
|
|
83
|
+
data = organizer_data
|
|
84
|
+
event.update(data)
|
|
85
|
+
events.append(event)
|
|
86
|
+
return events
|
|
87
|
+
|
|
88
|
+
def _deserialize(self, event: dict[str, Any], include_metadata: bool = True) -> tuple[ActivityDTO, bool, UserDTO]:
|
|
89
|
+
is_deleted = False
|
|
90
|
+
if event.get("change_type") == "deleted" or not event.get("id") or event.get("is_cancelled"):
|
|
91
|
+
is_deleted = True
|
|
92
|
+
user_dto = None
|
|
93
|
+
if subscription_id := event.get("subscription_id"):
|
|
94
|
+
user_dto = UserDTO(metadata={self.METADATA_KEY: {"subscription": {"id": subscription_id}}})
|
|
95
|
+
|
|
96
|
+
if include_metadata:
|
|
97
|
+
_metadata = {"event_uid": event["uid"], "event_id": event["id"]} if event.get("id") else {}
|
|
98
|
+
if resource := event.get("resource"):
|
|
99
|
+
_metadata["resources"] = [resource]
|
|
100
|
+
if organizer_resource := event.get("organizer_resource"):
|
|
101
|
+
_metadata["organizer_resource"] = organizer_resource
|
|
102
|
+
elif is_deleted and resource:
|
|
103
|
+
_metadata["organizer_resource"] = resource
|
|
104
|
+
metadata = {self.METADATA_KEY: _metadata}
|
|
105
|
+
else:
|
|
106
|
+
metadata = {}
|
|
107
|
+
|
|
108
|
+
if event.get("id"):
|
|
109
|
+
start = OutlookParser.convert_string_to_datetime(event["start.date_time"], event["start.time_zone"])
|
|
110
|
+
end = OutlookParser.convert_string_to_datetime(event["end.date_time"], event["end.time_zone"])
|
|
111
|
+
if start == end:
|
|
112
|
+
end += timedelta(seconds=1)
|
|
113
|
+
period = TimestamptzRange(start, end)
|
|
114
|
+
if event["is_all_day"] is True:
|
|
115
|
+
period = OutlookParser.convert_to_all_day_period(period)
|
|
116
|
+
participants, conference_room = OutlookParser.deserialize_participants(event)
|
|
117
|
+
recurrence_dict = OutlookParser.deserialize_recurring_activities(event)
|
|
118
|
+
if event.get("is_reminder_on"):
|
|
119
|
+
reminder_choice = OutlookParser.convert_reminder_minutes_to_choice(
|
|
120
|
+
event.get("reminder_minutes_before_start")
|
|
121
|
+
)
|
|
122
|
+
else:
|
|
123
|
+
reminder_choice = ActivityDTO.ReminderChoice.NEVER.name
|
|
124
|
+
activity_dto = ActivityDTO(
|
|
125
|
+
metadata=metadata,
|
|
126
|
+
title=event["subject"] if event.get("subject") else "(No Subject)",
|
|
127
|
+
period=period,
|
|
128
|
+
description=event.get("body.content", ""),
|
|
129
|
+
participants=participants,
|
|
130
|
+
creator=OutlookParser.deserialize_person(
|
|
131
|
+
event["organizer.email_address.address"], event["organizer.email_address.name"]
|
|
132
|
+
),
|
|
133
|
+
visibility=OutlookParser.convert_sensitivity_to_visibility(event.get("sensitivity")),
|
|
134
|
+
reminder_choice=reminder_choice,
|
|
135
|
+
is_cancelled=event.get("is_cancelled", False),
|
|
136
|
+
all_day=event.get("is_all_day", False),
|
|
137
|
+
online_meeting=event.get("is_online_meeting", False),
|
|
138
|
+
location=event.get("location.display_name"),
|
|
139
|
+
conference_room=conference_room[0] if conference_room else None,
|
|
140
|
+
recurrence_end=recurrence_dict["recurrence_end"],
|
|
141
|
+
recurrence_count=recurrence_dict["recurrence_count"],
|
|
142
|
+
repeat_choice=recurrence_dict["repeat_choice"],
|
|
143
|
+
)
|
|
144
|
+
if (
|
|
145
|
+
event["type"] == "seriesMaster"
|
|
146
|
+
and (tenant_id := self.msgraph.get_tenant_id(activity_dto.creator.email))
|
|
147
|
+
and (
|
|
148
|
+
occurrences := self.msgraph.get_instances_event(
|
|
149
|
+
tenant_id, event["id"], start.date(), end.date() + relativedelta(years=10)
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
):
|
|
153
|
+
recurring_activities = []
|
|
154
|
+
for occurrence in occurrences:
|
|
155
|
+
instance_dto, *_ = self._deserialize(occurrence, include_metadata=False)
|
|
156
|
+
instance_dto.recurrence_end = activity_dto.recurrence_end
|
|
157
|
+
instance_dto.recurrence_count = activity_dto.recurrence_count
|
|
158
|
+
instance_dto.repeat_choice = activity_dto.repeat_choice
|
|
159
|
+
instance_dto.metadata = {
|
|
160
|
+
self.METADATA_KEY: {
|
|
161
|
+
**metadata[self.METADATA_KEY],
|
|
162
|
+
"occurrence_id": occurrence["id"],
|
|
163
|
+
"occurrence_resource": f"Users/{tenant_id}/Events/{occurrence['id']}",
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (extension := self.msgraph.get_extension_event(tenant_id, occurrence["id"])) and (
|
|
167
|
+
act_id := extension.get("activity_id")
|
|
168
|
+
):
|
|
169
|
+
instance_dto.id = act_id
|
|
170
|
+
recurring_activities.append(instance_dto)
|
|
171
|
+
activity_dto.recurring_activities = recurring_activities
|
|
172
|
+
else:
|
|
173
|
+
activity_dto = ActivityDTO(metadata=metadata, title=event.get("subject", "(No Subject)"))
|
|
174
|
+
return activity_dto, is_deleted, user_dto
|
|
175
|
+
|
|
176
|
+
def _serialize(self, activity_dto: ActivityDTO, created: bool = False) -> dict[str, Any]:
|
|
177
|
+
attendees = OutlookParser.serialize_participants(activity_dto.participants)
|
|
178
|
+
if activity_dto.conference_room:
|
|
179
|
+
attendees.append(OutlookParser.serialize_conference_room(activity_dto.conference_room))
|
|
180
|
+
|
|
181
|
+
activity_dict = {
|
|
182
|
+
"subject": activity_dto.title,
|
|
183
|
+
"start": {
|
|
184
|
+
"dateTime": activity_dto.period.lower.isoformat(),
|
|
185
|
+
"timeZone": activity_dto.period.lower.tzname(),
|
|
186
|
+
},
|
|
187
|
+
"end": {
|
|
188
|
+
"dateTime": activity_dto.period.upper.isoformat(),
|
|
189
|
+
"timeZone": activity_dto.period.upper.tzname(),
|
|
190
|
+
},
|
|
191
|
+
"body": {
|
|
192
|
+
"contentType": "HTML",
|
|
193
|
+
"content": activity_dto.description,
|
|
194
|
+
},
|
|
195
|
+
"attendees": attendees,
|
|
196
|
+
"sensitivity": OutlookParser.convert_visibility_to_sensitivity(activity_dto.visibility),
|
|
197
|
+
"reminderMinutesBeforeStart": OutlookParser.convert_reminder_choice_to_minutes(
|
|
198
|
+
activity_dto.reminder_choice
|
|
199
|
+
),
|
|
200
|
+
"isReminderOn": False if activity_dto.reminder_choice == ActivityDTO.ReminderChoice.NEVER.name else True,
|
|
201
|
+
"isAllDay": activity_dto.all_day,
|
|
202
|
+
"responseRequested": True,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if activity_dto.online_meeting:
|
|
206
|
+
activity_dict.update(
|
|
207
|
+
{"allowNewTimeProposals": True, "isOnlineMeeting": True, "onlineMeetingProvider": "teamsForBusiness"}
|
|
208
|
+
)
|
|
209
|
+
if activity_dto.location:
|
|
210
|
+
activity_dict["location"] = {
|
|
211
|
+
"displayName": activity_dto.location
|
|
212
|
+
if isinstance(activity_dto.location, str)
|
|
213
|
+
else str(activity_dto.location)
|
|
214
|
+
}
|
|
215
|
+
activity_dict["locations"] = [{"displayName": activity_dto.location}] if activity_dto.location else []
|
|
216
|
+
if created and activity_dto.is_recurrent:
|
|
217
|
+
activity_dict.update(OutlookParser.serialize_recurring_activities(activity_dto))
|
|
218
|
+
return activity_dict
|
|
219
|
+
|
|
220
|
+
def _stream_deletion(self, activity_dto: ActivityDTO):
|
|
221
|
+
if event := self.get_external_event(activity_dto):
|
|
222
|
+
if tenant_id := self.msgraph.get_tenant_id(activity_dto.creator.email):
|
|
223
|
+
self.msgraph.delete_event(tenant_id, event["id"])
|
|
224
|
+
|
|
225
|
+
def _stream_creation(
|
|
226
|
+
self, activity_dto: ActivityDTO, activity_dict: dict[str, Any]
|
|
227
|
+
) -> tuple[ActivityDTO, dict[str, Any]]:
|
|
228
|
+
if not self.get_external_event(activity_dto) and (
|
|
229
|
+
tenant_id := self.msgraph.get_tenant_id(activity_dto.creator.email)
|
|
230
|
+
):
|
|
231
|
+
if event := self.msgraph.create_event(tenant_id, activity_dict):
|
|
232
|
+
metadata_list = self._get_metadata_from_event(activity_dto, event)
|
|
233
|
+
for act_dto, _metadata in metadata_list:
|
|
234
|
+
if act_dto.is_recurrent:
|
|
235
|
+
self._stream_extension_event(act_dto, _metadata)
|
|
236
|
+
return metadata_list
|
|
237
|
+
|
|
238
|
+
def _stream_update(
|
|
239
|
+
self,
|
|
240
|
+
activity_dto: ActivityDTO,
|
|
241
|
+
activity_dict: dict[str, Any],
|
|
242
|
+
only_participants_changed: bool = False,
|
|
243
|
+
external_participants: list = [],
|
|
244
|
+
keep_external_description: bool = False,
|
|
245
|
+
) -> tuple[ActivityDTO, dict[str, Any]]:
|
|
246
|
+
if old_event := self.get_external_event(activity_dto):
|
|
247
|
+
if tenant_id := self.msgraph.get_tenant_id(activity_dto.creator.email):
|
|
248
|
+
# When a participant is removed, we send a complete update of the event to remove this person in outlook
|
|
249
|
+
activity_dict["attendees"] += external_participants
|
|
250
|
+
if only_participants_changed:
|
|
251
|
+
# An event update that includes only the attendees property in the request body sends a meeting update to only the attendees that have changed.
|
|
252
|
+
# An event update that removes an attendee specified as a member of a distribution list sends a meeting update to all the attendees.
|
|
253
|
+
activity_dict = {"attendees": activity_dict["attendees"]}
|
|
254
|
+
elif keep_external_description:
|
|
255
|
+
# preserve body when description is not synchronized especially meeting blob for online meeting.
|
|
256
|
+
activity_dict["body"]["content"] = old_event.get("body.content", "")
|
|
257
|
+
|
|
258
|
+
if (
|
|
259
|
+
activity_dto.is_recurrent
|
|
260
|
+
and activity_dto.propagate_for_all_children
|
|
261
|
+
and not activity_dto.is_leaf
|
|
262
|
+
and (old_master_event := self.get_external_event(activity_dto, master_event=True))
|
|
263
|
+
):
|
|
264
|
+
if activity_dto.is_root:
|
|
265
|
+
self.msgraph.update_event(tenant_id, old_master_event["id"], activity_dict)
|
|
266
|
+
else:
|
|
267
|
+
return self._stream_update_from_occurrence(activity_dto, old_master_event, activity_dict)
|
|
268
|
+
else:
|
|
269
|
+
self.msgraph.update_event(tenant_id, old_event["id"], activity_dict)
|
|
270
|
+
elif not activity_dto.metadata.get(self.METADATA_KEY):
|
|
271
|
+
# creation of recurring activity is done only during the creation phase of the activity
|
|
272
|
+
if activity_dto.is_recurrent and activity_dict.get("recurrence"):
|
|
273
|
+
activity_dict.pop("recurrence")
|
|
274
|
+
return self._stream_creation(activity_dto, activity_dict)
|
|
275
|
+
|
|
276
|
+
def _stream_update_from_occurrence(self, activity_dto: ActivityDTO, old_master_event: dict, activity_dict: dict):
|
|
277
|
+
if (tenant_id := self.msgraph.get_tenant_id(activity_dto.creator.email)) and (
|
|
278
|
+
instances_dict := self._get_occurrences_from_master_event(
|
|
279
|
+
old_master_event, start_date_as_key=True, from_date=activity_dto.period.lower.date()
|
|
280
|
+
)
|
|
281
|
+
):
|
|
282
|
+
# Update recurrence pattern from old root to new root
|
|
283
|
+
remaining_old_recurrence = {
|
|
284
|
+
"recurrence": flattened_dict_into_nested_dict(OutlookParser.serialize_event_keys(old_master_event))[
|
|
285
|
+
"recurrence"
|
|
286
|
+
]
|
|
287
|
+
}
|
|
288
|
+
last_start_old = activity_dto.period.lower - timedelta(days=1)
|
|
289
|
+
if remaining_old_recurrence := OutlookParser.serialize_recurrence_range_with_end_date(
|
|
290
|
+
remaining_old_recurrence, last_start_old
|
|
291
|
+
):
|
|
292
|
+
self.msgraph.update_event(tenant_id, old_master_event["id"], remaining_old_recurrence)
|
|
293
|
+
# Create recurrence from new root
|
|
294
|
+
new_recurrence = OutlookParser.serialize_recurring_activities(activity_dto)
|
|
295
|
+
last_start_new = sorted(instances_dict.keys()).pop()
|
|
296
|
+
if new_recurrence := OutlookParser.serialize_recurrence_range_with_end_date(
|
|
297
|
+
new_recurrence, last_start_new
|
|
298
|
+
):
|
|
299
|
+
activity_dict.update(new_recurrence)
|
|
300
|
+
if new_master_event := self.msgraph.create_event(tenant_id, activity_dict):
|
|
301
|
+
# delete occurrences that had been deleted in the past
|
|
302
|
+
if new_instances_dict := self._get_occurrences_from_master_event(
|
|
303
|
+
new_master_event, start_date_as_key=True
|
|
304
|
+
):
|
|
305
|
+
for key, value in new_instances_dict.items():
|
|
306
|
+
if not instances_dict.get(key):
|
|
307
|
+
self.msgraph.delete_event(tenant_id, value["id"])
|
|
308
|
+
metadata_list = self._get_metadata_from_event(activity_dto, new_master_event)
|
|
309
|
+
for act_dto, _metadata in metadata_list:
|
|
310
|
+
if act_dto.is_recurrent:
|
|
311
|
+
self._stream_extension_event(act_dto, _metadata)
|
|
312
|
+
return metadata_list
|
|
313
|
+
|
|
314
|
+
def _stream_forward(self, activity_dto: ActivityDTO, participants_dto: list[ParticipantStatusDTO]):
|
|
315
|
+
if (event := self.get_external_event(activity_dto)) and (
|
|
316
|
+
tenant_id := self.msgraph.get_tenant_id(activity_dto.creator.email)
|
|
317
|
+
):
|
|
318
|
+
new_attendees = []
|
|
319
|
+
participants, _ = OutlookParser.deserialize_participants(event)
|
|
320
|
+
external_emails_participants = [participant.person.email for participant in participants]
|
|
321
|
+
for participant_dto in participants_dto:
|
|
322
|
+
# forward if it is not the organizer or if it is not already part of the participants
|
|
323
|
+
if (
|
|
324
|
+
participant_dto.person.email != activity_dto.creator.email
|
|
325
|
+
and participant_dto.person.email not in external_emails_participants
|
|
326
|
+
):
|
|
327
|
+
new_attendees.append(OutlookParser.serialize_person(participant_dto.person))
|
|
328
|
+
self.msgraph.forward_event(tenant_id, event["id"], new_attendees)
|
|
329
|
+
|
|
330
|
+
def _stream_participant_change(
|
|
331
|
+
self, participant_dto: ParticipantStatusDTO, is_deleted: bool = False, wait_before_changing: bool = False
|
|
332
|
+
):
|
|
333
|
+
if (organizer_event := self.get_external_event(participant_dto.activity)) and (
|
|
334
|
+
tenant_id := self.msgraph.get_tenant_id(participant_dto.person.email)
|
|
335
|
+
):
|
|
336
|
+
if wait_before_changing:
|
|
337
|
+
time.sleep(10)
|
|
338
|
+
if invitation_event := self.msgraph.get_event_by_uid(tenant_id, organizer_event["uid"]):
|
|
339
|
+
if participant_dto.person.email == participant_dto.activity.creator.email:
|
|
340
|
+
if is_deleted or participant_dto.status == ParticipantStatusDTO.ParticipationStatus.CANCELLED.name:
|
|
341
|
+
pass # self.msgraph.cancel_event(tenant_id, invitation_event["id"]) # delete activity if organizer is removed of participants
|
|
342
|
+
else:
|
|
343
|
+
if is_deleted or participant_dto.status == ParticipantStatusDTO.ParticipationStatus.CANCELLED.name:
|
|
344
|
+
self.msgraph.decline_event(tenant_id, invitation_event["id"])
|
|
345
|
+
elif participant_dto.status == ParticipantStatusDTO.ParticipationStatus.MAYBE.name:
|
|
346
|
+
self.msgraph.tentatively_accept_event(tenant_id, invitation_event["id"])
|
|
347
|
+
elif participant_dto.status in [
|
|
348
|
+
ParticipantStatusDTO.ParticipationStatus.ATTENDS.name,
|
|
349
|
+
ParticipantStatusDTO.ParticipationStatus.ATTENDS_DIGITALLY.name,
|
|
350
|
+
]:
|
|
351
|
+
self.msgraph.accept_event(tenant_id, invitation_event["id"])
|
|
352
|
+
|
|
353
|
+
def _stream_extension_event(self, activity_dto: ActivityDTO, metadata: dict | None = None) -> None:
|
|
354
|
+
metadata = metadata if metadata else activity_dto.metadata
|
|
355
|
+
if (
|
|
356
|
+
(tenant_id := self.msgraph.get_tenant_id(activity_dto.creator.email))
|
|
357
|
+
and (_metadata := metadata.get(self.METADATA_KEY))
|
|
358
|
+
and (occurrence_id := _metadata.get("occurrence_id"))
|
|
359
|
+
):
|
|
360
|
+
self.msgraph.update_or_create_extension_event(tenant_id, occurrence_id, {"activity_id": activity_dto.id})
|
|
361
|
+
|
|
362
|
+
def _set_web_hook(self, user: "User") -> dict[str, dict[str, Any]]:
|
|
363
|
+
response = {}
|
|
364
|
+
self.open()
|
|
365
|
+
if (
|
|
366
|
+
(outlook := user.metadata.get(self.METADATA_KEY))
|
|
367
|
+
and (subscription := outlook.get("subscription"))
|
|
368
|
+
and (ms_subscription := self.msgraph.subscription(subscription.get("id")))
|
|
369
|
+
):
|
|
370
|
+
response = self.msgraph._renew_subscription(ms_subscription.get("id"))
|
|
371
|
+
elif tenant_id := self.msgraph.get_tenant_id(user.email):
|
|
372
|
+
response = self.msgraph._subscribe(f"users/{tenant_id}/events/", "created, updated, deleted")
|
|
373
|
+
else:
|
|
374
|
+
raise Exception(gettext("Outlook TenantId not found for: ") + str(user))
|
|
375
|
+
self.close()
|
|
376
|
+
return {"subscription": response} if response else user.metadata.get(self.METADATA_KEY, {})
|
|
377
|
+
|
|
378
|
+
def _stop_web_hook(self, user: "User") -> dict[str, dict[str, Any]]:
|
|
379
|
+
if (
|
|
380
|
+
(outlook_backend := user.metadata.get(self.METADATA_KEY))
|
|
381
|
+
and (subscription := outlook_backend.get("subscription"))
|
|
382
|
+
and (subscription_id := subscription.get("id"))
|
|
383
|
+
):
|
|
384
|
+
self.open()
|
|
385
|
+
self.msgraph._unsubscribe(subscription_id)
|
|
386
|
+
self.close()
|
|
387
|
+
del user.metadata[self.METADATA_KEY]["subscription"]
|
|
388
|
+
else:
|
|
389
|
+
raise ValueError(str(user) + gettext(" has no active webhook"))
|
|
390
|
+
return user.metadata.get(self.METADATA_KEY)
|
|
391
|
+
|
|
392
|
+
def check_web_hook(self, user: "User", raise_error: bool = True) -> bool:
|
|
393
|
+
error = ""
|
|
394
|
+
self.open()
|
|
395
|
+
if (outlook := user.metadata.get(self.METADATA_KEY)) and (subscription := outlook.get("subscription")):
|
|
396
|
+
if not self.msgraph.subscription(subscription.get("id")):
|
|
397
|
+
error = gettext("Webhook is invalid, Remove or Stop it and Set again please. ")
|
|
398
|
+
else:
|
|
399
|
+
error = gettext("Webhook not found. ")
|
|
400
|
+
if error and raise_error:
|
|
401
|
+
if user_tenant_id := self.msgraph.get_tenant_id(user.email):
|
|
402
|
+
tenant_ids = [
|
|
403
|
+
items[1]
|
|
404
|
+
for sub in self.msgraph.subscriptions()
|
|
405
|
+
if (resource := sub.get("resource")) and (items := resource.split("/")) and len(items) > 2
|
|
406
|
+
]
|
|
407
|
+
error += gettext(
|
|
408
|
+
"Number of subscriptions found in outlook for {} out of the total number: {}/{}."
|
|
409
|
+
).format(user.email, tenant_ids.count(user_tenant_id), len(tenant_ids))
|
|
410
|
+
else:
|
|
411
|
+
error += gettext("TenantId not found for ") + str(user)
|
|
412
|
+
raise Exception(error)
|
|
413
|
+
self.close()
|
|
414
|
+
return False if error else True
|
|
415
|
+
|
|
416
|
+
def renew_web_hooks(self) -> None:
|
|
417
|
+
self.open()
|
|
418
|
+
lookup = {f"metadata__{self.METADATA_KEY}__subscription__id__isnull": False}
|
|
419
|
+
for user in User.objects.filter(**lookup):
|
|
420
|
+
if response := self.msgraph._renew_subscription(user.metadata[self.METADATA_KEY]["subscription"]["id"]):
|
|
421
|
+
user.metadata[self.METADATA_KEY]["subscription"] = response
|
|
422
|
+
user.save()
|
|
423
|
+
self.close()
|
|
424
|
+
|
|
425
|
+
def _get_webhook_inconsistencies(self) -> str:
|
|
426
|
+
self.open()
|
|
427
|
+
subscriptions = self.msgraph.subscriptions()
|
|
428
|
+
calls = set(
|
|
429
|
+
list(
|
|
430
|
+
map(
|
|
431
|
+
lambda x: x["id"],
|
|
432
|
+
list(filter(lambda x: x["resource"] == "/communications/callRecords", subscriptions)),
|
|
433
|
+
)
|
|
434
|
+
)
|
|
435
|
+
)
|
|
436
|
+
calendars = set(
|
|
437
|
+
list(
|
|
438
|
+
map(
|
|
439
|
+
lambda x: x["id"],
|
|
440
|
+
list(filter(lambda x: bool(re.match(r"users\/.*\/events\/", x["resource"])), subscriptions)),
|
|
441
|
+
)
|
|
442
|
+
)
|
|
443
|
+
)
|
|
444
|
+
lookup = f"metadata__{self.METADATA_KEY}__subscription__id"
|
|
445
|
+
stored_calendars = set(User.objects.filter(**{f"{lookup}__isnull": False}).values_list(lookup, flat=True))
|
|
446
|
+
message = ""
|
|
447
|
+
if len(calls) == 0:
|
|
448
|
+
message += gettext_lazy("<li>No Call Record subscription found in Microsoft</li>")
|
|
449
|
+
if calendars != stored_calendars:
|
|
450
|
+
diff = calendars.difference(stored_calendars)
|
|
451
|
+
diff_inv = stored_calendars.difference(calendars)
|
|
452
|
+
message += gettext_lazy(
|
|
453
|
+
"""
|
|
454
|
+
<li>Number of calendar subscription not found in our system : <b>{}</b></li>
|
|
455
|
+
<p>{}</p>
|
|
456
|
+
|
|
457
|
+
<li>Number of calendar subscription assumed to be active not found in outlook: <b>{}</b></li>
|
|
458
|
+
<p>{}</p>
|
|
459
|
+
"""
|
|
460
|
+
).format(len(diff), diff, len(diff_inv), diff_inv)
|
|
461
|
+
if message == "":
|
|
462
|
+
for subscription in subscriptions:
|
|
463
|
+
if expiration_date_time := subscription.get("expiration_date_time"):
|
|
464
|
+
date_time = parser.parse(expiration_date_time)
|
|
465
|
+
diff = date_time - timezone.now()
|
|
466
|
+
if diff.days < 1:
|
|
467
|
+
message += f"Resource: {subscription['resource']} expires on {date_time} (in {diff})"
|
|
468
|
+
self.close()
|
|
469
|
+
return message
|
|
470
|
+
|
|
471
|
+
def get_external_event(self, activity_dto: ActivityDTO, master_event: bool = False) -> dict:
|
|
472
|
+
event = None
|
|
473
|
+
metadata = activity_dto.metadata.get(self.METADATA_KEY, {})
|
|
474
|
+
if master_event or not activity_dto.is_recurrent:
|
|
475
|
+
if resource := metadata.get("organizer_resource"):
|
|
476
|
+
event = self.msgraph.get_event_by_resource(resource)
|
|
477
|
+
if (creator := activity_dto.creator) and (tenant_id := self.msgraph.get_tenant_id(creator.email)):
|
|
478
|
+
if not event and (event_id := metadata.get("event_id")):
|
|
479
|
+
event = self.msgraph.get_event(tenant_id, event_id)
|
|
480
|
+
if not event and (event_uid := metadata.get("event_uid")):
|
|
481
|
+
event = self.msgraph.get_event_by_uid(tenant_id, event_uid)
|
|
482
|
+
else:
|
|
483
|
+
if resource := metadata.get("occurrence_resource"):
|
|
484
|
+
event = self.msgraph.get_event_by_resource(resource)
|
|
485
|
+
elif (
|
|
486
|
+
not event
|
|
487
|
+
and (creator := activity_dto.creator)
|
|
488
|
+
and (tenant_id := self.msgraph.get_tenant_id(creator.email))
|
|
489
|
+
and metadata.get("occurrence_id")
|
|
490
|
+
):
|
|
491
|
+
event = self.msgraph.get_event(tenant_id, metadata["occurrence_id"])
|
|
492
|
+
return event
|
|
493
|
+
|
|
494
|
+
def get_external_participants(
|
|
495
|
+
self, activity_dto: ActivityDTO, internal_participants_dto: list[ParticipantStatusDTO]
|
|
496
|
+
) -> list:
|
|
497
|
+
external_participants = []
|
|
498
|
+
if event := self.get_external_event(activity_dto):
|
|
499
|
+
internal_emails = [
|
|
500
|
+
participant_dto.person.email for participant_dto in internal_participants_dto if participant_dto.person
|
|
501
|
+
]
|
|
502
|
+
for participant in event.get("attendees", []):
|
|
503
|
+
if participant["emailAddress"]["address"] not in internal_emails:
|
|
504
|
+
external_participants.append(participant)
|
|
505
|
+
return external_participants
|
|
506
|
+
|
|
507
|
+
def _is_participant_valid(self, user: "User") -> bool:
|
|
508
|
+
try:
|
|
509
|
+
return super()._is_participant_valid(user) and self.check_web_hook(user, raise_error=False)
|
|
510
|
+
except Exception:
|
|
511
|
+
return False
|
|
512
|
+
|
|
513
|
+
def _generate_event_metadata(self, tenant_id: str, master_event: dict, occurrence_event: dict = {}):
|
|
514
|
+
resource = f"Users/{tenant_id}/Events/{master_event['id']}"
|
|
515
|
+
metadata = {
|
|
516
|
+
self.METADATA_KEY: {
|
|
517
|
+
"resources": [resource],
|
|
518
|
+
"organizer_resource": resource,
|
|
519
|
+
"event_uid": master_event["uid"],
|
|
520
|
+
"event_id": master_event["id"],
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if occurrence_event:
|
|
524
|
+
metadata[self.METADATA_KEY].update(
|
|
525
|
+
{
|
|
526
|
+
"occurrence_id": occurrence_event["id"],
|
|
527
|
+
"occurrence_resource": f"Users/{tenant_id}/Events/{occurrence_event['id']}",
|
|
528
|
+
}
|
|
529
|
+
)
|
|
530
|
+
return metadata
|
|
531
|
+
|
|
532
|
+
def _get_metadata_from_event(self, activity_dto: ActivityDTO, event: dict) -> tuple[ActivityDTO, dict[str, Any]]:
|
|
533
|
+
metadata_list = []
|
|
534
|
+
if tenant_id := self.msgraph.get_tenant_id(activity_dto.creator.email):
|
|
535
|
+
if event.get("type") == "seriesMaster" and (
|
|
536
|
+
instances_dict := self._get_occurrences_from_master_event(event, start_date_as_key=True)
|
|
537
|
+
):
|
|
538
|
+
occurrence = instances_dict.get(activity_dto.period.lower.date(), {})
|
|
539
|
+
metadata_list.append((activity_dto, self._generate_event_metadata(tenant_id, event, occurrence)))
|
|
540
|
+
instances_dto = activity_dto.recurring_activities + activity_dto.invalid_recurring_activities
|
|
541
|
+
for instance_dto in instances_dto:
|
|
542
|
+
if occurrence := instances_dict.get(instance_dto.period.lower.date()):
|
|
543
|
+
metadata_list.append(
|
|
544
|
+
(instance_dto, self._generate_event_metadata(tenant_id, event, occurrence))
|
|
545
|
+
)
|
|
546
|
+
else:
|
|
547
|
+
metadata_list.append((activity_dto, self._generate_event_metadata(tenant_id, event)))
|
|
548
|
+
return metadata_list
|
|
549
|
+
|
|
550
|
+
def _get_occurrences_from_master_event(
|
|
551
|
+
self, event: dict, start_date_as_key: bool = False, from_date: date = None
|
|
552
|
+
) -> dict:
|
|
553
|
+
"""
|
|
554
|
+
Dict of instances event whose key is id by default otherwise start date and the value is the occurrence event data.
|
|
555
|
+
"""
|
|
556
|
+
start = (
|
|
557
|
+
from_date
|
|
558
|
+
if from_date
|
|
559
|
+
else OutlookParser.convert_string_to_datetime(event["start.date_time"], event["start.time_zone"]).date()
|
|
560
|
+
)
|
|
561
|
+
end = OutlookParser.convert_string_to_datetime(
|
|
562
|
+
event["end.date_time"], event["end.time_zone"]
|
|
563
|
+
).date() + relativedelta(years=10)
|
|
564
|
+
tenant_id = self.msgraph.get_tenant_id(event["organizer.email_address.address"])
|
|
565
|
+
occurrences_dict = {}
|
|
566
|
+
if occurrences := self.msgraph.get_instances_event(tenant_id, event["id"], start, end):
|
|
567
|
+
if start_date_as_key:
|
|
568
|
+
occurrences_dict = {
|
|
569
|
+
OutlookParser.convert_string_to_datetime(
|
|
570
|
+
occurrence["start.date_time"], occurrence["start.time_zone"]
|
|
571
|
+
).date(): occurrence
|
|
572
|
+
for occurrence in occurrences
|
|
573
|
+
}
|
|
574
|
+
else:
|
|
575
|
+
occurrences_dict = {occurrence["id"]: occurrence for occurrence in occurrences}
|
|
576
|
+
return occurrences_dict
|