wbintegrator_office365 1.55.3__tar.gz → 1.55.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/PKG-INFO +1 -1
  2. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/models/event.py +11 -3
  3. wbintegrator_office365-1.55.5/wbintegrator_office365/tasks.py +154 -0
  4. wbintegrator_office365-1.55.3/wbintegrator_office365/tasks.py +0 -109
  5. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/.gitignore +0 -0
  6. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/pyproject.toml +0 -0
  7. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/__init__.py +0 -0
  8. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/admin.py +0 -0
  9. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/apps.py +0 -0
  10. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/configurations/__init__.py +0 -0
  11. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/configurations/configurations/__init__.py +0 -0
  12. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/dynamic_preferences_registry.py +0 -0
  13. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/factories.py +0 -0
  14. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/filters.py +0 -0
  15. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/importer/__init__.py +0 -0
  16. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/importer/api.py +0 -0
  17. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/importer/disable_signals.py +0 -0
  18. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/importer/parser.py +0 -0
  19. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/kpi_handlers/__init__.py +0 -0
  20. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/kpi_handlers/calls.py +0 -0
  21. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/migrations/0001_initial_squashed_squashed_0003_alter_calendar_owner_alter_calendarevent_organizer_and_more.py +0 -0
  22. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/migrations/0002_remove_calendar_owner_remove_calendarevent_activity_and_more.py +0 -0
  23. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/migrations/0003_alter_event_options.py +0 -0
  24. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/migrations/__init__.py +0 -0
  25. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/models/__init__.py +0 -0
  26. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/models/subscription.py +0 -0
  27. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/models/tenant.py +0 -0
  28. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/serializers.py +0 -0
  29. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/templates/admin/tenant_change_list.html +0 -0
  30. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/tests/__init__.py +0 -0
  31. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/tests/conftest.py +0 -0
  32. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/tests/test_admin.py +0 -0
  33. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/tests/test_models.py +0 -0
  34. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/tests/test_tasks.py +0 -0
  35. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/tests/test_views.py +0 -0
  36. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/tests/tests.py +0 -0
  37. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/urls.py +0 -0
  38. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/viewsets/__init__.py +0 -0
  39. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/viewsets/display.py +0 -0
  40. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/viewsets/endpoints.py +0 -0
  41. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/viewsets/menu.py +0 -0
  42. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/viewsets/titles.py +0 -0
  43. {wbintegrator_office365-1.55.3 → wbintegrator_office365-1.55.5}/wbintegrator_office365/viewsets/viewsets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wbintegrator_office365
3
- Version: 1.55.3
3
+ Version: 1.55.5
4
4
  Author-email: Christopher Wittlinger <c.wittlinger@stainly.com>
5
5
  Requires-Dist: bs4==0.0.*
6
6
  Requires-Dist: oauthlib==3.1.*
@@ -290,15 +290,23 @@ class CallUser(WBModel):
290
290
  on_delete=models.deletion.SET_NULL,
291
291
  )
292
292
 
293
- def __str__(self):
293
+ def get_humanized_repr(self) -> str | None:
294
294
  if self.tenant_user:
295
295
  if self.tenant_user.profile:
296
- return f"{self.tenant_user.profile.computed_str}"
296
+ return str(self.tenant_user.profile)
297
297
  elif self.tenant_user.mail or self.tenant_user.display_name:
298
298
  mail = self.tenant_user.mail if self.tenant_user.mail else self.tenant_user.id
299
299
  return f"{self.tenant_user.display_name}({mail})"
300
300
  else:
301
- return f"{self.tenant_user.tenant_id}"
301
+ contacts = TelephoneContact.objects.filter(number=self.tenant_user.tenant_id, entry__isnull=False)
302
+ if contacts.exists():
303
+ return str(contacts.first().entry)
304
+ elif self.tenant_user.tenant_id[0] == "+":
305
+ return self.tenant_user.tenant_id
306
+
307
+ def __str__(self):
308
+ if repr := self.get_humanized_repr():
309
+ return repr
302
310
  return f"{self.id}"
303
311
 
304
312
  @classmethod
@@ -0,0 +1,154 @@
1
+ from datetime import date, timedelta
2
+
3
+ import humanize
4
+ from celery import shared_task
5
+ from django.contrib.auth import get_user_model
6
+ from django.contrib.auth.models import Group
7
+ from django.db.models import DurationField, ExpressionWrapper, F, Q
8
+ from wbcore.contrib.directory.models import Person
9
+ from wbcore.contrib.notifications.dispatch import send_notification
10
+ from wbcore.permissions.shortcuts import get_internal_users
11
+
12
+ from wbintegrator_office365.importer import MicrosoftGraphAPI
13
+ from wbintegrator_office365.models.event import CallEvent
14
+ from wbintegrator_office365.models.subscription import Subscription
15
+
16
+
17
+ def format_td(td: timedelta) -> str:
18
+ total_seconds = td.total_seconds()
19
+ if total_seconds == 0:
20
+ return "Missed"
21
+ elif total_seconds < 60:
22
+ return "< 1min"
23
+ return humanize.precisedelta(td, suppress=["hours"], minimum_unit="seconds", format="%0.0f")
24
+
25
+
26
+ @shared_task
27
+ def send_call_summary(
28
+ to_emails: list,
29
+ profile_ids: list[int] | None = None,
30
+ group_id: int | None = None,
31
+ offset: int = 0,
32
+ include_detail: bool = True,
33
+ ):
34
+ internal_users = get_internal_users().filter(is_active=True)
35
+ profiles = Person.objects.filter(user_account__in=internal_users)
36
+ if profile_ids:
37
+ profiles = profiles.filter(id__in=profile_ids)
38
+ elif group_id:
39
+ profiles = profiles.filter(user_account__in=Group.objects.get(id=group_id).user_set.all())
40
+
41
+ end_date = date.today()
42
+ start_date = end_date - timedelta(days=offset)
43
+ if offset == 0:
44
+ frequency_repr = "Daily"
45
+ date_repr = start_date.strftime("%Y-%m-%d")
46
+ elif offset == 7:
47
+ frequency_repr = "Weekly"
48
+ date_repr = f"From {start_date:%Y-%m-%d} To {end_date:%Y-%m-%d}"
49
+ else:
50
+ frequency_repr = f"{offset} days"
51
+ date_repr = f"From {start_date:%Y-%m-%d} To {end_date:%Y-%m-%d}"
52
+
53
+ date_repr = f"{date_repr} ({frequency_repr})"
54
+ calls = CallEvent.objects.filter(
55
+ start__date__gte=start_date,
56
+ end__date__lte=end_date,
57
+ ).annotate(duration=ExpressionWrapper(F("end") - F("start"), output_field=DurationField()))
58
+ if profiles.exists():
59
+ message = """
60
+ <div style="background-color: white; width: 720px; margin-bottom: 50px">
61
+ """
62
+ for profile in profiles:
63
+ call_events = calls.filter(
64
+ participants__tenant_user__profile=profile,
65
+ ).order_by("start")
66
+
67
+ message += f"""
68
+ <div style="text-align: left;">
69
+ <p><b>{profile.computed_str}</b></p>
70
+ <table width="100%; table-layout: fixed; border-collapse: collapse;">
71
+ <tr>
72
+ <td style="width: 33.33%; text-align: center;">Total Calls: <b>{call_events.count()}</b></td>
73
+ <td style="width: 33.33%; text-align: center;">under 1 minute: <b>{call_events.filter(duration__lte=timedelta(seconds=60)).count()}</b></td>
74
+ <td style="width: 33.33%; text-align: center;">above 1 minute: <b>{call_events.filter(duration__gt=timedelta(seconds=60)).count()}</b></td>
75
+ </tr>
76
+ </table>
77
+ </div>
78
+ """
79
+ if include_detail:
80
+ for call_date in call_events.dates("start", "day"):
81
+ call_day_events = call_events.filter(start__date=call_date)
82
+ if call_day_events.exists():
83
+ message += f"<p><b>{call_date:%Y-%m-%d}:</b></p>"
84
+ message += "<table style='border-collapse: collapse; width: 720px; table-layout: fixed;'> \
85
+ <tr style='color: white; background-color: #1868ae;'> \
86
+ <th style='border: 1px solid #ddd;padding: 2px 7px; width: 20px;' >Start</th> \
87
+ <th style='border: 1px solid #ddd;padding: 2px 7px; width: 20px;' >End</th> \
88
+ <th style='border: 1px solid #ddd;padding: 2px 7px; width: 60px;' >Duration</th> \
89
+ <th style='border: 1px solid #ddd;padding: 2px 7px; width: 80px;' >Organized by</th> \
90
+ <th style='border: 1px solid #ddd;padding: 2px 7px; width: 150px;' >Participants</th> \
91
+ </tr>"
92
+ for call in call_day_events:
93
+ participants = ",".join(
94
+ filter(
95
+ None,
96
+ [
97
+ p.get_humanized_repr()
98
+ for p in call.participants.exclude(tenant_user__profile=profile)
99
+ ],
100
+ )
101
+ )
102
+ message += f"<tr> \
103
+ <td style='border: 1px solid #ddd;padding: 2px; width: 20px;' >{call.start.astimezone():%H:%M}</td> \
104
+ <td style='border: 1px solid #ddd;padding: 2px; width: 20px;' >{call.end.astimezone():%H:%M}</td> \
105
+ <td style='border: 1px solid #ddd;padding: 2px; width: 60px;' text-align:center;><b>{format_td(call.end - call.start)}</b></td> \
106
+ <td style='border: 1px solid #ddd;padding: 2px; width: 80px;' ><b>{call.organizer}</b></td> \
107
+ <td style='border: 1px solid #ddd;padding: 2px; width: 150px;' >{participants}</td> \
108
+ </tr>"
109
+ message += "</table><br/>"
110
+
111
+ message += "</div>"
112
+ for to_email in to_emails:
113
+ recipient = get_user_model().objects.get(email=to_email)
114
+ send_notification(
115
+ code="wbintegrator_office365.callevent.call_summary",
116
+ title=f"Call summary - {date_repr}",
117
+ body=message,
118
+ user=recipient,
119
+ )
120
+
121
+
122
+ @shared_task
123
+ def notify_no_active_call_record_subscription(to_email):
124
+ recipient = get_user_model().objects.filter(email=to_email)
125
+ ms_subscriptions = [elt.get("id") for elt in MicrosoftGraphAPI().subscriptions()]
126
+ qs_subscriptions = Subscription.objects.filter(
127
+ Q(is_enable=True) & Q(subscription_id__isnull=False) & Q(type_resource=Subscription.TypeResource.CALLRECORD)
128
+ )
129
+ enable_subcriptions = qs_subscriptions.filter(subscription_id__in=ms_subscriptions)
130
+ if recipient.exists() and (
131
+ len(ms_subscriptions) == 0 or (qs_subscriptions.count() > 0 and enable_subcriptions.count() == 0)
132
+ ):
133
+ _day = date.today()
134
+ send_notification(
135
+ code="wbintegrator_office365.callevent.notify",
136
+ title=f"No active Call Record subscriptions in Microsoft - {_day}",
137
+ body=f"""<p>There are currently no active Call record subscriptions in Microsoft, so we are no longer receiving calls, Please check</p>
138
+ <ul>
139
+ <li>Number of subscriptions on Microsoft: <b>{len(ms_subscriptions)}</b></li>
140
+ <li>Number of Call subscriptions: <b>{qs_subscriptions.count()}</b></li>
141
+ <li>Number of enabled calling subscriptions: <b>{enable_subcriptions.count()}</b></li>
142
+
143
+ </ul>
144
+ """,
145
+ user=recipient.first(),
146
+ )
147
+
148
+
149
+ @shared_task
150
+ def periodic_resubscribe_task():
151
+ for subscription in Subscription.objects.filter(
152
+ is_enable=True, type_resource=Subscription.TypeResource.CALLRECORD, subscription_id__isnull=False
153
+ ):
154
+ subscription.resubscribe()
@@ -1,109 +0,0 @@
1
- import math
2
- from datetime import date, timedelta
3
-
4
- from celery import shared_task
5
- from django.contrib.auth import get_user_model
6
- from django.db.models import Q
7
- from wbcore.contrib.directory.models import Person
8
- from wbcore.contrib.notifications.dispatch import send_notification
9
-
10
- from wbintegrator_office365.importer import MicrosoftGraphAPI
11
- from wbintegrator_office365.models.event import CallEvent
12
- from wbintegrator_office365.models.subscription import Subscription
13
-
14
-
15
- @shared_task
16
- def send_call_summary(to_emails: list, profile_ids: list, offset: int = 1):
17
- for to_email in to_emails:
18
- if (recipient := get_user_model().objects.filter(email=to_email).first()) and (
19
- profiles := Person.objects.filter(id__in=profile_ids)
20
- ):
21
- _day = date.today() - timedelta(days=offset)
22
-
23
- message = "<html><head><style> #summery_table tr:nth-child(even){background-color: #f2f2f2;}</style></head><body>"
24
-
25
- for profile in profiles:
26
- call_events = CallEvent.objects.filter(
27
- participants__tenant_user__profile__computed_str__icontains=profile.computed_str,
28
- start__date=_day,
29
- end__date=_day,
30
- ).order_by("start")
31
-
32
- message += f"<p> \
33
- <span><b> {profile.computed_str} </b></span><br/> \
34
- <span>Date: <b>{_day}</b> </span><br/> \
35
- <span>Total number of calls: <b>{call_events.count()}</b> </span> \
36
- </p>"
37
- if call_events.count():
38
- message += "<table id='summery_table' style='border-collapse: collapse;'> \
39
- <tr style='color: white; background-color: #1868ae;'> \
40
- <th style='border: 1px solid #ddd;padding: 10px 7px;' >Start</th> \
41
- <th style='border: 1px solid #ddd;padding: 10px 7px;' >End</th> \
42
- <th style='border: 1px solid #ddd;padding: 10px 7px;' >Duration (min)</th> \
43
- <th style='border: 1px solid #ddd;padding: 10px 7px;' >Organized by</th> \
44
- <th style='border: 1px solid #ddd;padding: 10px 7px;' >Participants</th> \
45
- </tr>"
46
- for call in call_events:
47
- participants = ""
48
- count = 0
49
- total_participants = call.participants.exclude(tenant_user__profile=profile).count()
50
- for participant in call.participants.exclude(tenant_user__profile=profile):
51
- count += 1
52
- participants += f"{participant.__str__()}"
53
- if count < total_participants:
54
- participants += ", "
55
-
56
- delta_in_minutes = divmod((call.end - call.start).total_seconds(), 60)
57
- _duration = f"{math.floor(delta_in_minutes[0])}:{math.floor(delta_in_minutes[1])}"
58
- message += f"<tr> \
59
- <td style='border: 1px solid #ddd;padding: 2px;' >{call.start.astimezone().strftime('%Y-%m-%d %H:%M:%S')}</td> \
60
- <td style='border: 1px solid #ddd;padding: 2px;' >{call.end.astimezone().strftime('%Y-%m-%d %H:%M:%S')}</td> \
61
- <td style='border: 1px solid #ddd;padding: 0px;' text-align:center;><b>{_duration}</b></td> \
62
- <td style='border: 1px solid #ddd;padding: 2px;' ><b>{call.organizer.__str__()}</b></td> \
63
- <td style='border: 1px solid #ddd;padding: 2px;' >{participants}</td> \
64
- </tr>"
65
- message += "</table><br/>"
66
-
67
- message += "</body></html>"
68
-
69
- send_notification(
70
- code="wbintegrator_office365.callevent.call_summary",
71
- title=f"Call summary - {_day}",
72
- body=message,
73
- user=recipient,
74
- )
75
-
76
-
77
- @shared_task
78
- def notify_no_active_call_record_subscription(to_email):
79
- recipient = get_user_model().objects.filter(email=to_email)
80
- ms_subscriptions = [elt.get("id") for elt in MicrosoftGraphAPI().subscriptions()]
81
- qs_subscriptions = Subscription.objects.filter(
82
- Q(is_enable=True) & Q(subscription_id__isnull=False) & Q(type_resource=Subscription.TypeResource.CALLRECORD)
83
- )
84
- enable_subcriptions = qs_subscriptions.filter(subscription_id__in=ms_subscriptions)
85
- if recipient.exists() and (
86
- len(ms_subscriptions) == 0 or (qs_subscriptions.count() > 0 and enable_subcriptions.count() == 0)
87
- ):
88
- _day = date.today()
89
- send_notification(
90
- code="wbintegrator_office365.callevent.notify",
91
- title=f"No active Call Record subscriptions in Microsoft - {_day}",
92
- body=f"""<p>There are currently no active Call record subscriptions in Microsoft, so we are no longer receiving calls, Please check</p>
93
- <ul>
94
- <li>Number of subscriptions on Microsoft: <b>{len(ms_subscriptions)}</b></li>
95
- <li>Number of Call subscriptions: <b>{qs_subscriptions.count()}</b></li>
96
- <li>Number of enabled calling subscriptions: <b>{enable_subcriptions.count()}</b></li>
97
-
98
- </ul>
99
- """,
100
- user=recipient.first(),
101
- )
102
-
103
-
104
- @shared_task
105
- def periodic_resubscribe_task():
106
- for subscription in Subscription.objects.filter(
107
- is_enable=True, type_resource=Subscription.TypeResource.CALLRECORD, subscription_id__isnull=False
108
- ):
109
- subscription.resubscribe()