diff --git a/doc/dev.md b/doc/dev.md index 2413c88fc..ef4f17834 100644 --- a/doc/dev.md +++ b/doc/dev.md @@ -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). diff --git a/docker-compose.yml b/docker-compose.yml index db062dde8..003d6f056 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 @@ -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: diff --git a/tcf_core/context_processors.py b/tcf_core/context_processors.py index b3897a173..a36a0be95 100644 --- a/tcf_core/context_processors.py +++ b/tcf_core/context_processors.py @@ -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 diff --git a/tcf_core/settings/base.py b/tcf_core/settings/base.py index 31ed5d863..2c58d3dcc 100644 --- a/tcf_core/settings/base.py +++ b/tcf_core/settings/base.py @@ -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) diff --git a/tcf_core/settings/dev.py b/tcf_core/settings/dev.py index 9bd99e15d..45c55a4de 100644 --- a/tcf_core/settings/dev.py +++ b/tcf_core/settings/dev.py @@ -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(), } } @@ -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"}, +} diff --git a/tcf_website/migrations/0024_seed_browse_schools.py b/tcf_website/migrations/0024_seed_browse_schools.py new file mode 100644 index 000000000..9308e721e --- /dev/null +++ b/tcf_website/migrations/0024_seed_browse_schools.py @@ -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), + ] diff --git a/tcf_website/services/presence.py b/tcf_website/services/presence.py new file mode 100644 index 000000000..4b8d83f25 --- /dev/null +++ b/tcf_website/services/presence.py @@ -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}") + 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 + + diff --git a/tcf_website/templates/base/sidebar.html b/tcf_website/templates/base/sidebar.html index 5a50bd131..2109a41f8 100644 --- a/tcf_website/templates/base/sidebar.html +++ b/tcf_website/templates/base/sidebar.html @@ -60,6 +60,17 @@
Calendar
+Course browse data is not loaded. For local development, load the database dump (see doc/dev.md). You can browse Clubs instead.