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,438 @@
|
|
|
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
|
+
try:
|
|
214
|
+
if msuser := self.user(email):
|
|
215
|
+
return msuser.get("id", None)
|
|
216
|
+
except Exception:
|
|
217
|
+
pass
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
def delta_changes_events(self, tenant_id: str, minutes: int) -> list:
|
|
221
|
+
start = (timezone.now() - timedelta(minutes=minutes)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
222
|
+
end = (timezone.now()).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
223
|
+
url = f"{self.graph_url}/users/{tenant_id}/calendarView/delta?startdatetime={start}&enddatetime={end}"
|
|
224
|
+
response = self._query(url)
|
|
225
|
+
datum = []
|
|
226
|
+
if response:
|
|
227
|
+
if datum := response.json():
|
|
228
|
+
_value = datum.get("value")
|
|
229
|
+
external_event_list = parse(_value)
|
|
230
|
+
url = datum.get("@odata.deltaLink")
|
|
231
|
+
while _value:
|
|
232
|
+
response = self._query(url)
|
|
233
|
+
datum = response.json()
|
|
234
|
+
_value = datum.get("value")
|
|
235
|
+
external_event_list += parse(_value)
|
|
236
|
+
url = datum.get("@odata.deltaLink")
|
|
237
|
+
else:
|
|
238
|
+
raise ValueError(response, response.json())
|
|
239
|
+
return datum
|
|
240
|
+
|
|
241
|
+
def create_event(self, tenant_id: str, event_body: dict) -> dict | None:
|
|
242
|
+
url = f"{self.graph_url}/users/{tenant_id}/events"
|
|
243
|
+
return self._query_create(url, event_body)
|
|
244
|
+
|
|
245
|
+
def delete_event(self, tenant_id: str, external_id: str) -> None:
|
|
246
|
+
url = f"{self.graph_url}/users/{tenant_id}/events/{external_id}"
|
|
247
|
+
return self._query(url, method="DELETE")
|
|
248
|
+
|
|
249
|
+
def update_event(self, tenant_id: str, external_id: str, event_body: dict) -> dict | None:
|
|
250
|
+
url = f"{self.graph_url}/users/{tenant_id}/events/{external_id}"
|
|
251
|
+
return self._query_update(url, event_body)
|
|
252
|
+
|
|
253
|
+
def get_event(
|
|
254
|
+
self, tenant_id: str, external_id: str, extension: bool = False, extension_id: str = ""
|
|
255
|
+
) -> dict | None:
|
|
256
|
+
url = f"{self.graph_url}/users/{tenant_id}/events/{external_id}"
|
|
257
|
+
if extension:
|
|
258
|
+
extension_id = (
|
|
259
|
+
extension_id if extension_id else ".".join(reversed(Site.objects.get_current().domain.split(".")))
|
|
260
|
+
)
|
|
261
|
+
url += f"?$expand=Extensions($filter=Id eq '{extension_id}')"
|
|
262
|
+
return self._query_get(url)
|
|
263
|
+
|
|
264
|
+
def update_or_create_extension_event(
|
|
265
|
+
self, tenant_id: str, external_id: str, extension_body: dict, extension_id: str = ""
|
|
266
|
+
):
|
|
267
|
+
extension_id = (
|
|
268
|
+
extension_id if extension_id else ".".join(reversed(Site.objects.get_current().domain.split(".")))
|
|
269
|
+
)
|
|
270
|
+
url = f"{self.graph_url}/users/{tenant_id}/events/{external_id}/extensions"
|
|
271
|
+
data = {"@odata.type": "microsoft.graph.openTypeExtension", "extensionName": extension_id, **extension_body}
|
|
272
|
+
if self.get_extension_event(tenant_id, external_id, extension_id):
|
|
273
|
+
url += f"/{extension_id}"
|
|
274
|
+
return self._query_update(url, data)
|
|
275
|
+
else:
|
|
276
|
+
return self._query_create(url, data)
|
|
277
|
+
|
|
278
|
+
def get_extension_event(self, tenant_id: str, external_id: str, extension_id: str = "") -> dict | None:
|
|
279
|
+
extension_id = (
|
|
280
|
+
extension_id if extension_id else ".".join(reversed(Site.objects.get_current().domain.split(".")))
|
|
281
|
+
)
|
|
282
|
+
url = f"{self.graph_url}/users/{tenant_id}/events/{external_id}/extensions/{extension_id}"
|
|
283
|
+
return self._query_get(url)
|
|
284
|
+
|
|
285
|
+
def delete_extension_event(self, tenant_id: str, external_id: str, extension_id: str = "") -> None:
|
|
286
|
+
extension_id = (
|
|
287
|
+
extension_id if extension_id else ".".join(reversed(Site.objects.get_current().domain.split(".")))
|
|
288
|
+
)
|
|
289
|
+
url = f"{self.graph_url}/users/{tenant_id}/events/{external_id}/extensions/{extension_id}"
|
|
290
|
+
return self._query(url, method="DELETE")
|
|
291
|
+
|
|
292
|
+
def forward_event(self, tenant_id: str, external_id: str, participants: list) -> dict | None:
|
|
293
|
+
url = f"{self.graph_url}/users/{tenant_id}/events/{external_id}/forward"
|
|
294
|
+
data = {
|
|
295
|
+
"ToRecipients": participants,
|
|
296
|
+
}
|
|
297
|
+
return self._query_create(url, data)
|
|
298
|
+
|
|
299
|
+
def get_event_by_uid(self, tenant_id: str, uid: str) -> dict | None:
|
|
300
|
+
url = f"{self.graph_url}/users/{tenant_id}/events?$filter=uid eq '{uid}'"
|
|
301
|
+
event = list_data[0] if (list_data := self._query_get_list(url)) else None
|
|
302
|
+
return event
|
|
303
|
+
|
|
304
|
+
def get_list_events(self, tenant_id: str) -> list:
|
|
305
|
+
url = f"{self.graph_url}/users/{tenant_id}/events"
|
|
306
|
+
return self._query_get_list(url)
|
|
307
|
+
|
|
308
|
+
def get_instances_event(self, tenant_id: str, external_id: str, start: str | date, end: str | date) -> list:
|
|
309
|
+
url = f"{self.graph_url}/users/{tenant_id}/events/{external_id}/instances?startDateTime={start}&endDateTime={end}"
|
|
310
|
+
list_data = self._query_get_list(url)
|
|
311
|
+
return list_data if list_data else []
|
|
312
|
+
|
|
313
|
+
def get_instances_event_by_resource(self, resource: str, start: str | date, end: str | date) -> list:
|
|
314
|
+
url = f"{self.graph_url}/{resource}/instances?startDateTime={start}&endDateTime={end}"
|
|
315
|
+
list_data = self._query_get_list(url)
|
|
316
|
+
return list_data if list_data else []
|
|
317
|
+
|
|
318
|
+
def get_event_by_resource(self, resource: str, extension: bool = False, extension_id: str = "") -> dict | None:
|
|
319
|
+
url = f"{self.graph_url}/{resource}"
|
|
320
|
+
if extension:
|
|
321
|
+
extension_id = (
|
|
322
|
+
extension_id if extension_id else ".".join(reversed(Site.objects.get_current().domain.split(".")))
|
|
323
|
+
)
|
|
324
|
+
url += f"?$expand=Extensions($filter=Id eq '{extension_id}')"
|
|
325
|
+
return self._query_get(url)
|
|
326
|
+
|
|
327
|
+
def delete_event_by_resource(self, resource: str) -> dict | None:
|
|
328
|
+
url = f"{self.graph_url}/{resource}"
|
|
329
|
+
return self._query(url, method="DELETE")
|
|
330
|
+
|
|
331
|
+
def tentatively_accept_event(self, tenant_id: str, external_id: str) -> dict | None:
|
|
332
|
+
url = f"{self.graph_url}/users/{tenant_id}/events/{external_id}/tentativelyAccept"
|
|
333
|
+
data = {"sendResponse": False}
|
|
334
|
+
return self._query_create(url, data)
|
|
335
|
+
|
|
336
|
+
def decline_event(self, tenant_id: str, external_id: str) -> dict | None:
|
|
337
|
+
url = f"{self.graph_url}/users/{tenant_id}/events/{external_id}/decline"
|
|
338
|
+
data = {"sendResponse": False}
|
|
339
|
+
return self._query_create(url, data)
|
|
340
|
+
|
|
341
|
+
def accept_event(self, tenant_id: str, external_id: str) -> dict | None:
|
|
342
|
+
url = f"{self.graph_url}/users/{tenant_id}/events/{external_id}/accept"
|
|
343
|
+
data = {"sendResponse": False}
|
|
344
|
+
return self._query_create(url, data)
|
|
345
|
+
|
|
346
|
+
def cancel_event(self, tenant_id: str, external_id: str) -> dict | None:
|
|
347
|
+
# Only the organizer can cancel an event
|
|
348
|
+
url = f"{self.graph_url}/users/{tenant_id}/events/{external_id}/cancel"
|
|
349
|
+
data = {}
|
|
350
|
+
return self._query_create(url, data)
|
|
351
|
+
|
|
352
|
+
def _query_get(self, url: str, raise_error: bool = False) -> dict | None:
|
|
353
|
+
response = self._query(url)
|
|
354
|
+
data = None
|
|
355
|
+
if response.status_code < 400:
|
|
356
|
+
if response.json():
|
|
357
|
+
data = parse(pd.json_normalize(response.json()))
|
|
358
|
+
if data and isinstance(data, list):
|
|
359
|
+
data = data[0]
|
|
360
|
+
elif raise_error:
|
|
361
|
+
raise ValueError(response, response.json())
|
|
362
|
+
return data
|
|
363
|
+
|
|
364
|
+
def _query_get_list(self, url: str, raise_error: bool = False) -> list:
|
|
365
|
+
response = self._query(url)
|
|
366
|
+
datum = []
|
|
367
|
+
if response:
|
|
368
|
+
if (result := response.json()) and (value := result.get("value")):
|
|
369
|
+
datum = parse(pd.json_normalize(value))
|
|
370
|
+
next_url: str | None = result.get("@odata.nextLink")
|
|
371
|
+
while next_url:
|
|
372
|
+
response = self._query(next_url)
|
|
373
|
+
if (result := response.json()) and (value := result.get("value")):
|
|
374
|
+
datum += parse(pd.json_normalize(value))
|
|
375
|
+
next_url = result.get("@odata.nextLink")
|
|
376
|
+
else:
|
|
377
|
+
next_url = None
|
|
378
|
+
elif raise_error:
|
|
379
|
+
raise ValueError(response, response.json())
|
|
380
|
+
return datum
|
|
381
|
+
|
|
382
|
+
def _query_create(self, url: str, event_data: dict) -> dict | None:
|
|
383
|
+
response = self._query(url, method="POST", data=json.dumps(event_data))
|
|
384
|
+
data = None
|
|
385
|
+
if response.status_code < 400:
|
|
386
|
+
try:
|
|
387
|
+
if response.json():
|
|
388
|
+
data = parse(pd.json_normalize(response.json()))
|
|
389
|
+
if data and isinstance(data, list):
|
|
390
|
+
data = data[0]
|
|
391
|
+
except requests.exceptions.InvalidJSONError:
|
|
392
|
+
pass
|
|
393
|
+
else:
|
|
394
|
+
raise ValueError(response, response.__dict__)
|
|
395
|
+
return data
|
|
396
|
+
|
|
397
|
+
def _query_update(self, url: str, event_data: dict) -> dict | None:
|
|
398
|
+
response = self._query(url, method="PATCH", data=json.dumps(event_data))
|
|
399
|
+
data = None
|
|
400
|
+
if response:
|
|
401
|
+
if response.json():
|
|
402
|
+
data = parse(pd.json_normalize(response.json()))
|
|
403
|
+
else:
|
|
404
|
+
if response.status_code not in [status.HTTP_503_SERVICE_UNAVAILABLE, status.HTTP_409_CONFLICT]:
|
|
405
|
+
raise ValueError(response, response.json())
|
|
406
|
+
else:
|
|
407
|
+
import time
|
|
408
|
+
|
|
409
|
+
time.sleep(60)
|
|
410
|
+
if response := self._query(url, method="PATCH", data=json.dumps(data)):
|
|
411
|
+
if response.json():
|
|
412
|
+
data = parse(pd.json_normalize(response.json()))
|
|
413
|
+
if data and isinstance(data, list):
|
|
414
|
+
data = data[0]
|
|
415
|
+
return data
|
|
416
|
+
|
|
417
|
+
def _query(
|
|
418
|
+
self,
|
|
419
|
+
url: str,
|
|
420
|
+
method: str = "GET",
|
|
421
|
+
data: dict | str | None = None,
|
|
422
|
+
params: dict | None = None,
|
|
423
|
+
access_token: bool = True,
|
|
424
|
+
is_json: bool = True,
|
|
425
|
+
) -> HttpResponse:
|
|
426
|
+
headers = {"content-type": "application/json" if is_json else "application/x-www-form-urlencoded"}
|
|
427
|
+
if access_token:
|
|
428
|
+
global_preferences = global_preferences_registry.manager()
|
|
429
|
+
headers["Authorization"] = f'Bearer {global_preferences["wbactivity_sync__outlook_sync_access_token"]}'
|
|
430
|
+
if method == "POST":
|
|
431
|
+
response = requests.post(url, data=data, headers=headers)
|
|
432
|
+
elif method == "DELETE":
|
|
433
|
+
response = requests.delete(url, headers=headers)
|
|
434
|
+
elif method == "PATCH":
|
|
435
|
+
response = requests.patch(url, data=data, headers=headers)
|
|
436
|
+
else:
|
|
437
|
+
response = requests.get(url, headers=headers, params=params)
|
|
438
|
+
return response
|