Skip to content

Commit

Permalink
Merge pull request #244 from fasrc/cp_allocationrequestformpt2
Browse files Browse the repository at this point in the history
Allocation Request Form redesign
  • Loading branch information
claire-peters authored Aug 22, 2023
2 parents 1f64005 + 583c03e commit ce926e7
Show file tree
Hide file tree
Showing 24 changed files with 809 additions and 278 deletions.
2 changes: 1 addition & 1 deletion coldfront/config/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# General Center Information
#------------------------------------------------------------------------------
CENTER_NAME = ENV.str('CENTER_NAME', default='HPC Center')
CENTER_HELP_URL = ENV.str('CENTER_HELP_URL', default='help-page')
CENTER_HELP_URL = ENV.str('CENTER_HELP_URL', default='{% url help-page %}')
CENTER_PROJECT_RENEWAL_HELP_URL = ENV.str('CENTER_PROJECT_RENEWAL_HELP_URL', default='')
CENTER_BASE_URL = ENV.str('CENTER_BASE_URL', default='')

Expand Down
173 changes: 149 additions & 24 deletions coldfront/core/allocation/forms.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import re

from django import forms
from django.db.models.functions import Lower
from django.shortcuts import get_object_or_404

from coldfront.core.allocation.models import (Allocation, AllocationAccount,
AllocationAttributeType,
AllocationAttribute,
AllocationStatusChoice)
from coldfront.core.allocation.models import (
AllocationAccount,
AllocationAttributeType,
AllocationAttribute,
AllocationStatusChoice
)
from coldfront.core.allocation.utils import get_user_resources
from coldfront.core.project.models import Project
from coldfront.core.resource.models import Resource, ResourceType
Expand All @@ -15,23 +19,90 @@
'ALLOCATION_ACCOUNT_ENABLED', False)
ALLOCATION_CHANGE_REQUEST_EXTENSION_DAYS = import_from_settings(
'ALLOCATION_CHANGE_REQUEST_EXTENSION_DAYS', [])
HSPH_CODE = import_from_settings('HSPH_CODE', '000-000-000-000-000-000-000-000-000-000-000')
SEAS_CODE = import_from_settings('SEAS_CODE', '111-111-111-111-111-111-111-111-111-111-111')

class ExpenseCodeField(forms.CharField):
"""custom field for expense_code"""

# def validate(self, value):
# if value:
# digits_only = re.sub(r'\D', '', value)
# if not re.fullmatch(r'^(\d+-?)*[\d-]+$', value):
# raise ValidationError("Input must consist only of digits and dashes.")
# if len(digits_only) != 33:
# raise ValidationError("Input must contain exactly 33 digits.")

def clean(self, value):
# Remove all dashes from the input string to count the number of digits
value = super().clean(value)
digits_only = re.sub(r'[^0-9xX]', '', value)
insert_dashes = lambda d: '-'.join(
[d[:3], d[3:8], d[8:12], d[12:18], d[18:24], d[24:28], d[28:33]]
)
formatted_value = insert_dashes(digits_only)
return formatted_value


class AllocationForm(forms.Form):
DEFAULT_DESCRIPTION = """
We do not have information about your research. Please provide a detailed description of your work and update your field of science. Thank you!
"""
resource = forms.ModelChoiceField(queryset=None, empty_label=None)
quantity = forms.IntegerField(required=True)
justification = forms.CharField(widget=forms.Textarea)
# resource = forms.ModelChoiceField(queryset=None, empty_label=None)
quantity = forms.IntegerField(required=True, initial=1)

expense_code = ExpenseCodeField(
label="Lab's 33 digit expense code", required=False
)

hsph_code = forms.BooleanField(
label='The PI is part of HSPH and storage should be billed to their code',
required=False
)

seas_code = forms.BooleanField(
label='The PI is part of SEAS and storage should be billed to their code',
required=False
)

tier = forms.ModelChoiceField(
queryset=Resource.objects.filter(resource_type__name='Storage Tier'),
label='Resource Tier'
)
heavy_io = forms.BooleanField(
label='My lab will perform heavy I/O from the cluster against this space (more than 100 cores)',
required=False
)
mounted = forms.BooleanField(
label='My lab intends to mount the storage to our local machine as an additional drive',
required=False
)
external_sharing = forms.BooleanField(
label='My lab intends to share some of this data with collaborators outside of Harvard',
required=False
)
high_security = forms.BooleanField(
label='This allocation will store secure information (security level three or greater)',
required=False
)
dua = forms.BooleanField(
label="Some or all of my lab’s data is governed by DUAs", required=False
)
justification = forms.CharField(
widget=forms.Textarea,
help_text = '<br/>Justification for requesting this allocation. Please provide details here about the usecase or datacenter choices (what data needs to be accessed, expectation of frequent transfer to or from Campus, need for Samba connectivity, etc.)'
)

#users = forms.MultipleChoiceField(
# widget=forms.CheckboxSelectMultiple, required=False)


def __init__(self, request_user, project_pk, *args, **kwargs):
super().__init__(*args, **kwargs)
project_obj = get_object_or_404(Project, pk=project_pk)
self.fields['resource'].queryset = get_user_resources(request_user).order_by(Lower("name"))
self.fields['quantity'].initial = 1
self.fields['tier'].queryset = get_user_resources(request_user).filter(
resource_type__name='Storage Tier'
).order_by(Lower("name"))
user_query_set = project_obj.projectuser_set.select_related('user').filter(
status__name__in=['Active', ]).order_by("user__username")
user_query_set = user_query_set.exclude(user=project_obj.pi)
Expand All @@ -42,10 +113,48 @@ def __init__(self, request_user, project_pk, *args, **kwargs):
# else:
# self.fields['users'].widget = forms.HiddenInput()

self.fields['justification'].help_text = '<br/>Justification for requesting this allocation. Please provide details about the usecase or datacenter choices'

def clean(self):
cleaned_data = super().clean()
# Remove all dashes from the input string to count the number of digits
value = cleaned_data.get("expense_code")
hsph_val = cleaned_data.get("hsph_code")
seas_val = cleaned_data.get("seas_code")
trues = sum(x for x in [(value not in ['', '------']), hsph_val, seas_val])

if trues != 1:
self.add_error("expense_code", "you must do exactly one of the following: manually enter an expense code, check the box to use SEAS' expense code, or check the box to use HSPH's expense code")

elif value and value != '------':
digits_only = re.sub(r'[^0-9xX]', '', value)
if not re.fullmatch(r'^([0-9xX]+-?)*[0-9xX-]+$', value):
self.add_error("expense_code", "Input must consist only of digits (or x'es) and dashes.")
elif len(digits_only) != 33:
self.add_error("expense_code", "Input must contain exactly 33 digits.")
else:
insert_dashes = lambda d: '-'.join(
[d[:3], d[3:8], d[8:12], d[12:18], d[18:24], d[24:28], d[28:33]]
)
cleaned_data['expense_code'] = insert_dashes(digits_only)
elif hsph_val:
cleaned_data['expense_code'] = HSPH_CODE
elif seas_val:
cleaned_data['expense_code'] = SEAS_CODE
return cleaned_data


class AllocationResourceChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj):
label_str = f'{obj.name}'
if obj.used_percentage != None:
label_str += f' ({obj.used_percentage}% full)'
return label_str


class AllocationUpdateForm(forms.Form):
resource = AllocationResourceChoiceField(
label='Resource', queryset=Resource.objects.all(), required=False
)
status = forms.ModelChoiceField(
queryset=AllocationStatusChoice.objects.all().order_by(Lower("name")), empty_label=None)
start_date = forms.DateField(
Expand All @@ -56,26 +165,44 @@ class AllocationUpdateForm(forms.Form):
label='End Date',
widget=forms.DateInput(attrs={'class': 'datepicker'}),
required=False)
description = forms.CharField(max_length=512,
label='Description',
required=False)
description = forms.CharField(
max_length=512, label='Description', required=False
)
is_locked = forms.BooleanField(required=False)
is_changeable = forms.BooleanField(required=False)

def __init__(self, *args, **kwargs):
allo_resource = kwargs['initial'].pop('resource')
super().__init__(*args, **kwargs)
if not allo_resource:
self.fields['resource'].queryset = Resource.objects.exclude(
resource_type__name='Storage Tier'
)
else:
if allo_resource.resource_type.name == 'Storage Tier':
self.fields['resource'].queryset = Resource.objects.filter(
parent_resource=allo_resource
)
else:
self.fields['resource'].required = False
self.fields['resource'].queryset = Resource.objects.filter(
pk=allo_resource.pk
)


def clean(self):
cleaned_data = super().clean()
start_date = cleaned_data.get("start_date")
end_date = cleaned_data.get("end_date")

if start_date and end_date and end_date < start_date:
raise forms.ValidationError(
'End date cannot be less than start date'
)
raise forms.ValidationError('End date cannot be less than start date')
return cleaned_data


class AllocationInvoiceUpdateForm(forms.Form):
status = forms.ModelChoiceField(queryset=AllocationStatusChoice.objects.filter(name__in=[
'Payment Pending', 'Payment Requested', 'Payment Declined', 'Paid']).order_by(Lower("name")), empty_label=None)
status = forms.ModelChoiceField(queryset=AllocationStatusChoice.objects.filter(
name__in=['Payment Pending', 'Payment Requested', 'Payment Declined', 'Paid']
).order_by(Lower("name")), empty_label=None)


class AllocationAddUserForm(forms.Form):
Expand Down Expand Up @@ -108,8 +235,7 @@ def __init__(self, *args, **kwargs):
class AllocationSearchForm(forms.Form):
project = forms.CharField(label='Project Title',
max_length=100, required=False)
username = forms.CharField(
label='Username', max_length=100, required=False)
username = forms.CharField(label='Username', max_length=100, required=False)
resource_type = forms.ModelChoiceField(
label='Resource Type',
queryset=ResourceType.objects.all().order_by(Lower("name")),
Expand Down Expand Up @@ -157,8 +283,7 @@ class AllocationReviewUserForm(forms.Form):
class AllocationInvoiceNoteDeleteForm(forms.Form):
pk = forms.IntegerField(required=False, disabled=True)
note = forms.CharField(widget=forms.Textarea, disabled=True)
author = forms.CharField(
max_length=512, required=False, disabled=True)
author = forms.CharField(max_length=512, required=False, disabled=True)
selected = forms.BooleanField(initial=False, required=False)

def __init__(self, *args, **kwargs):
Expand Down Expand Up @@ -236,7 +361,7 @@ def __init__(self, *args, **kwargs):


class AllocationChangeNoteForm(forms.Form):
notes = forms.CharField(
notes = forms.CharField(
max_length=512,
label='Notes',
required=False,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,19 @@ def handle(self, *args, **options):
('Offer Letter', 'Float', False, True),
('RequiresPayment', 'Yes/No', False, True),
('Offer Letter Code', 'Text', False, True),
('Expense Code', 'Text', False, True),
('Subdirectory', 'Text', False, False),
('Heavy IO', 'Yes/No', False, False),
('Mounted', 'Yes/No', False, False),
('High Security', 'Yes/No', False, False),
('DUA', 'Yes/No', False, False),
('External Sharing', 'Yes/No', False, False),
# UBCCR defaults
# ('Cloud Account Name', 'Text', False, False),
# ('CLOUD_USAGE_NOTIFICATION', 'Yes/No', False, True),
# ('Core Usage (Hours)', 'Int', True, False),
# ('Accelerator Usage (Hours)', 'Int', True, False),
# ('Cloud Storage Quota (TB)', 'Float', True, False),
# ('EXPIRE NOTIFICATION', 'Yes/No', False, True),
# ('freeipa_group', 'Text', False, False),
# ('Is Course?', 'Yes/No', False, True),
Expand All @@ -58,15 +65,11 @@ def handle(self, *args, **options):
# ('slurm_user_specs_attriblist', 'Text', False, True),
# ('Storage Quota (GB)', 'Int', False, False),
# ('Storage_Group_Name', 'Text', False, False),
# ('Tier 0 - $50/TB/yr', 'Text', False, False),
# ('Tier 1 - $250/TB/yr', 'Text', False, False),
# ('Tier 2 - $100/TB/yr', 'Text', False, False),
# ('Tier 3 - $8/TB/yr', 'Text', False, False),
# ('Tier 0', 'Text', False, False),
# ('Tier 1', 'Text', False, False),
# ('Tier 2', 'Text', False, False),
# ('Tier 3', 'Text', False, False),

# ('SupportersQOS', 'Yes/No', False, False),
# ('SupportersQOSExpireDate', 'Date', False, False),
):
AllocationAttributeType.objects.get_or_create(name=name, attribute_type=AttributeType.objects.get(
name=attribute_type), has_usage=has_usage, is_private=is_private)
AllocationAttributeType.objects.get_or_create(
name=name,
attribute_type=AttributeType.objects.get(name=attribute_type),
has_usage=has_usage, is_private=is_private
)
48 changes: 44 additions & 4 deletions coldfront/core/allocation/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,41 @@ def save(self, *args, **kwargs):

super().save(*args, **kwargs)

def pull_allocationattribute(self, attr_name):
try:
return self.allocationattribute_set.get(
allocation_attribute_type__name=attr_name
).value
except ObjectDoesNotExist:
return None

@property
def offer_letter_code(self):
return self.pull_allocationattribute('Offer Letter Code')

@property
def expense_code(self):
return self.pull_allocationattribute('Expense Code')

@property
def heavy_io(self):
return self.pull_allocationattribute('Heavy IO')

@property
def mounted(self):
return self.pull_allocationattribute('Mounted')

@property
def external_sharing(self):
return self.pull_allocationattribute('External Sharing')

@property
def high_security(self):
return self.pull_allocationattribute('High Security')

@property
def dua(self):
return self.pull_allocationattribute('DUA')

@property
def size(self):
Expand Down Expand Up @@ -163,7 +198,10 @@ def path(self):

@property
def cost(self):
price = float(get_resource_rate(self.resources.first().name))
try:
price = float(get_resource_rate(self.resources.first().name))
except AttributeError:
return None
size = self.allocationattribute_set.get(allocation_attribute_type_id=1).value
return 0 if not size else price * float(size)

Expand All @@ -173,8 +211,9 @@ def expires_in(self):
Returns:
int: the number of days until the allocation expires
"""

return (self.end_date - datetime.date.today()).days
if self.end_date:
return (self.end_date - datetime.date.today()).days
return None

@property
def get_information(self, public_only=True):
Expand All @@ -184,7 +223,8 @@ def get_information(self, public_only=True):
"""
html_string = ''
if public_only:
allocationattribute_set = self.allocationattribute_set.filter(allocation_attribute_type__is_private=False)
allocationattribute_set = self.allocationattribute_set.filter(
allocation_attribute_type__is_private=False)
else:
allocationattribute_set = self.allocationattribute_set.all()
for attribute in allocationattribute_set:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ <h3 class="d-inline"><i class="fas fa-info-circle" aria-hidden="true"></i> Alloc
$('#resource_message').html(message);
}
else {
$('#resource_message').html('<div>EMPTY</div>');
$('#resource_message').html('<div></div>');
}
});

Expand Down
Loading

0 comments on commit ce926e7

Please sign in to comment.