From 1d13786e0aa308de066ee3f64d6e9b80fe61cafc Mon Sep 17 00:00:00 2001 From: Brian Scorcia Date: Fri, 6 Aug 2021 16:53:22 -0400 Subject: [PATCH 01/15] #294: Created a 'is changable' allocation attribute property. When checked this property allows a 'request change' button to appear next to the corresponding attribute on its Allocation Detail page. This button will be used to link the user to a new form which will allow them to request changes to be made to the attribute (form still in progress). --- .../allocation/migrations/0001_initial.py | 2 + coldfront/core/allocation/models.py | 1 + .../allocation_attribute_change.html | 167 ++++++++++++++++++ .../allocation/allocation_detail.html | 14 +- coldfront/core/allocation/urls.py | 2 + coldfront/core/allocation/views.py | 163 +++++++++++++++++ 6 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 coldfront/core/allocation/templates/allocation/allocation_attribute_change.html diff --git a/coldfront/core/allocation/migrations/0001_initial.py b/coldfront/core/allocation/migrations/0001_initial.py index 22ab23d55..49d584add 100644 --- a/coldfront/core/allocation/migrations/0001_initial.py +++ b/coldfront/core/allocation/migrations/0001_initial.py @@ -60,6 +60,7 @@ class Migration(migrations.Migration): ('is_required', models.BooleanField(default=False)), ('is_unique', models.BooleanField(default=False)), ('is_private', models.BooleanField(default=True)), + ('is_changeable', models.BooleanField(default=False)), ], options={ 'ordering': ['name'], @@ -166,6 +167,7 @@ class Migration(migrations.Migration): ('is_required', models.BooleanField(default=False)), ('is_unique', models.BooleanField(default=False)), ('is_private', models.BooleanField(default=True)), + ('is_changeable', models.BooleanField(default=False)), ('history_id', models.AutoField(primary_key=True, serialize=False)), ('history_date', models.DateTimeField()), ('history_change_reason', models.CharField(max_length=100, null=True)), diff --git a/coldfront/core/allocation/models.py b/coldfront/core/allocation/models.py index 6d99ef65b..eca53c388 100644 --- a/coldfront/core/allocation/models.py +++ b/coldfront/core/allocation/models.py @@ -206,6 +206,7 @@ class AllocationAttributeType(TimeStampedModel): is_required = models.BooleanField(default=False) is_unique = models.BooleanField(default=False) is_private = models.BooleanField(default=True) + is_changeable = models.BooleanField(default=False) history = HistoricalRecords() def __str__(self): diff --git a/coldfront/core/allocation/templates/allocation/allocation_attribute_change.html b/coldfront/core/allocation/templates/allocation/allocation_attribute_change.html new file mode 100644 index 000000000..c023dd0c9 --- /dev/null +++ b/coldfront/core/allocation/templates/allocation/allocation_attribute_change.html @@ -0,0 +1,167 @@ +{% extends "common/base.html" %} +{% load crispy_forms_tags %} +{% load common_tags %} +{% load static %} + + +{% block title %} +Request Allocation Attribute Change +{% endblock %} + + +{% block content %} +

Request Allocation Attribute Change
Project: {{ 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.

+ +
+ {% csrf_token %} + {{form |crispy}} + + + + + Back to Project
+
+ + + + + +{% endblock %} diff --git a/coldfront/core/allocation/templates/allocation/allocation_detail.html b/coldfront/core/allocation/templates/allocation/allocation_detail.html index c779570f1..8bbfde6e7 100644 --- a/coldfront/core/allocation/templates/allocation/allocation_detail.html +++ b/coldfront/core/allocation/templates/allocation/allocation_detail.html @@ -92,6 +92,11 @@

Allocation Information

{% endif %} + {% if request.user.userprofile.is_pi or request.user.role.name == "Manager" %} + + Request Change + + {% endif %} @@ -165,7 +170,14 @@

Alloc {% else %} {{attribute}} - {{attribute.value}} + + {{attribute.value}} + {% if request.user.userprofile.is_pi and attribute.allocation_attribute_type.is_changeable or request.user.role.name == "Manager" and attribute.allocation_attribute_type.is_changeable %} + + Request Change + + {% endif %} + {% endif %} {% endfor %} diff --git a/coldfront/core/allocation/urls.py b/coldfront/core/allocation/urls.py index 19925f8d4..e5a1802a2 100644 --- a/coldfront/core/allocation/urls.py +++ b/coldfront/core/allocation/urls.py @@ -22,6 +22,8 @@ name='allocation-renew'), path('/allocationattribute/add', allocation_views.AllocationAttributeCreateView.as_view(), name='allocation-attribute-add'), + path('project//change-attribute', + allocation_views.AllocationAttributeChangeView.as_view(), name='allocation-attribute-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 ae947d360..d9943ad1b 100644 --- a/coldfront/core/allocation/views.py +++ b/coldfront/core/allocation/views.py @@ -631,6 +631,169 @@ 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 = AllocationForm + template_name = 'allocation/allocation_create.html' + + def test_func(self): + """ UserPassesTestMixin Tests""" + if self.request.user.is_superuser: + return True + + project_obj = get_object_or_404( + Project, pk=self.kwargs.get('project_pk')) + + if project_obj.pi == self.request.user: + return True + + if project_obj.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 create a new allocation.') + + def dispatch(self, request, *args, **kwargs): + project_obj = get_object_or_404( + Project, pk=self.kwargs.get('project_pk')) + + if project_obj.needs_review: + messages.error( + request, 'You cannot request a new allocation because you have to review your project first.') + return HttpResponseRedirect(reverse('project-detail', kwargs={'pk': project_obj.pk})) + + if project_obj.status.name not in ['Active', 'New', ]: + messages.error( + request, 'You cannot request a new allocation to an archived project.') + return HttpResponseRedirect(reverse('project-detail', kwargs={'pk': project_obj.pk})) + + return super().dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + project_obj = get_object_or_404( + Project, pk=self.kwargs.get('project_pk')) + context['project'] = project_obj + + 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('project_pk'), **self.get_form_kwargs()) + + def form_valid(self, form): + form_data = form.cleaned_data + project_obj = get_object_or_404( + Project, pk=self.kwargs.get('project_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(project_obj.pi.username) + usernames = list(set(usernames)) + + users = [User.objects.get(username=username) for username in usernames] + if project_obj.pi not in users: + users.append(project_obj.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): + return reverse('project-detail', kwargs={'pk': self.kwargs.get('project_pk')}) + class AllocationAddUsersView(LoginRequiredMixin, UserPassesTestMixin, TemplateView): template_name = 'allocation/allocation_add_users.html' From 0944bd662287b0a4badeb6e8423b3f5bdbaafa93 Mon Sep 17 00:00:00 2001 From: Brian Scorcia Date: Fri, 13 Aug 2021 14:58:40 -0400 Subject: [PATCH 02/15] #294: Created an 'is changeable' attribute for allocations which will be used to allow PI's to request an extension on an allocation's End Date if checked in the admin portal. The 'Request Change' button on an Allocation Detail page has now been moved to the top of the page (and only one will appear for all desired changes). Fixed routing issue for the 'Request Allocation Attribute Change' form. Form in progress --- coldfront/core/allocation/admin.py | 2 +- coldfront/core/allocation/forms.py | 16 ++ .../migrations/0003_auto_20191018_1049.py | 10 ++ coldfront/core/allocation/models.py | 1 + .../allocation_attribute_change.html | 144 +----------------- .../allocation/allocation_detail.html | 25 +-- coldfront/core/allocation/urls.py | 2 +- coldfront/core/allocation/views.py | 53 +++---- 8 files changed, 71 insertions(+), 182 deletions(-) diff --git a/coldfront/core/allocation/admin.py b/coldfront/core/allocation/admin.py index c5b918d9a..73532dad0 100644 --- a/coldfront/core/allocation/admin.py +++ b/coldfront/core/allocation/admin.py @@ -53,7 +53,7 @@ class AllocationAdmin(SimpleHistoryAdmin): readonly_fields_change = ( 'project', 'justification', 'created', 'modified',) fields_change = ('project', 'resources', 'quantity', 'justification', - 'status', 'start_date', 'end_date', 'description', 'created', 'modified', 'is_locked') + 'status', 'start_date', 'end_date', 'description', 'created', 'modified', 'is_locked', 'is_changeable') list_display = ('pk', 'project_title', 'project_pi', 'resource', 'quantity', 'justification', 'start_date', 'end_date', 'status', 'created', 'modified', ) inlines = [AllocationUserInline, diff --git a/coldfront/core/allocation/forms.py b/coldfront/core/allocation/forms.py index d0e9ddd0b..b24b517f4 100644 --- a/coldfront/core/allocation/forms.py +++ b/coldfront/core/allocation/forms.py @@ -96,6 +96,22 @@ 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) diff --git a/coldfront/core/allocation/migrations/0003_auto_20191018_1049.py b/coldfront/core/allocation/migrations/0003_auto_20191018_1049.py index 7581bd0a8..91ccf7c98 100644 --- a/coldfront/core/allocation/migrations/0003_auto_20191018_1049.py +++ b/coldfront/core/allocation/migrations/0003_auto_20191018_1049.py @@ -20,4 +20,14 @@ 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/models.py b/coldfront/core/allocation/models.py index eca53c388..62e767d8e 100644 --- a/coldfront/core/allocation/models.py +++ b/coldfront/core/allocation/models.py @@ -46,6 +46,7 @@ class Allocation(TimeStampedModel): justification = models.TextField() description = models.CharField(max_length=512, blank=True, null=True) is_locked = models.BooleanField(default=False) + is_changeable = models.BooleanField(default=False) history = HistoricalRecords() class Meta: diff --git a/coldfront/core/allocation/templates/allocation/allocation_attribute_change.html b/coldfront/core/allocation/templates/allocation/allocation_attribute_change.html index c023dd0c9..9acd0cf02 100644 --- a/coldfront/core/allocation/templates/allocation/allocation_attribute_change.html +++ b/coldfront/core/allocation/templates/allocation/allocation_attribute_change.html @@ -10,7 +10,7 @@ {% block content %} -

Request Allocation Attribute Change
Project: {{ project.title }}

+

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


The following {% settings_value 'CENTER_NAME' %} @@ -19,149 +19,7 @@

Request Allocation Attribute Change
Project: {{ project.title }}< 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.

-
- {% csrf_token %} - {{form |crispy}} - - - - - Back to Project
-
- - - - {% endblock %} diff --git a/coldfront/core/allocation/templates/allocation/allocation_detail.html b/coldfront/core/allocation/templates/allocation/allocation_detail.html index 8bbfde6e7..20dbd7348 100644 --- a/coldfront/core/allocation/templates/allocation/allocation_detail.html +++ b/coldfront/core/allocation/templates/allocation/allocation_detail.html @@ -28,7 +28,20 @@

Allocation Detail

-

Allocation Information

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

Allocation Information

+
+ +
+ {% else %} +

Allocation Information

+ {% endif %}
@@ -92,11 +105,6 @@

Allocation Information

{% endif %} - {% if request.user.userprofile.is_pi or request.user.role.name == "Manager" %} - - Request Change - - {% endif %} @@ -172,11 +180,6 @@

Alloc {{attribute}} {{attribute.value}} - {% if request.user.userprofile.is_pi and attribute.allocation_attribute_type.is_changeable or request.user.role.name == "Manager" and attribute.allocation_attribute_type.is_changeable %} - - Request Change - - {% endif %} {% endif %} diff --git a/coldfront/core/allocation/urls.py b/coldfront/core/allocation/urls.py index e5a1802a2..198a80e6c 100644 --- a/coldfront/core/allocation/urls.py +++ b/coldfront/core/allocation/urls.py @@ -22,7 +22,7 @@ name='allocation-renew'), path('/allocationattribute/add', allocation_views.AllocationAttributeCreateView.as_view(), name='allocation-attribute-add'), - path('project//change-attribute', + path('/change-attribute', allocation_views.AllocationAttributeChangeView.as_view(), name='allocation-attribute-change'), path('/allocationattribute/delete', allocation_views.AllocationAttributeDeleteView.as_view(), name='allocation-attribute-delete'), diff --git a/coldfront/core/allocation/views.py b/coldfront/core/allocation/views.py index d9943ad1b..cf32e4509 100644 --- a/coldfront/core/allocation/views.py +++ b/coldfront/core/allocation/views.py @@ -26,6 +26,7 @@ from coldfront.core.allocation.forms import (AllocationAccountForm, AllocationAddUserForm, AllocationAttributeDeleteForm, + AllocationAttributeChangeForm, AllocationForm, AllocationInvoiceNoteDeleteForm, AllocationInvoiceUpdateForm, @@ -632,47 +633,46 @@ def get_success_url(self): return reverse('project-detail', kwargs={'pk': self.kwargs.get('project_pk')}) class AllocationAttributeChangeView(LoginRequiredMixin, UserPassesTestMixin, FormView): - form_class = AllocationForm - template_name = 'allocation/allocation_create.html' + 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) - project_obj = get_object_or_404( - Project, pk=self.kwargs.get('project_pk')) - - if project_obj.pi == self.request.user: + if allocation_obj.project.pi == self.request.user: return True - if project_obj.projectuser_set.filter(user=self.request.user, role__name='Manager', status__name='Active').exists(): + 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 create a new allocation.') + self.request, 'You do not have permission to request changes to this allocation.') def dispatch(self, request, *args, **kwargs): - project_obj = get_object_or_404( - Project, pk=self.kwargs.get('project_pk')) + allocation_obj = get_object_or_404(Allocation, pk=self.kwargs.get('pk')) - if project_obj.needs_review: + if allocation_obj.project.needs_review: messages.error( - request, 'You cannot request a new allocation because you have to review your project first.') - return HttpResponseRedirect(reverse('project-detail', kwargs={'pk': project_obj.pk})) + 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 project_obj.status.name not in ['Active', 'New', ]: + if allocation_obj.project.status.name not in ['Active', 'New', ]: messages.error( - request, 'You cannot request a new allocation to an archived project.') - return HttpResponseRedirect(reverse('project-detail', kwargs={'pk': project_obj.pk})) + 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) - project_obj = get_object_or_404( - Project, pk=self.kwargs.get('project_pk')) - context['project'] = project_obj + 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 = {} @@ -706,12 +706,12 @@ 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('project_pk'), **self.get_form_kwargs()) + return form_class(self.request.user, self.kwargs.get('pk'), **self.get_form_kwargs()) def form_valid(self, form): form_data = form.cleaned_data - project_obj = get_object_or_404( - Project, pk=self.kwargs.get('project_pk')) + 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) @@ -724,12 +724,12 @@ def form_valid(self, form): return self.form_invalid(form) usernames = form_data.get('users') - usernames.append(project_obj.pi.username) + usernames.append(allocation_obj.project.pi.username) usernames = list(set(usernames)) users = [User.objects.get(username=username) for username in usernames] - if project_obj.pi not in users: - users.append(project_obj.pi) + 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( @@ -792,7 +792,8 @@ def form_valid(self, form): return super().form_valid(form) def get_success_url(self): - return reverse('project-detail', kwargs={'pk': self.kwargs.get('project_pk')}) + 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): From 751f4c6a392907d5effa750d7212f2839568f3fa Mon Sep 17 00:00:00 2001 From: Brian Scorcia Date: Fri, 29 Oct 2021 15:14:02 -0400 Subject: [PATCH 03/15] #294: Allocation change flow complete --- coldfront/core/allocation/admin.py | 19 + coldfront/core/allocation/forms.py | 65 +- .../commands/add_allocation_defaults.py | 4 + .../migrations/0003_auto_20191018_1049.py | 10 - .../migrations/0004_auto_20211029_0412.py | 125 ++++ coldfront/core/allocation/models.py | 42 +- .../allocation_attribute_change.html | 25 - .../allocation/allocation_change.html | 137 ++++ .../allocation/allocation_change_detail.html | 200 ++++++ .../allocation/allocation_change_list.html | 72 +++ .../allocation/allocation_detail.html | 67 +- coldfront/core/allocation/urls.py | 12 +- coldfront/core/allocation/views.py | 591 +++++++++++++----- .../templates/project/project_detail.html | 2 +- coldfront/templates/common/navbar_admin.html | 2 + .../email/allocation_change_approved.txt | 10 + .../email/allocation_change_denied.txt | 10 + 17 files changed, 1168 insertions(+), 225 deletions(-) create mode 100644 coldfront/core/allocation/migrations/0004_auto_20211029_0412.py delete mode 100644 coldfront/core/allocation/templates/allocation/allocation_attribute_change.html create mode 100644 coldfront/core/allocation/templates/allocation/allocation_change.html create mode 100644 coldfront/core/allocation/templates/allocation/allocation_change_detail.html create mode 100644 coldfront/core/allocation/templates/allocation/allocation_change_list.html create mode 100644 coldfront/templates/email/allocation_change_approved.txt create mode 100644 coldfront/templates/email/allocation_change_denied.txt 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}} From ff755432ca42912b76d8e4c5e2e298f79f621fd2 Mon Sep 17 00:00:00 2001 From: Brian Scorcia Date: Tue, 2 Nov 2021 10:30:24 -0400 Subject: [PATCH 04/15] #294: Recreated allocation change request migration file so that the 'is_changable' field is explicitly added (not having this was causing issues for existing ColdFront installations) --- coldfront/core/allocation/migrations/0001_initial.py | 2 -- ...o_20211029_0412.py => 0004_auto_20211102_1017.py} | 12 +++++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) rename coldfront/core/allocation/migrations/{0004_auto_20211029_0412.py => 0004_auto_20211102_1017.py} (94%) diff --git a/coldfront/core/allocation/migrations/0001_initial.py b/coldfront/core/allocation/migrations/0001_initial.py index 49d584add..22ab23d55 100644 --- a/coldfront/core/allocation/migrations/0001_initial.py +++ b/coldfront/core/allocation/migrations/0001_initial.py @@ -60,7 +60,6 @@ class Migration(migrations.Migration): ('is_required', models.BooleanField(default=False)), ('is_unique', models.BooleanField(default=False)), ('is_private', models.BooleanField(default=True)), - ('is_changeable', models.BooleanField(default=False)), ], options={ 'ordering': ['name'], @@ -167,7 +166,6 @@ class Migration(migrations.Migration): ('is_required', models.BooleanField(default=False)), ('is_unique', models.BooleanField(default=False)), ('is_private', models.BooleanField(default=True)), - ('is_changeable', models.BooleanField(default=False)), ('history_id', models.AutoField(primary_key=True, serialize=False)), ('history_date', models.DateTimeField()), ('history_change_reason', models.CharField(max_length=100, null=True)), diff --git a/coldfront/core/allocation/migrations/0004_auto_20211029_0412.py b/coldfront/core/allocation/migrations/0004_auto_20211102_1017.py similarity index 94% rename from coldfront/core/allocation/migrations/0004_auto_20211029_0412.py rename to coldfront/core/allocation/migrations/0004_auto_20211102_1017.py index b86ee1da5..8a5b95d0a 100644 --- a/coldfront/core/allocation/migrations/0004_auto_20211029_0412.py +++ b/coldfront/core/allocation/migrations/0004_auto_20211102_1017.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.18 on 2021-10-29 08:12 +# Generated by Django 2.2.18 on 2021-11-02 14:17 from django.conf import settings from django.db import migrations, models @@ -47,11 +47,21 @@ class Migration(migrations.Migration): name='is_changeable', field=models.BooleanField(default=False), ), + migrations.AddField( + model_name='allocationattributetype', + name='is_changeable', + field=models.BooleanField(default=False), + ), migrations.AddField( model_name='historicalallocation', name='is_changeable', field=models.BooleanField(default=False), ), + migrations.AddField( + model_name='historicalallocationattributetype', + name='is_changeable', + field=models.BooleanField(default=False), + ), migrations.CreateModel( name='HistoricalAllocationChangeRequest', fields=[ From 0f57306670ec641cc6857df6d8f06bd8932fe61b Mon Sep 17 00:00:00 2001 From: Brian Scorcia Date: Fri, 5 Nov 2021 14:25:44 -0400 Subject: [PATCH 05/15] #294: Addressed PR comments. The 'request change' button on the 'Allocation Detail' page now only appears if the allocation's 'is_changeable' field is set to true. A user can also only access the 'Allocation Change' form if the allocation has an acceptable status (almost everything besides 'Expired') --- .../allocation/templates/allocation/allocation_detail.html | 2 +- coldfront/core/allocation/views.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/coldfront/core/allocation/templates/allocation/allocation_detail.html b/coldfront/core/allocation/templates/allocation/allocation_detail.html index 498358ebc..b3038e139 100644 --- a/coldfront/core/allocation/templates/allocation/allocation_detail.html +++ b/coldfront/core/allocation/templates/allocation/allocation_detail.html @@ -28,7 +28,7 @@

Allocation Detail

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

Allocation Information

diff --git a/coldfront/core/allocation/views.py b/coldfront/core/allocation/views.py index 416e3ef80..e8232f3de 100644 --- a/coldfront/core/allocation/views.py +++ b/coldfront/core/allocation/views.py @@ -1768,6 +1768,11 @@ def dispatch(self, request, *args, **kwargs): request, 'You cannot request a change to a locked allocation.') return HttpResponseRedirect(reverse('allocation-detail', kwargs={'pk': allocation_obj.pk})) + if allocation_obj.status.name not in ['Active', 'New', 'Renewal Requested', 'Payment Pending', 'Payment Requested', 'Paid']: + messages.error(request, 'You cannot request a change to an allocation with status {}.'.format( + allocation_obj.status.name)) + 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): From 44abfbfec259cda49c787d5268760a40bda21e61 Mon Sep 17 00:00:00 2001 From: Brian Scorcia Date: Thu, 11 Nov 2021 15:44:37 -0500 Subject: [PATCH 06/15] #294: Created new ColdFront config vars that allow users to decide whether or not to allow allocation change requests by default and to customize the list of days users can request allocation extensions --- coldfront/config/core.py | 2 ++ coldfront/core/allocation/forms.py | 7 ++++++- coldfront/core/allocation/views.py | 3 +++ docs/pages/config.md | 2 ++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/coldfront/config/core.py b/coldfront/config/core.py index 2823494fb..3c68dfaa2 100644 --- a/coldfront/config/core.py +++ b/coldfront/config/core.py @@ -21,6 +21,8 @@ #------------------------------------------------------------------------------ # Allocation related #------------------------------------------------------------------------------ +ALLOCATION_ENABLE_CHANGE_REQUESTS_BY_DEFAULT = ENV.bool('ALLOCATION_ENABLE_CHANGE_REQUESTS', default=True) +ALLOCATION_CHANGE_REQUEST_EXTENSION_DAYS = ENV.list('ALLOCATION_CHANGE_REQUEST_EXTENSION_DAYS', cast=int, default=[30, 60, 90]) ALLOCATION_ENABLE_ALLOCATION_RENEWAL = ENV.bool('ALLOCATION_ENABLE_ALLOCATION_RENEWAL', default=True) ALLOCATION_FUNCS_ON_EXPIRE = ['coldfront.core.allocation.utils.test_allocation_function', ] diff --git a/coldfront/core/allocation/forms.py b/coldfront/core/allocation/forms.py index 5ed1fcc68..4165c38de 100644 --- a/coldfront/core/allocation/forms.py +++ b/coldfront/core/allocation/forms.py @@ -12,6 +12,8 @@ ALLOCATION_ACCOUNT_ENABLED = import_from_settings( 'ALLOCATION_ACCOUNT_ENABLED', False) +ALLOCATION_CHANGE_REQUEST_EXTENSION_DAYS = import_from_settings( + 'ALLOCATION_CHANGE_REQUEST_EXTENSION_DAYS', []) class AllocationForm(forms.Form): @@ -198,8 +200,11 @@ def clean(self): class AllocationChangeForm(forms.Form): EXTENSION_CHOICES = [ - (0, "----"), (30, "30 days"), (60, "60 days"), (90, "90 days") + (0, "----") ] + for choice in ALLOCATION_CHANGE_REQUEST_EXTENSION_DAYS: + EXTENSION_CHOICES.append((choice, "{} days".format(choice))) + end_date_extension = forms.TypedChoiceField( label='Request End Date Extension', choices = EXTENSION_CHOICES, diff --git a/coldfront/core/allocation/views.py b/coldfront/core/allocation/views.py index e8232f3de..8d73e79bb 100644 --- a/coldfront/core/allocation/views.py +++ b/coldfront/core/allocation/views.py @@ -62,6 +62,8 @@ 'ALLOCATION_ENABLE_ALLOCATION_RENEWAL', True) ALLOCATION_DEFAULT_ALLOCATION_LENGTH = import_from_settings( 'ALLOCATION_DEFAULT_ALLOCATION_LENGTH', 365) +ALLOCATION_ENABLE_CHANGE_REQUESTS_BY_DEFAULT = import_from_settings( + 'ALLOCATION_ENABLE_CHANGE_REQUESTS_BY_DEFAULT', True) EMAIL_ENABLED = import_from_settings('EMAIL_ENABLED', False) if EMAIL_ENABLED: @@ -1060,6 +1062,7 @@ def get(self, request, pk): allocation_obj.status = allocation_status_active_obj allocation_obj.start_date = start_date allocation_obj.end_date = end_date + if ALLOCATION_ENABLE_CHANGE_REQUESTS_BY_DEFAULT: allocation_obj.is_changeable = True allocation_obj.save() messages.success(request, 'Allocation to {} has been ACTIVATED for {} {} ({})'.format( diff --git a/docs/pages/config.md b/docs/pages/config.md index 997983293..f2300f7ed 100644 --- a/docs/pages/config.md +++ b/docs/pages/config.md @@ -86,6 +86,8 @@ The following settings are ColdFront specific settings related to the core appli | PROJECT_ENABLE_PROJECT_REVIEW | Enable or disable project reviews. Default True| | ALLOCATION_ENABLE_ALLOCATION_RENEWAL | Enable or disable allocation renewals. Default True | | ALLOCATION_DEFAULT_ALLOCATION_LENGTH | Default number of days an allocation is active for. Default 365 | +| ALLOCATION_ENABLE_CHANGE_REQUESTS_BY_DEFAULT | Enable or disable allocation change requests. Default True | +| ALLOCATION_CHANGE_REQUEST_EXTENSION_DAYS | List of days users can request extensions in an allocation change request. Default 30,60,90 | | ALLOCATION_ACCOUNT_ENABLED | Allow user to select account name for allocation. Default False | | INVOICE_ENABLED | Enable or disable invoices. Default True | | ONDEMAND_URL | The URL to your Open OnDemand installation | From fc4cc3711742a4f826b06c6979ef5480366827b6 Mon Sep 17 00:00:00 2001 From: Brian Scorcia Date: Tue, 16 Nov 2021 15:38:00 -0500 Subject: [PATCH 07/15] #294: Fixed allocation change form submission issue ('ManagementForm data is missing or has been tampered with'). Also balanced form tag in allocation_change.html to fix invalid HTML error --- .../allocation/allocation_change.html | 4 +- coldfront/core/allocation/views.py | 122 +++++++++++------- 2 files changed, 79 insertions(+), 47 deletions(-) diff --git a/coldfront/core/allocation/templates/allocation/allocation_change.html b/coldfront/core/allocation/templates/allocation/allocation_change.html index eed9c99e1..a7128649f 100644 --- a/coldfront/core/allocation/templates/allocation/allocation_change.html +++ b/coldfront/core/allocation/templates/allocation/allocation_change.html @@ -18,13 +18,13 @@

Request change to {{ allocation.get_parent_resource }} for project: {{ alloc you must provide a justification.

+

Allocation Information

- {% csrf_token %}
@@ -130,7 +130,7 @@

Alloc Back to Allocation
- + diff --git a/coldfront/core/allocation/views.py b/coldfront/core/allocation/views.py index 8d73e79bb..9eac1ae8a 100644 --- a/coldfront/core/allocation/views.py +++ b/coldfront/core/allocation/views.py @@ -1827,65 +1827,97 @@ def post(self, request, *args, **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 allocation_attributes_to_change: + 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.is_valid() and formset.is_valid(): + form_data = form.cleaned_data + + if form_data.get('end_date_extension') != 0: change_requested = True - if form_data.get('end_date_extension') != 0: change_requested = True + for entry in formset: + formset_data = entry.cleaned_data - for entry in formset: - formset_data = entry.cleaned_data + new_value = formset_data.get('new_value') + + if new_value != "": + change_requested = True - new_value = formset_data.get('new_value') + allocation_attribute = AllocationAttribute.objects.get(pk=formset_data.get('pk')) + attribute_changes_to_make.add((allocation_attribute, new_value)) + + if change_requested == True: - if new_value != "": - 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_attribute = AllocationAttribute.objects.get(pk=formset_data.get('pk')) - attribute_changes_to_make.add((allocation_attribute, new_value)) + allocation_change_request_obj = AllocationChangeRequest.objects.create( + allocation=allocation_obj, + end_date_extension=end_date_extension, + justification=justification, + status=change_request_status_obj + ) - if change_requested == True: + 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})) - end_date_extension = form_data.get('end_date_extension') - justification = form_data.get('justification') + 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})) + else: + if form.is_valid(): + form_data = form.cleaned_data - change_request_status_obj = AllocationChangeStatusChoice.objects.get( - name='Pending') + if form_data.get('end_date_extension') != 0: + + end_date_extension = form_data.get('end_date_extension') + justification = form_data.get('justification') - allocation_change_request_obj = AllocationChangeRequest.objects.create( - allocation=allocation_obj, - end_date_extension=end_date_extension, - justification=justification, - status=change_request_status_obj - ) + change_request_status_obj = AllocationChangeStatusChoice.objects.get( + name='Pending') - 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] + allocation_change_request_obj = AllocationChangeRequest.objects.create( + allocation=allocation_obj, + end_date_extension=end_date_extension, + justification=justification, + status=change_request_status_obj ) - messages.success( - request, 'Allocation change request successfully submitted.') - - return HttpResponseRedirect(reverse('allocation-detail', kwargs={'pk': pk})) - + 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: - messages.error(request, 'You must request a change.') + for error in form.errors: + messages.error(request, error) 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): From 43f9c4b4185d89909404db9eb284123b4be24bc8 Mon Sep 17 00:00:00 2001 From: Brian Scorcia Date: Wed, 17 Nov 2021 14:37:12 -0500 Subject: [PATCH 08/15] #294: Created a data migration file so that existing ColdFront instances, can get the allocation change status choices loaded into their application automatically --- .../migrations/0005_auto_20211117_1413.py | 20 +++++++++++++++++++ coldfront/core/allocation/models.py | 7 +++++++ 2 files changed, 27 insertions(+) create mode 100644 coldfront/core/allocation/migrations/0005_auto_20211117_1413.py diff --git a/coldfront/core/allocation/migrations/0005_auto_20211117_1413.py b/coldfront/core/allocation/migrations/0005_auto_20211117_1413.py new file mode 100644 index 000000000..7512c62f2 --- /dev/null +++ b/coldfront/core/allocation/migrations/0005_auto_20211117_1413.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.24 on 2021-11-17 19:13 + +from django.db import migrations + + +def create_status_choices(apps, schema_editor): + AllocationChangeStatusChoice = apps.get_model('allocation', "AllocationChangeStatusChoice") + for choice in ('Pending', 'Approved', 'Denied',): + AllocationChangeStatusChoice.objects.get_or_create(name=choice) + + +class Migration(migrations.Migration): + + dependencies = [ + ('allocation', '0004_auto_20211102_1017'), + ] + + operations = [ + migrations.RunPython(create_status_choices), + ] diff --git a/coldfront/core/allocation/models.py b/coldfront/core/allocation/models.py index 92512bd59..d70893130 100644 --- a/coldfront/core/allocation/models.py +++ b/coldfront/core/allocation/models.py @@ -325,6 +325,13 @@ class AllocationChangeRequest(TimeStampedModel): notes = models.CharField(max_length=512, blank=True, null=True) history = HistoricalRecords() + @property + def get_parent_resource(self): + if self.allocation.resources.count() == 1: + return self.allocation.resources.first() + else: + return self.allocation.resources.filter(is_allocatable=True).first() + def __str__(self): return "%s (%s)" % (self.get_parent_resource.name, self.allocation.project.pi) From 64eebdefcd61f6aa56979b2b1fbfc6d185ab8ca1 Mon Sep 17 00:00:00 2001 From: Brian Scorcia Date: Wed, 17 Nov 2021 15:35:34 -0500 Subject: [PATCH 09/15] #294: Updated the Allocation Update Form in the Allocation Detail page template to include the new 'is changeable' field --- coldfront/core/allocation/forms.py | 1 + .../templates/allocation/allocation_detail.html | 11 +++++++++++ coldfront/core/allocation/views.py | 8 ++++++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/coldfront/core/allocation/forms.py b/coldfront/core/allocation/forms.py index 4165c38de..3d3c7129b 100644 --- a/coldfront/core/allocation/forms.py +++ b/coldfront/core/allocation/forms.py @@ -67,6 +67,7 @@ class AllocationUpdateForm(forms.Form): description = forms.CharField(max_length=512, label='Description', required=False) + is_changeable = forms.BooleanField(required=False) def clean(self): cleaned_data = super().clean() diff --git a/coldfront/core/allocation/templates/allocation/allocation_detail.html b/coldfront/core/allocation/templates/allocation/allocation_detail.html index b3038e139..6a18b5634 100644 --- a/coldfront/core/allocation/templates/allocation/allocation_detail.html +++ b/coldfront/core/allocation/templates/allocation/allocation_detail.html @@ -135,6 +135,17 @@

Allocation Information

{% endif %} + + + + +
Is Changeable + {% if request.user.is_superuser %} + {{ form.is_changeable }} + {% else %} + + {% endif %} +
{% if request.user.is_superuser %} diff --git a/coldfront/core/allocation/views.py b/coldfront/core/allocation/views.py index 9eac1ae8a..d0670a84e 100644 --- a/coldfront/core/allocation/views.py +++ b/coldfront/core/allocation/views.py @@ -204,7 +204,8 @@ def get(self, request, *args, **kwargs): 'status': allocation_obj.status, 'end_date': allocation_obj.end_date, 'start_date': allocation_obj.start_date, - 'description': allocation_obj.description + 'description': allocation_obj.description, + 'is_changeable': allocation_obj.is_changeable, } form = AllocationUpdateForm(initial=initial_data) @@ -227,7 +228,8 @@ def post(self, request, *args, **kwargs): 'status': allocation_obj.status, 'end_date': allocation_obj.end_date, 'start_date': allocation_obj.start_date, - 'description': allocation_obj.description + 'description': allocation_obj.description, + 'is_changeable': allocation_obj.is_changeable, } form = AllocationUpdateForm(request.POST, initial=initial_data) @@ -236,6 +238,7 @@ def post(self, request, *args, **kwargs): end_date = form_data.get('end_date') start_date = form_data.get('start_date') description = form_data.get('description') + is_changeable = form_data.get('is_changeable') allocation_obj.description = description allocation_obj.save() @@ -252,6 +255,7 @@ def post(self, request, *args, **kwargs): new_status = form_data.get('status').name allocation_obj.status = form_data.get('status') + allocation_obj.is_changeable = is_changeable allocation_obj.save() if EMAIL_ENABLED: From 254f15061a1f2ac2fec6326df435cd9c3e042663 Mon Sep 17 00:00:00 2001 From: Brian Scorcia Date: Tue, 23 Nov 2021 14:57:03 -0500 Subject: [PATCH 10/15] Made several requested updates/bug fixes such as updating the allocation change list format, fixing the 'Request Change' button functionality on the 'Allocation Detail' template, and updating who can see the 'is changeable' field on the 'Allocation Detail' Template --- .../allocation/allocation_change_list.html | 30 ++++++++----------- .../allocation/allocation_detail.html | 21 ++++++------- coldfront/core/allocation/views.py | 8 +++-- 3 files changed, 28 insertions(+), 31 deletions(-) diff --git a/coldfront/core/allocation/templates/allocation/allocation_change_list.html b/coldfront/core/allocation/templates/allocation/allocation_change_list.html index 68cef555d..376db2125 100644 --- a/coldfront/core/allocation/templates/allocation/allocation_change_list.html +++ b/coldfront/core/allocation/templates/allocation/allocation_change_list.html @@ -2,16 +2,11 @@ {% 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 %}
@@ -23,8 +18,7 @@

Allocation Change Requests

- - + @@ -41,18 +35,21 @@

Allocation Change Requests

{% else %} {{change.end_date_extension}} days {% endif %} - - {% endfor %} @@ -63,10 +60,9 @@

Allocation Change Requests

No new or pending allocation change requests! {% endif %} - -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/coldfront/core/allocation/templates/allocation/allocation_detail.html b/coldfront/core/allocation/templates/allocation/allocation_detail.html index 6a18b5634..91a507c2a 100644 --- a/coldfront/core/allocation/templates/allocation/allocation_detail.html +++ b/coldfront/core/allocation/templates/allocation/allocation_detail.html @@ -28,7 +28,7 @@

Allocation Detail

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

Allocation Information

@@ -135,17 +135,14 @@

Allocation Information

{% endif %} - - - - - + {% if request.user.is_superuser or request.user.is_staff %} + + + + + {% endif %}
PI Resource ExtensionChangesQuick ActionsActions
+ {% if change.allocationattributechangerequest_set.all %} - + + Approve + + Details {% else %} - + Approve + Details {% endif %} - Approve - Deny - Details -
Is Changeable - {% if request.user.is_superuser %} - {{ form.is_changeable }} - {% else %} - - {% endif %} -
Allow Change Requests + {{ form.is_changeable }} +
{% if request.user.is_superuser %} diff --git a/coldfront/core/allocation/views.py b/coldfront/core/allocation/views.py index d0670a84e..eea672373 100644 --- a/coldfront/core/allocation/views.py +++ b/coldfront/core/allocation/views.py @@ -611,6 +611,11 @@ def form_valid(self, form): quantity=quantity, status=allocation_status_obj ) + + if ALLOCATION_ENABLE_CHANGE_REQUESTS_BY_DEFAULT: + allocation_obj.is_changeable = True + allocation_obj.save() + allocation_obj.resources.add(resource_obj) if ALLOCATION_ACCOUNT_ENABLED and allocation_account and resource_obj.name in ALLOCATION_ACCOUNT_MAPPING: @@ -1066,7 +1071,6 @@ def get(self, request, pk): allocation_obj.status = allocation_status_active_obj allocation_obj.start_date = start_date allocation_obj.end_date = end_date - if ALLOCATION_ENABLE_CHANGE_REQUESTS_BY_DEFAULT: allocation_obj.is_changeable = True allocation_obj.save() messages.success(request, 'Allocation to {} has been ACTIVATED for {} {} ({})'.format( @@ -1430,7 +1434,7 @@ def post(self, request, *args, **kwargs): return HttpResponseRedirect(reverse('allocation-invoice-detail', kwargs={'pk': pk})) -class AllocationAddInvoiceNoteView(LoginRequiredMixin, UserPassesTestMixin, CreateView): +class AllocationAddInvoiceNoteView(LoginRequiredMixin, UserPassesTestMixin, View): model = AllocationUserNote template_name = 'allocation/allocation_add_invoice_note.html' fields = ('is_private', 'note',) From d2881d74b070980a96cf08fdfaa989171c486582 Mon Sep 17 00:00:00 2001 From: Brian Scorcia Date: Tue, 23 Nov 2021 15:00:03 -0500 Subject: [PATCH 11/15] #294: Patching up a line in the allocation views.py file that was accidentally edited --- .../templates/allocation/allocation_change_list.html | 8 +++++++- coldfront/core/allocation/views.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/coldfront/core/allocation/templates/allocation/allocation_change_list.html b/coldfront/core/allocation/templates/allocation/allocation_change_list.html index 376db2125..7492595ac 100644 --- a/coldfront/core/allocation/templates/allocation/allocation_change_list.html +++ b/coldfront/core/allocation/templates/allocation/allocation_change_list.html @@ -2,11 +2,16 @@ {% 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 %}
@@ -60,9 +65,10 @@

Allocation Change Requests

No new or pending allocation change requests! {% endif %} + -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/coldfront/core/allocation/views.py b/coldfront/core/allocation/views.py index eea672373..e819ea02f 100644 --- a/coldfront/core/allocation/views.py +++ b/coldfront/core/allocation/views.py @@ -1434,7 +1434,7 @@ def post(self, request, *args, **kwargs): return HttpResponseRedirect(reverse('allocation-invoice-detail', kwargs={'pk': pk})) -class AllocationAddInvoiceNoteView(LoginRequiredMixin, UserPassesTestMixin, View): +class AllocationAddInvoiceNoteView(LoginRequiredMixin, UserPassesTestMixin, CreateView): model = AllocationUserNote template_name = 'allocation/allocation_add_invoice_note.html' fields = ('is_private', 'note',) From 08a44707ba0df09985b34cc32ab02ae4568b5a80 Mon Sep 17 00:00:00 2001 From: Brian Scorcia Date: Wed, 1 Dec 2021 13:06:30 -0500 Subject: [PATCH 12/15] #294: Updated admin form in Allocation Change Detail template to post notes when a change is approved/denied --- .../allocation/allocation_change_detail.html | 6 +- coldfront/core/allocation/views.py | 131 ++++++++++++++++-- 2 files changed, 126 insertions(+), 11 deletions(-) diff --git a/coldfront/core/allocation/templates/allocation/allocation_change_detail.html b/coldfront/core/allocation/templates/allocation/allocation_change_detail.html index bef9dd8fb..35a6029b4 100644 --- a/coldfront/core/allocation/templates/allocation/allocation_change_detail.html +++ b/coldfront/core/allocation/templates/allocation/allocation_change_detail.html @@ -174,10 +174,10 @@

Actio {{note_form.notes | as_crispy_field}}
{% if allocation_change.status.name == 'Pending' %} - Approve - Deny + + {% endif %} -
diff --git a/coldfront/core/allocation/views.py b/coldfront/core/allocation/views.py index e819ea02f..192a879e7 100644 --- a/coldfront/core/allocation/views.py +++ b/coldfront/core/allocation/views.py @@ -1689,19 +1689,134 @@ def post(self, request, *args, **kwargs): 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') + if request.POST.get('choice') == 'approve': + form_data = note_form.cleaned_data + notes = form_data.get('notes') - allocation_change_obj.notes = notes - allocation_change_obj.save() + allocation_change_obj.notes = notes - messages.success( - request, 'Allocation change request updated!') - return HttpResponseRedirect(reverse('allocation-change-detail', kwargs={'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-detail', kwargs={'pk': pk})) + + + if request.POST.get('choice') == 'deny': + form_data = note_form.cleaned_data + notes = form_data.get('notes') + + allocation_change_obj.notes = notes + + 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-detail', kwargs={'pk': pk})) + + if request.POST.get('choice') == 'update': + 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}) From 3754baca209adf698f3751952949cd66d6f8c798 Mon Sep 17 00:00:00 2001 From: Brian Scorcia Date: Mon, 20 Dec 2021 13:40:09 -0500 Subject: [PATCH 13/15] #294: Allocation detail page now has a mechanism for admins to edit allocation change requests --- coldfront/core/allocation/forms.py | 1 + .../allocation/allocation_change_detail.html | 29 ++- coldfront/core/allocation/views.py | 214 ++++++++++++------ 3 files changed, 165 insertions(+), 79 deletions(-) diff --git a/coldfront/core/allocation/forms.py b/coldfront/core/allocation/forms.py index 3d3c7129b..32095d377 100644 --- a/coldfront/core/allocation/forms.py +++ b/coldfront/core/allocation/forms.py @@ -215,6 +215,7 @@ class AllocationChangeForm(forms.Form): justification = forms.CharField( label='Justification for Changes', widget=forms.Textarea, + required=False, help_text='Justification for requesting this allocation change request.') def __init__(self, *args, **kwargs): diff --git a/coldfront/core/allocation/templates/allocation/allocation_change_detail.html b/coldfront/core/allocation/templates/allocation/allocation_change_detail.html index 35a6029b4..890404c1f 100644 --- a/coldfront/core/allocation/templates/allocation/allocation_change_detail.html +++ b/coldfront/core/allocation/templates/allocation/allocation_change_detail.html @@ -39,7 +39,7 @@

{{allocation_change.status.name}}

{% endif %} - +

Allocation Information

@@ -133,16 +133,23 @@

Alloc

- {% for attribute in attribute_changes %} + {% for form in formset %} - + {% if allocation_change.status.name == 'Pending' %} - - {% endif %} - {% if attribute.new_value == '' %} - + + {% else %} - + {% if form.new_value.value == '' %} + + {% else %} + + {% endif %} {% endif %} {% endfor %} @@ -155,6 +162,7 @@

Alloc There are no requested allocation attribute changes to display. {% endif %} + {{ formset.management_form }} @@ -169,7 +177,7 @@

Actio
- + {% csrf_token %} {{note_form.notes | as_crispy_field}}
@@ -181,11 +189,12 @@

Actio Update

-
{% endif %} + + View Allocation diff --git a/coldfront/core/allocation/views.py b/coldfront/core/allocation/views.py index 192a879e7..584f32f64 100644 --- a/coldfront/core/allocation/views.py +++ b/coldfront/core/allocation/views.py @@ -1614,6 +1614,7 @@ def get_queryset(self): class AllocationChangeDetailView(LoginRequiredMixin, UserPassesTestMixin, FormView): + formset_class = AllocationAttributeChangeForm template_name = 'allocation/allocation_change_detail.html' def test_func(self): @@ -1644,16 +1645,42 @@ def test_func(self): return False + + def get_allocation_attributes_to_change(self, allocation_change_obj): + attributes_to_change = allocation_change_obj.allocationattributechangerequest_set.all() + + attributes_to_change = [ + + {'pk': attribute_change.pk, + 'name': attribute_change.allocation_attribute.allocation_attribute_type.name, + 'value': attribute_change.allocation_attribute.value, + 'new_value': attribute_change.new_value, + } + + for attribute_change in attributes_to_change + ] + + return attributes_to_change + 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() + + + allocation_attributes_to_change = self.get_allocation_attributes_to_change( + allocation_change_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_change'] = allocation_change_obj - context['attribute_changes'] = attribute_changes + context['attribute_changes'] = allocation_attributes_to_change return context @@ -1666,7 +1693,8 @@ def get(self, request, *args, **kwargs): 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 + if allocation_change_obj.status.name != 'Pending': + allocation_change_form.fields['end_date_extension'].disabled = True note_form = AllocationChangeNoteForm( initial={'notes': allocation_change_obj.notes}) @@ -1678,6 +1706,7 @@ def get(self, request, *args, **kwargs): 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) @@ -1686,82 +1715,105 @@ def post(self, request, *args, **kwargs): 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) + allocation_change_form = AllocationChangeForm(request.POST, + initial={'justification': allocation_change_obj.justification, + 'end_date_extension': allocation_change_obj.end_date_extension}) - if note_form.is_valid(): - if request.POST.get('choice') == 'approve': - form_data = note_form.cleaned_data - notes = form_data.get('notes') + allocation_attributes_to_change = self.get_allocation_attributes_to_change( + allocation_change_obj) + + print('length of allocation att list: ' + str(len(allocation_attributes_to_change))) - allocation_change_obj.notes = notes + if allocation_attributes_to_change: + formset = formset_factory(self.formset_class, max_num=len( + allocation_attributes_to_change)) + formset = formset( + request.POST, initial=allocation_attributes_to_change, prefix='attributeform') - allocation_change_status_active_obj = AllocationChangeStatusChoice.objects.get( - name='Approved') - - allocation_change_obj.status = allocation_change_status_active_obj + note_form = AllocationChangeNoteForm( + request.POST, initial={'notes': allocation_change_obj.notes}) - 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) + if note_form.is_valid(): + notes = note_form.cleaned_data.get('notes') - allocation_change_obj.allocation.end_date = new_end_date + if request.POST.get('choice') == 'approve': + if allocation_change_form.is_valid() and formset.is_valid(): + allocation_change_obj.notes = notes - allocation_change_obj.allocation.save() - allocation_change_obj.save() + allocation_change_status_active_obj = AllocationChangeStatusChoice.objects.get( + name='Approved') - attribute_change_list = allocation_change_obj.allocationattributechangerequest_set.all() + allocation_change_obj.status = allocation_change_status_active_obj - for attribute_change in attribute_change_list: - attribute_change.allocation_attribute.value = attribute_change.new_value - attribute_change.allocation_attribute.save() + form_data = allocation_change_form.cleaned_data - 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) - ) + if form_data.get('end_date_extension') != allocation_change_obj.end_date_extension: + allocation_change_obj.end_date_extension = form_data.get('end_date_extension') + new_end_date = allocation_change_obj.allocation.end_date + relativedelta( + days=form_data.get('end_date_extension')) - 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})) + allocation_change_obj.allocation.end_date = new_end_date + allocation_change_obj.allocation.save() - 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 - } + allocation_change_obj.save() - email_receiver_list = [] + for entry in formset: + formset_data = entry.cleaned_data + new_value = formset_data.get('new_value') + + attribute_change = AllocationAttributeChangeRequest.objects.get(pk=formset_data.get('pk')) + + if new_value != attribute_change.new_value: + attribute_change.new_value = new_value + attribute_change.save() - 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) + attribute_change_list = allocation_change_obj.allocationattributechangerequest_set.all() - send_email_template( - 'Allocation Change Approved', - 'email/allocation_change_approved.txt', - template_context, - EMAIL_SENDER, - email_receiver_list + 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) ) - - return HttpResponseRedirect(reverse('allocation-change-detail', kwargs={'pk': pk})) + 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-detail', kwargs={'pk': pk})) - if request.POST.get('choice') == 'deny': - form_data = note_form.cleaned_data - notes = form_data.get('notes') + if request.POST.get('choice') == 'deny': allocation_change_obj.notes = notes allocation_change_status_denied_obj = AllocationChangeStatusChoice.objects.get( @@ -1808,15 +1860,39 @@ def post(self, request, *args, **kwargs): return HttpResponseRedirect(reverse('allocation-change-detail', kwargs={'pk': pk})) if request.POST.get('choice') == 'update': - form_data = note_form.cleaned_data - notes = form_data.get('notes') + if allocation_change_form.is_valid() and formset.is_valid(): + allocation_change_obj.notes = notes - allocation_change_obj.notes = notes - allocation_change_obj.save() + form_data = allocation_change_form.cleaned_data + + if form_data.get('end_date_extension') != allocation_change_obj.end_date_extension: + allocation_change_obj.end_date_extension = form_data.get('end_date_extension') + + + for entry in formset: + formset_data = entry.cleaned_data + new_value = formset_data.get('new_value') + + attribute_change = AllocationAttributeChangeRequest.objects.get(pk=formset_data.get('pk')) + + if new_value != attribute_change.new_value: + attribute_change.new_value = new_value + attribute_change.save() + + allocation_change_obj.save() + + messages.success( + request, 'Allocation change request updated!') + return HttpResponseRedirect(reverse('allocation-change-detail', kwargs={'pk': pk})) + else: + attribute_errors = "" + for error in allocation_change_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-detail', kwargs={'pk': pk})) - 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}) From a94ccc1d41b9fa74be1d6122bbfb7a9027962e4d Mon Sep 17 00:00:00 2001 From: Brian Scorcia Date: Wed, 22 Dec 2021 10:12:18 -0500 Subject: [PATCH 14/15] #294: Allocation change request update form complete --- coldfront/core/allocation/forms.py | 22 +- .../allocation/allocation_change_detail.html | 8 +- coldfront/core/allocation/urls.py | 2 + coldfront/core/allocation/views.py | 319 ++++++++++++------ 4 files changed, 254 insertions(+), 97 deletions(-) diff --git a/coldfront/core/allocation/forms.py b/coldfront/core/allocation/forms.py index 32095d377..1b4967291 100644 --- a/coldfront/core/allocation/forms.py +++ b/coldfront/core/allocation/forms.py @@ -199,9 +199,29 @@ def clean(self): allocation_attribute.clean() +class AllocationAttributeUpdateForm(forms.Form): + change_pk = forms.IntegerField(required=False, disabled=True) + attribute_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['change_pk'].widget = forms.HiddenInput() + self.fields['attribute_pk'].widget = forms.HiddenInput() + + def clean(self): + cleaned_data = super().clean() + allocation_attribute = AllocationAttribute.objects.get(pk=cleaned_data.get('attribute_pk')) + + allocation_attribute.value = cleaned_data.get('new_value') + allocation_attribute.clean() + + class AllocationChangeForm(forms.Form): EXTENSION_CHOICES = [ - (0, "----") + (0, "No Extension") ] for choice in ALLOCATION_CHANGE_REQUEST_EXTENSION_DAYS: EXTENSION_CHOICES.append((choice, "{} days".format(choice))) diff --git a/coldfront/core/allocation/templates/allocation/allocation_change_detail.html b/coldfront/core/allocation/templates/allocation/allocation_change_detail.html index 890404c1f..db622f990 100644 --- a/coldfront/core/allocation/templates/allocation/allocation_change_detail.html +++ b/coldfront/core/allocation/templates/allocation/allocation_change_detail.html @@ -140,7 +140,7 @@

Alloc

@@ -206,4 +206,10 @@

Actio {% endif %}
+ {% endblock %} + diff --git a/coldfront/core/allocation/urls.py b/coldfront/core/allocation/urls.py index 29e378269..21a6d685c 100644 --- a/coldfront/core/allocation/urls.py +++ b/coldfront/core/allocation/urls.py @@ -18,6 +18,8 @@ name='allocation-activate-change'), path('/deny-change-request', allocation_views.AllocationChangeDenyView.as_view(), name='allocation-deny-change'), + path('/delete-attribute-change', allocation_views.AllocationChangeDeleteAttributeView.as_view(), + name='allocation-attribute-change-delete'), path('/add-users', allocation_views.AllocationAddUsersView.as_view(), name='allocation-add-users'), path('/remove-users', allocation_views.AllocationRemoveUsersView.as_view(), diff --git a/coldfront/core/allocation/views.py b/coldfront/core/allocation/views.py index 584f32f64..434b9000f 100644 --- a/coldfront/core/allocation/views.py +++ b/coldfront/core/allocation/views.py @@ -29,6 +29,7 @@ AllocationChangeForm, AllocationChangeNoteForm, AllocationAttributeChangeForm, + AllocationAttributeUpdateForm, AllocationForm, AllocationInvoiceNoteDeleteForm, AllocationInvoiceUpdateForm, @@ -1614,7 +1615,7 @@ def get_queryset(self): class AllocationChangeDetailView(LoginRequiredMixin, UserPassesTestMixin, FormView): - formset_class = AllocationAttributeChangeForm + formset_class = AllocationAttributeUpdateForm template_name = 'allocation/allocation_change_detail.html' def test_func(self): @@ -1651,7 +1652,8 @@ def get_allocation_attributes_to_change(self, allocation_change_obj): attributes_to_change = [ - {'pk': attribute_change.pk, + {'change_pk': attribute_change.pk, + 'attribute_pk': attribute_change.allocation_attribute.pk, 'name': attribute_change.allocation_attribute.allocation_attribute_type.name, 'value': attribute_change.allocation_attribute.value, 'new_value': attribute_change.new_value, @@ -1706,23 +1708,20 @@ def get(self, request, *args, **kwargs): 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') + messages.error( + request, 'You do not have permission to update an allocation change request') return HttpResponseRedirect(reverse('allocation-change-detail', kwargs={'pk': pk})) + allocation_change_obj = get_object_or_404( + AllocationChangeRequest, pk=pk) allocation_change_form = AllocationChangeForm(request.POST, initial={'justification': allocation_change_obj.justification, 'end_date_extension': allocation_change_obj.end_date_extension}) allocation_attributes_to_change = self.get_allocation_attributes_to_change( allocation_change_obj) - - print('length of allocation att list: ' + str(len(allocation_attributes_to_change))) if allocation_attributes_to_change: formset = formset_factory(self.formset_class, max_num=len( @@ -1735,82 +1734,153 @@ def post(self, request, *args, **kwargs): if note_form.is_valid(): notes = note_form.cleaned_data.get('notes') - + if request.POST.get('choice') == 'approve': - if allocation_change_form.is_valid() and formset.is_valid(): - allocation_change_obj.notes = notes + if allocation_attributes_to_change: + if allocation_change_form.is_valid() and formset.is_valid(): + allocation_change_obj.notes = notes - allocation_change_status_active_obj = AllocationChangeStatusChoice.objects.get( - name='Approved') + allocation_change_status_active_obj = AllocationChangeStatusChoice.objects.get( + name='Approved') - allocation_change_obj.status = allocation_change_status_active_obj + allocation_change_obj.status = allocation_change_status_active_obj - form_data = allocation_change_form.cleaned_data + form_data = allocation_change_form.cleaned_data + + if form_data.get('end_date_extension') != allocation_change_obj.end_date_extension: + allocation_change_obj.end_date_extension = form_data.get('end_date_extension') - if form_data.get('end_date_extension') != allocation_change_obj.end_date_extension: - allocation_change_obj.end_date_extension = form_data.get('end_date_extension') new_end_date = allocation_change_obj.allocation.end_date + relativedelta( - days=form_data.get('end_date_extension')) + 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() + allocation_change_obj.save() + + for entry in formset: + formset_data = entry.cleaned_data + new_value = formset_data.get('new_value') + + attribute_change = AllocationAttributeChangeRequest.objects.get(pk=formset_data.get('change_pk')) + + if new_value != attribute_change.new_value: + attribute_change.new_value = new_value + attribute_change.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) + ) - for entry in formset: - formset_data = entry.cleaned_data - new_value = formset_data.get('new_value') - - attribute_change = AllocationAttributeChangeRequest.objects.get(pk=formset_data.get('pk')) + 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 + ) - if new_value != attribute_change.new_value: - attribute_change.new_value = new_value - attribute_change.save() + return HttpResponseRedirect(reverse('allocation-change-detail', kwargs={'pk': pk})) + else: + if allocation_change_form.is_valid(): + form_data = allocation_change_form.cleaned_data + selected_extension = form_data.get('end_date_extension') - attribute_change_list = allocation_change_obj.allocationattributechangerequest_set.all() + if selected_extension != 0: + allocation_change_obj.notes = notes - for attribute_change in attribute_change_list: - attribute_change.allocation_attribute.value = attribute_change.new_value - attribute_change.allocation_attribute.save() + allocation_change_status_active_obj = AllocationChangeStatusChoice.objects.get( + name='Approved') + allocation_change_obj.status = allocation_change_status_active_obj - 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) - ) + if selected_extension != allocation_change_obj.end_date_extension: + allocation_change_obj.end_date_extension = selected_extension + + 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() + + 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-detail', kwargs={'pk': pk})) + 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-detail', kwargs={'pk': pk})) + + else: + messages.error(request, 'You must make a change to the allocation.') + return HttpResponseRedirect(reverse('allocation-change-detail', kwargs={'pk': pk})) + + else: + for error in allocation_change_form.errors: + messages.error(request, error) + return HttpResponseRedirect(reverse('allocation-change-detail', kwargs={'pk': pk})) if request.POST.get('choice') == 'deny': @@ -1860,38 +1930,72 @@ def post(self, request, *args, **kwargs): return HttpResponseRedirect(reverse('allocation-change-detail', kwargs={'pk': pk})) if request.POST.get('choice') == 'update': - if allocation_change_form.is_valid() and formset.is_valid(): + if allocation_change_obj.status.name != 'Pending': allocation_change_obj.notes = notes - - form_data = allocation_change_form.cleaned_data - - if form_data.get('end_date_extension') != allocation_change_obj.end_date_extension: - allocation_change_obj.end_date_extension = form_data.get('end_date_extension') - - - for entry in formset: - formset_data = entry.cleaned_data - new_value = formset_data.get('new_value') - - attribute_change = AllocationAttributeChangeRequest.objects.get(pk=formset_data.get('pk')) - - if new_value != attribute_change.new_value: - attribute_change.new_value = new_value - attribute_change.save() - allocation_change_obj.save() messages.success( request, 'Allocation change request updated!') return HttpResponseRedirect(reverse('allocation-change-detail', kwargs={'pk': pk})) - else: - attribute_errors = "" - for error in allocation_change_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-detail', kwargs={'pk': pk})) + else: + if allocation_attributes_to_change: + if allocation_change_form.is_valid() and formset.is_valid(): + allocation_change_obj.notes = notes + + form_data = allocation_change_form.cleaned_data + + if form_data.get('end_date_extension') != allocation_change_obj.end_date_extension: + allocation_change_obj.end_date_extension = form_data.get('end_date_extension') + + + for entry in formset: + formset_data = entry.cleaned_data + new_value = formset_data.get('new_value') + + attribute_change = AllocationAttributeChangeRequest.objects.get(pk=formset_data.get('change_pk')) + + if new_value != attribute_change.new_value: + attribute_change.new_value = new_value + attribute_change.save() + + allocation_change_obj.save() + + messages.success( + request, 'Allocation change request updated!') + return HttpResponseRedirect(reverse('allocation-change-detail', kwargs={'pk': pk})) + else: + attribute_errors = "" + for error in allocation_change_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-detail', kwargs={'pk': pk})) + else: + if allocation_change_form.is_valid(): + form_data = allocation_change_form.cleaned_data + selected_extension = form_data.get('end_date_extension') + + if selected_extension != 0: + allocation_change_obj.notes = notes + + if selected_extension != allocation_change_obj.end_date_extension: + allocation_change_obj.end_date_extension = selected_extension + + allocation_change_obj.save() + + messages.success( + request, 'Allocation change request updated!') + + return HttpResponseRedirect(reverse('allocation-change-detail', kwargs={'pk': pk})) + + else: + messages.error(request, 'You must make a change to the allocation.') + return HttpResponseRedirect(reverse('allocation-change-detail', kwargs={'pk': pk})) + else: + for error in allocation_change_form.errors: + messages.error(request, error) + return HttpResponseRedirect(reverse('allocation-change-detail', kwargs={'pk': pk})) else: allocation_change_form = AllocationChangeForm( @@ -2258,3 +2362,28 @@ def get(self, request, pk): ) return HttpResponseRedirect(reverse('allocation-change-list')) + +class AllocationChangeDeleteAttributeView(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 update an allocation change request.') + + def get(self, request, pk): + allocation_attribute_change_obj = get_object_or_404(AllocationAttributeChangeRequest, pk=pk) + allocation_change_pk = allocation_attribute_change_obj.allocation_change_request.pk + + allocation_attribute_change_obj.delete() + + messages.success( + request, 'Allocation attribute change request successfully deleted.') + return HttpResponseRedirect(reverse('allocation-change-detail', kwargs={'pk': allocation_change_pk})) From 526f9d90498e97d403d2147c452be5fe518db6f3 Mon Sep 17 00:00:00 2001 From: Brian Scorcia Date: Wed, 22 Dec 2021 15:54:03 -0500 Subject: [PATCH 15/15] #294: Updated Allocation Change Detail page so that only admins see the editable form fields --- .../allocation/allocation_change_detail.html | 24 +++++++++++-------- coldfront/core/allocation/views.py | 2 ++ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/coldfront/core/allocation/templates/allocation/allocation_change_detail.html b/coldfront/core/allocation/templates/allocation/allocation_change_detail.html index db622f990..7886d2de5 100644 --- a/coldfront/core/allocation/templates/allocation/allocation_change_detail.html +++ b/coldfront/core/allocation/templates/allocation/allocation_change_detail.html @@ -134,10 +134,11 @@

Alloc

{% for form in formset %} - - - {% if allocation_change.status.name == 'Pending' %} - + + + {% if allocation_change.status.name == 'Pending' %} + + {% if request.user.is_superuser %} {% else %} - {% if form.new_value.value == '' %} - - {% else %} - - {% endif %} + {% endif %} - + {% else %} + {% if form.new_value.value == '' %} + + {% else %} + + {% endif %} + {% endif %} + {% endfor %}
{{attribute.allocation_attribute}}{{form.name.value}}{{attribute.allocation_attribute.value}}None{{form.value.value}} + {{form.new_value}} + + + + {{attribute.new_value}}None{{form.new_value.value}}
{{form.value.value}} {{form.new_value}} - +
{{form.name.value}}{{form.value.value}}
{{form.name.value}}{{form.value.value}} {{form.new_value}} @@ -145,13 +146,16 @@

Alloc

None{{form.new_value.value}}{{form.new_value.value}}
None{{form.new_value.value}}
diff --git a/coldfront/core/allocation/views.py b/coldfront/core/allocation/views.py index 434b9000f..116dadeed 100644 --- a/coldfront/core/allocation/views.py +++ b/coldfront/core/allocation/views.py @@ -1697,6 +1697,8 @@ def get(self, request, *args, **kwargs): allocation_change_form.fields['justification'].disabled = True if allocation_change_obj.status.name != 'Pending': allocation_change_form.fields['end_date_extension'].disabled = True + if allocation_change_obj.allocation.project.pi == self.request.user: + allocation_change_form.fields['end_date_extension'].disabled = True note_form = AllocationChangeNoteForm( initial={'notes': allocation_change_obj.notes})