Skip to content

Commit c467196

Browse files
committed
Merge branch 'cp_slurm' of https://github.com/fasrc/coldfront into cp_slurm
2 parents 0658951 + 97aa551 commit c467196

File tree

20 files changed

+287
-190
lines changed

20 files changed

+287
-190
lines changed

coldfront/config/email.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@
2222
EMAIL_ALLOCATION_EXPIRING_NOTIFICATION_DAYS = ENV.list('EMAIL_ALLOCATION_EXPIRING_NOTIFICATION_DAYS', cast=int, default=[7, 14, 30])
2323
EMAIL_SIGNATURE = ENV.str('EMAIL_SIGNATURE', default='', multiline=True)
2424
EMAIL_ADMINS_ON_ALLOCATION_EXPIRE = ENV.bool('EMAIL_ADMINS_ON_ALLOCATION_EXPIRE', default=False)
25+
ADMIN_REMINDER_EMAIL = ENV.str('ADMIN_REMINDER_EMAIL', default='')

coldfront/core/allocation/forms.py

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -150,18 +150,7 @@ def clean(self):
150150
[d[:3], d[3:8], d[8:12], d[12:18], d[18:24], d[24:28], d[28:33]]
151151
)
152152
cleaned_expensecode = insert_dashes(replace_productcode(digits_only(expense_code)))
153-
if 'ifxbilling' in settings.INSTALLED_APPS:
154-
try:
155-
matched_fiineaccts = FiineAPI.listAccounts(code=cleaned_expensecode)
156-
if not matched_fiineaccts:
157-
self.add_error(
158-
"expense_code",
159-
"expense code not found in system - please check the code or get in touch with a system administrator."
160-
)
161-
except Exception:
162-
#Not authorized to use accounts_list
163-
pass
164-
cleaned_data['expense_code'] = cleaned_expensecode
153+
cleaned_data['expense_code'] = cleaned_expensecode
165154
elif existing_expense_codes and existing_expense_codes != '------':
166155
cleaned_data['expense_code'] = existing_expense_codes
167156
return cleaned_data

coldfront/core/allocation/models.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -598,20 +598,29 @@ def clean(self):
598598

599599
expected_value_type = self.allocation_attribute_type.attribute_type.name.strip()
600600
error = None
601-
if expected_value_type == 'Float' and not isinstance(literal_eval(self.value), (float,int)):
602-
error = 'Value must be a float.'
603-
elif expected_value_type == 'Int' and not isinstance(literal_eval(self.value), int):
604-
error = 'Value must be an integer.'
605-
elif expected_value_type == 'Yes/No' and self.value not in ['Yes', 'No']:
601+
if expected_value_type in ['Float', 'Int']:
602+
try:
603+
literal_val = literal_eval(self.value)
604+
except SyntaxError as exc:
605+
error = 'Value must be entirely numeric. Please remove any non-numeric characters.'
606+
raise ValidationError(
607+
f'Invalid Value "{self.value}" for "{self.allocation_attribute_type.name}". {error}'
608+
) from exc
609+
if expected_value_type == 'Float' and not isinstance(literal_val, (float,int)):
610+
error = 'Value must be a float.'
611+
elif expected_value_type == 'Int' and not isinstance(literal_val, int):
612+
error = 'Value must be an integer.'
613+
elif expected_value_type == "Yes/No" and self.value not in ["Yes", "No"]:
606614
error = 'Allowed inputs are "Yes" or "No".'
607-
elif expected_value_type == 'Date':
615+
elif expected_value_type == "Date":
608616
try:
609617
datetime.datetime.strptime(self.value.strip(), '%Y-%m-%d')
610618
except ValueError:
611619
error = 'Date must be in format YYYY-MM-DD'
612620
if error:
613621
raise ValidationError(
614-
'Invalid Value "%s" for "%s". %s' % (self.value, self.allocation_attribute_type.name, error))
622+
f'Invalid Value "{self.value}" for "{self.allocation_attribute_type.name}". {error}'
623+
)
615624

616625
def __str__(self):
617626
return str(self.allocation_attribute_type.name)

coldfront/core/allocation/tasks.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
EMAIL_ADMINS_ON_ALLOCATION_EXPIRE = import_from_settings('EMAIL_ADMINS_ON_ALLOCATION_EXPIRE')
3333
EMAIL_ADMIN_LIST = import_from_settings('EMAIL_ADMIN_LIST')
34+
ADMIN_REMINDER_EMAIL = import_from_settings('ADMIN_REMINDER_EMAIL')
3435

3536
def update_statuses():
3637

@@ -69,12 +70,14 @@ def send_request_reminder_emails():
6970
'signature': EMAIL_SIGNATURE,
7071
'url_base': f'{CENTER_BASE_URL.strip("/")}/allocation/change-request/'
7172
}
72-
send_admin_email_template(
73-
'Pending Allocation Changes',
74-
'email/pending_allocation_changes.txt',
75-
allocation_change_template_context,
76-
)
7773

74+
send_email_template(
75+
subject='Pending Allocation Changes',
76+
template_name='email/pending_allocation_changes.txt',
77+
template_context=allocation_change_template_context,
78+
sender=EMAIL_SENDER,
79+
receiver_list=[ADMIN_REMINDER_EMAIL,],
80+
)
7881
# Allocation Requests are allocations marked as "new"
7982
pending_allocations = Allocation.objects.filter(
8083
status__name = 'New', created__lte=req_alert_date
@@ -87,11 +90,15 @@ def send_request_reminder_emails():
8790
'signature': EMAIL_SIGNATURE,
8891
'url_base': f'{CENTER_BASE_URL.strip("/")}/allocation/'
8992
}
90-
send_admin_email_template(
91-
'Pending Allocations',
92-
'email/pending_allocations.txt',
93-
new_allocation_template_context,
93+
94+
send_email_template(
95+
subject='Pending Allocations',
96+
template_name='email/pending_allocations.txt',
97+
template_context=new_allocation_template_context,
98+
sender=EMAIL_SENDER,
99+
receiver_list=[ADMIN_REMINDER_EMAIL,],
94100
)
101+
95102
# return statement for testing
96103
return (pending_changerequests, pending_allocations)
97104

coldfront/core/allocation/templates/allocation/allocation_detail.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,11 @@ <h3><i class="fas fa-list" aria-hidden="true"></i> Allocation Information</h3>
106106
<th scope="row" class="text-nowrap">Service Period:</th>
107107
<td>1 Month</td>
108108
</tr>
109-
{% if offer_letter_code %}
109+
{% if expense_code %}
110110
<tr>
111111
<th scope="row" class="text-nowrap">Requested Expense Code:</th>
112112
<td>
113-
{% for code in offer_letter_code %}
113+
{% for code in expense_code %}
114114
{{ code.value }}<br>
115115
{% endfor %}
116116
</td>
Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
"""Unit tests for the allocation models"""
22

33
from django.test import TestCase
4+
from django.core.exceptions import ValidationError
45

56
from coldfront.core.test_helpers.factories import setup_models, AllocationFactory
67

78
UTIL_FIXTURES = [
89
"coldfront/core/test_helpers/test_data/test_fixtures/ifx.json",
910
]
1011

12+
1113
class AllocationModelTests(TestCase):
1214
"""tests for Allocation model"""
1315
fixtures = UTIL_FIXTURES
1416

1517
@classmethod
1618
def setUpTestData(cls):
17-
"""Set up project to test model properties and methods"""
19+
"""Set up allocation to test model properties and methods"""
1820
setup_models(cls)
1921

2022
def test_allocation_str(self):
@@ -23,9 +25,9 @@ def test_allocation_str(self):
2325
self.proj_allocation.get_parent_resource.name,
2426
self.proj_allocation.project.pi
2527
)
26-
2728
self.assertEqual(str(self.proj_allocation), allocation_str)
2829

30+
2931
def test_allocation_usage_property(self):
3032
"""Test that allocation usage property displays correctly"""
3133
self.assertEqual(self.proj_allocation.usage, 10)
@@ -34,3 +36,27 @@ def test_allocation_usage_property_na(self):
3436
"""Create allocation with no usage. Usage property should return None"""
3537
allocation = AllocationFactory()
3638
self.assertIsNone(allocation.usage)
39+
40+
class AllocationAttributeModelTests(TestCase):
41+
"""Tests for allocationattribute models"""
42+
fixtures = UTIL_FIXTURES
43+
44+
@classmethod
45+
def setUpTestData(cls):
46+
"""Set up allocationattribute to test model properties and methods"""
47+
setup_models(cls)
48+
cls.allocationattribute = cls.proj_allocation.allocationattribute_set.get(
49+
allocation_attribute_type__name='Storage Quota (TB)'
50+
)
51+
52+
def test_allocationattribute_clean_no_error(self):
53+
"""cleaning a numeric value for an int or float AllocationAttributeType produces no error"""
54+
self.allocationattribute.value = "1000"
55+
self.allocationattribute.clean()
56+
57+
def test_allocationattribute_clean_nonnumeric_error(self):
58+
"""cleaning a non-numeric value for int or float AllocationAttributeTypes returns an informative error message"""
59+
60+
self.allocationattribute.value = "1000TB"
61+
with self.assertRaisesMessage(ValidationError, 'Value must be entirely numeric. Please remove any non-numeric characters.'):
62+
self.allocationattribute.clean()

coldfront/core/allocation/views.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868

6969

7070
if 'ifxbilling' in settings.INSTALLED_APPS:
71+
from fiine.client import API as FiineAPI
7172
from ifxbilling.models import Account, UserProductAccount
7273
if 'django_q' in settings.INSTALLED_APPS:
7374
from django_q.tasks import Task
@@ -560,6 +561,10 @@ def form_valid(self, form):
560561
quantity = form_data.get('quantity', 1)
561562
allocation_account = form_data.get('allocation_account', None)
562563

564+
if resource_obj.name == "Tier 3" and quantity % 20 != 0:
565+
form.add_error("quantity", format_html("Tier 3 quantity must be a multiple of 20."))
566+
return self.form_invalid(form)
567+
563568
# A resource is selected that requires an account name selection but user has no account names
564569
if (
565570
ALLOCATION_ACCOUNT_ENABLED
@@ -659,7 +664,19 @@ def form_valid(self, form):
659664
'quantity':quantity,
660665
'nese': nese,
661666
'used_percentage': used_percentage,
667+
'expense_code': expense_code,
668+
'unmatched_code': False,
662669
}
670+
671+
if 'ifxbilling' in settings.INSTALLED_APPS:
672+
try:
673+
matched_fiineaccts = FiineAPI.listAccounts(code=expense_code)
674+
if not matched_fiineaccts:
675+
other_vars['unmatched_code'] = True
676+
except Exception:
677+
#Not authorized to use accounts_list
678+
pass
679+
663680
send_allocation_admin_email(
664681
allocation_obj,
665682
'New Allocation Request',
@@ -1800,7 +1817,6 @@ def post(self, request, *args, **kwargs):
18001817
if new_value != attribute_change.new_value:
18011818
attribute_change.new_value = new_value
18021819
attribute_change.save()
1803-
18041820
if action == 'update':
18051821
message = 'Allocation change request updated!'
18061822
if action == 'approve':
@@ -1986,11 +2002,21 @@ def post(self, request, *args, **kwargs):
19862002

19872003
if form_data.get('end_date_extension') != 0:
19882004
change_requested = True
2005+
2006+
# if requested resource is on NESE, add to vars
2007+
nese = bool(allocation_obj.resources.filter(name__contains="nesetape"))
2008+
19892009
if attrs_to_change:
19902010
for entry in formset:
19912011
formset_data = entry.cleaned_data
19922012

19932013
new_value = formset_data.get('new_value')
2014+
# require nese shares to be divisible by 20
2015+
tbs = int(new_value) if formset_data['name'] == 'Storage Quota (TB)' else False
2016+
if nese and tbs and tbs % 20 != 0:
2017+
messages.error(request, "Tier 3 quantity must be a multiple of 20.")
2018+
return HttpResponseRedirect(reverse('allocation-change', kwargs={'pk': pk}))
2019+
19942020
if new_value != '':
19952021
change_requested = True
19962022
allocation_attribute = AllocationAttribute.objects.get(
@@ -2027,17 +2053,19 @@ def post(self, request, *args, **kwargs):
20272053
for a in attribute_changes_to_make
20282054
if a[0].allocation_attribute_type.name == 'Storage Quota (TB)'
20292055
]
2030-
# if requested resource is on NESE, add to vars
2031-
nese = bool(allocation_obj.resources.filter(name__contains="nesetape"))
20322056

20332057
email_vars = {'justification': justification}
20342058
if quantity:
20352059
quantity_num = int(float(quantity[0][1]))
20362060
difference = quantity_num - int(float(allocation_obj.size))
20372061
used_percentage = allocation_obj.get_parent_resource.used_percentage
2062+
current_size = allocation_obj.size
2063+
if nese:
2064+
current_size = round(current_size, -1)
2065+
difference = round(difference, -1)
20382066
email_vars['quantity'] = quantity_num
20392067
email_vars['nese'] = nese
2040-
email_vars['current_size'] = allocation_obj.size
2068+
email_vars['current_size'] = current_size
20412069
email_vars['difference'] = difference
20422070
email_vars['used_percentage'] = used_percentage
20432071

coldfront/core/portal/views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def home(request):
3434
& Q(projectuser__status__name='Active')
3535
)
3636
)
37-
).distinct().order_by('-created')[:5]
37+
).distinct().order_by('-created')
3838

3939
allocation_list = Allocation.objects.filter(
4040
Q(status__name__in=['Active', 'New', 'Renewal Requested', ]) &
@@ -44,7 +44,7 @@ def home(request):
4444
(Q(project__projectuser__role__name='Manager') |
4545
Q(allocationuser__user=request.user) &
4646
Q(allocationuser__status__name='Active'))
47-
).distinct().order_by('-created')[:5]
47+
).distinct().order_by('-created')
4848

4949
managed_allocations = Allocation.objects.filter(
5050
Q(status__name__in=['Active', 'New', 'Renewal Requested', ])

coldfront/core/project/templates/project/project_detail.html

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,8 @@ <h3 class="d-inline" id="research_outputs"><i class="far fa-newspaper" aria-hidd
601601
<!-- End Admin Messages -->
602602

603603
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/dt/jszip-2.5.0/dt-1.10.24/af-2.3.5/b-1.7.0/b-colvis-1.7.0/b-html5-1.7.0/b-print-1.7.0/cr-1.5.3/date-1.0.2/fc-3.3.2/kt-2.6.1/r-2.2.7/rg-1.1.2/rr-1.2.7/sl-1.3.2/datatables.min.css"/>
604+
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.36/pdfmake.min.js"></script>
605+
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.36/vfs_fonts.js"></script>
604606
<script type="text/javascript" src="https://cdn.datatables.net/v/dt/jszip-2.5.0/dt-1.10.24/af-2.3.5/b-1.7.0/b-colvis-1.7.0/b-html5-1.7.0/b-print-1.7.0/cr-1.5.3/date-1.0.2/fc-3.3.2/kt-2.6.1/r-2.2.7/rg-1.1.2/rr-1.2.7/sl-1.3.2/datatables.min.js"></script>
605607

606608

@@ -677,34 +679,14 @@ <h3 class="d-inline" id="research_outputs"><i class="far fa-newspaper" aria-hidd
677679
}],
678680
dom: 'B<"clear">lfrtip',
679681
order: [[ 3, "desc" ]],
680-
buttons: [
681-
{
682-
name: 'primary',
683-
extend: 'collection',
684-
background: false,
685-
autoClose: true,
686-
text: 'Export',
687-
buttons: [ 'csv', 'excel', 'pdf' ]
688-
}
689-
// {
690-
// name: 'toggleusers',
691-
// text: function() {
692-
// return $('#projectuser_table').attr('filter') == "on" ? 'Show All Users' : "Show Active Users"
693-
// },
694-
// action: function(e, dt, node, config) {
695-
// var table = $('#projectuser_table');
696-
// var filter = table.attr('filter') === "on" ? 'off' : "on";
697-
// document.querySelector('#projectuser_table').setAttribute('filter', filter);
698-
// if (filter == 'on') {
699-
// $.fn.dataTable.ext.search.push(
700-
// function(settings, data, dataIndex) {
701-
// return $(dt.row(dataIndex).node()).attr('status') == "Active";
702-
// });
703-
// } else {$.fn.dataTable.ext.search.pop();}
704-
// table.DataTable().draw();
705-
// this.text(filter == 'on' ? 'Show All Users' : "Show Active Users")
706-
// }
707-
]
682+
buttons: [{
683+
name: 'primary',
684+
extend: 'collection',
685+
background: false,
686+
autoClose: true,
687+
text: 'Export',
688+
buttons: [ 'csv', 'excel', 'pdf' ]
689+
}]
708690
});
709691

710692
$('#allocation_history').DataTable({

coldfront/core/resource/management/commands/add_resource_defaults.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,26 @@ def handle(self, *args, **options):
7676
for name, desc, is_public, rtype, parent_name, default_value in (
7777
('Tier 0', 'Bulk - Lustre', True, storage_tier, None, 1),
7878
('Tier 1', 'Enterprise - Isilon', True, storage_tier, None, 1),
79+
('Tier 2', 'CEPH storage', True, storage_tier, None, 1),
7980
('Tier 3', 'Attic Storage - Tape', True, storage_tier, None, 20),
8081
('holylfs04/tier0', 'Holyoke data center lustre storage', True, storage, 'Tier 0', 1),
8182
('holylfs05/tier0', 'Holyoke data center lustre storage', True, storage, 'Tier 0', 1),
8283
('nesetape/tier3', 'Cold storage for past projects', True, storage, 'Tier 3', 20),
8384
('holy-isilon/tier1', 'Tier1 storage with snapshots and disaster recovery copy', True, storage, 'Tier 1', 1),
8485
('bos-isilon/tier1', 'Tier1 storage for on-campus storage mounting', True, storage, 'Tier 1', 1),
8586
('holystore01/tier0', 'Luster storage under Tier0', True, storage, 'Tier 0', 1),
87+
('b-nfs02-p/tier2', 'Tier2 CEPH storage', True, storage, 'Tier 2', 1),
88+
('b-nfs03-p/tier2', 'Tier2 CEPH storage', True, storage, 'Tier 2', 1),
89+
('b-nfs04-p/tier2', 'Tier2 CEPH storage', True, storage, 'Tier 2', 1),
90+
('b-nfs05-p/tier2', 'Tier2 CEPH storage', True, storage, 'Tier 2', 1),
91+
('b-nfs06-p/tier2', 'Tier2 CEPH storage', True, storage, 'Tier 2', 1),
92+
('b-nfs07-p/tier2', 'Tier2 CEPH storage', True, storage, 'Tier 2', 1),
93+
('b-nfs08-p/tier2', 'Tier2 CEPH storage', True, storage, 'Tier 2', 1),
94+
('b-nfs09-p/tier2', 'Tier2 CEPH storage', True, storage, 'Tier 2', 1),
95+
('h-nfs16-p/tier2', 'Tier2 CEPH storage', True, storage, 'Tier 2', 1),
96+
('h-nfs17-p/tier2', 'Tier2 CEPH storage', True, storage, 'Tier 2', 1),
97+
('h-nfs18-p/tier2', 'Tier2 CEPH storage', True, storage, 'Tier 2', 1),
98+
('h-nfs19-p/tier2', 'Tier2 CEPH storage', True, storage, 'Tier 2', 1),
8699
):
87100

88101
resource_defaults = {

0 commit comments

Comments
 (0)