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,139 @@
1
+ from typing import Dict, List, TypedDict
2
+
3
+
4
+ class _GoogleEntityInfo(TypedDict):
5
+ id: str
6
+ email: str
7
+ displayName: str
8
+ self: bool
9
+
10
+
11
+ class TimeInfo(TypedDict):
12
+ date: str
13
+ dateTime: str
14
+ timeZone: str
15
+
16
+
17
+ class _AttendeeInfo(_GoogleEntityInfo):
18
+ organizer: bool
19
+ resource: bool
20
+ optional: bool
21
+ responseStatus: str
22
+ comment: str
23
+ additionalGuests: int
24
+
25
+
26
+ class _ExtendedPropertiesInfo(TypedDict):
27
+ private: Dict[any, str]
28
+ shared: Dict[any, str]
29
+
30
+
31
+ class _CreateRequestInfo(TypedDict):
32
+ requestId: str
33
+ conferenceSolutionKey: Dict[str, str]
34
+ status: Dict[str, str]
35
+
36
+
37
+ class _ConferenceSolutionInfo(TypedDict):
38
+ key: Dict[any, str]
39
+ name: str
40
+ iconUri: str
41
+
42
+
43
+ class _EntryPointInfo(TypedDict):
44
+ entryPointType: str
45
+ uri: str
46
+ label: str
47
+ pin: str
48
+ accessCode: str
49
+ meetingCode: str
50
+ passcode: str
51
+ password: str
52
+
53
+
54
+ class _ConferenceDataInfo(TypedDict):
55
+ createRequest: _CreateRequestInfo
56
+ entryPoints: List[_EntryPointInfo]
57
+ conferenceSolution: _ConferenceSolutionInfo
58
+ conferenceId: str
59
+ signature: str
60
+ notes: str
61
+
62
+
63
+ class _GadgetInfo(TypedDict):
64
+ type: str
65
+ title: str
66
+ link: str
67
+ iconLink: str
68
+ width: int
69
+ height: int
70
+ display: str
71
+ preferences: Dict[any, str]
72
+
73
+
74
+ class _OverridesInfo(TypedDict):
75
+ method: str
76
+ minutes: int
77
+
78
+
79
+ class _RemindersInfo(TypedDict):
80
+ useDefault: bool
81
+ overrides: List[_OverridesInfo]
82
+
83
+
84
+ class _URLInfo(TypedDict):
85
+ url: str
86
+ title: str
87
+
88
+
89
+ class _AttachmentInfo(TypedDict):
90
+ fileUrl: str
91
+ title: str
92
+ mimeType: str
93
+ iconLink: str
94
+ fileId: str
95
+
96
+
97
+ class GoogleEventType(TypedDict):
98
+ """
99
+ The typing information for the Google event dictionary
100
+ """
101
+
102
+ kind: str
103
+ id: str
104
+ status: str
105
+ htmlLink: str
106
+ created: str
107
+ updated: str
108
+ summary: str
109
+ description: str
110
+ location: str
111
+ colorId: str
112
+ creator: _GoogleEntityInfo
113
+ organizer: _GoogleEntityInfo
114
+ start: TimeInfo
115
+ end: TimeInfo
116
+ endTimeUnspecified: bool
117
+ recurrence: List[str]
118
+ recurringEventId: str
119
+ originalStartTime: TimeInfo
120
+ transparency: str
121
+ visibility: str
122
+ iCalUID: str
123
+ sequence: int
124
+ attendees: List[_AttendeeInfo]
125
+ attendeesOmitted: bool
126
+ extendedProperties: _ExtendedPropertiesInfo
127
+ hangoutLink: str
128
+ conferenceData: _ConferenceDataInfo
129
+ gadget: _GadgetInfo
130
+ anyoneCanAddSelf: bool
131
+ guestsCanInviteOthers: bool
132
+ guestsCanModify: bool
133
+ guestsCanSeeOtherGuests: bool
134
+ privateCopy: bool
135
+ locked: bool
136
+ reminders: _RemindersInfo
137
+ source: _URLInfo
138
+ attachments: List[_AttachmentInfo]
139
+ eventType: str
@@ -0,0 +1,217 @@
1
+ from datetime import datetime, timedelta
2
+ from typing import Dict, List
3
+ from zoneinfo import ZoneInfo
4
+
5
+ from django.conf import settings
6
+ from django.db import models
7
+ from dynamic_preferences.registries import global_preferences_registry
8
+ from wbcore.contrib.agenda.models import CalendarItem
9
+ from wbcore.contrib.directory.models import EmailContact, Person
10
+
11
+ from wbcrm.models import Activity, ActivityParticipant
12
+ from wbcrm.synchronization.activity.preferences import (
13
+ can_synchronize_activity_description,
14
+ )
15
+
16
+ from .typing_informations import GoogleEventType, TimeInfo
17
+
18
+ global_preferences = global_preferences_registry.manager()
19
+
20
+
21
+ class GoogleSyncUtils:
22
+ @classmethod
23
+ def convert_event_visibility_to_activity_visibility(cls, event_visiblity: str):
24
+ if event_visiblity == "public":
25
+ return CalendarItem.Visibility.PUBLIC
26
+ return CalendarItem.Visibility.PRIVATE
27
+
28
+ @classmethod
29
+ def convert_activity_participants_to_attendees(cls, activity: Activity, event: GoogleEventType | None = None):
30
+ """
31
+ A method for converting the participants of a "Workbench" activity to a list of dictionaries, that can be used to create "Google Event" attendees.
32
+
33
+ :param activity: The activity from which the participants needs to be converted.
34
+ :return: The list with the dictionaries of attendees. Can be empty if no participant is in the original activity.
35
+ """
36
+ attendees_list = []
37
+ event_attendees = cls.get_or_create_participants(event, activity.creator) if event else []
38
+
39
+ event_attendees = [Person.all_objects.get(id=person_dict["person_id"]) for person_dict in event_attendees]
40
+ can_sync_external_participants: bool = global_preferences["wbactivity_sync__sync_external_participants"]
41
+ allowed_participants: models.QuerySet["ActivityParticipant"] = activity.activity_participants.filter(
42
+ participant__in=event_attendees,
43
+ ).union(activity.activity_participants.filter(participant__id__in=Person.objects.filter_only_internal()))
44
+ activity_participants: models.QuerySet["ActivityParticipant"] = (
45
+ activity.activity_participants.all() if can_sync_external_participants else allowed_participants
46
+ )
47
+ for activity_participant in activity_participants:
48
+ participant = activity_participant.participant
49
+ status = cls.convert_participant_status_to_attendee_status(activity_participant.participation_status)
50
+ attendees_list.append(
51
+ {
52
+ "displayName": participant.computed_str,
53
+ "email": str(participant.primary_email_contact()),
54
+ "responseStatus": status,
55
+ }
56
+ )
57
+ return attendees_list
58
+
59
+ @classmethod
60
+ def convert_activity_to_event(cls, activity: Activity, created=False):
61
+ """
62
+ Converts a "Workbench" activity into a dict, that can be used to create a "Google Event" instance.
63
+
64
+ :param activity: A "Workbench" activity. The activity that is converted to a dict.
65
+ :param created: A boolean. This should be True if the activity is created, in every other case it should be False. Per default it is False.
66
+ :returns: A dictionary which can be used to create a google event.
67
+ """
68
+ # If the activity instance is to be created (e.g. pre_save when creating the activity) we cannot interact with the activity_participants.
69
+ participants_list = cls.convert_activity_participants_to_attendees(activity=activity)
70
+ timezone = ZoneInfo(settings.TIME_ZONE)
71
+ recurrence = []
72
+ if not created:
73
+ if (google_backend := activity.metadata.get("google_backend")) and (event := google_backend.get("event")):
74
+ recurrence = event.get("recurrence", [])
75
+ elif wb_recurrence := activity.metadata.get("recurrence"):
76
+ recurrence = [wb_recurrence]
77
+ else:
78
+ if wb_recurrence := activity.metadata.get("recurrence"):
79
+ recurrence = [wb_recurrence]
80
+ else:
81
+ recurrence = []
82
+ event_body = {
83
+ "summary": activity.title,
84
+ "creator": str(activity.creator.primary_email_contact()) if activity.creator else "",
85
+ "organizer": str(activity.assigned_to.primary_email_contact()) if activity.assigned_to else "",
86
+ "attendees": participants_list,
87
+ "description": activity.description if can_synchronize_activity_description else "",
88
+ "start": {
89
+ "dateTime": activity.period.lower.astimezone(timezone).isoformat(), # type: ignore
90
+ "timeZone": settings.TIME_ZONE,
91
+ },
92
+ "end": {
93
+ "dateTime": activity.period.upper.astimezone(timezone).isoformat(), # type: ignore
94
+ "timeZone": settings.TIME_ZONE,
95
+ },
96
+ "recurrence": recurrence,
97
+ "location": activity.location,
98
+ "visibility": cls.convert_activity_visibility_to_event_visibility(activity.visibility),
99
+ "reminders": {
100
+ "useDefault": False,
101
+ "overrides": [
102
+ {
103
+ "method": "email",
104
+ "minutes": Activity.ReminderChoice.get_minutes_correspondance(activity.reminder_choice),
105
+ }
106
+ ],
107
+ },
108
+ }
109
+ return event_body
110
+
111
+ @classmethod
112
+ def get_start_and_end(cls, event: GoogleEventType):
113
+ """
114
+ Converts the google start & end times to a datetime format.
115
+
116
+ A google event dict contains either a dateTime-key or a date-key. If a date-key is present, this indicates that the event is an all day event.
117
+
118
+ :param event: A google event dictionary from which the start & and end time will be extracted and converted.
119
+
120
+ :note: If a google event takes place all day, the start date and end date will not be the same date.
121
+ For example, an event that will take place on the 01.06.2020 will have "2020-06-01" as the start date value and "2020-06-02" as the end date value.
122
+ A workbench activity will work with "datetime" values, even when the activity is an all-day activity.
123
+ Therefor the start date will be converted in a "datetime" object like "2020-06-01-00-00-00" and the end date will be converted to "2020-06-01-23-59-59"
124
+ """
125
+ event_start, event_end = None, None
126
+ start, end = event["start"], event["end"]
127
+ if start_datetime := start.get("dateTime"):
128
+ event_start = datetime.strptime(start_datetime, "%Y-%m-%dT%H:%M:%S%z")
129
+ else:
130
+ event_start = datetime.strptime(start["date"], "%Y-%m-%d")
131
+ if end_datetime := end.get("dateTime"):
132
+ event_end = datetime.strptime(end_datetime, "%Y-%m-%dT%H:%M:%S%z")
133
+ else:
134
+ event_end = datetime.strptime(end["date"], "%Y-%m-%d") - timedelta(seconds=1)
135
+ return event_start, event_end
136
+
137
+ @classmethod
138
+ def add_instance_metadata(
139
+ cls, parent_occurrence: Activity, google_event_items: List[GoogleEventType], new_metadata: Dict, created=False
140
+ ) -> None:
141
+ """
142
+ Adds the information of google event instances to the corresponding activities metadata field.
143
+
144
+ :param parent_occurrence: The "Workbench" parent activity. It is used to retrieve the child activities.
145
+ :google_event_items: The corresponding google event instances.
146
+ :new_metadata: The newly created metadata for the parent_occurrence
147
+ """
148
+
149
+ def compare_instances_and_activity(activity: Activity, is_parent=False):
150
+ for google_child in google_event_items:
151
+ if created:
152
+ google_start, _ = cls.get_start_and_end(google_child)
153
+ activity_start = activity.period.lower # type: ignore
154
+ else:
155
+ original_start_dict: TimeInfo = google_child.get("originalStartTime", {})
156
+ activity_start_dict: Dict = (
157
+ activity.metadata.get("google_backend", {}).get("instance", {}).get("originalStartTime", {})
158
+ )
159
+ google_start = original_start_dict.get("dateTime", original_start_dict.get("date"))
160
+ activity_start = activity_start_dict.get("dateTime", original_start_dict.get("date"))
161
+ if google_start == activity_start:
162
+ if is_parent:
163
+ new_metadata["google_backend"] |= {"instance": google_child}
164
+ external_id = new_metadata["google_backend"]["event"].get("id")
165
+ Activity.objects.filter(id=activity.id).update(external_id=external_id, metadata=new_metadata)
166
+ else:
167
+ metadata = activity.metadata | {"google_backend": {"instance": google_child}}
168
+ Activity.objects.filter(id=activity.id).update(
169
+ external_id=google_child["id"], metadata=metadata
170
+ )
171
+ google_event_items.remove(google_child)
172
+ break
173
+
174
+ compare_instances_and_activity(parent_occurrence, True)
175
+ child_activities = Activity.objects.filter(parent_occurrence=parent_occurrence)
176
+ for wb_child in child_activities:
177
+ compare_instances_and_activity(wb_child)
178
+
179
+ @classmethod
180
+ def get_or_create_participants(cls, event: GoogleEventType, creator: Person | None):
181
+ """
182
+ Converts the participants of an event into person objects by using the email address of the participants to search for the corresponding person entries in the "Workbench" database.
183
+ If no person with the right email address is found, a new person entry will automatically be created with the email address or, if possible, with the displayed name as last name.
184
+
185
+ :param event: A google event body.
186
+ :param creator: The creator of the event.
187
+
188
+ :return: A list of dicts with the following structure: {"person_id": int, "status":str}. Can be empty if the event has no attendees.
189
+ """
190
+
191
+ participants = (
192
+ [{"person_id": creator.id, "status": ActivityParticipant.ParticipationStatus.ATTENDS}] if creator else []
193
+ )
194
+ if attendees := event.get("attendees"):
195
+ for attendee in attendees:
196
+ mail = attendee.get("email")
197
+ mail_query = Person.objects.filter(
198
+ id__in=EmailContact.objects.filter(address__iexact=mail, primary=True).values_list(
199
+ "entry_id", flat=True
200
+ )
201
+ )
202
+ if mail and mail_query.exists():
203
+ person = mail_query.first()
204
+ else:
205
+ if display_name := attendee.get("displayName"):
206
+ person = Person.objects.create(last_name=display_name, is_draft_entry=True)
207
+ else:
208
+ person = Person.objects.create(last_name=mail, is_draft_entry=True)
209
+ EmailContact.objects.create(entry=person, address=mail, primary=True)
210
+ if not creator or person.id != creator.id:
211
+ person_obj = {
212
+ "person_id": person.id,
213
+ "status": cls.convert_attendee_status_to_participant_status(attendee.get("responseStatus")),
214
+ }
215
+ participants.append(person_obj)
216
+
217
+ return participants