diff --git a/coldfront/core/allocation/admin.py b/coldfront/core/allocation/admin.py index 73532dad0..729c2faad 100644 --- a/coldfront/core/allocation/admin.py +++ b/coldfront/core/allocation/admin.py @@ -9,7 +9,10 @@ AllocationAttribute, AllocationAttributeType, AllocationAttributeUsage, + AllocationChangeRequest, + AllocationAttributeChangeRequest, AllocationStatusChoice, + AllocationChangeStatusChoice, AllocationUser, AllocationUserNote, AllocationUserStatusChoice, @@ -343,3 +346,19 @@ def project_pi(self, obj): @admin.register(AllocationAccount) class AllocationAccountAdmin(SimpleHistoryAdmin): list_display = ('name', 'user', ) + + +@admin.register(AllocationChangeStatusChoice) +class AllocationChangeStatusChoiceAdmin(admin.ModelAdmin): + list_display = ('name', ) + + +@admin.register(AllocationChangeRequest) +class AllocationChangeRequestAdmin(admin.ModelAdmin): + list_display = ('pk', 'allocation', 'status', 'end_date_extension', 'justification', 'notes', ) + + +@admin.register(AllocationAttributeChangeRequest) +class AllocationChangeStatusChoiceAdmin(admin.ModelAdmin): + list_display = ('pk', 'allocation_change_request', 'allocation_attribute', 'new_value', ) + diff --git a/coldfront/core/allocation/forms.py b/coldfront/core/allocation/forms.py index b24b517f4..5ed1fcc68 100644 --- a/coldfront/core/allocation/forms.py +++ b/coldfront/core/allocation/forms.py @@ -3,6 +3,7 @@ from coldfront.core.allocation.models import (Allocation, AllocationAccount, AllocationAttributeType, + AllocationAttribute, AllocationStatusChoice) from coldfront.core.allocation.utils import get_user_resources from coldfront.core.project.models import Project @@ -96,22 +97,6 @@ class AllocationRemoveUserForm(forms.Form): email = forms.EmailField(max_length=100, required=False, disabled=True) selected = forms.BooleanField(initial=False, required=False) -class AllocationAttributeChangeForm(forms.Form): - username = forms.CharField(max_length=150, disabled=True) - first_name = forms.CharField(max_length=30, required=False, disabled=True) - last_name = forms.CharField(max_length=150, required=False, disabled=True) - # attribute = forms.ModelChoiceField(queryset=None, empty_label=None) - # change = forms.CharField(widget=forms.Textarea) - # justification = forms.CharField(widget=forms.Textarea) - # allocation_account = forms.ChoiceField(required=False) - - # def __init__(self, pk, *args, **kwargs): - # super().__init__(*args, **kwargs) - # allocation_obj = get_object_or_404(Allocation, pk=pk) - - # self.fields['change'].help_text = '
Desired change for this attribute.' - # self.fields['justification'].help_text = '
Justification for requesting this allocation.' - class AllocationAttributeDeleteForm(forms.Form): pk = forms.IntegerField(required=False, disabled=True) @@ -190,3 +175,51 @@ class AllocationAccountForm(forms.ModelForm): class Meta: model = AllocationAccount fields = ['name', ] + + +class AllocationAttributeChangeForm(forms.Form): + pk = forms.IntegerField(required=False, disabled=True) + name = forms.CharField(max_length=150, required=False, disabled=True) + value = forms.CharField(max_length=150, required=False, disabled=True) + new_value = forms.CharField(max_length=150, required=False, disabled=False) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['pk'].widget = forms.HiddenInput() + + def clean(self): + cleaned_data = super().clean() + + if cleaned_data.get('new_value') != "": + allocation_attribute = AllocationAttribute.objects.get(pk=cleaned_data.get('pk')) + allocation_attribute.value = cleaned_data.get('new_value') + allocation_attribute.clean() + + +class AllocationChangeForm(forms.Form): + EXTENSION_CHOICES = [ + (0, "----"), (30, "30 days"), (60, "60 days"), (90, "90 days") + ] + end_date_extension = forms.TypedChoiceField( + label='Request End Date Extension', + choices = EXTENSION_CHOICES, + coerce=int, + required=False, + empty_value=0,) + justification = forms.CharField( + label='Justification for Changes', + widget=forms.Textarea, + help_text='Justification for requesting this allocation change request.') + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +class AllocationChangeNoteForm(forms.Form): + notes = forms.CharField( + max_length=512, + label='Notes', + required=False, + widget=forms.Textarea, + help_text="Leave any feedback about the allocation change request.") + diff --git a/coldfront/core/allocation/management/commands/add_allocation_defaults.py b/coldfront/core/allocation/management/commands/add_allocation_defaults.py index c8c1ad437..0a9893abf 100644 --- a/coldfront/core/allocation/management/commands/add_allocation_defaults.py +++ b/coldfront/core/allocation/management/commands/add_allocation_defaults.py @@ -3,6 +3,7 @@ from coldfront.core.allocation.models import (AttributeType, AllocationAttributeType, AllocationStatusChoice, + AllocationChangeStatusChoice, AllocationUserStatusChoice) @@ -20,6 +21,9 @@ def handle(self, *args, **options): 'Renewal Requested', 'Revoked', 'Unpaid',): AllocationStatusChoice.objects.get_or_create(name=choice) + for choice in ('Pending', 'Approved', 'Denied',): + AllocationChangeStatusChoice.objects.get_or_create(name=choice) + for choice in ('Active', 'Error', 'Removed', ): AllocationUserStatusChoice.objects.get_or_create(name=choice) diff --git a/coldfront/core/allocation/migrations/0003_auto_20191018_1049.py b/coldfront/core/allocation/migrations/0003_auto_20191018_1049.py index 91ccf7c98..7581bd0a8 100644 --- a/coldfront/core/allocation/migrations/0003_auto_20191018_1049.py +++ b/coldfront/core/allocation/migrations/0003_auto_20191018_1049.py @@ -20,14 +20,4 @@ class Migration(migrations.Migration): name='is_locked', field=models.BooleanField(default=False), ), - migrations.AddField( - model_name='allocation', - name='is_changeable', - field=models.BooleanField(default=False), - ), - migrations.AddField( - model_name='historicalallocation', - name='is_changeable', - field=models.BooleanField(default=False), - ), ] diff --git a/coldfront/core/allocation/migrations/0004_auto_20211029_0412.py b/coldfront/core/allocation/migrations/0004_auto_20211029_0412.py new file mode 100644 index 000000000..b86ee1da5 --- /dev/null +++ b/coldfront/core/allocation/migrations/0004_auto_20211029_0412.py @@ -0,0 +1,125 @@ +# Generated by Django 2.2.18 on 2021-10-29 08:12 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import model_utils.fields +import simple_history.models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('allocation', '0003_auto_20191018_1049'), + ] + + operations = [ + migrations.CreateModel( + name='AllocationChangeRequest', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('end_date_extension', models.IntegerField(blank=True, null=True)), + ('justification', models.TextField()), + ('notes', models.CharField(blank=True, max_length=512, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='AllocationChangeStatusChoice', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('name', models.CharField(max_length=64)), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.AddField( + model_name='allocation', + name='is_changeable', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='historicalallocation', + name='is_changeable', + field=models.BooleanField(default=False), + ), + migrations.CreateModel( + name='HistoricalAllocationChangeRequest', + fields=[ + ('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('end_date_extension', models.IntegerField(blank=True, null=True)), + ('justification', models.TextField()), + ('notes', models.CharField(blank=True, max_length=512, null=True)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('allocation', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='allocation.Allocation')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('status', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='allocation.AllocationChangeStatusChoice', verbose_name='Status')), + ], + options={ + 'verbose_name': 'historical allocation change request', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='HistoricalAllocationAttributeChangeRequest', + fields=[ + ('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('new_value', models.CharField(max_length=128)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('allocation_attribute', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='allocation.AllocationAttribute')), + ('allocation_change_request', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='allocation.AllocationChangeRequest')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical allocation attribute change request', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.AddField( + model_name='allocationchangerequest', + name='allocation', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='allocation.Allocation'), + ), + migrations.AddField( + model_name='allocationchangerequest', + name='status', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='allocation.AllocationChangeStatusChoice', verbose_name='Status'), + ), + migrations.CreateModel( + name='AllocationAttributeChangeRequest', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('new_value', models.CharField(max_length=128)), + ('allocation_attribute', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='allocation.AllocationAttribute')), + ('allocation_change_request', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='allocation.AllocationChangeRequest')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/coldfront/core/allocation/models.py b/coldfront/core/allocation/models.py index 62e767d8e..92512bd59 100644 --- a/coldfront/core/allocation/models.py +++ b/coldfront/core/allocation/models.py @@ -240,19 +240,19 @@ def clean(self): if expected_value_type == "Int" and not isinstance(literal_eval(self.value), int): raise ValidationError( - 'Invalid Value "%s". Value must be an integer.' % (self.value)) + 'Invalid Value "%s" for "%s". Value must be an integer.' % (self.value, self.allocation_attribute_type.name)) elif expected_value_type == "Float" and not (isinstance(literal_eval(self.value), float) or isinstance(literal_eval(self.value), int)): raise ValidationError( - 'Invalid Value "%s". Value must be a float.' % (self.value)) + 'Invalid Value "%s" for "%s". Value must be a float.' % (self.value, self.allocation_attribute_type.name)) elif expected_value_type == "Yes/No" and self.value not in ["Yes", "No"]: raise ValidationError( - 'Invalid Value "%s". Allowed inputs are "Yes" or "No".' % (self.value)) + 'Invalid Value "%s" for "%s". Allowed inputs are "Yes" or "No".' % (self.value, self.allocation_attribute_type.name)) elif expected_value_type == "Date": try: datetime.datetime.strptime(self.value.strip(), "%Y-%m-%d") except ValueError: raise ValidationError( - 'Invalid Value "%s". Date must be in format YYYY-MM-DD' % (self.value)) + 'Invalid Value "%s" for "%s". Date must be in format YYYY-MM-DD' % (self.value, self.allocation_attribute_type.name)) def __str__(self): return '%s' % (self.allocation_attribute_type.name) @@ -304,3 +304,37 @@ def __str__(self): class Meta: ordering = ['name', ] + + +class AllocationChangeStatusChoice(TimeStampedModel): + name = models.CharField(max_length=64) + + def __str__(self): + return self.name + + class Meta: + ordering = ['name', ] + + +class AllocationChangeRequest(TimeStampedModel): + allocation = models.ForeignKey(Allocation, on_delete=models.CASCADE,) + status = models.ForeignKey( + AllocationChangeStatusChoice, on_delete=models.CASCADE, verbose_name='Status') + end_date_extension = models.IntegerField(blank=True, null=True) + justification = models.TextField() + notes = models.CharField(max_length=512, blank=True, null=True) + history = HistoricalRecords() + + def __str__(self): + return "%s (%s)" % (self.get_parent_resource.name, self.allocation.project.pi) + + +class AllocationAttributeChangeRequest(TimeStampedModel): + allocation_change_request = models.ForeignKey(AllocationChangeRequest, on_delete=models.CASCADE) + allocation_attribute = models.ForeignKey(AllocationAttribute, on_delete=models.CASCADE) + new_value = models.CharField(max_length=128) + history = HistoricalRecords() + + def __str__(self): + return '%s' % (self.allocation_attribute.allocation_attribute_type.name) + diff --git a/coldfront/core/allocation/templates/allocation/allocation_attribute_change.html b/coldfront/core/allocation/templates/allocation/allocation_attribute_change.html deleted file mode 100644 index 9acd0cf02..000000000 --- a/coldfront/core/allocation/templates/allocation/allocation_attribute_change.html +++ /dev/null @@ -1,25 +0,0 @@ -{% extends "common/base.html" %} -{% load crispy_forms_tags %} -{% load common_tags %} -{% load static %} - - -{% block title %} -Request Allocation Attribute Change -{% endblock %} - - -{% block content %} -

Renew allocation to {{allocation.get_parent_resource }} for project: {{allocation.project.title}}

-
- -

The following {% settings_value 'CENTER_NAME' %} - resources are available to request for this project. If you need access to - more than one of these, please submit a separate allocation request for each - resource. For each request you must provide the justification for how you - intend to use the resource to further the research goals of your project.

- - -{% endblock %} diff --git a/coldfront/core/allocation/templates/allocation/allocation_change.html b/coldfront/core/allocation/templates/allocation/allocation_change.html new file mode 100644 index 000000000..eed9c99e1 --- /dev/null +++ b/coldfront/core/allocation/templates/allocation/allocation_change.html @@ -0,0 +1,137 @@ +{% extends "common/base.html" %} +{% load crispy_forms_tags %} +{% load static %} + + +{% block title %} +Request Allocation Change +{% endblock %} + + +{% block content %} + +

Request change to {{ allocation.get_parent_resource }} for project: {{ allocation.project.title }}

+
+ +

+ Request changes to an existing allocation using the form below. For each change + you must provide a justification. +

+ +
+
+

Allocation Information

+
+ +
+
+ {% csrf_token %} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + {% if allocation.is_changeable %} + + + + + {% endif %} + + + + + + + + + + + + + + {% if allocation.is_locked %} + + + {% else %} + + + {% endif %} + +
Project:{{ allocation.project }}
Resource{{ allocation.resources.all|pluralize }} in allocation:{{ allocation.get_resources_as_string }}
Justification:{{ allocation.justification }}
Status:{{ allocation.status }}
Start Date:{{ allocation.start_date }}
End Date: + {{ allocation.end_date }} + {% if allocation.is_locked and allocation.status.name == 'Active' and allocation.expires_in <= 60 and allocation.expires_in >= 0 %} + + Expires in {{allocation.expires_in}} day{{allocation.expires_in|pluralize}} - Not renewable + + {% endif %} +
Request End Date Extension: + {{ form.end_date_extension }} +
Description:{{ allocation.description }}
Created:{{ allocation.created|date:"M. d, Y" }}
Last Modified:{{ allocation.modified|date:"M. d, Y" }}
LockedUnlocked
+
+
+
+ + {% if formset %} +
+
+

Allocation Attributes

+
+
+
+ + + + + + + + + + {% for form in formset %} + + + + + + {% endfor %} + +
AttributeCurrent ValueRequest New Value
{{form.name.value}}{{form.value.value}}{{form.new_value}}
+
+ {{ formset.management_form }} +
+
+ {% endif %} + +
+ {{form.justification | as_crispy_field }} + + Back to + Allocation
+
+ + + +{% endblock %} diff --git a/coldfront/core/allocation/templates/allocation/allocation_change_detail.html b/coldfront/core/allocation/templates/allocation/allocation_change_detail.html new file mode 100644 index 000000000..bef9dd8fb --- /dev/null +++ b/coldfront/core/allocation/templates/allocation/allocation_change_detail.html @@ -0,0 +1,200 @@ +{% extends "common/base.html" %} +{% load crispy_forms_tags %} +{% load static %} + + +{% block title %} +Allocation Change Detail +{% endblock %} + + +{% block content %} + +

Change requested to {{ allocation_change.allocation.get_parent_resource }} for project: {{ allocation_change.allocation.project.title }}

+
+ + {% if allocation_change.status.name == "Approved" %} + + {% elif allocation_change.status.name == "Denied"%} + + {% else %} + + {% endif %} + + +
+
+

Allocation Information

+
+ +
+ {% csrf_token %} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + {% if allocation_change.allocation.is_changeable %} + + + + + {% endif %} + + + + + + + + + + + + + + {% if allocation_change.allocation.is_locked %} + + + {% else %} + + + {% endif %} + +
Project:{{ allocation_change.allocation.project }}
Resource{{ allocation_change.allocation.resources.all|pluralize }} in allocation:{{ allocation_change.allocation.get_resources_as_string }}
Justification:{{ allocation_change.allocation.justification }}
Status:{{ allocation_change.allocation.status }}
Start Date:{{ allocation_change.allocation.start_date }}
End Date: + {{ allocation_change.allocation.end_date }} + {% if allocation_change.allocation.is_locked and allocation_change.allocation.status.name == 'Approved' and allocation_change.allocation.expires_in <= 60 and allocation_change.allocation.expires_in >= 0 %} + + Expires in {{allocation_change.allocation.expires_in}} day{{allocation_change.allocation.expires_in|pluralize}} - Not renewable + + {% endif %} +
Requested End Date Extension: + {{allocation_change_form.end_date_extension}} +
Description:{{ allocation_change.allocation.description }}
Created:{{ allocation_change.allocation.created|date:"M. d, Y" }}
Last Modified:{{ allocation_change.allocation.modified|date:"M. d, Y" }}
LockedUnlocked
+
+
+
+ + +
+
+

Allocation Attributes

+
+
+ {% if attribute_changes %} +
+ + + + + {% if allocation_change.status.name == 'Pending' %} + + {% endif %} + + + + + {% for attribute in attribute_changes %} + + + {% if allocation_change.status.name == 'Pending' %} + + {% endif %} + {% if attribute.new_value == '' %} + + {% else %} + + {% endif %} + + {% endfor %} + +
AttributeCurrent ValueRequested New Value
{{attribute.allocation_attribute}}{{attribute.allocation_attribute.value}}None{{attribute.new_value}}
+
+ {% else %} + + {% endif %} +
+
+ +

{{allocation_change_form.justification | as_crispy_field}}

+ +
+ + {% if request.user.is_superuser %} +
+
+

Actions

+
+
+ +
+ {% csrf_token %} + {{note_form.notes | as_crispy_field}} +
+ {% if allocation_change.status.name == 'Pending' %} + Approve + Deny + {% endif %} + +
+
+
+
+ + {% endif %} + + + View Allocation + + {% if request.user.is_superuser %} + + See all allocation change requests + + {% endif %} +
+ +{% endblock %} diff --git a/coldfront/core/allocation/templates/allocation/allocation_change_list.html b/coldfront/core/allocation/templates/allocation/allocation_change_list.html new file mode 100644 index 000000000..68cef555d --- /dev/null +++ b/coldfront/core/allocation/templates/allocation/allocation_change_list.html @@ -0,0 +1,72 @@ +{% extends "common/base.html" %} +{% load crispy_forms_tags %} +{% load common_tags %} +{% load static %} + + +{% block title %} +Allocation Review New and Pending Change Requests +{% endblock %} + + +{% block content %} +

Allocation Change Requests

+ +{% if allocation_change_list %} +
+ + + + + + + + + + + + + + + {% for change in allocation_change_list %} + + + + + + + + + + + {% endfor %} + +
#RequestedProject TitlePIResourceExtensionChangesQuick Actions
{{change.pk}}{{ change.modified|date:"M. d, Y" }}{{change.allocation.project.title|truncatechars:50}}{{change.allocation.project.pi.first_name}} {{change.allocation.project.pi.last_name}} + ({{change.allocation.project.pi.username}}){{change.allocation.get_parent_resource}} + {% if change.end_date_extension == 0 %} + {% else %} {{change.end_date_extension}} days + {% endif %} + + {% if change.allocationattributechangerequest_set.all %} + + {% else %} + + {% endif %} + + Approve + Deny + Details +
+
+{% else %} +
+ No new or pending allocation change requests! +
+{% endif %} + + +{% endblock %} diff --git a/coldfront/core/allocation/templates/allocation/allocation_detail.html b/coldfront/core/allocation/templates/allocation/allocation_detail.html index 20dbd7348..498358ebc 100644 --- a/coldfront/core/allocation/templates/allocation/allocation_detail.html +++ b/coldfront/core/allocation/templates/allocation/allocation_detail.html @@ -28,13 +28,13 @@

Allocation Detail

- {% if request.user.userprofile.is_pi or request.user.role.name == "Manager" %} + {% if request.user.is_superuser or request.user.userprofile.is_pi or request.user.role.name == "Manager" %}

Allocation Information

@@ -43,6 +43,7 @@

Allocation Information

Allocation Information

{% endif %}
+
{% csrf_token %} @@ -204,6 +205,56 @@

{{attribute}}

{% endif %} + +
+
+

Allocation Change Requests

{{allocation_changes.count}} +
+ +
+ {% if allocation_changes %} +
+ + + + + + + + + + + {% for change_request in allocation_changes %} + + + {% if change_request.status.name == 'Approved' %} + + {% elif change_request.status.name == 'Denied' %} + + {% else %} + + {% endif %} + + {% if change_request.notes %} + + {% else %} + + {% endif %} + + + {% endfor %} + +
Date RequestedStatusNotesActions
{{ change_request.modified|date:"M. d, Y" }}{{ change_request.status.name }}{{ change_request.status.name }}{{ change_request.status.name }}{{change_request.notes}}Edit
+
+ {% else %} + + {% endif %} +
+
+
@@ -257,7 +308,7 @@

Users in Al
-

Messages from System Administrators

+

Notifications

{{notes.count}}
@@ -296,6 +347,16 @@

Messages fr $(document).ready(function () { var guage_data = {{ guage_data | safe }}; drawGauges(guage_data); + + $('#allocation_change_table').DataTable({ + lengthMenu: [5, 10, 20, 50, 100], + "pageLength": 5, + "ordering": false, + 'aoColumnDefs': [{ + 'bSortable': false, + 'aTargets': ['nosort'] + }] + }); }); function drawGauges(guage_data) { diff --git a/coldfront/core/allocation/urls.py b/coldfront/core/allocation/urls.py index 198a80e6c..29e378269 100644 --- a/coldfront/core/allocation/urls.py +++ b/coldfront/core/allocation/urls.py @@ -8,22 +8,30 @@ allocation_views.AllocationCreateView.as_view(), name='allocation-create'), path('/', allocation_views.AllocationDetailView.as_view(), name='allocation-detail'), + path('change-request//', allocation_views.AllocationChangeDetailView.as_view(), + name='allocation-change-detail'), path('/activate-request', allocation_views.AllocationActivateRequestView.as_view(), name='allocation-activate-request'), path('/deny-request', allocation_views.AllocationDenyRequestView.as_view(), name='allocation-deny-request'), + path('/activate-change-request', allocation_views.AllocationChangeActivateView.as_view(), + name='allocation-activate-change'), + path('/deny-change-request', allocation_views.AllocationChangeDenyView.as_view(), + name='allocation-deny-change'), path('/add-users', allocation_views.AllocationAddUsersView.as_view(), name='allocation-add-users'), path('/remove-users', allocation_views.AllocationRemoveUsersView.as_view(), name='allocation-remove-users'), path('request-list', allocation_views.AllocationRequestListView.as_view(), name='allocation-request-list'), + path('change-list', allocation_views.AllocationChangeListView.as_view(), + name='allocation-change-list'), path('/renew', allocation_views.AllocationRenewView.as_view(), name='allocation-renew'), path('/allocationattribute/add', allocation_views.AllocationAttributeCreateView.as_view(), name='allocation-attribute-add'), - path('/change-attribute', - allocation_views.AllocationAttributeChangeView.as_view(), name='allocation-attribute-change'), + path('/change-request', + allocation_views.AllocationChangeView.as_view(), name='allocation-change'), path('/allocationattribute/delete', allocation_views.AllocationAttributeDeleteView.as_view(), name='allocation-attribute-delete'), path('allocation-invoice-list', allocation_views.AllocationInvoiceListView.as_view(), diff --git a/coldfront/core/allocation/views.py b/coldfront/core/allocation/views.py index cf32e4509..235607506 100644 --- a/coldfront/core/allocation/views.py +++ b/coldfront/core/allocation/views.py @@ -26,6 +26,8 @@ from coldfront.core.allocation.forms import (AllocationAccountForm, AllocationAddUserForm, AllocationAttributeDeleteForm, + AllocationChangeForm, + AllocationChangeNoteForm, AllocationAttributeChangeForm, AllocationForm, AllocationInvoiceNoteDeleteForm, @@ -37,6 +39,9 @@ from coldfront.core.allocation.models import (Allocation, AllocationAccount, AllocationAttribute, AllocationAttributeType, + AllocationChangeRequest, + AllocationChangeStatusChoice, + AllocationAttributeChangeRequest, AllocationStatusChoice, AllocationUser, AllocationUserNote, @@ -130,6 +135,8 @@ def get_context_data(self, **kwargs): attributes = [attribute for attribute in allocation_obj.allocationattribute_set.filter( allocation_attribute_type__is_private=False)] + allocation_changes = allocation_obj.allocationchangerequest_set.all().order_by('-pk') + guage_data = [] invalid_attributes = [] for attribute in attributes_with_usage: @@ -159,6 +166,7 @@ def get_context_data(self, **kwargs): context['guage_data'] = guage_data context['attributes_with_usage'] = attributes_with_usage context['attributes'] = attributes + context['allocation_changes'] = allocation_changes # Can the user update the project? if self.request.user.is_superuser: @@ -632,169 +640,6 @@ def form_valid(self, form): def get_success_url(self): return reverse('project-detail', kwargs={'pk': self.kwargs.get('project_pk')}) -class AllocationAttributeChangeView(LoginRequiredMixin, UserPassesTestMixin, FormView): - form_class = AllocationAttributeChangeForm - template_name = 'allocation/allocation_attribute_change.html' - - def test_func(self): - """ UserPassesTestMixin Tests""" - if self.request.user.is_superuser: - return True - - pk = self.kwargs.get('pk') - allocation_obj = get_object_or_404(Allocation, pk=pk) - - if allocation_obj.project.pi == self.request.user: - return True - - if allocation_obj.project.projectuser_set.filter(user=self.request.user, role__name='Manager', status__name='Active').exists(): - return True - - messages.error( - self.request, 'You do not have permission to request changes to this allocation.') - - def dispatch(self, request, *args, **kwargs): - allocation_obj = get_object_or_404(Allocation, pk=self.kwargs.get('pk')) - - if allocation_obj.project.needs_review: - messages.error( - request, 'You cannot request a change to this allocation because you have to review your project first.') - return HttpResponseRedirect(reverse('project-detail', kwargs={'pk': allocation_obj.project.pk})) - - if allocation_obj.project.status.name not in ['Active', 'New', ]: - messages.error( - request, 'You cannot request a change to an allocation in an archived project.') - return HttpResponseRedirect(reverse('project-detail', kwargs={'pk': allocation_obj.project.pk})) - - return super().dispatch(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - allocation_obj = get_object_or_404( - Allocation, pk=self.kwargs.get('pk')) - context['project'] = allocation_obj.project - - user_resources = get_user_resources(self.request.user) - resources_form_default_quantities = {} - resources_form_label_texts = {} - resources_with_eula = {} - for resource in user_resources: - if resource.resourceattribute_set.filter(resource_attribute_type__name='quantity_default_value').exists(): - value = resource.resourceattribute_set.get( - resource_attribute_type__name='quantity_default_value').value - resources_form_default_quantities[resource.id] = int(value) - if resource.resourceattribute_set.filter(resource_attribute_type__name='quantity_label').exists(): - value = resource.resourceattribute_set.get( - resource_attribute_type__name='quantity_label').value - resources_form_label_texts[resource.id] = mark_safe( - '{}*'.format(value)) - if resource.resourceattribute_set.filter(resource_attribute_type__name='eula').exists(): - value = resource.resourceattribute_set.get( - resource_attribute_type__name='eula').value - resources_with_eula[resource.id] = value - - context['AllocationAccountForm'] = AllocationAccountForm() - context['resources_form_default_quantities'] = resources_form_default_quantities - context['resources_form_label_texts'] = resources_form_label_texts - context['resources_with_eula'] = resources_with_eula - context['resources_with_accounts'] = list(Resource.objects.filter( - name__in=list(ALLOCATION_ACCOUNT_MAPPING.keys())).values_list('id', flat=True)) - - return context - - def get_form(self, form_class=None): - """Return an instance of the form to be used in this view.""" - if form_class is None: - form_class = self.get_form_class() - return form_class(self.request.user, self.kwargs.get('pk'), **self.get_form_kwargs()) - - def form_valid(self, form): - form_data = form.cleaned_data - allocation_obj = get_object_or_404( - Project, pk=self.kwargs.get('pk')) - resource_obj = form_data.get('resource') - justification = form_data.get('justification') - quantity = form_data.get('quantity', 1) - allocation_account = form_data.get('allocation_account', None) - # A resource is selected that requires an account name selection but user has no account names - if ALLOCATION_ACCOUNT_ENABLED and resource_obj.name in ALLOCATION_ACCOUNT_MAPPING and AllocationAttributeType.objects.filter( - name=ALLOCATION_ACCOUNT_MAPPING[resource_obj.name]).exists() and not allocation_account: - form.add_error(None, format_html( - 'You need to create an account name. Create it by clicking the link under the "Allocation account" field.')) - return self.form_invalid(form) - - usernames = form_data.get('users') - usernames.append(allocation_obj.project.pi.username) - usernames = list(set(usernames)) - - users = [User.objects.get(username=username) for username in usernames] - if allocation_obj.project.pi not in users: - users.append(allocation_obj.project.pi) - - if INVOICE_ENABLED and resource_obj.requires_payment: - allocation_status_obj = AllocationStatusChoice.objects.get( - name=INVOICE_DEFAULT_STATUS) - else: - allocation_status_obj = AllocationStatusChoice.objects.get( - name='New') - - allocation_obj = Allocation.objects.create( - project=project_obj, - justification=justification, - quantity=quantity, - status=allocation_status_obj - ) - allocation_obj.resources.add(resource_obj) - - if ALLOCATION_ACCOUNT_ENABLED and allocation_account and resource_obj.name in ALLOCATION_ACCOUNT_MAPPING: - - allocation_attribute_type_obj = AllocationAttributeType.objects.get( - name=ALLOCATION_ACCOUNT_MAPPING[resource_obj.name]) - AllocationAttribute.objects.create( - allocation_attribute_type=allocation_attribute_type_obj, - allocation=allocation_obj, - value=allocation_account - ) - - for linked_resource in resource_obj.linked_resources.all(): - allocation_obj.resources.add(linked_resource) - - allocation_user_active_status = AllocationUserStatusChoice.objects.get( - name='Active') - for user in users: - allocation_user_obj = AllocationUser.objects.create( - allocation=allocation_obj, - user=user, - status=allocation_user_active_status) - - pi_name = '{} {} ({})'.format(allocation_obj.project.pi.first_name, - allocation_obj.project.pi.last_name, allocation_obj.project.pi.username) - resource_name = allocation_obj.get_parent_resource - domain_url = get_domain_url(self.request) - url = '{}{}'.format(domain_url, reverse('allocation-request-list')) - - if EMAIL_ENABLED: - template_context = { - 'pi': pi_name, - 'resource': resource_name, - 'url': url - } - - send_email_template( - 'New allocation request: {} - {}'.format( - pi_name, resource_name), - 'email/new_allocation_request.txt', - template_context, - EMAIL_SENDER, - [EMAIL_TICKET_SYSTEM_ADDRESS, ] - ) - - return super().form_valid(form) - - def get_success_url(self): - allocation_obj = get_object_or_404(Allocation, pk=self.kwargs.get('pk')) - return reverse('project-detail', kwargs={'pk': allocation_obj.project.pk}) - class AllocationAddUsersView(LoginRequiredMixin, UserPassesTestMixin, TemplateView): template_name = 'allocation/allocation_add_users.html' @@ -1303,7 +1148,6 @@ def get(self, request, pk): EMAIL_SENDER, email_receiver_list ) - print(email_receiver_list) return HttpResponseRedirect(reverse('allocation-request-list')) @@ -1732,3 +1576,422 @@ def test_func(self): def get_queryset(self): return AllocationAccount.objects.filter(user=self.request.user) + + +class AllocationChangeDetailView(LoginRequiredMixin, UserPassesTestMixin, FormView): + template_name = 'allocation/allocation_change_detail.html' + + def test_func(self): + """ UserPassesTestMixin Tests""" + if self.request.user.is_superuser: + return True + + if self.request.user.has_perm('allocation.can_view_all_allocations'): + return True + + allocation_change_obj = get_object_or_404( + AllocationChangeRequest, pk=self.kwargs.get('pk')) + + if allocation_change_obj.allocation.project.pi == self.request.user: + return True + + if allocation_change_obj.allocation.project.projectuser_set.filter(user=self.request.user, role__name='Manager', status__name='Active').exists(): + return True + + user_can_access_project = allocation_change_obj.allocation.project.projectuser_set.filter( + user=self.request.user, status__name__in=['Active', 'New', ]).exists() + + user_can_access_allocation = allocation_change_obj.allocation.allocationuser_set.filter( + user=self.request.user, status__name__in=['Active', ]).exists() + + if user_can_access_project and user_can_access_allocation: + return True + + return False + + def get_context_data(self, **kwargs): + context = {} + + allocation_change_obj = get_object_or_404( + AllocationChangeRequest, pk=self.kwargs.get('pk')) + + attribute_changes = allocation_change_obj.allocationattributechangerequest_set.all() + + context['allocation_change'] = allocation_change_obj + context['attribute_changes'] = attribute_changes + + return context + + def get(self, request, *args, **kwargs): + + allocation_change_obj = get_object_or_404( + AllocationChangeRequest, pk=self.kwargs.get('pk')) + + allocation_change_form = AllocationChangeForm( + initial={'justification': allocation_change_obj.justification, + 'end_date_extension': allocation_change_obj.end_date_extension}) + allocation_change_form.fields['justification'].disabled = True + allocation_change_form.fields['end_date_extension'].disabled = True + + note_form = AllocationChangeNoteForm( + initial={'notes': allocation_change_obj.notes}) + + context = self.get_context_data() + + context['allocation_change_form'] = allocation_change_form + context['note_form'] = note_form + return render(request, self.template_name, context) + + def post(self, request, *args, **kwargs): + pk = self.kwargs.get('pk') + allocation_change_obj = get_object_or_404( + AllocationChangeRequest, pk=pk) + if not self.request.user.is_superuser: + messages.success( + request, 'You do not have permission to update the allocation change request') + return HttpResponseRedirect(reverse('allocation-change-detail', kwargs={'pk': pk})) + + initial_data = { + 'notes': allocation_change_obj.notes, + } + + note_form = AllocationChangeNoteForm(request.POST, initial=initial_data) + + if note_form.is_valid(): + form_data = note_form.cleaned_data + notes = form_data.get('notes') + + allocation_change_obj.notes = notes + allocation_change_obj.save() + + messages.success( + request, 'Allocation change request updated!') + return HttpResponseRedirect(reverse('allocation-change-detail', kwargs={'pk': pk})) + else: + allocation_change_form = AllocationChangeForm( + initial={'justification': allocation_change_obj.justification}) + allocation_change_form.fields['justification'].disabled = True + + context = self.get_context_data() + + context['note_form'] = note_form + context['allocation_change_form'] = allocation_change_form + return render(request, self.template_name, context) + + +class AllocationChangeListView(LoginRequiredMixin, UserPassesTestMixin, TemplateView): + template_name = 'allocation/allocation_change_list.html' + login_url = '/' + + def test_func(self): + """ UserPassesTestMixin Tests""" + + if self.request.user.is_superuser: + return True + + if self.request.user.has_perm('allocation.can_review_allocation_requests'): + return True + + messages.error( + self.request, 'You do not have permission to review allocation requests.') + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + allocation_change_list = AllocationChangeRequest.objects.filter( + status__name__in=['Pending', ]) + context['allocation_change_list'] = allocation_change_list + context['PROJECT_ENABLE_PROJECT_REVIEW'] = PROJECT_ENABLE_PROJECT_REVIEW + return context + + +class AllocationChangeView(LoginRequiredMixin, UserPassesTestMixin, FormView): + formset_class = AllocationAttributeChangeForm + template_name = 'allocation/allocation_change.html' + + def test_func(self): + """ UserPassesTestMixin Tests""" + if self.request.user.is_superuser: + return True + + allocation_obj = get_object_or_404( + Allocation, pk=self.kwargs.get('pk')) + + if allocation_obj.project.pi == self.request.user: + return True + + if allocation_obj.project.projectuser_set.filter(user=self.request.user, role__name='Manager', status__name='Active').exists(): + return True + + messages.error( + self.request, 'You do not have permission to request changes to this allocation.') + + def dispatch(self, request, *args, **kwargs): + allocation_obj = get_object_or_404( + Allocation, pk=self.kwargs.get('pk')) + + if allocation_obj.project.needs_review: + messages.error( + request, 'You cannot request a change to this allocation because you have to review your project first.') + return HttpResponseRedirect(reverse('allocation-detail', kwargs={'pk': allocation_obj.pk})) + + if allocation_obj.project.status.name not in ['Active', 'New', ]: + messages.error( + request, 'You cannot request a change to an allocation in an archived project.') + return HttpResponseRedirect(reverse('allocation-detail', kwargs={'pk': allocation_obj.pk})) + + if allocation_obj.is_locked: + messages.error( + request, 'You cannot request a change to a locked allocation.') + return HttpResponseRedirect(reverse('allocation-detail', kwargs={'pk': allocation_obj.pk})) + + return super().dispatch(request, *args, **kwargs) + + def get_allocation_attributes_to_change(self, allocation_obj): + attributes_to_change = allocation_obj.allocationattribute_set.filter( + allocation_attribute_type__is_changeable=True) + + attributes_to_change = [ + + {'pk': attribute.pk, + 'name': attribute.allocation_attribute_type.name, + 'value': attribute.value, + } + + for attribute in attributes_to_change + ] + + return attributes_to_change + + def get(self, request, *args, **kwargs): + context = {} + + allocation_obj = get_object_or_404( + Allocation, pk=self.kwargs.get('pk')) + + form = AllocationChangeForm(**self.get_form_kwargs()) + context['form'] = form + + allocation_attributes_to_change = self.get_allocation_attributes_to_change(allocation_obj) + + if allocation_attributes_to_change: + formset = formset_factory(self.formset_class, max_num=len( + allocation_attributes_to_change)) + formset = formset( + initial=allocation_attributes_to_change, prefix='attributeform') + context['formset'] = formset + context['allocation'] = allocation_obj + context['attributes'] = allocation_attributes_to_change + return render(request, self.template_name, context) + + def post(self, request, *args, **kwargs): + change_requested = False + attribute_changes_to_make = set({}) + + pk = self.kwargs.get('pk') + allocation_obj = get_object_or_404(Allocation, pk=pk) + + form = AllocationChangeForm(**self.get_form_kwargs()) + + allocation_attributes_to_change = self.get_allocation_attributes_to_change( + allocation_obj) + + formset = formset_factory(self.formset_class, max_num=len( + allocation_attributes_to_change)) + formset = formset( + request.POST, initial=allocation_attributes_to_change, prefix='attributeform') + + if form.is_valid() and formset.is_valid(): + form_data = form.cleaned_data + + if form_data.get('end_date_extension') != 0: change_requested = True + + for entry in formset: + formset_data = entry.cleaned_data + + new_value = formset_data.get('new_value') + + if new_value != "": + change_requested = True + + allocation_attribute = AllocationAttribute.objects.get(pk=formset_data.get('pk')) + attribute_changes_to_make.add((allocation_attribute, new_value)) + + if change_requested == True: + + end_date_extension = form_data.get('end_date_extension') + justification = form_data.get('justification') + + change_request_status_obj = AllocationChangeStatusChoice.objects.get( + name='Pending') + + allocation_change_request_obj = AllocationChangeRequest.objects.create( + allocation=allocation_obj, + end_date_extension=end_date_extension, + justification=justification, + status=change_request_status_obj + ) + + for attribute in attribute_changes_to_make: + attribute_change_request_obj = AllocationAttributeChangeRequest.objects.create( + allocation_change_request=allocation_change_request_obj, + allocation_attribute=attribute[0], + new_value=attribute[1] + ) + messages.success( + request, 'Allocation change request successfully submitted.') + + return HttpResponseRedirect(reverse('allocation-detail', kwargs={'pk': pk})) + + else: + messages.error(request, 'You must request a change.') + return HttpResponseRedirect(reverse('allocation-change', kwargs={'pk': pk})) + + else: + attribute_errors = "" + for error in form.errors: + messages.error(request, error) + for error in formset.errors: + if error: attribute_errors += error.get('__all__') + messages.error(request, attribute_errors) + return HttpResponseRedirect(reverse('allocation-change', kwargs={'pk': pk})) + + +class AllocationChangeActivateView(LoginRequiredMixin, UserPassesTestMixin, View): + login_url = '/' + + def test_func(self): + """ UserPassesTestMixin Tests""" + + if self.request.user.is_superuser: + return True + + if self.request.user.has_perm('allocation.can_review_allocation_requests'): + return True + + messages.error( + self.request, 'You do not have permission to approve an allocation change.') + + def get(self, request, pk): + allocation_change_obj = get_object_or_404(AllocationChangeRequest, pk=pk) + + allocation_change_status_active_obj = AllocationChangeStatusChoice.objects.get( + name='Approved') + + allocation_change_obj.status = allocation_change_status_active_obj + + if allocation_change_obj.end_date_extension != 0: + new_end_date = allocation_change_obj.allocation.end_date + relativedelta( + days=allocation_change_obj.end_date_extension) + + allocation_change_obj.allocation.end_date = new_end_date + + allocation_change_obj.allocation.save() + allocation_change_obj.save() + + attribute_change_list = allocation_change_obj.allocationattributechangerequest_set.all() + + for attribute_change in attribute_change_list: + attribute_change.allocation_attribute.value = attribute_change.new_value + attribute_change.allocation_attribute.save() + + messages.success(request, 'Allocation change request to {} has been APPROVED for {} {} ({})'.format( + allocation_change_obj.allocation.get_parent_resource, + allocation_change_obj.allocation.project.pi.first_name, + allocation_change_obj.allocation.project.pi.last_name, + allocation_change_obj.allocation.project.pi.username) + ) + + resource_name = allocation_change_obj.allocation.get_parent_resource + domain_url = get_domain_url(self.request) + allocation_url = '{}{}'.format(domain_url, reverse( + 'allocation-detail', kwargs={'pk': allocation_change_obj.allocation.pk})) + + if EMAIL_ENABLED: + template_context = { + 'center_name': EMAIL_CENTER_NAME, + 'resource': resource_name, + 'allocation_url': allocation_url, + 'signature': EMAIL_SIGNATURE, + 'opt_out_instruction_url': EMAIL_OPT_OUT_INSTRUCTION_URL + } + + email_receiver_list = [] + + for allocation_user in allocation_change_obj.allocation.allocationuser_set.exclude(status__name__in=['Removed', 'Error']): + allocation_activate_user.send( + sender=self.__class__, allocation_user_pk=allocation_user.pk) + if allocation_user.allocation.project.projectuser_set.get(user=allocation_user.user).enable_notifications: + email_receiver_list.append(allocation_user.user.email) + + send_email_template( + 'Allocation Change Approved', + 'email/allocation_change_approved.txt', + template_context, + EMAIL_SENDER, + email_receiver_list + ) + + return HttpResponseRedirect(reverse('allocation-change-list')) + + +class AllocationChangeDenyView(LoginRequiredMixin, UserPassesTestMixin, View): + login_url = '/' + + def test_func(self): + """ UserPassesTestMixin Tests""" + + if self.request.user.is_superuser: + return True + + if self.request.user.has_perm('allocation.can_review_allocation_requests'): + return True + + messages.error( + self.request, 'You do not have permission to deny an allocation change.') + + def get(self, request, pk): + allocation_change_obj = get_object_or_404(AllocationChangeRequest, pk=pk) + + allocation_change_status_denied_obj = AllocationChangeStatusChoice.objects.get( + name='Denied') + + allocation_change_obj.status = allocation_change_status_denied_obj + allocation_change_obj.save() + + messages.success(request, 'Allocation change request to {} has been DENIED for {} {} ({})'.format( + allocation_change_obj.allocation.resources.first(), + allocation_change_obj.allocation.project.pi.first_name, + allocation_change_obj.allocation.project.pi.last_name, + allocation_change_obj.allocation.project.pi.username) + ) + + resource_name = allocation_change_obj.allocation.get_parent_resource + domain_url = get_domain_url(self.request) + allocation_url = '{}{}'.format(domain_url, reverse( + 'allocation-detail', kwargs={'pk': allocation_change_obj.allocation.pk})) + + if EMAIL_ENABLED: + template_context = { + 'center_name': EMAIL_CENTER_NAME, + 'resource': resource_name, + 'allocation_url': allocation_url, + 'signature': EMAIL_SIGNATURE, + 'opt_out_instruction_url': EMAIL_OPT_OUT_INSTRUCTION_URL + } + + email_receiver_list = [] + for allocation_user in allocation_change_obj.allocation.allocationuser_set.exclude(status__name__in=['Removed', 'Error']): + allocation_remove_user.send( + sender=self.__class__, allocation_user_pk=allocation_user.pk) + if allocation_user.allocation.project.projectuser_set.get(user=allocation_user.user).enable_notifications: + email_receiver_list.append(allocation_user.user.email) + + send_email_template( + 'Allocation Change Denied', + 'email/allocation_change_denied.txt', + template_context, + EMAIL_SENDER, + email_receiver_list + ) + return HttpResponseRedirect(reverse('allocation-change-list')) + diff --git a/coldfront/core/project/templates/project/project_detail.html b/coldfront/core/project/templates/project/project_detail.html index 04674956c..9281606b4 100644 --- a/coldfront/core/project/templates/project/project_detail.html +++ b/coldfront/core/project/templates/project/project_detail.html @@ -382,7 +382,7 @@

-

Messages from System Administrators

{{project.projectusermessage_set.count}} +

Notifications

{{project.projectusermessage_set.count}}
{% if project.projectusermessage_set.all %} diff --git a/coldfront/templates/common/navbar_admin.html b/coldfront/templates/common/navbar_admin.html index 9872e4a74..31d8f689b 100644 --- a/coldfront/templates/common/navbar_admin.html +++ b/coldfront/templates/common/navbar_admin.html @@ -8,6 +8,8 @@ Project Reviews Allocation Requests + + Allocation Change Requests Grant Report
diff --git a/coldfront/templates/email/allocation_change_approved.txt b/coldfront/templates/email/allocation_change_approved.txt new file mode 100644 index 000000000..6052b46c8 --- /dev/null +++ b/coldfront/templates/email/allocation_change_approved.txt @@ -0,0 +1,10 @@ +Dear {{center_name}} user, + +Your allocation change request for {{resource}} has been approved. The requested changes are now active. + +To view your allocation's information, please go to {{allocation_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}} + +Thank you, +{{signature}} diff --git a/coldfront/templates/email/allocation_change_denied.txt b/coldfront/templates/email/allocation_change_denied.txt new file mode 100644 index 000000000..633b29d97 --- /dev/null +++ b/coldfront/templates/email/allocation_change_denied.txt @@ -0,0 +1,10 @@ +Dear {{center_name}} user, + +Your allocation change request for {{resource}} has been denied. + +Please login to view a message from the system administrators pertaining to your allocation change request: {{allocation_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}} + +Thank you, +{{signature}}