wbcrm 2.2.4__py2.py3-none-any.whl → 2.2.5__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wbcrm might be problematic. Click here for more details.
- wbcrm/kpi_handlers/activities.py +171 -0
- wbcrm/models/llm/activity_summaries.py +33 -0
- wbcrm/models/llm/analyze_relationship.py +54 -0
- wbcrm/report/activity_report.py +110 -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 +180 -0
- {wbcrm-2.2.4.dist-info → wbcrm-2.2.5.dist-info}/METADATA +1 -1
- {wbcrm-2.2.4.dist-info → wbcrm-2.2.5.dist-info}/RECORD +11 -3
- {wbcrm-2.2.4.dist-info → wbcrm-2.2.5.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
from typing import Type
|
|
2
|
+
|
|
3
|
+
from django.db.models import Q
|
|
4
|
+
from django.db.models.expressions import F
|
|
5
|
+
from django.db.models.query import QuerySet
|
|
6
|
+
from django.utils.translation import gettext_lazy as _
|
|
7
|
+
from wbcore import serializers
|
|
8
|
+
from wbcore.contrib.directory.models import Person
|
|
9
|
+
from wbcore.serializers.serializers import Serializer
|
|
10
|
+
from wbcrm.models import Activity, ActivityParticipant, ActivityType
|
|
11
|
+
from wbcrm.serializers import ActivityTypeRepresentationSerializer
|
|
12
|
+
from wbhuman_resources.models.kpi import KPI, KPIHandler
|
|
13
|
+
from wbhuman_resources.serializers import KPIModelSerializer
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NumberOfActivityKPISerializer(KPIModelSerializer):
|
|
17
|
+
activity_type = serializers.PrimaryKeyRelatedField(
|
|
18
|
+
required=True, many=True, queryset=ActivityType.objects.all(), label=_("Activity Type")
|
|
19
|
+
)
|
|
20
|
+
_activity_type = ActivityTypeRepresentationSerializer(source="activity_type", many=True)
|
|
21
|
+
|
|
22
|
+
activity_area = serializers.ChoiceField(
|
|
23
|
+
default="all",
|
|
24
|
+
choices=[("only_internal", _("Only Internal")), ("only_external", _("Only External")), ("all", _("All"))],
|
|
25
|
+
)
|
|
26
|
+
person_participates = serializers.BooleanField(
|
|
27
|
+
default=True,
|
|
28
|
+
label=_("Participants of Activity"),
|
|
29
|
+
help_text=_("Activities considered are related to the participants"),
|
|
30
|
+
)
|
|
31
|
+
person_created = serializers.BooleanField(
|
|
32
|
+
default=True, label=_("Creator of activity"), help_text=_("Activities considered are related to the creator")
|
|
33
|
+
)
|
|
34
|
+
person_assigned = serializers.BooleanField(
|
|
35
|
+
default=True,
|
|
36
|
+
label=_("Person Assigned"),
|
|
37
|
+
help_text=_("Activities considered are related to the persons assigned"),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def update(self, instance, validated_data):
|
|
41
|
+
activity_type = validated_data.get(
|
|
42
|
+
"activity_type",
|
|
43
|
+
instance.additional_data["serializer_data"].get(
|
|
44
|
+
"activity_type", list(ActivityType.objects.values_list("id", flat=True))
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
activity_area = validated_data.get(
|
|
48
|
+
"activity_area",
|
|
49
|
+
instance.additional_data["serializer_data"].get("activity_area", "all"),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
person_participates = validated_data.get(
|
|
53
|
+
"person_participates",
|
|
54
|
+
instance.additional_data["serializer_data"].get("person_participates", True),
|
|
55
|
+
)
|
|
56
|
+
person_created = validated_data.get(
|
|
57
|
+
"person_created",
|
|
58
|
+
instance.additional_data["serializer_data"].get("person_created", True),
|
|
59
|
+
)
|
|
60
|
+
person_assigned = validated_data.get(
|
|
61
|
+
"person_assigned",
|
|
62
|
+
instance.additional_data["serializer_data"].get("person_assigned", True),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
additional_data = instance.additional_data
|
|
66
|
+
additional_data["serializer_data"]["activity_type"] = (
|
|
67
|
+
[_type.id for _type in validated_data.get("activity_type")]
|
|
68
|
+
if validated_data.get("activity_type")
|
|
69
|
+
else activity_type
|
|
70
|
+
)
|
|
71
|
+
additional_data["serializer_data"]["activity_area"] = activity_area
|
|
72
|
+
additional_data["serializer_data"]["person_participates"] = person_participates
|
|
73
|
+
additional_data["serializer_data"]["person_created"] = person_created
|
|
74
|
+
additional_data["serializer_data"]["person_assigned"] = person_assigned
|
|
75
|
+
|
|
76
|
+
additional_data["list_data"] = instance.get_handler().get_list_data(additional_data["serializer_data"])
|
|
77
|
+
validated_data["additional_data"] = additional_data
|
|
78
|
+
|
|
79
|
+
return super().update(instance, validated_data)
|
|
80
|
+
|
|
81
|
+
class Meta(KPIModelSerializer.Meta):
|
|
82
|
+
fields = (
|
|
83
|
+
*KPIModelSerializer.Meta.fields,
|
|
84
|
+
"activity_type",
|
|
85
|
+
"_activity_type",
|
|
86
|
+
"activity_area",
|
|
87
|
+
"person_participates",
|
|
88
|
+
"person_created",
|
|
89
|
+
"person_assigned",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class NumberOfActivityKPI(KPIHandler):
|
|
94
|
+
def get_name(self) -> str:
|
|
95
|
+
return _("Number of Activities")
|
|
96
|
+
|
|
97
|
+
def get_serializer(self) -> Type[Serializer]:
|
|
98
|
+
return NumberOfActivityKPISerializer
|
|
99
|
+
|
|
100
|
+
def annotate_parameters(self, queryset: QuerySet[KPI]) -> QuerySet[KPI]:
|
|
101
|
+
return queryset.annotate(
|
|
102
|
+
activity_type=F("additional_data__serializer_data__activity_type"),
|
|
103
|
+
activity_area=F("additional_data__serializer_data__activity_area"),
|
|
104
|
+
person_participates=F("additional_data__serializer_data__person_participates"),
|
|
105
|
+
person_created=F("additional_data__serializer_data__person_created"),
|
|
106
|
+
person_assigned=F("additional_data__serializer_data__person_assigned"),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def get_list_data(self, serializer_data: dict) -> list[str]:
|
|
110
|
+
activity_types = list(
|
|
111
|
+
ActivityType.objects.filter(pk__in=serializer_data["activity_type"]).values_list("title", flat=True)
|
|
112
|
+
)
|
|
113
|
+
return [
|
|
114
|
+
_("Activity Type: {types}").format(types=activity_types),
|
|
115
|
+
_("Activity Area: {area}").format(area=serializer_data["activity_area"]),
|
|
116
|
+
_("Person Participates: {participating}").format(participating={serializer_data["person_participates"]}),
|
|
117
|
+
_("Person Created: {created}").format(created=serializer_data["person_created"]),
|
|
118
|
+
_("Person Assigned: {assigned}").format(assigned=serializer_data["person_assigned"]),
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
def get_display_grid(self) -> list[list[str]]:
|
|
122
|
+
return [
|
|
123
|
+
["activity_type"] * 3,
|
|
124
|
+
["activity_area"] * 3,
|
|
125
|
+
["person_created", "person_assigned", "person_participates"],
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
def evaluate(self, kpi: "KPI", evaluated_person=None, evaluation_date=None) -> int:
|
|
129
|
+
persons = (
|
|
130
|
+
[evaluated_person.id] if evaluated_person else kpi.evaluated_persons.all().values_list("id", flat=True)
|
|
131
|
+
)
|
|
132
|
+
serializer_data = kpi.additional_data.get("serializer_data")
|
|
133
|
+
qs = Activity.objects.filter(
|
|
134
|
+
period__startswith__date__gte=kpi.period.lower,
|
|
135
|
+
period__endswith__date__lte=evaluation_date if evaluation_date else kpi.period.upper,
|
|
136
|
+
).exclude(status=Activity.Status.CANCELLED)
|
|
137
|
+
qs = (
|
|
138
|
+
qs.filter(type__id__in=serializer_data.get("activity_type"))
|
|
139
|
+
if serializer_data.get("activity_type")
|
|
140
|
+
else qs
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
condition = None
|
|
144
|
+
if serializer_data.get("person_created") or serializer_data.get("person_created") is None:
|
|
145
|
+
condition = Q(creator__in=persons)
|
|
146
|
+
if serializer_data.get("person_assigned") or serializer_data.get("person_assigned") is None:
|
|
147
|
+
condition = (condition | Q(assigned_to__in=persons)) if condition else Q(assigned_to__in=persons)
|
|
148
|
+
if serializer_data.get("person_participates") or serializer_data.get("person_participates") is None:
|
|
149
|
+
condition = (condition | Q(participants__in=persons)) if condition else Q(participants__in=persons)
|
|
150
|
+
if condition:
|
|
151
|
+
qs = qs.filter(condition)
|
|
152
|
+
|
|
153
|
+
if (activity_area := serializer_data.get("activity_area")) and (activity_area != "all"):
|
|
154
|
+
participant_ids = set(
|
|
155
|
+
ActivityParticipant.objects.filter(activity__id__in=qs.values_list("id", flat=True)).values_list(
|
|
156
|
+
"participant", flat=True
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
employee_ids = Person.objects.filter_only_internal().values_list("id", flat=True)
|
|
160
|
+
externals = set(
|
|
161
|
+
Person.objects.filter(Q(id__in=participant_ids) & ~Q(id__in=employee_ids)).values_list("id", flat=True)
|
|
162
|
+
)
|
|
163
|
+
if activity_area == "only_internal":
|
|
164
|
+
qs = qs.exclude(
|
|
165
|
+
Q(creator__in=externals) | Q(assigned_to__in=externals) | Q(participants__id__in=externals)
|
|
166
|
+
)
|
|
167
|
+
elif activity_area == "only_external":
|
|
168
|
+
qs = qs.filter(
|
|
169
|
+
Q(creator__in=externals) | Q(assigned_to__in=externals) | Q(participants__id__in=externals)
|
|
170
|
+
)
|
|
171
|
+
return qs.distinct("id").count()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from langchain_core.messages import HumanMessage, SystemMessage
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
from wbcore.contrib.ai.llm.config import LLMConfig
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from wbcrm.models import Activity
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def analyze_activity_prompt(activity: "Activity"):
|
|
12
|
+
return [
|
|
13
|
+
SystemMessage(
|
|
14
|
+
content="You are a manager in a company and there are activities, both internal as well as external, meaning between service providers, clients and ourselves. We want to analyze these activities in regards of their sentiment and summarize them in English. The summary should be a very short bottom-line focussed text."
|
|
15
|
+
),
|
|
16
|
+
HumanMessage(
|
|
17
|
+
content=f"title={activity.title}, description={activity.description}, period={activity.period}, participants={activity.participants.all()}, companies={activity.companies.all()}, review={activity.result}"
|
|
18
|
+
),
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ActivityLLMResponseModel(BaseModel):
|
|
23
|
+
heat: int = Field(..., ge=1, le=4, description="The sentiment heat.")
|
|
24
|
+
summary: str = Field(..., description="A summary of the activity in English.")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
analyze_activity = LLMConfig["Activity"](
|
|
28
|
+
key="analyze",
|
|
29
|
+
prompt=analyze_activity_prompt,
|
|
30
|
+
on_save=True,
|
|
31
|
+
on_condition=lambda instance: instance.status == "REVIEWED",
|
|
32
|
+
output_model=ActivityLLMResponseModel,
|
|
33
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from langchain_core.messages import HumanMessage, SystemMessage
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
from wbcore.contrib.ai.llm.config import LLMConfig
|
|
6
|
+
from wbcrm.models.activities import Activity
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from wbcrm.models import Account
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def analyze_relationship_prompt(account: "Account"):
|
|
13
|
+
messages = [
|
|
14
|
+
SystemMessage(
|
|
15
|
+
content="Based on the recent interactions/activities, current relationship health, and planned actions with the customer, provide a status score (1 to 5, where 1 is cold and 5 is hot), a summary of the relationship, and recommended next steps for maintaining or improving the relationship. Keep in mind that more information might become available later to refine these insights. Please include all information that is relevant to the relationship in the summary. Regarding the activities/interactions, older interactions are less relevant than recent ones."
|
|
16
|
+
),
|
|
17
|
+
HumanMessage(
|
|
18
|
+
content=f"The title of the account: {account.title}",
|
|
19
|
+
),
|
|
20
|
+
]
|
|
21
|
+
if account.owner:
|
|
22
|
+
for activity in Activity.objects.filter(companies__id__in=[account.owner.id], status="REVIEWED"):
|
|
23
|
+
messages.append(
|
|
24
|
+
HumanMessage(
|
|
25
|
+
content=f"Activity: {activity.summary}, with sentiment: {activity.heat}, period: {activity.period}",
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
return messages
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AccountRelationshipResponseModel(BaseModel):
|
|
33
|
+
relationship_status: int = Field(
|
|
34
|
+
...,
|
|
35
|
+
ge=1,
|
|
36
|
+
le=5,
|
|
37
|
+
description="Rate the customer relationship status from 1 to 5. 1 being the cold and 5 being the hot.",
|
|
38
|
+
)
|
|
39
|
+
relationship_summary: str = Field(
|
|
40
|
+
...,
|
|
41
|
+
description="Briefly summarize the current state of the relationship and recent interactions. Also include any additional information that might be relevant.",
|
|
42
|
+
)
|
|
43
|
+
action_plan: str = Field(
|
|
44
|
+
..., description="Provide the next recommended actions or steps to engage with the customer."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
analyze_relationship = LLMConfig["Account"](
|
|
49
|
+
key="analyze_relationship",
|
|
50
|
+
prompt=analyze_relationship_prompt,
|
|
51
|
+
on_save=True,
|
|
52
|
+
on_condition=lambda act: (act.status == act.Status.OPEN) and act.owner is not None and act.is_root_node(),
|
|
53
|
+
output_model=AccountRelationshipResponseModel,
|
|
54
|
+
)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from io import BytesIO
|
|
3
|
+
|
|
4
|
+
import xlsxwriter
|
|
5
|
+
from celery import shared_task
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
from django.core.mail import EmailMultiAlternatives
|
|
8
|
+
from django.db.models import Q
|
|
9
|
+
from django.template.loader import get_template
|
|
10
|
+
from django.utils import timezone
|
|
11
|
+
from django.utils.html import strip_tags
|
|
12
|
+
from django.utils.translation import gettext as _
|
|
13
|
+
from wbcore.contrib.directory.models import Person
|
|
14
|
+
from wbcrm.models import Activity, ActivityType
|
|
15
|
+
from xlsxwriter.utility import xl_rowcol_to_cell
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def set_cell(worksheet, row, col, value, width_cols, base_format):
|
|
19
|
+
cell = xl_rowcol_to_cell(row, col)
|
|
20
|
+
if value:
|
|
21
|
+
worksheet.write_string(cell, value, base_format)
|
|
22
|
+
if len(value) > width_cols[col]:
|
|
23
|
+
width_cols[col] = len(value)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@shared_task
|
|
27
|
+
def create_report_and_send(profile_id, employee_id, start_date=None, end_date=None):
|
|
28
|
+
employee = Person.objects.get(id=employee_id)
|
|
29
|
+
profile = Person.objects.get(id=profile_id)
|
|
30
|
+
if end_date is None:
|
|
31
|
+
end_date = timezone.now()
|
|
32
|
+
if start_date is None:
|
|
33
|
+
start_date = datetime(year=end_date.year, month=1, day=1, hour=0, minute=0, second=0)
|
|
34
|
+
|
|
35
|
+
report = create_report(employee_id, start_date, end_date)
|
|
36
|
+
|
|
37
|
+
html = get_template("email/activity_report.html")
|
|
38
|
+
|
|
39
|
+
context = {"profile": profile, "employee": employee}
|
|
40
|
+
html_content = html.render(context)
|
|
41
|
+
|
|
42
|
+
msg = EmailMultiAlternatives(
|
|
43
|
+
_("Report"), strip_tags(html_content), settings.DEFAULT_FROM_EMAIL, [profile.user_account.email]
|
|
44
|
+
)
|
|
45
|
+
msg.attach_alternative(html_content, "text/html")
|
|
46
|
+
title = _("all")
|
|
47
|
+
if start_date and end_date:
|
|
48
|
+
title = "{}_{}".format(start_date.strftime("%m/%d/%Y-%H:%M:%S"), end_date.strftime("%m/%d/%Y-%H:%M:%S"))
|
|
49
|
+
msg.attach(
|
|
50
|
+
"report_{}_{}.xlsx".format(employee.computed_str, title),
|
|
51
|
+
report.read(),
|
|
52
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
53
|
+
)
|
|
54
|
+
msg.send()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def create_report(employee, start_date, end_date):
|
|
58
|
+
output = BytesIO()
|
|
59
|
+
workbook = xlsxwriter.Workbook(output, {"in_memory": True})
|
|
60
|
+
|
|
61
|
+
base_format = workbook.add_format({"font_name": "Liberation Sans", "font_size": 10})
|
|
62
|
+
bold_format = workbook.add_format({"font_name": "Liberation Sans", "font_size": 10, "bold": True})
|
|
63
|
+
related_activities = Activity.objects.filter(
|
|
64
|
+
Q(participants__id=employee) | Q(creator__id=employee) | Q(assigned_to__id=employee)
|
|
65
|
+
)
|
|
66
|
+
activities = Activity.get_inrange_activities(related_activities, start_date, end_date)
|
|
67
|
+
# HERE STARTS THE FIRST WORKSHEET
|
|
68
|
+
for type in ActivityType.objects.all():
|
|
69
|
+
width_cols = [5, 5, 16, 8, 7, 11, 12]
|
|
70
|
+
activities_type = activities.filter(type=type.id)
|
|
71
|
+
worksheet = workbook.add_worksheet(type.title)
|
|
72
|
+
worksheet.write_string(0, 0, _("Title"), bold_format)
|
|
73
|
+
worksheet.write_string(0, 1, _("Start"), bold_format)
|
|
74
|
+
worksheet.write_string(0, 2, _("Duration (hours)"), bold_format)
|
|
75
|
+
worksheet.write_string(0, 3, _("Location"), bold_format)
|
|
76
|
+
worksheet.write_string(0, 4, _("Creator"), bold_format)
|
|
77
|
+
worksheet.write_string(0, 5, _("Assigned to"), bold_format)
|
|
78
|
+
worksheet.write_string(0, 6, _("Participants"), bold_format)
|
|
79
|
+
|
|
80
|
+
worksheet.write_string(2, 0, _("Total"), bold_format)
|
|
81
|
+
worksheet.write_string(2, 1, str(len(activities_type.all())), bold_format)
|
|
82
|
+
|
|
83
|
+
for row, activity in enumerate(activities_type.all(), start=4):
|
|
84
|
+
hours = (activity.period.upper - activity.period.lower).total_seconds() / 3600
|
|
85
|
+
duration = format(hours, ".2f")
|
|
86
|
+
creator = activity.creator
|
|
87
|
+
creator_name = ""
|
|
88
|
+
if creator:
|
|
89
|
+
creator_name = creator.computed_str
|
|
90
|
+
assigned_to = activity.assigned_to
|
|
91
|
+
assigned_to_name = ""
|
|
92
|
+
if assigned_to:
|
|
93
|
+
assigned_to_name = assigned_to.computed_str
|
|
94
|
+
participants = ", ".join([participant.computed_str for participant in activity.participants.all()])
|
|
95
|
+
|
|
96
|
+
set_cell(worksheet, row, 0, activity.title, width_cols, base_format)
|
|
97
|
+
set_cell(worksheet, row, 1, f"{activity.period.lower:%d.%m.%Y}", width_cols, base_format)
|
|
98
|
+
set_cell(worksheet, row, 2, duration, width_cols, base_format)
|
|
99
|
+
set_cell(worksheet, row, 2, activity.location, width_cols, base_format)
|
|
100
|
+
set_cell(worksheet, row, 4, creator_name, width_cols, base_format)
|
|
101
|
+
set_cell(worksheet, row, 5, assigned_to_name, width_cols, base_format)
|
|
102
|
+
set_cell(worksheet, row, 6, participants, width_cols, base_format)
|
|
103
|
+
|
|
104
|
+
for col, max_width in enumerate(width_cols):
|
|
105
|
+
if max_width > 300:
|
|
106
|
+
max_width = 300
|
|
107
|
+
worksheet.set_column(col, col, max_width)
|
|
108
|
+
workbook.close()
|
|
109
|
+
output.seek(0)
|
|
110
|
+
return output
|
|
@@ -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,180 @@
|
|
|
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
|
+
)
|
|
117
|
+
|
|
118
|
+
# Get the current parent event from google
|
|
119
|
+
current_google_parent_event: dict = (
|
|
120
|
+
google_service.events().get(calendarId=creator_mail, eventId=current_parent_occurrence.external_id).execute()
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Get the current recurrence rules. We need to modify them for both the current parent event and the new parent event.
|
|
124
|
+
current_parent_rrule_str: str = "\n".join(current_google_parent_event["recurrence"])
|
|
125
|
+
|
|
126
|
+
# 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.
|
|
127
|
+
current_parent_child_count = Activity.objects.filter(parent_occurrence=current_parent_occurrence).count()
|
|
128
|
+
new_parent_child_count = Activity.objects.filter(parent_occurrence=internal_activity).count()
|
|
129
|
+
current_parent_new_rrule: rrule = rrulestr(current_parent_rrule_str).replace( # type: ignore
|
|
130
|
+
count=current_parent_child_count + 1, until=None
|
|
131
|
+
)
|
|
132
|
+
new_parent_rrule: rrule = rrulestr(current_parent_rrule_str).replace(count=new_parent_child_count + 1, until=None) # type: ignore
|
|
133
|
+
|
|
134
|
+
# Converting the rrule back to str. Since the .__str__() method adds a DTSTART value, we need to remove this by using regex.
|
|
135
|
+
current_parent_new_rrule_str: str = re.sub("[DTSTART].*[\n]", "", current_parent_new_rrule.__str__()).split("T0")[
|
|
136
|
+
0
|
|
137
|
+
]
|
|
138
|
+
new_parent_rrule_str: str = re.sub("[DTSTART].*[\n]", "", new_parent_rrule.__str__()).split("T0")[0]
|
|
139
|
+
|
|
140
|
+
# 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.
|
|
141
|
+
current_google_parent_event |= {"recurrence": [current_parent_new_rrule_str]}
|
|
142
|
+
updated_current_parent_event: dict = (
|
|
143
|
+
google_service.events()
|
|
144
|
+
.update(calendarId=creator_mail, eventId=current_google_parent_event["id"], body=current_google_parent_event)
|
|
145
|
+
.execute()
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Updating the corresponding metadata
|
|
149
|
+
current_parent_metadata = current_parent_occurrence.metadata | {
|
|
150
|
+
"google_backend": {"event": updated_current_parent_event}
|
|
151
|
+
}
|
|
152
|
+
Activity.objects.filter(id=current_parent_occurrence.id).update(metadata=current_parent_metadata)
|
|
153
|
+
current_instances = (
|
|
154
|
+
google_service.events()
|
|
155
|
+
.instances(calendarId=creator_mail, eventId=updated_current_parent_event["id"])
|
|
156
|
+
.execute()
|
|
157
|
+
)
|
|
158
|
+
current_instances_google_event_items = current_instances["items"]
|
|
159
|
+
current_parent_occurrence.refresh_from_db()
|
|
160
|
+
GoogleSyncUtils.add_instance_metadata(
|
|
161
|
+
current_parent_occurrence, current_instances_google_event_items, current_parent_metadata
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# 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.
|
|
165
|
+
|
|
166
|
+
internal_activity_query = Activity.objects.filter(id=internal_activity.id)
|
|
167
|
+
external_id = uuid4().hex
|
|
168
|
+
updates |= {"recurrence": [new_parent_rrule_str], "id": external_id}
|
|
169
|
+
internal_activity_query.update(external_id=external_id)
|
|
170
|
+
new_parent_event: dict = google_service.events().insert(calendarId=creator_mail, body=updates).execute()
|
|
171
|
+
|
|
172
|
+
# Updating the corresponding metadata
|
|
173
|
+
metadata = internal_activity.metadata | {"google_backend": {"event": new_parent_event}, "old_parent_id": None}
|
|
174
|
+
internal_activity_query.update(metadata=metadata)
|
|
175
|
+
new_instances = (
|
|
176
|
+
google_service.events().instances(calendarId=creator_mail, eventId=new_parent_event["id"]).execute()
|
|
177
|
+
)
|
|
178
|
+
new_google_event_items = new_instances["items"]
|
|
179
|
+
internal_activity.refresh_from_db()
|
|
180
|
+
GoogleSyncUtils.add_instance_metadata(internal_activity, new_google_event_items, metadata, True)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: wbcrm
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.5
|
|
4
4
|
Summary: A workbench module that contains all the functionality related to a customer relationship management.
|
|
5
5
|
Author-email: Christopher Wittlinger <c.wittlinger@stainly.com>
|
|
6
6
|
Requires-Dist: django-eventtools==1.*
|
|
@@ -23,6 +23,7 @@ wbcrm/filters/activities.py,sha256=aQ5pGGgeAzaskWnO25KeUiEk97BKfe8OZ543VU2_TTc,6
|
|
|
23
23
|
wbcrm/filters/groups.py,sha256=KcjyimEoJ7LB_KIRyTIsiNneVWf5ud9CvSqJasffud0,623
|
|
24
24
|
wbcrm/filters/products.py,sha256=gCiL4MVsaLZgLhyLeclWsBdKLRoYS-C7tL52_VWtfKE,1203
|
|
25
25
|
wbcrm/filters/signals.py,sha256=0BSomHJQjlGV4O_JI6kPDFcghH-u6l7i0cEizhM_wOA,3819
|
|
26
|
+
wbcrm/kpi_handlers/activities.py,sha256=xjq6dQUToDmYZFqaZm3Gj3deQnPeUZcH4VS2qvVtIXY,7900
|
|
26
27
|
wbcrm/migrations/0001_initial_squashed_squashed_0032_productcompanyrelationship_alter_product_prospects_and_more.py,sha256=tI2lU8Z8oW_-FexgquJOC41HwqFO6OyLuRtVY9Es2Ho,166228
|
|
27
28
|
wbcrm/migrations/0002_alter_activity_repeat_choice.py,sha256=OBl6j8p4l3f5k1T3TzOgQtunSXJx0M3Uk-gAuPjPxkM,1187
|
|
28
29
|
wbcrm/migrations/0003_remove_activity_external_id_and_more.py,sha256=Lz4RTsGJPnR2nKZ_FH_WSD22IRVqZ6_aZcleTJT4pSM,2097
|
|
@@ -46,6 +47,9 @@ wbcrm/models/activities.py,sha256=PxrNbP03jxciQPDieHrigj6l7faiNtzwVlC6KpTC9sY,53
|
|
|
46
47
|
wbcrm/models/groups.py,sha256=T4ikoRw6BNWL5oeHDITsGt0KJWibm3OMYppwnkBliMM,4300
|
|
47
48
|
wbcrm/models/products.py,sha256=q7BOU3hPGZwolVfOPkkAFhGYYBRDGHU8ldKui9FAMPQ,2472
|
|
48
49
|
wbcrm/models/recurrence.py,sha256=onftWStKx1nvaxqxOgVRO7yrHqBpke8mDQ4Erojxbtw,11998
|
|
50
|
+
wbcrm/models/llm/activity_summaries.py,sha256=8dJP_aDVoakanbOnj_jYnSpeqX8b7JYtU1aXj7pj1-Y,1359
|
|
51
|
+
wbcrm/models/llm/analyze_relationship.py,sha256=Po6nE9GrlwKZtL471fsWySJYDqRZGW20cLAQTdiqFGk,2346
|
|
52
|
+
wbcrm/report/activity_report.py,sha256=AzSfiWne--CG4C4jpcODJtqegxcALCqxvOs9jeD4EB8,4835
|
|
49
53
|
wbcrm/serializers/__init__.py,sha256=qKwo5e-Ix-Iow1RRdKFC0uZQmuSHzc8DIypIhi_E8HI,726
|
|
50
54
|
wbcrm/serializers/accounts.py,sha256=pt9tu8a7P0TO9h4zMBoS1jEfdlqcQ0IaiM1S9sOT13A,4915
|
|
51
55
|
wbcrm/serializers/activities.py,sha256=dVj6ta3EKh0uaXsbiZZmp9ycmW5etEvRvQFhfX4Hz3U,21756
|
|
@@ -79,6 +83,10 @@ wbcrm/synchronization/activity/backends/google/tasks.py,sha256=vQ-_z6koyAQd6kjp3
|
|
|
79
83
|
wbcrm/synchronization/activity/backends/google/typing_informations.py,sha256=7fDaQEok0kzm_5_vrRrMiakFAh2O3j4G09XWPwYSK1I,2655
|
|
80
84
|
wbcrm/synchronization/activity/backends/google/utils.py,sha256=glDYCtbQDnTrge2mpVM-GICebneaa_I9KZwCgFkwRMA,11572
|
|
81
85
|
wbcrm/synchronization/activity/backends/google/request_utils/__init__.py,sha256=FQ8XwEEPtUR-DKHBgiNJQXHPduUNcJM9YiTSEH5LXmQ,530
|
|
86
|
+
wbcrm/synchronization/activity/backends/google/request_utils/external_to_internal/create.py,sha256=VyAg2iftSO_hyQthX6dtL299cogIFWsmrhE_xOwZQbs,3622
|
|
87
|
+
wbcrm/synchronization/activity/backends/google/request_utils/external_to_internal/delete.py,sha256=AvhoJx-vzf2RlHtDPwXy4LXQtxSGaTWQXLiHlmlFCtE,3700
|
|
88
|
+
wbcrm/synchronization/activity/backends/google/request_utils/external_to_internal/update.py,sha256=4AWhTV-Rdtp4RVijULw7JlIr7ewYVLWv4s6SBb4OKuI,7810
|
|
89
|
+
wbcrm/synchronization/activity/backends/google/request_utils/internal_to_external/update.py,sha256=XqEfqrLzELrxhCCovP_qvL1FECeiqxhV6caVZHfwRvk,8990
|
|
82
90
|
wbcrm/synchronization/activity/backends/google/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
83
91
|
wbcrm/synchronization/activity/backends/google/tests/conftest.py,sha256=yLKwA_DjolyFXkU83ePpwxsVbtl5lrXJxYcUFLg-4fs,35
|
|
84
92
|
wbcrm/synchronization/activity/backends/google/tests/test_data.py,sha256=X7uwRyRkfM1PWtZ5ycLD_ZY2sxm4V6vZ9L4DV4OAzbI,3156
|
|
@@ -150,6 +158,6 @@ wbcrm/viewsets/titles/products.py,sha256=cFAK5zljjybabk2U0KzPT2PVfV5vmO_UlJd6QlI
|
|
|
150
158
|
wbcrm/viewsets/titles/utils.py,sha256=IaHQTmEG2OwIHS1bRv7sjuT950wefUJNi3yvPdrpNEs,1144
|
|
151
159
|
wbcrm/workflows/__init__.py,sha256=biwXXPkVJugT9Vc1cwbInAUY8EnVmOauxdPz7e_2w_A,32
|
|
152
160
|
wbcrm/workflows/assignee_methods.py,sha256=L7ymErtcpFgXdTMTj_lDOVJqsLAGLNT6qMlrkHGuXWM,999
|
|
153
|
-
wbcrm-2.2.
|
|
154
|
-
wbcrm-2.2.
|
|
155
|
-
wbcrm-2.2.
|
|
161
|
+
wbcrm-2.2.5.dist-info/METADATA,sha256=dWfaN2T-FAkxX0XUa7ZAjWX8RXTzsv6vAJNajqbKPhk,449
|
|
162
|
+
wbcrm-2.2.5.dist-info/WHEEL,sha256=aO3RJuuiFXItVSnAUEmQ0yRBvv9e1sbJh68PtuQkyAE,105
|
|
163
|
+
wbcrm-2.2.5.dist-info/RECORD,,
|
|
File without changes
|