Skip to content

feat: add csl status to header #1754

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/sourcedoc/intranet.apps.templatetags.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ intranet.apps.templatetags.paginate module
:undoc-members:
:show-inheritance:

intranet.apps.templatetags.status\_helper module
------------------------------------------------

.. automodule:: intranet.apps.templatetags.status_helper
:members:
:undoc-members:
:show-inheritance:

intranet.apps.templatetags.strings module
-----------------------------------------

Expand Down
80 changes: 39 additions & 41 deletions intranet/apps/emerg/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
import logging
import time
from typing import Tuple

import requests
from bs4 import BeautifulSoup
Expand All @@ -14,20 +14,19 @@


def check_emerg():
"""Fetch from FCPS and CSL emergency announcement pages.
"""Fetch from FCPS emergency announcement pages.

URLs defined in settings.FCPS_EMERGENCY_PAGE and settings.CSL_STATUS_PAGE
URLs defined in settings.FCPS_EMERGENCY_PAGE

Request timeout defined in settings.FCPS_EMERGENCY_TIMEOUT

"""
fcps_page = settings.FCPS_EMERGENCY_PAGE
csl_page = settings.CSL_STATUS_PAGE
announcements = []

if settings.EMERGENCY_MESSAGE:
return True, settings.EMERGENCY_MESSAGE
if not fcps_page or not csl_page:
if not fcps_page:
return None, None

timeout = settings.EMERGENCY_TIMEOUT
Expand Down Expand Up @@ -67,44 +66,10 @@ def check_emerg():

announcements.append({"title": f"<a target='_blank' href=\"{get_domain_name(fcps_page)}\">{title}</a>", "body": body})

try:
r = requests.get(csl_page, timeout=timeout)
except requests.exceptions.Timeout:
pass

try:
csl_status = json.loads(r.text)
except json.decoder.JSONDecodeError:
return False, None

for system in csl_status["systems"]:
if system["status"] != "ok":
status = True
issues = system["unresolvedIssues"]
for issue in issues:
desc = requests.get(issue["permalink"], timeout=timeout).text
soup = BeautifulSoup(desc, "html.parser")

text = soup.find_all(["p", "hr"])
desc = text[2 : len(text) - 5]
a = {
"title": f"<a target='_blank' href=\"{get_domain_name(csl_page)}\">{issue['title']}</a>",
"body": "".join(d.prettify() for d in desc),
}
if a not in announcements and issue["severity"] != "notice":
announcements.append(a)

# Not needed due to the filtering of "p" elements, but as a backup:
bad_text = [
'<p><strong class="bold">© tjCSL Status, 2022</strong>&nbsp; • &nbsp; <a href="#">Back to top</a></p>',
"<p>We continuously monitor the status of our services and if there are any interruptions, a note will be posted here.</p>",
]

message = "".join(
[
f"<h3><i class='fas fa-exclamation-triangle'></i>&nbsp; {announcement['title']}</h3><hr />{announcement['body']}\n"
for announcement in announcements
if announcement not in bad_text
]
)

Expand All @@ -117,12 +82,12 @@ def get_emerg_result(*, custom_logger=None):
custom_logger = logger

status, message = check_emerg()
custom_logger.debug("Fetched emergency info from FCPS and CSL status")
custom_logger.debug("Fetched emergency info from FCPS")
return {"status": status, "message": message}


def get_emerg():
"""Get the cached FCPS emergency page and CSL status page, or check it again.
"""Get the cached FCPS emergency page, or check it again.

Timeout defined in settings.CACHE_AGE["emerg"]

Expand Down Expand Up @@ -155,3 +120,36 @@ def update_emerg_cache(*, custom_logger=None) -> None:
key = f"emerg:{timezone.localdate()}"
result = get_emerg_result(custom_logger=custom_logger)
cache.set(key, result, timeout=settings.CACHE_AGE["emerg"])


def get_csl_status() -> Tuple[str, bool]:
"""Get the cached status of the TJCSL status page.

Returns:
Tuple with a string consisting of the aggregate status
of the TJ computer systems lab and a bool indicating whether
the status cache was updated

The string of the tuple will be one of the following: "error" (parse error), "operational", "downtime", "degraded", "maintenance"
"""

status = cache.get("emerg:csl_status")
updated = False

if not status:
response = requests.get(settings.CSL_STATUS_PAGE)
if response.status_code != 200:
status = "error"
logger.error("Could not fetch status page")

else:
try:
status = response.json()["data"]["attributes"]["aggregate_state"]
updated = True
except KeyError as e:
status = "error"
logger.error("Unexpected status page JSON format. %s", e)

cache.set("emerg:csl_status", status, settings.CACHE_AGE["csl_status"])

return status, updated
36 changes: 36 additions & 0 deletions intranet/apps/templatetags/status_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from urllib.parse import urlparse, urlunparse

from django import template
from django.core.cache import cache

from intranet import settings
from intranet.apps.emerg.views import get_csl_status

register = template.Library()


@register.simple_tag
def get_cache(key):
return cache.get(key)


class GetCSLStatusNode(template.Node):
def render(self, context):
context["csl_status"] = get_csl_status()[0]
return ""


@register.tag
def get_csl_status_from_cache(parser, token):
tokens = token.contents.split()
if len(tokens) == 1:
return GetCSLStatusNode()
else:
raise template.TemplateSyntaxError("Usage: {% get_csl_status_from_cache %} {{ csl_status }}")


@register.simple_tag
def get_csl_status_page_url():
parsed_url = urlparse(settings.CSL_STATUS_PAGE)

return urlunparse((parsed_url.scheme, parsed_url.netloc, "", "", "", ""))
6 changes: 4 additions & 2 deletions intranet/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,7 @@ def get_month_seconds():
"users_list": int(datetime.timedelta(hours=24).total_seconds()),
"printers_list": int(datetime.timedelta(minutes=10).total_seconds()),
"emerg": int(datetime.timedelta(minutes=5).total_seconds()),
"csl_status": int(datetime.timedelta(minutes=5).total_seconds()),
"sports_school_events": int(datetime.timedelta(hours=1).total_seconds()),
}

Expand Down Expand Up @@ -882,11 +883,12 @@ def get_log(name): # pylint: disable=redefined-outer-name; 'name' is used as th
# The address for FCPS' Emergency Announcement page
FCPS_EMERGENCY_PAGE = "https://www.fcps.edu/alert_msg_feed" # type: str

# The address for the CSL's BetterUptime status page
CSL_STATUS_PAGE = "https://status.tjhsst.edu/index.json"

# The timeout for the request to FCPS' emergency page (in seconds)
EMERGENCY_TIMEOUT = 5

CSL_STATUS_PAGE = "https://status.tjhsst.edu/index.json"

# How frequently the emergency announcement cache should be updated by the Celerybeat task.
# This should be less than CACHE_AGE["emerg"].
FCPS_EMERGENCY_CACHE_UPDATE_INTERVAL = CACHE_AGE["emerg"] - 30
Expand Down
26 changes: 26 additions & 0 deletions intranet/static/css/page_base.scss
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,32 @@ h1 {
}
}

.status-link {
align-items: center;
border-radius: 4px;
flex-shrink: 1;
height: 30px;
text-decoration: none;
display: inline-flex;
vertical-align: middle;
margin-bottom: 3px;
margin-right: 10px;
color: white;
}

.status-link:hover {
background-color: rgba(120, 211, 246, 0.4);
}

.status-icon {
margin-right: 2px;
display: inline-block;
line-height: 0;
text-align: center;
width: 22px;
height: 22px;
}

.header .username {
margin-right: 10px;
}
Expand Down
22 changes: 19 additions & 3 deletions intranet/static/css/responsive.core.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ body.disable-scroll {
overflow: hidden;
}

@media (max-width: 700px) {
@media (max-width: 800px) {
.header .search input[type="text"] {
width: 158px;
font-size: 12px;
padding-right: 3px;
}
}

@media (max-width: 550px) {
@media (max-width: 680px) {
/* absence notification */
ul.dropdown-menu.absence-notification {
position: fixed;
Expand All @@ -34,11 +34,27 @@ body.disable-scroll {
.badged-item {
position: absolute !important;
top: 9px;
left: 95px;
}

.status-link .user-name {
display: none;
}

.status-link {
position: absolute !important;
top: 4px;
left: 60px;
}
}

@media (max-width: 550px) {
@media (max-width: 960px) {
.status-link .user-name {
display: none;
}
}

@media (max-width: 680px) {
/* FIXED HEADER */
h1 {
margin-right: 0;
Expand Down
48 changes: 48 additions & 0 deletions intranet/templates/page_with_header.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{% extends "page_base.html" %}
{% load static %}
{% load pipeline %}
{% load status_helper %}

{% block css %}
{{ block.super }}
Expand Down Expand Up @@ -84,6 +85,53 @@ <h1>Ion</h1>
</li>
{% endif %}

{% get_csl_status_from_cache %}
<a class="status-link" href="{% get_csl_status_page_url %}" target="_blank">
<div class="status-icon">
{% if csl_status == "operational" %}
<svg xmlns="http://www.w3.org/2000/svg" class="status-circle" width="22" height="22" viewBox="0 0 18 18" fill="none">
<circle cx="9" cy="9" r="9" fill="currentColor" fill-opacity="0.2"></circle>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 15C10.5913 15 12.1174 14.3679 13.2426 13.2426C14.3679 12.1174 15 10.5913 15 9C15 7.4087 14.3679 5.88258 13.2426 4.75736C12.1174 3.63214 10.5913 3 9 3C7.4087 3 5.88258 3.63214 4.75736 4.75736C3.63214 5.88258 3 7.4087 3 9C3 10.5913 3.63214 12.1174 4.75736 13.2426C5.88258 14.3679 7.4087 15 9 15ZM11.8245 7.50675C11.8779 7.45573 11.9208 7.39469 11.9506 7.32711C11.9805 7.25952 11.9967 7.18673 11.9984 7.11287C12.0001 7.03901 11.9873 6.96554 11.9606 6.89665C11.9339 6.82776 11.8939 6.76481 11.8429 6.71137C11.7919 6.65794 11.7308 6.61508 11.6632 6.58524C11.5956 6.5554 11.5229 6.53917 11.449 6.53746C11.3751 6.53575 11.3017 6.54861 11.2328 6.5753C11.1639 6.60199 11.1009 6.64198 11.0475 6.693C9.91968 7.77061 8.9301 8.98415 8.1015 10.3058L6.96 9.165C6.9085 9.10974 6.8464 9.06541 6.7774 9.03466C6.7084 9.00392 6.63392 8.98739 6.55839 8.98606C6.48286 8.98472 6.40784 8.99862 6.3378 9.02691C6.26776 9.0552 6.20414 9.09731 6.15072 9.15072C6.09731 9.20414 6.0552 9.26776 6.02691 9.3378C5.99862 9.40784 5.98472 9.48286 5.98606 9.55839C5.98739 9.63392 6.00392 9.7084 6.03466 9.7774C6.06541 9.8464 6.10973 9.9085 6.165 9.96L7.815 11.6108C7.8761 11.6719 7.95045 11.7181 8.0323 11.7458C8.11415 11.7736 8.2013 11.7821 8.28696 11.7707C8.37263 11.7593 8.45452 11.7283 8.52629 11.6802C8.59805 11.632 8.65775 11.568 8.70075 11.493C9.54556 10.0214 10.5976 8.67891 11.8245 7.50675Z" fill="currentColor"></path>
</svg>
{% elif csl_status == "downtime" %}
<svg xmlns="http://www.w3.org/2000/svg" id="root" class="text-statuspage-red inline-block" width="22" height="22" viewBox="0 0 18 18" fill="none">
<circle cx="9" cy="9" r="9" fill="currentColor" fill-opacity="0.2"></circle>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15 9C15 10.5913 14.3679 12.1174 13.2426 13.2426C12.1174 14.3679 10.5913 15 9 15C7.4087 15 5.88258 14.3679 4.75736 13.2426C3.63214 12.1174 3 10.5913 3 9C3 7.4087 3.63214 5.88258 4.75736 4.75736C5.88258 3.63214 7.4087 3 9 3C10.5913 3 12.1174 3.63214 13.2426 4.75736C14.3679 5.88258 15 7.4087 15 9ZM9.75 12C9.75 12.1989 9.67098 12.3897 9.53033 12.5303C9.38968 12.671 9.19891 12.75 9 12.75C8.80109 12.75 8.61032 12.671 8.46967 12.5303C8.32902 12.3897 8.25 12.1989 8.25 12C8.25 11.8011 8.32902 11.6103 8.46967 11.4697C8.61032 11.329 8.80109 11.25 9 11.25C9.19891 11.25 9.38968 11.329 9.53033 11.4697C9.67098 11.6103 9.75 11.8011 9.75 12ZM9 5.25C8.80109 5.25 8.61032 5.32902 8.46967 5.46967C8.32902 5.61032 8.25 5.80109 8.25 6V9C8.25 9.19891 8.32902 9.38968 8.46967 9.53033C8.61032 9.67098 8.80109 9.75 9 9.75C9.19891 9.75 9.38968 9.67098 9.53033 9.53033C9.67098 9.38968 9.75 9.19891 9.75 9V6C9.75 5.80109 9.67098 5.61032 9.53033 5.46967C9.38968 5.32902 9.19891 5.25 9 5.25Z" fill="currentColor"></path>
</svg>
{% elif csl_status == "degraded" %}
<svg xmlns="http://www.w3.org/2000/svg" id="root" class="text-statuspage-yellow inline-block" width="22" height="22" viewBox="0 0 18 18" fill="none">
<circle cx="9" cy="9" r="9" fill="currentColor" fill-opacity="0.2"></circle>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15 9C15 10.5913 14.3679 12.1174 13.2426 13.2426C12.1174 14.3679 10.5913 15 9 15C7.4087 15 5.88258 14.3679 4.75736 13.2426C3.63214 12.1174 3 10.5913 3 9C3 7.4087 3.63214 5.88258 4.75736 4.75736C5.88258 3.63214 7.4087 3 9 3C10.5913 3 12.1174 3.63214 13.2426 4.75736C14.3679 5.88258 15 7.4087 15 9ZM9.75 12C9.75 12.1989 9.67098 12.3897 9.53033 12.5303C9.38968 12.671 9.19891 12.75 9 12.75C8.80109 12.75 8.61032 12.671 8.46967 12.5303C8.32902 12.3897 8.25 12.1989 8.25 12C8.25 11.8011 8.32902 11.6103 8.46967 11.4697C8.61032 11.329 8.80109 11.25 9 11.25C9.19891 11.25 9.38968 11.329 9.53033 11.4697C9.67098 11.6103 9.75 11.8011 9.75 12ZM9 5.25C8.80109 5.25 8.61032 5.32902 8.46967 5.46967C8.32902 5.61032 8.25 5.80109 8.25 6V9C8.25 9.19891 8.32902 9.38968 8.46967 9.53033C8.61032 9.67098 8.80109 9.75 9 9.75C9.19891 9.75 9.38968 9.67098 9.53033 9.53033C9.67098 9.38968 9.75 9.19891 9.75 9V6C9.75 5.80109 9.67098 5.61032 9.53033 5.46967C9.38968 5.32902 9.19891 5.25 9 5.25Z" fill="currentColor"></path>
</svg>
{% elif csl_status == "maintenance" %}
<svg xmlns="http://www.w3.org/2000/svg" id="root" class="text-statuspage-blue inline-block" width="22" height="22" viewBox="0 0 18 18" fill="none">
<circle cx="9" cy="9" r="9" fill="currentColor" fill-opacity="0.2"></circle>
<mask id="icon">
<circle cx="9" cy="9" r="6" fill="white"></circle>
<path d="M6.96375 8.09363C7.05752 8.18739 7.1847 8.24007 7.3173 8.24007H7.74005C8.0162 8.24007 8.24005 8.01621 8.24005 7.74007V7.31732C8.24005 7.18471 8.18738 7.05754 8.09361 6.96377L7.42189 6.29205C7.17787 6.04803 7.25484 5.63692 7.5969 5.59128C7.8219 5.56126 8.05148 5.56513 8.27787 5.60397C8.73843 5.68299 9.16317 5.90286 9.4936 6.23328C9.82403 6.56371 10.0439 6.98846 10.1229 7.44902C10.1819 7.79284 10.1602 8.144 10.0617 8.47536C10.0013 8.67846 10.035 8.90517 10.1848 9.055L12.1946 11.0647C12.3444 11.2145 12.4286 11.4178 12.4286 11.6296C12.4286 11.8415 12.3444 12.0447 12.1946 12.1946C12.0447 12.3444 11.8415 12.4286 11.6296 12.4286C11.4177 12.4286 11.2145 12.3444 11.0647 12.1946L9.05498 10.1849C8.90515 10.035 8.67844 10.0013 8.47535 10.0617C8.14399 10.1603 7.79282 10.1819 7.449 10.1229C6.98844 10.0439 6.5637 9.82405 6.23327 9.49362C5.90284 9.16319 5.68298 8.73845 5.60396 8.27788C5.56511 8.05149 5.56124 7.82192 5.59126 7.59692C5.6369 7.25485 6.04801 7.17789 6.29203 7.42191L6.96375 8.09363Z" fill="black"></path>
</mask>
<circle cx="9" cy="9" r="6" fill="currentColor" mask="url(#icon)"></circle>
</svg>
{% else %}
<circle cx="9" cy="9" r="9" fill="currentColor" fill-opacity="0.2"></circle>
<i class="fa fa-question-circle" style="margin-top: 0.281rem;"></i>
{% endif %}
</div>
<div class="user-name">
{% if csl_status == "operational" %}
All services operational
{% elif csl_status == "downtime" %}
Some services are down
{% elif csl_status == "degraded" %}
Some services are degraded
{% elif csl_status == "maintenance" %}
Services are undergoing maintenance
{% else %}
View status
{% endif %}
</div>
</a>

{% if csl_apps|length > 0 %}
<li class="csl-apps has-dropdown">
<div class="dropdown-item-wrapper">
Expand Down