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.
- wbcrm/__init__.py +1 -0
- wbcrm/admin/__init__.py +5 -0
- wbcrm/admin/accounts.py +60 -0
- wbcrm/admin/activities.py +104 -0
- wbcrm/admin/events.py +43 -0
- wbcrm/admin/groups.py +8 -0
- wbcrm/admin/products.py +9 -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 +57 -0
- wbcrm/factories/activities.py +124 -0
- wbcrm/factories/groups.py +24 -0
- wbcrm/factories/products.py +11 -0
- wbcrm/filters/__init__.py +10 -0
- wbcrm/filters/accounts.py +80 -0
- wbcrm/filters/activities.py +204 -0
- wbcrm/filters/groups.py +21 -0
- wbcrm/filters/products.py +38 -0
- wbcrm/filters/signals.py +95 -0
- wbcrm/fixtures/wbcrm.json +1215 -0
- wbcrm/kpi_handlers/activities.py +171 -0
- wbcrm/locale/de/LC_MESSAGES/django.mo +0 -0
- wbcrm/locale/de/LC_MESSAGES/django.po +1557 -0
- wbcrm/locale/de/LC_MESSAGES/django.po.translated +1630 -0
- wbcrm/locale/en/LC_MESSAGES/django.mo +0 -0
- wbcrm/locale/en/LC_MESSAGES/django.po +1466 -0
- wbcrm/locale/fr/LC_MESSAGES/django.mo +0 -0
- wbcrm/locale/fr/LC_MESSAGES/django.po +1467 -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/0017_event.py +40 -0
- wbcrm/migrations/0018_activity_search_vector.py +24 -0
- wbcrm/migrations/__init__.py +0 -0
- wbcrm/models/__init__.py +5 -0
- wbcrm/models/accounts.py +648 -0
- wbcrm/models/activities.py +1419 -0
- wbcrm/models/events.py +15 -0
- wbcrm/models/groups.py +119 -0
- wbcrm/models/llm/activity_summaries.py +41 -0
- wbcrm/models/llm/analyze_relationship.py +50 -0
- wbcrm/models/products.py +86 -0
- wbcrm/models/recurrence.py +280 -0
- wbcrm/preferences.py +13 -0
- wbcrm/report/activity_report.py +110 -0
- wbcrm/serializers/__init__.py +23 -0
- wbcrm/serializers/accounts.py +141 -0
- wbcrm/serializers/activities.py +525 -0
- wbcrm/serializers/groups.py +30 -0
- wbcrm/serializers/products.py +58 -0
- wbcrm/serializers/recurrence.py +91 -0
- wbcrm/serializers/signals.py +71 -0
- wbcrm/static/wbcrm/markdown/documentation/activity.md +86 -0
- wbcrm/static/wbcrm/markdown/documentation/activitytype.md +20 -0
- wbcrm/static/wbcrm/markdown/documentation/group.md +2 -0
- wbcrm/static/wbcrm/markdown/documentation/product.md +11 -0
- wbcrm/synchronization/__init__.py +0 -0
- wbcrm/synchronization/activity/__init__.py +0 -0
- wbcrm/synchronization/activity/admin.py +73 -0
- wbcrm/synchronization/activity/backend.py +214 -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 +406 -0
- wbcrm/synchronization/activity/backends/google/request_utils/__init__.py +16 -0
- wbcrm/synchronization/activity/backends/google/request_utils/external_to_internal/create.py +75 -0
- wbcrm/synchronization/activity/backends/google/request_utils/external_to_internal/delete.py +78 -0
- wbcrm/synchronization/activity/backends/google/request_utils/external_to_internal/update.py +155 -0
- wbcrm/synchronization/activity/backends/google/request_utils/internal_to_external/update.py +181 -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 +217 -0
- wbcrm/synchronization/activity/backends/outlook/__init__.py +0 -0
- wbcrm/synchronization/activity/backends/outlook/backend.py +593 -0
- wbcrm/synchronization/activity/backends/outlook/msgraph.py +436 -0
- wbcrm/synchronization/activity/backends/outlook/parser.py +432 -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 +118 -0
- wbcrm/synchronization/activity/backends/outlook/tests/test_backend.py +274 -0
- wbcrm/synchronization/activity/backends/outlook/tests/test_controller.py +249 -0
- wbcrm/synchronization/activity/backends/outlook/tests/test_parser.py +174 -0
- wbcrm/synchronization/activity/controller.py +627 -0
- wbcrm/synchronization/activity/dynamic_preferences_registry.py +119 -0
- wbcrm/synchronization/activity/preferences.py +27 -0
- wbcrm/synchronization/activity/shortcuts.py +16 -0
- wbcrm/synchronization/activity/tasks.py +21 -0
- wbcrm/synchronization/activity/urls.py +7 -0
- wbcrm/synchronization/activity/utils.py +46 -0
- wbcrm/synchronization/activity/views.py +41 -0
- wbcrm/synchronization/admin.py +1 -0
- wbcrm/synchronization/apps.py +14 -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 +264 -0
- wbcrm/templates/email/activity.html +98 -0
- wbcrm/templates/email/activity_report.html +6 -0
- wbcrm/templates/email/daily_summary.html +72 -0
- wbcrm/templates/email/global_daily_summary.html +85 -0
- wbcrm/tests/__init__.py +0 -0
- wbcrm/tests/accounts/__init__.py +0 -0
- wbcrm/tests/accounts/test_models.py +393 -0
- wbcrm/tests/accounts/test_viewsets.py +88 -0
- wbcrm/tests/conftest.py +76 -0
- wbcrm/tests/disable_signals.py +62 -0
- wbcrm/tests/e2e/__init__.py +1 -0
- wbcrm/tests/e2e/e2e_wbcrm_utility.py +83 -0
- wbcrm/tests/e2e/test_e2e.py +370 -0
- wbcrm/tests/test_assignee_methods.py +40 -0
- wbcrm/tests/test_chartviewsets.py +112 -0
- wbcrm/tests/test_dto.py +64 -0
- wbcrm/tests/test_filters.py +52 -0
- wbcrm/tests/test_models.py +217 -0
- wbcrm/tests/test_recurrence.py +292 -0
- wbcrm/tests/test_report.py +21 -0
- wbcrm/tests/test_serializers.py +171 -0
- wbcrm/tests/test_tasks.py +95 -0
- wbcrm/tests/test_viewsets.py +967 -0
- wbcrm/tests/tests.py +121 -0
- wbcrm/typings.py +109 -0
- wbcrm/urls.py +67 -0
- wbcrm/viewsets/__init__.py +22 -0
- wbcrm/viewsets/accounts.py +122 -0
- wbcrm/viewsets/activities.py +341 -0
- wbcrm/viewsets/buttons/__init__.py +7 -0
- wbcrm/viewsets/buttons/accounts.py +27 -0
- wbcrm/viewsets/buttons/activities.py +89 -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 +444 -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 +25 -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 +38 -0
- wbcrm/viewsets/menu/__init__.py +8 -0
- wbcrm/viewsets/menu/accounts.py +18 -0
- wbcrm/viewsets/menu/activities.py +49 -0
- wbcrm/viewsets/menu/groups.py +16 -0
- wbcrm/viewsets/menu/products.py +20 -0
- wbcrm/viewsets/mixins.py +35 -0
- wbcrm/viewsets/previews/__init__.py +1 -0
- wbcrm/viewsets/previews/activities.py +10 -0
- wbcrm/viewsets/products.py +57 -0
- wbcrm/viewsets/recurrence.py +27 -0
- wbcrm/viewsets/titles/__init__.py +13 -0
- wbcrm/viewsets/titles/accounts.py +23 -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-1.56.8.dist-info/METADATA +11 -0
- wbcrm-1.56.8.dist-info/RECORD +182 -0
- wbcrm-1.56.8.dist-info/WHEEL +5 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
|
|
3
|
+
from celery import shared_task
|
|
4
|
+
from dynamic_preferences.registries import global_preferences_registry
|
|
5
|
+
from googleapiclient.discovery import Resource
|
|
6
|
+
from psycopg.types.range import TimestamptzRange
|
|
7
|
+
from wbcore.contrib.authentication.models import User
|
|
8
|
+
from wbcrm.models import Activity
|
|
9
|
+
|
|
10
|
+
from ...typing_informations import GoogleEventType
|
|
11
|
+
from ...utils import GoogleSyncUtils
|
|
12
|
+
from .update import update_activity_participant
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@shared_task(queue="synchronization")
|
|
16
|
+
def create_internal_activity_based_on_google_event(
|
|
17
|
+
event: GoogleEventType, user: User, service: Resource, parent_occurrence: Activity | None = None, is_instance=False
|
|
18
|
+
):
|
|
19
|
+
"""
|
|
20
|
+
A method for creating a "Workbench" activity based on a "Google" event. If the google event is a recurring event, this method will also create the corresponding workbench activities.
|
|
21
|
+
|
|
22
|
+
:param event: A google event body.
|
|
23
|
+
:param user: The current workbench user.
|
|
24
|
+
:param service: Thee google Resource.
|
|
25
|
+
:param parent_occurrence: The parent activity. This is only used when the activity is part of a recurring chain. Per default it is None.
|
|
26
|
+
:param is_instance: True, when the event is a google instance (A instance is an event that is part of a recurring chain)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
external_id = event["id"]
|
|
30
|
+
if event.get("status") == "cancelled" or Activity.objects.filter(external_id=event["id"]).exists():
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
event_creator = event.get("organizer", {})
|
|
34
|
+
event_creator_mail = event_creator.get("email", "")
|
|
35
|
+
event_creator_displayed_name = event_creator.get("displayName", "")
|
|
36
|
+
creator = GoogleSyncUtils.get_or_create_person(event_creator_mail, event_creator_displayed_name)
|
|
37
|
+
event_start, event_end = GoogleSyncUtils.get_start_and_end(event)
|
|
38
|
+
if event_start == event_end:
|
|
39
|
+
event_end = event_end + timedelta(seconds=1)
|
|
40
|
+
period = TimestamptzRange(event_start, event_end) # type: ignore
|
|
41
|
+
all_day: bool = True if event["start"].get("date") else False
|
|
42
|
+
metadata = {"google_backend": {"instance": event}} if is_instance else {"google_backend": {"event": event}}
|
|
43
|
+
|
|
44
|
+
act = Activity.objects.create(
|
|
45
|
+
external_id=external_id,
|
|
46
|
+
title=event.get("summary", "(No Subject)"),
|
|
47
|
+
assigned_to=creator,
|
|
48
|
+
creator=creator,
|
|
49
|
+
description=event.get("description", ""),
|
|
50
|
+
start=event_start,
|
|
51
|
+
end=event_end,
|
|
52
|
+
period=period,
|
|
53
|
+
all_day=all_day,
|
|
54
|
+
location=event.get("location"),
|
|
55
|
+
visibility=GoogleSyncUtils.convert_event_visibility_to_activity_visibility(event.get("visibility", "")),
|
|
56
|
+
metadata=metadata,
|
|
57
|
+
parent_occurrence=parent_occurrence,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
update_activity_participant(event, act)
|
|
61
|
+
|
|
62
|
+
if event.get("recurrence") and not is_instance:
|
|
63
|
+
instances: dict = service.events().instances(calendarId=user.email, eventId=event["id"]).execute()
|
|
64
|
+
instance_items: list[GoogleEventType] = instances["items"]
|
|
65
|
+
global_preferences = global_preferences_registry.manager()
|
|
66
|
+
max_list_length: int = global_preferences["wbcrm__recurrence_maximum_count"]
|
|
67
|
+
instance_items = instance_items[:max_list_length]
|
|
68
|
+
|
|
69
|
+
for item in instance_items:
|
|
70
|
+
if item["start"] == act.metadata["google_backend"].get("event", {}).get("start"):
|
|
71
|
+
updated_metadata: dict = act.metadata
|
|
72
|
+
updated_metadata["google_backend"] |= {"instance": item}
|
|
73
|
+
Activity.objects.filter(id=act.id).update(metadata=updated_metadata)
|
|
74
|
+
else:
|
|
75
|
+
create_internal_activity_based_on_google_event(item, user, service, act, True)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from django.db.models import QuerySet
|
|
2
|
+
from dynamic_preferences.registries import global_preferences_registry
|
|
3
|
+
from googleapiclient.discovery import Resource
|
|
4
|
+
from wbcrm.models import Activity
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def cancel_or_delete_activity(activity: "Activity") -> None:
|
|
8
|
+
# Activity is cancelled rather than disable if global preference is True
|
|
9
|
+
if global_preferences_registry.manager()["wbactivity_sync__sync_cancelled_activity"]:
|
|
10
|
+
Activity.objects.filter(id=activity.id).update(status=Activity.Status.CANCELLED)
|
|
11
|
+
else:
|
|
12
|
+
activity.delete()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def cancel_or_delete_activity_queryset(activity_qs: QuerySet["Activity"]) -> None:
|
|
16
|
+
# Activities are cancelled rather than delete if global preference is True
|
|
17
|
+
if global_preferences_registry.manager()["wbactivity_sync__sync_cancelled_activity"]:
|
|
18
|
+
activity_qs.update(status=Activity.Status.CANCELLED)
|
|
19
|
+
else:
|
|
20
|
+
activity_qs.delete()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def delete_single_activity(activity: Activity):
|
|
24
|
+
"""
|
|
25
|
+
Deletes the activity that has the corresponding external ID
|
|
26
|
+
"""
|
|
27
|
+
Activity.objects.filter(id=activity.id).update(external_id=None)
|
|
28
|
+
cancel_or_delete_activity(activity)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def delete_recurring_activity(activity: Activity, event: dict, user_mail: str, service: Resource):
|
|
32
|
+
"""
|
|
33
|
+
Handles the deletion of recurring activities (either a single activity, a certain number of activities or all activities),
|
|
34
|
+
based on the changes done to the google event.
|
|
35
|
+
|
|
36
|
+
:param event: The google event dict that was deleted.
|
|
37
|
+
:param activity: The corresponding workbench activity.
|
|
38
|
+
:param user_mail: The e-mail address of the current user.
|
|
39
|
+
:param service: The google service to interact with googles resources.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
parent_occurrence = activity.parent_occurrence if activity.parent_occurrence else activity
|
|
43
|
+
metadata_event = parent_occurrence.metadata["google_backend"].get(
|
|
44
|
+
"event", parent_occurrence.metadata["google_backend"].get("instance", {})
|
|
45
|
+
)
|
|
46
|
+
if event.get("recurringEventId"):
|
|
47
|
+
# Delete single event in event chain
|
|
48
|
+
if activity.status not in [Activity.Status.REVIEWED, Activity.Status.FINISHED]:
|
|
49
|
+
cancel_or_delete_activity(activity)
|
|
50
|
+
|
|
51
|
+
elif event.get("recurrence") != metadata_event.get("recurrence"):
|
|
52
|
+
# Delete all events after a certain event in the event chain
|
|
53
|
+
|
|
54
|
+
event_instances: dict = service.events().instances(calendarId=user_mail, eventId=event["id"]).execute()
|
|
55
|
+
external_id_list: list = [instance["id"] for instance in event_instances["items"]]
|
|
56
|
+
activities_to_remove = Activity.objects.filter(
|
|
57
|
+
metadata__google_backend__instance__recurringEventId__startswith=event["id"],
|
|
58
|
+
status=Activity.Status.PLANNED,
|
|
59
|
+
).exclude(metadata__google_backend__instance__id__in=external_id_list)
|
|
60
|
+
first_in_list = activities_to_remove.order_by("start").first()
|
|
61
|
+
metadata = parent_occurrence.metadata
|
|
62
|
+
metadata["google_backend"] |= {"event": event}
|
|
63
|
+
Activity.objects.filter(id=parent_occurrence.id).update(metadata=metadata)
|
|
64
|
+
if (
|
|
65
|
+
first_in_list
|
|
66
|
+
and first_in_list.id == parent_occurrence.id
|
|
67
|
+
and parent_occurrence.status not in [Activity.Status.REVIEWED, Activity.Status.FINISHED]
|
|
68
|
+
):
|
|
69
|
+
cancel_or_delete_activity(parent_occurrence)
|
|
70
|
+
cancel_or_delete_activity_queryset(activities_to_remove)
|
|
71
|
+
|
|
72
|
+
else:
|
|
73
|
+
# Delete all events in event chain
|
|
74
|
+
activities_to_cancel = Activity.objects.filter(
|
|
75
|
+
parent_occurrence=parent_occurrence, status=Activity.Status.PLANNED
|
|
76
|
+
)
|
|
77
|
+
cancel_or_delete_activity_queryset(activities_to_cancel)
|
|
78
|
+
cancel_or_delete_activity(activity)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
|
|
3
|
+
from django.utils import timezone
|
|
4
|
+
from dynamic_preferences.registries import global_preferences_registry
|
|
5
|
+
from googleapiclient.discovery import Resource
|
|
6
|
+
from psycopg.types.range import TimestamptzRange
|
|
7
|
+
from wbcore.contrib.directory.models import Person
|
|
8
|
+
from wbcrm.models import Activity, ActivityParticipant
|
|
9
|
+
|
|
10
|
+
from ...typing_informations import GoogleEventType
|
|
11
|
+
from ...utils import GoogleSyncUtils
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def update_activity_participant(event: GoogleEventType, activity: Activity):
|
|
15
|
+
"""
|
|
16
|
+
Used to update the participants in a workbench activity.
|
|
17
|
+
|
|
18
|
+
:param event: The Google event dict with the participant informations.
|
|
19
|
+
:param activity:The corresponding activity.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
can_sync_external_participants: bool = global_preferences_registry.manager()[
|
|
23
|
+
"wbactivity_sync__sync_external_participants"
|
|
24
|
+
]
|
|
25
|
+
event_participants = GoogleSyncUtils.get_or_create_participants(event, activity.creator)
|
|
26
|
+
|
|
27
|
+
def update_or_add_participants():
|
|
28
|
+
for event_participant in event_participants:
|
|
29
|
+
if person := Person.objects.filter(id=event_participant["person_id"]).first():
|
|
30
|
+
activity.participants.add(person.id)
|
|
31
|
+
ActivityParticipant.objects.filter(activity=activity, participant=person).update(
|
|
32
|
+
participation_status=event_participant["status"]
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if can_sync_external_participants or not activity.creator.is_internal: # type: ignore
|
|
36
|
+
update_or_add_participants()
|
|
37
|
+
else:
|
|
38
|
+
internal_activity_participants_set = set(
|
|
39
|
+
activity.participants.filter(id__in=Person.objects.filter_only_internal())
|
|
40
|
+
)
|
|
41
|
+
event_participants_set = set(Person.objects.filter(id__in=[x["person_id"] for x in event_participants]))
|
|
42
|
+
missing_activity_participants = internal_activity_participants_set - event_participants_set
|
|
43
|
+
update_or_add_participants()
|
|
44
|
+
if missing_activity_participants:
|
|
45
|
+
ActivityParticipant.objects.filter(
|
|
46
|
+
participant__in=missing_activity_participants, activity=activity
|
|
47
|
+
).update(participation_status=ActivityParticipant.ParticipationStatus.CANCELLED)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def update_single_activity(event: GoogleEventType, activity: Activity, change_status=False):
|
|
51
|
+
"""
|
|
52
|
+
Updates a single workbench activity based on changes done in a google event.
|
|
53
|
+
|
|
54
|
+
:param event: The google event dict that was updated.
|
|
55
|
+
:param activity: The corresponding workbench activity.
|
|
56
|
+
:param change_status: Information wheter the status of the event was updated or not.
|
|
57
|
+
"""
|
|
58
|
+
if activity.external_id is None:
|
|
59
|
+
return
|
|
60
|
+
if activity.status == Activity.Status.REVIEWED or activity.status == Activity.Status.FINISHED:
|
|
61
|
+
metadata = activity.metadata
|
|
62
|
+
metadata["google_backend"] |= {"event": event} if not event.get("recurringEventId") else {"instance": event}
|
|
63
|
+
Activity.objects.filter(id=activity.id).update(
|
|
64
|
+
external_id=event["id"] if "_" in activity.external_id else activity.external_id,
|
|
65
|
+
metadata=metadata,
|
|
66
|
+
)
|
|
67
|
+
return
|
|
68
|
+
event_organizer = event.get("organizer", {})
|
|
69
|
+
event_organizer_mail = event_organizer.get("email", "")
|
|
70
|
+
event_organizer_displayed_name = event_organizer.get("displayName", "")
|
|
71
|
+
organizer = GoogleSyncUtils.get_or_create_person(event_organizer_mail, event_organizer_displayed_name)
|
|
72
|
+
event_start, event_end = GoogleSyncUtils.get_start_and_end(event)
|
|
73
|
+
if event_start == event_end:
|
|
74
|
+
event_end = event_end + timedelta(seconds=1)
|
|
75
|
+
period = TimestamptzRange(event_start, event_end) # type: ignore
|
|
76
|
+
all_day: bool = True if event["start"].get("date") else False
|
|
77
|
+
|
|
78
|
+
metadata = activity.metadata
|
|
79
|
+
metadata["google_backend"] |= {"event": event} if not event.get("recurringEventId") else {"instance": event}
|
|
80
|
+
Activity.objects.filter(id=activity.id).update(
|
|
81
|
+
title=event.get("summary", "(No Subject)"),
|
|
82
|
+
assigned_to=organizer,
|
|
83
|
+
description=event.get("description", ""),
|
|
84
|
+
start=event_start,
|
|
85
|
+
status=Activity.Status.PLANNED if change_status else activity.status,
|
|
86
|
+
end=event_end,
|
|
87
|
+
period=period,
|
|
88
|
+
all_day=all_day,
|
|
89
|
+
location=event.get("location"),
|
|
90
|
+
visibility=GoogleSyncUtils.convert_event_visibility_to_activity_visibility(event.get("visibility")),
|
|
91
|
+
metadata=metadata,
|
|
92
|
+
external_id=event["id"] if "_" in activity.external_id else activity.external_id,
|
|
93
|
+
)
|
|
94
|
+
update_activity_participant(event, activity)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def update_all_activities(activity: Activity, event: GoogleEventType, user_mail: str, service: Resource):
|
|
98
|
+
"""
|
|
99
|
+
Updates all workbench activities in a recurrence chain based on changes done in a google event.
|
|
100
|
+
|
|
101
|
+
:param event: The google event dict that was updated.
|
|
102
|
+
:param activity: The corresponding workbench activity.
|
|
103
|
+
:param user_mail: The e-mail address of the current user.
|
|
104
|
+
:param service: The google service to interact with googles resources.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
activity_instance = activity.metadata["google_backend"].get(
|
|
108
|
+
"instance", activity.metadata["google_backend"].get("event", {})
|
|
109
|
+
)
|
|
110
|
+
event_start, event_end = event["start"], event["end"]
|
|
111
|
+
activity_start, activity_end = activity_instance.get("start"), activity_instance.get("end")
|
|
112
|
+
event_instances: dict = service.events().instances(calendarId=user_mail, eventId=event["id"]).execute()
|
|
113
|
+
instance_items = event_instances["items"]
|
|
114
|
+
connected_activities = Activity.objects.filter(parent_occurrence=activity)
|
|
115
|
+
connected_activities |= Activity.objects.filter(metadata__google_backend__instance__recurringEventId=event["id"])
|
|
116
|
+
connected_activities |= Activity.objects.filter(id=activity.id)
|
|
117
|
+
if event_start != activity_start or event_end != activity_end:
|
|
118
|
+
# If the start/end time of the google event chain changes, google will also change the id of the event instances. That is why we need to handle this case seperatly
|
|
119
|
+
connected_activities = connected_activities.order_by("external_id")
|
|
120
|
+
activity_list = list(connected_activities)
|
|
121
|
+
instance_items.sort(key=lambda x: x["start"].get("date", x["start"]["dateTime"]))
|
|
122
|
+
for instance in instance_items:
|
|
123
|
+
if len(activity_list) == 0:
|
|
124
|
+
break
|
|
125
|
+
activity_instance = activity_list.pop(0)
|
|
126
|
+
update_single_activity(instance, activity_instance)
|
|
127
|
+
activity_instance.refresh_from_db()
|
|
128
|
+
else:
|
|
129
|
+
for instance in instance_items:
|
|
130
|
+
if activity_child := connected_activities.filter(external_id=instance["id"]).first() or (
|
|
131
|
+
activity_child := Activity.objects.filter(
|
|
132
|
+
metadata__google_backend__instance__id=instance["id"]
|
|
133
|
+
).first()
|
|
134
|
+
):
|
|
135
|
+
update_single_activity(instance, activity_child)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def update_activities_from_new_parent(event: dict, parent_occurrence: Activity, user_mail: str, service: Resource):
|
|
139
|
+
"""
|
|
140
|
+
This methods updates child activities whose parent activity was altered.
|
|
141
|
+
|
|
142
|
+
:param event: The google event dict that was updated.
|
|
143
|
+
:param parent_occurrence: The corresponding workbench parent activity.
|
|
144
|
+
:param user_mail: The e-mail address of the current user.
|
|
145
|
+
:param service: The google service to interact with googles resources.
|
|
146
|
+
|
|
147
|
+
"""
|
|
148
|
+
now = timezone.now()
|
|
149
|
+
canceled_child_activities = Activity.objects.filter(
|
|
150
|
+
parent_occurrence=parent_occurrence, period__startswith__gt=now
|
|
151
|
+
)
|
|
152
|
+
event_instances: dict = service.events().instances(calendarId=user_mail, eventId=event["id"]).execute()
|
|
153
|
+
for instance in event_instances["items"]:
|
|
154
|
+
if activity := canceled_child_activities.filter(external_id=instance["id"]).first():
|
|
155
|
+
update_single_activity(instance, activity, True)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import warnings
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
|
|
5
|
+
from dateutil.rrule import rrule, rrulestr
|
|
6
|
+
from googleapiclient.discovery import Resource
|
|
7
|
+
from wbcrm.models import Activity
|
|
8
|
+
|
|
9
|
+
from ...utils import GoogleSyncUtils
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def update_single_event(creator_mail: str, google_service: Resource, internal_activity: Activity, updates: dict):
|
|
13
|
+
"""
|
|
14
|
+
Updates a single Google-Event.
|
|
15
|
+
|
|
16
|
+
After the external event has been updated, the metadata field of the internal activity is also updated with the information of the external event.
|
|
17
|
+
|
|
18
|
+
Note: Do not use for recurring events.
|
|
19
|
+
|
|
20
|
+
:param creator_mail: The e-mail address of the activities creator.
|
|
21
|
+
:param google_service: The google service to interact with googles resources.
|
|
22
|
+
:param internal_activity: The internal workbench activity that corresponds to the external google event.
|
|
23
|
+
:param updates: The update information.
|
|
24
|
+
:returns: None
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
updated_external_event = (
|
|
28
|
+
google_service.events()
|
|
29
|
+
.update(calendarId=creator_mail, eventId=internal_activity.external_id, body=updates)
|
|
30
|
+
.execute()
|
|
31
|
+
)
|
|
32
|
+
metadata = internal_activity.metadata | {"google_backend": {"event": updated_external_event}}
|
|
33
|
+
Activity.objects.filter(id=internal_activity.id).update(metadata=metadata)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def update_single_recurring_event(
|
|
37
|
+
creator_mail: str, google_service: Resource, internal_activity: Activity, updates: dict
|
|
38
|
+
):
|
|
39
|
+
"""
|
|
40
|
+
Updates a single recurring Google-Event.
|
|
41
|
+
|
|
42
|
+
After the external event has been updated, the metadata field of the internal activity is also updated with the information of the external event.
|
|
43
|
+
|
|
44
|
+
Note: Do not use for not recurring events.
|
|
45
|
+
|
|
46
|
+
:param creator_mail: The e-mail address of the activities creator.
|
|
47
|
+
:param google_service: The google service to interact with googles resources.
|
|
48
|
+
:param internal_activity: The internal workbench activity that corresponds to the external google event.
|
|
49
|
+
:param updates: The update information.
|
|
50
|
+
:returns: None
|
|
51
|
+
"""
|
|
52
|
+
is_parent = Activity.objects.filter(parent_occurrence=internal_activity).exists()
|
|
53
|
+
external_id = (
|
|
54
|
+
internal_activity.metadata["google_backend"]["instance"].get("id")
|
|
55
|
+
if is_parent
|
|
56
|
+
else internal_activity.external_id
|
|
57
|
+
)
|
|
58
|
+
updated_event = google_service.events().patch(calendarId=creator_mail, eventId=external_id, body=updates).execute()
|
|
59
|
+
metadata = internal_activity.metadata
|
|
60
|
+
metadata["google_backend"] |= {"instance": updated_event}
|
|
61
|
+
Activity.objects.filter(id=internal_activity.id).update(metadata=metadata)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def update_all_recurring_events_from_parent(
|
|
65
|
+
creator_mail: str, google_service: Resource, internal_activity: Activity, updates: dict
|
|
66
|
+
):
|
|
67
|
+
"""
|
|
68
|
+
Updates all Google-Event-Instances belonging to the same recurring event chain.
|
|
69
|
+
|
|
70
|
+
After the external event instances have been updated, the metadata fields of the internal activities is also updated with the information of the corresponding external event instance.
|
|
71
|
+
|
|
72
|
+
Note: Do not use when creating a new parent activity from an existing child.
|
|
73
|
+
|
|
74
|
+
:param creator_mail: The e-mail address of the activities creator.
|
|
75
|
+
:param google_service: The google service to interact with googles resources.
|
|
76
|
+
:param internal_activity: The internal workbench parent activity that corresponds to the external google event.
|
|
77
|
+
:param updates: The update information.
|
|
78
|
+
:returns: None
|
|
79
|
+
"""
|
|
80
|
+
updated_event = (
|
|
81
|
+
google_service.events()
|
|
82
|
+
.patch(calendarId=creator_mail, eventId=internal_activity.external_id, body=updates)
|
|
83
|
+
.execute()
|
|
84
|
+
)
|
|
85
|
+
metadata = internal_activity.metadata | {"google_backend": {"event": updated_event}}
|
|
86
|
+
instances = google_service.events().instances(calendarId=creator_mail, eventId=updated_event["id"]).execute()
|
|
87
|
+
google_event_items = instances["items"]
|
|
88
|
+
GoogleSyncUtils.add_instance_metadata(internal_activity, google_event_items, metadata)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def update_all_recurring_events_from_new_parent(
|
|
92
|
+
creator_mail: str, google_service: Resource, internal_activity: Activity, updates: dict
|
|
93
|
+
):
|
|
94
|
+
"""
|
|
95
|
+
Updates all Google-Event-Instances belonging to the same parent event.
|
|
96
|
+
|
|
97
|
+
After the external event instances have been updated, the metadata fields of the internal activities is also updated with the information of the corresponding external event instance.
|
|
98
|
+
|
|
99
|
+
Note: Do not use when updating from the original event chain parent.
|
|
100
|
+
|
|
101
|
+
:param creator_mail: The e-mail address of the activities creator.
|
|
102
|
+
:param google_service: The google service to interact with googles resources.
|
|
103
|
+
:param internal_activity: The internal workbench parent activity that corresponds to the external google event.
|
|
104
|
+
:param updates: The update information.
|
|
105
|
+
:returns: None
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
# If the old parent does not exist anymore, we cannot update the child.
|
|
109
|
+
if not (
|
|
110
|
+
current_parent_occurrence := Activity.objects.filter(
|
|
111
|
+
id=internal_activity.metadata.get("old_parent_id")
|
|
112
|
+
).first()
|
|
113
|
+
):
|
|
114
|
+
return warnings.warn(
|
|
115
|
+
"Could not update the recurring events on google, because the old parent activity was already deleted.",
|
|
116
|
+
stacklevel=2,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Get the current parent event from google
|
|
120
|
+
current_google_parent_event: dict = (
|
|
121
|
+
google_service.events().get(calendarId=creator_mail, eventId=current_parent_occurrence.external_id).execute()
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Get the current recurrence rules. We need to modify them for both the current parent event and the new parent event.
|
|
125
|
+
current_parent_rrule_str: str = "\n".join(current_google_parent_event["recurrence"])
|
|
126
|
+
|
|
127
|
+
# We need to adjust the rrules to mimic the current status on the workbench. So we replace any until or count value with the current number of child activities.
|
|
128
|
+
current_parent_child_count = Activity.objects.filter(parent_occurrence=current_parent_occurrence).count()
|
|
129
|
+
new_parent_child_count = Activity.objects.filter(parent_occurrence=internal_activity).count()
|
|
130
|
+
current_parent_new_rrule: rrule = rrulestr(current_parent_rrule_str).replace( # type: ignore
|
|
131
|
+
count=current_parent_child_count + 1, until=None
|
|
132
|
+
)
|
|
133
|
+
new_parent_rrule: rrule = rrulestr(current_parent_rrule_str).replace(count=new_parent_child_count + 1, until=None) # type: ignore
|
|
134
|
+
|
|
135
|
+
# Converting the rrule back to str. Since the .__str__() method adds a DTSTART value, we need to remove this by using regex.
|
|
136
|
+
current_parent_new_rrule_str: str = re.sub("[DTSTART].*[\n]", "", current_parent_new_rrule.__str__()).split("T0")[
|
|
137
|
+
0
|
|
138
|
+
]
|
|
139
|
+
new_parent_rrule_str: str = re.sub("[DTSTART].*[\n]", "", new_parent_rrule.__str__()).split("T0")[0]
|
|
140
|
+
|
|
141
|
+
# Updating the current parent with the new rrules. This will remove all the child events on google that are not in the scope of the changed rrules anymore.
|
|
142
|
+
current_google_parent_event |= {"recurrence": [current_parent_new_rrule_str]}
|
|
143
|
+
updated_current_parent_event: dict = (
|
|
144
|
+
google_service.events()
|
|
145
|
+
.update(calendarId=creator_mail, eventId=current_google_parent_event["id"], body=current_google_parent_event)
|
|
146
|
+
.execute()
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Updating the corresponding metadata
|
|
150
|
+
current_parent_metadata = current_parent_occurrence.metadata | {
|
|
151
|
+
"google_backend": {"event": updated_current_parent_event}
|
|
152
|
+
}
|
|
153
|
+
Activity.objects.filter(id=current_parent_occurrence.id).update(metadata=current_parent_metadata)
|
|
154
|
+
current_instances = (
|
|
155
|
+
google_service.events()
|
|
156
|
+
.instances(calendarId=creator_mail, eventId=updated_current_parent_event["id"])
|
|
157
|
+
.execute()
|
|
158
|
+
)
|
|
159
|
+
current_instances_google_event_items = current_instances["items"]
|
|
160
|
+
current_parent_occurrence.refresh_from_db()
|
|
161
|
+
GoogleSyncUtils.add_instance_metadata(
|
|
162
|
+
current_parent_occurrence, current_instances_google_event_items, current_parent_metadata
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Updating the new parent with the created rrules. This will create the child events in google which are in the scope of the newly created rrules.
|
|
166
|
+
|
|
167
|
+
internal_activity_query = Activity.objects.filter(id=internal_activity.id)
|
|
168
|
+
external_id = uuid4().hex
|
|
169
|
+
updates |= {"recurrence": [new_parent_rrule_str], "id": external_id}
|
|
170
|
+
internal_activity_query.update(external_id=external_id)
|
|
171
|
+
new_parent_event: dict = google_service.events().insert(calendarId=creator_mail, body=updates).execute()
|
|
172
|
+
|
|
173
|
+
# Updating the corresponding metadata
|
|
174
|
+
metadata = internal_activity.metadata | {"google_backend": {"event": new_parent_event}, "old_parent_id": None}
|
|
175
|
+
internal_activity_query.update(metadata=metadata)
|
|
176
|
+
new_instances = (
|
|
177
|
+
google_service.events().instances(calendarId=creator_mail, eventId=new_parent_event["id"]).execute()
|
|
178
|
+
)
|
|
179
|
+
new_google_event_items = new_instances["items"]
|
|
180
|
+
internal_activity.refresh_from_db()
|
|
181
|
+
GoogleSyncUtils.add_instance_metadata(internal_activity, new_google_event_items, metadata, True)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
|
|
3
|
+
from celery import shared_task
|
|
4
|
+
from wbcore.contrib.authentication.models import User
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@shared_task(queue="synchronization")
|
|
8
|
+
def google_webhook_resubscription() -> None:
|
|
9
|
+
"""
|
|
10
|
+
A task to renew the google webhook subscriptions. The expiration date will be increased by 8 days.
|
|
11
|
+
Only the subscriptions of users who still have a valid subscription will be renewed.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .google import GoogleCalendarBackend
|
|
15
|
+
|
|
16
|
+
user: User
|
|
17
|
+
for user in User.objects.filter(metadata__google_backend__watch__isnull=False):
|
|
18
|
+
GoogleCalendarBackend.stop_web_hook(user)
|
|
19
|
+
user.refresh_from_db()
|
|
20
|
+
new_timestamp_ms = round((datetime.now() + timedelta(days=8)).timestamp() * 1000)
|
|
21
|
+
GoogleCalendarBackend.set_web_hook(user, new_timestamp_ms)
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from wbcrm.tests.conftest import *
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import calendar
|
|
2
|
+
import datetime as dt
|
|
3
|
+
|
|
4
|
+
week_ahead = dt.date.today() + dt.timedelta(days=7)
|
|
5
|
+
week_before = dt.date.today() - dt.timedelta(days=7)
|
|
6
|
+
week_ahead_timestamp = calendar.timegm(week_ahead.timetuple()) * 1000
|
|
7
|
+
week_before_timestamp = calendar.timegm(week_before.timetuple()) * 1000
|
|
8
|
+
credentials = '{"url": "https://fake_url.io", "type": "service_account", "project_id": "fake_project_id", "private_key_id": "fake_private_key_id", "private_key": "fake_private_key", "client_email": "client_mail@serviceaccount.com", "client_id": "fake_client_id", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://fake_auth_url", "client_x509_cert_url": "https://fake_cert_url"}'
|
|
9
|
+
|
|
10
|
+
person_metadata = {"google_backend": {"watch": {"expiration": str(week_ahead_timestamp)}}}
|
|
11
|
+
person_metadata_expired = {"google_backend": {"watch": {"expiration": str(week_before_timestamp)}, "expired": True}}
|
|
12
|
+
|
|
13
|
+
event = {
|
|
14
|
+
"attendees": [
|
|
15
|
+
{"displayName": "Foo", "email": "Foo@Foo.com", "responseStatus": "accepted"},
|
|
16
|
+
{"displayName": "Bar", "email": "Bar@Bar.com", "responseStatus": "declined"},
|
|
17
|
+
{"displayName": "Foo Bar", "email": "Foo@Bar.com", "responseStatus": "tentative"},
|
|
18
|
+
{"email": "Bar@Foo.com", "responseStatus": "tentative"},
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
event_data = {
|
|
23
|
+
"id": "test",
|
|
24
|
+
"items": [],
|
|
25
|
+
"start": {"date": "2022-12-06", "dateTime": "2022-12-06T17:25:00+0200", "timeZone": "UTC"},
|
|
26
|
+
"end": {"date": "2022-12-06", "dateTime": "2022-12-06T18:25:00+0200", "timeZone": "UTC"},
|
|
27
|
+
}
|
|
28
|
+
event_list = [
|
|
29
|
+
{"id": "1", "metaTest": "Parent", "originalStartTime": {"dateTime": "Fake Date Time"}},
|
|
30
|
+
{"id": "2", "metaTest": "Child A", "originalStartTime": {"dateTime": "Fake Date Time A"}},
|
|
31
|
+
{"id": "3", "metaTest": "Child B", "originalStartTime": {"dateTime": "Fake Date Time B"}},
|
|
32
|
+
{"id": "4", "metaTest": "Child C", "originalStartTime": {"dateTime": "Fake Date Time C"}},
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class EventService:
|
|
37
|
+
def insert(self, calendarId, body):
|
|
38
|
+
return ExecuteService(calendarId, body)
|
|
39
|
+
|
|
40
|
+
def instances(self, calendarId, eventId):
|
|
41
|
+
return ExecuteService(calendarId, eventId)
|
|
42
|
+
|
|
43
|
+
def delete(self, calendarId, eventId):
|
|
44
|
+
return ExecuteService(calendarId, eventId)
|
|
45
|
+
|
|
46
|
+
def update(self, calendarId, eventId, body=event):
|
|
47
|
+
return ExecuteService(calendarId, eventId)
|
|
48
|
+
|
|
49
|
+
def patch(self, calendarId, eventId, body=event):
|
|
50
|
+
return ExecuteService(calendarId, eventId)
|
|
51
|
+
|
|
52
|
+
def get(self, calendarId, eventId, body=event):
|
|
53
|
+
return ExecuteService(calendarId, eventId)
|
|
54
|
+
|
|
55
|
+
def list(self, calendarId, pageToken, syncToken):
|
|
56
|
+
return ExecuteService(calendarId, pageToken)
|
|
57
|
+
|
|
58
|
+
def watch(self, calendarId, body=event):
|
|
59
|
+
return ExecuteService(calendarId, event)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ChannelsService:
|
|
63
|
+
def stop(self, body):
|
|
64
|
+
return ExecuteService("", body)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ExecuteService:
|
|
68
|
+
def __init__(self, calendarId, body):
|
|
69
|
+
self.calendarId = calendarId
|
|
70
|
+
self.body = body
|
|
71
|
+
|
|
72
|
+
def execute(self):
|
|
73
|
+
return event_data
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ServiceData:
|
|
77
|
+
def events(self):
|
|
78
|
+
return EventService
|
|
79
|
+
|
|
80
|
+
def channels(self):
|
|
81
|
+
return ChannelsService
|