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 %}
-
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 changes to an existing allocation using the form below. For each change + you must provide a justification. +
+ +Attribute | +Current Value | +Request New Value | +
---|---|---|
{{form.name.value}} | +{{form.value.value}} | +{{form.new_value}} | +
{{ allocation_change.notes }}
+ {% endif %} +{{ allocation_change.notes }}
+ {% endif %} +{{ allocation_change.notes }}
+ {% 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" }} | +||
Locked | ++ {% else %} + | Unlocked | ++ {% endif %} + |
Attribute | + {% if allocation_change.status.name == 'Pending' %} +Current Value | + {% endif %} +Requested New Value | +|
---|---|---|---|
{{attribute.allocation_attribute}} | + {% if allocation_change.status.name == 'Pending' %} +{{attribute.allocation_attribute.value}} | + {% endif %} + {% if attribute.new_value == '' %} +None | + {% else %} +{{attribute.new_value}} | + {% endif %} +
{{allocation_change_form.justification | as_crispy_field}}
+ +# | +Requested | +Project Title | +PI | +Resource | +Extension | +Changes | +Quick 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 + | +
Date Requested | +Status | +Notes | +Actions | +|||
---|---|---|---|---|---|---|
{{ change_request.modified|date:"M. d, Y" }} | + {% if change_request.status.name == 'Approved' %} +{{ change_request.status.name }} | + {% elif change_request.status.name == 'Denied' %} +{{ change_request.status.name }} | + {% else %} +{{ change_request.status.name }} | + {% endif %} + + {% if change_request.notes %} +{{change_request.notes}} | + {% else %} ++ {% endif %} + | Edit | +