diff --git a/allocations/admin.py b/allocations/admin.py
index 891a9249..8fc1aa0d 100644
--- a/allocations/admin.py
+++ b/allocations/admin.py
@@ -1,4 +1,7 @@
+from django import forms
from django.contrib import admin
+from django.utils.safestring import mark_safe
+from django.urls import reverse
from util.keycloak_client import KeycloakClient
@@ -18,58 +21,291 @@ def project_description(self, obj):
def project_title(self, obj):
return str(obj.project.title)
- def pi_name(self, obj):
- return f"{obj.project.pi.first_name} {obj.project.pi.last_name}"
+ def pi_info(self, obj):
+ keycloak_client = KeycloakClient()
+ kc_user = keycloak_client.get_user_by_username(obj.project.pi.username)
+ if not kc_user:
+ kc_user = {}
+ institution = kc_user.get("attributes", {}).get("affiliationInstitution")
+ country = kc_user.get("attributes", {}).get("country")
- def pi_email(self, obj):
- return f"{obj.project.pi.email}"
+ return mark_safe(f"""
+
+ Name |
+ {obj.project.pi.first_name} {obj.project.pi.last_name} |
+
+
+ Email |
+
+ {obj.project.pi.email}
+ |
+
+
+ Institution | {institution} |
+
+
+ Country | {country} |
+
+
+ """)
- def pi_institution(self, obj):
- keycloak_client = KeycloakClient()
- uname = obj.project.pi.username
- user = keycloak_client.get_user_by_username(uname)
- return user.get("attributes", {}).get("affiliationInstitution", "")
+ def project_info(self, obj):
+ return mark_safe(f"""
+
+ Charge Code | {obj.project.charge_code} |
+
+
+ Title | {obj.project.title} |
+
+
+ Abstract | {obj.project.description} |
+
+
+ Tag | {obj.project.tag.name} - {obj.project.tag.description} |
+
+ """)
+
+ def allocation_status(self, obj):
+ if obj.status not in ["pending", "waiting"]:
+ return f"This allocation is {obj.status}."
+
+ rows = []
+ rows.append(f"""
+ {obj.id} |
+ {obj.requestor} |
+ {obj.date_requested.date()} |
+ |
+ {obj.su_requested} |
+ {obj.justification} |
+
+
+
+
+ |
+
""")
+
+ styles = """
+
+ """
+ approve_modal = f"""
+
+
Allocation Approval
+
+
+
+
+ """
+ reject_modal = f"""
+
+
Allocation Approval
+
+ Project | {obj.project.charge_code} |
+ Allocation ID | {obj.id} |
+ Requestor | {obj.requestor} |
+ Date Requested | {obj.date_requested.date()} |
+ Compute Requested | {obj.su_requested} |
+
+ Decision Summary |
+
+
+ |
+
+
+
+
+
+ """
+ contact_modal = """
+ """
+
+ return mark_safe(f"""
+
+
+
+ ID |
+ Requestor |
+ Date Requested |
+ Status |
+ SU Requested |
+ Justification |
+ Actions |
+
+
+
+ {"
".join(rows)}
+
+
+ {styles}
+ {approve_modal}
+ {reject_modal}
+ {contact_modal}
+ """)
+
+ def previous_allocations(self, obj):
+ rows = []
+ for alloc in sorted(obj.project.allocations.exclude(status="pending"), reverse=True, key=lambda x: x.date_requested):
+ rows.append(f"""
+ {alloc.id} |
+ {alloc.requestor} |
+ {alloc.date_requested.date()} |
+ {alloc.status} |
+ {alloc.start_date.date() if alloc.start_date else ""} |
+ {alloc.expiration_date.date() if alloc.expiration_date else ""} |
+ {alloc.su_used if alloc.su_used else ""} |
+ {alloc.su_allocated if alloc.su_allocated else ""} |
+ {alloc.su_requested} |
+ Expand{alloc.justification} |
+
+
+ Expand
+
+ - Reviewer - {alloc.reviewer if alloc.reviewer else ""}
+ - Date Reviewed - {alloc.date_reviewed.date() if alloc.date_reviewed else ""}
+ - Summary - {alloc.decision_summary}
+
+
+ |
+
""")
+ return mark_safe(f"""
+
+
+
+ ID |
+ Requestor |
+ Date Requested |
+ Status |
+ Start |
+ End |
+ SU Usage |
+ SU Allocated |
+ SU Requested |
+ Justification |
+ Decision summary |
+
+
+
+ {"
".join(rows)}
+
+
+ """)
list_display = (
- "project_title",
"project",
"status",
"date_requested",
"date_reviewed",
"reviewer",
)
- fields = (
- "pi_name",
- "pi_email",
- "pi_institution",
- "project",
- "project_title",
- "project_description",
- "justification",
- "status",
- "requestor",
- "decision_summary",
- "reviewer",
- "date_requested",
- "date_reviewed",
- "start_date",
- "expiration_date",
- "su_allocated",
+ fieldsets = (
+ (None, {
+ "fields": (
+ "project_info",
+ "pi_info",
+ "allocation_status",
+ "previous_allocations",
+ "status",
+ "start_date",
+ "expiration_date",
+ ),
+ }),
+ # TODO other allocations
)
readonly_fields = [
- "pi_name",
- "pi_email",
- "pi_institution",
- "project",
- "project_title",
- "project_description",
- "justification",
- "status",
- "requestor",
- "decision_summary",
- "reviewer",
- "date_requested",
- "date_reviewed",
+ "pi_info",
+ "project_info",
+ "allocation_status",
+ "previous_allocations",
]
ordering = ["-date_requested"]
search_fields = [
@@ -79,6 +315,16 @@ def pi_institution(self, obj):
]
list_filter = ["status", "date_requested"]
inlines = [ChargeInline]
+ # form = ReviewAllocationForm
+
+ class Media:
+ css = {
+ "all": ("/static/allocations/css/admin.css",),
+ }
+ js = (
+ '/static/allocations/js/admin.js',
+ '/static/scripts/cannedresponses.js',
+ )
admin.site.register(Allocation, AllocationAdmin)
diff --git a/allocations/static/allocations/css/admin.css b/allocations/static/allocations/css/admin.css
new file mode 100644
index 00000000..fcbbdfa8
--- /dev/null
+++ b/allocations/static/allocations/css/admin.css
@@ -0,0 +1,16 @@
+.admin-pi .flex-container label {
+ display: none;
+}
+
+.admin-pi .form-row {
+ padding-bottom: 0;
+ display: inline-block;
+}
+
+.admin-pi .form-row .flex-container {
+ margin-right: 2rem;
+}
+
+fieldset.admin-pi div.form-row div.flex-container div.readonly {
+ width: 100%;
+}
diff --git a/allocations/static/allocations/css/main.css b/allocations/static/allocations/css/main.css
index 2279e1d7..876f9381 100644
--- a/allocations/static/allocations/css/main.css
+++ b/allocations/static/allocations/css/main.css
@@ -28,4 +28,4 @@ td.ng-binding{
td p { margin: 0px;}
-.allocations-header{border-bottom:2px solid #000}a:hover{cursor:hand;cursor:pointer}.btn,.btn:hover,button:hover{background:none!important;box-shadow:none!important}.btn-danger,.btn-danger:hover{color:#fff!important;background-color:#C53C36!important;border-color:#C53C36!important}.btn-info,.btn-info:hover{color:#fff!important;background-color:#31B0D5!important;border-color:#31B0D5!important}.btn-primary,.btn-primary:hover{color:#fff!important;background-color:#3071A9!important;border-color:#3071A9!important}.btn-success,.btn-success:hover{color:#fff!important;background-color:#59B259!important;border-color:#59B259!important}.btn-warning,.btn-warning:hover{color:#fff!important;background-color:#F89406!important;border-color:#F89406!important}.form-group{margin:10px}.list-group-item{padding:5px}.modal-footer{border-top:0}.modal-header{padding:15px 15px 0;border-bottom:0}.no-border{border:0}.ng-cloak,[ng-cloak],[ng\:cloak]{display:none!important}.row{margin:0}table{margin:0 0!important}.more-text.show-inline{display:inline!important}
\ No newline at end of file
+.allocations-header{border-bottom:2px solid #000}a:hover{cursor:hand;cursor:pointer}.btn,.btn:hover,button:hover{background:none!important;box-shadow:none!important}.btn-danger,.btn-danger:hover{color:#fff!important;background-color:#C53C36!important;border-color:#C53C36!important}.btn-info,.btn-info:hover{color:#fff!important;background-color:#31B0D5!important;border-color:#31B0D5!important}.btn-primary,.btn-primary:hover{color:#fff!important;background-color:#3071A9!important;border-color:#3071A9!important}.btn-success,.btn-success:hover{color:#fff!important;background-color:#59B259!important;border-color:#59B259!important}.btn-warning,.btn-warning:hover{color:#fff!important;background-color:#F89406!important;border-color:#F89406!important}.form-group{margin:10px}.list-group-item{padding:5px}.modal-footer{border-top:0}.modal-header{padding:15px 15px 0;border-bottom:0}.no-border{border:0}.ng-cloak,[ng-cloak],[ng\:cloak]{display:none!important}.row{margin:0}table{margin:0 0!important}.more-text.show-inline{display:inline!important}
diff --git a/allocations/static/allocations/js/admin.js b/allocations/static/allocations/js/admin.js
new file mode 100644
index 00000000..08f898ef
--- /dev/null
+++ b/allocations/static/allocations/js/admin.js
@@ -0,0 +1,147 @@
+document.addEventListener('DOMContentLoaded', () => {
+ // Opening/closing modal code
+ const modal = document.getElementById("alloc-modal-approve");
+ const modalReject = document.getElementById("alloc-modal-reject");
+ const modalContact = document.getElementById("alloc-modal-contact");
+ const closeButton = document.getElementById("close-btn");
+ const approveBtn = document.getElementById("approve-btn");
+ const rejectBtn = document.getElementById("reject-btn");
+ const contactBtn = document.getElementById("contact-btn");
+ const closeBtnReject = document.getElementById("close-btn-reject");
+ const closeBtnContact = document.getElementById("close-btn-contact");
+ approveBtn.addEventListener("click", function () {
+ modal.style.display = "block";
+ });
+ rejectBtn.addEventListener("click", function () {
+ modalReject.style.display = "block";
+ });
+ contactBtn.addEventListener("click", function () {
+ modalContact.style.display = "block";
+ });
+ closeButton.addEventListener("click", function () {
+ modal.style.display = "none";
+ });
+ closeBtnReject.addEventListener("click", function () {
+ modalReject.style.display = "none";
+ });
+ closeBtnContact.addEventListener("click", function () {
+ modalContact.style.display = "none";
+ });
+ window.onclick = function(event) {
+ if (event.target == modal) {
+ modal.style.display = "none";
+ }
+ if (event.target == modalContact) {
+ modalContact.style.display = "none"
+ }
+ if (event.target == modalReject) {
+ modalReject.style.display = "none";
+ }
+ }
+
+ // Setting defaults on form
+ const inputStartDate = document.getElementById("alloc-start-date");
+ const today = new Date();
+ inputStartDate.value = today.toISOString().split('T')[0];
+
+ const inputEndDate = document.getElementById("alloc-end-date");
+ const sixMonthsLater = new Date();
+ sixMonthsLater.setMonth((today.getMonth() + 6) % 12);
+ sixMonthsLater.setFullYear(today.getFullYear() + ((today.getMonth() + 6) >= 12 ? 1 : 0));
+ if (sixMonthsLater.getDate() !== today.getDate()) {
+ sixMonthsLater.setDate(0); // Set to the last day of the previous month
+ }
+ inputEndDate.value = sixMonthsLater.toISOString().split('T')[0];
+
+ // Handling for prefilled responses
+ const decisionSummary = document.getElementById("idDecisionSummary")
+ function updateDecisionSummary() {
+ const selectedType = typeSelect.value;
+ if (selectedType == 'new') {
+ decisionSummary.value=allocationNewApproval;
+ } else if (selectedType == 'renewal') {
+ decisionSummary.value=allocationRenewalApproval;
+ } else if (selectedType == 'recharge') {
+ decisionSummary.value=allocationRechargeApproval;
+ } else {
+ decisionSummary.value="";
+ }
+ }
+ const typeSelect = document.getElementById("allocationApprovalType")
+ typeSelect.addEventListener("change", updateDecisionSummary);
+
+ function formatErrors(errors) {
+ return Object.entries(errors)
+ .map(([field, message]) => `- ${field.charAt(0).toUpperCase() + field.slice(1)}: ${message}`)
+ .join("\n");
+ }
+
+ // Submitting approval
+ const approveSubmitBtn = document.getElementById("approve-submit-btn");
+ approveSubmitBtn.addEventListener("click", function () {
+ let payload = {
+ "status": "approved",
+ "dateReviewed": new Date().toISOString().split("T")[0],
+ "start": inputStartDate.value,
+ "end": inputEndDate.value,
+ "decisionSummary": document.getElementById("idDecisionSummary").value,
+ "computeAllocated": parseInt(document.getElementById("alloc-compute-allocated").value),
+ "project": document.getElementById("chargeCode").value,
+ "requestorId": document.getElementById("requestorId").value,
+ "id": document.getElementById("allocationId").value,
+ }
+ fetch("/admin/allocations/approval/", {
+ method: "POST",
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ "X-CSRFToken": document.querySelector('[name=csrfmiddlewaretoken]').value,
+ },
+ body: JSON.stringify(payload)
+ })
+ .then( res => res.json())
+ .then( res => {
+ if(res.status == "error"){
+ console.error(res.errors)
+ alert(`Error approving allocation:\n${formatErrors(res.errors)}`)
+ } else {
+ alert("Approved allocation")
+ location.reload(); // Refresh admin form
+ }
+ });
+ });
+
+ const contactSubmitBtn = document.getElementById("contact-submit-btn")
+ const inputContactMessage = document.getElementById("idContactMessage")
+ contactSubmitBtn.addEventListener("click", function(){
+ let message = inputContactMessage.value
+ let payload = {
+ "allocation": {
+ "status": document.getElementById("allocationStatus").value,
+ "id": document.getElementById("allocationId").value,
+ },
+ "rt": {
+ "requestor": document.getElementById("requestor").value, // requestor username
+ "problem_description": message,
+ },
+ }
+ fetch("/admin/allocations/contact/", {
+ method: "POST",
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ "X-CSRFToken": document.querySelector('[name=csrfmiddlewaretoken]').value,
+ },
+ body: JSON.stringify(payload)
+ })
+ .then( res => res.json())
+ .then( res => {
+ if(res.status == "error"){
+ alert(`Error contacting PI:\n${formatErrors(res.errors)}`)
+ } else {
+ alert(`Successfully contacted PI.`)
+ location.reload(); // Refresh admin form
+ }
+ });
+ })
+});
diff --git a/allocations/views.py b/allocations/views.py
index cb2f0971..a14e0a81 100644
--- a/allocations/views.py
+++ b/allocations/views.py
@@ -136,9 +136,6 @@ def approval(request):
logger.info("Allocation approval requested by admin: %s", request.user)
logger.info("Allocation approval request data: %s", json.dumps(data))
validate_datestring = validators.RegexValidator(r"^\d{4}-\d{2}-\d{2}$")
- validate_datetimestring = validators.RegexValidator(
- r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$"
- )
if not data["decisionSummary"]:
errors["decisionSummary"] = "Decision Summary is required."
@@ -184,44 +181,6 @@ def approval(request):
except ValueError:
errors["computeAllocated"] = "Compute Allocated must be a number."
- if data["computeRequested"]:
- try:
- data["computeRequested"] = int(data["computeRequested"])
- except ValueError:
- errors["computeRequested"] = "Compute Requested must be a number."
-
- if data["storageAllocated"]:
- try:
- data["storageAllocated"] = int(data["storageAllocated"])
- except ValueError:
- errors["storageAllocated"] = "Storage Allocated must be a number."
-
- if data["storageRequested"]:
- try:
- data["storageRequested"] = int(data["storageRequested"])
- except ValueError:
- errors["storageRequested"] = "Storage Requested must be a number."
-
- if data["memoryAllocated"]:
- try:
- data["memoryAllocated"] = int(data["memoryAllocated"])
- except ValueError:
- errors["memoryAllocated"] = "Memory Allocated must be a number."
-
- if data["memoryRequested"]:
- try:
- data["memoryRequested"] = int(data["memoryRequested"])
- except ValueError:
- errors["memoryRequested"] = "Memory Requested must be a number."
-
- if data["projectId"]:
- try:
- data["projectId"] = int(data["projectId"])
- except ValueError:
- errors["projectId"] = "Project id must be number."
- else:
- errors["projectId"] = "Project id is required."
-
if not data["project"]:
errors["project"] = "Project charge code is required."
@@ -233,19 +192,6 @@ def approval(request):
else:
errors["reviewerId"] = "Reviewer id is required."
- if not data["reviewer"]:
- errors["reviewer"] = "Reviewer username is required."
-
- if data["dateRequested"]:
- try:
- validate_datetimestring(data["dateRequested"])
- except ValidationError:
- errors["dateRequested"] = (
- 'Requested date must be a valid date string e.g. "2015-05-20T05:00:00Z" .'
- )
- # else:
- # errors['dateRequested'] = 'Requested date is required.'
-
if data["dateReviewed"]:
try:
validate_datestring(data["dateReviewed"])