Skip to content

Commit

Permalink
Merge pull request #567 from ucb-rit/develop
Browse files Browse the repository at this point in the history
Allow admin management of billing IDs; improve user-facing wording
  • Loading branch information
matthew-li authored Sep 27, 2023
2 parents 34e473e + be6da5f commit 6111d32
Show file tree
Hide file tree
Showing 8 changed files with 461 additions and 26 deletions.
118 changes: 107 additions & 11 deletions coldfront/core/billing/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,35 @@
from django.core.validators import RegexValidator

from coldfront.core.billing.models import BillingActivity
from coldfront.core.billing.utils.queries import get_billing_activity_from_full_id
from coldfront.core.billing.utils.validation import is_billing_id_valid
from coldfront.core.project.models import Project


# TODO: Replace this module with a directory as needed.


class BillingActivityChoiceField(forms.ModelChoiceField):

@staticmethod
def label_from_instance(obj):
return obj.full_id()


class BillingIDValidityMixin(object):

def __init__(self, *args, **kwargs):
self.is_billing_id_invalid = False
super().__init__(*args, **kwargs)

def validate_billing_id(self, billing_id, ignore_invalid=False):
if not is_billing_id_valid(billing_id):
self.is_billing_id_invalid = True
if not ignore_invalid:
raise forms.ValidationError(
f'Project ID {billing_id} is not currently valid.')


def billing_id_validators():
"""Return a list of validators for billing IDs."""
return [
Expand All @@ -24,7 +46,7 @@ def billing_id_validators():
]


class BillingIDValidationForm(forms.Form):
class BillingIDValidationForm(BillingIDValidityMixin, forms.Form):

billing_id = forms.CharField(
help_text='Example: 123456-789',
Expand All @@ -38,26 +60,100 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

def clean_billing_id(self):
"""Return the BillingActivity representing the given billing ID
if it exists, and optionally, is valid. Otherwise, raise a
ValidationError."""
"""Return the given billing ID if it exists, and optionally, is
valid. Otherwise, raise a ValidationError."""
billing_id = self.cleaned_data['billing_id']
self.validate_billing_id(
billing_id, ignore_invalid=not self.enforce_validity)
return billing_id


class BillingIDCreationForm(BillingIDValidityMixin, forms.Form):

billing_id = forms.CharField(
help_text='Example: 123456-789',
label='Project ID',
max_length=10,
required=True,
validators=billing_id_validators())
ignore_invalid = forms.BooleanField(
initial=False,
label='Create the Project ID even if it is invalid.',
required=False)

def clean_billing_id(self):
billing_id = self.cleaned_data['billing_id']
if self.enforce_validity and not is_billing_id_valid(billing_id):
if not is_billing_id_valid(billing_id):
return billing_id
if get_billing_activity_from_full_id(billing_id):
raise forms.ValidationError(
f'Project ID {billing_id} is not currently valid.')
f'Project ID {billing_id} already exists.')
return billing_id

def clean(self):
"""Disallow invalid billing IDs from being created, unless the
user explicitly allows it."""
cleaned_data = super().clean()
billing_id = cleaned_data.get('billing_id', None)
if not billing_id:
# Validation failed.
return cleaned_data
ignore_invalid = cleaned_data.get('ignore_invalid')
self.validate_billing_id(billing_id, ignore_invalid=ignore_invalid)
return cleaned_data

class BillingActivityChoiceField(forms.ModelChoiceField):

@staticmethod
def label_from_instance(obj):
return obj.full_id()
class BillingIDBaseSetForm(BillingIDValidityMixin, forms.Form):

billing_activity = BillingActivityChoiceField(
label='Project ID',
queryset=BillingActivity.objects.all(),
required=True)
ignore_invalid = forms.BooleanField(
initial=False,
label='Set the Project ID even if it is invalid.',
required=False)

def clean(self):
"""Disallow invalid billing IDs from being set, unless the user
explicitly allows it."""
cleaned_data = super().clean()
billing_activity = cleaned_data.get('billing_activity')
billing_id = billing_activity.full_id()
ignore_invalid = cleaned_data.get('ignore_invalid')
self.validate_billing_id(billing_id, ignore_invalid=ignore_invalid)
return cleaned_data


class BillingIDSetProjectDefaultForm(BillingIDBaseSetForm):

project = forms.CharField(
widget=forms.TextInput(attrs={'readonly': 'readonly'}))

field_order = ['project', 'billing_activity', 'ignore_invalid']


class BillingIDSetRechargeForm(BillingIDBaseSetForm):

project = forms.CharField(
widget=forms.TextInput(attrs={'readonly': 'readonly'}))
user = forms.CharField(
widget=forms.TextInput(attrs={'readonly': 'readonly'}))

field_order = ['project', 'user', 'billing_activity', 'ignore_invalid']


class BillingIDSetUserAccountForm(BillingIDBaseSetForm):

user = forms.CharField(
widget=forms.TextInput(attrs={'readonly': 'readonly'}))

field_order = ['user', 'billing_activity', 'ignore_invalid']


class BillingIDUsagesSearchForm(forms.Form):

billing_id = BillingActivityChoiceField(
billing_activity = BillingActivityChoiceField(
help_text=(
'Filter results to only include usages of the selected ID. If an '
'ID does not appear in the list, then there are no usages.'),
Expand Down
27 changes: 27 additions & 0 deletions coldfront/core/billing/templates/billing/billing_id_create.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{% extends "common/base.html" %}
{% load common_tags %}
{% load crispy_forms_tags %}
{% load static %}

{% block title %}
Create LBL Project ID
{% endblock %}

{% block content %}

<h1>Create LBL Project ID</h1>
<hr>

<form method="post">
{% csrf_token %}
{{ form|crispy }}
<input type="submit" class="btn btn-primary" value="Create">
</form>

<script>
$("#navbar-main > ul > li.active").removeClass("active");
$("#navbar-admin").addClass("active");
$("#navbar-billing-id-usages").addClass("active");
</script>

{% endblock %}
37 changes: 37 additions & 0 deletions coldfront/core/billing/templates/billing/billing_id_set.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{% extends "common/base.html" %}
{% load common_tags %}
{% load crispy_forms_tags %}
{% load static %}

{% block title %}
Set LBL Project ID: {{ billing_id_type_label }}
{% endblock %}

{% block content %}
<script type='text/javascript' src="{% static 'selectize/selectize.min.js' %}"></script>
<link rel='stylesheet' type='text/css' href="{% static 'selectize/selectize.bootstrap3.css' %}"/>

<h1>Set LBL Project ID: {{ billing_id_type_label }}</h1>
<hr>

<form method="post">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.GET.next }}">
{{ form|crispy }}
<input type="submit" class="btn btn-primary" value="Set">
</form>

<script>
$('select').selectize({
create: false,
sortField: 'text'
});
</script>

<script>
$("#navbar-main > ul > li.active").removeClass("active");
$("#navbar-admin").addClass("active");
$("#navbar-billing-id-usages").addClass("active");
</script>

{% endblock %}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@
<h1>LBL Project ID Usages</h1>
<hr>

<!-- Create -->
{% if user.is_superuser %}
<a class="class btn btn-primary" href="{% url 'billing-id-create' %}">
<i class="fas fa-plus" aria-hidden="true"></i>
Create
</a>
<hr>
{% endif %}

<div class="mb-3" id="accordion">
<div class="card">
<div class="card-header">
Expand Down Expand Up @@ -55,7 +64,15 @@ <h3>Project Default</h3>
{% for usage in project_default_usages %}
<tr>
<td>{{ usage.project_name }}</td>
<td>{{ usage.full_id }}</td>
<td>
{{ usage.full_id }}
{% if user.is_superuser %}
&nbsp;
<a href="{% url 'billing-id-set' billing_id_type='project_default' %}?project={{ usage.project_pk }}&{{ next_url_parameter }}">
<i class="fas fa-edit" aria-hidden="true"></i>
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
Expand Down Expand Up @@ -86,7 +103,15 @@ <h3>Recharge</h3>
<tr>
<td>{{ usage.project_name }}</td>
<td>{{ usage.username }}</td>
<td>{{ usage.full_id }}</td>
<td>
{{ usage.full_id }}
{% if user.is_superuser %}
&nbsp;
<a href="{% url 'billing-id-set' billing_id_type='recharge' %}?project={{ usage.project_pk }}&user={{ usage.user_pk }}&{{ next_url_parameter }}">
<i class="fas fa-edit" aria-hidden="true"></i>
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
Expand All @@ -113,7 +138,15 @@ <h3>User Account</h3>
{% for usage in user_account_usages %}
<tr>
<td>{{ usage.username }}</td>
<td>{{ usage.full_id }}</td>
<td>
{{ usage.full_id }}
{% if user.is_superuser %}
&nbsp;
<a href="{% url 'billing-id-set' billing_id_type='user_account' %}?user={{ usage.user_pk }}&{{ next_url_parameter }}">
<i class="fas fa-edit" aria-hidden="true"></i>
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
Expand Down
6 changes: 6 additions & 0 deletions coldfront/core/billing/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@

with flagged_paths('LRC_ONLY') as path:
flagged_url_patterns = [
path('create/',
admin_views.BillingIDCreateView.as_view(),
name='billing-id-create'),
path('set/<str:billing_id_type>/',
admin_views.BillingIDSetView.as_view(),
name='billing-id-set'),
path('usages/',
admin_views.BillingIDUsagesSearchView.as_view(),
name='billing-id-usages'),
Expand Down
17 changes: 17 additions & 0 deletions coldfront/core/billing/utils/billing_activity_managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ def billing_activity(self, billing_activity):
else:
self._create_container_with_value(value)

@property
@abstractmethod
def entity_str(self):
pass

@abstractmethod
def _create_container_with_value(self, value):
"""Create the container to store the given value, and set the
Expand Down Expand Up @@ -119,6 +124,10 @@ def __init__(self, project):
self._allocation = get_project_compute_allocation(project)
super().__init__(project)

@property
def entity_str(self):
return self._entity.name

def _create_container_with_value(self, value):
self._container = AllocationAttribute.objects.create(
allocation_attribute_type=self._allocation_attribute_type,
Expand Down Expand Up @@ -156,6 +165,10 @@ def __init__(self, project_user):
allocation=self._allocation, user=project_user.user)
super().__init__(project_user)

@property
def entity_str(self):
return f'({self._entity.project.name}, {self._entity.user.username})'

def _create_container_with_value(self, value):
self._container = self.container_type.objects.create(
allocation_attribute_type=self._allocation_attribute_type,
Expand Down Expand Up @@ -190,6 +203,10 @@ class UserBillingActivityManager(BillingActivityManager):
def __init__(self, user):
super().__init__(user)

@property
def entity_str(self):
return self._entity.username

def _create_container_with_value(self, value):
self._container = self.container_type.objects.create(
user=self._entity,
Expand Down
Loading

0 comments on commit 6111d32

Please sign in to comment.