diff --git a/endorsement/dao/shared_drive.py b/endorsement/dao/shared_drive.py index e5c385aa..0f9058a5 100644 --- a/endorsement/dao/shared_drive.py +++ b/endorsement/dao/shared_drive.py @@ -15,6 +15,13 @@ r'^(?P[^@]+)@(uw|(u\.)?washington)\.edu$', re.I) +def shared_drive_lifecycle_expired(drive_record): + """ + Set lifecycle to expired for shared drive + """ + logger.info(f"Shared drive {drive_record} lifecycle expired") + + def load_shared_drives_from_csv(file_path): """ populate shared drive models diff --git a/endorsement/fixtures/test_data/member.json b/endorsement/fixtures/test_data/member.json index ab79fdba..758bfea1 100644 --- a/endorsement/fixtures/test_data/member.json +++ b/endorsement/fixtures/test_data/member.json @@ -2,54 +2,54 @@ "model": "endorsement.member", "pk": 1, "fields": { - "name": "javerage"}}, + "netid": "javerage"}}, { "model": "endorsement.member", "pk": 2, "fields": { - "name": "endorsee7"}}, + "netid": "endorsee7"}}, { "model": "endorsement.member", "pk": 3, "fields": { - "name": "yahoodood@yahoo.com"}}, + "netid": "yahoodood@yahoo.com"}}, { "model": "endorsement.member", "pk": 4, "fields": { - "name": "endorsee6"}}, + "netid": "endorsee6"}}, { "model": "endorsement.member", "pk": 5, "fields": { - "name": "jstaff"}}, + "netid": "jstaff"}}, { "model": "endorsement.member", "pk": 6, "fields": { - "name": "jinter"}}, + "netid": "jinter"}}, { "model": "endorsement.member", "pk": 7, "fields": { - "name": "endorsee5"}}, + "netid": "endorsee5"}}, { "model": "endorsement.member", "pk": 8, "fields": { - "name": "endorsee4"}}, + "netid": "endorsee4"}}, { "model": "endorsement.member", "pk": 9, "fields": { - "name": "endorsee3"}}, + "netid": "endorsee3"}}, { "model": "endorsement.member", "pk": 10, "fields": { - "name": "endorsee2"}}, + "netid": "endorsee2"}}, { "model": "endorsement.member", "pk": 11, "fields": { - "name": "endorsee1"}}] + "netid": "endorsee1"}}] diff --git a/endorsement/management/commands/expire_endorsees.py b/endorsement/management/commands/expire_endorsees.py index 670a265b..41b66fbe 100644 --- a/endorsement/management/commands/expire_endorsees.py +++ b/endorsement/management/commands/expire_endorsees.py @@ -4,7 +4,7 @@ from django.core.management.base import BaseCommand from django.core.mail import mail_managers from django.template import loader -from endorsement.policy import endorsements_to_expire +from endorsement.policy.endorsement import endorsements_to_expire from endorsement.dao.endorse import clear_endorsement import logging import urllib3 diff --git a/endorsement/migrations/0028_auto_20240506_1259.py b/endorsement/migrations/0028_auto_20240506_1259.py new file mode 100644 index 00000000..6a856df7 --- /dev/null +++ b/endorsement/migrations/0028_auto_20240506_1259.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.25 on 2024-05-06 19:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('endorsement', '0027_auto_20240503_1436'), + ] + + operations = [ + migrations.RenameField( + model_name='member', + old_name='name', + new_name='netid', + ), + migrations.AlterField( + model_name='shareddrive', + name='members', + field=models.ManyToManyField(blank=True, to='endorsement.SharedDriveMember'), + ), + ] diff --git a/endorsement/migrations/0029_auto_20240508_1240.py b/endorsement/migrations/0029_auto_20240508_1240.py new file mode 100644 index 00000000..990438d0 --- /dev/null +++ b/endorsement/migrations/0029_auto_20240508_1240.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.25 on 2024-05-08 19:40 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('endorsement', '0028_auto_20240506_1259'), + ] + + operations = [ + migrations.RemoveField( + model_name='shareddriverecord', + name='acceptance', + ), + migrations.DeleteModel( + name='SharedDriveAcceptance', + ), + ] diff --git a/endorsement/migrations/0030_shareddriveacceptance.py b/endorsement/migrations/0030_shareddriveacceptance.py new file mode 100644 index 00000000..a03dce14 --- /dev/null +++ b/endorsement/migrations/0030_shareddriveacceptance.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.25 on 2024-05-08 19:42 + +from django.db import migrations, models +import django.db.models.deletion +import django_prometheus.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('endorsement', '0029_auto_20240508_1240'), + ] + + operations = [ + migrations.CreateModel( + name='SharedDriveAcceptance', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('action', models.SmallIntegerField(choices=[(0, 'Accept'), (1, 'Revoke')], default=0)), + ('datetime_accepted', models.DateTimeField(auto_now_add=True)), + ('member', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='endorsement.member')), + ('shared_drive_record', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='endorsement.shareddriverecord')), + ], + bases=(django_prometheus.models.ExportModelOperationsMixin('shared_drive_acceptance'), models.Model), + ), + ] diff --git a/endorsement/models/shared_drive.py b/endorsement/models/shared_drive.py index 0c3174f5..729f1abe 100644 --- a/endorsement/models/shared_drive.py +++ b/endorsement/models/shared_drive.py @@ -11,8 +11,8 @@ class MemberManager(models.Manager): - def get_member(self, name): - member, _ = self.get_or_create(name=name) + def get_member(self, netid): + member, _ = self.get_or_create(netid=netid) return member @@ -20,10 +20,10 @@ class Member(ExportModelOperationsMixin('member'), models.Model): """ Member represents user associated with a shared drive """ - name = models.CharField(max_length=128) + netid = models.CharField(max_length=128) def json_data(self): - return self.name + return self.netid objects = MemberManager() @@ -52,7 +52,7 @@ class SharedDriveMember( def json_data(self): return { - "name": self.member.json_data(), + "netid": self.member.json_data(), "role": self.role.json_data() } @@ -84,6 +84,9 @@ def json_data(self): "is_subsidized": self.is_subsidized } + def __str__(self): + return json.dumps(self.json_data()) + class SharedDrive(ExportModelOperationsMixin('shared_drive'), models.Model): """ @@ -94,9 +97,12 @@ class SharedDrive(ExportModelOperationsMixin('shared_drive'), models.Model): drive_name = models.CharField(max_length=128) drive_quota = models.ForeignKey(SharedDriveQuota, on_delete=models.PROTECT) drive_usage = models.IntegerField(null=True) - members = models.ManyToManyField(SharedDriveMember) + members = models.ManyToManyField(SharedDriveMember, blank=True) query_date = models.DateTimeField(null=True) + def get_members(self): + return [m.member.netid for m in self.members.all()] + def json_data(self): return { "drive_id": self.drive_id, @@ -108,41 +114,10 @@ def json_data(self): def __str__(self): return json.dumps(self.json_data()) - - -class SharedDriveAcceptance( - ExportModelOperationsMixin('shared_drive_acceptance'), models.Model): - """ - SharedDriveAcceptance model records each instance of a shared drive - record being accepted or revoked by a shared drive manager. - """ - ACCEPT = 0 - REVOKE = 1 - - ACCEPTANCE_ACTION_CHOICES = ( - (ACCEPT, "Accept"), - (REVOKE, "Revoke")) - - member = models.ForeignKey(Member, on_delete=models.PROTECT) - action = models.SmallIntegerField( - default=ACCEPT, choices=ACCEPTANCE_ACTION_CHOICES) - datetime_accepted = models.DateTimeField(auto_now_add=True) - - def json_data(self): - return { - "member": self.member.json_data(), - "action": self.ACCEPTANCE_ACTION_CHOICES[self.action][1], - "datetime_accepted": datetime_to_str(self.datetime_accepted) - } - - def __str__(self): - return json.dumps(self.json_data()) - - class SharedDriveRecordManager(models.Manager): def get_member_drives(self, member_netid, drive_id=None): parms = { - "shared_drive__members__member__name": member_netid, + "shared_drive__members__member__netid": member_netid, "is_deleted__isnull": True} if drive_id: @@ -177,7 +152,6 @@ class SharedDriveRecord( datetime_notice_4_emailed = models.DateTimeField(null=True) datetime_renewed = models.DateTimeField(null=True) datetime_expired = models.DateTimeField(null=True) - acceptance = models.ManyToManyField(SharedDriveAcceptance, blank=True) is_deleted = models.BooleanField(null=True) objects = SharedDriveRecordManager() @@ -204,20 +178,31 @@ def itbill_form_url(self): f"&remote_key={self.subscription.key_remote}" f"&shared_drive={self.shared_drive.drive_name}") + @property + def acceptor(self): + if not self.datetime_accepted: + return None + + return SharedDriveAcceptance.objects.get( + shared_drive_record=self, + datetime_accepted=self.datetime_accepted) + + def get_acceptance(self): + return SharedDriveAcceptance.objects.filter( + shared_drive_record=self) + def set_acceptance(self, member_netid, accept=True): member = Member.objects.get_member(member_netid) action = SharedDriveAcceptance.ACCEPT if ( accept) else SharedDriveAcceptance.REVOKE acceptance = SharedDriveAcceptance.objects.create( - member=member, action=action) - self.acceptance.add(acceptance) + shared_drive_record=self, member=member, action=action) if accept: self.datetime_accepted = acceptance.datetime_accepted else: self.datetime_expired = acceptance.datetime_accepted - self.is_deleted = True self.save() @@ -245,6 +230,7 @@ def json_data(self): "datetime_notice_4_emailed": datetime_to_str( self.datetime_notice_4_emailed), "datetime_accepted": datetime_to_str(self.datetime_accepted), + "acceptance": [a.json_data() for a in self.get_acceptance()], "datetime_renewed": datetime_to_str(self.datetime_renewed), "datetime_expired": datetime_to_str(self.datetime_expired), "datetime_expiration": datetime_to_str(self.expiration_date), @@ -253,3 +239,35 @@ def json_data(self): def __str__(self): return json.dumps(self.json_data()) + + +class SharedDriveAcceptance( + ExportModelOperationsMixin('shared_drive_acceptance'), models.Model): + """ + SharedDriveAcceptance model records each instance of a shared drive + record being accepted or revoked by a shared drive manager. + """ + ACCEPT = 0 + REVOKE = 1 + + ACCEPTANCE_ACTION_CHOICES = ( + (ACCEPT, "Accept"), + (REVOKE, "Revoke")) + + shared_drive_record = models.ForeignKey( + SharedDriveRecord, on_delete=models.PROTECT) + member = models.ForeignKey( + Member, on_delete=models.PROTECT) + action = models.SmallIntegerField( + default=ACCEPT, choices=ACCEPTANCE_ACTION_CHOICES) + datetime_accepted = models.DateTimeField(auto_now_add=True) + + def json_data(self): + return { + "member": self.member.json_data(), + "action": self.ACCEPTANCE_ACTION_CHOICES[self.action][1], + "datetime_accepted": datetime_to_str(self.datetime_accepted) + } + + def __str__(self): + return json.dumps(self.json_data()) diff --git a/endorsement/notifications/access.py b/endorsement/notifications/access.py index 9d8645cb..f7e3fa79 100644 --- a/endorsement/notifications/access.py +++ b/endorsement/notifications/access.py @@ -4,6 +4,7 @@ from endorsement.models import AccessRecord from endorsement.dao.notification import send_notification from endorsement.dao.accessors import get_accessor_email +from endorsement.exceptions import EmailFailureException from django.template import loader, Template, Context import logging diff --git a/endorsement/notifications/endorsement.py b/endorsement/notifications/endorsement.py index 8c3e8c25..020b4cc6 100644 --- a/endorsement/notifications/endorsement.py +++ b/endorsement/notifications/endorsement.py @@ -8,9 +8,10 @@ from endorsement.dao.user import get_endorsee_email_model from endorsement.dao import display_datetime from endorsement.dao.endorse import clear_endorsement -from endorsement.policy import endorsements_to_warn +from endorsement.policy.endorsement import endorsements_to_warn from endorsement.util.email import uw_email_address from endorsement.util.string import listed_list +from endorsement.exceptions import EmailFailureException from django.template import loader, Template, Context from django.utils import timezone import re diff --git a/endorsement/notifications/shared_drive.py b/endorsement/notifications/shared_drive.py new file mode 100644 index 00000000..81ff14a6 --- /dev/null +++ b/endorsement/notifications/shared_drive.py @@ -0,0 +1,71 @@ +# Copyright 2024 UW-IT, University of Washington +# SPDX-License-Identifier: Apache-2.0 + +from endorsement.dao.notification import send_notification +from endorsement.models import SharedDriveRecord +from endorsement.dao.user import get_endorsee_email_model +from endorsement.dao import display_datetime +from endorsement.policy.shared_drive import ( + shared_drives_to_warn, expiration_warning, + DEFAULT_SHARED_DRIVE_LIFETIME) +from endorsement.util.email import uw_email_address +from endorsement.exceptions import EmailFailureException +from django.template import loader, Template, Context +from django.utils import timezone +import re +import logging + + +logger = logging.getLogger(__name__) + + +def _email_template(template_name): + return "email/shared_drive/{}".format(template_name) + + +def _create_notification_expiration_notice(notice_level, lifetime, drive): + context = { + 'drive': drive, + 'acceptor': drive.acceptor, + 'lifetime': lifetime, + 'notice_time': expiration_warning(notice_level) + } + + if notice_level < 4: + subject = ("Action Required: Shared Drive " + "service will expire soon") + text_template = _email_template("notice_warning.txt") + html_template = _email_template("notice_warning.html") + else: + subject = "Action Required: Shared Drive services have expired" + text_template = _email_template("notice_warning_final.txt") + html_template = _email_template("notice_warning_final.html") + + return (subject, + loader.render_to_string(text_template, context), + loader.render_to_string(html_template, context)) + + +def warn_members(notice_level): + drives = shared_drives_to_warn(notice_level) + lifetime = DEFAULT_SHARED_DRIVE_LIFETIME + + for drive in drives: + try: + members = [uw_email_address(netid) for ( + netid) in drive.shared_drive.get_members()] + (subject, + text_body, + html_body) = _create_expire_notice_message( + notice_level, lifetime, drive) + send_notification( + members, subject, text_body, html_body, + "Shared Drive Warning") + + sent_date = { + 'datetime_notice_{}_emailed'.format( + notice_level): timezone.now() + } + drives.update(**sent_date) + except EmailFailureException as ex: + pass diff --git a/endorsement/policy/access.py b/endorsement/policy/access.py new file mode 100644 index 00000000..7cdbdbe7 --- /dev/null +++ b/endorsement/policy/access.py @@ -0,0 +1,125 @@ +# Copyright 2024 UW-IT, University of Washington +# SPDX-License-Identifier: Apache-2.0 + +""" +Office Mailbox Access lifecycle policy + +Basic notions: + * Intial and subsequent warnings are sent prior to expiration. + * The expiration clock starts on the date of the first warning notice. +""" +from django.utils import timezone +from django.db.models import Q +from endorsement.models import AccessRecord +from endorsement.dao.access import revoke_access +from datetime import timedelta + + +# Default lifecycle day counts +DEFAULT_ACCESS_LIFETIME = 365 +DEFAULT_ACCESS_GRACETIME = 90 +PRIOR_DAYS_NOTICE_WARNING_1 = 90 +PRIOR_DAYS_NOTICE_WARNING_2 = 30 +PRIOR_DAYS_NOTICE_WARNING_3 = 7 +PRIOR_DAYS_NOTICE_WARNING_4 = 0 + + +def accessees_to_warn(level): + """ + """ + return _accessees_to_warn(timezone.now(), level) + + +def _accessees_to_warn(now, level): + """ + Gather records to receive expiration warning messages where + level is the index of the warning message: first, second and so forth + + The expiration clock starts on the date of the first warning notice + """ + if level < 1 or level > 4: + raise Exception('bad warning level {}'.format(level)) + + q = Q() + + # select on appropriate time span for warning notice index (level) + days_prior = _expiration_warning(level) + if days_prior is None: + return + + if level == 1: + granted = now - timedelta( + days=DEFAULT_ACCESS_LIFETIME - days_prior) + q = q | Q(datetime_granted__lt=granted, + datetime_notice_1_emailed__isnull=True, + is_deleted__isnull=True) + else: + prev_days_prior = _expiration_warning(level - 1) + prev_warning_date = now - timedelta( + days=prev_days_prior - days_prior) + + if level == 2: + q = q | Q(datetime_notice_1_emailed__lt=prev_warning_date, + datetime_notice_2_emailed__isnull=True, + is_deleted__isnull=True) + elif level == 3: + q = q | Q(datetime_notice_2_emailed__lt=prev_warning_date, + datetime_notice_3_emailed__isnull=True, + is_deleted__isnull=True) + else: + q = q | Q(datetime_notice_3_emailed__lt=prev_warning_date, + datetime_notice_4_emailed__isnull=True, + is_deleted__isnull=True) + + return AccessRecord.objects.filter(q) + + +def access_to_expire(): + """ + Return query set of access records to expire + """ + return _access_to_expire(timezone.now()) + + +def _access_to_expire(now): + """ + Return query set of accees to expire for each service + """ + q = Q() + + expiration_date = now - timedelta(days=DEFAULT_ACCESS_GRACETIME) + q = q | Q(datetime_notice_4_emailed__lt=expiration_date, + datetime_notice_3_emailed__isnull=False, + datetime_notice_2_emailed__isnull=False, + datetime_notice_1_emailed__isnull=False, + is_deleted__isnull=True) + + return AccessRecord.objects.filter(q) + + +def expire_office_access(gracetime, lifetime): + """ + """ + access = access_to_expire(gracetime, lifetime) + if len(access): + for a in acccess: + revoke_access(a) + + +def _expiration_warning(self, level=1): + """ + for the given warning message level, return days prior to + expiration that a warning should be sent. + + level 1 is the first warning, level 2 the second and so on + to final warning at 0 days before expiration + """ + try: + return [ + PRIOR_DAYS_NOTICE_WARNING_1, + PRIOR_DAYS_NOTICE_WARNING_2, + PRIOR_DAYS_NOTICE_WARNING_3, + PRIOR_DAYS_NOTICE_WARNING_4 + ][level - 1] + except IndexError: + return None diff --git a/endorsement/policy.py b/endorsement/policy/endorsement.py similarity index 90% rename from endorsement/policy.py rename to endorsement/policy/endorsement.py index 422ec20e..8190c82c 100644 --- a/endorsement/policy.py +++ b/endorsement/policy/endorsement.py @@ -45,7 +45,7 @@ def _endorsements_to_warn(now, level): if level == 1: endorsed = now - timedelta( days=service.endorsement_lifetime - days_prior) - q = q | Q(datetime_endorsed__lt=endorsed, + q = q | Q(datetime_endorsed__lte=endorsed, datetime_notice_1_emailed__isnull=True, category_code=service.category_code, is_deleted__isnull=True) @@ -55,17 +55,17 @@ def _endorsements_to_warn(now, level): days=prev_days_prior - days_prior) if level == 2: - q = q | Q(datetime_notice_1_emailed__lt=prev_warning_date, + q = q | Q(datetime_notice_1_emailed__lte=prev_warning_date, datetime_notice_2_emailed__isnull=True, category_code=service.category_code, is_deleted__isnull=True) elif level == 3: - q = q | Q(datetime_notice_2_emailed__lt=prev_warning_date, + q = q | Q(datetime_notice_2_emailed__lte=prev_warning_date, datetime_notice_3_emailed__isnull=True, category_code=service.category_code, is_deleted__isnull=True) else: - q = q | Q(datetime_notice_3_emailed__lt=prev_warning_date, + q = q | Q(datetime_notice_3_emailed__lte=prev_warning_date, datetime_notice_4_emailed__isnull=True, category_code=service.category_code, is_deleted__isnull=True) @@ -88,7 +88,7 @@ def _endorsements_to_expire(now): for service in endorsement_services(): expiration_date = now - timedelta(days=service.endorsement_graceperiod) - q = q | Q(datetime_notice_4_emailed__lt=expiration_date, + q = q | Q(datetime_notice_4_emailed__lte=expiration_date, datetime_notice_3_emailed__isnull=False, datetime_notice_2_emailed__isnull=False, datetime_notice_1_emailed__isnull=False, diff --git a/endorsement/policy/shared_drive.py b/endorsement/policy/shared_drive.py new file mode 100644 index 00000000..dc0a8040 --- /dev/null +++ b/endorsement/policy/shared_drive.py @@ -0,0 +1,125 @@ +# Copyright 2024 UW-IT, University of Washington +# SPDX-License-Identifier: Apache-2.0 + +""" +Shared Drive lifecycle policy + +Basic notions: + * Intial and subsequent warnings are sent prior to expiration. + * The expiration clock starts on the date of the first warning notice. +""" +from django.utils import timezone +from django.db.models import Q +from endorsement.models import SharedDriveRecord +from endorsement.dao.shared_drive import shared_drive_lifecycle_expired +from datetime import timedelta + + +# Default lifecycle day counts +DEFAULT_SHARED_DRIVE_LIFETIME = 365 +DEFAULT_SHARED_DRIVE_GRACETIME = 0 +PRIOR_DAYS_NOTICE_WARNING_1 = 90 +PRIOR_DAYS_NOTICE_WARNING_2 = 30 +PRIOR_DAYS_NOTICE_WARNING_3 = 7 +PRIOR_DAYS_NOTICE_WARNING_4 = 0 + + +def shared_drives_to_warn(level): + """ + """ + return _shared_drives_to_warn(timezone.now(), level) + + +def _shared_drives_to_warn(now, level): + """ + Gather records to receive expiration warning messages where + level is the index of the warning message: first, second and so forth + + The expiration clock starts on the date of the first warning notice + """ + if level < 1 or level > 4: + raise Exception('bad warning level {}'.format(level)) + + q = Q() + + # select on appropriate time span for warning notice index (level) + days_prior = expiration_warning(level) + if days_prior is None: + return SharedDriveRecord.objects.none() + + if level == 1: + accepted = now - timedelta( + days=DEFAULT_SHARED_DRIVE_LIFETIME - days_prior) + q = q | Q(datetime_accepted__lte=accepted, + datetime_notice_1_emailed__isnull=True, + is_deleted__isnull=True) + else: + prev_days_prior = expiration_warning(level - 1) + prev_warning_date = now - timedelta( + days=prev_days_prior - days_prior) + + if level == 2: + q = q | Q(datetime_notice_1_emailed__lte=prev_warning_date, + datetime_notice_2_emailed__isnull=True, + is_deleted__isnull=True) + elif level == 3: + q = q | Q(datetime_notice_2_emailed__lte=prev_warning_date, + datetime_notice_3_emailed__isnull=True, + is_deleted__isnull=True) + else: + q = q | Q(datetime_notice_3_emailed__lte=prev_warning_date, + datetime_notice_4_emailed__isnull=True, + is_deleted__isnull=True) + + return SharedDriveRecord.objects.filter(q) + + +def shared_drives_to_expire(): + """ + Return query set of shared drive records to expire + """ + return _shared_drives_to_expire(timezone.now()) + + +def _shared_drives_to_expire(now): + """ + Return query set of shared drives to expire for each service + """ + q = Q() + + expiration_date = now - timedelta(days=DEFAULT_SHARED_DRIVE_GRACETIME) + q = q | Q(datetime_notice_4_emailed__lte=expiration_date, + datetime_notice_3_emailed__isnull=False, + datetime_notice_2_emailed__isnull=False, + datetime_notice_1_emailed__isnull=False, + is_deleted__isnull=True) + + return SharedDriveRecord.objects.filter(q) + + +def expire_shared_drives(gracetime, lifetime): + """ + """ + drives = shared_drives_to_expire(gracetime, lifetime) + if len(drives): + for drive in drives: + shared_drive_lifecycle_expired(drive) + + +def expiration_warning(level=1): + """ + for the given warning message level, return days prior to + expiration that a warning should be sent. + + level 1 is the first warning, level 2 the second and so on + to final warning at 0 days before expiration + """ + try: + return [ + PRIOR_DAYS_NOTICE_WARNING_1, + PRIOR_DAYS_NOTICE_WARNING_2, + PRIOR_DAYS_NOTICE_WARNING_3, + PRIOR_DAYS_NOTICE_WARNING_4 + ][level - 1] + except IndexError: + return None diff --git a/endorsement/templates/email/shared_drive/notice_warning.html b/endorsement/templates/email/shared_drive/notice_warning.html new file mode 100644 index 00000000..3e7ad506 --- /dev/null +++ b/endorsement/templates/email/shared_drive/notice_warning.html @@ -0,0 +1,23 @@ +Hello, +

+You are receiving this email because you are identified as +{%if drive.shared_drive.members.all.count > 0 %}one of {{ drive.shared_drive.members.all.count }}{% else %}the{% endif %} UW manager{{drive.shared_drive.members.all.count|pluralize}} of the Google Shared Drive named +"{{drive.shared_drive.drive_name}}". +

+

+To maintain service, Google Shared Drive use must be actively +acknowledged every {{lifetime}} days. The drive "{{drive.shared_drive.drive_name}}" +was last acknowledged by the netid {{acceptor.member.netid}} on {{acceptor.datetime_accepted|date:"M j, Y"}}, +and is now within {{notice_time}} days of its required renewal. +

+

+To log in to the Provisioning Request Tool (PRT) and renew the expiring shared drive visit: +

+

+Click here to log in to the Provisioning Request Tool (PRT) to renew expiring services. +

+

+If you have any questions, please contact help@uw.edu or 206-221-5000. +

+Thank you,
+UW-IT
diff --git a/endorsement/templates/email/shared_drive/notice_warning.txt b/endorsement/templates/email/shared_drive/notice_warning.txt new file mode 100644 index 00000000..1ceb0930 --- /dev/null +++ b/endorsement/templates/email/shared_drive/notice_warning.txt @@ -0,0 +1,17 @@ +Hello, + +You are receiving this email because you are identified as {%if drive.shared_drive.members.all.count > 0 %}one of {{ drive.shared_drive.members.all.count }}{% else %}the{% endif %} UW manager{{drive.shared_drive.members.all.count|pluralize}} +of the Google Shared Drive named "{{drive.shared_drive.drive_name}}". + +To maintain service, Google Shared Drive use must be actively acknowledged +every {{lifetime}} days. The drive "{{drive.shared_drive.drive_name}}" was last acknowledged by +the netid {{acceptor.member.netid}} on {{acceptor.datetime_accepted|date:"M j, Y"}}, and is now within {{notice_time}} days of its required renewal. + +To log in to the Provisioning Request Tool (PRT) and renew the expiring shared drive visit: + +https://itconnect.uw.edu/connect/productivity-platforms/provisioning-request-tool/#prt + +If you have any questions, please contact help@uw.edu or 206-221-5000. + +Thank you, +UW-IT diff --git a/endorsement/templates/email/shared_drive/notice_warning_final.html b/endorsement/templates/email/shared_drive/notice_warning_final.html new file mode 100644 index 00000000..3cba8403 --- /dev/null +++ b/endorsement/templates/email/shared_drive/notice_warning_final.html @@ -0,0 +1,19 @@ +Hello, +

+You are receiving this email because you are identified as +{%if drive.shared_drive.members.all.count > 0 %}one of {{ drive.shared_drive.members.all.count }}{% else %}the{% endif %} UW manager{{drive.shared_drive.members.all.count|pluralize}} of the Google Shared Drive named +"{{drive.shared_drive.drive_name}}". +

+

+To maintain service, Google Shared Drive use must be actively acknowledged +every {{lifetime}} days. The drive "{{drive.shared_drive.drive_name}}" +has expired and will be marked for deletion soon. +

+

+Click here to log in to the Provisioning Request Tool (PRT) to renew expiring services. +

+

+If you have any questions, please contact help@uw.edu or 206-221-5000. +

+Thank you,
+UW-IT
diff --git a/endorsement/templates/email/shared_drive/notice_warning_final.txt b/endorsement/templates/email/shared_drive/notice_warning_final.txt new file mode 100644 index 00000000..c81eec6f --- /dev/null +++ b/endorsement/templates/email/shared_drive/notice_warning_final.txt @@ -0,0 +1,18 @@ +Hello, + +You are receiving this email because you are identified as {%if drive.shared_drive.members.all.count > 0 %}one of {{ drive.shared_drive.members.all.count }}{% else %}the{% endif %} UW manager{{drive.shared_drive.members.all.count|pluralize}} +of the Google Shared Drive named "{{drive.shared_drive.drive_name}}". + +To maintain service, Google Shared Drive use must be actively acknowledged +every {{lifetime}} days. The drive "{{drive.shared_drive.drive_name}}" +has expired and will be marked for deletion soon. + +To log in to the Provisioning Request Tool (PRT) and renew expiring services visit: + +https://itconnect.uw.edu/connect/productivity-platforms/provisioning-request-tool/#prt + +If you have any questions, please contact help@uw.edu or 206-221-5000. + +Thank you, +UW-IT Help Center +help@uw.edu diff --git a/endorsement/templates/handlebars/tab/drives/google.html b/endorsement/templates/handlebars/tab/drives/google.html index 7b478c10..c22b3d32 100644 --- a/endorsement/templates/handlebars/tab/drives/google.html +++ b/endorsement/templates/handlebars/tab/drives/google.html @@ -76,13 +76,13 @@ diff --git a/endorsement/templates/support/notifications.html b/endorsement/templates/support/notifications.html index edafdee9..953e8f6f 100644 --- a/endorsement/templates/support/notifications.html +++ b/endorsement/templates/support/notifications.html @@ -27,7 +27,8 @@

Lifecycle Notifications

@@ -96,6 +97,41 @@

Provisioned Services

-->
+
+

+ Choose a notification type to see the email contents that will be sent. Notification email is sent as multipart messages containing a text and html representation. +

+ +
+ {% csrf_token %} +
+

Notification Message Type

+
+ +
+
+ +
+ First renewal notice sent to provisioner {{ warning_1 }} days prior to service expiration. +
+
+ Second renewal notice sent to provisioner {{ warning_2 }} days prior to service expiration. +
+
+ Third and final renewal notice sent to provisioner {{ warning_3 }} days before expiration. +
+
+ Notice that provisioned services have expired sent to the provisioner on expiration day. +
+
+ +

Choose an access type to see the the email text sent to the new delegate. Notification email is sent as multipart messages containing a text and html representation. diff --git a/endorsement/test/notifications/__init__.py b/endorsement/test/notifications/__init__.py new file mode 100644 index 00000000..6cf39286 --- /dev/null +++ b/endorsement/test/notifications/__init__.py @@ -0,0 +1,52 @@ +# Copyright 2024 UW-IT, University of Washington +# SPDX-License-Identifier: Apache-2.0 + +from django.test import TestCase +from django.utils import timezone +from datetime import timedelta + + +class NotificationsTestCase(TestCase): + def days_ago(self, days): + return self.now - timedelta(days=days) + + def notice_and_expire_test(self, expirer, warner, offset, expected): + test_date = self.now - timedelta(days=offset[1]) + days = self.lifetime - offset[1] + + models = expirer(test_date) + self.assertEqual( + models.count(), expected[0], + f"test expired at offset level {offset[0]} (day {days})") + models.update(datetime_expired=test_date, is_deleted=True) + + for level in range(1, 5): + models = warner(test_date, level) + self.assertEqual( + models.count(), expected[level], + (f"level {level} test at offset " + f"{offset[0]} days ago (day {days})")) + models.update(**{f"datetime_notice_{level}_emailed": test_date}) + + def message_timing(self, warning_level, results): + offsets = [ + ('one', warning_level(1)), + ('one + 1 day', warning_level(1) - 1), + ('two - 1 day', warning_level(2) + 1), + ('two', warning_level(2)), + ('two + 1 day', warning_level(2) - 1), + ('three - 1 day', warning_level(3) + 1), + ('three', warning_level(3)), + ('three + 1 day', warning_level(3) - 1), + ('four - 1 day', warning_level(4) + 1), + ('four', warning_level(4)), + ('four + 1 day', warning_level(4) - 1), + ('four + 2 days', warning_level(4) - 2), + ('grace - 1 day', -(self.graceperiod - 1)), + ('grace', -(self.graceperiod)), + ('grace + 1 day', -(self.graceperiod + 1))] + + self.assertEqual(len(offsets), len(results)) + + for i, offset in enumerate(offsets): + self.notice_and_expire(offset, results[i]) diff --git a/endorsement/test/notifications/test_drive_expiration_warnings.py b/endorsement/test/notifications/test_drive_expiration_warnings.py new file mode 100644 index 00000000..efe50f48 --- /dev/null +++ b/endorsement/test/notifications/test_drive_expiration_warnings.py @@ -0,0 +1,104 @@ +# Copyright 2024 UW-IT, University of Washington +# SPDX-License-Identifier: Apache-2.0 + +from django.test import TestCase +from django.core import mail +from django.db.models import F +from django.utils import timezone +from endorsement.test.notifications import NotificationsTestCase +from endorsement.models import SharedDriveRecord +from endorsement.policy.shared_drive import ( + _shared_drives_to_warn, _shared_drives_to_expire, expiration_warning, + DEFAULT_SHARED_DRIVE_LIFETIME, DEFAULT_SHARED_DRIVE_GRACETIME) +from endorsement.notifications.shared_drive import warn_members +from datetime import timedelta + + +class TestSharedDriveExpirationNotices(NotificationsTestCase): + fixtures = [ + 'test_data/member.json', + 'test_data/role.json', + 'test_data/itbill_subscription.json', + 'test_data/itbill_provision.json', + 'test_data/itbill_quantity.json', + 'test_data/shared_drive_member.json', + 'test_data/shared_drive_quota.json', + 'test_data/shared_drive.json', + 'test_data/shared_drive_record.json' + ] + + def setUp(self): + self.now = timezone.now() + self.lifetime = DEFAULT_SHARED_DRIVE_LIFETIME + self.graceperiod = DEFAULT_SHARED_DRIVE_GRACETIME + + # reset all mock dates + SharedDriveRecord.objects.all().update(datetime_accepted=self.now) + + # accepted date long ago + drive = SharedDriveRecord.objects.get(pk=1) + drive.datetime_accepted = self.days_ago(self.lifetime + 200) + drive.save() + + # expire date today + drive = SharedDriveRecord.objects.get(pk=2) + drive.datetime_accepted = self.days_ago(self.lifetime) + drive.save() + + # expire date tomorrow + drive = SharedDriveRecord.objects.get(pk=6) + drive.datetime_accepted = self.days_ago(self.lifetime - 1) + drive.save() + + def notice_and_expire(self, offset_days, expected_results): + self.notice_and_expire_test( + _shared_drives_to_expire, _shared_drives_to_warn, + offset_days, expected_results) + + def test_expiration_and_notices(self): + expected_results = [ + [0, 2, 0, 0, 0], # level one + [0, 1, 0, 0, 0], # one plus a day + [0, 0, 0, 0, 0], # two minus a day + [0, 0, 2, 0, 0], # level two + [0, 0, 1, 0, 0], # two plus a day + [0, 0, 0, 0, 0], # three minus a day + [0, 0, 0, 2, 0], # level three + [0, 0, 0, 1, 0], # three plus a day + [0, 0, 0, 0, 0], # four minus a day + [0, 0, 0, 0, 2], # level four + [2, 0, 0, 0, 1], # four plus a day + [1, 0, 0, 0, 0], # four plus two days + [0, 0, 0, 0, 0], # grace minus a day + [0, 0, 0, 0, 0], # grace + [0, 0, 0, 0, 0]] # grace plus a day + + self.message_timing(expiration_warning, expected_results) + + def test_expiration_and_notice_email(self): + warn_members(1) + self.assertEqual(len(mail.outbox), 3) + + SharedDriveRecord.objects.filter( + datetime_notice_1_emailed__isnull=False).update( + datetime_notice_1_emailed=F( + 'datetime_notice_1_emailed')-timedelta(days=61)) + + warn_members(2) + self.assertEqual(len(mail.outbox), 6) + + SharedDriveRecord.objects.filter( + datetime_notice_2_emailed__isnull=False).update( + datetime_notice_2_emailed=F( + 'datetime_notice_2_emailed')-timedelta(days=30)) + + warn_members(3) + self.assertEqual(len(mail.outbox), 9) + + SharedDriveRecord.objects.filter( + datetime_notice_3_emailed__isnull=False).update( + datetime_notice_3_emailed=F( + 'datetime_notice_2_emailed')-timedelta(days=23)) + + warn_members(4) + self.assertEqual(len(mail.outbox), 12) diff --git a/endorsement/test/notifications/test_expiration_warnings.py b/endorsement/test/notifications/test_expiration_warnings.py new file mode 100644 index 00000000..5c9cc215 --- /dev/null +++ b/endorsement/test/notifications/test_expiration_warnings.py @@ -0,0 +1,120 @@ +# Copyright 2024 UW-IT, University of Washington +# SPDX-License-Identifier: Apache-2.0 + +from django.test import TestCase +from django.core import mail +from django.db.models import F +from django.utils import timezone +from endorsement.test.notifications import NotificationsTestCase +from endorsement.models import Endorser, Endorsee, EndorsementRecord +from endorsement.policy.endorsement import ( + _endorsements_to_warn, _endorsements_to_expire) +from endorsement.services import get_endorsement_service +from endorsement.notifications.endorsement import warn_endorsers +from datetime import timedelta + + +class TestProvisioneExpirationNotices(NotificationsTestCase): + def setUp(self): + self.now = timezone.now() + + self.endorser1 = Endorser.objects.create( + netid='endorser1', regid='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + display_name='Not Valid', is_valid=True) + self.endorser2 = Endorser.objects.create( + netid='endorser2', regid='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + display_name='Not Valid', is_valid=True) + self.endorsee1 = Endorsee.objects.create( + netid='endorsee1', regid='cccccccccccccccccccccccccccccccc', + display_name='Endorsee Six', is_person=True) + self.endorsee2 = Endorsee.objects.create( + netid='endorsee2', regid='dddddddddddddddddddddddddddddddd', + display_name='Endorsee Seven', is_person=True) + + # common lifecycle dates, two services to test combined email + o365 = get_endorsement_service('o365') + google = get_endorsement_service('google') + + self.assertEqual(o365.endorsement_lifetime, + google.endorsement_lifetime) + + self.lifetime = o365.endorsement_lifetime + self.graceperiod = o365.endorsement_graceperiod + + # expire date long ago + EndorsementRecord.objects.create( + endorser=self.endorser1, endorsee=self.endorsee1, + category_code=o365.category_code, + reason="Just Because", + datetime_endorsed=self.days_ago(self.lifetime + 200)) + + + # expire date today + EndorsementRecord.objects.create( + endorser=self.endorser1, endorsee=self.endorsee1, + category_code=google.category_code, reason="Just Because", + datetime_endorsed=self.days_ago(self.lifetime)) + + # expire date tomorrow + EndorsementRecord.objects.create( + endorser=self.endorser2, endorsee=self.endorsee2, + category_code=o365.category_code, + reason="I said so", + datetime_endorsed=self.days_ago(self.lifetime - 1)) + + def notice_and_expire(self, offset_days, expected_results): + self.notice_and_expire_test( + _endorsements_to_expire, _endorsements_to_warn, + offset_days, expected_results) + + def test_expiration_and_notices(self): + service = get_endorsement_service('o365') + + # use first service to get lifecycle dates + expected_results = [ + [0, 2, 0, 0, 0], # level one + [0, 1, 0, 0, 0], # one plus a day + [0, 0, 0, 0, 0], # two minus a day + [0, 0, 2, 0, 0], # level two + [0, 0, 1, 0, 0], # two plus a day + [0, 0, 0, 0, 0], # three minus a day + [0, 0, 0, 2, 0], # level three + [0, 0, 0, 1, 0], # three plus a day + [0, 0, 0, 0, 0], # four minus a day + [0, 0, 0, 0, 2], # level four + [0, 0, 0, 0, 1], # four plus a day + [0, 0, 0, 0, 0], # four plus two days + [0, 0, 0, 0, 0], # grace minus a day + [2, 0, 0, 0, 0], # grace + [1, 0, 0, 0, 0]] # grace plus a day + + self.message_timing( + service.endorsement_expiration_warning, expected_results) + + def test_expiration_and_notice_email(self): + warn_endorsers(1) + self.assertEqual(len(mail.outbox), 2) + + EndorsementRecord.objects.filter( + datetime_notice_1_emailed__isnull=False).update( + datetime_notice_1_emailed=F( + 'datetime_notice_1_emailed')-timedelta(days=61)) + + warn_endorsers(2) + self.assertEqual(len(mail.outbox), 4) + + EndorsementRecord.objects.filter( + datetime_notice_2_emailed__isnull=False).update( + datetime_notice_2_emailed=F( + 'datetime_notice_2_emailed')-timedelta(days=30)) + + warn_endorsers(3) + self.assertEqual(len(mail.outbox), 6) + + EndorsementRecord.objects.filter( + datetime_notice_3_emailed__isnull=False).update( + datetime_notice_3_emailed=F( + 'datetime_notice_2_emailed')-timedelta(days=23)) + + warn_endorsers(4) + self.assertEqual(len(mail.outbox), 8) diff --git a/endorsement/test/test_expiration_warnings.py b/endorsement/test/test_expiration_warnings.py deleted file mode 100644 index eb760ff7..00000000 --- a/endorsement/test/test_expiration_warnings.py +++ /dev/null @@ -1,166 +0,0 @@ -# Copyright 2024 UW-IT, University of Washington -# SPDX-License-Identifier: Apache-2.0 - -from django.test import TestCase -from django.utils import timezone -from django.core import mail -from django.db.models import F -from endorsement.models import Endorser, Endorsee, EndorsementRecord -from endorsement.policy import (_endorsements_to_warn, _endorsements_to_expire) -from endorsement.services import get_endorsement_service -from endorsement.notifications.endorsement import warn_endorsers -from datetime import timedelta - - -class TestProvisioneExpirationNotices(TestCase): - def setUp(self): - now = timezone.now() - timedelta(hours=1) - self.endorser1 = Endorser.objects.create( - netid='endorser1', regid='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - display_name='Not Valid', is_valid=True) - self.endorser2 = Endorser.objects.create( - netid='endorser2', regid='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', - display_name='Not Valid', is_valid=True) - self.endorsee1 = Endorsee.objects.create( - netid='endorsee1', regid='cccccccccccccccccccccccccccccccc', - display_name='Endorsee Six', is_person=True) - self.endorsee2 = Endorsee.objects.create( - netid='endorsee2', regid='dddddddddddddddddddddddddddddddd', - display_name='Endorsee Seven', is_person=True) - - # common lifecycle dates, two services to test combined email - o365 = get_endorsement_service('o365') - google = get_endorsement_service('google') - - self.assertEqual(o365.endorsement_lifetime, - google.endorsement_lifetime) - - # expire date long ago - EndorsementRecord.objects.create( - endorser=self.endorser1, endorsee=self.endorsee1, - category_code=o365.category_code, - reason="Just Because", - datetime_endorsed=now - timedelta(days=( - o365.endorsement_lifetime + 200))) - # expire date today - EndorsementRecord.objects.create( - endorser=self.endorser1, endorsee=self.endorsee1, - category_code=google.category_code, reason="Just Because", - datetime_endorsed=now - timedelta(days=( - google.endorsement_lifetime))) - # expire date tomorrow - EndorsementRecord.objects.create( - endorser=self.endorser2, endorsee=self.endorsee2, - category_code=o365.category_code, - reason="I said so", - datetime_endorsed=now - timedelta(days=( - o365.endorsement_lifetime - 1))) - - def _notice_and_expire(self, now, expected): - endorsements = _endorsements_to_expire(now) - self.assertEqual(len(endorsements), expected[0]) - - endorsements = _endorsements_to_warn(now, 1) - self.assertEqual(len(endorsements), expected[1]) - endorsements.update(datetime_notice_1_emailed=now) - - endorsements = _endorsements_to_warn(now, 2) - self.assertEqual(len(endorsements), expected[2]) - endorsements.update(datetime_notice_2_emailed=now) - - endorsements = _endorsements_to_warn(now, 3) - self.assertEqual(len(endorsements), expected[3]) - endorsements.update(datetime_notice_3_emailed=now) - - endorsements = _endorsements_to_warn(now, 4) - self.assertEqual(len(endorsements), expected[4]) - endorsements.update(datetime_notice_4_emailed=now) - - def test_expiration_and_notices(self): - # use first service to get lifecycle dates - service = get_endorsement_service('o365') - - # notice one days prior to expiration: - # two first notices, no expirations - now = timezone.now() - timedelta( - days=service.endorsement_expiration_warning(1)) - self._notice_and_expire(now, [0, 2, 0, 0, 0]) - - # next day: one first notice, no expirations - self._notice_and_expire(now + timedelta(days=1), [0, 1, 0, 0, 0]) - - # 30 days later: no notices, no expiration - self._notice_and_expire(now + timedelta(days=30), [0, 0, 0, 0, 0]) - - # notice two days prior: two second notices, no expiration - now += timedelta( - days=(service.endorsement_expiration_warning(1) - - service.endorsement_expiration_warning(2) + 1)) - self._notice_and_expire(now, [0, 0, 2, 0, 0]) - - # next day: one second notice, no expiration - self._notice_and_expire(now + timedelta(days=1), [0, 0, 1, 0, 0]) - - # next day: no notice, no expiration - self._notice_and_expire(now + timedelta(days=2), [0, 0, 0, 0, 0]) - - # notice three days prior: two third notices, no expiration - now += timedelta( - days=(service.endorsement_expiration_warning(2) - - service.endorsement_expiration_warning(3) + 1)) - self._notice_and_expire(now, [0, 0, 0, 2, 0]) - - # next day: one third notice, no expiration - self._notice_and_expire(now + timedelta(days=1), [0, 0, 0, 1, 0]) - - # next day: no notices, no expiration - self._notice_and_expire(now + timedelta(days=2), [0, 0, 0, 0, 0]) - - # expiration day: two fourth notices, no expiration - now += timedelta( - days=(service.endorsement_expiration_warning(3) - - service.endorsement_expiration_warning(4) + 1)) - self._notice_and_expire(now, [0, 0, 0, 0, 2]) - - # next day: one fourth notices, no expiration - self._notice_and_expire(now + timedelta(days=1), [0, 0, 0, 0, 1]) - - # 89 days forward: no notices, no expiration - now += timedelta(days=service.endorsement_graceperiod) - self._notice_and_expire(now, [0, 0, 0, 0, 0]) - - # next day forward: no notices, two expirations - now += timedelta(days=1) - self._notice_and_expire(now, [2, 0, 0, 0, 0]) - - # next day forward: no notices, three expirations - now += timedelta(days=1) - self._notice_and_expire(now, [3, 0, 0, 0, 0]) - - def test_expiration_and_notice_email(self): - warn_endorsers(1) - self.assertEqual(len(mail.outbox), 2) - - EndorsementRecord.objects.filter( - datetime_notice_1_emailed__isnull=False).update( - datetime_notice_1_emailed=F( - 'datetime_notice_1_emailed')-timedelta(days=61)) - - warn_endorsers(2) - self.assertEqual(len(mail.outbox), 4) - - EndorsementRecord.objects.filter( - datetime_notice_2_emailed__isnull=False).update( - datetime_notice_2_emailed=F( - 'datetime_notice_2_emailed')-timedelta(days=30)) - - warn_endorsers(3) - self.assertEqual(len(mail.outbox), 6) - - EndorsementRecord.objects.filter( - datetime_notice_3_emailed__isnull=False).update( - datetime_notice_3_emailed=F( - 'datetime_notice_2_emailed')-timedelta(days=23)) - - warn_endorsers(4) - self.assertEqual(len(mail.outbox), 8) diff --git a/endorsement/views/api/notification.py b/endorsement/views/api/notification.py index 7b847db4..7313769d 100644 --- a/endorsement/views/api/notification.py +++ b/endorsement/views/api/notification.py @@ -5,7 +5,9 @@ from endorsement.views.rest_dispatch import RESTDispatch from endorsement.models import ( Endorser, Endorsee, EndorsementRecord, - Accessor, Accessee, AccessRight, AccessRecord) + Accessor, Accessee, AccessRight, AccessRecord, + Member, Role, SharedDriveMember, SharedDrive, + SharedDriveQuota, SharedDriveRecord, SharedDriveRecord) from endorsement.services import endorsement_services, get_endorsement_service from endorsement.util.auth import SupportGroupAuthentication from endorsement.notifications.endorsement import ( @@ -15,6 +17,8 @@ _create_warn_shared_owner_message) from endorsement.notifications.access import ( _create_accessor_message) +from endorsement.notifications.shared_drive import ( + _create_notification_expiration_notice) from endorsement.dao.accessors import get_accessor_email from datetime import datetime, timedelta import re @@ -39,6 +43,8 @@ def post(self, request, *args, **kwargs): return self._service_notification(request) elif notice_type == 'access': return self._access_notification(request) + elif notice_type == 'shared_drive': + return self._shared_drive_notification(request) return self.error_response(400, "Incomplete or unknown notification.") @@ -162,6 +168,29 @@ def _access_notification(self, request): 'html': html_body }) + def _shared_drive_notification(self, request): + notification = request.data.get('notification', None) + + warning_level = None + m = re.match(r'^warning_([1-4])$', notification) + if m: + warning_level = int(m.group(1)) + + record = SharedDriveRecord.objects.filter( + is_deleted__isnull=True).first() + + if warning_level: + subject, text, html = _create_notification_expiration_notice( + warning_level, 365, record) + else: + return self.error_response(400, "Unknown notification.") + + return self.json_response({ + 'subject': subject, + 'text': text, + 'html': html + }) + # mimic function in dao.notification def _get_unendorsed_unnotified(unendorsed):