Skip to content

Commit

Permalink
Enables gsheet Integration for condo procurement
Browse files Browse the repository at this point in the history
- Implement fetching and caching for "Savio" and "LRC" tabs in gsheets.py
- Parse decommission dates and flag alerts based on DECOMMISSION_WARNING_DAYS
- Display decommission alerts in a vertical card layout
- Add decommission_details view and URL route
- Update README with Google Sheets setup and configuration instructions

closes #546
  • Loading branch information
helbashandy committed Feb 11, 2025
1 parent 99d70f3 commit 81b8f0f
Show file tree
Hide file tree
Showing 10 changed files with 342 additions and 21 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Documentation resides in a separate repository. Please request access.
### Miscellaneous Topics

- [Deployment](bootstrap/ansible/README.md)
- [Google Sheets Setup](bootstrap/docs/g-sheets-setup.md)
- [REST API](coldfront/api/README.md)

## License
Expand Down
50 changes: 50 additions & 0 deletions bootstrap/development/docs/g-sheets-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
## Google Sheets Integration

This project uses Google Sheets to track acitivites such as:
- HPC hardware procurement and decommission information.
- TBD



### Setup

1. **Service Account & Credentials:**
- Create a Google Cloud service account with Sheets API (readonly) access.
- Set the environment variable `GOOGLE_SERVICE_ACCOUNT_JSON_PATH` to the path of your JSON credentials.
- Set `DECOMISSION_SHEET_ID` to your target spreadsheet's ID.

2. **Django Settings:**
- **`DECOMMISSION_WARNING_DAYS`**: Days before the decommission date to trigger a warning (default: `30`).
- **`GSHEETS_CACHE_TIMEOUT`**: Cache duration in seconds (default: `86400` for 24 hours).
- **`GSHEETS_DISABLE_CACHE`**: Set to `True` during development/testing to disable caching.

3. ** Docker-compose environment:**
- Add the following to your `docker-compose.yml` file:
```yaml
services:
web:
environment:
- GOOGLE_SERVICE_ACCOUNT_JSON_PATH=/path/to/your/credentials.json
- DECOMISSION_SHEET_ID=your-spreadsheet-id
- GSHEETS_DISABLE_CACHE=True
# ...
```

### Google Sheets Format

- **Tabs:** The spreadsheet must include two tabs: **Savio** and **LRC**.
- **Header:** The header is on row 3 and must include at least:
- `PI Email`
- `Expected Decomission Date` (in MM/DD/YYYY format)
- **Data Rows:** Data starts from row 4.

### HPCS Hardware Procurement Tracking

Upon user login, the application:
- Checks both the **Savio** and **LRC** tabs for a record where `PI Email` matches the logged-in user's email.
- Parses the `Expected Decomission Date` and, if the current date is within `DECOMMISSION_WARNING_DAYS` of that date, flags the record.
- Displays decommission alerts using a card layout that lists each field vertically for clear, readable details.

---

This summary should help users and developers quickly understand and set up the Google Sheets integration and the HPCS Hardware Procurement Tracking functionality.
5 changes: 5 additions & 0 deletions coldfront/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,11 @@
ALLOCATION_MIN = Decimal('0.00')
ALLOCATION_MAX = Decimal('100000000.00')

DECOMMISSION_WARNING_DAYS = 30

GSHEETS_DISABLE_CACHE = True


# Whether to allow all jobs, bypassing all checks at job submission time.
ALLOW_ALL_JOBS = False

Expand Down
2 changes: 2 additions & 0 deletions coldfront/config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
# path('grant/', include('coldfront.core.grant.urls')),
# path('publication/', include('coldfront.core.publication.urls')),
# path('research-output/', include('coldfront.core.research_output.urls')),
path('decommission-details/', portal_views.decommission_details, name='decommission_details'),

path('help', TemplateView.as_view(template_name='portal/help.html'), name='help'),
]

Expand Down
33 changes: 33 additions & 0 deletions coldfront/core/portal/templates/portal/authorized_home.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,44 @@
{% extends "common/base.html" %} {% load common_tags %} {% block content %}

{% load portal_tags %}

<div class="row">
<div class="col-lg-12">
<h2>Welcome</h2>
<hr>

{% include 'portal/feedback_alert.html' %}

{% if decommission_alerts %}
<div class="alert alert-warning">
<strong>Decommission Notice:</strong> Your condo allocation is scheduled for decommissioning.
Please <a href="{% url 'decommission_details' %}">click here</a> for details.

<table class="table table-sm mt-2">
<thead>
<tr>
<th>Hardware Type</th>
<th>Expected Decomission Date</th>
<th>Status</th>
<th>Department Division</th>
<th>Hardware Specification Details</th>
</tr>
</thead>
<tbody>
{% for alert in decommission_alerts %}
<tr>
<td>{{ alert.record|get_item:"Hardware Type" }}</td>
<td>{{ alert.record|get_item:"Expected Decomission Date" }}</td>
<td>{{ alert.record|get_item:"Status" }}</td>
<td>{{ alert.record|get_item:"Department Division" }}</td>
<td>{{ alert.record|get_item:"Hardware Specification Details" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}

<p>If you would like to set up or update your access to a cluster, please complete the following steps.</p>
<p>First review and sign the cluster user agreement. Only then you can join a cluster project and gain access to the cluster.</p>
<table class="table">
Expand Down Expand Up @@ -113,6 +145,7 @@ <h2>Welcome</h2>
<span class="alert alert-danger"><b>No Cluster Account</b></span>
{% endif %}
</div>
</div>
</div>
<br></br>

Expand Down
39 changes: 39 additions & 0 deletions coldfront/core/portal/templates/portal/decommission_details.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{% extends "common/base.html" %}
{% load static %}

{% block title %}
Condo Decommission Details
{% endblock %}

{% block content %}
<div class="container my-4">
<h1>Allocated Condos</h1>
<hr>
{% if decommission_alerts %}
{% for alert in decommission_alerts %}
<div class="card mb-4">
<div class="card-header">
<strong>Cluster:</strong> {{ alert.sheet }}
</div>
<div class="card-body">
{# Iterate over the record dictionary to display each field #}
{% for key, value in alert.record.items %}
<div class="row mb-2">
<div class="col-sm-4 font-weight-bold">
{{ key }}:
</div>
<div class="col-sm-8">
{{ value }}
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
{% else %}
<div class="alert alert-info">
No decommission alerts found!
</div>
{% endif %}
</div>
{% endblock %}
7 changes: 7 additions & 0 deletions coldfront/core/portal/templatetags/portal_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,10 @@ def get_version():
@register.simple_tag
def get_setting(name):
return getattr(settings, name, "")

@register.filter
def get_item(dictionary, key):
"""Usage: {{ my_dict|get_item:"my_key" }}"""
if isinstance(dictionary, dict):
return dictionary.get(key, "")
return ""
59 changes: 38 additions & 21 deletions coldfront/core/portal/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,27 @@
from coldfront.core.project.models import ProjectUserRemovalRequest
from coldfront.core.project.utils import render_project_compute_usage

from coldfront.core.utils.gsheets import get_decommission_alerts_for_user
from django.contrib.auth.decorators import login_required


# from coldfront.core.publication.models import Publication
# from coldfront.core.research_output.models import ResearchOutput


def home(request):

context = {}
if request.user.is_authenticated:
template_name = 'portal/authorized_home.html'
project_list = Project.objects.filter(
(Q(status__name__in=['New', 'Active', ]) &
Q(projectuser__user=request.user) &
Q(projectuser__status__name__in=['Active', 'Pending - Remove']))
Q(status__name__in=['New', 'Active', ]) &
Q(projectuser__user=request.user) &
Q(projectuser__status__name__in=['Active', 'Pending - Remove'])
).distinct().order_by('name')

cluster_access_attributes = AllocationUserAttribute.objects.filter(allocation_attribute_type__name='Cluster Account Status',
allocation_user__user=request.user)
cluster_access_attributes = AllocationUserAttribute.objects.filter(
allocation_attribute_type__name='Cluster Account Status',
allocation_user__user=request.user
)
access_states = {}
for attribute in cluster_access_attributes:
project = attribute.allocation.project
Expand All @@ -49,8 +52,7 @@ def home(request):

for project in project_list:
project.display_status = access_states.get(project, None)
if (project.display_status is not None and
'Active' in project.display_status):
if project.display_status is not None and 'Active' in project.display_status:
context['cluster_username'] = request.user.username

resource_name = get_project_compute_resource_name(project)
Expand All @@ -72,21 +74,24 @@ def home(request):
context['project_list'] = project_list
context['allocation_list'] = allocation_list

num_join_requests = \
ProjectUserJoinRequest.objects.filter(
num_join_requests = ProjectUserJoinRequest.objects.filter(
project_user__status__name='Pending - Add',
project_user__user=request.user). \
order_by('project_user', '-created'). \
distinct('project_user').count()

project_user__user=request.user
).order_by('project_user', '-created').distinct('project_user').count()
context['num_join_requests'] = num_join_requests

context['pending_removal_request_projects'] = \
[removal_request.project_user.project.name
for removal_request in
ProjectUserRemovalRequest.objects.filter(
Q(project_user__user__username=request.user.username) &
Q(status__name='Pending'))]
context['pending_removal_request_projects'] = [
removal_request.project_user.project.name
for removal_request in ProjectUserRemovalRequest.objects.filter(
Q(project_user__user__username=request.user.username) &
Q(status__name='Pending')
)
]

# Add decommission alerts to the context.
alerts = get_decommission_alerts_for_user(request.user.email)
if alerts:
context['decommission_alerts'] = alerts

else:
template_name = 'portal/nonauthorized_home.html'
Expand Down Expand Up @@ -214,3 +219,15 @@ def allocation_summary(request):
context['resources_chart_data'] = resources_chart_data

return render(request, 'portal/allocation_summary.html', context)

@login_required
def decommission_details(request):
"""
Displays a page with detailed information (all fields) for each decommission record
associated with the current user's email.
"""
# Reuse the same helper function to get alerts.
alerts = get_decommission_alerts_for_user(request.user.email)
return render(request, "portal/decommission_details.html", {
"decommission_alerts": alerts
})
Loading

0 comments on commit 81b8f0f

Please sign in to comment.