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
24 changes: 17 additions & 7 deletions tcf_core/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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),
}
}
41 changes: 39 additions & 2 deletions tcf_website/services/presence.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,46 @@


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


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


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

Expand Down Expand Up @@ -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]
7 changes: 6 additions & 1 deletion 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):
"""
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):
Expand Down Expand Up @@ -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'])
5 changes: 4 additions & 1 deletion tcf_website/tests/test_event_detail.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand All @@ -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)
59 changes: 53 additions & 6 deletions tcf_website/views/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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

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