3
3
import datetime
4
4
import logging
5
5
import re
6
+ import typing
6
7
from collections import namedtuple
7
8
from typing import TYPE_CHECKING
8
9
35
36
"""
36
37
if TYPE_CHECKING :
37
38
from apps .schedules .models import OnCallSchedule
38
- from apps .user_management .models import User
39
+ from apps .user_management .models import Organization , User
40
+ from apps .user_management .models .user import UserQuerySet
41
+
42
+ logger = logging .getLogger (__name__ )
43
+ logger .setLevel (logging .DEBUG )
39
44
40
45
41
- def users_in_ical (usernames_from_ical , organization , include_viewers = False ):
46
+ def users_in_ical (
47
+ usernames_from_ical : typing .List [str ],
48
+ organization : Organization ,
49
+ include_viewers = False ,
50
+ users_to_filter : typing .Optional [UserQuerySet ] = None ,
51
+ ) -> UserQuerySet :
42
52
"""
43
- Parse ical file and return list of users found
44
- NOTE: only grafana username will be used, consider adding grafana email and id
53
+ This method returns a `UserQuerySet`, filtered by users whose username, or case-insensitive e-mail,
54
+ is present in `usernames_from_ical`. If `include_viewers` is set to `True`, users are further filtered down
55
+ based on their granted permissions.
56
+
57
+ Parameters
58
+ ----------
59
+ usernames_from_ical : typing.List[str]
60
+ A list of usernames present in the ical feed
61
+ organization : apps.user_management.models.organization.Organization
62
+ The organization in question
63
+ include_viewers : bool
64
+ Whether or not the list should be further filtered to exclude users based on granted permissions
65
+ users_to_filter : typing.Optional[UserQuerySet]
66
+ Filter users without making SQL queries if users_to_filter arg is provided
67
+ users_to_filter is passed in `apps.schedules.ical_utils.get_oncall_users_for_multiple_schedules`
45
68
"""
46
69
from apps .user_management .models import User
47
70
71
+ emails_from_ical = [username .lower () for username in usernames_from_ical ]
72
+
73
+ if users_to_filter is not None :
74
+ return list (
75
+ {user for user in users_to_filter if user .username in usernames_from_ical or user .email in emails_from_ical }
76
+ )
77
+
48
78
users_found_in_ical = organization .users
49
79
if not include_viewers :
50
- # TODO: this is a breaking change....
51
80
users_found_in_ical = users_found_in_ical .filter (
52
81
** User .build_permissions_query (RBACPermission .Permissions .SCHEDULES_WRITE , organization )
53
82
)
54
83
55
- user_emails = [v .lower () for v in usernames_from_ical ]
56
84
users_found_in_ical = users_found_in_ical .filter (
57
- (Q (username__in = usernames_from_ical ) | Q (email__lower__in = user_emails ))
85
+ (Q (username__in = usernames_from_ical ) | Q (email__lower__in = emails_from_ical ))
58
86
).distinct ()
59
87
60
- # Here is the example how we extracted users previously, using slack fields too
61
- # user_roles_found_in_ical = team.org_user_role.filter(role__in=[ROLE_ADMIN, ROLE_USER]).filter(
62
- # Q(
63
- # Q(amixr_user__slack_user_identities__slack_team_identity__amixr_team=team) &
64
- # Q(
65
- # Q(amixr_user__slack_user_identities__profile_display_name__in=usernames_from_ical) |
66
- # Q(amixr_user__slack_user_identities__cached_name__in=usernames_from_ical) |
67
- # Q(amixr_user__slack_user_identities__slack_id__in=[username.split(" ")[0] for username in
68
- # usernames_from_ical]) |
69
- # Q(amixr_user__slack_user_identities__cached_slack_login__in=usernames_from_ical) |
70
- # Q(amixr_user__slack_user_identities__profile_real_name__in=usernames_from_ical)
71
- # )
72
- # )
73
- # |
74
- # Q(username__in=usernames_from_ical)
75
- # ).annotate(is_deleted_sui=Subquery(slack_user_identity_subquery.values("deleted")[:1])).filter(
76
- # ~Q(is_deleted_sui=True) | Q(is_deleted_sui__isnull=True)).distinct()
77
- # return user_roles_found_in_ical
78
-
79
88
return users_found_in_ical
80
89
81
90
82
91
@timed_lru_cache (timeout = 100 )
83
- def memoized_users_in_ical (usernames_from_ical , organization ) :
92
+ def memoized_users_in_ical (usernames_from_ical : typing . List [ str ] , organization : Organization ) -> UserQuerySet :
84
93
# using in-memory cache instead of redis to avoid pickling python objects
85
94
return users_in_ical (usernames_from_ical , organization )
86
95
87
96
88
- logger = logging .getLogger (__name__ )
89
- logger .setLevel (logging .DEBUG )
90
-
91
-
92
97
# used for display schedule events on web
93
98
def list_of_oncall_shifts_from_ical (
94
99
schedule ,
@@ -288,17 +293,29 @@ def list_of_empty_shifts_in_schedule(schedule, start_date, end_date):
288
293
return sorted (empty_shifts , key = lambda dt : dt .start )
289
294
290
295
291
- def list_users_to_notify_from_ical (schedule , events_datetime = None , include_viewers = False ):
296
+ def list_users_to_notify_from_ical (
297
+ schedule , events_datetime = None , include_viewers = False , users_to_filter = None
298
+ ) -> UserQuerySet :
292
299
"""
293
300
Retrieve on-call users for the current time
294
301
"""
295
302
events_datetime = events_datetime if events_datetime else timezone .datetime .now (timezone .utc )
296
303
return list_users_to_notify_from_ical_for_period (
297
- schedule , events_datetime , events_datetime , include_viewers = include_viewers
304
+ schedule ,
305
+ events_datetime ,
306
+ events_datetime ,
307
+ include_viewers = include_viewers ,
308
+ users_to_filter = users_to_filter ,
298
309
)
299
310
300
311
301
- def list_users_to_notify_from_ical_for_period (schedule , start_datetime , end_datetime , include_viewers = False ):
312
+ def list_users_to_notify_from_ical_for_period (
313
+ schedule ,
314
+ start_datetime ,
315
+ end_datetime ,
316
+ include_viewers = False ,
317
+ users_to_filter = None ,
318
+ ) -> UserQuerySet :
302
319
# get list of iCalendars from current iCal files. If there is more than one calendar, primary calendar will always
303
320
# be the first
304
321
calendars = schedule .get_icalendars ()
@@ -316,7 +333,9 @@ def list_users_to_notify_from_ical_for_period(schedule, start_datetime, end_date
316
333
parsed_ical_events .setdefault (current_priority , []).extend (current_usernames )
317
334
# find users by usernames. if users are not found for shift, get users from lower priority
318
335
for _ , usernames in sorted (parsed_ical_events .items (), reverse = True ):
319
- users_found_in_ical = users_in_ical (usernames , schedule .organization , include_viewers = include_viewers )
336
+ users_found_in_ical = users_in_ical (
337
+ usernames , schedule .organization , include_viewers = include_viewers , users_to_filter = users_to_filter
338
+ )
320
339
if users_found_in_ical :
321
340
break
322
341
if users_found_in_ical :
@@ -325,6 +344,52 @@ def list_users_to_notify_from_ical_for_period(schedule, start_datetime, end_date
325
344
return users_found_in_ical
326
345
327
346
347
+ def get_oncall_users_for_multiple_schedules (
348
+ schedules , events_datetime = None
349
+ ) -> typing .Dict [OnCallSchedule , typing .List [User ]]:
350
+ from apps .user_management .models import User
351
+
352
+ if events_datetime is None :
353
+ events_datetime = timezone .datetime .now (timezone .utc )
354
+
355
+ # Exit early if there are no schedules
356
+ if not schedules .exists ():
357
+ return {}
358
+
359
+ # Assume all schedules from the queryset belong to the same organization
360
+ organization = schedules [0 ].organization
361
+
362
+ # Gather usernames from all events from all schedules
363
+ usernames = set ()
364
+ for schedule in schedules .all ():
365
+ calendars = schedule .get_icalendars ()
366
+ for calendar in calendars :
367
+ if calendar is None :
368
+ continue
369
+ events = ical_events .get_events_from_ical_between (calendar , events_datetime , events_datetime )
370
+ for event in events :
371
+ current_usernames , _ = get_usernames_from_ical_event (event )
372
+ usernames .update (current_usernames )
373
+
374
+ # Fetch relevant users from the db
375
+ emails = [username .lower () for username in usernames ]
376
+ users = organization .users .filter (
377
+ Q (** User .build_permissions_query (RBACPermission .Permissions .SCHEDULES_WRITE , organization ))
378
+ & (Q (username__in = usernames ) | Q (email__lower__in = emails ))
379
+ )
380
+
381
+ # Get on-call users
382
+ oncall_users = {}
383
+ for schedule in schedules .all ():
384
+ # pass user list to list_users_to_notify_from_ical
385
+ schedule_oncall_users = list_users_to_notify_from_ical (
386
+ schedule , events_datetime = events_datetime , users_to_filter = users
387
+ )
388
+ oncall_users .update ({schedule .pk : schedule_oncall_users })
389
+
390
+ return oncall_users
391
+
392
+
328
393
def parse_username_from_string (string ):
329
394
"""
330
395
Parse on-call shift user from the given string
0 commit comments