wbcrm 1.56.8__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.
Files changed (182) hide show
  1. wbcrm/__init__.py +1 -0
  2. wbcrm/admin/__init__.py +5 -0
  3. wbcrm/admin/accounts.py +60 -0
  4. wbcrm/admin/activities.py +104 -0
  5. wbcrm/admin/events.py +43 -0
  6. wbcrm/admin/groups.py +8 -0
  7. wbcrm/admin/products.py +9 -0
  8. wbcrm/apps.py +5 -0
  9. wbcrm/configurations/__init__.py +1 -0
  10. wbcrm/configurations/base.py +16 -0
  11. wbcrm/dynamic_preferences_registry.py +38 -0
  12. wbcrm/factories/__init__.py +14 -0
  13. wbcrm/factories/accounts.py +57 -0
  14. wbcrm/factories/activities.py +124 -0
  15. wbcrm/factories/groups.py +24 -0
  16. wbcrm/factories/products.py +11 -0
  17. wbcrm/filters/__init__.py +10 -0
  18. wbcrm/filters/accounts.py +80 -0
  19. wbcrm/filters/activities.py +204 -0
  20. wbcrm/filters/groups.py +21 -0
  21. wbcrm/filters/products.py +38 -0
  22. wbcrm/filters/signals.py +95 -0
  23. wbcrm/fixtures/wbcrm.json +1215 -0
  24. wbcrm/kpi_handlers/activities.py +171 -0
  25. wbcrm/locale/de/LC_MESSAGES/django.mo +0 -0
  26. wbcrm/locale/de/LC_MESSAGES/django.po +1557 -0
  27. wbcrm/locale/de/LC_MESSAGES/django.po.translated +1630 -0
  28. wbcrm/locale/en/LC_MESSAGES/django.mo +0 -0
  29. wbcrm/locale/en/LC_MESSAGES/django.po +1466 -0
  30. wbcrm/locale/fr/LC_MESSAGES/django.mo +0 -0
  31. wbcrm/locale/fr/LC_MESSAGES/django.po +1467 -0
  32. wbcrm/migrations/0001_initial_squashed_squashed_0032_productcompanyrelationship_alter_product_prospects_and_more.py +3948 -0
  33. wbcrm/migrations/0002_alter_activity_repeat_choice.py +32 -0
  34. wbcrm/migrations/0003_remove_activity_external_id_and_more.py +63 -0
  35. wbcrm/migrations/0004_alter_activity_status.py +28 -0
  36. wbcrm/migrations/0005_account_accountrole_accountroletype_and_more.py +182 -0
  37. wbcrm/migrations/0006_alter_activity_location.py +17 -0
  38. wbcrm/migrations/0007_alter_account_status.py +23 -0
  39. wbcrm/migrations/0008_alter_activity_options.py +16 -0
  40. wbcrm/migrations/0009_alter_account_is_public.py +19 -0
  41. wbcrm/migrations/0010_alter_account_reference_id.py +17 -0
  42. wbcrm/migrations/0011_activity_summary.py +22 -0
  43. wbcrm/migrations/0012_alter_activity_summary.py +17 -0
  44. wbcrm/migrations/0013_account_action_plan_account_relationship_status_and_more.py +34 -0
  45. wbcrm/migrations/0014_alter_account_relationship_status.py +24 -0
  46. wbcrm/migrations/0015_alter_activity_type.py +23 -0
  47. wbcrm/migrations/0016_auto_20241205_1015.py +106 -0
  48. wbcrm/migrations/0017_event.py +40 -0
  49. wbcrm/migrations/0018_activity_search_vector.py +24 -0
  50. wbcrm/migrations/__init__.py +0 -0
  51. wbcrm/models/__init__.py +5 -0
  52. wbcrm/models/accounts.py +648 -0
  53. wbcrm/models/activities.py +1419 -0
  54. wbcrm/models/events.py +15 -0
  55. wbcrm/models/groups.py +119 -0
  56. wbcrm/models/llm/activity_summaries.py +41 -0
  57. wbcrm/models/llm/analyze_relationship.py +50 -0
  58. wbcrm/models/products.py +86 -0
  59. wbcrm/models/recurrence.py +280 -0
  60. wbcrm/preferences.py +13 -0
  61. wbcrm/report/activity_report.py +110 -0
  62. wbcrm/serializers/__init__.py +23 -0
  63. wbcrm/serializers/accounts.py +141 -0
  64. wbcrm/serializers/activities.py +525 -0
  65. wbcrm/serializers/groups.py +30 -0
  66. wbcrm/serializers/products.py +58 -0
  67. wbcrm/serializers/recurrence.py +91 -0
  68. wbcrm/serializers/signals.py +71 -0
  69. wbcrm/static/wbcrm/markdown/documentation/activity.md +86 -0
  70. wbcrm/static/wbcrm/markdown/documentation/activitytype.md +20 -0
  71. wbcrm/static/wbcrm/markdown/documentation/group.md +2 -0
  72. wbcrm/static/wbcrm/markdown/documentation/product.md +11 -0
  73. wbcrm/synchronization/__init__.py +0 -0
  74. wbcrm/synchronization/activity/__init__.py +0 -0
  75. wbcrm/synchronization/activity/admin.py +73 -0
  76. wbcrm/synchronization/activity/backend.py +214 -0
  77. wbcrm/synchronization/activity/backends/__init__.py +0 -0
  78. wbcrm/synchronization/activity/backends/google/__init__.py +2 -0
  79. wbcrm/synchronization/activity/backends/google/google_calendar_backend.py +406 -0
  80. wbcrm/synchronization/activity/backends/google/request_utils/__init__.py +16 -0
  81. wbcrm/synchronization/activity/backends/google/request_utils/external_to_internal/create.py +75 -0
  82. wbcrm/synchronization/activity/backends/google/request_utils/external_to_internal/delete.py +78 -0
  83. wbcrm/synchronization/activity/backends/google/request_utils/external_to_internal/update.py +155 -0
  84. wbcrm/synchronization/activity/backends/google/request_utils/internal_to_external/update.py +181 -0
  85. wbcrm/synchronization/activity/backends/google/tasks.py +21 -0
  86. wbcrm/synchronization/activity/backends/google/tests/__init__.py +0 -0
  87. wbcrm/synchronization/activity/backends/google/tests/conftest.py +1 -0
  88. wbcrm/synchronization/activity/backends/google/tests/test_data.py +81 -0
  89. wbcrm/synchronization/activity/backends/google/tests/test_google_backend.py +319 -0
  90. wbcrm/synchronization/activity/backends/google/tests/test_utils.py +274 -0
  91. wbcrm/synchronization/activity/backends/google/typing_informations.py +139 -0
  92. wbcrm/synchronization/activity/backends/google/utils.py +217 -0
  93. wbcrm/synchronization/activity/backends/outlook/__init__.py +0 -0
  94. wbcrm/synchronization/activity/backends/outlook/backend.py +593 -0
  95. wbcrm/synchronization/activity/backends/outlook/msgraph.py +436 -0
  96. wbcrm/synchronization/activity/backends/outlook/parser.py +432 -0
  97. wbcrm/synchronization/activity/backends/outlook/tests/__init__.py +0 -0
  98. wbcrm/synchronization/activity/backends/outlook/tests/conftest.py +1 -0
  99. wbcrm/synchronization/activity/backends/outlook/tests/fixtures.py +606 -0
  100. wbcrm/synchronization/activity/backends/outlook/tests/test_admin.py +118 -0
  101. wbcrm/synchronization/activity/backends/outlook/tests/test_backend.py +274 -0
  102. wbcrm/synchronization/activity/backends/outlook/tests/test_controller.py +249 -0
  103. wbcrm/synchronization/activity/backends/outlook/tests/test_parser.py +174 -0
  104. wbcrm/synchronization/activity/controller.py +627 -0
  105. wbcrm/synchronization/activity/dynamic_preferences_registry.py +119 -0
  106. wbcrm/synchronization/activity/preferences.py +27 -0
  107. wbcrm/synchronization/activity/shortcuts.py +16 -0
  108. wbcrm/synchronization/activity/tasks.py +21 -0
  109. wbcrm/synchronization/activity/urls.py +7 -0
  110. wbcrm/synchronization/activity/utils.py +46 -0
  111. wbcrm/synchronization/activity/views.py +41 -0
  112. wbcrm/synchronization/admin.py +1 -0
  113. wbcrm/synchronization/apps.py +14 -0
  114. wbcrm/synchronization/dynamic_preferences_registry.py +1 -0
  115. wbcrm/synchronization/management.py +36 -0
  116. wbcrm/synchronization/tasks.py +1 -0
  117. wbcrm/synchronization/urls.py +5 -0
  118. wbcrm/tasks.py +264 -0
  119. wbcrm/templates/email/activity.html +98 -0
  120. wbcrm/templates/email/activity_report.html +6 -0
  121. wbcrm/templates/email/daily_summary.html +72 -0
  122. wbcrm/templates/email/global_daily_summary.html +85 -0
  123. wbcrm/tests/__init__.py +0 -0
  124. wbcrm/tests/accounts/__init__.py +0 -0
  125. wbcrm/tests/accounts/test_models.py +393 -0
  126. wbcrm/tests/accounts/test_viewsets.py +88 -0
  127. wbcrm/tests/conftest.py +76 -0
  128. wbcrm/tests/disable_signals.py +62 -0
  129. wbcrm/tests/e2e/__init__.py +1 -0
  130. wbcrm/tests/e2e/e2e_wbcrm_utility.py +83 -0
  131. wbcrm/tests/e2e/test_e2e.py +370 -0
  132. wbcrm/tests/test_assignee_methods.py +40 -0
  133. wbcrm/tests/test_chartviewsets.py +112 -0
  134. wbcrm/tests/test_dto.py +64 -0
  135. wbcrm/tests/test_filters.py +52 -0
  136. wbcrm/tests/test_models.py +217 -0
  137. wbcrm/tests/test_recurrence.py +292 -0
  138. wbcrm/tests/test_report.py +21 -0
  139. wbcrm/tests/test_serializers.py +171 -0
  140. wbcrm/tests/test_tasks.py +95 -0
  141. wbcrm/tests/test_viewsets.py +967 -0
  142. wbcrm/tests/tests.py +121 -0
  143. wbcrm/typings.py +109 -0
  144. wbcrm/urls.py +67 -0
  145. wbcrm/viewsets/__init__.py +22 -0
  146. wbcrm/viewsets/accounts.py +122 -0
  147. wbcrm/viewsets/activities.py +341 -0
  148. wbcrm/viewsets/buttons/__init__.py +7 -0
  149. wbcrm/viewsets/buttons/accounts.py +27 -0
  150. wbcrm/viewsets/buttons/activities.py +89 -0
  151. wbcrm/viewsets/buttons/signals.py +17 -0
  152. wbcrm/viewsets/display/__init__.py +12 -0
  153. wbcrm/viewsets/display/accounts.py +110 -0
  154. wbcrm/viewsets/display/activities.py +444 -0
  155. wbcrm/viewsets/display/groups.py +22 -0
  156. wbcrm/viewsets/display/products.py +105 -0
  157. wbcrm/viewsets/endpoints/__init__.py +8 -0
  158. wbcrm/viewsets/endpoints/accounts.py +25 -0
  159. wbcrm/viewsets/endpoints/activities.py +30 -0
  160. wbcrm/viewsets/endpoints/groups.py +7 -0
  161. wbcrm/viewsets/endpoints/products.py +9 -0
  162. wbcrm/viewsets/groups.py +38 -0
  163. wbcrm/viewsets/menu/__init__.py +8 -0
  164. wbcrm/viewsets/menu/accounts.py +18 -0
  165. wbcrm/viewsets/menu/activities.py +49 -0
  166. wbcrm/viewsets/menu/groups.py +16 -0
  167. wbcrm/viewsets/menu/products.py +20 -0
  168. wbcrm/viewsets/mixins.py +35 -0
  169. wbcrm/viewsets/previews/__init__.py +1 -0
  170. wbcrm/viewsets/previews/activities.py +10 -0
  171. wbcrm/viewsets/products.py +57 -0
  172. wbcrm/viewsets/recurrence.py +27 -0
  173. wbcrm/viewsets/titles/__init__.py +13 -0
  174. wbcrm/viewsets/titles/accounts.py +23 -0
  175. wbcrm/viewsets/titles/activities.py +61 -0
  176. wbcrm/viewsets/titles/products.py +13 -0
  177. wbcrm/viewsets/titles/utils.py +46 -0
  178. wbcrm/workflows/__init__.py +1 -0
  179. wbcrm/workflows/assignee_methods.py +25 -0
  180. wbcrm-1.56.8.dist-info/METADATA +11 -0
  181. wbcrm-1.56.8.dist-info/RECORD +182 -0
  182. wbcrm-1.56.8.dist-info/WHEEL +5 -0
@@ -0,0 +1,436 @@
1
+ import json
2
+ from contextlib import suppress
3
+ from datetime import date, timedelta
4
+ from urllib.parse import urlparse
5
+
6
+ import pandas as pd
7
+ import requests
8
+ from django.contrib.sites.models import Site
9
+ from django.db.utils import ProgrammingError
10
+ from django.http import HttpResponse
11
+ from django.utils import timezone
12
+ from dynamic_preferences.registries import global_preferences_registry
13
+ from rest_framework import status
14
+
15
+ from .parser import parse
16
+
17
+
18
+ class MicrosoftGraphAPI:
19
+ def __init__(self):
20
+ with suppress(ProgrammingError):
21
+ global_preferences = global_preferences_registry.manager()
22
+ if (credentials := global_preferences["wbactivity_sync__outlook_sync_credentials"]) and (
23
+ serivce_account_file := json.loads(credentials)
24
+ ):
25
+ self.authority = serivce_account_file.get("authority")
26
+ self.client_id = serivce_account_file.get("client_id")
27
+ self.client_secret = serivce_account_file.get("client_secret")
28
+ self.token_endpoint = serivce_account_file.get("token_endpoint")
29
+ self.notification_url = serivce_account_file.get("notification_url")
30
+ self.graph_url = serivce_account_file.get("graph_url")
31
+
32
+ if global_preferences["wbactivity_sync__outlook_sync_access_token"] == "":
33
+ self._get_access_token()
34
+ else:
35
+ # try to get a data in the api (i.e list of applications), update the token if it is expired
36
+ self.applications()
37
+ else:
38
+ self.authority = None
39
+ self.client_id = None
40
+ self.client_secret = None
41
+ self.token_endpoint = None
42
+ self.notification_url = None
43
+ self.graph_url = None
44
+
45
+ def _get_administrator_consent(self) -> HttpResponse:
46
+ url = f"{self.authority}/adminconsent?client_id={self.client_id}&state=12345&redirect_uri={self.notification_url}"
47
+ if urlparse(url):
48
+ response = self._query(url, access_token=False)
49
+ if response:
50
+ return response
51
+ else:
52
+ raise ValueError("get administrator consent does not return response 200")
53
+ else:
54
+ raise ValueError("Invalid URL")
55
+
56
+ def _get_access_token(self) -> str | None:
57
+ # Get administrator consent
58
+ self._get_administrator_consent()
59
+ # Get an access token
60
+ url = f"{self.authority}{self.token_endpoint}"
61
+ payload = {
62
+ "grant_type": "client_credentials",
63
+ "client_id": self.client_id,
64
+ "scope": "https://graph.microsoft.com/.default",
65
+ "client_secret": self.client_secret,
66
+ }
67
+
68
+ response = self._query(url, method="POST", data=payload, is_json=False, access_token=False)
69
+ data = None
70
+ if response:
71
+ if response.json():
72
+ data = response.json().get("access_token")
73
+ global_preferences = global_preferences_registry.manager()
74
+ global_preferences["wbactivity_sync__outlook_sync_access_token"] = data
75
+ else:
76
+ raise ValueError(response, response.json())
77
+ return data
78
+
79
+ def _subscribe(
80
+ self, resource: str, change_type: str, minutes: int = 4230, raise_error: bool = False
81
+ ) -> dict | None:
82
+ subscription_data: dict = {
83
+ "changeType": change_type,
84
+ "notificationUrl": self.notification_url,
85
+ "resource": resource,
86
+ "clientState": global_preferences_registry.manager()["wbactivity_sync__outlook_sync_client_state"],
87
+ "latestSupportedTlsVersion": "v1_2",
88
+ }
89
+ if minutes:
90
+ # maximum time of subscription 4230 minutes (under 3 days)
91
+ date = timezone.now() + timedelta(minutes=minutes)
92
+ date = date.strftime("%Y-%m-%dT%H:%M:%SZ")
93
+ subscription_data["expirationDateTime"] = date
94
+ url = f"{self.graph_url}/subscriptions"
95
+ response = self._query(url, method="POST", data=json.dumps(subscription_data))
96
+ data = None
97
+ if response and response.status_code == status.HTTP_201_CREATED:
98
+ if response.json():
99
+ data = parse(response.json(), scalar_value=True)
100
+ if data and isinstance(data, list):
101
+ data = data[0]
102
+ elif raise_error:
103
+ raise ValueError(response, response.json())
104
+ return data
105
+
106
+ def _unsubscribe(self, subscription_id: str) -> HttpResponse:
107
+ url = f"{self.graph_url}/subscriptions/{subscription_id}"
108
+ return self._query(url, method="DELETE")
109
+
110
+ def _renew_subscription(self, subscription_id: str, minutes: int = 4230, raise_error: bool = False) -> dict | None:
111
+ url = f"{self.graph_url}/subscriptions/{subscription_id}"
112
+ date = timezone.now() + timedelta(minutes=minutes)
113
+ date = date.strftime("%Y-%m-%dT%H:%M:%SZ")
114
+ data = {"expirationDateTime": date}
115
+ response = self._query(url, method="PATCH", data=json.dumps(data))
116
+ data = None
117
+ if response and response.status_code == status.HTTP_200_OK:
118
+ if response.json():
119
+ data = parse(response.json(), scalar_value=True)
120
+ if data and isinstance(data, list):
121
+ data = data[0]
122
+ elif raise_error:
123
+ raise ValueError(response, response.json())
124
+ return data
125
+
126
+ def subscriptions(self) -> list[dict]:
127
+ url = f"{self.graph_url}/subscriptions"
128
+ response = self._query(url)
129
+ data = []
130
+ if response:
131
+ if datum := response.json():
132
+ data = parse(datum.get("value"))
133
+ url = datum.get("@odata.nextLink")
134
+ while url:
135
+ response = self._query(url)
136
+ if datum := response.json():
137
+ data += parse(datum.get("value"))
138
+ url = datum.get("@odata.nextLink")
139
+ else:
140
+ url = None
141
+ else:
142
+ raise ValueError(response, response.json())
143
+ return data
144
+
145
+ def subscription(self, subscription_id: str, raise_error: bool = False) -> dict | None:
146
+ url = f"{self.graph_url}/subscriptions/{subscription_id}"
147
+ response = self._query(url)
148
+ data = None
149
+ if response and response.status_code == status.HTTP_200_OK:
150
+ if response.json():
151
+ data = parse(response.json(), scalar_value=True)
152
+ if data and isinstance(data, list):
153
+ data = data[0]
154
+ elif raise_error:
155
+ raise ValueError(response, response.json())
156
+ return data
157
+
158
+ def applications(self) -> dict | None:
159
+ # List of applications in MS graph
160
+ url = f"{self.graph_url}/applications"
161
+ response = self._query(url)
162
+ data = None
163
+ if response:
164
+ if datum := response.json():
165
+ data = parse(datum.get("value"))
166
+ url = datum.get("@odata.nextLink")
167
+ while url:
168
+ response = self._query(url)
169
+ if datum := response.json():
170
+ data += parse(datum.get("value"))
171
+ url = datum.get("@odata.nextLink")
172
+ else:
173
+ url = None
174
+ elif response.status_code == status.HTTP_401_UNAUTHORIZED:
175
+ self._get_access_token()
176
+ if response := self._query(url):
177
+ data = response.json()
178
+ else:
179
+ raise ValueError(response, response.json())
180
+ return data
181
+
182
+ def user(self, email: str, raise_error: bool = False) -> dict | None:
183
+ query_params = {"$select": "id, userPrincipalName, displayName"}
184
+ url = f"{self.graph_url}/users/{email}"
185
+ response = self._query(url, params=query_params)
186
+ data = None
187
+ if response:
188
+ if response.json():
189
+ data = parse(response.json(), scalar_value=True)
190
+ if data and isinstance(data, list):
191
+ data = data[0]
192
+ elif raise_error:
193
+ raise ValueError(response, response.json())
194
+ return data
195
+
196
+ def users(self, filter_params: bool = True) -> dict:
197
+ query_params = {
198
+ "$select": "id,displayName,businessPhones,mobilePhone, userPrincipalName, mail,email, mailNickname, givenName, surname, imAddresses"
199
+ }
200
+ url = f"{self.graph_url}/users"
201
+ if filter_params:
202
+ response = self._query(url, params=query_params)
203
+ else:
204
+ response = self._query(url)
205
+ data = None
206
+ if response:
207
+ data = parse(response.json().get("value"))
208
+ else:
209
+ raise ValueError(response, response.json())
210
+ return data
211
+
212
+ def get_tenant_id(self, email: str) -> str | None:
213
+ with suppress(Exception):
214
+ if msuser := self.user(email):
215
+ return msuser.get("id", None)
216
+ return None
217
+
218
+ def delta_changes_events(self, tenant_id: str, minutes: int) -> list:
219
+ start = (timezone.now() - timedelta(minutes=minutes)).strftime("%Y-%m-%dT%H:%M:%SZ")
220
+ end = (timezone.now()).strftime("%Y-%m-%dT%H:%M:%SZ")
221
+ url = f"{self.graph_url}/users/{tenant_id}/calendarView/delta?startdatetime={start}&enddatetime={end}"
222
+ response = self._query(url)
223
+ datum = []
224
+ if response:
225
+ if datum := response.json():
226
+ _value = datum.get("value")
227
+ external_event_list = parse(_value)
228
+ url = datum.get("@odata.deltaLink")
229
+ while _value:
230
+ response = self._query(url)
231
+ datum = response.json()
232
+ _value = datum.get("value")
233
+ external_event_list += parse(_value)
234
+ url = datum.get("@odata.deltaLink")
235
+ else:
236
+ raise ValueError(response, response.json())
237
+ return datum
238
+
239
+ def create_event(self, tenant_id: str, event_body: dict) -> dict | None:
240
+ url = f"{self.graph_url}/users/{tenant_id}/events"
241
+ return self._query_create(url, event_body)
242
+
243
+ def delete_event(self, tenant_id: str, external_id: str) -> None:
244
+ url = f"{self.graph_url}/users/{tenant_id}/events/{external_id}"
245
+ return self._query(url, method="DELETE")
246
+
247
+ def update_event(self, tenant_id: str, external_id: str, event_body: dict) -> dict | None:
248
+ url = f"{self.graph_url}/users/{tenant_id}/events/{external_id}"
249
+ return self._query_update(url, event_body)
250
+
251
+ def get_event(
252
+ self, tenant_id: str, external_id: str, extension: bool = False, extension_id: str = ""
253
+ ) -> dict | None:
254
+ url = f"{self.graph_url}/users/{tenant_id}/events/{external_id}"
255
+ if extension:
256
+ extension_id = (
257
+ extension_id if extension_id else ".".join(reversed(Site.objects.get_current().domain.split(".")))
258
+ )
259
+ url += f"?$expand=Extensions($filter=Id eq '{extension_id}')"
260
+ return self._query_get(url)
261
+
262
+ def update_or_create_extension_event(
263
+ self, tenant_id: str, external_id: str, extension_body: dict, extension_id: str = ""
264
+ ):
265
+ extension_id = (
266
+ extension_id if extension_id else ".".join(reversed(Site.objects.get_current().domain.split(".")))
267
+ )
268
+ url = f"{self.graph_url}/users/{tenant_id}/events/{external_id}/extensions"
269
+ data = {"@odata.type": "microsoft.graph.openTypeExtension", "extensionName": extension_id, **extension_body}
270
+ if self.get_extension_event(tenant_id, external_id, extension_id):
271
+ url += f"/{extension_id}"
272
+ return self._query_update(url, data)
273
+ else:
274
+ return self._query_create(url, data)
275
+
276
+ def get_extension_event(self, tenant_id: str, external_id: str, extension_id: str = "") -> dict | None:
277
+ extension_id = (
278
+ extension_id if extension_id else ".".join(reversed(Site.objects.get_current().domain.split(".")))
279
+ )
280
+ url = f"{self.graph_url}/users/{tenant_id}/events/{external_id}/extensions/{extension_id}"
281
+ return self._query_get(url)
282
+
283
+ def delete_extension_event(self, tenant_id: str, external_id: str, extension_id: str = "") -> None:
284
+ extension_id = (
285
+ extension_id if extension_id else ".".join(reversed(Site.objects.get_current().domain.split(".")))
286
+ )
287
+ url = f"{self.graph_url}/users/{tenant_id}/events/{external_id}/extensions/{extension_id}"
288
+ return self._query(url, method="DELETE")
289
+
290
+ def forward_event(self, tenant_id: str, external_id: str, participants: list) -> dict | None:
291
+ url = f"{self.graph_url}/users/{tenant_id}/events/{external_id}/forward"
292
+ data = {
293
+ "ToRecipients": participants,
294
+ }
295
+ return self._query_create(url, data)
296
+
297
+ def get_event_by_uid(self, tenant_id: str, uid: str) -> dict | None:
298
+ url = f"{self.graph_url}/users/{tenant_id}/events?$filter=uid eq '{uid}'"
299
+ event = list_data[0] if (list_data := self._query_get_list(url)) else None
300
+ return event
301
+
302
+ def get_list_events(self, tenant_id: str) -> list:
303
+ url = f"{self.graph_url}/users/{tenant_id}/events"
304
+ return self._query_get_list(url)
305
+
306
+ def get_instances_event(self, tenant_id: str, external_id: str, start: str | date, end: str | date) -> list:
307
+ url = f"{self.graph_url}/users/{tenant_id}/events/{external_id}/instances?startDateTime={start}&endDateTime={end}"
308
+ list_data = self._query_get_list(url)
309
+ return list_data if list_data else []
310
+
311
+ def get_instances_event_by_resource(self, resource: str, start: str | date, end: str | date) -> list:
312
+ url = f"{self.graph_url}/{resource}/instances?startDateTime={start}&endDateTime={end}"
313
+ list_data = self._query_get_list(url)
314
+ return list_data if list_data else []
315
+
316
+ def get_event_by_resource(self, resource: str, extension: bool = False, extension_id: str = "") -> dict | None:
317
+ url = f"{self.graph_url}/{resource}"
318
+ if extension:
319
+ extension_id = (
320
+ extension_id if extension_id else ".".join(reversed(Site.objects.get_current().domain.split(".")))
321
+ )
322
+ url += f"?$expand=Extensions($filter=Id eq '{extension_id}')"
323
+ return self._query_get(url)
324
+
325
+ def delete_event_by_resource(self, resource: str) -> dict | None:
326
+ url = f"{self.graph_url}/{resource}"
327
+ return self._query(url, method="DELETE")
328
+
329
+ def tentatively_accept_event(self, tenant_id: str, external_id: str) -> dict | None:
330
+ url = f"{self.graph_url}/users/{tenant_id}/events/{external_id}/tentativelyAccept"
331
+ data = {"sendResponse": False}
332
+ return self._query_create(url, data)
333
+
334
+ def decline_event(self, tenant_id: str, external_id: str) -> dict | None:
335
+ url = f"{self.graph_url}/users/{tenant_id}/events/{external_id}/decline"
336
+ data = {"sendResponse": False}
337
+ return self._query_create(url, data)
338
+
339
+ def accept_event(self, tenant_id: str, external_id: str) -> dict | None:
340
+ url = f"{self.graph_url}/users/{tenant_id}/events/{external_id}/accept"
341
+ data = {"sendResponse": False}
342
+ return self._query_create(url, data)
343
+
344
+ def cancel_event(self, tenant_id: str, external_id: str) -> dict | None:
345
+ # Only the organizer can cancel an event
346
+ url = f"{self.graph_url}/users/{tenant_id}/events/{external_id}/cancel"
347
+ data = {}
348
+ return self._query_create(url, data)
349
+
350
+ def _query_get(self, url: str, raise_error: bool = False) -> dict | None:
351
+ response = self._query(url)
352
+ data = None
353
+ if response.status_code < 400:
354
+ if response.json():
355
+ data = parse(pd.json_normalize(response.json()))
356
+ if data and isinstance(data, list):
357
+ data = data[0]
358
+ elif raise_error:
359
+ raise ValueError(response, response.json())
360
+ return data
361
+
362
+ def _query_get_list(self, url: str, raise_error: bool = False) -> list:
363
+ response = self._query(url)
364
+ datum = []
365
+ if response:
366
+ if (result := response.json()) and (value := result.get("value")):
367
+ datum = parse(pd.json_normalize(value))
368
+ next_url: str | None = result.get("@odata.nextLink")
369
+ while next_url:
370
+ response = self._query(next_url)
371
+ if (result := response.json()) and (value := result.get("value")):
372
+ datum += parse(pd.json_normalize(value))
373
+ next_url = result.get("@odata.nextLink")
374
+ else:
375
+ next_url = None
376
+ elif raise_error:
377
+ raise ValueError(response, response.json())
378
+ return datum
379
+
380
+ def _query_create(self, url: str, event_data: dict) -> dict | None:
381
+ response = self._query(url, method="POST", data=json.dumps(event_data))
382
+ data = None
383
+ if response.status_code < 400:
384
+ try:
385
+ if response.json():
386
+ data = parse(pd.json_normalize(response.json()))
387
+ if data and isinstance(data, list):
388
+ data = data[0]
389
+ except requests.exceptions.InvalidJSONError:
390
+ pass
391
+ else:
392
+ raise ValueError(response, response.__dict__)
393
+ return data
394
+
395
+ def _query_update(self, url: str, event_data: dict) -> dict | None:
396
+ response = self._query(url, method="PATCH", data=json.dumps(event_data))
397
+ data = None
398
+ if response:
399
+ if response.json():
400
+ data = parse(pd.json_normalize(response.json()))
401
+ else:
402
+ if response.status_code not in [status.HTTP_503_SERVICE_UNAVAILABLE, status.HTTP_409_CONFLICT]:
403
+ raise ValueError(response, response.json())
404
+ else:
405
+ import time
406
+
407
+ time.sleep(60)
408
+ if response := self._query(url, method="PATCH", data=json.dumps(data)):
409
+ if response.json():
410
+ data = parse(pd.json_normalize(response.json()))
411
+ if data and isinstance(data, list):
412
+ data = data[0]
413
+ return data
414
+
415
+ def _query(
416
+ self,
417
+ url: str,
418
+ method: str = "GET",
419
+ data: dict | str | None = None,
420
+ params: dict | None = None,
421
+ access_token: bool = True,
422
+ is_json: bool = True,
423
+ ) -> HttpResponse:
424
+ headers = {"content-type": "application/json" if is_json else "application/x-www-form-urlencoded"}
425
+ if access_token:
426
+ global_preferences = global_preferences_registry.manager()
427
+ headers["Authorization"] = f'Bearer {global_preferences["wbactivity_sync__outlook_sync_access_token"]}'
428
+ if method == "POST":
429
+ response = requests.post(url, data=data, headers=headers, timeout=10)
430
+ elif method == "DELETE":
431
+ response = requests.delete(url, headers=headers, timeout=10)
432
+ elif method == "PATCH":
433
+ response = requests.patch(url, data=data, headers=headers, timeout=10)
434
+ else:
435
+ response = requests.get(url, headers=headers, params=params, timeout=10)
436
+ return response