Skip to content

Commit

Permalink
Merge pull request #262 from fasrc/cp_fiine_integration
Browse files Browse the repository at this point in the history
FIINE integration
  • Loading branch information
claire-peters authored Oct 26, 2023
2 parents acaa9d3 + 74eda37 commit 75ee89f
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 68 deletions.
126 changes: 77 additions & 49 deletions coldfront/core/allocation/forms.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import re

from django import forms
from django.conf import settings
from django.db.models.functions import Lower
from django.shortcuts import get_object_or_404
from django.core.exceptions import ValidationError

from coldfront.core.allocation.models import (
AllocationAccount,
Expand All @@ -15,33 +17,44 @@
from coldfront.core.resource.models import Resource, ResourceType
from coldfront.core.utils.common import import_from_settings

if 'ifxbilling' in settings.INSTALLED_APPS:
from fiine.client import API as FiineAPI
from ifxbilling.models import Account, UserProductAccount

ALLOCATION_ACCOUNT_ENABLED = import_from_settings(
'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 validate(self, value):
digits_only = lambda v: re.sub(r'[^0-9xX]', '', v)
if value and value != '------':
if re.search(r'[^0-9xX\-\.]', value):
raise ValidationError(
"Input must consist only of digits (or x'es) and dashes."
)
if len(digits_only(value)) != 33:
raise ValidationError("Input must contain exactly 33 digits.")
if 'x' in digits_only(value)[:8]+digits_only(value)[12:]:
raise ValidationError(
"xes are only allowed in place of the product code (the third grouping of characters in the code)"
)

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
# digits_only = lambda v: re.sub(r'[^0-9xX]', '', v)
# 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 value

ALLOCATION_SPECIFICATIONS = [
('Heavy IO', 'My lab will perform heavy I/O from the cluster against this space (more than 100 cores)'),
Expand All @@ -52,23 +65,23 @@ def clean(self, value):
]

class AllocationForm(forms.Form):
QS_CHOICES = [
(HSPH_CODE, f'{HSPH_CODE} (PI is part of HSPH and storage should be billed to their code)'),
(SEAS_CODE, f'{SEAS_CODE} (PI is part of SEAS and storage should be billed to their code)')
]
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)

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
existing_expense_codes = forms.ChoiceField(
label='Either select an existing expense code...',
choices=QS_CHOICES,
required=False,
)

seas_code = forms.BooleanField(
label='The PI is part of SEAS and storage should be billed to their code',
required=False
expense_code = ExpenseCodeField(
label="...or add a new 33 digit expense code manually here.", required=False
)

tier = forms.ModelChoiceField(
Expand Down Expand Up @@ -98,43 +111,59 @@ def __init__(self, request_user, project_pk, *args, **kwargs):
self.fields['tier'].queryset = get_user_resources(request_user).filter(
resource_type__name='Storage Tier'
).order_by(Lower("name"))
existing_expense_codes = [(None, '------')] + [
(a.code, f'{a.code} ({a.name})') for a in Account.objects.filter(
userproductaccount__is_valid=1,
userproductaccount__user=project_obj.pi
).distinct()
] + self.QS_CHOICES
self.fields['existing_expense_codes'].choices = existing_expense_codes
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)
status__name__in=['Active', ]
).order_by("user__username").exclude(user=project_obj.pi)
# if user_query_set:
# self.fields['users'].choices = ((user.user.username, "%s %s (%s)" % (
# user.user.first_name, user.user.last_name, user.user.username)) for user in user_query_set)
# self.fields['users'].help_text = '<br/>Select users in your project to add to this allocation.'
# else:
# self.fields['users'].widget = forms.HiddenInput()


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])

expense_code = cleaned_data.get("expense_code")
existing_expense_codes = cleaned_data.get("existing_expense_codes")
trues = sum(x for x in [
(expense_code not in ['', '------']),
(existing_expense_codes not in ['', '------']),
])
digits_only = lambda v: re.sub(r'[^0-9xX]', '', v)
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
self.add_error(
"existing_expense_codes",
"You must either select an existing expense code or manually enter a new one."
)

elif expense_code and expense_code != '------':
replace_productcode = lambda s: s[:8] + '8250' + s[12:]
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_expensecode = insert_dashes(replace_productcode(digits_only(expense_code)))
if 'ifxbilling' in settings.INSTALLED_APPS:
try:
matched_fiineaccts = FiineAPI.listAccounts(code=cleaned_expensecode)
if not matched_fiineaccts:
self.add_error(
"expense_code",
"expense code not found in system - please check the code or get in touch with a system administrator."
)
except Exception:
#Not authorized to use accounts_list
pass
cleaned_data['expense_code'] = cleaned_expensecode
elif existing_expense_codes and existing_expense_codes != '------':
cleaned_data['expense_code'] = existing_expense_codes
return cleaned_data


Expand Down Expand Up @@ -184,7 +213,6 @@ def __init__(self, *args, **kwargs):
pk=allo_resource.pk
)


def clean(self):
cleaned_data = super().clean()
start_date = cleaned_data.get("start_date")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,11 @@ <h3><i class="fas fa-list" aria-hidden="true"></i> Allocation Information</h3>
</tr>
<tr>
<th scope="row" class="text-nowrap">Expense Code:</th>
<td>{{ allocation.expense_code }}</td>
<td>
{% for code in expense_codes %}
{{ code.account.name }} ({{ code.account.code }}): {{ code.percent }}%<br>
{% endfor %}
</td>
</tr>
<tr>
<th scope="row" class="text-nowrap">Total Users in Your Bill:</th>
Expand Down
60 changes: 46 additions & 14 deletions coldfront/core/allocation/tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django.test import TestCase

from coldfront.core.resource.models import Resource
from coldfront.core.allocation.forms import AllocationForm
from coldfront.core.allocation.forms import AllocationForm, HSPH_CODE
from coldfront.core.test_helpers.factories import (
setup_models,
ResourceTypeFactory,
Expand Down Expand Up @@ -48,7 +48,7 @@ def setUp(self):
'tier': Resource.objects.filter(resource_type=tier_restype).first()
}

def test_allocationform_offerlettercode_invalid(self):
def test_allocationform_expense_code_invalid1(self):
"""ensure correct error messages for incorrect expense_code value
"""
self.post_data['expense_code'] = '123456789'
Expand All @@ -57,46 +57,78 @@ def test_allocationform_offerlettercode_invalid(self):
form.errors['expense_code'], ['Input must contain exactly 33 digits.']
)

def test_allocationform_offerlettercode_valid(self):
def test_allocationform_expense_code_invalid2(self):
"""ensure correct error messages for incorrect expense_code value
"""
self.post_data['expense_code'] = '123-456AB-CDE789-22222-22222-22222-22222'
form = AllocationForm(data=self.post_data, request_user=self.pi_user, project_pk=self.project.pk)
self.assertEqual(
form.errors['expense_code'], ["Input must consist only of digits (or x'es) and dashes."]
)

def test_allocationform_expense_code_invalid3(self):
"""ensure correct error messages for incorrect expense_code value
"""
self.post_data['expense_code'] = '1Xx-' * 11
form = AllocationForm(data=self.post_data, request_user=self.pi_user, project_pk=self.project.pk)
self.assertEqual(
form.errors['expense_code'], ["xes are only allowed in place of the product code (the third grouping of characters in the code)"]
)

def test_allocationform_expense_code_valid(self):
"""Test POST to the AllocationCreateView
- ensure 33-digit codes go through
- ensure correctly entered codes get properly formatted
"""
# correct # of digits with no dashes
cleaned_form = self.return_cleaned_form(AllocationForm)
self.assertEqual(
cleaned_form['expense_code'], '123-12312-3123-123123-123123-1231-23123'
cleaned_form['expense_code'], '123-12312-8250-123123-123123-1231-23123'
)

def test_allocationform_offerlettercode_valid2(self):
# check that offer code was correctly formatted
def test_allocationform_expense_code_valid2(self):
# check that expense_code was correctly formatted
# correct # of digits with many dashes
self.post_data['expense_code'] = '123-' * 11
cleaned_form = self.return_cleaned_form(AllocationForm)

self.assertEqual(
cleaned_form['expense_code'], '123-12312-3123-123123-123123-1231-23123'
cleaned_form['expense_code'], '123-12312-8250-123123-123123-1231-23123'
)

def test_allocationform_offerlettercode_valid3(self):
def test_allocationform_expense_code_valid3(self):
"""Test POST to the AllocationCreateView
- ensure xes count as digits
"""
# correct # of digits with no dashes
self.post_data['expense_code'] = '1Xx-' * 11
self.post_data['expense_code'] = '123-12312-xxxx-123123-123123-1231-23123'
cleaned_form = self.return_cleaned_form(AllocationForm)
self.assertEqual(
cleaned_form['expense_code'], '123-12312-8250-123123-123123-1231-23123'
)

def test_allocationform_expense_code_valid4(self):
"""Test POST to the AllocationCreateView
- ensure xes count as digits
"""
# correct # of digits with no dashes
self.post_data['expense_code'] = '123.12312.xxxx.123123.123123.1231.23123'
cleaned_form = self.return_cleaned_form(AllocationForm)
self.assertEqual(
cleaned_form['expense_code'], '1Xx-1Xx1X-x1Xx-1Xx1Xx-1Xx1Xx-1Xx1-Xx1Xx'
cleaned_form['expense_code'], '123-12312-8250-123123-123123-1231-23123'
)

def test_allocationform_offerlettercode_multiplefield_invalid(self):
"""Test POST to AllocationCreateView in circumstance where hsph and seas values are also checked"""
def test_allocationform_expense_code_multiplefield_invalid(self):
"""
Test POST to AllocationCreateView in circumstance where code is entered
and an existing_expense_codes value has also been selected
"""
self.post_data['expense_code'] = '123-' * 11
self.post_data['hsph_code'] = True
self.post_data['existing_expense_codes'] = HSPH_CODE
form = AllocationForm(
data=self.post_data, request_user=self.pi_user, project_pk=self.project.pk
)
self.assertIn("you must do exactly one of the following", form.errors['expense_code'][0])
self.assertIn("must either select an existing expense code or", form.errors['existing_expense_codes'][0])


class AllocationUpdateFormTest(AllocationFormBaseTest):
Expand Down
8 changes: 4 additions & 4 deletions coldfront/core/allocation/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,14 +439,14 @@ def test_allocationcreateview_post_bools(self):

def test_allocationcreateview_post_offerlettercode_multiplefield_invalid(self):
"""Ensure that form won't pass if multiple expense codes are given"""
self.post_data['hsph_code'] = '000-000-000-000-000-000-000-000-000-000-000'
self.post_data['existing_expense_codes'] = '000-000-000-000-000-000-000-000-000-000-000'
response = self.client.post(self.url, data=self.post_data, follow=True)
self.assertContains(response, "you must do exactly one of the following")
self.assertContains(response, "must either select an existing expense code or")


def test_allocationcreateview_post_hsph_offerlettercode(self):
"""Ensure that form goes through if hsph is checked"""
self.post_data['hsph_code'] = '000-000-000-000-000-000-000-000-000-000-000'
"""Ensure that form goes through if existing_expense_codes is checked"""
self.post_data['existing_expense_codes'] = '000-000-000-000-000-000-000-000-000-000-000'
self.post_data.pop('expense_code')
response = self.client.post(self.url, data=self.post_data, follow=True)
self.assertContains(response, "Allocation requested.")
Expand Down
11 changes: 11 additions & 0 deletions coldfront/core/allocation/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@
from coldfront.core.utils.common import get_domain_url, import_from_settings
from coldfront.core.utils.mail import send_allocation_admin_email, send_allocation_customer_email


if 'ifxbilling' in settings.INSTALLED_APPS:
from ifxbilling.models import Account, UserProductAccount
if 'django_q' in settings.INSTALLED_APPS:
from django_q.tasks import Task

Expand Down Expand Up @@ -215,6 +218,14 @@ def get_context_data(self, **kwargs):
user_sync_dt = None
context['user_sync_dt'] = user_sync_dt

if 'ifxbilling' in settings.INSTALLED_APPS:
expense_codes = UserProductAccount.objects.filter(
user=allocation_obj.project.pi,
is_valid=1,
product__product_name=allocation_obj.get_parent_resource.name
)
context['expense_codes'] = expense_codes

context['allocation_quota_bytes'] = quota_bytes
context['allocation_usage_bytes'] = usage_bytes
quota_tb = 0 if not quota_bytes else quota_bytes / 1099511627776
Expand Down

0 comments on commit 75ee89f

Please sign in to comment.