wbintegrator_office365 1.56.1__py2.py3-none-any.whl → 1.56.2__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 wbintegrator_office365 might be problematic. Click here for more details.

@@ -1,3 +1,4 @@
1
1
  from .event import CallEvent, CallUser, Event, EventLog
2
2
  from .subscription import Subscription
3
3
  from .tenant import TenantUser
4
+ from .reports import *
@@ -0,0 +1,122 @@
1
+ from datetime import date, timedelta
2
+
3
+ import humanize
4
+ from django.contrib.auth.models import Group
5
+ from django.db.models import DurationField, ExpressionWrapper, F, Q, QuerySet
6
+ from django.dispatch import receiver
7
+ from wbcore.contrib.directory.models import Person
8
+ from wbcore.permissions.shortcuts import get_internal_users
9
+ from wbcrm.signals import add_employee_activity_to_daily_brief
10
+
11
+ from wbintegrator_office365.models import CallEvent
12
+
13
+
14
+ def format_td(td: timedelta) -> str:
15
+ total_seconds = td.total_seconds()
16
+ if total_seconds == 0:
17
+ return "Missed"
18
+ elif total_seconds < 60:
19
+ return "< 1min"
20
+ return humanize.precisedelta(td, suppress=["hours"], minimum_unit="seconds", format="%0.0f")
21
+
22
+
23
+ def generate_call_summary(
24
+ profiles: QuerySet[Person],
25
+ start_date: date,
26
+ end_date: date,
27
+ include_detail: bool = True,
28
+ ) -> str:
29
+ calls = CallEvent.objects.filter(
30
+ start__date__gte=start_date,
31
+ end__date__lte=end_date,
32
+ ).annotate(duration=ExpressionWrapper(F("end") - F("start"), output_field=DurationField()))
33
+ message = """
34
+ <div style="background-color: white; width: 720px; margin-bottom: 50px">
35
+ """
36
+ for profile in profiles:
37
+ call_events = calls.filter(
38
+ participants__tenant_user__profile=profile,
39
+ ).order_by("start")
40
+
41
+ message += f"""
42
+ <div style="text-align: left;">
43
+ <p><b>{profile.computed_str}</b></p>
44
+ <table width="100%; table-layout: fixed; border-collapse: collapse;">
45
+ <tr>
46
+ <td style="width: 33.33%; text-align: center;">Total Calls: <b>{call_events.count()}</b></td>
47
+ <td style="width: 33.33%; text-align: center;">under 1 minute: <b>{call_events.filter(duration__lte=timedelta(seconds=60)).count()}</b></td>
48
+ <td style="width: 33.33%; text-align: center;">above 1 minute: <b>{call_events.filter(duration__gt=timedelta(seconds=60)).count()}</b></td>
49
+ </tr>
50
+ </table>
51
+ </div>
52
+ """
53
+ if include_detail:
54
+ for call_date in call_events.dates("start", "day", order="DESC"):
55
+ call_day_events = call_events.filter(start__date=call_date)
56
+ if call_day_events.exists():
57
+ message += f"<p><b>{call_date:%Y-%m-%d}:</b></p>"
58
+ message += "<table style='border-collapse: collapse; width: 720px; table-layout: fixed;'> \
59
+ <tr style='color: white; background-color: #1868ae;'> \
60
+ <th style='border: 1px solid #ddd;padding: 2px 7px; width: 20px;' >Start</th> \
61
+ <th style='border: 1px solid #ddd;padding: 2px 7px; width: 20px;' >End</th> \
62
+ <th style='border: 1px solid #ddd;padding: 2px 7px; width: 60px;' >Duration</th> \
63
+ <th style='border: 1px solid #ddd;padding: 2px 7px; width: 80px;' >Organized by</th> \
64
+ <th style='border: 1px solid #ddd;padding: 2px 7px; width: 150px;' >Participants</th> \
65
+ </tr>"
66
+ for call in call_day_events:
67
+ participants = ",".join(
68
+ filter(
69
+ None,
70
+ [
71
+ p.get_humanized_repr()
72
+ for p in call.participants.exclude(tenant_user__profile=profile)
73
+ ],
74
+ )
75
+ )
76
+ message += f"<tr> \
77
+ <td style='border: 1px solid #ddd;padding: 2px; width: 20px;' >{call.start.astimezone():%H:%M}</td> \
78
+ <td style='border: 1px solid #ddd;padding: 2px; width: 20px;' >{call.end.astimezone():%H:%M}</td> \
79
+ <td style='border: 1px solid #ddd;padding: 2px; width: 60px;' text-align:center;><b>{format_td(call.end - call.start)}</b></td> \
80
+ <td style='border: 1px solid #ddd;padding: 2px; width: 80px;' ><b>{call.organizer.get_humanized_repr()}</b></td> \
81
+ <td style='border: 1px solid #ddd;padding: 2px; width: 150px;' >{participants}</td> \
82
+ </tr>"
83
+ message += "</table><br/>"
84
+ message += "</div>"
85
+ return message
86
+
87
+
88
+ @receiver(add_employee_activity_to_daily_brief, sender="directory.Person")
89
+ def send_call_daily_summary(
90
+ sender,
91
+ instance: Person,
92
+ val_date: date,
93
+ daily_call_summary_receiver_group_ids: list[int] | None = None,
94
+ daily_call_summary_profile_ids: list[int] | None = None,
95
+ daily_call_summary_profile_group_ids: list[int] | None = None,
96
+ **kwargs,
97
+ ) -> tuple[str, str] | None:
98
+ # if either sales or management
99
+ if daily_call_summary_receiver_group_ids:
100
+ groups = Group.objects.filter(id__in=daily_call_summary_receiver_group_ids)
101
+ else:
102
+ groups = Group.objects.filter(Q(name__iexact="sales") | Q(name__iexact="management"))
103
+ if instance.user_account.groups.filter(id__in=groups.values("id")).exists():
104
+ internal_users = get_internal_users().filter(is_active=True)
105
+ profiles = Person.objects.filter(user_account__in=internal_users)
106
+ if daily_call_summary_profile_ids:
107
+ profiles = profiles.filter(id__in=daily_call_summary_profile_ids)
108
+ elif daily_call_summary_profile_group_ids:
109
+ profiles = profiles.filter(
110
+ user_account__groups__in=Group.objects.filter(id__in=daily_call_summary_profile_group_ids)
111
+ )
112
+
113
+ end_date = val_date
114
+ if val_date.weekday() == 0:
115
+ start_date = end_date - timedelta(days=8)
116
+ title = f"Call summary - From {start_date:%Y-%m-%d} To {end_date:%Y-%m-%d} (Weekly)"
117
+ message = generate_call_summary(profiles, start_date=start_date, end_date=end_date, include_detail=False)
118
+ else:
119
+ start_date = end_date - timedelta(days=1)
120
+ title = "Detailed Daily Call summary"
121
+ message = generate_call_summary(profiles, start_date=start_date, end_date=end_date, include_detail=True)
122
+ return title, message
@@ -1,204 +1,14 @@
1
- import os
2
- from collections import defaultdict
3
- from datetime import date, timedelta
1
+ from datetime import date
4
2
 
5
- import boto3
6
- import humanize
7
3
  from celery import shared_task
8
4
  from django.contrib.auth import get_user_model
9
- from django.contrib.auth.models import Group
10
- from django.db.models import DurationField, ExpressionWrapper, F, Q
11
- from wbcore.contrib.directory.models import Person
5
+ from django.db.models import Q
12
6
  from wbcore.contrib.notifications.dispatch import send_notification
13
- from wbcore.permissions.shortcuts import get_internal_users
14
7
 
15
8
  from wbintegrator_office365.importer import MicrosoftGraphAPI
16
- from wbintegrator_office365.models.event import CallEvent
17
9
  from wbintegrator_office365.models.subscription import Subscription
18
10
 
19
11
 
20
- def format_td(td: timedelta) -> str:
21
- total_seconds = td.total_seconds()
22
- if total_seconds == 0:
23
- return "Missed"
24
- elif total_seconds < 60:
25
- return "< 1min"
26
- return humanize.precisedelta(td, suppress=["hours"], minimum_unit="seconds", format="%0.0f")
27
-
28
-
29
- #################################################
30
- ################# TEMPORARY #####################
31
- #################################################
32
-
33
-
34
- def convert_user(user):
35
- email = user["Attributes"][0]["Value"]
36
- first = user["Attributes"][3]["Value"]
37
- last = user["Attributes"][2]["Value"]
38
-
39
- return f"{email} ({first} {last})"
40
-
41
-
42
- def get_fundy_users():
43
- access_key = os.environ["FUNDY_BOTO_ACCESS_KEY"]
44
- secret_access_key = os.environ["FUNDY_BOTO_SECRET_ACCESS_KEY"]
45
-
46
- client = boto3.client(
47
- "cognito-idp", region_name="eu-north-1", aws_access_key_id=access_key, aws_secret_access_key=secret_access_key
48
- )
49
-
50
- pagination_token = None
51
- users = []
52
- while True:
53
- if not pagination_token:
54
- response = client.list_users(
55
- UserPoolId="eu-north-1_IFRQriJAf",
56
- )
57
- else:
58
- response = client.list_users(
59
- UserPoolId="eu-north-1_IFRQriJAf",
60
- PaginationToken=pagination_token,
61
- )
62
-
63
- users.extend(response["Users"])
64
- if response.get("PaginationToken", None) is None:
65
- break
66
- else:
67
- pagination_token = response["PaginationToken"]
68
-
69
- users_date = defaultdict(list)
70
- for user in sorted(users, key=lambda x: x["UserCreateDate"]):
71
- users_date[user["UserCreateDate"].date()].append(convert_user(user))
72
- return users_date, len(users)
73
-
74
-
75
- def get_fundy_user_statistics():
76
- today = date.today()
77
- users, total_users = get_fundy_users()
78
-
79
- yesterday_users = users.get(today - timedelta(days=1), [])
80
- return yesterday_users, len(yesterday_users), total_users
81
-
82
-
83
- #################################################
84
- ################ TEMPORARY END ##################
85
- #################################################
86
-
87
-
88
- @shared_task
89
- def send_call_summary(
90
- to_emails: list,
91
- profile_ids: list[int] | None = None,
92
- group_id: int | None = None,
93
- offset: int = 0,
94
- include_detail: bool = True,
95
- ):
96
- internal_users = get_internal_users().filter(is_active=True)
97
- profiles = Person.objects.filter(user_account__in=internal_users)
98
- if profile_ids:
99
- profiles = profiles.filter(id__in=profile_ids)
100
- elif group_id:
101
- profiles = profiles.filter(user_account__in=Group.objects.get(id=group_id).user_set.all())
102
-
103
- end_date = date.today()
104
- start_date = end_date - timedelta(days=offset + 1)
105
- if offset == 0:
106
- frequency_repr = "Daily"
107
- date_repr = start_date.strftime("%Y-%m-%d")
108
- elif offset == 7:
109
- frequency_repr = "Weekly"
110
- date_repr = f"From {start_date:%Y-%m-%d} To {end_date:%Y-%m-%d}"
111
- else:
112
- frequency_repr = f"{offset} days"
113
- date_repr = f"From {start_date:%Y-%m-%d} To {end_date:%Y-%m-%d}"
114
-
115
- date_repr = f"{date_repr} ({frequency_repr})"
116
- calls = CallEvent.objects.filter(
117
- start__date__gte=start_date,
118
- end__date__lte=end_date,
119
- ).annotate(duration=ExpressionWrapper(F("end") - F("start"), output_field=DurationField()))
120
- if profiles.exists():
121
- message = """
122
- <div style="background-color: white; width: 720px; margin-bottom: 50px">
123
- """
124
- for profile in profiles:
125
- call_events = calls.filter(
126
- participants__tenant_user__profile=profile,
127
- ).order_by("start")
128
-
129
- message += f"""
130
- <div style="text-align: left;">
131
- <p><b>{profile.computed_str}</b></p>
132
- <table width="100%; table-layout: fixed; border-collapse: collapse;">
133
- <tr>
134
- <td style="width: 33.33%; text-align: center;">Total Calls: <b>{call_events.count()}</b></td>
135
- <td style="width: 33.33%; text-align: center;">under 1 minute: <b>{call_events.filter(duration__lte=timedelta(seconds=60)).count()}</b></td>
136
- <td style="width: 33.33%; text-align: center;">above 1 minute: <b>{call_events.filter(duration__gt=timedelta(seconds=60)).count()}</b></td>
137
- </tr>
138
- </table>
139
- </div>
140
- """
141
- if include_detail:
142
- for call_date in call_events.dates("start", "day", order="DESC"):
143
- call_day_events = call_events.filter(start__date=call_date)
144
- if call_day_events.exists():
145
- message += f"<p><b>{call_date:%Y-%m-%d}:</b></p>"
146
- message += "<table style='border-collapse: collapse; width: 720px; table-layout: fixed;'> \
147
- <tr style='color: white; background-color: #1868ae;'> \
148
- <th style='border: 1px solid #ddd;padding: 2px 7px; width: 20px;' >Start</th> \
149
- <th style='border: 1px solid #ddd;padding: 2px 7px; width: 20px;' >End</th> \
150
- <th style='border: 1px solid #ddd;padding: 2px 7px; width: 60px;' >Duration</th> \
151
- <th style='border: 1px solid #ddd;padding: 2px 7px; width: 80px;' >Organized by</th> \
152
- <th style='border: 1px solid #ddd;padding: 2px 7px; width: 150px;' >Participants</th> \
153
- </tr>"
154
- for call in call_day_events:
155
- participants = ",".join(
156
- filter(
157
- None,
158
- [
159
- p.get_humanized_repr()
160
- for p in call.participants.exclude(tenant_user__profile=profile)
161
- ],
162
- )
163
- )
164
- message += f"<tr> \
165
- <td style='border: 1px solid #ddd;padding: 2px; width: 20px;' >{call.start.astimezone():%H:%M}</td> \
166
- <td style='border: 1px solid #ddd;padding: 2px; width: 20px;' >{call.end.astimezone():%H:%M}</td> \
167
- <td style='border: 1px solid #ddd;padding: 2px; width: 60px;' text-align:center;><b>{format_td(call.end - call.start)}</b></td> \
168
- <td style='border: 1px solid #ddd;padding: 2px; width: 80px;' ><b>{call.organizer.get_humanized_repr()}</b></td> \
169
- <td style='border: 1px solid #ddd;padding: 2px; width: 150px;' >{participants}</td> \
170
- </tr>"
171
- message += "</table><br/>"
172
-
173
- message += "</div>"
174
-
175
- ######## TEMPORARY START ########
176
- yesterday_users, yesterday_users_count, total_users_count = get_fundy_user_statistics()
177
- message += f"""
178
- <div>
179
- <h3>FUNDY USER STATISTICS</h3>
180
- <strong>Yesterday Users:</strong> {yesterday_users_count}
181
- <strong>Total Users:</strong> {total_users_count}
182
- <ul>
183
- """
184
- for user in yesterday_users:
185
- message += f"<li>{user}</li>"
186
- message += "</ul></div>"
187
- ######## TEMPORARY END ########
188
-
189
- title = f"Call summary - {date_repr}"
190
- if include_detail:
191
- title = "Detailed " + title
192
- for to_email in to_emails:
193
- recipient = get_user_model().objects.get(email=to_email)
194
- send_notification(
195
- code="wbintegrator_office365.callevent.call_summary",
196
- title=title,
197
- body=message,
198
- user=recipient,
199
- )
200
-
201
-
202
12
  @shared_task
203
13
  def notify_no_active_call_record_subscription(to_email):
204
14
  recipient = get_user_model().objects.filter(email=to_email)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wbintegrator_office365
3
- Version: 1.56.1
3
+ Version: 1.56.2
4
4
  Author-email: Christopher Wittlinger <c.wittlinger@stainly.com>
5
5
  Requires-Dist: boto3>=1.35.99
6
6
  Requires-Dist: bs4==0.0.*
@@ -5,7 +5,7 @@ wbintegrator_office365/dynamic_preferences_registry.py,sha256=RIptl8WGibV_XMynXl
5
5
  wbintegrator_office365/factories.py,sha256=pG8moGurbSElIugs_Gz3nd7-UJt3vC6mGy-EZQIeldI,3872
6
6
  wbintegrator_office365/filters.py,sha256=bSX1jrP1SrqiGkysoX__AqPRUPSU-uQq_-tOHinqijU,7447
7
7
  wbintegrator_office365/serializers.py,sha256=tR_HdsyxwCZo2_sOdhj2NErOEjTRyoIXA6DRR-BjVKs,8097
8
- wbintegrator_office365/tasks.py,sha256=-kji2Bipo95nP4UWWfRi4JrW2-Hn8AXv4KiM-oQYsyU,10176
8
+ wbintegrator_office365/tasks.py,sha256=tuIyMhYV3sDMPF_ZOwFbKj6nRB1KwU6ArhThfWD9BjE,1942
9
9
  wbintegrator_office365/urls.py,sha256=qfIIb3CNJw40xhGJnE7ztEMt2l-kgnRXA_P8Y3sDlPg,1922
10
10
  wbintegrator_office365/configurations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  wbintegrator_office365/configurations/configurations/__init__.py,sha256=tIBWiG0KWrDJ1xtKe5m1W9aKX5PMNLwsYuoy7xrQ4uI,1145
@@ -19,8 +19,9 @@ wbintegrator_office365/migrations/0001_initial_squashed_squashed_0003_alter_cale
19
19
  wbintegrator_office365/migrations/0002_remove_calendar_owner_remove_calendarevent_activity_and_more.py,sha256=VDbggoPh0HKghwoC-FnSmm1cT1KnsWid1VNg1MOu34s,2313
20
20
  wbintegrator_office365/migrations/0003_alter_event_options.py,sha256=rUYPQdwEuJowo10veLOZPtV3WMqxJu8_uan600EcpN4,585
21
21
  wbintegrator_office365/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
- wbintegrator_office365/models/__init__.py,sha256=sZKutR9oNMm2IaON35doaMVPgeMfxmWDqoAdALHRLIA,126
22
+ wbintegrator_office365/models/__init__.py,sha256=lS3EejdieisEQOZUIf-uMxjR1nVU9t6fV6fCeG7_BVU,149
23
23
  wbintegrator_office365/models/event.py,sha256=tw9GaCrNieQ9KRDFcMVoobwjYBZObTkTmgldVtwcLCY,27110
24
+ wbintegrator_office365/models/reports.py,sha256=BjAOfPwZkrEX9_9nPtZrfxHPevILeNkQ5kR-7saPk1w,6344
24
25
  wbintegrator_office365/models/subscription.py,sha256=ynp74wjd8OFRrnkomGl7pc8OyXRRvdHyl36TzHiCVSM,6423
25
26
  wbintegrator_office365/models/tenant.py,sha256=-FS6jg9nt8IgUv_729D92QFvrrbKleBUGnXhwaLv5Sc,2357
26
27
  wbintegrator_office365/templates/admin/tenant_change_list.html,sha256=mI4C1ZmTkl5MdN4CgoIWgCziSReqlHiK71aI7zJpVEM,358
@@ -37,6 +38,6 @@ wbintegrator_office365/viewsets/endpoints.py,sha256=NxEdH-tKk87mJdSNxpOZdN2z0AhY
37
38
  wbintegrator_office365/viewsets/menu.py,sha256=hafwoiHZmN3ltf30dTfwHreDwhyfdHlGOQk8bB7jB_o,2406
38
39
  wbintegrator_office365/viewsets/titles.py,sha256=9GZ_fqN9-sJzYHFvibOhNrRQQMNTh_m4eAv66VaGzDM,1214
39
40
  wbintegrator_office365/viewsets/viewsets.py,sha256=Mu6loE9ICDYEhWZNURsbdFj8ofdz4qZUSVStydQ7pW4,26353
40
- wbintegrator_office365-1.56.1.dist-info/METADATA,sha256=K7YOguL0jNaeQ9QZoKl68bfq1GSNirWmwBTBsOfIVQU,334
41
- wbintegrator_office365-1.56.1.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
42
- wbintegrator_office365-1.56.1.dist-info/RECORD,,
41
+ wbintegrator_office365-1.56.2.dist-info/METADATA,sha256=MScFdyQYQ16vhMtumKF2Dacu8QoVlO6PPAOHdk8fT94,334
42
+ wbintegrator_office365-1.56.2.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
43
+ wbintegrator_office365-1.56.2.dist-info/RECORD,,