diff --git a/docs/sourcedoc/intranet.apps.templatetags.rst b/docs/sourcedoc/intranet.apps.templatetags.rst
index a5820320a6b..331be35421b 100644
--- a/docs/sourcedoc/intranet.apps.templatetags.rst
+++ b/docs/sourcedoc/intranet.apps.templatetags.rst
@@ -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
-----------------------------------------
diff --git a/intranet/apps/emerg/views.py b/intranet/apps/emerg/views.py
index fd69b3d49da..c525e10d9c6 100644
--- a/intranet/apps/emerg/views.py
+++ b/intranet/apps/emerg/views.py
@@ -1,6 +1,6 @@
-import json
import logging
import time
+from typing import Tuple
import requests
from bs4 import BeautifulSoup
@@ -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
@@ -67,44 +66,10 @@ def check_emerg():
announcements.append({"title": f"{title}", "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"{issue['title']}",
- "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 = [
- '
© tjCSL Status, 2022 • Back to top
',
- "We continuously monitor the status of our services and if there are any interruptions, a note will be posted here.
",
- ]
-
message = "".join(
[
f" {announcement['title']}
{announcement['body']}\n"
for announcement in announcements
- if announcement not in bad_text
]
)
@@ -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"]
@@ -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
diff --git a/intranet/apps/templatetags/status_helper.py b/intranet/apps/templatetags/status_helper.py
new file mode 100644
index 00000000000..715d142c3e7
--- /dev/null
+++ b/intranet/apps/templatetags/status_helper.py
@@ -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, "", "", "", ""))
diff --git a/intranet/settings/__init__.py b/intranet/settings/__init__.py
index 10d7d627646..ef18e4c765c 100644
--- a/intranet/settings/__init__.py
+++ b/intranet/settings/__init__.py
@@ -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()),
}
@@ -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
diff --git a/intranet/static/css/page_base.scss b/intranet/static/css/page_base.scss
index 8f34ea909ca..231e54f9a6e 100644
--- a/intranet/static/css/page_base.scss
+++ b/intranet/static/css/page_base.scss
@@ -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;
}
diff --git a/intranet/static/css/responsive.core.scss b/intranet/static/css/responsive.core.scss
index 479a1b4c956..4addc3145bb 100644
--- a/intranet/static/css/responsive.core.scss
+++ b/intranet/static/css/responsive.core.scss
@@ -10,7 +10,7 @@ body.disable-scroll {
overflow: hidden;
}
-@media (max-width: 700px) {
+@media (max-width: 800px) {
.header .search input[type="text"] {
width: 158px;
font-size: 12px;
@@ -18,7 +18,7 @@ body.disable-scroll {
}
}
-@media (max-width: 550px) {
+@media (max-width: 680px) {
/* absence notification */
ul.dropdown-menu.absence-notification {
position: fixed;
@@ -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;
diff --git a/intranet/templates/page_with_header.html b/intranet/templates/page_with_header.html
index bea36898f69..c8e83b1ec36 100644
--- a/intranet/templates/page_with_header.html
+++ b/intranet/templates/page_with_header.html
@@ -1,6 +1,7 @@
{% extends "page_base.html" %}
{% load static %}
{% load pipeline %}
+{% load status_helper %}
{% block css %}
{{ block.super }}
@@ -84,6 +85,53 @@ Ion
{% endif %}
+ {% get_csl_status_from_cache %}
+
+
+ {% if csl_status == "operational" %}
+
+ {% elif csl_status == "downtime" %}
+
+ {% elif csl_status == "degraded" %}
+
+ {% elif csl_status == "maintenance" %}
+
+ {% else %}
+
+
+ {% endif %}
+
+
+ {% 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 %}
+
+
+
{% if csl_apps|length > 0 %}