diff --git a/coldfront/config/email.py b/coldfront/config/email.py index 1a39c28d3..88546157a 100644 --- a/coldfront/config/email.py +++ b/coldfront/config/email.py @@ -21,3 +21,4 @@ EMAIL_OPT_OUT_INSTRUCTION_URL = ENV.str('EMAIL_OPT_OUT_INSTRUCTION_URL', default='') EMAIL_ALLOCATION_EXPIRING_NOTIFICATION_DAYS = ENV.list('EMAIL_ALLOCATION_EXPIRING_NOTIFICATION_DAYS', cast=int, default=[7, 14, 30]) EMAIL_SIGNATURE = ENV.str('EMAIL_SIGNATURE', default='', multiline=True) +EMAIL_ADMINS_ON_ALLOCATION_EXPIRE = ENV.bool('EMAIL_ADMINS_ON_ALLOCATION_EXPIRE', default=False) diff --git a/coldfront/core/allocation/management/commands/add_allocation_defaults.py b/coldfront/core/allocation/management/commands/add_allocation_defaults.py index 7d6f0713d..c28bc808e 100644 --- a/coldfront/core/allocation/management/commands/add_allocation_defaults.py +++ b/coldfront/core/allocation/management/commands/add_allocation_defaults.py @@ -12,7 +12,7 @@ class Command(BaseCommand): def handle(self, *args, **options): - for attribute_type in ('Date', 'Float', 'Int', 'Text', 'Yes/No', + for attribute_type in ('Date', 'Float', 'Int', 'Text', 'Yes/No', 'No', 'Attribute Expanded Text'): AttributeType.objects.get_or_create(name=attribute_type) diff --git a/coldfront/core/allocation/tasks.py b/coldfront/core/allocation/tasks.py index d56f807e7..e1a37fc05 100644 --- a/coldfront/core/allocation/tasks.py +++ b/coldfront/core/allocation/tasks.py @@ -2,12 +2,10 @@ # import the logging library import logging -from django.conf import settings -from django.core.exceptions import ImproperlyConfigured - -from coldfront.core.allocation.models import (Allocation, AllocationAttribute, +from coldfront.core.allocation.models import (Allocation, AllocationStatusChoice) -from coldfront.core.utils.common import get_domain_url, import_from_settings +from coldfront.core.user.models import User +from coldfront.core.utils.common import import_from_settings from coldfront.core.utils.mail import send_email_template # Get an instance of a logger @@ -25,6 +23,8 @@ EMAIL_ALLOCATION_EXPIRING_NOTIFICATION_DAYS = import_from_settings( 'EMAIL_ALLOCATION_EXPIRING_NOTIFICATION_DAYS', [7, ]) +EMAIL_ADMINS_ON_ALLOCATION_EXPIRE = import_from_settings('EMAIL_ADMINS_ON_ALLOCATION_EXPIRE') +EMAIL_ADMIN_LIST = import_from_settings('EMAIL_ADMIN_LIST') def update_statuses(): @@ -41,148 +41,169 @@ def update_statuses(): def send_expiry_emails(): - # Allocations expiring - - for days_remaining in sorted(set(EMAIL_ALLOCATION_EXPIRING_NOTIFICATION_DAYS)): - expring_in_days = datetime.datetime.today( - ) + datetime.timedelta(days=days_remaining) - - for allocation_obj in Allocation.objects.filter(status__name='Active', end_date=expring_in_days): - - expire_notification = allocation_obj.allocationattribute_set.filter( - allocation_attribute_type__name='EXPIRE NOTIFICATION').first() - if expire_notification and expire_notification.value == 'No': - continue - - cloud_usage_notification = allocation_obj.allocationattribute_set.filter( - allocation_attribute_type__name='CLOUD_USAGE_NOTIFICATION').first() - if cloud_usage_notification and cloud_usage_notification.value == 'No': - continue - - - allocation_renew_url = '{}/{}/{}/{}'.format( - CENTER_BASE_URL.strip('/'), 'allocation', allocation_obj.pk, 'renew') - - resource_name = allocation_obj.get_parent_resource.name - - template_context = { - 'center_name': CENTER_NAME, - 'allocation_type': resource_name, - 'expring_in_days': days_remaining, - 'allocation_renew_url': allocation_renew_url, - 'project_renewal_help_url': CENTER_PROJECT_RENEWAL_HELP_URL, - 'opt_out_instruction_url': EMAIL_OPT_OUT_INSTRUCTION_URL, - 'signature': EMAIL_SIGNATURE - - } - - email_receiver_list = [] - for allocation_user in allocation_obj.project.projectuser_set.all(): - if (allocation_user.enable_notifications and - allocation_obj.allocationuser_set.filter( - user=allocation_user.user, status__name='Active') - and allocation_user.user.email not in email_receiver_list): - - email_receiver_list.append(allocation_user.user.email) - - send_email_template('Allocation to {} expiring in {} days'.format(resource_name, days_remaining), - 'email/allocation_expiring.txt', - template_context, - EMAIL_SENDER, - email_receiver_list - ) - - logger.info('Allocation to {} expiring in {} days email sent to PI {}.'.format( - resource_name, days_remaining, allocation_obj.project.pi.username)) - - # Allocations expiring today - today = datetime.datetime.now().strftime('%Y-%m-%d') + #Allocations expiring soon + for user in User.objects.all(): + projectdict = {} + expirationdict = {} + email_receiver_list = [] + for days_remaining in sorted(set(EMAIL_ALLOCATION_EXPIRING_NOTIFICATION_DAYS)): + + expring_in_days = (datetime.datetime.today( + ) + datetime.timedelta(days=days_remaining)).date() + + for allocationuser in user.allocationuser_set.all(): + allocation = allocationuser.allocation + + if (((allocation.status.name in ['Active', 'Payment Pending', 'Payment Requested', 'Unpaid']) and (allocation.end_date == expring_in_days))): + + project_url = f'{CENTER_BASE_URL.strip("/")}/{"project"}/{allocation.project.pk}/' + + if (allocation.status.name in ['Payment Pending', 'Payment Requested', 'Unpaid']): + allocation_renew_url = f'{CENTER_BASE_URL.strip("/")}/{"allocation"}/{allocation.pk}/' + else: + allocation_renew_url = f'{CENTER_BASE_URL.strip("/")}/{"allocation"}/{allocation.pk}/{"renew"}/' + + resource_name = allocation.get_parent_resource.name + + template_context = { + 'center_name': CENTER_NAME, + 'expring_in_days': days_remaining, + 'project_dict': projectdict, + 'expiration_dict': expirationdict, + 'expiration_days': sorted(set(EMAIL_ALLOCATION_EXPIRING_NOTIFICATION_DAYS)), + 'project_renewal_help_url': CENTER_PROJECT_RENEWAL_HELP_URL, + 'opt_out_instruction_url': EMAIL_OPT_OUT_INSTRUCTION_URL, + 'signature': EMAIL_SIGNATURE + } + + expire_notification = allocation.allocationattribute_set.filter( + allocation_attribute_type__name='EXPIRE NOTIFICATION').first() + if expire_notification and expire_notification.value == 'No': + continue + + cloud_usage_notification = allocation.allocationattribute_set.filter( + allocation_attribute_type__name='CLOUD_USAGE_NOTIFICATION').first() + if cloud_usage_notification and cloud_usage_notification.value == 'No': + continue + + for projectuser in allocation.project.projectuser_set.filter(user=user, status__name='Active'): + if ((projectuser.enable_notifications) and + (allocationuser.user == user and allocationuser.status.name == 'Active')): + + if (user.email not in email_receiver_list): + email_receiver_list.append(user.email) + + if days_remaining not in expirationdict: + expirationdict[days_remaining] = [] + expirationdict[days_remaining].append((project_url, allocation_renew_url, resource_name)) + else: + expirationdict[days_remaining].append((project_url, allocation_renew_url, resource_name)) + + if allocation.project.title not in projectdict: + projectdict[allocation.project.title] = (project_url, allocation.project.pi.username,) + + if email_receiver_list: + + send_email_template(f'Your access to {CENTER_NAME}\'s resources is expiring soon', + 'email/allocation_expiring.txt', + template_context, + EMAIL_SENDER, + email_receiver_list + ) + + logger.debug(f'Allocation(s) expiring in soon, email sent to user {user}.') + + #Allocations expired + admin_projectdict = {} + admin_allocationdict = {} + for user in User.objects.all(): + projectdict = {} + allocationdict = {} + email_receiver_list = [] + + expring_in_days = (datetime.datetime.today() + datetime.timedelta(days=-1)).date() + + for allocationuser in user.allocationuser_set.all(): + allocation = allocationuser.allocation - for allocation_attribute in AllocationAttribute.objects.filter( - value=today, - allocation_attribute_type__name='send_expiry_email_on_date'): + if (allocation.end_date == expring_in_days): + + project_url = f'{CENTER_BASE_URL.strip("/")}/{"project"}/{allocation.project.pk}/' - allocation_obj = allocation_attribute.allocation - days_remaining = allocation_obj.expires_in + allocation_renew_url = f'{CENTER_BASE_URL.strip("/")}/{"allocation"}/{allocation.pk}/{"renew"}/' - allocation_renew_url = '{}/{}/{}/{}'.format( - CENTER_BASE_URL.strip('/'), 'allocation', allocation_obj.pk, 'renew') + allocation_url = f'{CENTER_BASE_URL.strip("/")}/{"allocation"}/{allocation.pk}/' - resource_name = allocation_obj.get_parent_resource.name + resource_name = allocation.get_parent_resource.name - template_context = { - 'center_name': CENTER_NAME, - 'allocation_type': resource_name, - 'expring_in_days': days_remaining, - 'allocation_renew_url': allocation_renew_url, - 'project_renewal_help_url': CENTER_PROJECT_RENEWAL_HELP_URL, - 'opt_out_instruction_url': EMAIL_OPT_OUT_INSTRUCTION_URL, - 'signature': EMAIL_SIGNATURE + template_context = { + 'center_name': CENTER_NAME, + 'project_dict': projectdict, + 'allocation_dict': allocationdict, + 'project_renewal_help_url': CENTER_PROJECT_RENEWAL_HELP_URL, + 'opt_out_instruction_url': EMAIL_OPT_OUT_INSTRUCTION_URL, + 'signature': EMAIL_SIGNATURE + } - } + expire_notification = allocation.allocationattribute_set.filter( + allocation_attribute_type__name='EXPIRE NOTIFICATION').first() - email_receiver_list = [] - for allocation_user in allocation_obj.project.projectuser_set.all(): - if (allocation_user.enable_notifications and - allocation_obj.allocationuser_set.filter( - user=allocation_user.user, status__name='Active') - and allocation_user.user.email not in email_receiver_list): + for projectuser in allocation.project.projectuser_set.filter(user=user, status__name='Active'): + if ((projectuser.enable_notifications) and + (allocationuser.user == user and allocationuser.status.name == 'Active')): - email_receiver_list.append(allocation_user.user.email) + if expire_notification and expire_notification.value == 'Yes': - send_email_template('Allocation to {} expiring in {} days'.format(resource_name, days_remaining), - 'email/allocation_expiring.txt', - template_context, - EMAIL_SENDER, - email_receiver_list - ) + if (user.email not in email_receiver_list): + email_receiver_list.append(user.email) - logger.info('Allocation to {} expiring in {} days email sent to PI {}.'.format( - resource_name, days_remaining, allocation_obj.project.pi.username)) + if project_url not in allocationdict: + allocationdict[project_url] = [] + allocationdict[project_url].append({allocation_renew_url : resource_name}) + else: + if {allocation_renew_url : resource_name} not in allocationdict[project_url]: + allocationdict[project_url].append({allocation_renew_url : resource_name}) - # Expired allocations + if allocation.project.title not in projectdict: + projectdict[allocation.project.title] = (project_url, allocation.project.pi.username) - expring_in_days = datetime.datetime.today() + datetime.timedelta(days=-1) + if EMAIL_ADMINS_ON_ALLOCATION_EXPIRE: + + if project_url not in admin_allocationdict: + admin_allocationdict[project_url] = [] + admin_allocationdict[project_url].append({allocation_url : resource_name}) + else: + if {allocation_url : resource_name} not in admin_allocationdict[project_url]: + admin_allocationdict[project_url].append({allocation_url : resource_name}) - for allocation_obj in Allocation.objects.filter(end_date=expring_in_days): + if allocation.project.title not in admin_projectdict: + admin_projectdict[allocation.project.title] = (project_url, allocation.project.pi.username) - expire_notification = allocation_obj.allocationattribute_set.filter( - allocation_attribute_type__name='EXPIRE NOTIFICATION').first() - if expire_notification and expire_notification.value == 'No': - continue + + if email_receiver_list: - resource_name = allocation_obj.get_parent_resource.name + send_email_template('Your access to resource(s) have expired', + 'email/allocation_expired.txt', + template_context, + EMAIL_SENDER, + email_receiver_list + ) - allocation_renew_url = '{}/{}/{}/{}'.format( - CENTER_BASE_URL.strip('/'), 'allocation', allocation_obj.pk, 'renew') + logger.debug(f'Allocation(s) expired email sent to user {user}.') - project_url = '{}/{}/{}/'.format(CENTER_BASE_URL.strip('/'), - 'project', allocation_obj.project.pk) + if EMAIL_ADMINS_ON_ALLOCATION_EXPIRE: - template_context = { - 'center_name': CENTER_NAME, - 'allocation_type': resource_name, - 'project_renewal_help_url': CENTER_PROJECT_RENEWAL_HELP_URL, - 'project_url': project_url, - 'opt_out_instruction_url': EMAIL_OPT_OUT_INSTRUCTION_URL, - 'signature': EMAIL_SIGNATURE - } + if admin_projectdict: - email_receiver_list = [] - for allocation_user in allocation_obj.project.projectuser_set.all(): - if (allocation_user.enable_notifications and - allocation_obj.allocationuser_set.filter( - user=allocation_user.user, status__name='Active') - and allocation_user.user.email not in email_receiver_list): - - email_receiver_list.append(allocation_user.user.email) - - send_email_template('Allocation to {} has expired'.format(resource_name), - 'email/allocation_expired.txt', - template_context, - EMAIL_SENDER, - email_receiver_list - ) - - logger.info('Allocation to {} expired email sent to PI {}.'.format( - resource_name, allocation_obj.project.pi.username)) + admin_template_context = { + 'project_dict': admin_projectdict, + 'allocation_dict': admin_allocationdict, + 'signature': EMAIL_SIGNATURE + } + + send_email_template('Allocation(s) have expired', + 'email/admin_allocation_expired.txt', + admin_template_context, + EMAIL_SENDER, + [EMAIL_ADMIN_LIST,] + ) \ No newline at end of file diff --git a/coldfront/templates/email/admin_allocation_expired.txt b/coldfront/templates/email/admin_allocation_expired.txt new file mode 100644 index 000000000..d80e1361b --- /dev/null +++ b/coldfront/templates/email/admin_allocation_expired.txt @@ -0,0 +1,12 @@ +Dear System Administrator, + +Allocations to your system resources have expired. You should confirm access to these resources has been removed for all users on the allocations. +Below is a list of the allocations that expired today: + + Expired Allocation(s):{% for project_key, project_url in project_dict.items %} + {% spaceless %} {% for allocation_key, allocation_value in allocation_dict.items %}{% if allocation_key == project_url.0 %}{% for allocation in allocation_value %}{% for allocation_url, resource_name in allocation.items %} + {{ resource_name }} : {{ allocation_url }}{% endfor %}{% endfor %}{% endif %}{% endfor %}{% endspaceless %} +{% endfor %} + +Thank you, +{{ signature }} \ No newline at end of file diff --git a/coldfront/templates/email/allocation_expired.txt b/coldfront/templates/email/allocation_expired.txt index a59f1bdde..6ed63116e 100644 --- a/coldfront/templates/email/allocation_expired.txt +++ b/coldfront/templates/email/allocation_expired.txt @@ -1,9 +1,16 @@ Dear {{center_name}} user, -Your allocation for access to {{ allocation_type }} has expired. Any accounts under this allocation are now unable to -access this resource. If you wish to continue using it, the group owner responsible for this project must create a new -allocation request. {{project_url}} +Your access to {{center_name}} resources has expired. Any accounts under these allocation(s) are now unable to +access the associated resource. If you wish to continue using them, the group owners responsible for the following project(s) must create a new +allocation request(s). Below is a list of links to your project(s) containing at least one expired allocation: +{% for project_key, project_url in project_dict.items %} +Project Title: {{project_key}} +Project URL: {{ project_url.0 }} +Project PI: {{ project_url.1 }} + {% spaceless %} {% for allocation_key, allocation_value in allocation_dict.items %}{% if allocation_key == project_url.0 %}Expired Allocation(s):{% for allocation in allocation_value %}{% for allocation_url, resource_name in allocation.items %} + {{ resource_name }}{% endfor %}{% endfor %}{% endif %}{% endfor %}{% endspaceless %} +{% endfor %} The group owner responsible for this project is required to renew it on a yearly basis. If you have not done so in the last year, you will be required to before requesting a new allocation. For more information about the yearly renewal, visit our knowledge base: {{ project_renewal_help_url }}. diff --git a/coldfront/templates/email/allocation_expiring.txt b/coldfront/templates/email/allocation_expiring.txt index 9c52da64f..57d3f5ee3 100644 --- a/coldfront/templates/email/allocation_expiring.txt +++ b/coldfront/templates/email/allocation_expiring.txt @@ -1,13 +1,21 @@ Dear {{center_name}} user, -Your allocation for access to {{ allocation_type }} is expiring in {{ expring_in_days }} days. -Failure to renew the allocation before expiration will result in removal of access to this resource for all accounts -under this project. -To renew, login to ColdFront and complete the short process: {{ allocation_renew_url }}. +Your access to {{center_name}}'s resources is expiring soon. Failure to renew the expiring allocation(s) on time will terminate access to the resource +for all users on the allocation. To renew, login to ColdFront and complete the short renewal process for each expiring allocation. Below is a list of links to +your project(s) containing at least one expiring allocation: +{% for project_key, project_url in project_dict.items %} +Project Title: {{project_key}} +Project URL: {{ project_url.0 }} +Project PI: {{ project_url.1 }} + {% spaceless %}{% for days_key, days_value in expiration_dict.items %} + Allocation(s) expiring in {{days_key}} days:{% for allocations in days_value %}{% if allocations.0 == project_url.0 %} + {% spaceless %}{{ allocations.2 }}{% endspaceless %}{% endif %}{% endfor %} + {% endfor %}{% endspaceless %} +{% endfor %} The group owner responsible for this project is required to renew it on a yearly basis. If you have not done so in the last year, you will be required to before renewing the allocation. -For more information about the yearly renewal, visit our knowledge base: {{ project_renewal_help_url }}. +For more information about the yearly renewal, visit our knowledge base: {{ project_renewal_help_url }} If you are a student or collaborator under this project, you are receiving this notice as a courtesy. If you would like to opt out of future notifications, instructions can be found here: {{ opt_out_instruction_url }} diff --git a/docs/pages/config.md b/docs/pages/config.md index d4890336d..068589820 100644 --- a/docs/pages/config.md +++ b/docs/pages/config.md @@ -138,6 +138,7 @@ disabled: | EMAIL_OPT_OUT_INSTRUCTION_URL | URL of article regarding opt out | | EMAIL_SIGNATURE | Email signature to add to outgoing emails | | EMAIL_ALLOCATION_EXPIRING_NOTIFICATION_DAYS | List of days to send email notifications for expiring allocations. Default 7,14,30 | +| EMAIL_ADMINS_ON_ALLOCATION_EXPIRE | Setting this to True will send a daily email notification to administrators with a list of allocations that have expired that day. | ### Plugin settings For more info on [ColdFront plugins](plugin.md) (Django apps)