wbcrm 1.43.1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (175) hide show
  1. wbcrm/__init__.py +1 -0
  2. wbcrm/admin/__init__.py +5 -0
  3. wbcrm/admin/accounts.py +59 -0
  4. wbcrm/admin/activities.py +102 -0
  5. wbcrm/admin/events.py +42 -0
  6. wbcrm/admin/groups.py +7 -0
  7. wbcrm/admin/products.py +8 -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 +56 -0
  14. wbcrm/factories/activities.py +125 -0
  15. wbcrm/factories/groups.py +23 -0
  16. wbcrm/factories/products.py +10 -0
  17. wbcrm/filters/__init__.py +10 -0
  18. wbcrm/filters/accounts.py +67 -0
  19. wbcrm/filters/activities.py +199 -0
  20. wbcrm/filters/groups.py +20 -0
  21. wbcrm/filters/products.py +37 -0
  22. wbcrm/filters/signals.py +94 -0
  23. wbcrm/fixtures/wbcrm.json +1215 -0
  24. wbcrm/kpi_handlers/activities.py +171 -0
  25. wbcrm/locale/de/LC_MESSAGES/django.po +1538 -0
  26. wbcrm/migrations/0001_initial_squashed_squashed_0032_productcompanyrelationship_alter_product_prospects_and_more.py +3948 -0
  27. wbcrm/migrations/0002_alter_activity_repeat_choice.py +32 -0
  28. wbcrm/migrations/0003_remove_activity_external_id_and_more.py +63 -0
  29. wbcrm/migrations/0004_alter_activity_status.py +28 -0
  30. wbcrm/migrations/0005_account_accountrole_accountroletype_and_more.py +182 -0
  31. wbcrm/migrations/0006_alter_activity_location.py +17 -0
  32. wbcrm/migrations/0007_alter_account_status.py +23 -0
  33. wbcrm/migrations/0008_alter_activity_options.py +16 -0
  34. wbcrm/migrations/0009_alter_account_is_public.py +19 -0
  35. wbcrm/migrations/0010_alter_account_reference_id.py +17 -0
  36. wbcrm/migrations/0011_activity_summary.py +22 -0
  37. wbcrm/migrations/0012_alter_activity_summary.py +17 -0
  38. wbcrm/migrations/0013_account_action_plan_account_relationship_status_and_more.py +34 -0
  39. wbcrm/migrations/0014_alter_account_relationship_status.py +24 -0
  40. wbcrm/migrations/0015_alter_activity_type.py +23 -0
  41. wbcrm/migrations/0016_auto_20241205_1015.py +106 -0
  42. wbcrm/migrations/0017_event.py +40 -0
  43. wbcrm/migrations/__init__.py +0 -0
  44. wbcrm/models/__init__.py +5 -0
  45. wbcrm/models/accounts.py +637 -0
  46. wbcrm/models/activities.py +1341 -0
  47. wbcrm/models/events.py +12 -0
  48. wbcrm/models/groups.py +118 -0
  49. wbcrm/models/llm/activity_summaries.py +33 -0
  50. wbcrm/models/llm/analyze_relationship.py +54 -0
  51. wbcrm/models/products.py +83 -0
  52. wbcrm/models/recurrence.py +279 -0
  53. wbcrm/preferences.py +14 -0
  54. wbcrm/report/activity_report.py +110 -0
  55. wbcrm/serializers/__init__.py +23 -0
  56. wbcrm/serializers/accounts.py +126 -0
  57. wbcrm/serializers/activities.py +526 -0
  58. wbcrm/serializers/groups.py +30 -0
  59. wbcrm/serializers/products.py +57 -0
  60. wbcrm/serializers/recurrence.py +90 -0
  61. wbcrm/serializers/signals.py +70 -0
  62. wbcrm/static/wbcrm/markdown/documentation/activity.md +86 -0
  63. wbcrm/static/wbcrm/markdown/documentation/activitytype.md +20 -0
  64. wbcrm/static/wbcrm/markdown/documentation/group.md +2 -0
  65. wbcrm/static/wbcrm/markdown/documentation/product.md +11 -0
  66. wbcrm/synchronization/__init__.py +0 -0
  67. wbcrm/synchronization/activity/__init__.py +0 -0
  68. wbcrm/synchronization/activity/admin.py +72 -0
  69. wbcrm/synchronization/activity/backend.py +213 -0
  70. wbcrm/synchronization/activity/backends/__init__.py +0 -0
  71. wbcrm/synchronization/activity/backends/google/__init__.py +2 -0
  72. wbcrm/synchronization/activity/backends/google/google_calendar_backend.py +399 -0
  73. wbcrm/synchronization/activity/backends/google/request_utils/__init__.py +16 -0
  74. wbcrm/synchronization/activity/backends/google/request_utils/external_to_internal/create.py +75 -0
  75. wbcrm/synchronization/activity/backends/google/request_utils/external_to_internal/delete.py +78 -0
  76. wbcrm/synchronization/activity/backends/google/request_utils/external_to_internal/update.py +155 -0
  77. wbcrm/synchronization/activity/backends/google/request_utils/internal_to_external/update.py +180 -0
  78. wbcrm/synchronization/activity/backends/google/tasks.py +21 -0
  79. wbcrm/synchronization/activity/backends/google/tests/__init__.py +0 -0
  80. wbcrm/synchronization/activity/backends/google/tests/conftest.py +1 -0
  81. wbcrm/synchronization/activity/backends/google/tests/test_data.py +81 -0
  82. wbcrm/synchronization/activity/backends/google/tests/test_google_backend.py +319 -0
  83. wbcrm/synchronization/activity/backends/google/tests/test_utils.py +274 -0
  84. wbcrm/synchronization/activity/backends/google/typing_informations.py +139 -0
  85. wbcrm/synchronization/activity/backends/google/utils.py +216 -0
  86. wbcrm/synchronization/activity/backends/outlook/__init__.py +0 -0
  87. wbcrm/synchronization/activity/backends/outlook/backend.py +585 -0
  88. wbcrm/synchronization/activity/backends/outlook/msgraph.py +438 -0
  89. wbcrm/synchronization/activity/backends/outlook/parser.py +431 -0
  90. wbcrm/synchronization/activity/backends/outlook/tests/__init__.py +0 -0
  91. wbcrm/synchronization/activity/backends/outlook/tests/conftest.py +1 -0
  92. wbcrm/synchronization/activity/backends/outlook/tests/fixtures.py +606 -0
  93. wbcrm/synchronization/activity/backends/outlook/tests/test_admin.py +117 -0
  94. wbcrm/synchronization/activity/backends/outlook/tests/test_backend.py +273 -0
  95. wbcrm/synchronization/activity/backends/outlook/tests/test_controller.py +248 -0
  96. wbcrm/synchronization/activity/backends/outlook/tests/test_parser.py +173 -0
  97. wbcrm/synchronization/activity/controller.py +624 -0
  98. wbcrm/synchronization/activity/dynamic_preferences_registry.py +119 -0
  99. wbcrm/synchronization/activity/preferences.py +27 -0
  100. wbcrm/synchronization/activity/shortcuts.py +10 -0
  101. wbcrm/synchronization/activity/tasks.py +21 -0
  102. wbcrm/synchronization/activity/urls.py +6 -0
  103. wbcrm/synchronization/activity/utils.py +46 -0
  104. wbcrm/synchronization/activity/views.py +40 -0
  105. wbcrm/synchronization/admin.py +1 -0
  106. wbcrm/synchronization/apps.py +14 -0
  107. wbcrm/synchronization/dynamic_preferences_registry.py +1 -0
  108. wbcrm/synchronization/management.py +36 -0
  109. wbcrm/synchronization/tasks.py +1 -0
  110. wbcrm/synchronization/urls.py +5 -0
  111. wbcrm/tasks.py +312 -0
  112. wbcrm/templates/email/activity.html +98 -0
  113. wbcrm/templates/email/activity_report.html +6 -0
  114. wbcrm/templates/email/daily_summary.html +72 -0
  115. wbcrm/templates/email/global_daily_summary.html +85 -0
  116. wbcrm/tests/__init__.py +0 -0
  117. wbcrm/tests/accounts/__init__.py +0 -0
  118. wbcrm/tests/accounts/test_models.py +380 -0
  119. wbcrm/tests/accounts/test_viewsets.py +87 -0
  120. wbcrm/tests/conftest.py +76 -0
  121. wbcrm/tests/disable_signals.py +52 -0
  122. wbcrm/tests/e2e/__init__.py +1 -0
  123. wbcrm/tests/e2e/e2e_wbcrm_utility.py +82 -0
  124. wbcrm/tests/e2e/test_e2e.py +369 -0
  125. wbcrm/tests/test_assignee_methods.py +39 -0
  126. wbcrm/tests/test_chartviewsets.py +111 -0
  127. wbcrm/tests/test_dto.py +63 -0
  128. wbcrm/tests/test_filters.py +51 -0
  129. wbcrm/tests/test_models.py +216 -0
  130. wbcrm/tests/test_recurrence.py +291 -0
  131. wbcrm/tests/test_report.py +20 -0
  132. wbcrm/tests/test_serializers.py +170 -0
  133. wbcrm/tests/test_tasks.py +94 -0
  134. wbcrm/tests/test_viewsets.py +967 -0
  135. wbcrm/tests/tests.py +120 -0
  136. wbcrm/typings.py +109 -0
  137. wbcrm/urls.py +67 -0
  138. wbcrm/viewsets/__init__.py +22 -0
  139. wbcrm/viewsets/accounts.py +121 -0
  140. wbcrm/viewsets/activities.py +344 -0
  141. wbcrm/viewsets/buttons/__init__.py +7 -0
  142. wbcrm/viewsets/buttons/accounts.py +27 -0
  143. wbcrm/viewsets/buttons/activities.py +88 -0
  144. wbcrm/viewsets/buttons/signals.py +17 -0
  145. wbcrm/viewsets/display/__init__.py +12 -0
  146. wbcrm/viewsets/display/accounts.py +110 -0
  147. wbcrm/viewsets/display/activities.py +443 -0
  148. wbcrm/viewsets/display/groups.py +22 -0
  149. wbcrm/viewsets/display/products.py +105 -0
  150. wbcrm/viewsets/endpoints/__init__.py +8 -0
  151. wbcrm/viewsets/endpoints/accounts.py +32 -0
  152. wbcrm/viewsets/endpoints/activities.py +30 -0
  153. wbcrm/viewsets/endpoints/groups.py +7 -0
  154. wbcrm/viewsets/endpoints/products.py +9 -0
  155. wbcrm/viewsets/groups.py +37 -0
  156. wbcrm/viewsets/menu/__init__.py +8 -0
  157. wbcrm/viewsets/menu/accounts.py +18 -0
  158. wbcrm/viewsets/menu/activities.py +61 -0
  159. wbcrm/viewsets/menu/groups.py +16 -0
  160. wbcrm/viewsets/menu/products.py +20 -0
  161. wbcrm/viewsets/mixins.py +34 -0
  162. wbcrm/viewsets/previews/__init__.py +1 -0
  163. wbcrm/viewsets/previews/activities.py +10 -0
  164. wbcrm/viewsets/products.py +56 -0
  165. wbcrm/viewsets/recurrence.py +26 -0
  166. wbcrm/viewsets/titles/__init__.py +13 -0
  167. wbcrm/viewsets/titles/accounts.py +22 -0
  168. wbcrm/viewsets/titles/activities.py +61 -0
  169. wbcrm/viewsets/titles/products.py +13 -0
  170. wbcrm/viewsets/titles/utils.py +46 -0
  171. wbcrm/workflows/__init__.py +1 -0
  172. wbcrm/workflows/assignee_methods.py +25 -0
  173. wbcrm-1.43.1.dist-info/METADATA +11 -0
  174. wbcrm-1.43.1.dist-info/RECORD +175 -0
  175. wbcrm-1.43.1.dist-info/WHEEL +5 -0
@@ -0,0 +1,431 @@
1
+ import calendar as calendar_reference
2
+ from datetime import datetime
3
+ from typing import Any
4
+
5
+ import pandas as pd
6
+ import pytz
7
+ from dateutil import parser, rrule
8
+ from django.utils.timezone import get_default_timezone, get_default_timezone_name
9
+ from psycopg.types.range import TimestamptzRange
10
+ from wbcore.utils.rrules import (
11
+ convert_rrulestr_to_dict,
12
+ convert_weekday_rrule_to_day_name,
13
+ )
14
+ from wbcrm.typings import Activity as ActivityDTO
15
+ from wbcrm.typings import ConferenceRoom as ConferenceRoomDTO
16
+ from wbcrm.typings import ParticipantStatus as ParticipantStatusDTO
17
+ from wbcrm.typings import Person as PersonDTO
18
+
19
+
20
+ class OutlookParser:
21
+ default_visibility = "normal"
22
+ visibility_map = {
23
+ ActivityDTO.Visibility.PUBLIC.name: default_visibility,
24
+ ActivityDTO.Visibility.PRIVATE.name: "private",
25
+ ActivityDTO.Visibility.CONFIDENTIAL.name: "confidential",
26
+ }
27
+ default_response_status = "notResponded"
28
+ response_status_map = {
29
+ ParticipantStatusDTO.ParticipationStatus.NOTRESPONDED.name: default_response_status,
30
+ ParticipantStatusDTO.ParticipationStatus.ATTENDS.name: "accepted",
31
+ ParticipantStatusDTO.ParticipationStatus.CANCELLED.name: "declined",
32
+ ParticipantStatusDTO.ParticipationStatus.MAYBE.name: "tentativelyAccepted",
33
+ }
34
+ default_reminder_minutes = 15
35
+ reminder_map = {
36
+ ActivityDTO.ReminderChoice.NEVER.name: -1,
37
+ ActivityDTO.ReminderChoice.EVENT_TIME.name: 0,
38
+ ActivityDTO.ReminderChoice.MINUTES_5.name: 5,
39
+ ActivityDTO.ReminderChoice.MINUTES_15.name: default_reminder_minutes,
40
+ ActivityDTO.ReminderChoice.MINUTES_30.name: 30,
41
+ ActivityDTO.ReminderChoice.HOURS_1.name: 60,
42
+ ActivityDTO.ReminderChoice.HOURS_2.name: 120,
43
+ ActivityDTO.ReminderChoice.HOURS_12.name: 720,
44
+ ActivityDTO.ReminderChoice.WEEKS_1.name: 10080,
45
+ }
46
+
47
+ @classmethod
48
+ def convert_visibility_to_sensitivity(cls, visibility: str) -> str:
49
+ return cls.visibility_map.get(visibility, cls.default_visibility)
50
+
51
+ @classmethod
52
+ def convert_sensitivity_to_visibility(cls, sensitivity: str) -> str:
53
+ visibility_map_inv = {v: k for k, v in cls.visibility_map.items()}
54
+ return visibility_map_inv.get(sensitivity, visibility_map_inv[cls.default_visibility])
55
+
56
+ @classmethod
57
+ def convert_participant_status_to_attendee_status(cls, participant_status: str) -> str:
58
+ if participant_status == ParticipantStatusDTO.ParticipationStatus.ATTENDS_DIGITALLY.name:
59
+ return "accepted"
60
+ if participant_status == ParticipantStatusDTO.ParticipationStatus.PENDING_INVITATION.name:
61
+ return cls.default_response_status
62
+ return cls.response_status_map.get(participant_status, cls.default_response_status)
63
+
64
+ @classmethod
65
+ def convert_attendee_status_to_participant_status(cls, attendee_status: str) -> str:
66
+ if attendee_status == "organizer":
67
+ return ParticipantStatusDTO.ParticipationStatus.ATTENDS.name
68
+ response_status_map_inv = {v: k for k, v in cls.response_status_map.items()}
69
+ return response_status_map_inv.get(attendee_status, response_status_map_inv[cls.default_response_status])
70
+
71
+ @classmethod
72
+ def convert_reminder_choice_to_minutes(cls, choice: str) -> int:
73
+ return cls.reminder_map[choice]
74
+
75
+ @classmethod
76
+ def convert_reminder_minutes_to_choice(cls, minutes: int) -> str:
77
+ reminder_map_inv = {v: k for k, v in cls.reminder_map.items()}
78
+ return reminder_map_inv.get(minutes, reminder_map_inv[cls.default_reminder_minutes])
79
+
80
+ @classmethod
81
+ def convert_string_to_datetime(cls, date_time_str: str, time_zone: str) -> datetime:
82
+ date_time = parser.parse(date_time_str)
83
+ try:
84
+ return pytz.timezone(time_zone).localize(date_time)
85
+ except pytz.exceptions.UnknownTimeZoneError:
86
+ return pytz.timezone(get_default_timezone_name()).localize(date_time)
87
+
88
+ @classmethod
89
+ def convert_to_all_day_period(cls, period: TimestamptzRange) -> TimestamptzRange:
90
+ return TimestamptzRange(
91
+ lower=datetime(
92
+ year=period.lower.year, month=period.lower.month, day=period.lower.day, tzinfo=get_default_timezone()
93
+ ),
94
+ upper=datetime(
95
+ year=period.upper.year, month=period.upper.month, day=period.upper.day, tzinfo=get_default_timezone()
96
+ ),
97
+ )
98
+
99
+ @classmethod
100
+ def deserialize_person(cls, email: str, mame: str) -> PersonDTO:
101
+ if (items := mame.split(" ")) and len(items) > 1:
102
+ first_name = items[0]
103
+ last_name = " ".join(items[1:])
104
+ else:
105
+ last_name = first_name = items[0]
106
+ return PersonDTO(first_name=first_name, last_name=last_name, email=email)
107
+
108
+ @classmethod
109
+ def deserialize_conference_room(cls, email: str, name: str) -> ConferenceRoomDTO:
110
+ return ConferenceRoomDTO(name=name, email=email)
111
+
112
+ @classmethod
113
+ def deserialize_participants(cls, event: dict) -> tuple[list[ParticipantStatusDTO], list[ConferenceRoomDTO]]:
114
+ participants = {
115
+ event["organizer.email_address.address"]: ParticipantStatusDTO(
116
+ person=OutlookParser.deserialize_person(
117
+ event["organizer.email_address.address"], event["organizer.email_address.name"]
118
+ ),
119
+ status=ParticipantStatusDTO.ParticipationStatus.ATTENDS.name,
120
+ )
121
+ }
122
+ conference_rooms = {}
123
+ for attendee in event.get("attendees", []):
124
+ if mail_info := attendee.get("emailAddress"):
125
+ email = mail_info["address"]
126
+ name = mail_info.get("name", email)
127
+ if attendee.get("type") == "resource":
128
+ if not conference_rooms.get(email):
129
+ conference_rooms[email] = cls.deserialize_conference_room(email, name)
130
+ else:
131
+ if (status_participant := attendee["status"].get("response")) and (
132
+ event["is_organizer"] or (not event["is_organizer"] and status_participant != "none")
133
+ ):
134
+ status = cls.convert_attendee_status_to_participant_status(status_participant)
135
+ else:
136
+ status = None
137
+ defaults = {"status": status} if status else {}
138
+ if not participants.get(email):
139
+ participants[email] = ParticipantStatusDTO(
140
+ person=cls.deserialize_person(email, name), **defaults
141
+ )
142
+ # handle conference room when user adds conference room from participant list
143
+ for location in event.get("locations", []):
144
+ if location.get("locationType") == "conferenceRoom":
145
+ email = location["locationUri"]
146
+ name = location.get("displayName", email)
147
+ conference_rooms[email] = cls.deserialize_conference_room(email, name)
148
+ return list(participants.values()), list(conference_rooms.values())
149
+
150
+ @classmethod
151
+ def serialize_person(cls, person: PersonDTO) -> dict:
152
+ return {
153
+ "emailAddress": {
154
+ "address": person.email,
155
+ "name": f"{person.first_name} {person.last_name}",
156
+ }
157
+ }
158
+
159
+ @classmethod
160
+ def serialize_conference_room(cls, conference_room_dto=ConferenceRoomDTO) -> dict:
161
+ return {
162
+ "emailAddress": {"address": conference_room_dto.email, "name": conference_room_dto.name},
163
+ "type": "resource",
164
+ }
165
+
166
+ @classmethod
167
+ def serialize_participants(cls, participants_dto: list[ParticipantStatusDTO]) -> list[dict[str, Any]]:
168
+ attendees = []
169
+ for participant_dto in participants_dto:
170
+ status = cls.convert_participant_status_to_attendee_status(participant_dto.status)
171
+ _data = {
172
+ **cls.serialize_person(participant_dto.person),
173
+ "type": "optional",
174
+ }
175
+ if status != "notResponded":
176
+ _data["status"] = {"response": status}
177
+ attendees += [_data]
178
+ return attendees
179
+
180
+ @classmethod
181
+ def _convert_weekday_to_day_name(cls, weekday: rrule.weekday) -> str:
182
+ default_day_name = calendar_reference.day_name[0] # Monday
183
+ if weekday and (_day_name := convert_weekday_rrule_to_day_name(weekday)):
184
+ return _day_name
185
+ return default_day_name
186
+
187
+ @classmethod
188
+ def deserialize_recurring_activities(cls, event: dict) -> dict:
189
+ recurrence = {
190
+ "recurrence_end": None,
191
+ "recurrence_count": 0,
192
+ "repeat_choice": ActivityDTO.ReoccuranceChoice.NEVER.value,
193
+ }
194
+ if event.get("type") == "seriesMaster":
195
+ freq = event["recurrence.pattern.type"]
196
+ interval = event["recurrence.pattern.interval"]
197
+ if freq == "yearly":
198
+ recurrence["repeat_choice"] = ActivityDTO.ReoccuranceChoice.YEARLY.value
199
+ elif freq == "monthly":
200
+ if interval == 3:
201
+ recurrence["repeat_choice"] = ActivityDTO.ReoccuranceChoice.QUARTERLY.value
202
+ else:
203
+ recurrence["repeat_choice"] = ActivityDTO.ReoccuranceChoice.MONTHLY.value
204
+ elif freq == "weekly":
205
+ days_of_week = {_day.lower() for _day in event.get("recurrence.pattern.days_of_week", [])}
206
+ if interval == 2:
207
+ recurrence["repeat_choice"] = ActivityDTO.ReoccuranceChoice.BIWEEKLY.value
208
+ elif interval == 1 and days_of_week == {"monday", "tuesday", "wednesday", "thursday", "friday"}:
209
+ recurrence["repeat_choice"] = ActivityDTO.ReoccuranceChoice.BUSINESS_DAILY.value
210
+ else:
211
+ recurrence["repeat_choice"] = ActivityDTO.ReoccuranceChoice.WEEKLY.value
212
+ else:
213
+ recurrence["repeat_choice"] = ActivityDTO.ReoccuranceChoice.DAILY.value
214
+ if event["recurrence.range.type"] == "endDate":
215
+ recurrence["recurrence_end"] = datetime.strptime(event["recurrence.range.end_date"], "%Y-%m-%d")
216
+ else:
217
+ recurrence["recurrence_count"] = event["recurrence.range.number_of_occurrences"] - 1
218
+ return recurrence
219
+
220
+ @classmethod
221
+ def serialize_recurring_activities(cls, activity_dto: ActivityDTO) -> dict:
222
+ recurrence = {}
223
+ if activity_dto.is_recurrent and (rule_dict := convert_rrulestr_to_dict(activity_dto.repeat_choice)):
224
+ freq_names = {idx: freq for idx, freq in enumerate(rrule.FREQNAMES)}
225
+ if freq_name := freq_names.get(rule_dict.get("freq")):
226
+ pattern = {
227
+ "interval": rule_dict.get("interval", 1),
228
+ "firstDayOfWeek": cls._convert_weekday_to_day_name(rule_dict.get("wkst")),
229
+ }
230
+ if freq_name == "MONTHLY":
231
+ pattern.update({"type": "absoluteMonthly", "dayOfMonth": activity_dto.period.lower.day})
232
+ elif freq_name == "YEARLY":
233
+ pattern.update(
234
+ {
235
+ "type": "absoluteYearly",
236
+ "month": activity_dto.period.lower.month,
237
+ "dayOfMonth": activity_dto.period.lower.day,
238
+ }
239
+ )
240
+ else: # daily or weekly
241
+ pattern["type"] = freq_name.lower()
242
+ if weekdays := rule_dict.get("byweekday", []):
243
+ pattern.update(
244
+ {
245
+ "type": "weekly",
246
+ "daysOfWeek": [cls._convert_weekday_to_day_name(weekday) for weekday in weekdays],
247
+ }
248
+ )
249
+ elif freq_name == "WEEKLY":
250
+ pattern["daysOfWeek"] = [calendar_reference.day_name[activity_dto.period.lower.weekday()]]
251
+
252
+ range = {"startDate": activity_dto.period.lower.strftime("%Y-%m-%d")}
253
+ if activity_dto.recurrence_count:
254
+ range.update(
255
+ {
256
+ "type": "numbered",
257
+ "numberOfOccurrences": activity_dto.recurrence_count + 1,
258
+ "recurrenceTimeZone": activity_dto.period.lower.tzname(),
259
+ }
260
+ )
261
+ else:
262
+ range.update(
263
+ {
264
+ "type": "endDate",
265
+ "endDate": activity_dto.recurrence_end.strftime("%Y-%m-%d"),
266
+ }
267
+ )
268
+
269
+ recurrence = {"recurrence": {"pattern": pattern, "range": range}}
270
+ return recurrence
271
+
272
+ @classmethod
273
+ def serialize_recurrence_range_with_end_date(cls, recurrence: dict, end_date: datetime) -> dict:
274
+ if (
275
+ (recc := recurrence.get("recurrence"))
276
+ and (_range := recc.get("range"))
277
+ and (start_date := _range.get("startDate"))
278
+ ):
279
+ if start_date <= end_date.strftime("%Y-%m-%d"):
280
+ recurrence["recurrence"]["range"] = {
281
+ "type": "endDate",
282
+ "startDate": start_date,
283
+ "endDate": end_date.strftime("%Y-%m-%d"),
284
+ }
285
+ if tz := _range.get("recurrenceTimeZone"):
286
+ recurrence["recurrence"]["range"]["recurrenceTimeZone"] = tz
287
+ return recurrence
288
+
289
+ @classmethod
290
+ def serialize_event_keys(cls, event: dict) -> dict:
291
+ inv_map = {value: key for key, value in _map.items()}
292
+ return dict(map(lambda item: (inv_map.get(item[0], item[0]), item[1]), event.items()))
293
+
294
+
295
+ _map = {
296
+ "lastModifiedDateTime": "last_modified_date_time",
297
+ "startDateTime": "start_date_time",
298
+ "endDateTime": "end_date_time",
299
+ "organizer": "organizer",
300
+ "displayName": "display_name",
301
+ "businessPhones": "business_phones",
302
+ "mobilePhone": "mobile_phone",
303
+ "surname": "surname",
304
+ "givenName": "given_name",
305
+ "mailNickname": "mail_nickname",
306
+ "userPrincipalName": "user_principal_name",
307
+ "notificationUrl": "notification_url",
308
+ "changeType": "change_type",
309
+ "expirationDateTime": "expiration_date_time",
310
+ "resourceData": "resource_data",
311
+ "subscriptionExpirationDateTime": "subscription_expiration_date_time",
312
+ "clientState": "client_state",
313
+ "tenantId": "tenant_id",
314
+ "subscriptionId": "subscription_id",
315
+ "organizer.user": "organizer.user",
316
+ "organizer.user.id": "organizer.user.id",
317
+ "organizer.user.displayName": "organizer.user.display_name",
318
+ "organizer.phone": "organizer.phone",
319
+ "organizer.phone.id": "organizer.phone.id",
320
+ "organizer.phone.displayName": "organizer.phone.display_name",
321
+ "organizer.guest": "organizer.guest",
322
+ "organizer.guest.id": "organizer.guest.id",
323
+ "organizer.guest.displayName": "organizer.guest.display_name",
324
+ "organizer.spoolUser": "organizer.splool_user",
325
+ "organizer.acsUser": "organizer.acs_user",
326
+ "organizer.encrypted": "organizer.encrypted",
327
+ "organizer.onPremises": "organizer.on_premises",
328
+ "organizer.acsApplicationInstance": "organizer.acs_application_instance",
329
+ "organizer.spoolApplicationInstance": "organizer.spool_application_instance",
330
+ "organizer.applicationInstance": "organizer.application_instance",
331
+ "organizer.application": "organizer.application",
332
+ "organizer.device": "organizer.device",
333
+ "joinWebUrl": "join_web_url",
334
+ "preferredLanguage": "preferred_language",
335
+ "officeLocation": "office_location",
336
+ "jobTitle": "job_title",
337
+ "createdDateTime": "created_date_time",
338
+ "isReminderOn": "is_reminder_on",
339
+ "reminderMinutesBeforeStart": "reminder_minutes_before_start",
340
+ "hasAttachments": "has_attachments",
341
+ "webLink": "web_link",
342
+ "start.dateTime": "start.date_time",
343
+ "start.timeZone": "start.time_zone",
344
+ "end.dateTime": "end.date_time",
345
+ "end.timeZone": "end.time_zone",
346
+ "location.displayName": "location.display_name",
347
+ "location.locationUri": "location.location_uri",
348
+ "location.locationType": "location.location_type",
349
+ "location.uniqueId": "location.unique_id",
350
+ "location.uniqueIdType": "location.unique_id_type",
351
+ "location.address.type": "location.address.type",
352
+ "location.address.street": "location.address.street",
353
+ "location.address.city": "location.address.city",
354
+ "location.address.postalCode": "location.address.postal_code",
355
+ "location.address.countryOrRegion": "location.address.country_or_region",
356
+ "recurrence.pattern.daysOfWeek": "recurrence.pattern.days_of_week",
357
+ "recurrence.pattern.dayOfMonth": "recurrence.pattern.day_of_month",
358
+ "recurrence.pattern.firstDayOfWeek": "recurrence.pattern.first_day_of_week",
359
+ "recurrence.range.recurrenceTimeZone": "recurrence.range.recurrence_time_zone",
360
+ "recurrence.range.numberOfOccurrences": "recurrence.range.number_of_occurrences",
361
+ "recurrence.range.startDate": "recurrence.range.start_date",
362
+ "recurrence.range.endDate": "recurrence.range.end_date",
363
+ "organizer.emailAddress.name": "organizer.email_address.name",
364
+ "organizer.emailAddress.address": "organizer.email_address.address",
365
+ "responseStatus.response": "response_status.response",
366
+ "responseStatus.time": "response_status.time",
367
+ "attendees": "attendees",
368
+ "@odata.context": "odata_context",
369
+ "@odata.etag": "odata_etag",
370
+ "applicationId": "application_id",
371
+ "includeResourceData": "include_resource_data",
372
+ "latestSupportedTlsVersion": "latest_supported_tls_version",
373
+ "encryptionCertificate": "encryption_certificate",
374
+ "encryptionCertificateId": "encryption_certificate_id",
375
+ "notificationQueryOptions": "notification_query_options",
376
+ "notificationContentType": "notification_content_type",
377
+ "lifecycleNotificationUrl": "lifecycle_notification_url",
378
+ "creatorId": "creator_id",
379
+ "isDefaultCalendar": "is_default_calendar",
380
+ "hexColor": "hex_color",
381
+ "changeKey": "change_key",
382
+ "canShare": "can_share",
383
+ "canViewPrivateItems": "can_view_private_items",
384
+ "isShared": "is_shared",
385
+ "isSharedWithMe": "is_shared_with_me",
386
+ "canEdit": "can_edit",
387
+ "allowedOnlineMeetingProviders": "allowed_online_meeting_providers",
388
+ "defaultOnlineMeetingProvider": "default_online_meeting_provider",
389
+ "isTallyingResponses": "is_tallying_responses",
390
+ "isRemovable": "is_removable",
391
+ "teamsForBusiness": "teams_for_business",
392
+ "transactionId": "transaction_id",
393
+ "originalStartTimeZone": "original_start_time_zone",
394
+ "originalEndTimeZone": "original_end_time_zone",
395
+ "bodyPreview": "body_preview",
396
+ "isAllDay": "is_all_day",
397
+ "isCancelled": "is_cancelled",
398
+ "isOrganizer": "is_organizer",
399
+ "responseRequested": "response_requested",
400
+ "seriesMasterId": "series_master_id",
401
+ "showAs": "show_as",
402
+ "onlineMeeting": "online_meeting",
403
+ "onlineMeetingUrl": "online_meeting_url",
404
+ "isOnlineMeeting": "is_online_meeting",
405
+ "onlineMeetingProvider": "online_meeting_provider",
406
+ "allowNewTimeProposals": "allow_new_time_proposals",
407
+ "occurrenceId": "occurrence_id",
408
+ "isDraft": "is_draft",
409
+ "hideAttendees": "hide_attendees",
410
+ "responseStatus": "response_status",
411
+ "appId": "app_id",
412
+ "passwordCredentials": "password_credentials",
413
+ "iCalUId": "uid",
414
+ "id": "id",
415
+ "body.contentType": "body.content_type",
416
+ }
417
+
418
+
419
+ def parse(_dict: dict, scalar_value: bool = False):
420
+ data = None
421
+ try:
422
+ if scalar_value:
423
+ df = pd.DataFrame(_dict, index=[0])
424
+ else:
425
+ df = pd.DataFrame(_dict)
426
+ except ValueError:
427
+ _dict = pd.json_normalize(_dict)
428
+ df = pd.DataFrame(_dict)
429
+ df = df.rename(columns=_map)
430
+ data = df.to_dict("records")
431
+ return data
@@ -0,0 +1 @@
1
+ from wbcrm.tests.conftest import *