Skip to content
Open
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
26 changes: 19 additions & 7 deletions tcf_core/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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),
}
}
36 changes: 34 additions & 2 deletions tcf_website/services/presence.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,43 @@


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::<uri>").

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()


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)
Expand All @@ -35,11 +59,19 @@ 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:
data = _get(f"{BASE}/events/{event_uri}")
cache.set(key, data, CACHE_TTL)
return data


26 changes: 23 additions & 3 deletions tcf_website/templatetags/custom_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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]
16 changes: 14 additions & 2 deletions tcf_website/tests/test_calendar_overview.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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 = [
{
Expand Down Expand Up @@ -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'])
16 changes: 14 additions & 2 deletions tcf_website/tests/test_event_detail.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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/")
Expand All @@ -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)
43 changes: 38 additions & 5 deletions tcf_website/views/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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

Expand All @@ -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 []:
Expand Down Expand Up @@ -138,4 +171,4 @@ def event_detail(request, event_uri):
{
"event": event,
},
)
)
Loading