diff --git a/tcf_core/context_processors.py b/tcf_core/context_processors.py index d915b2416..9b18b1028 100644 --- a/tcf_core/context_processors.py +++ b/tcf_core/context_processors.py @@ -15,7 +15,15 @@ def base(request): def searchbar_context(request): - """Provide context for the search bar.""" + """ + Provide template context for a search bar with discipline, subdepartment, and recent semester options. + + Returns: + context (dict): Mapping with: + - 'disciplines': QuerySet of all Discipline objects ordered by name. + - 'subdepartments': QuerySet of all Subdepartment objects ordered by mnemonic. + - 'semesters': QuerySet of recent Semester objects (semesters whose number is within 50 of the latest semester, ordered by descending number), or an empty QuerySet if no latest semester exists. + """ latest_semester = Semester.latest() if latest_semester is None: recent_semesters = Semester.objects.none() @@ -35,12 +43,16 @@ def searchbar_context(request): def flags(_request): - """Expose template context flags. - - _request is unused. - - Returns a dict containing ENABLE_CLUB_CALENDAR with its default. + """ + Expose template context flags for templates. + + Parameters: + _request: The incoming request (unused). + + Returns: + A dict with the key "ENABLE_CLUB_CALENDAR" set to the value of + settings.ENABLE_CLUB_CALENDAR if present, 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..ddfdce289 100644 --- a/tcf_website/services/presence.py +++ b/tcf_website/services/presence.py @@ -12,11 +12,32 @@ def _cache_key(key: str) -> str: + """ + Builds a namespaced cache key for Presence-related data. + + Parameters: + key (str): The key suffix identifying the cached item (e.g., "events_all" or "event::"). + + Returns: + str: A cache key of the form "presence::{subdomain}::{key}", where {subdomain} is the configured Presence subdomain. + """ 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: + """ + Retrieve and parse JSON from the specified URL. + + Parameters: + params (dict | None): Optional query parameters to include in the request. + + Returns: + dict: Parsed JSON response as a dictionary. + + Raises: + requests.RequestException: If the HTTP request fails or the response has a non-2xx status. + """ resp = requests.get(url, params=params or {}, timeout=TIMEOUT) resp.raise_for_status() return resp.json() @@ -24,7 +45,10 @@ 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 upcoming events from Presence, using a short-lived cache to reduce API calls. + + Returns: + dict: Parsed JSON response from the Presence `/events` endpoint containing the list of upcoming events and related metadata. """ key = _cache_key("events_all") data = cache.get(key) @@ -35,6 +59,15 @@ def get_events(): def get_event_details(event_uri: str): + """ + Retrieve and cache details for a specific Presence event. + + Parameters: + event_uri (str): The event's URI or identifier relative to the Presence API (used to build the request path). + + Returns: + dict: Parsed JSON containing the event details. The result is cached under a namespaced key for CACHE_TTL seconds. + """ key = _cache_key(f"event::{event_uri}") data = cache.get(key) if data is None: @@ -42,4 +75,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..d64849917 100644 --- a/tcf_website/templatetags/custom_tags.py +++ b/tcf_website/templatetags/custom_tags.py @@ -15,13 +15,33 @@ def get_item(dictionary, key): @register.filter def remove_email(value): - """This filter will remove the professors email from the string""" + """ + Remove any parenthetical suffix from the input string. + + Converts the input to a string and returns the substring before the first "(" character. If no "(" is present, returns the full string. + + Parameters: + value: The value to process; it will be converted to a string. + + Returns: + The substring before the first "(". + """ 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. + + Given a tag name, return a deterministic Bootstrap background class. Common tag names are mapped to specific colors; for other non-empty names a consistent color is selected based on the tag content. If `tag_name` is empty or falsy, returns `'bg-secondary'`. + + Parameters: + tag_name (str): The tag name to map; may include hyphens or underscores. + + Returns: + str: A Bootstrap background class such as `'bg-primary'`, `'bg-success'`, `'bg-info'`, `'bg-warning'`, `'bg-danger'`, `'bg-dark'`, or `'bg-secondary'`. + """ if not tag_name: return "bg-secondary" @@ -71,4 +91,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 3f8bd577f..47a386b64 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): + """ + Create a test HTTP client for use in test methods. + + Initializes self.client with a Django test Client instance so tests can perform HTTP requests to views. + """ self.client = Client() def test_calendar_route_ok(self): @@ -18,7 +23,14 @@ def test_calendar_route_ok(self): @patch('tcf_website.views.calendar.presence.get_events') def test_events_sorted_by_date(self, mock_get_events): - """Test that events are sorted ascending by start date.""" + """ + Verify the calendar view presents events grouped by date in ascending order. + + Patches `presence.get_events` to return events with out-of-order `startDateTimeUtc` values, requests the calendar route, and asserts the response is OK and that `resp.context['upcoming_groups']` has date keys sorted as ['2025-01-01', '2025-01-02', '2025-01-03']. + + Parameters: + mock_get_events (unittest.mock.Mock): Patched replacement for `presence.get_events`; the test sets its return value to a list of events out of chronological order. + """ # Mock events out of order mock_events = [ { @@ -48,4 +60,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, ['2025-01-01', '2025-01-02', '2025-01-03']) + self.assertEqual(dates, ['2025-01-01', '2025-01-02', '2025-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 6f90d9cb5..65de3ecb2 100644 --- a/tcf_website/tests/test_event_detail.py +++ b/tcf_website/tests/test_event_detail.py @@ -10,6 +10,11 @@ class EventDetailTests(TestCase): """Tests for event detail functionality.""" def setUp(self): + """ + Set up a Django test client for use by each test. + + Runs before each test method and assigns a `django.test.Client` instance to `self.client`. + """ self.client = Client() def test_event_detail_route_ok(self): @@ -20,7 +25,14 @@ def test_event_detail_route_ok(self): @patch('tcf_website.views.calendar.presence.get_event_details') def test_event_detail_not_found_on_exception(self, mock_get_details): - """Test that event detail raises 404 on service exception.""" + """ + Verify the event detail view responds with HTTP 404 when the event service raises an exception. + + This test sets the patched `get_event_details` mock to raise an Exception and asserts that a GET request to the event detail URL returns a 404 status. + + Parameters: + mock_get_details: patched mock for `get_event_details`, configured to raise an exception. + """ mock_get_details.side_effect = Exception("Service error") resp = self.client.get("/clubs/calendar/event/test-uri/") @@ -32,4 +44,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..5e8e11874 100644 --- a/tcf_website/views/calendar.py +++ b/tcf_website/views/calendar.py @@ -16,12 +16,28 @@ def _safe_text(html): def _sort_key(evt): - """Return the sort key for an event based on its start time.""" + """ + Produce a sortable datetime string representing an event's start time. + + Parameters: + evt (dict): Event mapping; if present, the `start_utc` key should be an ISO8601 UTC datetime string. + + Returns: + sort_key (str): The `start_utc` value when available, otherwise the placeholder "9999-12-31T23:59:59Z" so undated events sort after dated events. + """ return evt.get("start_utc") or "9999-12-31T23:59:59Z" def _format_datetime(dt_string): - """Format UTC datetime string for display""" + """ + Format an ISO 8601 UTC datetime string into a human-friendly display. + + Parameters: + dt_string (str | None): ISO 8601 UTC datetime (may end with 'Z'); if falsy, treated as missing. + + Returns: + str: Formatted datetime like "Weekday, Month day, Year at HH:MM AM/PM", "TBD" when input is missing, or the original string if parsing fails. + """ if not dt_string: return "TBD" try: @@ -32,7 +48,17 @@ def _format_datetime(dt_string): def _is_upcoming(dt_string): - """Check if event is upcoming (today or later)""" + """ + Determine whether an event datetime string represents today or a future date. + + If `dt_string` is missing or cannot be parsed as an ISO-8601 datetime, the event is treated as upcoming. + + Parameters: + dt_string (str | None): UTC ISO-8601 datetime string (e.g. "2023-12-31T12:00:00Z") or None. + + Returns: + `True` if the event is today or in the future (or if `dt_string` is missing/invalid), `False` otherwise. + """ if not dt_string: return True # Treat events without dates as upcoming @@ -45,7 +71,14 @@ def _is_upcoming(dt_string): def calendar_overview(request): - """Display an overview of upcoming and past club events.""" + """ + Render a calendar overview grouping upcoming and past events by date. + + Returns: + HttpResponse: The rendered "calendar/calendar_overview.html" response. Context includes + `upcoming_groups` and `past_groups`, each mapping a date key (YYYY-MM-DD or "TBD") to a + list of event dictionaries. + """ raw = presence.get_events() events = [] for e in raw or []: @@ -138,4 +171,4 @@ def event_detail(request, event_uri): { "event": event, }, - ) + ) \ No newline at end of file