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 %}