-
Notifications
You must be signed in to change notification settings - Fork 12
ClubForum Calendar event list view #1178
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
f165655
4c31afa
5e4606a
2dd557b
dc9112b
b3abdfa
b508f44
65536c3
3fce8f5
5e6647b
f868b1f
4d1650b
9f8873a
14a2b7a
0f840dc
d5f2472
44739fb
03b94dd
7c5ac19
dcb4963
2e70c05
e281751
da3e72c
780dfd4
6329469
f40abf4
04cce56
5a0cf2a
d0b971a
720e444
77f9390
3a81d98
58d3d80
b9354ef
ed413c0
27b3e7e
c30e9da
a37506d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| # Generated for local dev: seed School records so course browse page loads without full DB dump | ||
|
|
||
| from django.db import migrations | ||
|
|
||
|
|
||
| def seed_schools(apps, schema_editor): | ||
| School = apps.get_model("tcf_website", "School") | ||
| for name in ( | ||
| "College of Arts & Sciences", | ||
| "School of Engineering & Applied Science", | ||
| ): | ||
| School.objects.get_or_create(name=name, defaults={"description": "", "website": ""}) | ||
|
|
||
|
|
||
| def noop(apps, schema_editor): | ||
| pass | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ("tcf_website", "0023_remove_sectionenrollment_section_and_more"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.RunPython(seed_schools, noop), | ||
| ] |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,83 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||
| """Service for interacting with the Presence API.""" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| import backoff | ||||||||||||||||||||||||||||||||||||||||||||||||
| import requests | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| from django.conf import settings | ||||||||||||||||||||||||||||||||||||||||||||||||
| from django.core.cache import cache | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| logger = logging.getLogger(__name__) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| BASE = f"https://api.presence.io/{settings.PRESENCE_SUBDOMAIN}/v1" | ||||||||||||||||||||||||||||||||||||||||||||||||
| CACHE_TTL = getattr(settings, "PRESENCE_CACHE_SECONDS", 300) | ||||||||||||||||||||||||||||||||||||||||||||||||
| TIMEOUT = getattr(settings, "PRESENCE_TIMEOUT_SECONDS", 8) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| def _cache_key(key: str) -> str: | ||||||||||||||||||||||||||||||||||||||||||||||||
| return f"presence::{settings.PRESENCE_SUBDOMAIN}::{key}" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Request with browser-like headers so the API allows the request (avoids 403 in some environments) | ||||||||||||||||||||||||||||||||||||||||||||||||
| PRESENCE_HEADERS = { | ||||||||||||||||||||||||||||||||||||||||||||||||
| "User-Agent": "Mozilla/5.0 (compatible; theCourseForum/1.0; +https://thecourseforum.com)", | ||||||||||||||||||||||||||||||||||||||||||||||||
| "Accept": "application/json", | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| @backoff.on_exception(backoff.expo, (requests.RequestException,), max_tries=3) | ||||||||||||||||||||||||||||||||||||||||||||||||
| def _get(url: str, params: dict | None = None) -> dict: | ||||||||||||||||||||||||||||||||||||||||||||||||
| resp = requests.get( | ||||||||||||||||||||||||||||||||||||||||||||||||
| url, params=params or {}, timeout=TIMEOUT, headers=PRESENCE_HEADERS | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| resp.raise_for_status() | ||||||||||||||||||||||||||||||||||||||||||||||||
| return resp.json() | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| def get_events(): | ||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||
| Returns a list of upcoming events from Presence. Cached briefly to avoid rate limits. | ||||||||||||||||||||||||||||||||||||||||||||||||
| On API errors (e.g. 403 Forbidden in local dev), returns an empty list so the page still loads. | ||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||
| key = _cache_key("events_all") | ||||||||||||||||||||||||||||||||||||||||||||||||
| data = cache.get(key) | ||||||||||||||||||||||||||||||||||||||||||||||||
| if data is None: | ||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||
| data = _get(f"{BASE}/events") | ||||||||||||||||||||||||||||||||||||||||||||||||
| cache.set(key, data, CACHE_TTL) | ||||||||||||||||||||||||||||||||||||||||||||||||
| except requests.HTTPError as e: | ||||||||||||||||||||||||||||||||||||||||||||||||
| logger.warning( | ||||||||||||||||||||||||||||||||||||||||||||||||
| "Presence API error (events): %s %s - %s", | ||||||||||||||||||||||||||||||||||||||||||||||||
| e.response.status_code if e.response is not None else "?", | ||||||||||||||||||||||||||||||||||||||||||||||||
| e.response.reason if e.response is not None else "", | ||||||||||||||||||||||||||||||||||||||||||||||||
| str(e), | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| return [] | ||||||||||||||||||||||||||||||||||||||||||||||||
| except requests.RequestException as e: | ||||||||||||||||||||||||||||||||||||||||||||||||
| logger.warning("Presence API request failed (events): %s", e) | ||||||||||||||||||||||||||||||||||||||||||||||||
| return [] | ||||||||||||||||||||||||||||||||||||||||||||||||
| return data | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| def get_event_details(event_uri: str): | ||||||||||||||||||||||||||||||||||||||||||||||||
| key = _cache_key(f"event::{event_uri}") | ||||||||||||||||||||||||||||||||||||||||||||||||
| data = cache.get(key) | ||||||||||||||||||||||||||||||||||||||||||||||||
| if data is None: | ||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||
| data = _get(f"{BASE}/events/{event_uri}") | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+63
to
+68
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check how event_uri is captured and validated in the views/urls
echo "=== Checking URL patterns for event_uri ==="
rg -n 'event_uri|event-uri' --type py -C 3
echo -e "\n=== Checking calendar views for event_uri handling ==="
fd -t f 'calendar.py' --exec cat {} 2>/dev/null | head -100Repository: thecourseforum/theCourseForum2 Length of output: 6177 🏁 Script executed: #!/bin/bash
# Check for Presence API documentation or comments about event_uri format
echo "=== Searching for Presence API base URL and documentation ==="
rg -n 'BASE.*=' tcf_website/services/presence.py -A 2 -B 2
echo -e "\n=== Looking for sample event URIs or format hints ==="
rg -n 'uri|eventUri|event_uri' --type py tcf_website/views/calendar.py | head -20
echo -e "\n=== Checking if there are any tests or examples with event URIs ==="
fd -t f -e py | xargs rg -l 'event_uri|event-uri' | head -10Repository: thecourseforum/theCourseForum2 Length of output: 816 Django's URL converter already prevents path traversal; explicit validation is a reasonable defense-in-depth measure.
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
| cache.set(key, data, CACHE_TTL) | ||||||||||||||||||||||||||||||||||||||||||||||||
| except requests.HTTPError as e: | ||||||||||||||||||||||||||||||||||||||||||||||||
| logger.warning( | ||||||||||||||||||||||||||||||||||||||||||||||||
| "Presence API error (event %s): %s %s", | ||||||||||||||||||||||||||||||||||||||||||||||||
| event_uri, | ||||||||||||||||||||||||||||||||||||||||||||||||
| e.response.status_code if e.response is not None else "?", | ||||||||||||||||||||||||||||||||||||||||||||||||
| e.response.reason if e.response is not None else "", | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||||||||||||||||||||||||||
| except requests.RequestException as e: | ||||||||||||||||||||||||||||||||||||||||||||||||
| logger.warning("Presence API request failed (event %s): %s", event_uri, e) | ||||||||||||||||||||||||||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||||||||||||||||||||||||||
| return data | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+63
to
+81
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a docstring for consistency and coverage goals.
📝 Proposed docstring def get_event_details(event_uri: str):
+ """
+ Returns details for a specific event by URI. Cached briefly to avoid rate limits.
+
+ Args:
+ event_uri: The unique identifier for the event from Presence.
+
+ Returns:
+ Event details dictionary from the Presence API.
+ """
key = _cache_key(f"event::{event_uri}")📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Backoff retries non-retriable HTTP errors (4xx) unnecessarily.
raise_for_status()throwsHTTPError(a subclass ofRequestException), so client errors like 403/404 will be retried 3 times with exponential backoff. This wastes time and could aggravate rate limits. Consider limiting retries to connection/timeout errors, or adding agiveuppredicate for 4xx responses.🔧 Proposed fix — skip retries on client errors
🤖 Prompt for AI Agents