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