diff --git a/tcf_core/context_processors.py b/tcf_core/context_processors.py index d915b2416..2ea0bee6b 100644 --- a/tcf_core/context_processors.py +++ b/tcf_core/context_processors.py @@ -15,7 +15,14 @@ def base(request): def searchbar_context(request): - """Provide context for the search bar.""" + """ + Builds template context used by the site search bar. + + Provides a mapping with the following keys: + - "disciplines": QuerySet of all Discipline objects ordered by name. + - "subdepartments": QuerySet of all Subdepartment objects ordered by mnemonic. + - "semesters": QuerySet of recent Semester objects; if no latest Semester exists, an empty QuerySet is returned, otherwise semesters with number greater than or equal to latest.number - 50 ordered by descending number. + """ latest_semester = Semester.latest() if latest_semester is None: recent_semesters = Semester.objects.none() @@ -35,12 +42,15 @@ def searchbar_context(request): def flags(_request): - """Expose template context flags. - - _request is unused. - - Returns a dict containing ENABLE_CLUB_CALENDAR with its default. + """ + Provide feature flags for templates. + + The `_request` argument is unused. + + Returns: + dict: Mapping containing `"ENABLE_CLUB_CALENDAR"` set to the value of + `settings.ENABLE_CLUB_CALENDAR` if defined, otherwise `False`. """ return { "ENABLE_CLUB_CALENDAR": getattr(settings, "ENABLE_CLUB_CALENDAR", False), - } + } \ No newline at end of file diff --git a/tcf_website/services/presence.py b/tcf_website/services/presence.py index 510619913..69961ef3e 100644 --- a/tcf_website/services/presence.py +++ b/tcf_website/services/presence.py @@ -12,11 +12,33 @@ def _cache_key(key: str) -> str: + """ + Construct a namespaced cache key for Presence data. + + Parameters: + key (str): The key to be namespaced for caching. + + Returns: + str: Cache key prefixed with the Presence subdomain and module namespace. + """ return f"presence::{settings.PRESENCE_SUBDOMAIN}::{key}" @backoff.on_exception(backoff.expo, (requests.RequestException,), max_tries=3) def _get(url: str, params: dict | None = None) -> dict: + """ + Fetches JSON from the specified URL via HTTP GET. + + Parameters: + url: The request URL. + params: Optional query parameters to include in the request. + + Returns: + dict: Parsed JSON response. + + Raises: + requests.HTTPError: If the HTTP response status indicates an error. + """ resp = requests.get(url, params=params or {}, timeout=TIMEOUT) resp.raise_for_status() return resp.json() @@ -24,7 +46,12 @@ def _get(url: str, params: dict | None = None) -> dict: def get_events(): """ - Returns a list of upcoming events from Presence. Cached briefly to avoid rate limits. + Retrieve the list of upcoming Presence events. + + The parsed JSON response is cached for CACHE_TTL seconds to reduce API calls and avoid rate limits; on cache miss the function requests the data from the Presence API. + + Returns: + dict: Parsed JSON response from the Presence API containing upcoming events. """ key = _cache_key("events_all") data = cache.get(key) @@ -35,6 +62,17 @@ def get_events(): def get_event_details(event_uri: str): + """ + Retrieve details for a specific Presence event identified by its URI. + + The result is cached under a presence‑namespaced key for the configured CACHE_TTL; a cache miss triggers a request to the Presence API. + + Parameters: + event_uri (str): The event's URI or identifier appended to the API path (e.g. "my-event-slug"). + + Returns: + dict: Event details as returned by the Presence API. + """ key = _cache_key(f"event::{event_uri}") data = cache.get(key) if data is None: @@ -42,4 +80,3 @@ def get_event_details(event_uri: str): cache.set(key, data, CACHE_TTL) return data - diff --git a/tcf_website/templatetags/custom_tags.py b/tcf_website/templatetags/custom_tags.py index d24412be3..c509b6f86 100644 --- a/tcf_website/templatetags/custom_tags.py +++ b/tcf_website/templatetags/custom_tags.py @@ -15,13 +15,31 @@ def get_item(dictionary, key): @register.filter def remove_email(value): - """This filter will remove the professors email from the string""" + """ + Remove any parenthetical portion from the input string. + + Parameters: + value: The input value to process; non-string inputs will be converted to `str`. + + Returns: + The portion of the input before the first `(` as a `str`. If no `(` is present, returns the entire string. + """ return str(value).split("(", maxsplit=1)[0] @register.filter def tag_color(tag_name): - """Returns a consistent Bootstrap color class for a given tag name""" + """ + Map a tag name to a consistent Bootstrap background color class. + + Normalizes the input (lowercased, trimmed, and with hyphens/underscores replaced by spaces), applies a set of predefined mappings for common tag names, and otherwise selects a deterministic fallback class based on a hash of the normalized tag. An empty or falsy `tag_name` yields `"bg-secondary"`. + + Parameters: + tag_name (str): The tag label to map; may contain hyphens or underscores and will be normalized. + + Returns: + str: A Bootstrap background class (e.g., `"bg-primary"`, `"bg-success"`, `"bg-secondary"`). + """ if not tag_name: return "bg-secondary" @@ -71,4 +89,4 @@ def tag_color(tag_name): tag_hash = int(hashlib.md5(normalized_tag.encode()).hexdigest(), 16) color_index = tag_hash % len(colors) - return colors[color_index] + return colors[color_index] \ No newline at end of file diff --git a/tcf_website/tests/test_calendar_overview.py b/tcf_website/tests/test_calendar_overview.py index 5cbe56823..91e23a202 100644 --- a/tcf_website/tests/test_calendar_overview.py +++ b/tcf_website/tests/test_calendar_overview.py @@ -9,6 +9,11 @@ class CalendarOverviewTests(TestCase): """Tests for calendar overview functionality.""" def setUp(self): + """ + Prepare the test case by creating a Django test client. + + Attaches a django.test.Client instance to `self.client` for use in test methods; run before each test. + """ self.client = Client() def test_calendar_route_ok(self): @@ -48,4 +53,4 @@ def test_events_sorted_by_date(self, mock_get_events): # Check context has sorted groups upcoming_groups = resp.context['upcoming_groups'] dates = list(upcoming_groups.keys()) - self.assertEqual(dates, ['2026-01-01', '2026-01-02', '2026-01-03']) + self.assertEqual(dates, ['2026-01-01', '2026-01-02', '2026-01-03']) \ No newline at end of file diff --git a/tcf_website/tests/test_event_detail.py b/tcf_website/tests/test_event_detail.py index 8f4ea0078..faeb198f0 100644 --- a/tcf_website/tests/test_event_detail.py +++ b/tcf_website/tests/test_event_detail.py @@ -10,6 +10,9 @@ class EventDetailTests(TestCase): """Tests for event detail functionality.""" def setUp(self): + """ + Create a Django test client instance and assign it to self.client for use by test methods. + """ self.client = Client() @@ -27,4 +30,4 @@ def test_event_detail_not_found_on_none(self, mock_get_details): mock_get_details.return_value = None resp = self.client.get("/clubs/calendar/event/test-uri/") - self.assertEqual(resp.status_code, 404) + self.assertEqual(resp.status_code, 404) \ No newline at end of file diff --git a/tcf_website/views/calendar.py b/tcf_website/views/calendar.py index 8806259a8..9d520707c 100644 --- a/tcf_website/views/calendar.py +++ b/tcf_website/views/calendar.py @@ -11,17 +11,41 @@ def _safe_text(html): - """Strip HTML tags from text and remove leading/trailing whitespace.""" + """ + Return plain text with HTML tags removed and trimmed. + + Parameters: + html (str | None): HTML string to sanitize; falsy values are treated as an empty string. + + Returns: + str: Plain text with HTML tags removed and leading/trailing whitespace trimmed. + """ return strip_tags(html or "").strip() def _sort_key(evt): - """Return the sort key for an event based on its start time.""" + """ + Produce a sortable key for an event that places unknown start times after dated events. + + Parameters: + evt (dict): Event mapping that may include a "start_utc" ISO 8601 UTC datetime string. + + Returns: + str: The event's "start_utc" value if present, otherwise the placeholder "9999-12-31T23:59:59Z". + """ return evt.get("start_utc") or "9999-12-31T23:59:59Z" def _format_datetime(dt_string): - """Format UTC datetime string for display""" + """ + Format a UTC ISO-8601 datetime string into a human-readable display string. + + Parameters: + dt_string (str | None): UTC datetime in ISO 8601 format (may end with 'Z'), or a falsy value. + + Returns: + str: A formatted datetime like "Friday, January 01, 2021 at 05:00 PM". Returns "TBD" when `dt_string` is falsy. If parsing fails, returns the original `dt_string`. + """ if not dt_string: return "TBD" try: @@ -32,7 +56,17 @@ def _format_datetime(dt_string): def _is_upcoming(dt_string): - """Check if event is upcoming (today or later)""" + """ + Determine whether an event date is today or in the future. + + If `dt_string` is empty or cannot be parsed as an ISO 8601 datetime, the event is treated as upcoming. + + Parameters: + dt_string (str): ISO 8601 UTC datetime string (e.g. "2025-01-01T12:00:00Z") or None. + + Returns: + True if the event occurs today or later, False otherwise. + """ if not dt_string: return True # Treat events without dates as upcoming @@ -103,7 +137,20 @@ def calendar_overview(request): }, ) def event_detail(request, event_uri): - """Display detailed information for a specific event""" + """ + Render the detail page for a single calendar event. + + Fetches event details by `event_uri`, formats fields for display, and returns a response rendering the event detail template. + + Parameters: + event_uri (str): The event identifier/URI used to retrieve event details. + + Returns: + HttpResponse: The rendered "calendar/event_detail.html" page containing the event data. + + Raises: + Http404: If the event cannot be retrieved or no event data is returned. + """ try: event_data = presence.get_event_details(event_uri) except Exception as exc: @@ -138,4 +185,4 @@ def event_detail(request, event_uri): { "event": event, }, - ) + ) \ No newline at end of file