Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
f165655
Calendar: add calendar views, templates, tests; integrate into URLs a…
Oct 19, 2025
4c31afa
Merge remote-tracking branch 'origin/dev' into ClubForum+-Calender
Oct 26, 2025
5e4606a
Updated calendar event list view, added event detail template, and ad…
Oct 26, 2025
2dd557b
Merge branch 'dev' into ClubForum+-Calender
Charka123 Nov 2, 2025
dc9112b
Remove calendar-related feature flags and conditional logic, standard…
Nov 9, 2025
b3abdfa
Merge remote-tracking branch 'origin/dev' into ClubForum+-Calender
Nov 9, 2025
b508f44
Finalize calendar feature removal: clean settings, update templates, …
Nov 9, 2025
65536c3
Merge remote-tracking branch 'origin/dev' into ClubForum+-Calender
Nov 9, 2025
3fce8f5
Merge branch 'dev' into ClubForum+-Calender
R0hit-0 Nov 9, 2025
5e6647b
Merge branch 'dev' into ClubForum+-Calender
Jay-Lalwani Nov 9, 2025
f868b1f
Merge branch 'dev' into ClubForum+-Calender
Charka123 Nov 9, 2025
4d1650b
Add flags context processor with docstring and fix calendar icon colo…
Nov 9, 2025
9f8873a
Merge remote-tracking branch 'origin/dev' into ClubForum+-Calender
Nov 15, 2025
14a2b7a
Fix Pylint errors: remove unused imports, unnecessary pass, long line…
Nov 15, 2025
0f840dc
Update test_event_detail.py
Nov 15, 2025
d5f2472
Fix calendar feature issues: add docstrings, handle None semester in …
Nov 15, 2025
44739fb
Update calendar test to use future dates for upcoming events
Nov 15, 2025
03b94dd
Remove unnecessary pass statement and placeholder test method
Nov 15, 2025
7c5ac19
Remove trailing whitespace
Nov 15, 2025
dcb4963
Add tag filter feature to Club Calendar with dropdown menu and 6x6 gr…
Nov 15, 2025
2e70c05
Remove unused ENABLE_CLUB_CALENDAR flag and context processor
Nov 15, 2025
e281751
Fix trailing newlines in context_processors.py
Nov 15, 2025
da3e72c
Merge branch 'dev' into ClubForum+-Calender
Charka123 Dec 7, 2025
780dfd4
Merge branch 'dev' into ClubForum+-Calender
Feb 1, 2026
6329469
Fixed Presence
Feb 1, 2026
f40abf4
Add Riti Lahoti to engineering team
ritilahoti Feb 1, 2026
04cce56
feat(adblocker): show popup on adblocker detected
Jay-Lalwani Feb 2, 2026
5a0cf2a
fix(eslint)
Jay-Lalwani Feb 2, 2026
d0b971a
fix(adblocker): reduce check to 100 ms
Jay-Lalwani Feb 2, 2026
720e444
fix(adblocker): simplify detection
Jay-Lalwani Feb 4, 2026
77f9390
fix(api): critical remove public facing api; keep internal server-sid…
Jay-Lalwani Feb 6, 2026
3a81d98
Restore calendar overview template (ClubForum calendar feature)
Feb 8, 2026
58d3d80
Local dev fixes: Docker DB config, Presence API, browse resilience, c…
Feb 8, 2026
b9354ef
Merge branch 'dev' into ClubForum+-Calender
Feb 8, 2026
ed413c0
Merge origin/dev into ClubForum+-Calender
Feb 8, 2026
27b3e7e
Merge branch 'ClubForum+-Calender' of https://github.com/thecoursefor…
Feb 8, 2026
c30e9da
Merge remote-tracking branch 'origin/dev' into ClubForum+-Calender
Mar 15, 2026
a37506d
Local dev: Presence API headers and DB defaults
Mar 15, 2026
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
11 changes: 11 additions & 0 deletions doc/dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ docker compose up

8. Ensure the website is up, running, and functional at `localhost:8000`.

## Loading full data

- **Club calendar events** – Loaded live from the Presence API (`api.presence.io/virginia/v1/events`). No extra steps; if you see no events, check that your network allows outbound requests to that URL.
- **Course browse (departments & courses)** – Requires the full database dump:
1. Download the [latest database backup](https://drive.google.com/drive/u/0/folders/1a7OkHkepOBWKiDou8nEhpAG41IzLi7mh) from Google Drive.
2. Save it as `db/latest.dump` in the project root.
3. Run `./scripts/reset-db.sh` (this restores the dump into the local Postgres container).
4. Start the app again: `docker compose up`.

Without the dump, the browse page shows only the two school names and no departments. After loading the dump, you get full departments and courses.

### VSCode Setup

When you open the project, VSCode may prompt you to install the recommended extensions for this project. Click yes and ensure that they are in your extension library. A list of the necessary libraries can be found [here](.././.vscode/extensions.json).
Expand Down
16 changes: 9 additions & 7 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ services:
context: .
args:
REQUIREMENTS: requirements/dev.txt
environment:
DJANGO_SETTINGS_MODULE: tcf_core.settings.dev
DB_HOST: db
DB_PORT: "5432"
command:
bash -c "/wait-for-it.sh tcf_db:${DB_PORT} -- \
bash -c "/wait-for-it.sh db:5432 -- \
python manage.py migrate && \
python manage.py collectstatic --noinput && \
python manage.py invalidate_cachalot tcf_website && \
echo 'Starting Django Server...' && \
python manage.py runserver 0.0.0.0:8000"
environment:
- DJANGO_SETTINGS_MODULE=tcf_core.settings.dev
volumes:
- .:/app
- /app/db/ # exclude the subfolder to prevent potential interference
Expand All @@ -32,9 +34,9 @@ services:
restart: always
container_name: tcf_db
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
POSTGRES_PORT: ${DB_PORT}
# Defaults (postgres/postgres/tcf) when .env is missing or empty
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
POSTGRES_DB: ${DB_NAME:-tcf}
volumes:
node_modules:
9 changes: 6 additions & 3 deletions tcf_core/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ def base(request):
def searchbar_context(request):
"""Provide context for the search bar."""
latest_semester = Semester.latest()
recent_semesters = Semester.objects.filter(
number__gte=latest_semester.number - 50 # 50 = 5 years * 10 semesters
).order_by("-number")
if latest_semester is None:
recent_semesters = Semester.objects.none()
else:
recent_semesters = Semester.objects.filter(
number__gte=latest_semester.number - 50 # 50 = 5 years * 10 semesters
).order_by("-number")

# Provide only the data needed for the filter options
# Filter values are managed by localStorage on the client side
Expand Down
5 changes: 5 additions & 0 deletions tcf_core/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,8 @@

# Toxicity threshold for filtering reviews
TOXICITY_THRESHOLD = 74

# Presence / Calendar feature
PRESENCE_SUBDOMAIN = env.str("PRESENCE_SUBDOMAIN", default="virginia")
PRESENCE_TIMEOUT_SECONDS = env.int("PRESENCE_TIMEOUT_SECONDS", default=8)
PRESENCE_CACHE_SECONDS = env.int("PRESENCE_CACHE_SECONDS", default=300)
28 changes: 21 additions & 7 deletions tcf_core/settings/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,25 @@

ALLOWED_HOSTS = ["localhost", "127.0.0.1", ".grok.io"]

# Local PostgreSQL database
# Local PostgreSQL database (defaults for empty .env / Docker)
def _dev_db_port():
val = env.get_value("DB_PORT", default="5432")
return int(val or "5432")


def _dev_db_str(key: str, default: str):
val = env.get_value(key, default=default)
return val or default


DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": env.str("DB_NAME"),
"USER": env.str("DB_USER"),
"PASSWORD": env.str("DB_PASSWORD"),
"HOST": env.str("DB_HOST"),
"PORT": env.int("DB_PORT"),
"NAME": _dev_db_str("DB_NAME", "tcf"),
"USER": _dev_db_str("DB_USER", "postgres"),
"PASSWORD": _dev_db_str("DB_PASSWORD", "postgres"),
"HOST": env.get_value("DB_HOST", default="localhost") or "localhost",
"PORT": _dev_db_port(),
}
}

Expand All @@ -29,4 +39,8 @@
+ ["debug_toolbar.middleware.DebugToolbarMiddleware"]
+ MIDDLEWARE[2:]
)
DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": lambda r: True}
DEBUG_TOOLBAR_CONFIG = {
"SHOW_TOOLBAR_CALLBACK": lambda r: True,
# Hide the Settings panel to avoid exposing full config dump
"DISABLE_PANELS": {"debug_toolbar.panels.settings.SettingsPanel"},
}
27 changes: 27 additions & 0 deletions tcf_website/migrations/0024_seed_browse_schools.py
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),
]
83 changes: 83 additions & 0 deletions tcf_website/services/presence.py
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()
Comment on lines +29 to +35
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Backoff retries non-retriable HTTP errors (4xx) unnecessarily.

raise_for_status() throws HTTPError (a subclass of RequestException), 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 a giveup predicate for 4xx responses.

🔧 Proposed fix — skip retries on client errors
+def _is_client_error(exc):
+    """Don't retry 4xx responses — they won't succeed on retry."""
+    if isinstance(exc, requests.HTTPError) and exc.response is not None:
+        return 400 <= exc.response.status_code < 500
+    return False
+
-@backoff.on_exception(backoff.expo, (requests.RequestException,), max_tries=3)
+@backoff.on_exception(
+    backoff.expo,
+    (requests.RequestException,),
+    max_tries=3,
+    giveup=_is_client_error,
+)
 def _get(url: str, params: dict | None = None) -> dict:
🤖 Prompt for AI Agents
In `@tcf_website/services/presence.py` around lines 31 - 35, The backoff decorator
on _get currently retries all RequestException subclasses (including HTTPError
from resp.raise_for_status()), causing 4xx client errors to be retried; update
the retry policy so client errors are not retried by adding a giveup predicate
or changing the exception handling: in the _get function/decorator (symbols:
_get, backoff.on_exception, requests.RequestException, resp.raise_for_status()),
detect HTTPError and its response status (e.response.status_code) and call
giveup for 4xx responses (or check resp.status_code before calling
raise_for_status and only raise/allow retries for 5xx/connection/timeouts) so
only network/5xx errors are retried with backoff.



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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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 -100

Repository: 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 -10

Repository: thecourseforum/theCourseForum2

Length of output: 816


Django's URL converter already prevents path traversal; explicit validation is a reasonable defense-in-depth measure.

event_uri is captured by Django's <str:event_uri> URL converter, which excludes forward slashes and prevents path traversal attacks. The requests library also URL-encodes path segments. However, adding explicit format validation in the service layer as a defensive enhancement is still reasonable if you want to enforce stricter constraints (e.g., allowing only alphanumeric characters and hyphens).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tcf_website/services/presence.py` around lines 63 - 68, Add explicit input
validation in get_event_details to enforce a safe event_uri format (e.g., only
alphanumerics and hyphens) before computing the cache key or calling _get; use a
simple regex (e.g., r'^[A-Za-z0-9-]+$') to validate event_uri, and if it fails
either raise ValueError or return None (consistent with existing error handling)
so malformed values are rejected early; update the start of get_event_details
(where event_uri is used to compute key and call _get) to perform this check and
handle invalid input.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a docstring for consistency and coverage goals.

get_events() has a docstring but get_event_details() does not. Given the PR comments mention targeting >80% docstring coverage, adding one here would help.

📝 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def get_event_details(event_uri: str):
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
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}")
data = cache.get(key)
if data is None:
data = _get(f"{BASE}/events/{event_uri}")
cache.set(key, data, CACHE_TTL)
return data
🤖 Prompt for AI Agents
In `@tcf_website/services/presence.py` around lines 46 - 52, Add a concise
docstring to the function get_event_details describing its purpose (fetches
event details, using cache), parameters (event_uri: str), return value (event
data dict or JSON), and side-effects (reads/writes cache via _cache_key/cache
with CACHE_TTL and calls _get on BASE). Place the docstring immediately below
the def get_event_details(...) signature and mirror style/format used by
get_events() for consistency in coverage and documentation.



11 changes: 11 additions & 0 deletions tcf_website/templates/base/sidebar.html
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ <h4><i class="fa fa-calendar fa-fw" aria-hidden="true" style="width: 1em; height
</a>
{% endif %}
</li>
<li>
<a href="{% url 'calendar_overview' %}"
class="list-group-item list-group-item-action pb-0
{% if request.resolver_match.url_name == 'calendar_overview' %}active{% endif %}">
<h4>
<i class="fa fa-calendar fa-fw" aria-hidden="true" style="width: 1em; height: 1em; vertical-align: middle; display: inline-block; filter: invert(100%) brightness(150%);"></i>
</h4>
<p>Calendar</p>
<hr>
</a>
</li>
<li>
<a href="{% url 'about' %}"
class="list-group-item list-group-item-action pb-0
Expand Down
14 changes: 9 additions & 5 deletions tcf_website/templates/browse/browse.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,15 @@ <h2 class="font-weight-light mb-0">Browse by {% if is_club %}Category{% else %}D
</div>
{% else %}
<div class="schools text-left">
{% include "browse/school.html" with school=CLAS %}
{% include "browse/school.html" with school=SEAS %}
{% for school in other_schools %}
{% include "browse/school.html" with school=school %}
{% endfor %}
{% if schools_loaded %}
{% if CLAS %}{% include "browse/school.html" with school=CLAS %}{% endif %}
{% if SEAS %}{% include "browse/school.html" with school=SEAS %}{% endif %}
{% for school in other_schools %}
{% include "browse/school.html" with school=school %}
{% endfor %}
{% else %}
<p class="text-muted">Course browse data is not loaded. For local development, load the database dump (see <code>doc/dev.md</code>). You can browse <a href="?mode=clubs">Clubs</a> instead.</p>
{% endif %}
</div>
{% endif %}
</div>
Expand Down
Loading
Loading