diff --git a/backend/create_fake_users.py b/backend/create_fake_users.py index 3a2a2f5..2a9c9ec 100644 --- a/backend/create_fake_users.py +++ b/backend/create_fake_users.py @@ -1,3 +1,9 @@ +"""Local seed script: insert a handful of fake users for development. + +Run once after pointing the backend at a fresh database (``python +create_fake_users.py``). Idempotent — users with an existing email are +skipped rather than duplicated. Not used in production. +""" from database import SessionLocal, engine, Base from models import User import uuid diff --git a/backend/database.py b/backend/database.py index a2b3c69..308de59 100644 --- a/backend/database.py +++ b/backend/database.py @@ -1,4 +1,3 @@ -# database.py import os from dotenv import load_dotenv from sqlalchemy import create_engine @@ -15,9 +14,20 @@ Base = declarative_base() def get_db(): + """ + Used as a FastAPI ``Depends(get_db)`` so each request gets its own + session and the connection is always returned to the pool. + """ db = SessionLocal() try: yield db finally: db.close() + + +# Limit pdoc's rendered surface to the public API. The hidden symbols +# (engine, SessionLocal, SQLALCHEMY_DATABASE_URL) are still importable +# in Python; this just keeps their values out of the docs HTML, since +# their reprs would leak the local connection string. +__all__ = ["Base", "get_db"] \ No newline at end of file diff --git a/backend/export_openapi.py b/backend/export_openapi.py new file mode 100644 index 0000000..1da9e97 --- /dev/null +++ b/backend/export_openapi.py @@ -0,0 +1,26 @@ +"""Dump the FastAPI app's OpenAPI schema to docs/api/openapi.json. + +Run from the backend directory with the project venv active: + + python export_openapi.py + +Importing ``main`` triggers the schema bootstrap (``create_all`` plus +``ensure_schema_updates``), so a working DATABASE_URL is required — +the same one you use for local development. +""" +import json +from pathlib import Path + +from main import app + +OUT_PATH = Path(__file__).resolve().parent.parent / "docs" / "api" / "openapi.json" + + +def main() -> None: + OUT_PATH.parent.mkdir(parents=True, exist_ok=True) + OUT_PATH.write_text(json.dumps(app.openapi(), indent=2)) + print(f"Wrote {OUT_PATH}") + + +if __name__ == "__main__": + main() diff --git a/backend/main.py b/backend/main.py index 869c99f..7df3b90 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,3 +1,12 @@ +""" +- Wires up the FastAPI app and CORS for the React frontend. +- Defines Pydantic request/response schemas alongside their routes. +- Implements all REST endpoints, grouped by feature: users & profile, + courses & enrollment, posts & moderation, conversations & messages, + study groups, study sessions, and Google Calendar availability sync. +- Runs lightweight additive schema migrations at startup via + ``ensure_schema_updates`` so local dev databases stay compatible. +""" from fastapi import FastAPI, Depends, HTTPException, Query from sqlalchemy.orm import Session from sqlalchemy import func, text, or_ @@ -8,9 +17,6 @@ from typing import List, Optional from datetime import datetime, timedelta, timezone -# WARNING: This deletes all data! -# Uncomment once to reset the schema for your new multi-class structure. -# Base.metadata.drop_all(bind=engine) def ensure_schema_updates(): """Apply minimal additive schema updates for local development.""" diff --git a/backend/models.py b/backend/models.py index 47fffdc..2f8ee66 100644 --- a/backend/models.py +++ b/backend/models.py @@ -1,3 +1,14 @@ +""" +High-level entity map: + +- ``User`` — a person (student, TA, or admin). +- ``Course`` + ``Enrollment`` — a class and its roster. +- ``Post`` + ``PostVote`` — discussion/resource posts and their votes. +- ``StudyGroup`` + ``StudyGroupMember`` — long-lived groups inside a course. +- ``StudySession`` + ``StudySessionInvitee`` — scheduled meetings (solo or group). +- ``UserAvailability`` — busy/free blocks, typically synced from Google Calendar. +- ``Conversation`` + ``ConversationParticipant`` + ``Message`` — chat. +""" from database import Base from sqlalchemy import Column, Integer, String, DateTime, Enum, ForeignKey, Text, Boolean from sqlalchemy.orm import relationship @@ -6,12 +17,19 @@ class UserRole(str, enum.Enum): + """Allowed values for ``User.role``. Stored as a plain string column.""" STUDENT = "Student" TA = "TA" ADMIN = "Admin" class User(Base): + """A StudySync user, keyed by their Firebase Auth UID. + + Holds profile basics plus an optional Google Calendar token used by + the calendar-sync feature. All ownership/authorship foreign keys in + other tables point at ``firebase_uid``. + """ __tablename__ = "users" # Firebase UID is now the primary key (string) @@ -31,6 +49,7 @@ class User(Base): class Course(Base): + """A class/course that students can enroll in and post to.""" __tablename__ = "courses" id = Column(Integer, primary_key=True, index=True) @@ -49,6 +68,7 @@ class Course(Base): class Enrollment(Base): + """Join table linking a ``User`` to a ``Course`` they are enrolled in.""" __tablename__ = "enrollments" id = Column(Integer, primary_key=True, index=True) @@ -61,6 +81,11 @@ class Enrollment(Base): class Conversation(Base): + """A chat thread, either 1:1 (``is_group`` False) or a named group chat. + + Optionally scoped to a course via ``course_id`` so course-specific + chats can be filtered out of personal DMs. + """ __tablename__ = "conversations" conversation_id = Column(Integer, primary_key=True, index=True) @@ -75,6 +100,7 @@ class Conversation(Base): class ConversationParticipant(Base): + """Membership of a user in a conversation; controls who can read/send.""" __tablename__ = "conversation_participants" participant_id = Column(Integer, primary_key=True, index=True) @@ -88,6 +114,7 @@ class ConversationParticipant(Base): class Message(Base): + """A single chat message inside a ``Conversation``.""" __tablename__ = "messages" message_id = Column("id", Integer, primary_key=True, index=True) @@ -102,6 +129,12 @@ class Message(Base): class Post(Base): + """A discussion or resource post within a course. + + ``score`` is a denormalized vote tally maintained alongside + ``PostVote`` rows. ``is_flagged`` lets moderators hide a post + without deleting it. + """ __tablename__ = "posts" id = Column(Integer, primary_key=True, index=True) @@ -120,6 +153,7 @@ class Post(Base): class PostVote(Base): + """One user's upvote (+1) or downvote (-1) on a ``Post``.""" __tablename__ = "post_votes" id = Column(Integer, primary_key=True, index=True) @@ -131,6 +165,11 @@ class PostVote(Base): class StudyGroup(Base): + """A persistent study group inside a course. + + Holds the long-lived membership (``StudyGroupMember``) and any + scheduled meetings (``StudySession``). + """ __tablename__ = "study_groups" id = Column(Integer, primary_key=True, index=True) @@ -144,6 +183,12 @@ class StudyGroup(Base): class StudySession(Base): + """A scheduled study meeting. + + ``session_type`` is ``"solo"`` for personal study blocks or + ``"group"`` when tied to a ``StudyGroup`` via ``group_id``. + Invitees (for group sessions) live in ``StudySessionInvitee``. + """ __tablename__ = "study_sessions" id = Column(Integer, primary_key=True, index=True) @@ -161,6 +206,7 @@ class StudySession(Base): class StudyGroupMember(Base): + """A user's membership in a ``StudyGroup``, keyed by email.""" __tablename__ = "study_group_members" id = Column(Integer, primary_key=True, index=True) @@ -172,6 +218,13 @@ class StudyGroupMember(Base): class UserAvailability(Base): + """A busy/free time block for a user. + + Most rows are imported from Google Calendar (``source = + "google_calendar"``); rows tied to a StudySession reflect blocks + StudySync itself created. Used by the scheduler to find overlap + when proposing meeting times. + """ __tablename__ = "user_availability" id = Column(Integer, primary_key=True, index=True) @@ -184,6 +237,7 @@ class UserAvailability(Base): class StudySessionInvitee(Base): + """An email invited to a ``StudySession``; the invitee may not yet be a registered user.""" __tablename__ = "study_session_invitees" id = Column(Integer, primary_key=True, index=True) diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 9d4b994..d9d250b 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -1,3 +1,16 @@ +"""Shared pytest fixtures for the StudySync backend test suite. + +Provides: + +- ``_guard_test_db`` (session-scoped): refuses to run if ``DATABASE_URL`` + doesn't point at a database whose name contains ``test``, so the + destructive ``db`` fixture can never wipe a developer's real data. + Override with ``ALLOW_DB_RESET_FOR_TESTS=1`` only if you know what + you're doing. +- ``client``: a FastAPI ``TestClient`` lazily imported after the guard. +- ``db``: a clean SQLAlchemy session, with the schema dropped and + recreated before each test that requests it. +""" import os import pytest diff --git a/backend/tests/test_authentication.py b/backend/tests/test_authentication.py index 0fbaca3..d3f83bc 100644 --- a/backend/tests/test_authentication.py +++ b/backend/tests/test_authentication.py @@ -1,3 +1,9 @@ +"""Tests for the User Authentication & Profile Management requirement. + +Covers the Firebase-sync upsert path (``POST /sync-user``), profile +fetch and update endpoints, and a Hypothesis property-based pass over +``/sync-user`` to fuzz unusual but valid inputs. +""" import pytest import models from fastapi.testclient import TestClient @@ -7,6 +13,7 @@ def _seed_test_users(db): + """Insert one Student, one TA, and one Admin so role-gated paths can be exercised.""" import models student = models.User( diff --git a/backend/tests/test_messaging_integrity.py b/backend/tests/test_messaging_integrity.py index 7bea418..fac4447 100644 --- a/backend/tests/test_messaging_integrity.py +++ b/backend/tests/test_messaging_integrity.py @@ -1,7 +1,14 @@ +"""Tests for messaging-data integrity. + +Verifies that the messaging endpoints reject malformed input (e.g. +empty/whitespace-only message bodies) before persisting anything, +which keeps the conversation tables free of garbage rows. +""" import pytest def _seed_users_course_enrollments(db): + """Create a TA, two students, a course, and enroll both students in it.""" import models ta = models.User( @@ -45,6 +52,7 @@ def _seed_users_course_enrollments(db): def test_send_message_rejects_empty_content(client, db): + """POST /messages with whitespace-only content must return 400, not persist.""" seeded = _seed_users_course_enrollments(db) course_id = seeded["course"].id diff --git a/backend/tests/test_permissions.py b/backend/tests/test_permissions.py index 8c4d2cc..5f2a4b8 100644 --- a/backend/tests/test_permissions.py +++ b/backend/tests/test_permissions.py @@ -1,3 +1,17 @@ +"""Permission and inbox-shape tests against the live FastAPI app. + +Two concerns: + +- Role gating: a Student must not be able to delete a post (only TAs + and Admins can). +- Inbox/message endpoint contracts: response shape and required-param + validation for ``/conversations/inbox/global`` and ``/messages``, + plus a Hypothesis property-based pass over the inbox endpoint. + +These tests share a module-level ``TestClient`` and do not use the +``db`` fixture from conftest, so they run against whatever database +``main.app`` is configured for. +""" import pytest from fastapi.testclient import TestClient from main import app @@ -5,14 +19,9 @@ client = TestClient(app) + def test_student_cannot_delete_post(): - """ - Requirement: Permissions (TA vs Student) - Verifies that a Student role receives a 403 Forbidden when attempting to delete. - """ - # 1. We assume a post with ID 1 exists in the DB for this test - # 2. We pass a user_uid that belongs to a 'Student' - # Note: You may need to ensure this user exists in your test DB or mock the DB call + """DELETE /posts/{id} as a Student must return 403 with the expected detail.""" response = client.delete("/posts/1?user_uid=student_user_123") # Assert that the backend blocks the action @@ -20,23 +29,19 @@ def test_student_cannot_delete_post(): assert response.json()["detail"] == "Only TAs or Admins can delete posts." def test_get_global_inbox_structure(): - """ - Requirement: Core Functionality (Global Inbox) - Verifies the inbox returns the expected data structure. - """ + """Global inbox must return a JSON list (the conversation feed shape).""" response = client.get("/conversations/inbox/global?user_uid=test_user") assert response.status_code == 200 assert isinstance(response.json(), list) def test_get_global_inbox_empty(): - """Verifies that a new user with no chats gets an empty list, not an error.""" + """A user with no conversations should get an empty list, not a 404 or error.""" response = client.get("/conversations/inbox/global?user_uid=new_user_999") assert response.status_code == 200 assert response.json() == [] def test_get_class_discussion_messages(): - """Verifies fetching public group messages for a specific course.""" - # Even for group chats, user1 and user2 must be present to pass validation + """Fetching the public class group chat (is_group=true) must return a list.""" params = { "user1": "test_user", "user2": "GROUP_CHAT", @@ -47,9 +52,9 @@ def test_get_class_discussion_messages(): assert response.status_code == 200 assert isinstance(response.json(), list) - + def test_global_inbox_missing_params(): - """Verifies the backend returns 422 Unprocessable Entity if UID is missing.""" + """Global inbox without the required user_uid query param must 422 (FastAPI validation).""" response = client.get("/conversations/inbox/global") # No query param assert response.status_code == 422 diff --git a/backend/tests/test_resource_library_crud.py b/backend/tests/test_resource_library_crud.py index 18ee409..7e0958f 100644 --- a/backend/tests/test_resource_library_crud.py +++ b/backend/tests/test_resource_library_crud.py @@ -1,4 +1,12 @@ +"""Tests for the resource-library CRUD path on /posts. + +Covers the create-and-list happy path and the role-gated delete: +Students can create posts, but only TAs/Admins may delete them. +""" + + def _seed_users_course(db): + """Create one TA, one Student, and a course owned by the TA.""" import models ta = models.User( @@ -29,6 +37,7 @@ def _seed_users_course(db): def test_posts_create_and_list(client, db): + """A Student can POST a resource post and the new post appears in GET /posts.""" seeded = _seed_users_course(db) course_id = seeded["course"].id @@ -54,6 +63,7 @@ def test_posts_create_and_list(client, db): def test_posts_delete_requires_ta_or_admin(client, db): + """DELETE /posts/{id} must 403 for the student author and 200 for the TA.""" seeded = _seed_users_course(db) course_id = seeded["course"].id diff --git a/docs/api/openapi.json b/docs/api/openapi.json new file mode 100644 index 0000000..2dd81c8 --- /dev/null +++ b/docs/api/openapi.json @@ -0,0 +1,1946 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "paths": { + "/sync-user": { + "post": { + "summary": "Sync User", + "description": "Synchronizes Firebase Auth user with PostgreSQL database.", + "operationId": "sync_user_sync_user_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserCreate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/user/{firebase_uid}": { + "get": { + "summary": "Get User Profile", + "description": "Fetch user profile by Firebase UID.", + "operationId": "get_user_profile_user__firebase_uid__get", + "parameters": [ + { + "name": "firebase_uid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Firebase Uid" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/user/{firebase_uid}/update": { + "put": { + "summary": "Update User Profile", + "description": "Updates profile while preventing login issues during pending email verification.", + "operationId": "update_user_profile_user__firebase_uid__update_put", + "parameters": [ + { + "name": "firebase_uid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Firebase Uid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserCreate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/users": { + "get": { + "summary": "List Users", + "description": "Return all users for chat roster.", + "operationId": "list_users_users_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/UserSimple" + }, + "type": "array", + "title": "Response List Users Users Get" + } + } + } + } + } + } + }, + "/users/{firebase_uid}/courses": { + "get": { + "summary": "Get User Courses", + "description": "Returns all courses a user is enrolled in for the home page grid.", + "operationId": "get_user_courses_users__firebase_uid__courses_get", + "parameters": [ + { + "name": "firebase_uid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Firebase Uid" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CourseResponse" + }, + "title": "Response Get User Courses Users Firebase Uid Courses Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/courses/{course_id}/members": { + "get": { + "summary": "Get Course Members", + "description": "Returns all enrolled users for a course.", + "operationId": "get_course_members_courses__course_id__members_get", + "parameters": [ + { + "name": "course_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Course Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserSimple" + }, + "title": "Response Get Course Members Courses Course Id Members Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/conversations/one-on-one": { + "post": { + "summary": "Get Or Create One On One", + "description": "Creates or retrieves a 1-on-1 DM scoped to a specific course.", + "operationId": "get_or_create_one_on_one_conversations_one_on_one_post", + "parameters": [ + { + "name": "user_uid_1", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "User Uid 1" + } + }, + { + "name": "user_uid_2", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "User Uid 2" + } + }, + { + "name": "course_id", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "title": "Course Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/conversations/group": { + "get": { + "summary": "Get Or Create Group Chat", + "description": "Retrieves or creates a group chat for a class or study group.", + "operationId": "get_or_create_group_chat_conversations_group_get", + "parameters": [ + { + "name": "course_id", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "title": "Course Id" + } + }, + { + "name": "group_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Group Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/messages": { + "get": { + "summary": "Get Messages", + "description": "Fetch messages for either a private DM or a Class Group chat.", + "operationId": "get_messages_messages_get", + "parameters": [ + { + "name": "user1", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "User1" + } + }, + { + "name": "user2", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "User2" + } + }, + { + "name": "course_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Course Id" + } + }, + { + "name": "is_group", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false, + "title": "Is Group" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "summary": "Send Message", + "description": "Send a message in either a 1-on-1 or Class Group context.", + "operationId": "send_message_messages_post", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageSend" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/conversations/class/{course_id}": { + "get": { + "summary": "Get Class Conversation", + "description": "Find or create the public group conversation for a specific course.", + "operationId": "get_class_conversation_conversations_class__course_id__get", + "parameters": [ + { + "name": "course_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Course Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/conversations/inbox/global": { + "get": { + "summary": "Get Global Inbox", + "description": "Fetches ALL active conversations for a user across ALL enrolled courses.\nAppends course codes to group chat names.", + "operationId": "get_global_inbox_conversations_inbox_global_get", + "parameters": [ + { + "name": "user_uid", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "User Uid" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/posts": { + "post": { + "summary": "Create Post", + "description": "Create a resource post in a course.", + "operationId": "create_post_posts_post", + "parameters": [ + { + "name": "course_id", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "title": "Course Id" + } + }, + { + "name": "author_uid", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "Author Uid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostCreate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "get": { + "summary": "Get Posts", + "description": "Fetch posts for a course with vote information.", + "operationId": "get_posts_posts_get", + "parameters": [ + { + "name": "course_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Course Id" + } + }, + { + "name": "current_user_uid", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Current User Uid" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/posts/{post_id}/flag": { + "post": { + "summary": "Flag Post", + "description": "Flags a post for TA/Admin review.", + "operationId": "flag_post_posts__post_id__flag_post", + "parameters": [ + { + "name": "post_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Post Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/posts/{post_id}/vote": { + "post": { + "summary": "Vote On Post", + "description": "Vote on a post (+1 upvote, -1 downvote, 0 neutral).", + "operationId": "vote_on_post_posts__post_id__vote_post", + "parameters": [ + { + "name": "post_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Post Id" + } + }, + { + "name": "user_uid", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "User Uid" + } + }, + { + "name": "vote", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "title": "Vote" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/posts/{post_id}": { + "delete": { + "summary": "Delete Post", + "description": "Allows TAs and Admins to delete any post.", + "operationId": "delete_post_posts__post_id__delete", + "parameters": [ + { + "name": "post_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Post Id" + } + }, + { + "name": "user_uid", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "User Uid" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/posts/flagged": { + "get": { + "summary": "Get Flagged Posts", + "description": "Fetch all posts that have been flagged by users.", + "operationId": "get_flagged_posts_posts_flagged_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/PostOut" + }, + "type": "array", + "title": "Response Get Flagged Posts Posts Flagged Get" + } + } + } + } + } + } + }, + "/posts/{post_id}/dismiss-flag": { + "post": { + "summary": "Dismiss Flag", + "description": "Unflags a post (TA determines content is safe).", + "operationId": "dismiss_flag_posts__post_id__dismiss_flag_post", + "parameters": [ + { + "name": "post_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Post Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/courses": { + "post": { + "summary": "Create Course", + "description": "Creates a new class workspace. Only for Admins/TAs.", + "operationId": "create_course_courses_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CourseCreate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CourseResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/courses/join": { + "post": { + "summary": "Join Course", + "description": "Enrolls a student via a class code.", + "operationId": "join_course_courses_join_post", + "parameters": [ + { + "name": "course_code", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "Course Code" + } + }, + { + "name": "firebase_uid", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "Firebase Uid" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/study-groups": { + "post": { + "summary": "Create Study Group", + "description": "Create a study group in a course.", + "operationId": "create_study_group_study_groups_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StudyGroupCreate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/study-groups/{group_id}/members": { + "post": { + "summary": "Add Group Member", + "description": "Add a member to a study group.", + "operationId": "add_group_member_study_groups__group_id__members_post", + "parameters": [ + { + "name": "group_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Group Id" + } + }, + { + "name": "user_email", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "User Email" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/study-groups/{group_id}": { + "get": { + "summary": "Get Study Group", + "description": "Fetch study group details.", + "operationId": "get_study_group_study_groups__group_id__get", + "parameters": [ + { + "name": "group_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Group Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/study-groups/course/{course_id}": { + "get": { + "summary": "Get Course Study Groups", + "description": "Fetch all study groups for a course.", + "operationId": "get_course_study_groups_study_groups_course__course_id__get", + "parameters": [ + { + "name": "course_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Course Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/study-sessions": { + "get": { + "summary": "Get Study Sessions", + "description": "Fetches study sessions for a user, optionally filtered by course.", + "operationId": "get_study_sessions_study_sessions_get", + "parameters": [ + { + "name": "user_email", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "User Email" + } + }, + { + "name": "range_start", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "Range Start" + } + }, + { + "name": "range_end", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "Range End" + } + }, + { + "name": "course_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Course Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "summary": "Create Study Session", + "description": "Creates a study session.", + "operationId": "create_study_session_study_sessions_post", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StudySessionCreate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/study-sessions/course/{course_id}": { + "get": { + "summary": "Get Course Sessions", + "description": "Fetch study sessions in a course visible to the requester.", + "operationId": "get_course_sessions_study_sessions_course__course_id__get", + "parameters": [ + { + "name": "course_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Course Id" + } + }, + { + "name": "range_start", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "Range Start" + } + }, + { + "name": "range_end", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "Range End" + } + }, + { + "name": "requester_email", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "Requester Email" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/study-sessions/course/{course_id}/summary": { + "get": { + "summary": "Get Course Session Summary", + "description": "Fetch upcoming visible study sessions and completed study hours for a course.", + "operationId": "get_course_session_summary_study_sessions_course__course_id__summary_get", + "parameters": [ + { + "name": "course_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Course Id" + } + }, + { + "name": "requester_email", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "Requester Email" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/study-sessions/{session_id}": { + "put": { + "summary": "Update Study Session", + "description": "Updates an existing study session and refreshes invitees/busy blocks.", + "operationId": "update_study_session_study_sessions__session_id__put", + "parameters": [ + { + "name": "session_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Session Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StudySessionCreate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/availability/sync": { + "post": { + "summary": "Sync Availability", + "description": "Syncs busy blocks from Google Calendar.", + "operationId": "sync_availability_availability_sync_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AvailabilitySync" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/availability/connected": { + "get": { + "summary": "Check Availability Connected", + "description": "Returns whether a user has synced Google Calendar availability.", + "operationId": "check_availability_connected_availability_connected_get", + "parameters": [ + { + "name": "user_email", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "User Email" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/availability": { + "get": { + "summary": "Get Availability", + "description": "Fetch busy blocks for one or more users in a time range.", + "operationId": "get_availability_availability_get", + "parameters": [ + { + "name": "user_emails", + "in": "query", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "title": "User Emails" + } + }, + { + "name": "time_min", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "Time Min" + } + }, + { + "name": "time_max", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "Time Max" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "AvailabilitySync": { + "properties": { + "user_email": { + "type": "string", + "title": "User Email" + }, + "starts_at": { + "type": "string", + "title": "Starts At" + }, + "ends_at": { + "type": "string", + "title": "Ends At" + }, + "timezone": { + "type": "string", + "title": "Timezone", + "default": "UTC" + }, + "source": { + "type": "string", + "title": "Source", + "default": "google_calendar" + }, + "busy_slots": { + "items": { + "$ref": "#/components/schemas/BusySlot" + }, + "type": "array", + "title": "Busy Slots" + } + }, + "type": "object", + "required": [ + "user_email", + "starts_at", + "ends_at", + "busy_slots" + ], + "title": "AvailabilitySync" + }, + "BusySlot": { + "properties": { + "starts_at": { + "type": "string", + "title": "Starts At" + }, + "ends_at": { + "type": "string", + "title": "Ends At" + } + }, + "type": "object", + "required": [ + "starts_at", + "ends_at" + ], + "title": "BusySlot" + }, + "CourseCreate": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "course_code": { + "type": "string", + "title": "Course Code" + }, + "owner_id": { + "type": "string", + "title": "Owner Id" + } + }, + "type": "object", + "required": [ + "name", + "course_code", + "owner_id" + ], + "title": "CourseCreate" + }, + "CourseResponse": { + "properties": { + "id": { + "type": "integer", + "title": "Id" + }, + "course_code": { + "type": "string", + "title": "Course Code" + }, + "name": { + "type": "string", + "title": "Name" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + } + }, + "type": "object", + "required": [ + "id", + "course_code", + "name", + "description" + ], + "title": "CourseResponse" + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "MessageSend": { + "properties": { + "sender_uid": { + "type": "string", + "title": "Sender Uid" + }, + "receiver_uid": { + "type": "string", + "title": "Receiver Uid" + }, + "content": { + "type": "string", + "title": "Content" + }, + "course_id": { + "type": "integer", + "title": "Course Id" + }, + "is_group": { + "type": "boolean", + "title": "Is Group", + "default": false + } + }, + "type": "object", + "required": [ + "sender_uid", + "receiver_uid", + "content", + "course_id" + ], + "title": "MessageSend" + }, + "PostCreate": { + "properties": { + "title": { + "type": "string", + "title": "Title" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "resource_link": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Resource Link" + } + }, + "type": "object", + "required": [ + "title" + ], + "title": "PostCreate" + }, + "PostOut": { + "properties": { + "id": { + "type": "integer", + "title": "Id" + }, + "author_uid": { + "type": "string", + "title": "Author Uid" + }, + "author_name": { + "type": "string", + "title": "Author Name" + }, + "author_role": { + "type": "string", + "title": "Author Role" + }, + "title": { + "type": "string", + "title": "Title" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "resource_link": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Resource Link" + }, + "score": { + "type": "integer", + "title": "Score" + }, + "user_vote": { + "type": "integer", + "title": "User Vote" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "course_id": { + "type": "integer", + "title": "Course Id" + } + }, + "type": "object", + "required": [ + "id", + "author_uid", + "author_name", + "author_role", + "title", + "description", + "resource_link", + "score", + "user_vote", + "created_at", + "course_id" + ], + "title": "PostOut" + }, + "StudyGroupCreate": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "course_id": { + "type": "integer", + "title": "Course Id" + } + }, + "type": "object", + "required": [ + "name", + "course_id" + ], + "title": "StudyGroupCreate" + }, + "StudySessionCreate": { + "properties": { + "creator_email": { + "type": "string", + "title": "Creator Email" + }, + "course_id": { + "type": "integer", + "title": "Course Id" + }, + "session_type": { + "type": "string", + "title": "Session Type", + "default": "solo" + }, + "title": { + "type": "string", + "title": "Title" + }, + "starts_at": { + "type": "string", + "title": "Starts At" + }, + "ends_at": { + "type": "string", + "title": "Ends At" + }, + "group_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Group Id" + }, + "invitees": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Invitees", + "default": [] + } + }, + "type": "object", + "required": [ + "creator_email", + "course_id", + "title", + "starts_at", + "ends_at" + ], + "title": "StudySessionCreate" + }, + "UserCreate": { + "properties": { + "firebase_uid": { + "type": "string", + "title": "Firebase Uid" + }, + "email": { + "type": "string", + "title": "Email" + }, + "full_name": { + "type": "string", + "title": "Full Name" + }, + "role": { + "type": "string", + "title": "Role" + } + }, + "type": "object", + "required": [ + "firebase_uid", + "email", + "full_name", + "role" + ], + "title": "UserCreate" + }, + "UserSimple": { + "properties": { + "firebase_uid": { + "type": "string", + "title": "Firebase Uid" + }, + "full_name": { + "type": "string", + "title": "Full Name" + }, + "email": { + "type": "string", + "title": "Email" + }, + "role": { + "type": "string", + "title": "Role" + } + }, + "type": "object", + "required": [ + "firebase_uid", + "full_name", + "email", + "role" + ], + "title": "UserSimple" + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + } + }, + "type": "object", + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError" + } + } + } +} \ No newline at end of file diff --git a/docs/api/redoc.html b/docs/api/redoc.html new file mode 100644 index 0000000..8d060e9 --- /dev/null +++ b/docs/api/redoc.html @@ -0,0 +1,15 @@ + + + + + + StudySync — REST API Reference + + + + + + + diff --git a/docs/backend/database.html b/docs/backend/database.html new file mode 100644 index 0000000..34fd024 --- /dev/null +++ b/docs/backend/database.html @@ -0,0 +1,408 @@ + + + + + + + database API documentation + + + + + + + + + +
+
+

+database

+ + + + + + +
 1import os
+ 2from dotenv import load_dotenv
+ 3from sqlalchemy import create_engine
+ 4from sqlalchemy.orm import sessionmaker, declarative_base
+ 5
+ 6# Load variables from .env
+ 7load_dotenv()
+ 8
+ 9# Use the environment variable, or a fallback if it's missing
+10SQLALCHEMY_DATABASE_URL = os.getenv("DATABASE_URL")
+11
+12engine = create_engine(SQLALCHEMY_DATABASE_URL)
+13SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
+14Base = declarative_base()
+15
+16def get_db():
+17    """
+18    Used as a FastAPI ``Depends(get_db)`` so each request gets its own
+19    session and the connection is always returned to the pool.
+20    """
+21    db = SessionLocal()
+22    try:
+23        yield db
+24    finally:
+25        db.close()
+26
+27
+28# Limit pdoc's rendered surface to the public API. The hidden symbols
+29# (engine, SessionLocal, SQLALCHEMY_DATABASE_URL) are still importable
+30# in Python; this just keeps their values out of the docs HTML, since
+31# their reprs would leak the local connection string.
+32__all__ = ["Base", "get_db"]
+33        
+
+ + +
+
+
+ + class + Base(sqlalchemy.orm.decl_api._DynamicAttributesType, sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]): + + +
+ + +

The base class of the class hierarchy.

+ +

When called, it accepts no arguments and returns a new featureless +instance that has no instance attributes and cannot be given any.

+
+ + +
+ +
+ + Base(**kwargs: Any) + + + +
+ +
2167def _declarative_constructor(self: Any, **kwargs: Any) -> None:
+2168    """A simple constructor that allows initialization from kwargs.
+2169
+2170    Sets attributes on the constructed instance using the names and
+2171    values in ``kwargs``.
+2172
+2173    Only keys that are present as
+2174    attributes of the instance's class are allowed. These could be,
+2175    for example, any mapped columns or relationships.
+2176    """
+2177    cls_ = type(self)
+2178    for k in kwargs:
+2179        if not hasattr(cls_, k):
+2180            raise TypeError(
+2181                "%r is an invalid keyword argument for %s" % (k, cls_.__name__)
+2182            )
+2183        setattr(self, k, kwargs[k])
+
+ + +

A simple constructor that allows initialization from kwargs.

+ +

Sets attributes on the constructed instance using the names and +values in kwargs.

+ +

Only keys that are present as +attributes of the instance's class are allowed. These could be, +for example, any mapped columns or relationships.

+
+ + +
+
+
+ registry: sqlalchemy.orm.decl_api.registry = +<sqlalchemy.orm.decl_api.registry object> + + +
+ + + + +
+
+
+ metadata: sqlalchemy.sql.schema.MetaData = +MetaData() + + +
+ + + + +
+
+
+ +
+ + def + get_db(): + + + +
+ +
17def get_db():
+18    """
+19    Used as a FastAPI ``Depends(get_db)`` so each request gets its own
+20    session and the connection is always returned to the pool.
+21    """
+22    db = SessionLocal()
+23    try:
+24        yield db
+25    finally:
+26        db.close()
+
+ + +

Used as a FastAPI Depends(get_db) so each request gets its own +session and the connection is always returned to the pool.

+
+ + +
+
+ + \ No newline at end of file diff --git a/docs/backend/index.html b/docs/backend/index.html new file mode 100644 index 0000000..d090396 --- /dev/null +++ b/docs/backend/index.html @@ -0,0 +1,217 @@ + + + + + + + Module List + + + + + + + + + + +
+ + pdoc + + +
+
+ + \ No newline at end of file diff --git a/docs/backend/models.html b/docs/backend/models.html new file mode 100644 index 0000000..2d3758c --- /dev/null +++ b/docs/backend/models.html @@ -0,0 +1,3974 @@ + + + + + + + models API documentation + + + + + + + + + + +
+
+

models

+ +
+

High-level entity map:

+ + +
+ + + + + +
+
  1"""
+  2High-level entity map:
+  3
+  4- ``User`` — a person (student, TA, or admin).
+  5- ``Course`` + ``Enrollment`` — a class and its roster.
+  6- ``Post`` + ``PostVote`` — discussion/resource posts and their votes.
+  7- ``StudyGroup`` + ``StudyGroupMember`` — long-lived groups inside a course.
+  8- ``StudySession`` + ``StudySessionInvitee`` — scheduled meetings (solo or group).
+  9- ``UserAvailability`` — busy/free blocks, typically synced from Google Calendar.
+ 10- ``Conversation`` + ``ConversationParticipant`` + ``Message`` — chat.
+ 11"""
+ 12from database import Base
+ 13from sqlalchemy import Column, Integer, String, DateTime, Enum, ForeignKey, Text, Boolean
+ 14from sqlalchemy.orm import relationship
+ 15import datetime
+ 16import enum
+ 17
+ 18
+ 19class UserRole(str, enum.Enum):
+ 20    """Allowed values for ``User.role``. Stored as a plain string column."""
+ 21    STUDENT = "Student"
+ 22    TA = "TA"
+ 23    ADMIN = "Admin"
+ 24
+ 25
+ 26class User(Base):
+ 27    """A StudySync user, keyed by their Firebase Auth UID.
+ 28
+ 29    Holds profile basics plus an optional Google Calendar token used by
+ 30    the calendar-sync feature. All ownership/authorship foreign keys in
+ 31    other tables point at ``firebase_uid``.
+ 32    """
+ 33    __tablename__ = "users"
+ 34
+ 35    # Firebase UID is now the primary key (string)
+ 36    firebase_uid = Column(String, primary_key=True, index=True, nullable=False)
+ 37    email = Column(String, unique=True, index=True, nullable=False)
+ 38    full_name = Column(String)
+ 39    role = Column(String, default="Student")
+ 40    google_calendar_token = Column(String, nullable=True)
+ 41    created_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.UTC))
+ 42    
+ 43    # Relationships
+ 44    messages = relationship("Message", back_populates="sender")
+ 45    conversation_participants = relationship("ConversationParticipant", back_populates="user")
+ 46    courses_created = relationship("Course", back_populates="owner")
+ 47    posts = relationship("Post", back_populates="author")
+ 48    enrollments = relationship("Enrollment", back_populates="user")
+ 49
+ 50
+ 51class Course(Base):
+ 52    """A class/course that students can enroll in and post to."""
+ 53    __tablename__ = "courses"
+ 54
+ 55    id = Column(Integer, primary_key=True, index=True)
+ 56    course_code = Column(String, unique=True, index=True, nullable=False)
+ 57    name = Column(String, nullable=False)
+ 58    description = Column(Text, nullable=True)
+ 59    owner_id = Column(String, ForeignKey("users.firebase_uid"), nullable=False)
+ 60    created_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.UTC))
+ 61
+ 62    # Relationships
+ 63    owner = relationship("User", back_populates="courses_created")
+ 64    members = relationship("Enrollment", back_populates="course", cascade="all, delete-orphan")
+ 65    posts = relationship("Post", back_populates="course")
+ 66    study_groups = relationship("StudyGroup", back_populates="course")
+ 67    conversations = relationship("Conversation", back_populates="course")
+ 68
+ 69
+ 70class Enrollment(Base):
+ 71    """Join table linking a ``User`` to a ``Course`` they are enrolled in."""
+ 72    __tablename__ = "enrollments"
+ 73
+ 74    id = Column(Integer, primary_key=True, index=True)
+ 75    user_id = Column(String, ForeignKey("users.firebase_uid"), nullable=False)
+ 76    course_id = Column(Integer, ForeignKey("courses.id"), nullable=False)
+ 77    enrolled_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.UTC))
+ 78
+ 79    user = relationship("User", back_populates="enrollments")
+ 80    course = relationship("Course", back_populates="members")
+ 81
+ 82
+ 83class Conversation(Base):
+ 84    """A chat thread, either 1:1 (``is_group`` False) or a named group chat.
+ 85
+ 86    Optionally scoped to a course via ``course_id`` so course-specific
+ 87    chats can be filtered out of personal DMs.
+ 88    """
+ 89    __tablename__ = "conversations"
+ 90    
+ 91    conversation_id = Column(Integer, primary_key=True, index=True)
+ 92    course_id = Column(Integer, ForeignKey("courses.id"), nullable=True)
+ 93    is_group = Column(Boolean, default=False)
+ 94    group_name = Column(String, nullable=True) 
+ 95    created_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.UTC))
+ 96    
+ 97    course = relationship("Course", back_populates="conversations")
+ 98    participants = relationship("ConversationParticipant", back_populates="conversation", cascade="all, delete-orphan")
+ 99    messages = relationship("Message", back_populates="conversation", cascade="all, delete-orphan")
+100
+101
+102class ConversationParticipant(Base):
+103    """Membership of a user in a conversation; controls who can read/send."""
+104    __tablename__ = "conversation_participants"
+105    
+106    participant_id = Column(Integer, primary_key=True, index=True)
+107    conversation_id = Column(Integer, ForeignKey("conversations.conversation_id"), nullable=False)
+108    user_id = Column("user_uid", String, ForeignKey("users.firebase_uid"), nullable=False)
+109    joined_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.UTC))
+110    
+111    # Relationships
+112    conversation = relationship("Conversation", back_populates="participants")
+113    user = relationship("User", back_populates="conversation_participants")
+114
+115
+116class Message(Base):
+117    """A single chat message inside a ``Conversation``."""
+118    __tablename__ = "messages"
+119    
+120    message_id = Column("id", Integer, primary_key=True, index=True)
+121    conversation_id = Column(Integer, ForeignKey("conversations.conversation_id"), nullable=False)
+122    sender_id = Column("sender_uid", String, ForeignKey("users.firebase_uid"), nullable=False)
+123    content = Column(Text, nullable=False)
+124    created_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.UTC))
+125    
+126    # Relationships
+127    conversation = relationship("Conversation", back_populates="messages")
+128    sender = relationship("User", back_populates="messages")
+129
+130
+131class Post(Base):
+132    """A discussion or resource post within a course.
+133
+134    ``score`` is a denormalized vote tally maintained alongside
+135    ``PostVote`` rows. ``is_flagged`` lets moderators hide a post
+136    without deleting it.
+137    """
+138    __tablename__ = "posts"
+139
+140    id = Column(Integer, primary_key=True, index=True)
+141    course_id = Column(Integer, ForeignKey("courses.id"), nullable=False)
+142    author_uid = Column(String, ForeignKey("users.firebase_uid"), nullable=False)
+143    title = Column(String, nullable=False)
+144    description = Column(Text, nullable=True)
+145    resource_link = Column(String, nullable=True)
+146    score = Column(Integer, default=0)
+147    is_flagged = Column(Boolean, default=False)
+148    created_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.UTC))
+149
+150    author = relationship("User", back_populates="posts")
+151    course = relationship("Course", back_populates="posts")
+152    votes = relationship("PostVote", back_populates="post", cascade="all, delete-orphan")
+153
+154
+155class PostVote(Base):
+156    """One user's upvote (+1) or downvote (-1) on a ``Post``."""
+157    __tablename__ = "post_votes"
+158
+159    id = Column(Integer, primary_key=True, index=True)
+160    post_id = Column(Integer, ForeignKey("posts.id"), nullable=False)
+161    user_uid = Column(String, nullable=False)
+162    vote = Column(Integer, nullable=False)
+163
+164    post = relationship("Post", back_populates="votes")
+165
+166
+167class StudyGroup(Base):
+168    """A persistent study group inside a course.
+169
+170    Holds the long-lived membership (``StudyGroupMember``) and any
+171    scheduled meetings (``StudySession``).
+172    """
+173    __tablename__ = "study_groups"
+174
+175    id = Column(Integer, primary_key=True, index=True)
+176    course_id = Column(Integer, ForeignKey("courses.id"), nullable=False)
+177    name = Column(String, nullable=False)
+178    created_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.UTC))
+179
+180    course = relationship("Course", back_populates="study_groups")
+181    members = relationship("StudyGroupMember", back_populates="group", cascade="all, delete-orphan")
+182    sessions = relationship("StudySession", back_populates="group", cascade="all, delete-orphan")
+183
+184
+185class StudySession(Base):
+186    """A scheduled study meeting.
+187
+188    ``session_type`` is ``"solo"`` for personal study blocks or
+189    ``"group"`` when tied to a ``StudyGroup`` via ``group_id``.
+190    Invitees (for group sessions) live in ``StudySessionInvitee``.
+191    """
+192    __tablename__ = "study_sessions"
+193
+194    id = Column(Integer, primary_key=True, index=True)
+195    course_id = Column(Integer, ForeignKey("courses.id"), nullable=False)
+196    creator_email = Column(String, nullable=False)
+197    session_type = Column(String, default="solo")
+198    title = Column(String, nullable=False)
+199    starts_at = Column(DateTime, nullable=False)
+200    ends_at = Column(DateTime, nullable=False)
+201    group_id = Column(Integer, ForeignKey("study_groups.id"), nullable=True)
+202    created_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.UTC))
+203
+204    group = relationship("StudyGroup", back_populates="sessions")
+205    invitees = relationship("StudySessionInvitee", back_populates="session", cascade="all, delete-orphan")
+206
+207
+208class StudyGroupMember(Base):
+209    """A user's membership in a ``StudyGroup``, keyed by email."""
+210    __tablename__ = "study_group_members"
+211
+212    id = Column(Integer, primary_key=True, index=True)
+213    group_id = Column(Integer, ForeignKey("study_groups.id"), nullable=False)
+214    user_email = Column(String, nullable=False)
+215    joined_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.UTC))
+216
+217    group = relationship("StudyGroup", back_populates="members")
+218
+219
+220class UserAvailability(Base):
+221    """A busy/free time block for a user.
+222
+223    Most rows are imported from Google Calendar (``source =
+224    "google_calendar"``); rows tied to a StudySession reflect blocks
+225    StudySync itself created. Used by the scheduler to find overlap
+226    when proposing meeting times.
+227    """
+228    __tablename__ = "user_availability"
+229
+230    id = Column(Integer, primary_key=True, index=True)
+231    user_email = Column(String, nullable=False)
+232    starts_at = Column(DateTime, nullable=False)
+233    ends_at = Column(DateTime, nullable=False)
+234    source = Column(String, default="google_calendar")
+235    study_session_id = Column(Integer, ForeignKey("study_sessions.id"), nullable=True)
+236    created_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.UTC))
+237
+238
+239class StudySessionInvitee(Base):
+240    """An email invited to a ``StudySession``; the invitee may not yet be a registered user."""
+241    __tablename__ = "study_session_invitees"
+242
+243    id = Column(Integer, primary_key=True, index=True)
+244    study_session_id = Column(Integer, ForeignKey("study_sessions.id"), nullable=False)
+245    user_email = Column(String, nullable=False)
+246    created_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.UTC))
+247
+248    session = relationship("StudySession", back_populates="invitees")
+
+
+
+
+ +
+ class + UserRole(builtins.str, enum.Enum): + + +
+ +
+
20class UserRole(str, enum.Enum):
+21    """Allowed values for ``User.role``. Stored as a plain string column."""
+22    STUDENT = "Student"
+23    TA = "TA"
+24    ADMIN = "Admin"
+
+
+ +
+

+ Allowed values for User.role. Stored as a plain string column. +

+
+ +
+
+ STUDENT = + <UserRole.STUDENT: + 'Student'> +
+ +
+
+
+ TA = + <UserRole.TA: + 'TA'> +
+ +
+
+
+ ADMIN = + <UserRole.ADMIN: + 'Admin'> +
+ +
+
+
+ +
+ class + User(sqlalchemy.orm.decl_api._DynamicAttributesType, + sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]): + + +
+ +
+
27class User(Base):
+28    """A StudySync user, keyed by their Firebase Auth UID.
+29
+30    Holds profile basics plus an optional Google Calendar token used by
+31    the calendar-sync feature. All ownership/authorship foreign keys in
+32    other tables point at ``firebase_uid``.
+33    """
+34    __tablename__ = "users"
+35
+36    # Firebase UID is now the primary key (string)
+37    firebase_uid = Column(String, primary_key=True, index=True, nullable=False)
+38    email = Column(String, unique=True, index=True, nullable=False)
+39    full_name = Column(String)
+40    role = Column(String, default="Student")
+41    google_calendar_token = Column(String, nullable=True)
+42    created_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.UTC))
+43    
+44    # Relationships
+45    messages = relationship("Message", back_populates="sender")
+46    conversation_participants = relationship("ConversationParticipant", back_populates="user")
+47    courses_created = relationship("Course", back_populates="owner")
+48    posts = relationship("Post", back_populates="author")
+49    enrollments = relationship("Enrollment", back_populates="user")
+
+
+ +
+

A StudySync user, keyed by their Firebase Auth UID.

+ +

+ Holds profile basics plus an optional Google Calendar token used by + the calendar-sync feature. All ownership/authorship foreign keys in + other tables point at + firebase_uid. +

+
+ +
+
+ User(**kwargs) +
+ + +
+

A simple constructor that allows initialization from kwargs.

+ +

+ Sets attributes on the constructed instance using the names and + values in kwargs. +

+ +

+ Only keys that are present as attributes of the instance's class + are allowed. These could be, for example, any mapped columns or + relationships. +

+
+
+
+
+ firebase_uid +
+ +
+
+
+ email +
+ +
+
+
+ full_name +
+ +
+
+
+ role +
+ +
+
+
+ google_calendar_token +
+ +
+
+
+ created_at +
+ +
+
+
+ messages +
+ +
+
+
+ conversation_participants +
+ +
+
+
+ courses_created +
+ +
+
+
+ posts +
+ +
+
+
+ enrollments +
+ +
+
+
+ +
+ class + Course(sqlalchemy.orm.decl_api._DynamicAttributesType, + sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]): + + +
+ +
+
52class Course(Base):
+53    """A class/course that students can enroll in and post to."""
+54    __tablename__ = "courses"
+55
+56    id = Column(Integer, primary_key=True, index=True)
+57    course_code = Column(String, unique=True, index=True, nullable=False)
+58    name = Column(String, nullable=False)
+59    description = Column(Text, nullable=True)
+60    owner_id = Column(String, ForeignKey("users.firebase_uid"), nullable=False)
+61    created_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.UTC))
+62
+63    # Relationships
+64    owner = relationship("User", back_populates="courses_created")
+65    members = relationship("Enrollment", back_populates="course", cascade="all, delete-orphan")
+66    posts = relationship("Post", back_populates="course")
+67    study_groups = relationship("StudyGroup", back_populates="course")
+68    conversations = relationship("Conversation", back_populates="course")
+
+
+ +
+

A class/course that students can enroll in and post to.

+
+ +
+
+ Course(**kwargs) +
+ + +
+

A simple constructor that allows initialization from kwargs.

+ +

+ Sets attributes on the constructed instance using the names and + values in kwargs. +

+ +

+ Only keys that are present as attributes of the instance's class + are allowed. These could be, for example, any mapped columns or + relationships. +

+
+
+
+
+ id +
+ +
+
+
+ course_code +
+ +
+
+
+ name +
+ +
+
+
+ description +
+ +
+
+
+ owner_id +
+ +
+
+
+ created_at +
+ +
+
+
+ owner +
+ +
+
+
+ members +
+ +
+
+
+ posts +
+ +
+
+
+ study_groups +
+ +
+
+
+ conversations +
+ +
+
+
+ +
+ class + Enrollment(sqlalchemy.orm.decl_api._DynamicAttributesType, + sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]): + + +
+ +
+
71class Enrollment(Base):
+72    """Join table linking a ``User`` to a ``Course`` they are enrolled in."""
+73    __tablename__ = "enrollments"
+74
+75    id = Column(Integer, primary_key=True, index=True)
+76    user_id = Column(String, ForeignKey("users.firebase_uid"), nullable=False)
+77    course_id = Column(Integer, ForeignKey("courses.id"), nullable=False)
+78    enrolled_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.UTC))
+79
+80    user = relationship("User", back_populates="enrollments")
+81    course = relationship("Course", back_populates="members")
+
+
+ +
+

+ Join table linking a User to a + Course they are enrolled in. +

+
+ +
+
+ Enrollment(**kwargs) +
+ + +
+

A simple constructor that allows initialization from kwargs.

+ +

+ Sets attributes on the constructed instance using the names and + values in kwargs. +

+ +

+ Only keys that are present as attributes of the instance's class + are allowed. These could be, for example, any mapped columns or + relationships. +

+
+
+
+
+ id +
+ +
+
+
+ user_id +
+ +
+
+
+ course_id +
+ +
+
+
+ enrolled_at +
+ +
+
+
+ user +
+ +
+
+
+ course +
+ +
+
+
+ +
+ class + Conversation(sqlalchemy.orm.decl_api._DynamicAttributesType, + sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]): + + +
+ +
+
 84class Conversation(Base):
+ 85    """A chat thread, either 1:1 (``is_group`` False) or a named group chat.
+ 86
+ 87    Optionally scoped to a course via ``course_id`` so course-specific
+ 88    chats can be filtered out of personal DMs.
+ 89    """
+ 90    __tablename__ = "conversations"
+ 91    
+ 92    conversation_id = Column(Integer, primary_key=True, index=True)
+ 93    course_id = Column(Integer, ForeignKey("courses.id"), nullable=True)
+ 94    is_group = Column(Boolean, default=False)
+ 95    group_name = Column(String, nullable=True) 
+ 96    created_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.UTC))
+ 97    
+ 98    course = relationship("Course", back_populates="conversations")
+ 99    participants = relationship("ConversationParticipant", back_populates="conversation", cascade="all, delete-orphan")
+100    messages = relationship("Message", back_populates="conversation", cascade="all, delete-orphan")
+
+
+ +
+

+ A chat thread, either 1:1 (is_group + False) or a named group chat. +

+ +

+ Optionally scoped to a course via + course_id so + course-specific chats can be filtered out of personal DMs. +

+
+ +
+
+ Conversation(**kwargs) +
+ + +
+

A simple constructor that allows initialization from kwargs.

+ +

+ Sets attributes on the constructed instance using the names and + values in kwargs. +

+ +

+ Only keys that are present as attributes of the instance's class + are allowed. These could be, for example, any mapped columns or + relationships. +

+
+
+
+
+ conversation_id +
+ +
+
+
+ course_id +
+ +
+
+
+ is_group +
+ +
+
+
+ group_name +
+ +
+
+
+ created_at +
+ +
+
+
+ course +
+ +
+
+
+ participants +
+ +
+
+
+ messages +
+ +
+
+
+ +
+ class + ConversationParticipant(sqlalchemy.orm.decl_api._DynamicAttributesType, + sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]): + + +
+ +
+
103class ConversationParticipant(Base):
+104    """Membership of a user in a conversation; controls who can read/send."""
+105    __tablename__ = "conversation_participants"
+106    
+107    participant_id = Column(Integer, primary_key=True, index=True)
+108    conversation_id = Column(Integer, ForeignKey("conversations.conversation_id"), nullable=False)
+109    user_id = Column("user_uid", String, ForeignKey("users.firebase_uid"), nullable=False)
+110    joined_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.UTC))
+111    
+112    # Relationships
+113    conversation = relationship("Conversation", back_populates="participants")
+114    user = relationship("User", back_populates="conversation_participants")
+
+
+ +
+

+ Membership of a user in a conversation; controls who can read/send. +

+
+ +
+
+ ConversationParticipant(**kwargs) +
+ + +
+

A simple constructor that allows initialization from kwargs.

+ +

+ Sets attributes on the constructed instance using the names and + values in kwargs. +

+ +

+ Only keys that are present as attributes of the instance's class + are allowed. These could be, for example, any mapped columns or + relationships. +

+
+
+
+
+ participant_id +
+ +
+
+
+ conversation_id +
+ +
+
+
+ user_id +
+ +
+
+
+ joined_at +
+ +
+
+
+ conversation +
+ +
+
+
+ user +
+ +
+
+
+ +
+ class + Message(sqlalchemy.orm.decl_api._DynamicAttributesType, + sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]): + + +
+ +
+
117class Message(Base):
+118    """A single chat message inside a ``Conversation``."""
+119    __tablename__ = "messages"
+120    
+121    message_id = Column("id", Integer, primary_key=True, index=True)
+122    conversation_id = Column(Integer, ForeignKey("conversations.conversation_id"), nullable=False)
+123    sender_id = Column("sender_uid", String, ForeignKey("users.firebase_uid"), nullable=False)
+124    content = Column(Text, nullable=False)
+125    created_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.UTC))
+126    
+127    # Relationships
+128    conversation = relationship("Conversation", back_populates="messages")
+129    sender = relationship("User", back_populates="messages")
+
+
+ +
+

+ A single chat message inside a + Conversation. +

+
+ +
+
+ Message(**kwargs) +
+ + +
+

A simple constructor that allows initialization from kwargs.

+ +

+ Sets attributes on the constructed instance using the names and + values in kwargs. +

+ +

+ Only keys that are present as attributes of the instance's class + are allowed. These could be, for example, any mapped columns or + relationships. +

+
+
+
+
+ message_id +
+ +
+
+
+ conversation_id +
+ +
+
+
+ sender_id +
+ +
+
+
+ content +
+ +
+
+
+ created_at +
+ +
+
+
+ conversation +
+ +
+
+
+ sender +
+ +
+
+
+ +
+ class + Post(sqlalchemy.orm.decl_api._DynamicAttributesType, + sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]): + + +
+ +
+
132class Post(Base):
+133    """A discussion or resource post within a course.
+134
+135    ``score`` is a denormalized vote tally maintained alongside
+136    ``PostVote`` rows. ``is_flagged`` lets moderators hide a post
+137    without deleting it.
+138    """
+139    __tablename__ = "posts"
+140
+141    id = Column(Integer, primary_key=True, index=True)
+142    course_id = Column(Integer, ForeignKey("courses.id"), nullable=False)
+143    author_uid = Column(String, ForeignKey("users.firebase_uid"), nullable=False)
+144    title = Column(String, nullable=False)
+145    description = Column(Text, nullable=True)
+146    resource_link = Column(String, nullable=True)
+147    score = Column(Integer, default=0)
+148    is_flagged = Column(Boolean, default=False)
+149    created_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.UTC))
+150
+151    author = relationship("User", back_populates="posts")
+152    course = relationship("Course", back_populates="posts")
+153    votes = relationship("PostVote", back_populates="post", cascade="all, delete-orphan")
+
+
+ +
+

A discussion or resource post within a course.

+ +

+ score is a denormalized vote + tally maintained alongside + PostVote rows. + is_flagged lets + moderators hide a post without deleting it. +

+
+ +
+
+ Post(**kwargs) +
+ + +
+

A simple constructor that allows initialization from kwargs.

+ +

+ Sets attributes on the constructed instance using the names and + values in kwargs. +

+ +

+ Only keys that are present as attributes of the instance's class + are allowed. These could be, for example, any mapped columns or + relationships. +

+
+
+
+
+ id +
+ +
+
+
+ course_id +
+ +
+
+
+ author_uid +
+ +
+
+
+ title +
+ +
+
+
+ description +
+ +
+ +
+
+ score +
+ +
+
+
+ is_flagged +
+ +
+
+
+ created_at +
+ +
+
+
+ author +
+ +
+
+
+ course +
+ +
+
+
+ votes +
+ +
+
+
+ +
+ class + PostVote(sqlalchemy.orm.decl_api._DynamicAttributesType, + sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]): + + +
+ +
+
156class PostVote(Base):
+157    """One user's upvote (+1) or downvote (-1) on a ``Post``."""
+158    __tablename__ = "post_votes"
+159
+160    id = Column(Integer, primary_key=True, index=True)
+161    post_id = Column(Integer, ForeignKey("posts.id"), nullable=False)
+162    user_uid = Column(String, nullable=False)
+163    vote = Column(Integer, nullable=False)
+164
+165    post = relationship("Post", back_populates="votes")
+
+
+ +
+

+ One user's upvote (+1) or downvote (-1) on a + Post. +

+
+ +
+
+ PostVote(**kwargs) +
+ + +
+

A simple constructor that allows initialization from kwargs.

+ +

+ Sets attributes on the constructed instance using the names and + values in kwargs. +

+ +

+ Only keys that are present as attributes of the instance's class + are allowed. These could be, for example, any mapped columns or + relationships. +

+
+
+
+
+ id +
+ +
+
+
+ post_id +
+ +
+
+
+ user_uid +
+ +
+
+
+ vote +
+ +
+
+
+ post +
+ +
+
+
+ +
+ class + StudyGroup(sqlalchemy.orm.decl_api._DynamicAttributesType, + sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]): + + +
+ +
+
168class StudyGroup(Base):
+169    """A persistent study group inside a course.
+170
+171    Holds the long-lived membership (``StudyGroupMember``) and any
+172    scheduled meetings (``StudySession``).
+173    """
+174    __tablename__ = "study_groups"
+175
+176    id = Column(Integer, primary_key=True, index=True)
+177    course_id = Column(Integer, ForeignKey("courses.id"), nullable=False)
+178    name = Column(String, nullable=False)
+179    created_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.UTC))
+180
+181    course = relationship("Course", back_populates="study_groups")
+182    members = relationship("StudyGroupMember", back_populates="group", cascade="all, delete-orphan")
+183    sessions = relationship("StudySession", back_populates="group", cascade="all, delete-orphan")
+
+
+ +
+

A persistent study group inside a course.

+ +

+ Holds the long-lived membership (StudyGroupMember) and any scheduled meetings (StudySession). +

+
+ +
+
+ StudyGroup(**kwargs) +
+ + +
+

A simple constructor that allows initialization from kwargs.

+ +

+ Sets attributes on the constructed instance using the names and + values in kwargs. +

+ +

+ Only keys that are present as attributes of the instance's class + are allowed. These could be, for example, any mapped columns or + relationships. +

+
+
+
+
+ id +
+ +
+
+
+ course_id +
+ +
+
+
+ name +
+ +
+
+
+ created_at +
+ +
+
+
+ course +
+ +
+
+
+ members +
+ +
+
+
+ sessions +
+ +
+
+
+ +
+ class + StudySession(sqlalchemy.orm.decl_api._DynamicAttributesType, + sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]): + + +
+ +
+
186class StudySession(Base):
+187    """A scheduled study meeting.
+188
+189    ``session_type`` is ``"solo"`` for personal study blocks or
+190    ``"group"`` when tied to a ``StudyGroup`` via ``group_id``.
+191    Invitees (for group sessions) live in ``StudySessionInvitee``.
+192    """
+193    __tablename__ = "study_sessions"
+194
+195    id = Column(Integer, primary_key=True, index=True)
+196    course_id = Column(Integer, ForeignKey("courses.id"), nullable=False)
+197    creator_email = Column(String, nullable=False)
+198    session_type = Column(String, default="solo")
+199    title = Column(String, nullable=False)
+200    starts_at = Column(DateTime, nullable=False)
+201    ends_at = Column(DateTime, nullable=False)
+202    group_id = Column(Integer, ForeignKey("study_groups.id"), nullable=True)
+203    created_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.UTC))
+204
+205    group = relationship("StudyGroup", back_populates="sessions")
+206    invitees = relationship("StudySessionInvitee", back_populates="session", cascade="all, delete-orphan")
+
+
+ +
+

A scheduled study meeting.

+ +

+ session_type + is "solo" for personal study blocks or + "group" when tied to a + StudyGroup via + group_id. Invitees (for group sessions) live in + StudySessionInvitee. +

+
+ +
+
+ StudySession(**kwargs) +
+ + +
+

A simple constructor that allows initialization from kwargs.

+ +

+ Sets attributes on the constructed instance using the names and + values in kwargs. +

+ +

+ Only keys that are present as attributes of the instance's class + are allowed. These could be, for example, any mapped columns or + relationships. +

+
+
+
+
+ id +
+ +
+
+
+ course_id +
+ +
+
+
+ creator_email +
+ +
+
+
+ session_type +
+ +
+
+
+ title +
+ +
+
+
+ starts_at +
+ +
+
+
+ ends_at +
+ +
+
+
+ group_id +
+ +
+
+
+ created_at +
+ +
+
+
+ group +
+ +
+
+
+ invitees +
+ +
+
+
+ +
+ class + StudyGroupMember(sqlalchemy.orm.decl_api._DynamicAttributesType, + sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]): + + +
+ +
+
209class StudyGroupMember(Base):
+210    """A user's membership in a ``StudyGroup``, keyed by email."""
+211    __tablename__ = "study_group_members"
+212
+213    id = Column(Integer, primary_key=True, index=True)
+214    group_id = Column(Integer, ForeignKey("study_groups.id"), nullable=False)
+215    user_email = Column(String, nullable=False)
+216    joined_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.UTC))
+217
+218    group = relationship("StudyGroup", back_populates="members")
+
+
+ +
+

+ A user's membership in a + StudyGroup, keyed by email. +

+
+ +
+
+ StudyGroupMember(**kwargs) +
+ + +
+

A simple constructor that allows initialization from kwargs.

+ +

+ Sets attributes on the constructed instance using the names and + values in kwargs. +

+ +

+ Only keys that are present as attributes of the instance's class + are allowed. These could be, for example, any mapped columns or + relationships. +

+
+
+
+
+ id +
+ +
+
+
+ group_id +
+ +
+
+
+ user_email +
+ +
+
+
+ joined_at +
+ +
+
+
+ group +
+ +
+
+
+ +
+ class + UserAvailability(sqlalchemy.orm.decl_api._DynamicAttributesType, + sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]): + + +
+ +
+
221class UserAvailability(Base):
+222    """A busy/free time block for a user.
+223
+224    Most rows are imported from Google Calendar (``source =
+225    "google_calendar"``); rows tied to a StudySession reflect blocks
+226    StudySync itself created. Used by the scheduler to find overlap
+227    when proposing meeting times.
+228    """
+229    __tablename__ = "user_availability"
+230
+231    id = Column(Integer, primary_key=True, index=True)
+232    user_email = Column(String, nullable=False)
+233    starts_at = Column(DateTime, nullable=False)
+234    ends_at = Column(DateTime, nullable=False)
+235    source = Column(String, default="google_calendar")
+236    study_session_id = Column(Integer, ForeignKey("study_sessions.id"), nullable=True)
+237    created_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.UTC))
+
+
+ +
+

A busy/free time block for a user.

+ +

+ Most rows are imported from Google Calendar (source = "google_calendar"); rows tied to a StudySession reflect blocks StudySync itself + created. Used by the scheduler to find overlap when proposing + meeting times. +

+
+ +
+
+ UserAvailability(**kwargs) +
+ + +
+

A simple constructor that allows initialization from kwargs.

+ +

+ Sets attributes on the constructed instance using the names and + values in kwargs. +

+ +

+ Only keys that are present as attributes of the instance's class + are allowed. These could be, for example, any mapped columns or + relationships. +

+
+
+
+
+ id +
+ +
+
+
+ user_email +
+ +
+
+
+ starts_at +
+ +
+
+
+ ends_at +
+ +
+
+
+ source +
+ +
+
+
+ study_session_id +
+ +
+
+
+ created_at +
+ +
+
+
+ +
+ class + StudySessionInvitee(sqlalchemy.orm.decl_api._DynamicAttributesType, + sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]): + + +
+ +
+
240class StudySessionInvitee(Base):
+241    """An email invited to a ``StudySession``; the invitee may not yet be a registered user."""
+242    __tablename__ = "study_session_invitees"
+243
+244    id = Column(Integer, primary_key=True, index=True)
+245    study_session_id = Column(Integer, ForeignKey("study_sessions.id"), nullable=False)
+246    user_email = Column(String, nullable=False)
+247    created_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.UTC))
+248
+249    session = relationship("StudySession", back_populates="invitees")
+
+
+ +
+

+ An email invited to a + StudySession; the invitee may not yet be a registered user. +

+
+ +
+
+ StudySessionInvitee(**kwargs) +
+ + +
+

A simple constructor that allows initialization from kwargs.

+ +

+ Sets attributes on the constructed instance using the names and + values in kwargs. +

+ +

+ Only keys that are present as attributes of the instance's class + are allowed. These could be, for example, any mapped columns or + relationships. +

+
+
+
+
+ id +
+ +
+
+
+ study_session_id +
+ +
+
+
+ user_email +
+ +
+
+
+ created_at +
+ +
+
+
+ session +
+ +
+
+
+ + + diff --git a/docs/backend/search.js b/docs/backend/search.js new file mode 100644 index 0000000..cf79c77 --- /dev/null +++ b/docs/backend/search.js @@ -0,0 +1,46 @@ +window.pdocSearch = (function(){ +/** elasticlunr - http://weixsong.github.io * Copyright (C) 2017 Oliver Nightingale * Copyright (C) 2017 Wei Song * MIT Licensed */!function(){function e(e){if(null===e||"object"!=typeof e)return e;var t=e.constructor();for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}var t=function(e){var n=new t.Index;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),e&&e.call(n,n),n};t.version="0.9.5",lunr=t,t.utils={},t.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),t.utils.toString=function(e){return void 0===e||null===e?"":e.toString()},t.EventEmitter=function(){this.events={}},t.EventEmitter.prototype.addListener=function(){var e=Array.prototype.slice.call(arguments),t=e.pop(),n=e;if("function"!=typeof t)throw new TypeError("last argument must be a function");n.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},t.EventEmitter.prototype.removeListener=function(e,t){if(this.hasHandler(e)){var n=this.events[e].indexOf(t);-1!==n&&(this.events[e].splice(n,1),0==this.events[e].length&&delete this.events[e])}},t.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){var t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},t.EventEmitter.prototype.hasHandler=function(e){return e in this.events},t.tokenizer=function(e){if(!arguments.length||null===e||void 0===e)return[];if(Array.isArray(e)){var n=e.filter(function(e){return null===e||void 0===e?!1:!0});n=n.map(function(e){return t.utils.toString(e).toLowerCase()});var i=[];return n.forEach(function(e){var n=e.split(t.tokenizer.seperator);i=i.concat(n)},this),i}return e.toString().trim().toLowerCase().split(t.tokenizer.seperator)},t.tokenizer.defaultSeperator=/[\s\-]+/,t.tokenizer.seperator=t.tokenizer.defaultSeperator,t.tokenizer.setSeperator=function(e){null!==e&&void 0!==e&&"object"==typeof e&&(t.tokenizer.seperator=e)},t.tokenizer.resetSeperator=function(){t.tokenizer.seperator=t.tokenizer.defaultSeperator},t.tokenizer.getSeperator=function(){return t.tokenizer.seperator},t.Pipeline=function(){this._queue=[]},t.Pipeline.registeredFunctions={},t.Pipeline.registerFunction=function(e,n){n in t.Pipeline.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[n]=e},t.Pipeline.getRegisteredFunction=function(e){return e in t.Pipeline.registeredFunctions!=!0?null:t.Pipeline.registeredFunctions[e]},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(e){var i=t.Pipeline.getRegisteredFunction(e);if(!i)throw new Error("Cannot load un-registered function: "+e);n.add(i)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){t.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i+1,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i,0,n)},t.Pipeline.prototype.remove=function(e){var t=this._queue.indexOf(e);-1!==t&&this._queue.splice(t,1)},t.Pipeline.prototype.run=function(e){for(var t=[],n=e.length,i=this._queue.length,o=0;n>o;o++){for(var r=e[o],s=0;i>s&&(r=this._queue[s](r,o,e),void 0!==r&&null!==r);s++);void 0!==r&&null!==r&&t.push(r)}return t},t.Pipeline.prototype.reset=function(){this._queue=[]},t.Pipeline.prototype.get=function(){return this._queue},t.Pipeline.prototype.toJSON=function(){return this._queue.map(function(e){return t.Pipeline.warnIfFunctionNotRegistered(e),e.label})},t.Index=function(){this._fields=[],this._ref="id",this.pipeline=new t.Pipeline,this.documentStore=new t.DocumentStore,this.index={},this.eventEmitter=new t.EventEmitter,this._idfCache={},this.on("add","remove","update",function(){this._idfCache={}}.bind(this))},t.Index.prototype.on=function(){var e=Array.prototype.slice.call(arguments);return this.eventEmitter.addListener.apply(this.eventEmitter,e)},t.Index.prototype.off=function(e,t){return this.eventEmitter.removeListener(e,t)},t.Index.load=function(e){e.version!==t.version&&t.utils.warn("version mismatch: current "+t.version+" importing "+e.version);var n=new this;n._fields=e.fields,n._ref=e.ref,n.documentStore=t.DocumentStore.load(e.documentStore),n.pipeline=t.Pipeline.load(e.pipeline),n.index={};for(var i in e.index)n.index[i]=t.InvertedIndex.load(e.index[i]);return n},t.Index.prototype.addField=function(e){return this._fields.push(e),this.index[e]=new t.InvertedIndex,this},t.Index.prototype.setRef=function(e){return this._ref=e,this},t.Index.prototype.saveDocument=function(e){return this.documentStore=new t.DocumentStore(e),this},t.Index.prototype.addDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.addDoc(i,e),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));this.documentStore.addFieldLength(i,n,o.length);var r={};o.forEach(function(e){e in r?r[e]+=1:r[e]=1},this);for(var s in r){var u=r[s];u=Math.sqrt(u),this.index[n].addToken(s,{ref:i,tf:u})}},this),n&&this.eventEmitter.emit("add",e,this)}},t.Index.prototype.removeDocByRef=function(e){if(e&&this.documentStore.isDocStored()!==!1&&this.documentStore.hasDoc(e)){var t=this.documentStore.getDoc(e);this.removeDoc(t,!1)}},t.Index.prototype.removeDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.hasDoc(i)&&(this.documentStore.removeDoc(i),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));o.forEach(function(e){this.index[n].removeToken(e,i)},this)},this),n&&this.eventEmitter.emit("remove",e,this))}},t.Index.prototype.updateDoc=function(e,t){var t=void 0===t?!0:t;this.removeDocByRef(e[this._ref],!1),this.addDoc(e,!1),t&&this.eventEmitter.emit("update",e,this)},t.Index.prototype.idf=function(e,t){var n="@"+t+"/"+e;if(Object.prototype.hasOwnProperty.call(this._idfCache,n))return this._idfCache[n];var i=this.index[t].getDocFreq(e),o=1+Math.log(this.documentStore.length/(i+1));return this._idfCache[n]=o,o},t.Index.prototype.getFields=function(){return this._fields.slice()},t.Index.prototype.search=function(e,n){if(!e)return[];e="string"==typeof e?{any:e}:JSON.parse(JSON.stringify(e));var i=null;null!=n&&(i=JSON.stringify(n));for(var o=new t.Configuration(i,this.getFields()).get(),r={},s=Object.keys(e),u=0;u0&&t.push(e);for(var i in n)"docs"!==i&&"df"!==i&&this.expandToken(e+i,t,n[i]);return t},t.InvertedIndex.prototype.toJSON=function(){return{root:this.root}},t.Configuration=function(e,n){var e=e||"";if(void 0==n||null==n)throw new Error("fields should not be null");this.config={};var i;try{i=JSON.parse(e),this.buildUserConfig(i,n)}catch(o){t.utils.warn("user configuration parse failed, will use default configuration"),this.buildDefaultConfig(n)}},t.Configuration.prototype.buildDefaultConfig=function(e){this.reset(),e.forEach(function(e){this.config[e]={boost:1,bool:"OR",expand:!1}},this)},t.Configuration.prototype.buildUserConfig=function(e,n){var i="OR",o=!1;if(this.reset(),"bool"in e&&(i=e.bool||i),"expand"in e&&(o=e.expand||o),"fields"in e)for(var r in e.fields)if(n.indexOf(r)>-1){var s=e.fields[r],u=o;void 0!=s.expand&&(u=s.expand),this.config[r]={boost:s.boost||0===s.boost?s.boost:1,bool:s.bool||i,expand:u}}else t.utils.warn("field name in user configuration not found in index instance fields");else this.addAllFields2UserConfig(i,o,n)},t.Configuration.prototype.addAllFields2UserConfig=function(e,t,n){n.forEach(function(n){this.config[n]={boost:1,bool:e,expand:t}},this)},t.Configuration.prototype.get=function(){return this.config},t.Configuration.prototype.reset=function(){this.config={}},lunr.SortedSet=function(){this.length=0,this.elements=[]},lunr.SortedSet.load=function(e){var t=new this;return t.elements=e,t.length=e.length,t},lunr.SortedSet.prototype.add=function(){var e,t;for(e=0;e1;){if(r===e)return o;e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o]}return r===e?o:-1},lunr.SortedSet.prototype.locationFor=function(e){for(var t=0,n=this.elements.length,i=n-t,o=t+Math.floor(i/2),r=this.elements[o];i>1;)e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o];return r>e?o:e>r?o+1:void 0},lunr.SortedSet.prototype.intersect=function(e){for(var t=new lunr.SortedSet,n=0,i=0,o=this.length,r=e.length,s=this.elements,u=e.elements;;){if(n>o-1||i>r-1)break;s[n]!==u[i]?s[n]u[i]&&i++:(t.add(s[n]),n++,i++)}return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){var t,n,i;this.length>=e.length?(t=this,n=e):(t=e,n=this),i=t.clone();for(var o=0,r=n.toArray();o

\n"}, "database.Base": {"fullname": "database.Base", "modulename": "database", "qualname": "Base", "kind": "class", "doc": "

The base class of the class hierarchy.

\n\n

When called, it accepts no arguments and returns a new featureless\ninstance that has no instance attributes and cannot be given any.

\n", "bases": "sqlalchemy.orm.decl_api._DynamicAttributesType, sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]"}, "database.Base.__init__": {"fullname": "database.Base.__init__", "modulename": "database", "qualname": "Base.__init__", "kind": "function", "doc": "

A simple constructor that allows initialization from kwargs.

\n\n

Sets attributes on the constructed instance using the names and\nvalues in kwargs.

\n\n

Only keys that are present as\nattributes of the instance's class are allowed. These could be,\nfor example, any mapped columns or relationships.

\n", "signature": "(**kwargs: Any)"}, "database.Base.registry": {"fullname": "database.Base.registry", "modulename": "database", "qualname": "Base.registry", "kind": "variable", "doc": "

\n", "annotation": ": sqlalchemy.orm.decl_api.registry", "default_value": "<sqlalchemy.orm.decl_api.registry object>"}, "database.Base.metadata": {"fullname": "database.Base.metadata", "modulename": "database", "qualname": "Base.metadata", "kind": "variable", "doc": "

\n", "annotation": ": sqlalchemy.sql.schema.MetaData", "default_value": "MetaData()"}, "database.get_db": {"fullname": "database.get_db", "modulename": "database", "qualname": "get_db", "kind": "function", "doc": "

Used as a FastAPI Depends(get_db) so each request gets its own\nsession and the connection is always returned to the pool.

\n", "signature": "():", "funcdef": "def"}, "models": {"fullname": "models", "modulename": "models", "kind": "module", "doc": "

High-level entity map:

\n\n
    \n
  • User \u2014 a person (student, TA, or admin).
  • \n
  • Course + Enrollment \u2014 a class and its roster.
  • \n
  • Post + PostVote \u2014 discussion/resource posts and their votes.
  • \n
  • StudyGroup + StudyGroupMember \u2014 long-lived groups inside a course.
  • \n
  • StudySession + StudySessionInvitee \u2014 scheduled meetings (solo or group).
  • \n
  • UserAvailability \u2014 busy/free blocks, typically synced from Google Calendar.
  • \n
  • Conversation + ConversationParticipant + Message \u2014 chat.
  • \n
\n"}, "models.UserRole": {"fullname": "models.UserRole", "modulename": "models", "qualname": "UserRole", "kind": "class", "doc": "

Allowed values for User.role. Stored as a plain string column.

\n", "bases": "builtins.str, enum.Enum"}, "models.UserRole.STUDENT": {"fullname": "models.UserRole.STUDENT", "modulename": "models", "qualname": "UserRole.STUDENT", "kind": "variable", "doc": "

\n", "default_value": "<UserRole.STUDENT: 'Student'>"}, "models.UserRole.TA": {"fullname": "models.UserRole.TA", "modulename": "models", "qualname": "UserRole.TA", "kind": "variable", "doc": "

\n", "default_value": "<UserRole.TA: 'TA'>"}, "models.UserRole.ADMIN": {"fullname": "models.UserRole.ADMIN", "modulename": "models", "qualname": "UserRole.ADMIN", "kind": "variable", "doc": "

\n", "default_value": "<UserRole.ADMIN: 'Admin'>"}, "models.User": {"fullname": "models.User", "modulename": "models", "qualname": "User", "kind": "class", "doc": "

A StudySync user, keyed by their Firebase Auth UID.

\n\n

Holds profile basics plus an optional Google Calendar token used by\nthe calendar-sync feature. All ownership/authorship foreign keys in\nother tables point at firebase_uid.

\n", "bases": "sqlalchemy.orm.decl_api._DynamicAttributesType, sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]"}, "models.User.__init__": {"fullname": "models.User.__init__", "modulename": "models", "qualname": "User.__init__", "kind": "function", "doc": "

A simple constructor that allows initialization from kwargs.

\n\n

Sets attributes on the constructed instance using the names and\nvalues in kwargs.

\n\n

Only keys that are present as\nattributes of the instance's class are allowed. These could be,\nfor example, any mapped columns or relationships.

\n", "signature": "(**kwargs)"}, "models.User.firebase_uid": {"fullname": "models.User.firebase_uid", "modulename": "models", "qualname": "User.firebase_uid", "kind": "variable", "doc": "

\n"}, "models.User.email": {"fullname": "models.User.email", "modulename": "models", "qualname": "User.email", "kind": "variable", "doc": "

\n"}, "models.User.full_name": {"fullname": "models.User.full_name", "modulename": "models", "qualname": "User.full_name", "kind": "variable", "doc": "

\n"}, "models.User.role": {"fullname": "models.User.role", "modulename": "models", "qualname": "User.role", "kind": "variable", "doc": "

\n"}, "models.User.google_calendar_token": {"fullname": "models.User.google_calendar_token", "modulename": "models", "qualname": "User.google_calendar_token", "kind": "variable", "doc": "

\n"}, "models.User.created_at": {"fullname": "models.User.created_at", "modulename": "models", "qualname": "User.created_at", "kind": "variable", "doc": "

\n"}, "models.User.messages": {"fullname": "models.User.messages", "modulename": "models", "qualname": "User.messages", "kind": "variable", "doc": "

\n"}, "models.User.conversation_participants": {"fullname": "models.User.conversation_participants", "modulename": "models", "qualname": "User.conversation_participants", "kind": "variable", "doc": "

\n"}, "models.User.courses_created": {"fullname": "models.User.courses_created", "modulename": "models", "qualname": "User.courses_created", "kind": "variable", "doc": "

\n"}, "models.User.posts": {"fullname": "models.User.posts", "modulename": "models", "qualname": "User.posts", "kind": "variable", "doc": "

\n"}, "models.User.enrollments": {"fullname": "models.User.enrollments", "modulename": "models", "qualname": "User.enrollments", "kind": "variable", "doc": "

\n"}, "models.Course": {"fullname": "models.Course", "modulename": "models", "qualname": "Course", "kind": "class", "doc": "

A class/course that students can enroll in and post to.

\n", "bases": "sqlalchemy.orm.decl_api._DynamicAttributesType, sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]"}, "models.Course.__init__": {"fullname": "models.Course.__init__", "modulename": "models", "qualname": "Course.__init__", "kind": "function", "doc": "

A simple constructor that allows initialization from kwargs.

\n\n

Sets attributes on the constructed instance using the names and\nvalues in kwargs.

\n\n

Only keys that are present as\nattributes of the instance's class are allowed. These could be,\nfor example, any mapped columns or relationships.

\n", "signature": "(**kwargs)"}, "models.Course.id": {"fullname": "models.Course.id", "modulename": "models", "qualname": "Course.id", "kind": "variable", "doc": "

\n"}, "models.Course.course_code": {"fullname": "models.Course.course_code", "modulename": "models", "qualname": "Course.course_code", "kind": "variable", "doc": "

\n"}, "models.Course.name": {"fullname": "models.Course.name", "modulename": "models", "qualname": "Course.name", "kind": "variable", "doc": "

\n"}, "models.Course.description": {"fullname": "models.Course.description", "modulename": "models", "qualname": "Course.description", "kind": "variable", "doc": "

\n"}, "models.Course.owner_id": {"fullname": "models.Course.owner_id", "modulename": "models", "qualname": "Course.owner_id", "kind": "variable", "doc": "

\n"}, "models.Course.created_at": {"fullname": "models.Course.created_at", "modulename": "models", "qualname": "Course.created_at", "kind": "variable", "doc": "

\n"}, "models.Course.owner": {"fullname": "models.Course.owner", "modulename": "models", "qualname": "Course.owner", "kind": "variable", "doc": "

\n"}, "models.Course.members": {"fullname": "models.Course.members", "modulename": "models", "qualname": "Course.members", "kind": "variable", "doc": "

\n"}, "models.Course.posts": {"fullname": "models.Course.posts", "modulename": "models", "qualname": "Course.posts", "kind": "variable", "doc": "

\n"}, "models.Course.study_groups": {"fullname": "models.Course.study_groups", "modulename": "models", "qualname": "Course.study_groups", "kind": "variable", "doc": "

\n"}, "models.Course.conversations": {"fullname": "models.Course.conversations", "modulename": "models", "qualname": "Course.conversations", "kind": "variable", "doc": "

\n"}, "models.Enrollment": {"fullname": "models.Enrollment", "modulename": "models", "qualname": "Enrollment", "kind": "class", "doc": "

Join table linking a User to a Course they are enrolled in.

\n", "bases": "sqlalchemy.orm.decl_api._DynamicAttributesType, sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]"}, "models.Enrollment.__init__": {"fullname": "models.Enrollment.__init__", "modulename": "models", "qualname": "Enrollment.__init__", "kind": "function", "doc": "

A simple constructor that allows initialization from kwargs.

\n\n

Sets attributes on the constructed instance using the names and\nvalues in kwargs.

\n\n

Only keys that are present as\nattributes of the instance's class are allowed. These could be,\nfor example, any mapped columns or relationships.

\n", "signature": "(**kwargs)"}, "models.Enrollment.id": {"fullname": "models.Enrollment.id", "modulename": "models", "qualname": "Enrollment.id", "kind": "variable", "doc": "

\n"}, "models.Enrollment.user_id": {"fullname": "models.Enrollment.user_id", "modulename": "models", "qualname": "Enrollment.user_id", "kind": "variable", "doc": "

\n"}, "models.Enrollment.course_id": {"fullname": "models.Enrollment.course_id", "modulename": "models", "qualname": "Enrollment.course_id", "kind": "variable", "doc": "

\n"}, "models.Enrollment.enrolled_at": {"fullname": "models.Enrollment.enrolled_at", "modulename": "models", "qualname": "Enrollment.enrolled_at", "kind": "variable", "doc": "

\n"}, "models.Enrollment.user": {"fullname": "models.Enrollment.user", "modulename": "models", "qualname": "Enrollment.user", "kind": "variable", "doc": "

\n"}, "models.Enrollment.course": {"fullname": "models.Enrollment.course", "modulename": "models", "qualname": "Enrollment.course", "kind": "variable", "doc": "

\n"}, "models.Conversation": {"fullname": "models.Conversation", "modulename": "models", "qualname": "Conversation", "kind": "class", "doc": "

A chat thread, either 1:1 (is_group False) or a named group chat.

\n\n

Optionally scoped to a course via course_id so course-specific\nchats can be filtered out of personal DMs.

\n", "bases": "sqlalchemy.orm.decl_api._DynamicAttributesType, sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]"}, "models.Conversation.__init__": {"fullname": "models.Conversation.__init__", "modulename": "models", "qualname": "Conversation.__init__", "kind": "function", "doc": "

A simple constructor that allows initialization from kwargs.

\n\n

Sets attributes on the constructed instance using the names and\nvalues in kwargs.

\n\n

Only keys that are present as\nattributes of the instance's class are allowed. These could be,\nfor example, any mapped columns or relationships.

\n", "signature": "(**kwargs)"}, "models.Conversation.conversation_id": {"fullname": "models.Conversation.conversation_id", "modulename": "models", "qualname": "Conversation.conversation_id", "kind": "variable", "doc": "

\n"}, "models.Conversation.course_id": {"fullname": "models.Conversation.course_id", "modulename": "models", "qualname": "Conversation.course_id", "kind": "variable", "doc": "

\n"}, "models.Conversation.is_group": {"fullname": "models.Conversation.is_group", "modulename": "models", "qualname": "Conversation.is_group", "kind": "variable", "doc": "

\n"}, "models.Conversation.group_name": {"fullname": "models.Conversation.group_name", "modulename": "models", "qualname": "Conversation.group_name", "kind": "variable", "doc": "

\n"}, "models.Conversation.created_at": {"fullname": "models.Conversation.created_at", "modulename": "models", "qualname": "Conversation.created_at", "kind": "variable", "doc": "

\n"}, "models.Conversation.course": {"fullname": "models.Conversation.course", "modulename": "models", "qualname": "Conversation.course", "kind": "variable", "doc": "

\n"}, "models.Conversation.participants": {"fullname": "models.Conversation.participants", "modulename": "models", "qualname": "Conversation.participants", "kind": "variable", "doc": "

\n"}, "models.Conversation.messages": {"fullname": "models.Conversation.messages", "modulename": "models", "qualname": "Conversation.messages", "kind": "variable", "doc": "

\n"}, "models.ConversationParticipant": {"fullname": "models.ConversationParticipant", "modulename": "models", "qualname": "ConversationParticipant", "kind": "class", "doc": "

Membership of a user in a conversation; controls who can read/send.

\n", "bases": "sqlalchemy.orm.decl_api._DynamicAttributesType, sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]"}, "models.ConversationParticipant.__init__": {"fullname": "models.ConversationParticipant.__init__", "modulename": "models", "qualname": "ConversationParticipant.__init__", "kind": "function", "doc": "

A simple constructor that allows initialization from kwargs.

\n\n

Sets attributes on the constructed instance using the names and\nvalues in kwargs.

\n\n

Only keys that are present as\nattributes of the instance's class are allowed. These could be,\nfor example, any mapped columns or relationships.

\n", "signature": "(**kwargs)"}, "models.ConversationParticipant.participant_id": {"fullname": "models.ConversationParticipant.participant_id", "modulename": "models", "qualname": "ConversationParticipant.participant_id", "kind": "variable", "doc": "

\n"}, "models.ConversationParticipant.conversation_id": {"fullname": "models.ConversationParticipant.conversation_id", "modulename": "models", "qualname": "ConversationParticipant.conversation_id", "kind": "variable", "doc": "

\n"}, "models.ConversationParticipant.user_id": {"fullname": "models.ConversationParticipant.user_id", "modulename": "models", "qualname": "ConversationParticipant.user_id", "kind": "variable", "doc": "

\n"}, "models.ConversationParticipant.joined_at": {"fullname": "models.ConversationParticipant.joined_at", "modulename": "models", "qualname": "ConversationParticipant.joined_at", "kind": "variable", "doc": "

\n"}, "models.ConversationParticipant.conversation": {"fullname": "models.ConversationParticipant.conversation", "modulename": "models", "qualname": "ConversationParticipant.conversation", "kind": "variable", "doc": "

\n"}, "models.ConversationParticipant.user": {"fullname": "models.ConversationParticipant.user", "modulename": "models", "qualname": "ConversationParticipant.user", "kind": "variable", "doc": "

\n"}, "models.Message": {"fullname": "models.Message", "modulename": "models", "qualname": "Message", "kind": "class", "doc": "

A single chat message inside a Conversation.

\n", "bases": "sqlalchemy.orm.decl_api._DynamicAttributesType, sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]"}, "models.Message.__init__": {"fullname": "models.Message.__init__", "modulename": "models", "qualname": "Message.__init__", "kind": "function", "doc": "

A simple constructor that allows initialization from kwargs.

\n\n

Sets attributes on the constructed instance using the names and\nvalues in kwargs.

\n\n

Only keys that are present as\nattributes of the instance's class are allowed. These could be,\nfor example, any mapped columns or relationships.

\n", "signature": "(**kwargs)"}, "models.Message.message_id": {"fullname": "models.Message.message_id", "modulename": "models", "qualname": "Message.message_id", "kind": "variable", "doc": "

\n"}, "models.Message.conversation_id": {"fullname": "models.Message.conversation_id", "modulename": "models", "qualname": "Message.conversation_id", "kind": "variable", "doc": "

\n"}, "models.Message.sender_id": {"fullname": "models.Message.sender_id", "modulename": "models", "qualname": "Message.sender_id", "kind": "variable", "doc": "

\n"}, "models.Message.content": {"fullname": "models.Message.content", "modulename": "models", "qualname": "Message.content", "kind": "variable", "doc": "

\n"}, "models.Message.created_at": {"fullname": "models.Message.created_at", "modulename": "models", "qualname": "Message.created_at", "kind": "variable", "doc": "

\n"}, "models.Message.conversation": {"fullname": "models.Message.conversation", "modulename": "models", "qualname": "Message.conversation", "kind": "variable", "doc": "

\n"}, "models.Message.sender": {"fullname": "models.Message.sender", "modulename": "models", "qualname": "Message.sender", "kind": "variable", "doc": "

\n"}, "models.Post": {"fullname": "models.Post", "modulename": "models", "qualname": "Post", "kind": "class", "doc": "

A discussion or resource post within a course.

\n\n

score is a denormalized vote tally maintained alongside\nPostVote rows. is_flagged lets moderators hide a post\nwithout deleting it.

\n", "bases": "sqlalchemy.orm.decl_api._DynamicAttributesType, sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]"}, "models.Post.__init__": {"fullname": "models.Post.__init__", "modulename": "models", "qualname": "Post.__init__", "kind": "function", "doc": "

A simple constructor that allows initialization from kwargs.

\n\n

Sets attributes on the constructed instance using the names and\nvalues in kwargs.

\n\n

Only keys that are present as\nattributes of the instance's class are allowed. These could be,\nfor example, any mapped columns or relationships.

\n", "signature": "(**kwargs)"}, "models.Post.id": {"fullname": "models.Post.id", "modulename": "models", "qualname": "Post.id", "kind": "variable", "doc": "

\n"}, "models.Post.course_id": {"fullname": "models.Post.course_id", "modulename": "models", "qualname": "Post.course_id", "kind": "variable", "doc": "

\n"}, "models.Post.author_uid": {"fullname": "models.Post.author_uid", "modulename": "models", "qualname": "Post.author_uid", "kind": "variable", "doc": "

\n"}, "models.Post.title": {"fullname": "models.Post.title", "modulename": "models", "qualname": "Post.title", "kind": "variable", "doc": "

\n"}, "models.Post.description": {"fullname": "models.Post.description", "modulename": "models", "qualname": "Post.description", "kind": "variable", "doc": "

\n"}, "models.Post.resource_link": {"fullname": "models.Post.resource_link", "modulename": "models", "qualname": "Post.resource_link", "kind": "variable", "doc": "

\n"}, "models.Post.score": {"fullname": "models.Post.score", "modulename": "models", "qualname": "Post.score", "kind": "variable", "doc": "

\n"}, "models.Post.is_flagged": {"fullname": "models.Post.is_flagged", "modulename": "models", "qualname": "Post.is_flagged", "kind": "variable", "doc": "

\n"}, "models.Post.created_at": {"fullname": "models.Post.created_at", "modulename": "models", "qualname": "Post.created_at", "kind": "variable", "doc": "

\n"}, "models.Post.author": {"fullname": "models.Post.author", "modulename": "models", "qualname": "Post.author", "kind": "variable", "doc": "

\n"}, "models.Post.course": {"fullname": "models.Post.course", "modulename": "models", "qualname": "Post.course", "kind": "variable", "doc": "

\n"}, "models.Post.votes": {"fullname": "models.Post.votes", "modulename": "models", "qualname": "Post.votes", "kind": "variable", "doc": "

\n"}, "models.PostVote": {"fullname": "models.PostVote", "modulename": "models", "qualname": "PostVote", "kind": "class", "doc": "

One user's upvote (+1) or downvote (-1) on a Post.

\n", "bases": "sqlalchemy.orm.decl_api._DynamicAttributesType, sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]"}, "models.PostVote.__init__": {"fullname": "models.PostVote.__init__", "modulename": "models", "qualname": "PostVote.__init__", "kind": "function", "doc": "

A simple constructor that allows initialization from kwargs.

\n\n

Sets attributes on the constructed instance using the names and\nvalues in kwargs.

\n\n

Only keys that are present as\nattributes of the instance's class are allowed. These could be,\nfor example, any mapped columns or relationships.

\n", "signature": "(**kwargs)"}, "models.PostVote.id": {"fullname": "models.PostVote.id", "modulename": "models", "qualname": "PostVote.id", "kind": "variable", "doc": "

\n"}, "models.PostVote.post_id": {"fullname": "models.PostVote.post_id", "modulename": "models", "qualname": "PostVote.post_id", "kind": "variable", "doc": "

\n"}, "models.PostVote.user_uid": {"fullname": "models.PostVote.user_uid", "modulename": "models", "qualname": "PostVote.user_uid", "kind": "variable", "doc": "

\n"}, "models.PostVote.vote": {"fullname": "models.PostVote.vote", "modulename": "models", "qualname": "PostVote.vote", "kind": "variable", "doc": "

\n"}, "models.PostVote.post": {"fullname": "models.PostVote.post", "modulename": "models", "qualname": "PostVote.post", "kind": "variable", "doc": "

\n"}, "models.StudyGroup": {"fullname": "models.StudyGroup", "modulename": "models", "qualname": "StudyGroup", "kind": "class", "doc": "

A persistent study group inside a course.

\n\n

Holds the long-lived membership (StudyGroupMember) and any\nscheduled meetings (StudySession).

\n", "bases": "sqlalchemy.orm.decl_api._DynamicAttributesType, sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]"}, "models.StudyGroup.__init__": {"fullname": "models.StudyGroup.__init__", "modulename": "models", "qualname": "StudyGroup.__init__", "kind": "function", "doc": "

A simple constructor that allows initialization from kwargs.

\n\n

Sets attributes on the constructed instance using the names and\nvalues in kwargs.

\n\n

Only keys that are present as\nattributes of the instance's class are allowed. These could be,\nfor example, any mapped columns or relationships.

\n", "signature": "(**kwargs)"}, "models.StudyGroup.id": {"fullname": "models.StudyGroup.id", "modulename": "models", "qualname": "StudyGroup.id", "kind": "variable", "doc": "

\n"}, "models.StudyGroup.course_id": {"fullname": "models.StudyGroup.course_id", "modulename": "models", "qualname": "StudyGroup.course_id", "kind": "variable", "doc": "

\n"}, "models.StudyGroup.name": {"fullname": "models.StudyGroup.name", "modulename": "models", "qualname": "StudyGroup.name", "kind": "variable", "doc": "

\n"}, "models.StudyGroup.created_at": {"fullname": "models.StudyGroup.created_at", "modulename": "models", "qualname": "StudyGroup.created_at", "kind": "variable", "doc": "

\n"}, "models.StudyGroup.course": {"fullname": "models.StudyGroup.course", "modulename": "models", "qualname": "StudyGroup.course", "kind": "variable", "doc": "

\n"}, "models.StudyGroup.members": {"fullname": "models.StudyGroup.members", "modulename": "models", "qualname": "StudyGroup.members", "kind": "variable", "doc": "

\n"}, "models.StudyGroup.sessions": {"fullname": "models.StudyGroup.sessions", "modulename": "models", "qualname": "StudyGroup.sessions", "kind": "variable", "doc": "

\n"}, "models.StudySession": {"fullname": "models.StudySession", "modulename": "models", "qualname": "StudySession", "kind": "class", "doc": "

A scheduled study meeting.

\n\n

session_type is \"solo\" for personal study blocks or\n\"group\" when tied to a StudyGroup via group_id.\nInvitees (for group sessions) live in StudySessionInvitee.

\n", "bases": "sqlalchemy.orm.decl_api._DynamicAttributesType, sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]"}, "models.StudySession.__init__": {"fullname": "models.StudySession.__init__", "modulename": "models", "qualname": "StudySession.__init__", "kind": "function", "doc": "

A simple constructor that allows initialization from kwargs.

\n\n

Sets attributes on the constructed instance using the names and\nvalues in kwargs.

\n\n

Only keys that are present as\nattributes of the instance's class are allowed. These could be,\nfor example, any mapped columns or relationships.

\n", "signature": "(**kwargs)"}, "models.StudySession.id": {"fullname": "models.StudySession.id", "modulename": "models", "qualname": "StudySession.id", "kind": "variable", "doc": "

\n"}, "models.StudySession.course_id": {"fullname": "models.StudySession.course_id", "modulename": "models", "qualname": "StudySession.course_id", "kind": "variable", "doc": "

\n"}, "models.StudySession.creator_email": {"fullname": "models.StudySession.creator_email", "modulename": "models", "qualname": "StudySession.creator_email", "kind": "variable", "doc": "

\n"}, "models.StudySession.session_type": {"fullname": "models.StudySession.session_type", "modulename": "models", "qualname": "StudySession.session_type", "kind": "variable", "doc": "

\n"}, "models.StudySession.title": {"fullname": "models.StudySession.title", "modulename": "models", "qualname": "StudySession.title", "kind": "variable", "doc": "

\n"}, "models.StudySession.starts_at": {"fullname": "models.StudySession.starts_at", "modulename": "models", "qualname": "StudySession.starts_at", "kind": "variable", "doc": "

\n"}, "models.StudySession.ends_at": {"fullname": "models.StudySession.ends_at", "modulename": "models", "qualname": "StudySession.ends_at", "kind": "variable", "doc": "

\n"}, "models.StudySession.group_id": {"fullname": "models.StudySession.group_id", "modulename": "models", "qualname": "StudySession.group_id", "kind": "variable", "doc": "

\n"}, "models.StudySession.created_at": {"fullname": "models.StudySession.created_at", "modulename": "models", "qualname": "StudySession.created_at", "kind": "variable", "doc": "

\n"}, "models.StudySession.group": {"fullname": "models.StudySession.group", "modulename": "models", "qualname": "StudySession.group", "kind": "variable", "doc": "

\n"}, "models.StudySession.invitees": {"fullname": "models.StudySession.invitees", "modulename": "models", "qualname": "StudySession.invitees", "kind": "variable", "doc": "

\n"}, "models.StudyGroupMember": {"fullname": "models.StudyGroupMember", "modulename": "models", "qualname": "StudyGroupMember", "kind": "class", "doc": "

A user's membership in a StudyGroup, keyed by email.

\n", "bases": "sqlalchemy.orm.decl_api._DynamicAttributesType, sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]"}, "models.StudyGroupMember.__init__": {"fullname": "models.StudyGroupMember.__init__", "modulename": "models", "qualname": "StudyGroupMember.__init__", "kind": "function", "doc": "

A simple constructor that allows initialization from kwargs.

\n\n

Sets attributes on the constructed instance using the names and\nvalues in kwargs.

\n\n

Only keys that are present as\nattributes of the instance's class are allowed. These could be,\nfor example, any mapped columns or relationships.

\n", "signature": "(**kwargs)"}, "models.StudyGroupMember.id": {"fullname": "models.StudyGroupMember.id", "modulename": "models", "qualname": "StudyGroupMember.id", "kind": "variable", "doc": "

\n"}, "models.StudyGroupMember.group_id": {"fullname": "models.StudyGroupMember.group_id", "modulename": "models", "qualname": "StudyGroupMember.group_id", "kind": "variable", "doc": "

\n"}, "models.StudyGroupMember.user_email": {"fullname": "models.StudyGroupMember.user_email", "modulename": "models", "qualname": "StudyGroupMember.user_email", "kind": "variable", "doc": "

\n"}, "models.StudyGroupMember.joined_at": {"fullname": "models.StudyGroupMember.joined_at", "modulename": "models", "qualname": "StudyGroupMember.joined_at", "kind": "variable", "doc": "

\n"}, "models.StudyGroupMember.group": {"fullname": "models.StudyGroupMember.group", "modulename": "models", "qualname": "StudyGroupMember.group", "kind": "variable", "doc": "

\n"}, "models.UserAvailability": {"fullname": "models.UserAvailability", "modulename": "models", "qualname": "UserAvailability", "kind": "class", "doc": "

A busy/free time block for a user.

\n\n

Most rows are imported from Google Calendar (source =\n\"google_calendar\"); rows tied to a StudySession reflect blocks\nStudySync itself created. Used by the scheduler to find overlap\nwhen proposing meeting times.

\n", "bases": "sqlalchemy.orm.decl_api._DynamicAttributesType, sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]"}, "models.UserAvailability.__init__": {"fullname": "models.UserAvailability.__init__", "modulename": "models", "qualname": "UserAvailability.__init__", "kind": "function", "doc": "

A simple constructor that allows initialization from kwargs.

\n\n

Sets attributes on the constructed instance using the names and\nvalues in kwargs.

\n\n

Only keys that are present as\nattributes of the instance's class are allowed. These could be,\nfor example, any mapped columns or relationships.

\n", "signature": "(**kwargs)"}, "models.UserAvailability.id": {"fullname": "models.UserAvailability.id", "modulename": "models", "qualname": "UserAvailability.id", "kind": "variable", "doc": "

\n"}, "models.UserAvailability.user_email": {"fullname": "models.UserAvailability.user_email", "modulename": "models", "qualname": "UserAvailability.user_email", "kind": "variable", "doc": "

\n"}, "models.UserAvailability.starts_at": {"fullname": "models.UserAvailability.starts_at", "modulename": "models", "qualname": "UserAvailability.starts_at", "kind": "variable", "doc": "

\n"}, "models.UserAvailability.ends_at": {"fullname": "models.UserAvailability.ends_at", "modulename": "models", "qualname": "UserAvailability.ends_at", "kind": "variable", "doc": "

\n"}, "models.UserAvailability.source": {"fullname": "models.UserAvailability.source", "modulename": "models", "qualname": "UserAvailability.source", "kind": "variable", "doc": "

\n"}, "models.UserAvailability.study_session_id": {"fullname": "models.UserAvailability.study_session_id", "modulename": "models", "qualname": "UserAvailability.study_session_id", "kind": "variable", "doc": "

\n"}, "models.UserAvailability.created_at": {"fullname": "models.UserAvailability.created_at", "modulename": "models", "qualname": "UserAvailability.created_at", "kind": "variable", "doc": "

\n"}, "models.StudySessionInvitee": {"fullname": "models.StudySessionInvitee", "modulename": "models", "qualname": "StudySessionInvitee", "kind": "class", "doc": "

An email invited to a StudySession; the invitee may not yet be a registered user.

\n", "bases": "sqlalchemy.orm.decl_api._DynamicAttributesType, sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]"}, "models.StudySessionInvitee.__init__": {"fullname": "models.StudySessionInvitee.__init__", "modulename": "models", "qualname": "StudySessionInvitee.__init__", "kind": "function", "doc": "

A simple constructor that allows initialization from kwargs.

\n\n

Sets attributes on the constructed instance using the names and\nvalues in kwargs.

\n\n

Only keys that are present as\nattributes of the instance's class are allowed. These could be,\nfor example, any mapped columns or relationships.

\n", "signature": "(**kwargs)"}, "models.StudySessionInvitee.id": {"fullname": "models.StudySessionInvitee.id", "modulename": "models", "qualname": "StudySessionInvitee.id", "kind": "variable", "doc": "

\n"}, "models.StudySessionInvitee.study_session_id": {"fullname": "models.StudySessionInvitee.study_session_id", "modulename": "models", "qualname": "StudySessionInvitee.study_session_id", "kind": "variable", "doc": "

\n"}, "models.StudySessionInvitee.user_email": {"fullname": "models.StudySessionInvitee.user_email", "modulename": "models", "qualname": "StudySessionInvitee.user_email", "kind": "variable", "doc": "

\n"}, "models.StudySessionInvitee.created_at": {"fullname": "models.StudySessionInvitee.created_at", "modulename": "models", "qualname": "StudySessionInvitee.created_at", "kind": "variable", "doc": "

\n"}, "models.StudySessionInvitee.session": {"fullname": "models.StudySessionInvitee.session", "modulename": "models", "qualname": "StudySessionInvitee.session", "kind": "variable", "doc": "

\n"}}, "docInfo": {"database": {"qualname": 0, "fullname": 1, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "database.Base": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 12, "doc": 35}, "database.Base.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 16, "bases": 0, "doc": 56}, "database.Base.registry": {"qualname": 2, "fullname": 3, "annotation": 6, "default_value": 10, "signature": 0, "bases": 0, "doc": 3}, "database.Base.metadata": {"qualname": 2, "fullname": 3, "annotation": 5, "default_value": 2, "signature": 0, "bases": 0, "doc": 3}, "database.get_db": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 7, "bases": 0, "doc": 29}, "models": {"qualname": 0, "fullname": 1, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 118}, "models.UserRole": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 4, "doc": 16}, "models.UserRole.STUDENT": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 9, "signature": 0, "bases": 0, "doc": 3}, "models.UserRole.TA": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 9, "signature": 0, "bases": 0, "doc": 3}, "models.UserRole.ADMIN": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 9, "signature": 0, "bases": 0, "doc": 3}, "models.User": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 12, "doc": 43}, "models.User.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 11, "bases": 0, "doc": 56}, "models.User.firebase_uid": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.User.email": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.User.full_name": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.User.role": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.User.google_calendar_token": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.User.created_at": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.User.messages": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.User.conversation_participants": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.User.courses_created": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.User.posts": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.User.enrollments": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Course": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 12, "doc": 13}, "models.Course.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 11, "bases": 0, "doc": 56}, "models.Course.id": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Course.course_code": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Course.name": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Course.description": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Course.owner_id": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Course.created_at": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Course.owner": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Course.members": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Course.posts": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Course.study_groups": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Course.conversations": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Enrollment": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 12, "doc": 19}, "models.Enrollment.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 11, "bases": 0, "doc": 56}, "models.Enrollment.id": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Enrollment.user_id": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Enrollment.course_id": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Enrollment.enrolled_at": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Enrollment.user": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Enrollment.course": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Conversation": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 12, "doc": 42}, "models.Conversation.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 11, "bases": 0, "doc": 56}, "models.Conversation.conversation_id": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Conversation.course_id": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Conversation.is_group": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Conversation.group_name": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Conversation.created_at": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Conversation.course": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Conversation.participants": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Conversation.messages": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.ConversationParticipant": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 12, "doc": 14}, "models.ConversationParticipant.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 11, "bases": 0, "doc": 56}, "models.ConversationParticipant.participant_id": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.ConversationParticipant.conversation_id": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.ConversationParticipant.user_id": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.ConversationParticipant.joined_at": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.ConversationParticipant.conversation": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.ConversationParticipant.user": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Message": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 12, "doc": 12}, "models.Message.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 11, "bases": 0, "doc": 56}, "models.Message.message_id": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Message.conversation_id": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Message.sender_id": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Message.content": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Message.created_at": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Message.conversation": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Message.sender": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Post": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 12, "doc": 40}, "models.Post.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 11, "bases": 0, "doc": 56}, "models.Post.id": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Post.course_id": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Post.author_uid": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Post.title": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Post.description": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Post.resource_link": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Post.score": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Post.is_flagged": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Post.created_at": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Post.author": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Post.course": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.Post.votes": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.PostVote": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 12, "doc": 16}, "models.PostVote.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 11, "bases": 0, "doc": 56}, "models.PostVote.id": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.PostVote.post_id": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.PostVote.user_uid": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.PostVote.vote": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.PostVote.post": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudyGroup": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 12, "doc": 28}, "models.StudyGroup.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 11, "bases": 0, "doc": 56}, "models.StudyGroup.id": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudyGroup.course_id": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudyGroup.name": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudyGroup.created_at": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudyGroup.course": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudyGroup.members": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudyGroup.sessions": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudySession": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 12, "doc": 51}, "models.StudySession.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 11, "bases": 0, "doc": 56}, "models.StudySession.id": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudySession.course_id": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudySession.creator_email": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudySession.session_type": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudySession.title": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudySession.starts_at": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudySession.ends_at": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudySession.group_id": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudySession.created_at": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudySession.group": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudySession.invitees": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudyGroupMember": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 12, "doc": 15}, "models.StudyGroupMember.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 11, "bases": 0, "doc": 56}, "models.StudyGroupMember.id": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudyGroupMember.group_id": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudyGroupMember.user_email": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudyGroupMember.joined_at": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudyGroupMember.group": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.UserAvailability": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 12, "doc": 47}, "models.UserAvailability.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 11, "bases": 0, "doc": 56}, "models.UserAvailability.id": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.UserAvailability.user_email": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.UserAvailability.starts_at": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.UserAvailability.ends_at": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.UserAvailability.source": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.UserAvailability.study_session_id": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.UserAvailability.created_at": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudySessionInvitee": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 12, "doc": 20}, "models.StudySessionInvitee.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 11, "bases": 0, "doc": 56}, "models.StudySessionInvitee.id": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudySessionInvitee.study_session_id": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudySessionInvitee.user_email": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudySessionInvitee.created_at": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "models.StudySessionInvitee.session": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}}, "length": 138, "save": true}, "index": {"qualname": {"root": {"docs": {"database.Base.__init__": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 14, "b": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"database.Base": {"tf": 1}, "database.Base.__init__": {"tf": 1}, "database.Base.registry": {"tf": 1}, "database.Base.metadata": {"tf": 1}}, "df": 4}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"database.Base.__init__": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 14}}, "v": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"models.StudySession.invitees": {"tf": 1}}, "df": 1}}}}}}}, "d": {"docs": {"models.Course.id": {"tf": 1}, "models.Course.owner_id": {"tf": 1}, "models.Enrollment.id": {"tf": 1}, "models.Enrollment.user_id": {"tf": 1}, "models.Enrollment.course_id": {"tf": 1}, "models.Conversation.conversation_id": {"tf": 1}, "models.Conversation.course_id": {"tf": 1}, "models.ConversationParticipant.participant_id": {"tf": 1}, "models.ConversationParticipant.conversation_id": {"tf": 1}, "models.ConversationParticipant.user_id": {"tf": 1}, "models.Message.message_id": {"tf": 1}, "models.Message.conversation_id": {"tf": 1}, "models.Message.sender_id": {"tf": 1}, "models.Post.id": {"tf": 1}, "models.Post.course_id": {"tf": 1}, "models.PostVote.id": {"tf": 1}, "models.PostVote.post_id": {"tf": 1}, "models.StudyGroup.id": {"tf": 1}, "models.StudyGroup.course_id": {"tf": 1}, "models.StudySession.id": {"tf": 1}, "models.StudySession.course_id": {"tf": 1}, "models.StudySession.group_id": {"tf": 1}, "models.StudyGroupMember.id": {"tf": 1}, "models.StudyGroupMember.group_id": {"tf": 1}, "models.UserAvailability.id": {"tf": 1}, "models.UserAvailability.study_session_id": {"tf": 1}, "models.StudySessionInvitee.id": {"tf": 1}, "models.StudySessionInvitee.study_session_id": {"tf": 1}}, "df": 28}, "s": {"docs": {"models.Conversation.is_group": {"tf": 1}, "models.Post.is_flagged": {"tf": 1}}, "df": 2}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "y": {"docs": {"database.Base.registry": {"tf": 1}}, "df": 1}}}}}}, "s": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"models.Post.resource_link": {"tf": 1}}, "df": 1}}}}}}}, "o": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"models.User.role": {"tf": 1}}, "df": 1}}}}, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {"database.Base.metadata": {"tf": 1}}, "df": 1}}}}}}, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {"models.Message": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Message.message_id": {"tf": 1.4142135623730951}, "models.Message.conversation_id": {"tf": 1}, "models.Message.sender_id": {"tf": 1}, "models.Message.content": {"tf": 1}, "models.Message.created_at": {"tf": 1}, "models.Message.conversation": {"tf": 1}, "models.Message.sender": {"tf": 1}}, "df": 9, "s": {"docs": {"models.User.messages": {"tf": 1}, "models.Conversation.messages": {"tf": 1}}, "df": 2}}}}}}, "m": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {"models.Course.members": {"tf": 1}, "models.StudyGroup.members": {"tf": 1}}, "df": 2}}}}}}}, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {"database.get_db": {"tf": 1}}, "df": 1}}, "o": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"models.User.google_calendar_token": {"tf": 1}}, "df": 1}}}}}, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "p": {"docs": {"models.Conversation.is_group": {"tf": 1}, "models.Conversation.group_name": {"tf": 1}, "models.StudySession.group_id": {"tf": 1}, "models.StudySession.group": {"tf": 1}, "models.StudyGroupMember.group_id": {"tf": 1}, "models.StudyGroupMember.group": {"tf": 1}}, "df": 6, "s": {"docs": {"models.Course.study_groups": {"tf": 1}}, "df": 1}}}}}}, "d": {"docs": {}, "df": 0, "b": {"docs": {"database.get_db": {"tf": 1}}, "df": 1}, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"models.Course.description": {"tf": 1}, "models.Post.description": {"tf": 1}}, "df": 2}}}}}}}}}}}, "u": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"models.User": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.User.firebase_uid": {"tf": 1}, "models.User.email": {"tf": 1}, "models.User.full_name": {"tf": 1}, "models.User.role": {"tf": 1}, "models.User.google_calendar_token": {"tf": 1}, "models.User.created_at": {"tf": 1}, "models.User.messages": {"tf": 1}, "models.User.conversation_participants": {"tf": 1}, "models.User.courses_created": {"tf": 1}, "models.User.posts": {"tf": 1}, "models.User.enrollments": {"tf": 1}, "models.Enrollment.user_id": {"tf": 1}, "models.Enrollment.user": {"tf": 1}, "models.ConversationParticipant.user_id": {"tf": 1}, "models.ConversationParticipant.user": {"tf": 1}, "models.PostVote.user_uid": {"tf": 1}, "models.StudyGroupMember.user_email": {"tf": 1}, "models.UserAvailability.user_email": {"tf": 1}, "models.StudySessionInvitee.user_email": {"tf": 1}}, "df": 21, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"models.UserRole": {"tf": 1}, "models.UserRole.STUDENT": {"tf": 1}, "models.UserRole.TA": {"tf": 1}, "models.UserRole.ADMIN": {"tf": 1}}, "df": 4}}}}, "a": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "y": {"docs": {"models.UserAvailability": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.UserAvailability.id": {"tf": 1}, "models.UserAvailability.user_email": {"tf": 1}, "models.UserAvailability.starts_at": {"tf": 1}, "models.UserAvailability.ends_at": {"tf": 1}, "models.UserAvailability.source": {"tf": 1}, "models.UserAvailability.study_session_id": {"tf": 1}, "models.UserAvailability.created_at": {"tf": 1}}, "df": 9}}}}}}}}}}}}}}}, "i": {"docs": {}, "df": 0, "d": {"docs": {"models.User.firebase_uid": {"tf": 1}, "models.Post.author_uid": {"tf": 1}, "models.PostVote.user_uid": {"tf": 1}}, "df": 3}}}, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"models.UserRole.STUDENT": {"tf": 1}}, "df": 1}}}, "y": {"docs": {"models.Course.study_groups": {"tf": 1}, "models.UserAvailability.study_session_id": {"tf": 1}, "models.StudySessionInvitee.study_session_id": {"tf": 1}}, "df": 3, "g": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "p": {"docs": {"models.StudyGroup": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudyGroup.id": {"tf": 1}, "models.StudyGroup.course_id": {"tf": 1}, "models.StudyGroup.name": {"tf": 1}, "models.StudyGroup.created_at": {"tf": 1}, "models.StudyGroup.course": {"tf": 1}, "models.StudyGroup.members": {"tf": 1}, "models.StudyGroup.sessions": {"tf": 1}}, "df": 9, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"models.StudyGroupMember": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.StudyGroupMember.id": {"tf": 1}, "models.StudyGroupMember.group_id": {"tf": 1}, "models.StudyGroupMember.user_email": {"tf": 1}, "models.StudyGroupMember.joined_at": {"tf": 1}, "models.StudyGroupMember.group": {"tf": 1}}, "df": 7}}}}}}}}}}}, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"models.StudySession": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudySession.id": {"tf": 1}, "models.StudySession.course_id": {"tf": 1}, "models.StudySession.creator_email": {"tf": 1}, "models.StudySession.session_type": {"tf": 1}, "models.StudySession.title": {"tf": 1}, "models.StudySession.starts_at": {"tf": 1}, "models.StudySession.ends_at": {"tf": 1}, "models.StudySession.group_id": {"tf": 1}, "models.StudySession.created_at": {"tf": 1}, "models.StudySession.group": {"tf": 1}, "models.StudySession.invitees": {"tf": 1}}, "df": 13, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "e": {"docs": {"models.StudySessionInvitee": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}, "models.StudySessionInvitee.id": {"tf": 1}, "models.StudySessionInvitee.study_session_id": {"tf": 1}, "models.StudySessionInvitee.user_email": {"tf": 1}, "models.StudySessionInvitee.created_at": {"tf": 1}, "models.StudySessionInvitee.session": {"tf": 1}}, "df": 7}}}}}}}}}}}}}}}}}, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "s": {"docs": {"models.StudySession.starts_at": {"tf": 1}, "models.UserAvailability.starts_at": {"tf": 1}}, "df": 2}}}}}, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"models.Message.sender_id": {"tf": 1}, "models.Message.sender": {"tf": 1}}, "df": 2}}}}, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"models.StudySession.session_type": {"tf": 1}, "models.UserAvailability.study_session_id": {"tf": 1}, "models.StudySessionInvitee.study_session_id": {"tf": 1}, "models.StudySessionInvitee.session": {"tf": 1}}, "df": 4, "s": {"docs": {"models.StudyGroup.sessions": {"tf": 1}}, "df": 1}}}}}}}, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {"models.Post.score": {"tf": 1}}, "df": 1}}}}, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"models.UserAvailability.source": {"tf": 1}}, "df": 1}}}}}}, "t": {"docs": {}, "df": 0, "a": {"docs": {"models.UserRole.TA": {"tf": 1}}, "df": 1}, "o": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {"models.User.google_calendar_token": {"tf": 1}}, "df": 1}}}}, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"models.Post.title": {"tf": 1}, "models.StudySession.title": {"tf": 1}}, "df": 2}}}}, "y": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {"models.StudySession.session_type": {"tf": 1}}, "df": 1}}}}, "a": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {"models.UserRole.ADMIN": {"tf": 1}}, "df": 1}}}}, "t": {"docs": {"models.User.created_at": {"tf": 1}, "models.Course.created_at": {"tf": 1}, "models.Enrollment.enrolled_at": {"tf": 1}, "models.Conversation.created_at": {"tf": 1}, "models.ConversationParticipant.joined_at": {"tf": 1}, "models.Message.created_at": {"tf": 1}, "models.Post.created_at": {"tf": 1}, "models.StudyGroup.created_at": {"tf": 1}, "models.StudySession.starts_at": {"tf": 1}, "models.StudySession.ends_at": {"tf": 1}, "models.StudySession.created_at": {"tf": 1}, "models.StudyGroupMember.joined_at": {"tf": 1}, "models.UserAvailability.starts_at": {"tf": 1}, "models.UserAvailability.ends_at": {"tf": 1}, "models.UserAvailability.created_at": {"tf": 1}, "models.StudySessionInvitee.created_at": {"tf": 1}}, "df": 16}, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"models.Post.author_uid": {"tf": 1}, "models.Post.author": {"tf": 1}}, "df": 2}}}}}}, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"models.User.firebase_uid": {"tf": 1}}, "df": 1}}}}}}}, "u": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {"models.User.full_name": {"tf": 1}}, "df": 1}}}, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"models.Post.is_flagged": {"tf": 1}}, "df": 1}}}}}}}, "e": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {"models.User.email": {"tf": 1}, "models.StudySession.creator_email": {"tf": 1}, "models.StudyGroupMember.user_email": {"tf": 1}, "models.UserAvailability.user_email": {"tf": 1}, "models.StudySessionInvitee.user_email": {"tf": 1}}, "df": 5}}}}, "n": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"models.Enrollment": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Enrollment.id": {"tf": 1}, "models.Enrollment.user_id": {"tf": 1}, "models.Enrollment.course_id": {"tf": 1}, "models.Enrollment.enrolled_at": {"tf": 1}, "models.Enrollment.user": {"tf": 1}, "models.Enrollment.course": {"tf": 1}}, "df": 8, "s": {"docs": {"models.User.enrollments": {"tf": 1}}, "df": 1}}}}}, "e": {"docs": {}, "df": 0, "d": {"docs": {"models.Enrollment.enrolled_at": {"tf": 1}}, "df": 1}}}}}}, "d": {"docs": {}, "df": 0, "s": {"docs": {"models.StudySession.ends_at": {"tf": 1}, "models.UserAvailability.ends_at": {"tf": 1}}, "df": 2}}}}, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {"models.User.full_name": {"tf": 1}, "models.Course.name": {"tf": 1}, "models.Conversation.group_name": {"tf": 1}, "models.StudyGroup.name": {"tf": 1}}, "df": 4}}}}, "c": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {"models.User.google_calendar_token": {"tf": 1}}, "df": 1}}}}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"models.User.created_at": {"tf": 1}, "models.User.courses_created": {"tf": 1}, "models.Course.created_at": {"tf": 1}, "models.Conversation.created_at": {"tf": 1}, "models.Message.created_at": {"tf": 1}, "models.Post.created_at": {"tf": 1}, "models.StudyGroup.created_at": {"tf": 1}, "models.StudySession.created_at": {"tf": 1}, "models.UserAvailability.created_at": {"tf": 1}, "models.StudySessionInvitee.created_at": {"tf": 1}}, "df": 10}}, "o": {"docs": {}, "df": 0, "r": {"docs": {"models.StudySession.creator_email": {"tf": 1}}, "df": 1}}}}}}, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"models.User.conversation_participants": {"tf": 1}, "models.Conversation": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.Conversation.conversation_id": {"tf": 1.4142135623730951}, "models.Conversation.course_id": {"tf": 1}, "models.Conversation.is_group": {"tf": 1}, "models.Conversation.group_name": {"tf": 1}, "models.Conversation.created_at": {"tf": 1}, "models.Conversation.course": {"tf": 1}, "models.Conversation.participants": {"tf": 1}, "models.Conversation.messages": {"tf": 1}, "models.ConversationParticipant.conversation_id": {"tf": 1}, "models.ConversationParticipant.conversation": {"tf": 1}, "models.Message.conversation_id": {"tf": 1}, "models.Message.conversation": {"tf": 1}}, "df": 15, "s": {"docs": {"models.Course.conversations": {"tf": 1}}, "df": 1}, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"models.ConversationParticipant": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.ConversationParticipant.participant_id": {"tf": 1}, "models.ConversationParticipant.conversation_id": {"tf": 1}, "models.ConversationParticipant.user_id": {"tf": 1}, "models.ConversationParticipant.joined_at": {"tf": 1}, "models.ConversationParticipant.conversation": {"tf": 1}, "models.ConversationParticipant.user": {"tf": 1}}, "df": 8}}}}}}}}}}}}}}}}}}}}, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"models.Message.content": {"tf": 1}}, "df": 1}}}}}, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"models.Course": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Course.id": {"tf": 1}, "models.Course.course_code": {"tf": 1.4142135623730951}, "models.Course.name": {"tf": 1}, "models.Course.description": {"tf": 1}, "models.Course.owner_id": {"tf": 1}, "models.Course.created_at": {"tf": 1}, "models.Course.owner": {"tf": 1}, "models.Course.members": {"tf": 1}, "models.Course.posts": {"tf": 1}, "models.Course.study_groups": {"tf": 1}, "models.Course.conversations": {"tf": 1}, "models.Enrollment.course_id": {"tf": 1}, "models.Enrollment.course": {"tf": 1}, "models.Conversation.course_id": {"tf": 1}, "models.Conversation.course": {"tf": 1}, "models.Post.course_id": {"tf": 1}, "models.Post.course": {"tf": 1}, "models.StudyGroup.course_id": {"tf": 1}, "models.StudyGroup.course": {"tf": 1}, "models.StudySession.course_id": {"tf": 1}}, "df": 22, "s": {"docs": {"models.User.courses_created": {"tf": 1}}, "df": 1}}}}}, "d": {"docs": {}, "df": 0, "e": {"docs": {"models.Course.course_code": {"tf": 1}}, "df": 1}}}}, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"models.ConversationParticipant.participant_id": {"tf": 1}}, "df": 1, "s": {"docs": {"models.User.conversation_participants": {"tf": 1}, "models.Conversation.participants": {"tf": 1}}, "df": 2}}}}}}}}}}}, "o": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"models.Post": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.Post.id": {"tf": 1}, "models.Post.course_id": {"tf": 1}, "models.Post.author_uid": {"tf": 1}, "models.Post.title": {"tf": 1}, "models.Post.description": {"tf": 1}, "models.Post.resource_link": {"tf": 1}, "models.Post.score": {"tf": 1}, "models.Post.is_flagged": {"tf": 1}, "models.Post.created_at": {"tf": 1}, "models.Post.author": {"tf": 1}, "models.Post.course": {"tf": 1}, "models.Post.votes": {"tf": 1}, "models.PostVote.post_id": {"tf": 1}, "models.PostVote.post": {"tf": 1}}, "df": 16, "s": {"docs": {"models.User.posts": {"tf": 1}, "models.Course.posts": {"tf": 1}}, "df": 2}, "v": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"models.PostVote": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.PostVote.id": {"tf": 1}, "models.PostVote.post_id": {"tf": 1}, "models.PostVote.user_uid": {"tf": 1}, "models.PostVote.vote": {"tf": 1}, "models.PostVote.post": {"tf": 1}}, "df": 7}}}}}}}}, "o": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"models.Course.owner_id": {"tf": 1}, "models.Course.owner": {"tf": 1}}, "df": 2}}}}}, "j": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"models.ConversationParticipant.joined_at": {"tf": 1}, "models.StudyGroupMember.joined_at": {"tf": 1}}, "df": 2}}}}}}, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "k": {"docs": {"models.Post.resource_link": {"tf": 1}}, "df": 1}}}}, "v": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"models.PostVote.vote": {"tf": 1}}, "df": 1, "s": {"docs": {"models.Post.votes": {"tf": 1}}, "df": 1}}}}}}}, "fullname": {"root": {"docs": {"database.Base.__init__": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 14, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"database": {"tf": 1}, "database.Base": {"tf": 1}, "database.Base.__init__": {"tf": 1}, "database.Base.registry": {"tf": 1}, "database.Base.metadata": {"tf": 1}, "database.get_db": {"tf": 1}}, "df": 6}}}}}}}, "b": {"docs": {"database.get_db": {"tf": 1}}, "df": 1}, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"models.Course.description": {"tf": 1}, "models.Post.description": {"tf": 1}}, "df": 2}}}}}}}}}}}, "b": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"database.Base": {"tf": 1}, "database.Base.__init__": {"tf": 1}, "database.Base.registry": {"tf": 1}, "database.Base.metadata": {"tf": 1}}, "df": 4}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"database.Base.__init__": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 14}}, "v": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"models.StudySession.invitees": {"tf": 1}}, "df": 1}}}}}}}, "d": {"docs": {"models.Course.id": {"tf": 1}, "models.Course.owner_id": {"tf": 1}, "models.Enrollment.id": {"tf": 1}, "models.Enrollment.user_id": {"tf": 1}, "models.Enrollment.course_id": {"tf": 1}, "models.Conversation.conversation_id": {"tf": 1}, "models.Conversation.course_id": {"tf": 1}, "models.ConversationParticipant.participant_id": {"tf": 1}, "models.ConversationParticipant.conversation_id": {"tf": 1}, "models.ConversationParticipant.user_id": {"tf": 1}, "models.Message.message_id": {"tf": 1}, "models.Message.conversation_id": {"tf": 1}, "models.Message.sender_id": {"tf": 1}, "models.Post.id": {"tf": 1}, "models.Post.course_id": {"tf": 1}, "models.PostVote.id": {"tf": 1}, "models.PostVote.post_id": {"tf": 1}, "models.StudyGroup.id": {"tf": 1}, "models.StudyGroup.course_id": {"tf": 1}, "models.StudySession.id": {"tf": 1}, "models.StudySession.course_id": {"tf": 1}, "models.StudySession.group_id": {"tf": 1}, "models.StudyGroupMember.id": {"tf": 1}, "models.StudyGroupMember.group_id": {"tf": 1}, "models.UserAvailability.id": {"tf": 1}, "models.UserAvailability.study_session_id": {"tf": 1}, "models.StudySessionInvitee.id": {"tf": 1}, "models.StudySessionInvitee.study_session_id": {"tf": 1}}, "df": 28}, "s": {"docs": {"models.Conversation.is_group": {"tf": 1}, "models.Post.is_flagged": {"tf": 1}}, "df": 2}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "y": {"docs": {"database.Base.registry": {"tf": 1}}, "df": 1}}}}}}, "s": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"models.Post.resource_link": {"tf": 1}}, "df": 1}}}}}}}, "o": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"models.User.role": {"tf": 1}}, "df": 1}}}}, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {"database.Base.metadata": {"tf": 1}}, "df": 1}}}}}}, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {"models.Message": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Message.message_id": {"tf": 1.4142135623730951}, "models.Message.conversation_id": {"tf": 1}, "models.Message.sender_id": {"tf": 1}, "models.Message.content": {"tf": 1}, "models.Message.created_at": {"tf": 1}, "models.Message.conversation": {"tf": 1}, "models.Message.sender": {"tf": 1}}, "df": 9, "s": {"docs": {"models.User.messages": {"tf": 1}, "models.Conversation.messages": {"tf": 1}}, "df": 2}}}}}}, "m": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {"models.Course.members": {"tf": 1}, "models.StudyGroup.members": {"tf": 1}}, "df": 2}}}}}}, "o": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "s": {"docs": {"models": {"tf": 1}, "models.UserRole": {"tf": 1}, "models.UserRole.STUDENT": {"tf": 1}, "models.UserRole.TA": {"tf": 1}, "models.UserRole.ADMIN": {"tf": 1}, "models.User": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.User.firebase_uid": {"tf": 1}, "models.User.email": {"tf": 1}, "models.User.full_name": {"tf": 1}, "models.User.role": {"tf": 1}, "models.User.google_calendar_token": {"tf": 1}, "models.User.created_at": {"tf": 1}, "models.User.messages": {"tf": 1}, "models.User.conversation_participants": {"tf": 1}, "models.User.courses_created": {"tf": 1}, "models.User.posts": {"tf": 1}, "models.User.enrollments": {"tf": 1}, "models.Course": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Course.id": {"tf": 1}, "models.Course.course_code": {"tf": 1}, "models.Course.name": {"tf": 1}, "models.Course.description": {"tf": 1}, "models.Course.owner_id": {"tf": 1}, "models.Course.created_at": {"tf": 1}, "models.Course.owner": {"tf": 1}, "models.Course.members": {"tf": 1}, "models.Course.posts": {"tf": 1}, "models.Course.study_groups": {"tf": 1}, "models.Course.conversations": {"tf": 1}, "models.Enrollment": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Enrollment.id": {"tf": 1}, "models.Enrollment.user_id": {"tf": 1}, "models.Enrollment.course_id": {"tf": 1}, "models.Enrollment.enrolled_at": {"tf": 1}, "models.Enrollment.user": {"tf": 1}, "models.Enrollment.course": {"tf": 1}, "models.Conversation": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.Conversation.conversation_id": {"tf": 1}, "models.Conversation.course_id": {"tf": 1}, "models.Conversation.is_group": {"tf": 1}, "models.Conversation.group_name": {"tf": 1}, "models.Conversation.created_at": {"tf": 1}, "models.Conversation.course": {"tf": 1}, "models.Conversation.participants": {"tf": 1}, "models.Conversation.messages": {"tf": 1}, "models.ConversationParticipant": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.ConversationParticipant.participant_id": {"tf": 1}, "models.ConversationParticipant.conversation_id": {"tf": 1}, "models.ConversationParticipant.user_id": {"tf": 1}, "models.ConversationParticipant.joined_at": {"tf": 1}, "models.ConversationParticipant.conversation": {"tf": 1}, "models.ConversationParticipant.user": {"tf": 1}, "models.Message": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Message.message_id": {"tf": 1}, "models.Message.conversation_id": {"tf": 1}, "models.Message.sender_id": {"tf": 1}, "models.Message.content": {"tf": 1}, "models.Message.created_at": {"tf": 1}, "models.Message.conversation": {"tf": 1}, "models.Message.sender": {"tf": 1}, "models.Post": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.Post.id": {"tf": 1}, "models.Post.course_id": {"tf": 1}, "models.Post.author_uid": {"tf": 1}, "models.Post.title": {"tf": 1}, "models.Post.description": {"tf": 1}, "models.Post.resource_link": {"tf": 1}, "models.Post.score": {"tf": 1}, "models.Post.is_flagged": {"tf": 1}, "models.Post.created_at": {"tf": 1}, "models.Post.author": {"tf": 1}, "models.Post.course": {"tf": 1}, "models.Post.votes": {"tf": 1}, "models.PostVote": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.PostVote.id": {"tf": 1}, "models.PostVote.post_id": {"tf": 1}, "models.PostVote.user_uid": {"tf": 1}, "models.PostVote.vote": {"tf": 1}, "models.PostVote.post": {"tf": 1}, "models.StudyGroup": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudyGroup.id": {"tf": 1}, "models.StudyGroup.course_id": {"tf": 1}, "models.StudyGroup.name": {"tf": 1}, "models.StudyGroup.created_at": {"tf": 1}, "models.StudyGroup.course": {"tf": 1}, "models.StudyGroup.members": {"tf": 1}, "models.StudyGroup.sessions": {"tf": 1}, "models.StudySession": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudySession.id": {"tf": 1}, "models.StudySession.course_id": {"tf": 1}, "models.StudySession.creator_email": {"tf": 1}, "models.StudySession.session_type": {"tf": 1}, "models.StudySession.title": {"tf": 1}, "models.StudySession.starts_at": {"tf": 1}, "models.StudySession.ends_at": {"tf": 1}, "models.StudySession.group_id": {"tf": 1}, "models.StudySession.created_at": {"tf": 1}, "models.StudySession.group": {"tf": 1}, "models.StudySession.invitees": {"tf": 1}, "models.StudyGroupMember": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.StudyGroupMember.id": {"tf": 1}, "models.StudyGroupMember.group_id": {"tf": 1}, "models.StudyGroupMember.user_email": {"tf": 1}, "models.StudyGroupMember.joined_at": {"tf": 1}, "models.StudyGroupMember.group": {"tf": 1}, "models.UserAvailability": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.UserAvailability.id": {"tf": 1}, "models.UserAvailability.user_email": {"tf": 1}, "models.UserAvailability.starts_at": {"tf": 1}, "models.UserAvailability.ends_at": {"tf": 1}, "models.UserAvailability.source": {"tf": 1}, "models.UserAvailability.study_session_id": {"tf": 1}, "models.UserAvailability.created_at": {"tf": 1}, "models.StudySessionInvitee": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}, "models.StudySessionInvitee.id": {"tf": 1}, "models.StudySessionInvitee.study_session_id": {"tf": 1}, "models.StudySessionInvitee.user_email": {"tf": 1}, "models.StudySessionInvitee.created_at": {"tf": 1}, "models.StudySessionInvitee.session": {"tf": 1}}, "df": 132}}}}}}, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {"database.get_db": {"tf": 1}}, "df": 1}}, "o": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"models.User.google_calendar_token": {"tf": 1}}, "df": 1}}}}}, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "p": {"docs": {"models.Conversation.is_group": {"tf": 1}, "models.Conversation.group_name": {"tf": 1}, "models.StudySession.group_id": {"tf": 1}, "models.StudySession.group": {"tf": 1}, "models.StudyGroupMember.group_id": {"tf": 1}, "models.StudyGroupMember.group": {"tf": 1}}, "df": 6, "s": {"docs": {"models.Course.study_groups": {"tf": 1}}, "df": 1}}}}}}, "u": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"models.User": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.User.firebase_uid": {"tf": 1}, "models.User.email": {"tf": 1}, "models.User.full_name": {"tf": 1}, "models.User.role": {"tf": 1}, "models.User.google_calendar_token": {"tf": 1}, "models.User.created_at": {"tf": 1}, "models.User.messages": {"tf": 1}, "models.User.conversation_participants": {"tf": 1}, "models.User.courses_created": {"tf": 1}, "models.User.posts": {"tf": 1}, "models.User.enrollments": {"tf": 1}, "models.Enrollment.user_id": {"tf": 1}, "models.Enrollment.user": {"tf": 1}, "models.ConversationParticipant.user_id": {"tf": 1}, "models.ConversationParticipant.user": {"tf": 1}, "models.PostVote.user_uid": {"tf": 1}, "models.StudyGroupMember.user_email": {"tf": 1}, "models.UserAvailability.user_email": {"tf": 1}, "models.StudySessionInvitee.user_email": {"tf": 1}}, "df": 21, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"models.UserRole": {"tf": 1}, "models.UserRole.STUDENT": {"tf": 1}, "models.UserRole.TA": {"tf": 1}, "models.UserRole.ADMIN": {"tf": 1}}, "df": 4}}}}, "a": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "y": {"docs": {"models.UserAvailability": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.UserAvailability.id": {"tf": 1}, "models.UserAvailability.user_email": {"tf": 1}, "models.UserAvailability.starts_at": {"tf": 1}, "models.UserAvailability.ends_at": {"tf": 1}, "models.UserAvailability.source": {"tf": 1}, "models.UserAvailability.study_session_id": {"tf": 1}, "models.UserAvailability.created_at": {"tf": 1}}, "df": 9}}}}}}}}}}}}}}}, "i": {"docs": {}, "df": 0, "d": {"docs": {"models.User.firebase_uid": {"tf": 1}, "models.Post.author_uid": {"tf": 1}, "models.PostVote.user_uid": {"tf": 1}}, "df": 3}}}, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"models.UserRole.STUDENT": {"tf": 1}}, "df": 1}}}, "y": {"docs": {"models.Course.study_groups": {"tf": 1}, "models.UserAvailability.study_session_id": {"tf": 1}, "models.StudySessionInvitee.study_session_id": {"tf": 1}}, "df": 3, "g": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "p": {"docs": {"models.StudyGroup": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudyGroup.id": {"tf": 1}, "models.StudyGroup.course_id": {"tf": 1}, "models.StudyGroup.name": {"tf": 1}, "models.StudyGroup.created_at": {"tf": 1}, "models.StudyGroup.course": {"tf": 1}, "models.StudyGroup.members": {"tf": 1}, "models.StudyGroup.sessions": {"tf": 1}}, "df": 9, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"models.StudyGroupMember": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.StudyGroupMember.id": {"tf": 1}, "models.StudyGroupMember.group_id": {"tf": 1}, "models.StudyGroupMember.user_email": {"tf": 1}, "models.StudyGroupMember.joined_at": {"tf": 1}, "models.StudyGroupMember.group": {"tf": 1}}, "df": 7}}}}}}}}}}}, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"models.StudySession": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudySession.id": {"tf": 1}, "models.StudySession.course_id": {"tf": 1}, "models.StudySession.creator_email": {"tf": 1}, "models.StudySession.session_type": {"tf": 1}, "models.StudySession.title": {"tf": 1}, "models.StudySession.starts_at": {"tf": 1}, "models.StudySession.ends_at": {"tf": 1}, "models.StudySession.group_id": {"tf": 1}, "models.StudySession.created_at": {"tf": 1}, "models.StudySession.group": {"tf": 1}, "models.StudySession.invitees": {"tf": 1}}, "df": 13, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "e": {"docs": {"models.StudySessionInvitee": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}, "models.StudySessionInvitee.id": {"tf": 1}, "models.StudySessionInvitee.study_session_id": {"tf": 1}, "models.StudySessionInvitee.user_email": {"tf": 1}, "models.StudySessionInvitee.created_at": {"tf": 1}, "models.StudySessionInvitee.session": {"tf": 1}}, "df": 7}}}}}}}}}}}}}}}}}, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "s": {"docs": {"models.StudySession.starts_at": {"tf": 1}, "models.UserAvailability.starts_at": {"tf": 1}}, "df": 2}}}}}, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"models.Message.sender_id": {"tf": 1}, "models.Message.sender": {"tf": 1}}, "df": 2}}}}, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"models.StudySession.session_type": {"tf": 1}, "models.UserAvailability.study_session_id": {"tf": 1}, "models.StudySessionInvitee.study_session_id": {"tf": 1}, "models.StudySessionInvitee.session": {"tf": 1}}, "df": 4, "s": {"docs": {"models.StudyGroup.sessions": {"tf": 1}}, "df": 1}}}}}}}, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {"models.Post.score": {"tf": 1}}, "df": 1}}}}, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"models.UserAvailability.source": {"tf": 1}}, "df": 1}}}}}}, "t": {"docs": {}, "df": 0, "a": {"docs": {"models.UserRole.TA": {"tf": 1}}, "df": 1}, "o": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {"models.User.google_calendar_token": {"tf": 1}}, "df": 1}}}}, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"models.Post.title": {"tf": 1}, "models.StudySession.title": {"tf": 1}}, "df": 2}}}}, "y": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {"models.StudySession.session_type": {"tf": 1}}, "df": 1}}}}, "a": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {"models.UserRole.ADMIN": {"tf": 1}}, "df": 1}}}}, "t": {"docs": {"models.User.created_at": {"tf": 1}, "models.Course.created_at": {"tf": 1}, "models.Enrollment.enrolled_at": {"tf": 1}, "models.Conversation.created_at": {"tf": 1}, "models.ConversationParticipant.joined_at": {"tf": 1}, "models.Message.created_at": {"tf": 1}, "models.Post.created_at": {"tf": 1}, "models.StudyGroup.created_at": {"tf": 1}, "models.StudySession.starts_at": {"tf": 1}, "models.StudySession.ends_at": {"tf": 1}, "models.StudySession.created_at": {"tf": 1}, "models.StudyGroupMember.joined_at": {"tf": 1}, "models.UserAvailability.starts_at": {"tf": 1}, "models.UserAvailability.ends_at": {"tf": 1}, "models.UserAvailability.created_at": {"tf": 1}, "models.StudySessionInvitee.created_at": {"tf": 1}}, "df": 16}, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"models.Post.author_uid": {"tf": 1}, "models.Post.author": {"tf": 1}}, "df": 2}}}}}}, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"models.User.firebase_uid": {"tf": 1}}, "df": 1}}}}}}}, "u": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {"models.User.full_name": {"tf": 1}}, "df": 1}}}, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"models.Post.is_flagged": {"tf": 1}}, "df": 1}}}}}}}, "e": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {"models.User.email": {"tf": 1}, "models.StudySession.creator_email": {"tf": 1}, "models.StudyGroupMember.user_email": {"tf": 1}, "models.UserAvailability.user_email": {"tf": 1}, "models.StudySessionInvitee.user_email": {"tf": 1}}, "df": 5}}}}, "n": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"models.Enrollment": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Enrollment.id": {"tf": 1}, "models.Enrollment.user_id": {"tf": 1}, "models.Enrollment.course_id": {"tf": 1}, "models.Enrollment.enrolled_at": {"tf": 1}, "models.Enrollment.user": {"tf": 1}, "models.Enrollment.course": {"tf": 1}}, "df": 8, "s": {"docs": {"models.User.enrollments": {"tf": 1}}, "df": 1}}}}}, "e": {"docs": {}, "df": 0, "d": {"docs": {"models.Enrollment.enrolled_at": {"tf": 1}}, "df": 1}}}}}}, "d": {"docs": {}, "df": 0, "s": {"docs": {"models.StudySession.ends_at": {"tf": 1}, "models.UserAvailability.ends_at": {"tf": 1}}, "df": 2}}}}, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {"models.User.full_name": {"tf": 1}, "models.Course.name": {"tf": 1}, "models.Conversation.group_name": {"tf": 1}, "models.StudyGroup.name": {"tf": 1}}, "df": 4}}}}, "c": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {"models.User.google_calendar_token": {"tf": 1}}, "df": 1}}}}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"models.User.created_at": {"tf": 1}, "models.User.courses_created": {"tf": 1}, "models.Course.created_at": {"tf": 1}, "models.Conversation.created_at": {"tf": 1}, "models.Message.created_at": {"tf": 1}, "models.Post.created_at": {"tf": 1}, "models.StudyGroup.created_at": {"tf": 1}, "models.StudySession.created_at": {"tf": 1}, "models.UserAvailability.created_at": {"tf": 1}, "models.StudySessionInvitee.created_at": {"tf": 1}}, "df": 10}}, "o": {"docs": {}, "df": 0, "r": {"docs": {"models.StudySession.creator_email": {"tf": 1}}, "df": 1}}}}}}, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"models.User.conversation_participants": {"tf": 1}, "models.Conversation": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.Conversation.conversation_id": {"tf": 1.4142135623730951}, "models.Conversation.course_id": {"tf": 1}, "models.Conversation.is_group": {"tf": 1}, "models.Conversation.group_name": {"tf": 1}, "models.Conversation.created_at": {"tf": 1}, "models.Conversation.course": {"tf": 1}, "models.Conversation.participants": {"tf": 1}, "models.Conversation.messages": {"tf": 1}, "models.ConversationParticipant.conversation_id": {"tf": 1}, "models.ConversationParticipant.conversation": {"tf": 1}, "models.Message.conversation_id": {"tf": 1}, "models.Message.conversation": {"tf": 1}}, "df": 15, "s": {"docs": {"models.Course.conversations": {"tf": 1}}, "df": 1}, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"models.ConversationParticipant": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.ConversationParticipant.participant_id": {"tf": 1}, "models.ConversationParticipant.conversation_id": {"tf": 1}, "models.ConversationParticipant.user_id": {"tf": 1}, "models.ConversationParticipant.joined_at": {"tf": 1}, "models.ConversationParticipant.conversation": {"tf": 1}, "models.ConversationParticipant.user": {"tf": 1}}, "df": 8}}}}}}}}}}}}}}}}}}}}, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"models.Message.content": {"tf": 1}}, "df": 1}}}}}, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"models.Course": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Course.id": {"tf": 1}, "models.Course.course_code": {"tf": 1.4142135623730951}, "models.Course.name": {"tf": 1}, "models.Course.description": {"tf": 1}, "models.Course.owner_id": {"tf": 1}, "models.Course.created_at": {"tf": 1}, "models.Course.owner": {"tf": 1}, "models.Course.members": {"tf": 1}, "models.Course.posts": {"tf": 1}, "models.Course.study_groups": {"tf": 1}, "models.Course.conversations": {"tf": 1}, "models.Enrollment.course_id": {"tf": 1}, "models.Enrollment.course": {"tf": 1}, "models.Conversation.course_id": {"tf": 1}, "models.Conversation.course": {"tf": 1}, "models.Post.course_id": {"tf": 1}, "models.Post.course": {"tf": 1}, "models.StudyGroup.course_id": {"tf": 1}, "models.StudyGroup.course": {"tf": 1}, "models.StudySession.course_id": {"tf": 1}}, "df": 22, "s": {"docs": {"models.User.courses_created": {"tf": 1}}, "df": 1}}}}}, "d": {"docs": {}, "df": 0, "e": {"docs": {"models.Course.course_code": {"tf": 1}}, "df": 1}}}}, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"models.ConversationParticipant.participant_id": {"tf": 1}}, "df": 1, "s": {"docs": {"models.User.conversation_participants": {"tf": 1}, "models.Conversation.participants": {"tf": 1}}, "df": 2}}}}}}}}}}}, "o": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"models.Post": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.Post.id": {"tf": 1}, "models.Post.course_id": {"tf": 1}, "models.Post.author_uid": {"tf": 1}, "models.Post.title": {"tf": 1}, "models.Post.description": {"tf": 1}, "models.Post.resource_link": {"tf": 1}, "models.Post.score": {"tf": 1}, "models.Post.is_flagged": {"tf": 1}, "models.Post.created_at": {"tf": 1}, "models.Post.author": {"tf": 1}, "models.Post.course": {"tf": 1}, "models.Post.votes": {"tf": 1}, "models.PostVote.post_id": {"tf": 1}, "models.PostVote.post": {"tf": 1}}, "df": 16, "s": {"docs": {"models.User.posts": {"tf": 1}, "models.Course.posts": {"tf": 1}}, "df": 2}, "v": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"models.PostVote": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.PostVote.id": {"tf": 1}, "models.PostVote.post_id": {"tf": 1}, "models.PostVote.user_uid": {"tf": 1}, "models.PostVote.vote": {"tf": 1}, "models.PostVote.post": {"tf": 1}}, "df": 7}}}}}}}}, "o": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"models.Course.owner_id": {"tf": 1}, "models.Course.owner": {"tf": 1}}, "df": 2}}}}}, "j": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"models.ConversationParticipant.joined_at": {"tf": 1}, "models.StudyGroupMember.joined_at": {"tf": 1}}, "df": 2}}}}}}, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "k": {"docs": {"models.Post.resource_link": {"tf": 1}}, "df": 1}}}}, "v": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"models.PostVote.vote": {"tf": 1}}, "df": 1, "s": {"docs": {"models.Post.votes": {"tf": 1}}, "df": 1}}}}}}}, "annotation": {"root": {"docs": {"database.Base.registry": {"tf": 1}, "database.Base.metadata": {"tf": 1}}, "df": 2, "s": {"docs": {}, "df": 0, "q": {"docs": {}, "df": 0, "l": {"docs": {"database.Base.metadata": {"tf": 1}}, "df": 1, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "y": {"docs": {"database.Base.registry": {"tf": 1}, "database.Base.metadata": {"tf": 1}}, "df": 2}}}}}}}}}, "c": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "a": {"docs": {"database.Base.metadata": {"tf": 1}}, "df": 1}}}}}}, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "m": {"docs": {"database.Base.registry": {"tf": 1}}, "df": 1}}}, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "l": {"docs": {"database.Base.registry": {"tf": 1}}, "df": 1}}}}, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {"database.Base.registry": {"tf": 1}}, "df": 1}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "y": {"docs": {"database.Base.registry": {"tf": 1}}, "df": 1}}}}}}}}, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {"database.Base.metadata": {"tf": 1}}, "df": 1}}}}}}}}}}, "default_value": {"root": {"docs": {"database.Base.registry": {"tf": 1.4142135623730951}, "database.Base.metadata": {"tf": 1}, "models.UserRole.STUDENT": {"tf": 1.4142135623730951}, "models.UserRole.TA": {"tf": 1.4142135623730951}, "models.UserRole.ADMIN": {"tf": 1.4142135623730951}}, "df": 5, "l": {"docs": {}, "df": 0, "t": {"docs": {"database.Base.registry": {"tf": 1}, "models.UserRole.STUDENT": {"tf": 1}, "models.UserRole.TA": {"tf": 1}, "models.UserRole.ADMIN": {"tf": 1}}, "df": 4}}, "s": {"docs": {}, "df": 0, "q": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "y": {"docs": {"database.Base.registry": {"tf": 1}}, "df": 1}}}}}}}}}, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"models.UserRole.STUDENT": {"tf": 1.4142135623730951}}, "df": 1}}}}}}}, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "m": {"docs": {"database.Base.registry": {"tf": 1}}, "df": 1}}, "b": {"docs": {}, "df": 0, "j": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"database.Base.registry": {"tf": 1}}, "df": 1}}}}}}, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "l": {"docs": {"database.Base.registry": {"tf": 1}}, "df": 1}}}}, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {"database.Base.registry": {"tf": 1}}, "df": 1}}, "d": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {"models.UserRole.ADMIN": {"tf": 1.4142135623730951}}, "df": 1}}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "y": {"docs": {"database.Base.registry": {"tf": 1}}, "df": 1}}}}}}}}, "g": {"docs": {}, "df": 0, "t": {"docs": {"database.Base.registry": {"tf": 1}, "models.UserRole.STUDENT": {"tf": 1}, "models.UserRole.TA": {"tf": 1}, "models.UserRole.ADMIN": {"tf": 1}}, "df": 4}}, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {"database.Base.metadata": {"tf": 1}}, "df": 1}}}}}}}}, "u": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"models.UserRole.STUDENT": {"tf": 1}, "models.UserRole.TA": {"tf": 1}, "models.UserRole.ADMIN": {"tf": 1}}, "df": 3}}}}}}}}, "x": {"2": {"7": {"docs": {"models.UserRole.STUDENT": {"tf": 1.4142135623730951}, "models.UserRole.TA": {"tf": 1.4142135623730951}, "models.UserRole.ADMIN": {"tf": 1.4142135623730951}}, "df": 3}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "t": {"docs": {}, "df": 0, "a": {"docs": {"models.UserRole.TA": {"tf": 1.4142135623730951}}, "df": 1}}}}, "signature": {"root": {"docs": {"database.Base.__init__": {"tf": 3.7416573867739413}, "database.get_db": {"tf": 2.6457513110645907}, "models.User.__init__": {"tf": 3.1622776601683795}, "models.Course.__init__": {"tf": 3.1622776601683795}, "models.Enrollment.__init__": {"tf": 3.1622776601683795}, "models.Conversation.__init__": {"tf": 3.1622776601683795}, "models.ConversationParticipant.__init__": {"tf": 3.1622776601683795}, "models.Message.__init__": {"tf": 3.1622776601683795}, "models.Post.__init__": {"tf": 3.1622776601683795}, "models.PostVote.__init__": {"tf": 3.1622776601683795}, "models.StudyGroup.__init__": {"tf": 3.1622776601683795}, "models.StudySession.__init__": {"tf": 3.1622776601683795}, "models.StudyGroupMember.__init__": {"tf": 3.1622776601683795}, "models.UserAvailability.__init__": {"tf": 3.1622776601683795}, "models.StudySessionInvitee.__init__": {"tf": 3.1622776601683795}}, "df": 15, "k": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "s": {"docs": {"database.Base.__init__": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 14}}}}}}, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "y": {"docs": {"database.Base.__init__": {"tf": 1}}, "df": 1}}}}}, "bases": {"root": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "q": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "y": {"docs": {"database.Base": {"tf": 1.4142135623730951}, "models.User": {"tf": 1.4142135623730951}, "models.Course": {"tf": 1.4142135623730951}, "models.Enrollment": {"tf": 1.4142135623730951}, "models.Conversation": {"tf": 1.4142135623730951}, "models.ConversationParticipant": {"tf": 1.4142135623730951}, "models.Message": {"tf": 1.4142135623730951}, "models.Post": {"tf": 1.4142135623730951}, "models.PostVote": {"tf": 1.4142135623730951}, "models.StudyGroup": {"tf": 1.4142135623730951}, "models.StudySession": {"tf": 1.4142135623730951}, "models.StudyGroupMember": {"tf": 1.4142135623730951}, "models.UserAvailability": {"tf": 1.4142135623730951}, "models.StudySessionInvitee": {"tf": 1.4142135623730951}}, "df": 14}}}}}}}}}, "t": {"docs": {}, "df": 0, "r": {"docs": {"models.UserRole": {"tf": 1}}, "df": 1}}}, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "m": {"docs": {"database.Base": {"tf": 1.4142135623730951}, "models.User": {"tf": 1.4142135623730951}, "models.Course": {"tf": 1.4142135623730951}, "models.Enrollment": {"tf": 1.4142135623730951}, "models.Conversation": {"tf": 1.4142135623730951}, "models.ConversationParticipant": {"tf": 1.4142135623730951}, "models.Message": {"tf": 1.4142135623730951}, "models.Post": {"tf": 1.4142135623730951}, "models.PostVote": {"tf": 1.4142135623730951}, "models.StudyGroup": {"tf": 1.4142135623730951}, "models.StudySession": {"tf": 1.4142135623730951}, "models.StudyGroupMember": {"tf": 1.4142135623730951}, "models.UserAvailability": {"tf": 1.4142135623730951}, "models.StudySessionInvitee": {"tf": 1.4142135623730951}}, "df": 14}}}, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "l": {"docs": {"database.Base": {"tf": 1}, "models.User": {"tf": 1}, "models.Course": {"tf": 1}, "models.Enrollment": {"tf": 1}, "models.Conversation": {"tf": 1}, "models.ConversationParticipant": {"tf": 1}, "models.Message": {"tf": 1}, "models.Post": {"tf": 1}, "models.PostVote": {"tf": 1}, "models.StudyGroup": {"tf": 1}, "models.StudySession": {"tf": 1}, "models.StudyGroupMember": {"tf": 1}, "models.UserAvailability": {"tf": 1}, "models.StudySessionInvitee": {"tf": 1}}, "df": 14}}}, "y": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {"database.Base": {"tf": 1}, "models.User": {"tf": 1}, "models.Course": {"tf": 1}, "models.Enrollment": {"tf": 1}, "models.Conversation": {"tf": 1}, "models.ConversationParticipant": {"tf": 1}, "models.Message": {"tf": 1}, "models.Post": {"tf": 1}, "models.PostVote": {"tf": 1}, "models.StudyGroup": {"tf": 1}, "models.StudySession": {"tf": 1}, "models.StudyGroupMember": {"tf": 1}, "models.UserAvailability": {"tf": 1}, "models.StudySessionInvitee": {"tf": 1}}, "df": 14}}}}}}}}}}}}}}}}}}}}}, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {"database.Base": {"tf": 1}, "models.User": {"tf": 1}, "models.Course": {"tf": 1}, "models.Enrollment": {"tf": 1}, "models.Conversation": {"tf": 1}, "models.ConversationParticipant": {"tf": 1}, "models.Message": {"tf": 1}, "models.Post": {"tf": 1}, "models.PostVote": {"tf": 1}, "models.StudyGroup": {"tf": 1}, "models.StudySession": {"tf": 1}, "models.StudyGroupMember": {"tf": 1}, "models.UserAvailability": {"tf": 1}, "models.StudySessionInvitee": {"tf": 1}}, "df": 14}}, "n": {"docs": {}, "df": 0, "y": {"docs": {"database.Base": {"tf": 1}, "models.User": {"tf": 1}, "models.Course": {"tf": 1}, "models.Enrollment": {"tf": 1}, "models.Conversation": {"tf": 1}, "models.ConversationParticipant": {"tf": 1}, "models.Message": {"tf": 1}, "models.Post": {"tf": 1}, "models.PostVote": {"tf": 1}, "models.StudyGroup": {"tf": 1}, "models.StudySession": {"tf": 1}, "models.StudyGroupMember": {"tf": 1}, "models.UserAvailability": {"tf": 1}, "models.StudySessionInvitee": {"tf": 1}}, "df": 14}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"database.Base": {"tf": 1}, "models.User": {"tf": 1}, "models.Course": {"tf": 1}, "models.Enrollment": {"tf": 1}, "models.Conversation": {"tf": 1}, "models.ConversationParticipant": {"tf": 1}, "models.Message": {"tf": 1}, "models.Post": {"tf": 1}, "models.PostVote": {"tf": 1}, "models.StudyGroup": {"tf": 1}, "models.StudySession": {"tf": 1}, "models.StudyGroupMember": {"tf": 1}, "models.UserAvailability": {"tf": 1}, "models.StudySessionInvitee": {"tf": 1}}, "df": 14}}}, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "[": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "q": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "y": {"docs": {"database.Base": {"tf": 1}, "models.User": {"tf": 1}, "models.Course": {"tf": 1}, "models.Enrollment": {"tf": 1}, "models.Conversation": {"tf": 1}, "models.ConversationParticipant": {"tf": 1}, "models.Message": {"tf": 1}, "models.Post": {"tf": 1}, "models.PostVote": {"tf": 1}, "models.StudyGroup": {"tf": 1}, "models.StudySession": {"tf": 1}, "models.StudyGroupMember": {"tf": 1}, "models.UserAvailability": {"tf": 1}, "models.StudySessionInvitee": {"tf": 1}}, "df": 14}}}}}}}}}}}}}}}}}}}}}}, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"database.Base": {"tf": 1}, "models.User": {"tf": 1}, "models.Course": {"tf": 1}, "models.Enrollment": {"tf": 1}, "models.Conversation": {"tf": 1}, "models.ConversationParticipant": {"tf": 1}, "models.Message": {"tf": 1}, "models.Post": {"tf": 1}, "models.PostVote": {"tf": 1}, "models.StudyGroup": {"tf": 1}, "models.StudySession": {"tf": 1}, "models.StudyGroupMember": {"tf": 1}, "models.UserAvailability": {"tf": 1}, "models.StudySessionInvitee": {"tf": 1}}, "df": 14, "[": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"database.Base": {"tf": 1}, "models.User": {"tf": 1}, "models.Course": {"tf": 1}, "models.Enrollment": {"tf": 1}, "models.Conversation": {"tf": 1}, "models.ConversationParticipant": {"tf": 1}, "models.Message": {"tf": 1}, "models.Post": {"tf": 1}, "models.PostVote": {"tf": 1}, "models.StudyGroup": {"tf": 1}, "models.StudySession": {"tf": 1}, "models.StudyGroupMember": {"tf": 1}, "models.UserAvailability": {"tf": 1}, "models.StudySessionInvitee": {"tf": 1}}, "df": 14}}}}}}}}}}}}}, "b": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {"models.UserRole": {"tf": 1}}, "df": 1}}}}}}}}, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "m": {"docs": {"models.UserRole": {"tf": 1.4142135623730951}}, "df": 1}}}}}}, "doc": {"root": {"1": {"docs": {"models.PostVote": {"tf": 1.4142135623730951}}, "df": 1, ":": {"1": {"docs": {"models.Conversation": {"tf": 1}}, "df": 1}, "docs": {}, "df": 0}}, "docs": {"database": {"tf": 1.7320508075688772}, "database.Base": {"tf": 2.449489742783178}, "database.Base.__init__": {"tf": 3.3166247903554}, "database.Base.registry": {"tf": 1.7320508075688772}, "database.Base.metadata": {"tf": 1.7320508075688772}, "database.get_db": {"tf": 2.449489742783178}, "models": {"tf": 8.12403840463596}, "models.UserRole": {"tf": 2.23606797749979}, "models.UserRole.STUDENT": {"tf": 1.7320508075688772}, "models.UserRole.TA": {"tf": 1.7320508075688772}, "models.UserRole.ADMIN": {"tf": 1.7320508075688772}, "models.User": {"tf": 2.8284271247461903}, "models.User.__init__": {"tf": 3.3166247903554}, "models.User.firebase_uid": {"tf": 1.7320508075688772}, "models.User.email": {"tf": 1.7320508075688772}, "models.User.full_name": {"tf": 1.7320508075688772}, "models.User.role": {"tf": 1.7320508075688772}, "models.User.google_calendar_token": {"tf": 1.7320508075688772}, "models.User.created_at": {"tf": 1.7320508075688772}, "models.User.messages": {"tf": 1.7320508075688772}, "models.User.conversation_participants": {"tf": 1.7320508075688772}, "models.User.courses_created": {"tf": 1.7320508075688772}, "models.User.posts": {"tf": 1.7320508075688772}, "models.User.enrollments": {"tf": 1.7320508075688772}, "models.Course": {"tf": 1.7320508075688772}, "models.Course.__init__": {"tf": 3.3166247903554}, "models.Course.id": {"tf": 1.7320508075688772}, "models.Course.course_code": {"tf": 1.7320508075688772}, "models.Course.name": {"tf": 1.7320508075688772}, "models.Course.description": {"tf": 1.7320508075688772}, "models.Course.owner_id": {"tf": 1.7320508075688772}, "models.Course.created_at": {"tf": 1.7320508075688772}, "models.Course.owner": {"tf": 1.7320508075688772}, "models.Course.members": {"tf": 1.7320508075688772}, "models.Course.posts": {"tf": 1.7320508075688772}, "models.Course.study_groups": {"tf": 1.7320508075688772}, "models.Course.conversations": {"tf": 1.7320508075688772}, "models.Enrollment": {"tf": 2.6457513110645907}, "models.Enrollment.__init__": {"tf": 3.3166247903554}, "models.Enrollment.id": {"tf": 1.7320508075688772}, "models.Enrollment.user_id": {"tf": 1.7320508075688772}, "models.Enrollment.course_id": {"tf": 1.7320508075688772}, "models.Enrollment.enrolled_at": {"tf": 1.7320508075688772}, "models.Enrollment.user": {"tf": 1.7320508075688772}, "models.Enrollment.course": {"tf": 1.7320508075688772}, "models.Conversation": {"tf": 3.1622776601683795}, "models.Conversation.__init__": {"tf": 3.3166247903554}, "models.Conversation.conversation_id": {"tf": 1.7320508075688772}, "models.Conversation.course_id": {"tf": 1.7320508075688772}, "models.Conversation.is_group": {"tf": 1.7320508075688772}, "models.Conversation.group_name": {"tf": 1.7320508075688772}, "models.Conversation.created_at": {"tf": 1.7320508075688772}, "models.Conversation.course": {"tf": 1.7320508075688772}, "models.Conversation.participants": {"tf": 1.7320508075688772}, "models.Conversation.messages": {"tf": 1.7320508075688772}, "models.ConversationParticipant": {"tf": 1.7320508075688772}, "models.ConversationParticipant.__init__": {"tf": 3.3166247903554}, "models.ConversationParticipant.participant_id": {"tf": 1.7320508075688772}, "models.ConversationParticipant.conversation_id": {"tf": 1.7320508075688772}, "models.ConversationParticipant.user_id": {"tf": 1.7320508075688772}, "models.ConversationParticipant.joined_at": {"tf": 1.7320508075688772}, "models.ConversationParticipant.conversation": {"tf": 1.7320508075688772}, "models.ConversationParticipant.user": {"tf": 1.7320508075688772}, "models.Message": {"tf": 2.23606797749979}, "models.Message.__init__": {"tf": 3.3166247903554}, "models.Message.message_id": {"tf": 1.7320508075688772}, "models.Message.conversation_id": {"tf": 1.7320508075688772}, "models.Message.sender_id": {"tf": 1.7320508075688772}, "models.Message.content": {"tf": 1.7320508075688772}, "models.Message.created_at": {"tf": 1.7320508075688772}, "models.Message.conversation": {"tf": 1.7320508075688772}, "models.Message.sender": {"tf": 1.7320508075688772}, "models.Post": {"tf": 3.4641016151377544}, "models.Post.__init__": {"tf": 3.3166247903554}, "models.Post.id": {"tf": 1.7320508075688772}, "models.Post.course_id": {"tf": 1.7320508075688772}, "models.Post.author_uid": {"tf": 1.7320508075688772}, "models.Post.title": {"tf": 1.7320508075688772}, "models.Post.description": {"tf": 1.7320508075688772}, "models.Post.resource_link": {"tf": 1.7320508075688772}, "models.Post.score": {"tf": 1.7320508075688772}, "models.Post.is_flagged": {"tf": 1.7320508075688772}, "models.Post.created_at": {"tf": 1.7320508075688772}, "models.Post.author": {"tf": 1.7320508075688772}, "models.Post.course": {"tf": 1.7320508075688772}, "models.Post.votes": {"tf": 1.7320508075688772}, "models.PostVote": {"tf": 2.23606797749979}, "models.PostVote.__init__": {"tf": 3.3166247903554}, "models.PostVote.id": {"tf": 1.7320508075688772}, "models.PostVote.post_id": {"tf": 1.7320508075688772}, "models.PostVote.user_uid": {"tf": 1.7320508075688772}, "models.PostVote.vote": {"tf": 1.7320508075688772}, "models.PostVote.post": {"tf": 1.7320508075688772}, "models.StudyGroup": {"tf": 3.1622776601683795}, "models.StudyGroup.__init__": {"tf": 3.3166247903554}, "models.StudyGroup.id": {"tf": 1.7320508075688772}, "models.StudyGroup.course_id": {"tf": 1.7320508075688772}, "models.StudyGroup.name": {"tf": 1.7320508075688772}, "models.StudyGroup.created_at": {"tf": 1.7320508075688772}, "models.StudyGroup.course": {"tf": 1.7320508075688772}, "models.StudyGroup.members": {"tf": 1.7320508075688772}, "models.StudyGroup.sessions": {"tf": 1.7320508075688772}, "models.StudySession": {"tf": 4.69041575982343}, "models.StudySession.__init__": {"tf": 3.3166247903554}, "models.StudySession.id": {"tf": 1.7320508075688772}, "models.StudySession.course_id": {"tf": 1.7320508075688772}, "models.StudySession.creator_email": {"tf": 1.7320508075688772}, "models.StudySession.session_type": {"tf": 1.7320508075688772}, "models.StudySession.title": {"tf": 1.7320508075688772}, "models.StudySession.starts_at": {"tf": 1.7320508075688772}, "models.StudySession.ends_at": {"tf": 1.7320508075688772}, "models.StudySession.group_id": {"tf": 1.7320508075688772}, "models.StudySession.created_at": {"tf": 1.7320508075688772}, "models.StudySession.group": {"tf": 1.7320508075688772}, "models.StudySession.invitees": {"tf": 1.7320508075688772}, "models.StudyGroupMember": {"tf": 2.23606797749979}, "models.StudyGroupMember.__init__": {"tf": 3.3166247903554}, "models.StudyGroupMember.id": {"tf": 1.7320508075688772}, "models.StudyGroupMember.group_id": {"tf": 1.7320508075688772}, "models.StudyGroupMember.user_email": {"tf": 1.7320508075688772}, "models.StudyGroupMember.joined_at": {"tf": 1.7320508075688772}, "models.StudyGroupMember.group": {"tf": 1.7320508075688772}, "models.UserAvailability": {"tf": 3}, "models.UserAvailability.__init__": {"tf": 3.3166247903554}, "models.UserAvailability.id": {"tf": 1.7320508075688772}, "models.UserAvailability.user_email": {"tf": 1.7320508075688772}, "models.UserAvailability.starts_at": {"tf": 1.7320508075688772}, "models.UserAvailability.ends_at": {"tf": 1.7320508075688772}, "models.UserAvailability.source": {"tf": 1.7320508075688772}, "models.UserAvailability.study_session_id": {"tf": 1.7320508075688772}, "models.UserAvailability.created_at": {"tf": 1.7320508075688772}, "models.StudySessionInvitee": {"tf": 2.23606797749979}, "models.StudySessionInvitee.__init__": {"tf": 3.3166247903554}, "models.StudySessionInvitee.id": {"tf": 1.7320508075688772}, "models.StudySessionInvitee.study_session_id": {"tf": 1.7320508075688772}, "models.StudySessionInvitee.user_email": {"tf": 1.7320508075688772}, "models.StudySessionInvitee.created_at": {"tf": 1.7320508075688772}, "models.StudySessionInvitee.session": {"tf": 1.7320508075688772}}, "df": 138, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "e": {"docs": {"database.Base": {"tf": 1.4142135623730951}, "database.Base.__init__": {"tf": 1.7320508075688772}, "database.get_db": {"tf": 1.4142135623730951}, "models.User": {"tf": 1}, "models.User.__init__": {"tf": 1.7320508075688772}, "models.Course.__init__": {"tf": 1.7320508075688772}, "models.Enrollment.__init__": {"tf": 1.7320508075688772}, "models.Conversation.__init__": {"tf": 1.7320508075688772}, "models.ConversationParticipant.__init__": {"tf": 1.7320508075688772}, "models.Message.__init__": {"tf": 1.7320508075688772}, "models.Post.__init__": {"tf": 1.7320508075688772}, "models.PostVote.__init__": {"tf": 1.7320508075688772}, "models.StudyGroup": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1.7320508075688772}, "models.StudySession.__init__": {"tf": 1.7320508075688772}, "models.StudyGroupMember.__init__": {"tf": 1.7320508075688772}, "models.UserAvailability": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1.7320508075688772}, "models.StudySessionInvitee": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1.7320508075688772}}, "df": 20, "s": {"docs": {}, "df": 0, "e": {"docs": {"database.Base.__init__": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 14}}, "i": {"docs": {}, "df": 0, "r": {"docs": {"models": {"tf": 1}, "models.User": {"tf": 1}}, "df": 2}}, "y": {"docs": {"models.Enrollment": {"tf": 1}}, "df": 1}}, "a": {"docs": {}, "df": 0, "t": {"docs": {"database.Base": {"tf": 1}, "database.Base.__init__": {"tf": 1.4142135623730951}, "models.User.__init__": {"tf": 1.4142135623730951}, "models.Course": {"tf": 1}, "models.Course.__init__": {"tf": 1.4142135623730951}, "models.Enrollment.__init__": {"tf": 1.4142135623730951}, "models.Conversation.__init__": {"tf": 1.4142135623730951}, "models.ConversationParticipant.__init__": {"tf": 1.4142135623730951}, "models.Message.__init__": {"tf": 1.4142135623730951}, "models.Post.__init__": {"tf": 1.4142135623730951}, "models.PostVote.__init__": {"tf": 1.4142135623730951}, "models.StudyGroup.__init__": {"tf": 1.4142135623730951}, "models.StudySession.__init__": {"tf": 1.4142135623730951}, "models.StudyGroupMember.__init__": {"tf": 1.4142135623730951}, "models.UserAvailability.__init__": {"tf": 1.4142135623730951}, "models.StudySessionInvitee.__init__": {"tf": 1.4142135623730951}}, "df": 16}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "d": {"docs": {"models.Conversation": {"tf": 1}}, "df": 1}}}}}, "o": {"docs": {"database.get_db": {"tf": 1}, "models.Course": {"tf": 1}, "models.Enrollment": {"tf": 1}, "models.Conversation": {"tf": 1}, "models.StudySession": {"tf": 1}, "models.UserAvailability": {"tf": 1.4142135623730951}, "models.StudySessionInvitee": {"tf": 1}}, "df": 7, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {"models.User": {"tf": 1}}, "df": 1}}}}, "a": {"docs": {"models": {"tf": 1}}, "df": 1, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"models.Enrollment": {"tf": 1}}, "df": 1, "s": {"docs": {"models.User": {"tf": 1}}, "df": 1}}}}, "l": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "y": {"docs": {"models.Post": {"tf": 1}}, "df": 1}}}}, "y": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "y": {"docs": {"models": {"tf": 1}}, "df": 1}}}}}}, "e": {"docs": {"models.StudySession": {"tf": 1}}, "df": 1}}}, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"models.StudySession": {"tf": 1}, "models.UserAvailability": {"tf": 1}}, "df": 2}}, "m": {"docs": {}, "df": 0, "e": {"docs": {"models.UserAvailability": {"tf": 1}}, "df": 1, "s": {"docs": {"models.UserAvailability": {"tf": 1}}, "df": 1}}}}}, "b": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"database.Base": {"tf": 1}}, "df": 1}, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "s": {"docs": {"models.User": {"tf": 1}}, "df": 1}}}}}, "e": {"docs": {"database.Base": {"tf": 1}, "database.Base.__init__": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 17}, "u": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "/": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "e": {"docs": {"models": {"tf": 1}, "models.UserAvailability": {"tf": 1}}, "df": 2}}}}}}}}, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "k": {"docs": {"models.UserAvailability": {"tf": 1}}, "df": 1, "s": {"docs": {"models": {"tf": 1}, "models.StudySession": {"tf": 1}, "models.UserAvailability": {"tf": 1}}, "df": 3}}}}}, "y": {"docs": {"models.User": {"tf": 1.4142135623730951}, "models.StudyGroupMember": {"tf": 1}, "models.UserAvailability": {"tf": 1}}, "df": 3}}, "c": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {"database.Base": {"tf": 1.4142135623730951}, "database.Base.__init__": {"tf": 1}, "models": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 16, "/": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"models.Course": {"tf": 1}}, "df": 1}}}}}}}}}}}, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"database.Base": {"tf": 1}}, "df": 1}}}, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {"models": {"tf": 1}, "models.User": {"tf": 1.4142135623730951}, "models.UserAvailability": {"tf": 1.4142135623730951}}, "df": 3}}}}}}, "n": {"docs": {"models.Course": {"tf": 1}, "models.Conversation": {"tf": 1}, "models.ConversationParticipant": {"tf": 1}}, "df": 3, "n": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "t": {"docs": {"database.Base": {"tf": 1}}, "df": 1}}}}}, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"database.Base.__init__": {"tf": null}, "models.User.__init__": {"tf": null}, "models.Course.__init__": {"tf": null}, "models.Enrollment.__init__": {"tf": null}, "models.Conversation.__init__": {"tf": null}, "models.ConversationParticipant.__init__": {"tf": null}, "models.Message.__init__": {"tf": null}, "models.Post.__init__": {"tf": null}, "models.PostVote.__init__": {"tf": null}, "models.StudyGroup.__init__": {"tf": null}, "models.StudySession.__init__": {"tf": null}, "models.StudyGroupMember.__init__": {"tf": null}, "models.UserAvailability.__init__": {"tf": null}, "models.StudySessionInvitee.__init__": {"tf": null}}, "df": 14}}, "e": {"docs": {}, "df": 0, "d": {"docs": {"database.Base.__init__": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 14}}}}}}}}, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"database.get_db": {"tf": 1}}, "df": 1}}}}}}}, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"models": {"tf": 1}, "models.ConversationParticipant": {"tf": 1}, "models.Message": {"tf": 1}}, "df": 3, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"models": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}}}}, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "s": {"docs": {"models.ConversationParticipant": {"tf": 1}}, "df": 1}}}}}}, "u": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "d": {"docs": {"database.Base.__init__": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 14}}, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"models": {"tf": 1.4142135623730951}, "models.Enrollment": {"tf": 1}, "models.Conversation": {"tf": 1.7320508075688772}, "models.Post": {"tf": 1}, "models.StudyGroup": {"tf": 1}}, "df": 5}}}}, "l": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "n": {"docs": {"models.UserRole": {"tf": 1}}, "df": 1, "s": {"docs": {"database.Base.__init__": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 14}}}}}}, "h": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {"models": {"tf": 1}, "models.Conversation": {"tf": 1.4142135623730951}, "models.Message": {"tf": 1}}, "df": 3, "s": {"docs": {"models.Conversation": {"tf": 1}}, "df": 1}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"models.UserAvailability": {"tf": 1}}, "df": 1}}}}}}}, "o": {"docs": {}, "df": 0, "f": {"docs": {"database.Base": {"tf": 1}, "database.Base.__init__": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 17}, "n": {"docs": {"database.Base.__init__": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 15, "l": {"docs": {}, "df": 0, "y": {"docs": {"database.Base.__init__": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 14}}, "e": {"docs": {"models.PostVote": {"tf": 1}}, "df": 1}}, "r": {"docs": {"database.Base.__init__": {"tf": 1}, "models": {"tf": 1.4142135623730951}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 19}, "w": {"docs": {}, "df": 0, "n": {"docs": {"database.get_db": {"tf": 1}}, "df": 1, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "/": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "p": {"docs": {"models.User": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}}}, "p": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"models.User": {"tf": 1}}, "df": 1, "l": {"docs": {}, "df": 0, "y": {"docs": {"models.Conversation": {"tf": 1}}, "df": 1}}}}}}}}}, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"models.User": {"tf": 1}}, "df": 1}}}}, "u": {"docs": {}, "df": 0, "t": {"docs": {"models.Conversation": {"tf": 1}}, "df": 1}}, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "p": {"docs": {"models.UserAvailability": {"tf": 1}}, "df": 1}}}}}}}, "h": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "y": {"docs": {"database.Base": {"tf": 1}}, "df": 1}}}}}}}, "g": {"docs": {}, "df": 0, "h": {"docs": {"models": {"tf": 1}}, "df": 1}}, "d": {"docs": {}, "df": 0, "e": {"docs": {"models.Post": {"tf": 1}}, "df": 1}}}, "a": {"docs": {}, "df": 0, "s": {"docs": {"database.Base": {"tf": 1}}, "df": 1}}, "o": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "s": {"docs": {"models.User": {"tf": 1}, "models.StudyGroup": {"tf": 1}}, "df": 2}}}}}, "w": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {"database.Base": {"tf": 1}, "models.StudySession": {"tf": 1}, "models.UserAvailability": {"tf": 1}}, "df": 3}}, "o": {"docs": {"models.ConversationParticipant": {"tf": 1}}, "df": 1}}, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {"models.Post": {"tf": 1}}, "df": 1}}, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {"models.Post": {"tf": 1}}, "df": 1}}}}}}}, "i": {"docs": {}, "df": 0, "t": {"docs": {"database.Base": {"tf": 1}, "models.Post": {"tf": 1}}, "df": 2, "s": {"docs": {"database.get_db": {"tf": 1}, "models": {"tf": 1}}, "df": 2, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "f": {"docs": {"models.UserAvailability": {"tf": 1}}, "df": 1}}}}}, "n": {"docs": {"database.Base.__init__": {"tf": 1}, "models.User": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 20, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"database.Base": {"tf": 1.4142135623730951}, "database.Base.__init__": {"tf": 1.4142135623730951}, "models.User.__init__": {"tf": 1.4142135623730951}, "models.Course.__init__": {"tf": 1.4142135623730951}, "models.Enrollment.__init__": {"tf": 1.4142135623730951}, "models.Conversation.__init__": {"tf": 1.4142135623730951}, "models.ConversationParticipant.__init__": {"tf": 1.4142135623730951}, "models.Message.__init__": {"tf": 1.4142135623730951}, "models.Post.__init__": {"tf": 1.4142135623730951}, "models.PostVote.__init__": {"tf": 1.4142135623730951}, "models.StudyGroup.__init__": {"tf": 1.4142135623730951}, "models.StudySession.__init__": {"tf": 1.4142135623730951}, "models.StudyGroupMember.__init__": {"tf": 1.4142135623730951}, "models.UserAvailability.__init__": {"tf": 1.4142135623730951}, "models.StudySessionInvitee.__init__": {"tf": 1.4142135623730951}}, "df": 15}}}}}, "i": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {"models": {"tf": 1}, "models.Message": {"tf": 1}, "models.StudyGroup": {"tf": 1}}, "df": 3}}}}, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "z": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"database.Base.__init__": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 14}}}}}}}}}}}}, "v": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "e": {"docs": {"models.StudySessionInvitee": {"tf": 1}}, "df": 1, "s": {"docs": {"models.StudySession": {"tf": 1}}, "df": 1}}, "d": {"docs": {"models.StudySessionInvitee": {"tf": 1}}, "df": 1}}}}}}, "s": {"docs": {"database.get_db": {"tf": 1}, "models.Conversation": {"tf": 1}, "models.Post": {"tf": 1.4142135623730951}, "models.StudySession": {"tf": 1}}, "df": 4}, "d": {"docs": {"models.Conversation": {"tf": 1}, "models.StudySession": {"tf": 1}}, "df": 2}, "m": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"models.UserAvailability": {"tf": 1}}, "df": 1}}}}}}}}, "a": {"docs": {"database.Base": {"tf": 1}, "database.Base.__init__": {"tf": 1}, "database.get_db": {"tf": 1}, "models": {"tf": 1.7320508075688772}, "models.UserRole": {"tf": 1}, "models.User": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment": {"tf": 1.4142135623730951}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation": {"tf": 1.7320508075688772}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant": {"tf": 1.4142135623730951}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message": {"tf": 1.4142135623730951}, "models.Message.__init__": {"tf": 1}, "models.Post": {"tf": 2}, "models.Post.__init__": {"tf": 1}, "models.PostVote": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup": {"tf": 1.4142135623730951}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession": {"tf": 1.4142135623730951}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember": {"tf": 1.4142135623730951}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability": {"tf": 1.7320508075688772}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee": {"tf": 1.4142135623730951}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 31, "c": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "s": {"docs": {"database.Base": {"tf": 1}}, "df": 1}}}}}}, "r": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "s": {"docs": {"database.Base": {"tf": 1}}, "df": 1}}}}}}}, "e": {"docs": {"database.Base.__init__": {"tf": 1.4142135623730951}, "models.User.__init__": {"tf": 1.4142135623730951}, "models.Course.__init__": {"tf": 1.4142135623730951}, "models.Enrollment": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1.4142135623730951}, "models.Conversation.__init__": {"tf": 1.4142135623730951}, "models.ConversationParticipant.__init__": {"tf": 1.4142135623730951}, "models.Message.__init__": {"tf": 1.4142135623730951}, "models.Post.__init__": {"tf": 1.4142135623730951}, "models.PostVote.__init__": {"tf": 1.4142135623730951}, "models.StudyGroup.__init__": {"tf": 1.4142135623730951}, "models.StudySession.__init__": {"tf": 1.4142135623730951}, "models.StudyGroupMember.__init__": {"tf": 1.4142135623730951}, "models.UserAvailability": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1.4142135623730951}, "models.StudySessionInvitee.__init__": {"tf": 1.4142135623730951}}, "df": 16}}, "n": {"docs": {"models.User": {"tf": 1}, "models.StudySessionInvitee": {"tf": 1}}, "df": 2, "d": {"docs": {"database.Base": {"tf": 1.4142135623730951}, "database.Base.__init__": {"tf": 1}, "database.get_db": {"tf": 1}, "models": {"tf": 1.4142135623730951}, "models.User.__init__": {"tf": 1}, "models.Course": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 19}, "y": {"docs": {"database.Base": {"tf": 1}, "database.Base.__init__": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 16}}, "t": {"docs": {"models.User": {"tf": 1}}, "df": 1, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"database.Base": {"tf": 1}, "database.Base.__init__": {"tf": 1.4142135623730951}, "models.User.__init__": {"tf": 1.4142135623730951}, "models.Course.__init__": {"tf": 1.4142135623730951}, "models.Enrollment.__init__": {"tf": 1.4142135623730951}, "models.Conversation.__init__": {"tf": 1.4142135623730951}, "models.ConversationParticipant.__init__": {"tf": 1.4142135623730951}, "models.Message.__init__": {"tf": 1.4142135623730951}, "models.Post.__init__": {"tf": 1.4142135623730951}, "models.PostVote.__init__": {"tf": 1.4142135623730951}, "models.StudyGroup.__init__": {"tf": 1.4142135623730951}, "models.StudySession.__init__": {"tf": 1.4142135623730951}, "models.StudyGroupMember.__init__": {"tf": 1.4142135623730951}, "models.UserAvailability.__init__": {"tf": 1.4142135623730951}, "models.StudySessionInvitee.__init__": {"tf": 1.4142135623730951}}, "df": 15}}}}}}}}}, "l": {"docs": {}, "df": 0, "l": {"docs": {"models.User": {"tf": 1}}, "df": 1, "o": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "s": {"docs": {"database.Base.__init__": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 14}, "e": {"docs": {}, "df": 0, "d": {"docs": {"database.Base.__init__": {"tf": 1}, "models.UserRole": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 15}}}}}, "w": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "s": {"docs": {"database.get_db": {"tf": 1}}, "df": 1}}}}, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {"models.Post": {"tf": 1}}, "df": 1}}}}}}}}, "s": {"docs": {"database.Base.__init__": {"tf": 1}, "database.get_db": {"tf": 1}, "models.UserRole": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 16}, "d": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {"models": {"tf": 1}}, "df": 1}}}}, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {"models.User": {"tf": 1}}, "df": 1}}}}, "n": {"docs": {}, "df": 0, "o": {"docs": {"database.Base": {"tf": 1.4142135623730951}}, "df": 1, "t": {"docs": {"models.StudySessionInvitee": {"tf": 1}}, "df": 1}}, "e": {"docs": {}, "df": 0, "w": {"docs": {"database.Base": {"tf": 1}}, "df": 1}}, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"database.Base.__init__": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 14}, "d": {"docs": {"models.Conversation": {"tf": 1}}, "df": 1}}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {"database.Base": {"tf": 1}}, "df": 1}, "e": {"docs": {}, "df": 0, "d": {"docs": {"database.get_db": {"tf": 1}}, "df": 1}}}}}}, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "s": {"docs": {"database.Base.__init__": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 14}}}}}}}}}}}, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"database.get_db": {"tf": 1}}, "df": 1}}}}}, "a": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "/": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {"models.ConversationParticipant": {"tf": 1}}, "df": 1}}}}}}}, "s": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"models.Post": {"tf": 1}}, "df": 1}}}}}}, "f": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"models.UserAvailability": {"tf": 1}}, "df": 1}}}}}, "g": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"models.StudySessionInvitee": {"tf": 1}}, "df": 1}}}}}}}}}, "o": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"models": {"tf": 1}}, "df": 1}}}}, "l": {"docs": {}, "df": 0, "e": {"docs": {"models.UserRole": {"tf": 1}}, "df": 1}}, "w": {"docs": {}, "df": 0, "s": {"docs": {"models.Post": {"tf": 1}, "models.UserAvailability": {"tf": 1.4142135623730951}}, "df": 2}}}}, "f": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {"models.User": {"tf": 1}}, "df": 1, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {"database.Base": {"tf": 1}}, "df": 1}}}}}}}}}}, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "m": {"docs": {"database.Base.__init__": {"tf": 1}, "models": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 16}}}, "o": {"docs": {}, "df": 0, "r": {"docs": {"database.Base.__init__": {"tf": 1}, "models.UserRole": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession": {"tf": 1.4142135623730951}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 17, "e": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "n": {"docs": {"models.User": {"tf": 1}}, "df": 1}}}}}}, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {"database.get_db": {"tf": 1}}, "df": 1}}}}}, "l": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"models.Conversation": {"tf": 1}}, "df": 1}}}}, "i": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"models.User": {"tf": 1.4142135623730951}}, "df": 1}}}}}}, "l": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"models.Conversation": {"tf": 1}}, "df": 1}}}}}}, "n": {"docs": {}, "df": 0, "d": {"docs": {"models.UserAvailability": {"tf": 1}}, "df": 1}}}, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"models.Post": {"tf": 1}}, "df": 1}}}}}}}, "g": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {"database.Base": {"tf": 1}}, "df": 1}}}}, "e": {"docs": {}, "df": 0, "t": {"docs": {"database.get_db": {"tf": 1}}, "df": 1, "s": {"docs": {"database.get_db": {"tf": 1}}, "df": 1}}}, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "p": {"docs": {"models": {"tf": 1}, "models.Conversation": {"tf": 1.4142135623730951}, "models.StudyGroup": {"tf": 1}, "models.StudySession": {"tf": 1.7320508075688772}}, "df": 4, "s": {"docs": {"models": {"tf": 1}}, "df": 1}}}}}, "o": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"models": {"tf": 1}, "models.User": {"tf": 1}, "models.UserAvailability": {"tf": 1.4142135623730951}}, "df": 3}}}}}}, "s": {"docs": {"database.Base.__init__": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 16, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"database.Base.__init__": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 14}}}}, "n": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"models.Message": {"tf": 1}}, "df": 1}}}}}, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "s": {"docs": {"database.Base.__init__": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 14}}, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"database.get_db": {"tf": 1}, "models.StudySession": {"tf": 1}}, "df": 2, "s": {"docs": {"models.StudySession": {"tf": 1}}, "df": 1}}}}}}}, "o": {"docs": {"database.get_db": {"tf": 1}, "models.Conversation": {"tf": 1}}, "df": 2, "l": {"docs": {}, "df": 0, "o": {"docs": {"models": {"tf": 1}, "models.StudySession": {"tf": 1}}, "df": 2}}, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"models.UserAvailability": {"tf": 1}}, "df": 1}}}}}, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"models": {"tf": 1}}, "df": 1, "s": {"docs": {"models.Course": {"tf": 1}}, "df": 1}}}}, "y": {"docs": {"models.StudyGroup": {"tf": 1}, "models.StudySession": {"tf": 1.4142135623730951}}, "df": 2, "g": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "p": {"docs": {"models": {"tf": 1}, "models.StudySession": {"tf": 1}, "models.StudyGroupMember": {"tf": 1}}, "df": 3, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"models": {"tf": 1}, "models.StudyGroup": {"tf": 1}}, "df": 2}}}}}}}}}}}, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"models": {"tf": 1}, "models.StudyGroup": {"tf": 1}, "models.UserAvailability": {"tf": 1}, "models.StudySessionInvitee": {"tf": 1}}, "df": 4, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "e": {"docs": {"models": {"tf": 1}, "models.StudySession": {"tf": 1}}, "df": 2}}}}}}}}}}}}}, "y": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {"models.User": {"tf": 1}, "models.UserAvailability": {"tf": 1}}, "df": 2}}}}}}}, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"models.UserRole": {"tf": 1}}, "df": 1}}}}, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"models.UserRole": {"tf": 1}}, "df": 1}}}}}, "c": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"models": {"tf": 1}, "models.StudyGroup": {"tf": 1}, "models.StudySession": {"tf": 1}}, "df": 3}, "r": {"docs": {"models.UserAvailability": {"tf": 1}}, "df": 1}}}}}}}, "o": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"models.Conversation": {"tf": 1}}, "df": 1}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {"models.Post": {"tf": 1}}, "df": 1}}}}, "y": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {"models.User": {"tf": 1}}, "df": 1, "e": {"docs": {}, "df": 0, "d": {"docs": {"models": {"tf": 1}}, "df": 1}}}}}, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {"models.Conversation": {"tf": 1}}, "df": 1}}}}}}}}, "k": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "s": {"docs": {"database.Base.__init__": {"tf": 1.4142135623730951}, "models.User.__init__": {"tf": 1.4142135623730951}, "models.Course.__init__": {"tf": 1.4142135623730951}, "models.Enrollment.__init__": {"tf": 1.4142135623730951}, "models.Conversation.__init__": {"tf": 1.4142135623730951}, "models.ConversationParticipant.__init__": {"tf": 1.4142135623730951}, "models.Message.__init__": {"tf": 1.4142135623730951}, "models.Post.__init__": {"tf": 1.4142135623730951}, "models.PostVote.__init__": {"tf": 1.4142135623730951}, "models.StudyGroup.__init__": {"tf": 1.4142135623730951}, "models.StudySession.__init__": {"tf": 1.4142135623730951}, "models.StudyGroupMember.__init__": {"tf": 1.4142135623730951}, "models.UserAvailability.__init__": {"tf": 1.4142135623730951}, "models.StudySessionInvitee.__init__": {"tf": 1.4142135623730951}}, "df": 14}}}}}, "e": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "s": {"docs": {"database.Base.__init__": {"tf": 1}, "models.User": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 15}, "e": {"docs": {}, "df": 0, "d": {"docs": {"models.User": {"tf": 1}, "models.StudyGroupMember": {"tf": 1}}, "df": 2}}}}}, "u": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"database.Base.__init__": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 14}}}, "e": {"docs": {}, "df": 0, "d": {"docs": {"database.get_db": {"tf": 1}, "models.User": {"tf": 1}, "models.UserAvailability": {"tf": 1}}, "df": 3}, "r": {"docs": {"models": {"tf": 1}, "models.UserRole": {"tf": 1}, "models.User": {"tf": 1}, "models.Enrollment": {"tf": 1}, "models.ConversationParticipant": {"tf": 1}, "models.PostVote": {"tf": 1}, "models.StudyGroupMember": {"tf": 1}, "models.UserAvailability": {"tf": 1}, "models.StudySessionInvitee": {"tf": 1}}, "df": 9, "a": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "y": {"docs": {"models": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}, "i": {"docs": {}, "df": 0, "d": {"docs": {"models.User": {"tf": 1.4142135623730951}}, "df": 1}}, "p": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"models.PostVote": {"tf": 1}}, "df": 1}}}}}}, "v": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"database.Base.__init__": {"tf": 1}, "models.UserRole": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 15}}}}}, "o": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"models.Post": {"tf": 1}}, "df": 1, "s": {"docs": {"models": {"tf": 1}}, "df": 1}}}}, "i": {"docs": {}, "df": 0, "a": {"docs": {"models.Conversation": {"tf": 1}, "models.StudySession": {"tf": 1}}, "df": 2}}}, "p": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"database.Base.__init__": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 14}}}}}, "o": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"models.User": {"tf": 1}}, "df": 1}}}}, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"models.UserAvailability": {"tf": 1}}, "df": 1}}}}}}}}, "o": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "l": {"docs": {"database.get_db": {"tf": 1}}, "df": 1}}, "s": {"docs": {}, "df": 0, "t": {"docs": {"models": {"tf": 1}, "models.Course": {"tf": 1}, "models.Post": {"tf": 1.4142135623730951}, "models.PostVote": {"tf": 1}}, "df": 4, "v": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"models": {"tf": 1}, "models.Post": {"tf": 1}}, "df": 2}}}}, "s": {"docs": {"models": {"tf": 1}}, "df": 1}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"models.User": {"tf": 1}}, "df": 1}}}}, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"models": {"tf": 1}}, "df": 1, "a": {"docs": {}, "df": 0, "l": {"docs": {"models.Conversation": {"tf": 1}, "models.StudySession": {"tf": 1}}, "df": 2}}}}, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"models.StudyGroup": {"tf": 1}}, "df": 1}}}}}}}}}, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {"models.UserRole": {"tf": 1}}, "df": 1}}}, "u": {"docs": {}, "df": 0, "s": {"docs": {"models.User": {"tf": 1}}, "df": 1}}}}, "e": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"database.Base.__init__": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 14}}}}}}, "a": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "h": {"docs": {"database.get_db": {"tf": 1}}, "df": 1}}}, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "y": {"docs": {"models": {"tf": 1}}, "df": 1}}}}, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {"models.Course": {"tf": 1}}, "df": 1, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"models": {"tf": 1}}, "df": 1}}}}, "e": {"docs": {}, "df": 0, "d": {"docs": {"models.Enrollment": {"tf": 1}}, "df": 1}}}}}}}, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"models.Conversation": {"tf": 1}}, "df": 1}}}}}, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {"models.StudyGroupMember": {"tf": 1}, "models.StudySessionInvitee": {"tf": 1}}, "df": 2}}}}}, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "p": {"docs": {"models": {"tf": 1}}, "df": 1, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"database.Base.__init__": {"tf": 1}, "models.User.__init__": {"tf": 1}, "models.Course.__init__": {"tf": 1}, "models.Enrollment.__init__": {"tf": 1}, "models.Conversation.__init__": {"tf": 1}, "models.ConversationParticipant.__init__": {"tf": 1}, "models.Message.__init__": {"tf": 1}, "models.Post.__init__": {"tf": 1}, "models.PostVote.__init__": {"tf": 1}, "models.StudyGroup.__init__": {"tf": 1}, "models.StudySession.__init__": {"tf": 1}, "models.StudyGroupMember.__init__": {"tf": 1}, "models.UserAvailability.__init__": {"tf": 1}, "models.StudySessionInvitee.__init__": {"tf": 1}}, "df": 14}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"models.Post": {"tf": 1}}, "df": 1}}}}}}}}, "y": {"docs": {"models.StudySessionInvitee": {"tf": 1}}, "df": 1}}, "e": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"models.StudySession": {"tf": 1}, "models.UserAvailability": {"tf": 1}}, "df": 2, "s": {"docs": {"models": {"tf": 1}, "models.StudyGroup": {"tf": 1}}, "df": 2}}}}}}, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {"models": {"tf": 1}, "models.Message": {"tf": 1}}, "df": 2}}}}}, "m": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "p": {"docs": {"models.ConversationParticipant": {"tf": 1}, "models.StudyGroup": {"tf": 1}, "models.StudyGroupMember": {"tf": 1}}, "df": 3}}}}}}}}}, "o": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {"models.Post": {"tf": 1}}, "df": 1}}}}}}}}, "s": {"docs": {}, "df": 0, "t": {"docs": {"models.UserAvailability": {"tf": 1}}, "df": 1}}}}, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "s": {"docs": {"database.get_db": {"tf": 1}}, "df": 1}}}}}, "n": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "z": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"models.Post": {"tf": 1}}, "df": 1}}}}}}}}}}, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"models.Post": {"tf": 1}}, "df": 1}}}}}}}, "b": {"docs": {"database.get_db": {"tf": 1}}, "df": 1}, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"models.Post": {"tf": 1}}, "df": 1, "/": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"models": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}}, "m": {"docs": {}, "df": 0, "s": {"docs": {"models.Conversation": {"tf": 1}}, "df": 1}}, "o": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"models.PostVote": {"tf": 1}}, "df": 1}}}}}}}}, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {"models": {"tf": 1}}, "df": 1}}}, "t": {"docs": {}, "df": 0, "s": {"docs": {"models.Post": {"tf": 1}}, "df": 1}}}, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"models": {"tf": 1}, "models.StudyGroup": {"tf": 1}}, "df": 2}}}, "i": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {"models.StudySession": {"tf": 1}}, "df": 1, "d": {"docs": {"models": {"tf": 1}, "models.StudyGroup": {"tf": 1}}, "df": 2}}}, "n": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"models.Enrollment": {"tf": 1}}, "df": 1}}}}}}}, "j": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {"models.Enrollment": {"tf": 1}}, "df": 1}}}}, "y": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {"models.StudySessionInvitee": {"tf": 1}}, "df": 1}}}}}}, "pipeline": ["trimmer"], "_isPrebuiltIndex": true}; + + // mirrored in build-search-index.js (part 1) + // Also split on html tags. this is a cheap heuristic, but good enough. + elasticlunr.tokenizer.setSeperator(/[\s\-.;&_'"=,()]+|<[^>]*>/); + + let searchIndex; + if (docs._isPrebuiltIndex) { + console.info("using precompiled search index"); + searchIndex = elasticlunr.Index.load(docs); + } else { + console.time("building search index"); + // mirrored in build-search-index.js (part 2) + searchIndex = elasticlunr(function () { + this.pipeline.remove(elasticlunr.stemmer); + this.pipeline.remove(elasticlunr.stopWordFilter); + this.addField("qualname"); + this.addField("fullname"); + this.addField("annotation"); + this.addField("default_value"); + this.addField("signature"); + this.addField("bases"); + this.addField("doc"); + this.setRef("fullname"); + }); + for (let doc of docs) { + searchIndex.addDoc(doc); + } + console.timeEnd("building search index"); + } + + return (term) => searchIndex.search(term, { + fields: { + qualname: {boost: 4}, + fullname: {boost: 2}, + annotation: {boost: 2}, + default_value: {boost: 2}, + signature: {boost: 2}, + bases: {boost: 2}, + doc: {boost: 1}, + }, + expand: true + }); +})(); \ No newline at end of file diff --git a/docs/frontend/App.js.html b/docs/frontend/App.js.html new file mode 100644 index 0000000..4cd5def --- /dev/null +++ b/docs/frontend/App.js.html @@ -0,0 +1,122 @@ + + + + + JSDoc: Source: App.js + + + + + + + + + + +
+ +

Source: App.js

+ + + + + + +
+
+
/**
+ * Top-level router for StudySync.
+ *
+ * Routes split into two layout groups behind a ProtectedRoute auth guard:
+ *  - GlobalLayout: Navbar only (Home, personal Dashboard, global Inbox).
+ *  - ClassLayout: Navbar + ClassHeader tabs (Summary, Resources, Chat,
+ *    Schedule), all scoped to a `:courseId` URL param.
+ *
+ * The login page at "/" is the only unauthenticated route; anything
+ * unmatched redirects to "/home".
+ *
+ * @module App
+ */
+import { BrowserRouter as Router, Routes, Route, Navigate, Outlet } from "react-router-dom";
+import LoginPage from "./LoginPage";
+import HomePage from "./HomePage";
+import SchedulePage from "./SchedulePage";
+import ChatPage from "./ChatPage";
+import ResourcesPage from "./ResourcesPage";
+import DashboardPage from "./DashboardPage";
+import ProtectedRoute from "./ProtectedRoute";
+import Navbar from "./Navbar";
+import ClassHeader from "./ClassHeader";
+
+// Layout for Global Pages (Home, Dashboard, Discussion)
+const GlobalLayout = () => (
+  <div className="page with-navbar">
+    <Navbar />
+    <Outlet />
+  </div>
+);
+
+// Layout for Class Pages (with Navbar + Class Header + Secondary Nav)
+const ClassLayout = () => (
+  <div className="page with-navbar">
+    <Navbar />
+    <ClassHeader />
+    <div className="page-content">
+      <Outlet />
+    </div>
+  </div>
+);
+
+function App() {
+  return (
+    <Router>
+      <Routes>
+        <Route path="/" element={<LoginPage />} />
+
+        {/* Global Pages with Top Navbar Only */}
+        <Route element={<ProtectedRoute><GlobalLayout /></ProtectedRoute>}>
+          <Route path="/home" element={<HomePage />} />
+          <Route path="/dashboard" element={<DashboardPage isClassScoped={false} />} />
+          <Route path="/inbox" element={<ChatPage isGlobal={true} />} />
+        </Route>
+
+        {/* Class Pages with Navbar + Class Header + Secondary Nav */}
+        <Route element={<ProtectedRoute><ClassLayout /></ProtectedRoute>}>
+          <Route path="/class/:courseId/summary" element={<DashboardPage isClassScoped={true} />} />
+          <Route path="/class/:courseId/resources" element={<ResourcesPage />} />
+          <Route path="/class/:courseId/chat" element={<ChatPage isGlobal={false} />} />
+          <Route path="/class/:courseId/schedule" element={<SchedulePage />} />
+        </Route>
+
+        <Route path="*" element={<Navigate to="/home" />} />
+      </Routes>
+    </Router>
+  );
+}
+
+export default App;
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.5 on Wed Apr 29 2026 18:41:17 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/frontend/CalendarPage.js.html b/docs/frontend/CalendarPage.js.html new file mode 100644 index 0000000..c796009 --- /dev/null +++ b/docs/frontend/CalendarPage.js.html @@ -0,0 +1,735 @@ + + + + + JSDoc: Source: CalendarPage.js + + + + + + + + + + +
+ +

Source: CalendarPage.js

+ + + + + + +
+
+
/**
+ * Standalone calendar / availability page.
+ *
+ * Lets the user sync their Google Calendar busy times into StudySync,
+ * create or join study groups, request meeting-time suggestions based
+ * on group availability, and create solo or group study sessions on a
+ * monthly calendar grid.
+ *
+ * @module CalendarPage
+ */
+import "./LoginPage.css";
+import "./CalendarPage.css";
+import { useEffect, useMemo, useState } from "react";
+import { auth } from "./firebase";
+
+const API_BASE = process.env.REACT_APP_API_BASE_URL || "http://127.0.0.1:8000";
+const GOOGLE_CLIENT_ID = process.env.REACT_APP_GOOGLE_CLIENT_ID;
+
+const CalendarPage = () => {
+  const [groups, setGroups] = useState([]);
+  const [selectedGroupId, setSelectedGroupId] = useState("");
+  const [groupName, setGroupName] = useState("");
+  const [joinGroupId, setJoinGroupId] = useState("");
+  const [syncStatus, setSyncStatus] = useState("");
+  const [groupStatus, setGroupStatus] = useState("");
+  const [suggestions, setSuggestions] = useState([]);
+  const [loadingSuggestions, setLoadingSuggestions] = useState(false);
+  const [daysAhead, setDaysAhead] = useState(14);
+  const [meetingMinutes, setMeetingMinutes] = useState(60);
+  const [sessions, setSessions] = useState([]);
+  const [sessionType, setSessionType] = useState("solo");
+  const [sessionTitle, setSessionTitle] = useState("");
+  const [sessionStart, setSessionStart] = useState("");
+  const [sessionEnd, setSessionEnd] = useState("");
+  const [sessionStatus, setSessionStatus] = useState("");
+  const [currentMonth, setCurrentMonth] = useState(() => {
+    const d = new Date();
+    return new Date(d.getFullYear(), d.getMonth(), 1);
+  });
+
+  // Disable page scroll on this page
+  useEffect(() => {
+    document.body.classList.add("calendar-page");
+    return () => {
+      document.body.classList.remove("calendar-page");
+    };
+  }, []);
+  const [selectedDate, setSelectedDate] = useState(() => new Date().toISOString().slice(0, 10));
+
+  const user = auth.currentUser;
+  const userEmail = user?.email || "";
+  const canUseGoogle = Boolean(GOOGLE_CLIENT_ID);
+
+  const selectedGroup = useMemo(
+    () => groups.find((g) => String(g.id) === String(selectedGroupId)),
+    [groups, selectedGroupId]
+  );
+
+  /** Lazily inject the Google Identity Services script and resolve once it is ready. */
+  const loadGoogleScript = () =>
+    new Promise((resolve, reject) => {
+      if (window.google?.accounts?.oauth2) {
+        resolve();
+        return;
+      }
+      const existing = document.querySelector('script[data-google-identity="1"]');
+      if (existing) {
+        existing.addEventListener("load", () => resolve());
+        existing.addEventListener("error", () => reject(new Error("Could not load Google Identity script")));
+        return;
+      }
+      const script = document.createElement("script");
+      script.src = "https://accounts.google.com/gsi/client";
+      script.async = true;
+      script.defer = true;
+      script.dataset.googleIdentity = "1";
+      script.onload = () => resolve();
+      script.onerror = () => reject(new Error("Could not load Google Identity script"));
+      document.body.appendChild(script);
+    });
+
+  /** Prompt the user for a Google OAuth token with calendar.readonly scope. */
+  const getGoogleAccessToken = async () => {
+    await loadGoogleScript();
+    return new Promise((resolve, reject) => {
+      const tokenClient = window.google.accounts.oauth2.initTokenClient({
+        client_id: GOOGLE_CLIENT_ID,
+        scope: "https://www.googleapis.com/auth/calendar.readonly",
+        callback: (response) => {
+          if (response?.access_token) {
+            resolve(response.access_token);
+          } else {
+            reject(new Error("Google OAuth failed"));
+          }
+        },
+      });
+      tokenClient.requestAccessToken({ prompt: "consent" });
+    });
+  };
+
+  /** Load the user's study groups; auto-select the first group if none is selected yet. */
+  const fetchGroups = async () => {
+    if (!userEmail) return;
+    const response = await fetch(
+      `${API_BASE}/study-groups?user_email=${encodeURIComponent(userEmail)}`
+    );
+    if (!response.ok) throw new Error("Failed to fetch groups");
+    const data = await response.json();
+    setGroups(data);
+    if (!selectedGroupId && data.length > 0) {
+      setSelectedGroupId(String(data[0].id));
+    }
+  };
+
+  useEffect(() => {
+    fetchGroups().catch((err) => setGroupStatus(err.message));
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [userEmail]);
+
+  /** Create a new study group from the form input and switch the active selection to it. */
+  const createGroup = async () => {
+    if (!groupName.trim()) return;
+    setGroupStatus("Creating group...");
+    try {
+      const response = await fetch(`${API_BASE}/study-groups`, {
+        method: "POST",
+        headers: { "Content-Type": "application/json" },
+        body: JSON.stringify({
+          name: groupName.trim(),
+          user_email: userEmail,
+        }),
+      });
+      if (!response.ok) throw new Error("Could not create group");
+      const created = await response.json();
+      await fetchGroups();
+      setSelectedGroupId(String(created.id));
+      setGroupName("");
+      setGroupStatus("Group created.");
+    } catch (err) {
+      setGroupStatus(err.message || "Failed creating group");
+    }
+  };
+
+  /** Join an existing group by numeric id, then select it as the active group. */
+  const joinGroup = async () => {
+    const id = Number(joinGroupId);
+    if (!id) return;
+    setGroupStatus("Joining group...");
+    try {
+      const response = await fetch(`${API_BASE}/study-groups/${id}/join`, {
+        method: "POST",
+        headers: { "Content-Type": "application/json" },
+        body: JSON.stringify({ user_email: userEmail }),
+      });
+      if (!response.ok) throw new Error("Could not join group");
+      await fetchGroups();
+      setSelectedGroupId(String(id));
+      setJoinGroupId("");
+      setGroupStatus("Joined group.");
+    } catch (err) {
+      setGroupStatus(err.message || "Failed joining group");
+    }
+  };
+
+  /**
+   * Pull busy ranges from Google Calendar's freeBusy endpoint over
+   * the next `daysAhead` days and POST them to the backend so other
+   * StudySync features can see when this user is unavailable.
+   */
+  const syncGoogleBusyTimes = async () => {
+    if (!userEmail) return;
+    setSyncStatus("Connecting to Google Calendar...");
+    try {
+      if (!canUseGoogle) {
+        throw new Error("Missing REACT_APP_GOOGLE_CLIENT_ID in frontend env");
+      }
+      const token = await getGoogleAccessToken();
+      const now = new Date();
+      const end = new Date(now.getTime() + daysAhead * 24 * 60 * 60 * 1000);
+
+      const freeBusyResponse = await fetch(
+        "https://www.googleapis.com/calendar/v3/freeBusy",
+        {
+          method: "POST",
+          headers: {
+            Authorization: `Bearer ${token}`,
+            "Content-Type": "application/json",
+          },
+          body: JSON.stringify({
+            timeMin: now.toISOString(),
+            timeMax: end.toISOString(),
+            items: [{ id: "primary" }],
+          }),
+        }
+      );
+      if (!freeBusyResponse.ok) throw new Error("Failed to read Google Calendar busy times");
+      const freeBusyJson = await freeBusyResponse.json();
+      const busy =
+        freeBusyJson?.calendars?.primary?.busy?.map((slot) => ({
+          starts_at: slot.start,
+          ends_at: slot.end,
+        })) || [];
+
+      const backendResponse = await fetch(`${API_BASE}/availability/sync`, {
+        method: "POST",
+        headers: { "Content-Type": "application/json" },
+        body: JSON.stringify({
+          user_email: userEmail,
+          starts_at: now.toISOString(),
+          ends_at: end.toISOString(),
+          timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
+          source: "google_calendar",
+          busy_slots: busy,
+        }),
+      });
+      if (!backendResponse.ok) throw new Error("Failed to sync busy times to backend");
+      const result = await backendResponse.json();
+      setSyncStatus(`Synced ${result.inserted_busy_blocks} busy blocks (no event details stored).`);
+    } catch (err) {
+      setSyncStatus(err.message || "Calendar sync failed");
+    }
+  };
+
+  /**
+   * Ask the backend for meeting-time suggestions for the active group
+   * over the next `daysAhead` days, restricted to working hours
+   * (8am–10pm) and `meetingMinutes` long, in the user's local TZ.
+   */
+  const loadSuggestions = async () => {
+    if (!selectedGroupId) return;
+    setLoadingSuggestions(true);
+    try {
+      const start = new Date();
+      const end = new Date(start.getTime() + daysAhead * 24 * 60 * 60 * 1000);
+      const params = new URLSearchParams({
+        range_start: start.toISOString(),
+        range_end: end.toISOString(),
+        duration_minutes: String(meetingMinutes),
+        slot_minutes: "30",
+        day_start_hour: "8",
+        day_end_hour: "22",
+        min_available_members: "1",
+        user_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
+      });
+      const response = await fetch(
+        `${API_BASE}/study-groups/${selectedGroupId}/suggestions?${params.toString()}`
+      );
+      if (!response.ok) throw new Error("Failed to load suggestions");
+      const data = await response.json();
+      setSuggestions(data.suggestions || []);
+    } catch (err) {
+      setSyncStatus(err.message || "Could not load suggestions");
+    } finally {
+      setLoadingSuggestions(false);
+    }
+  };
+
+  const fmt = (iso) =>
+    new Date(iso).toLocaleString([], {
+      weekday: "short",
+      month: "short",
+      day: "numeric",
+      hour: "numeric",
+      minute: "2-digit",
+    });
+
+  const dayKey = (date) => date.toISOString().slice(0, 10);
+
+  const suggestionsByDay = useMemo(() => {
+    const map = {};
+    suggestions.forEach((slot) => {
+      const key = dayKey(new Date(slot.starts_at));
+      map[key] = (map[key] || 0) + 1;
+    });
+    return map;
+  }, [suggestions]);
+
+  const selectedDaySuggestions = useMemo(
+    () => suggestions.filter((slot) => dayKey(new Date(slot.starts_at)) === selectedDate),
+    [suggestions, selectedDate]
+  );
+
+  const sessionsByDay = useMemo(() => {
+    const map = {};
+    sessions.forEach((s) => {
+      const key = dayKey(new Date(s.starts_at));
+      map[key] = (map[key] || 0) + 1;
+    });
+    return map;
+  }, [sessions]);
+
+  const selectedDaySessions = useMemo(
+    () => sessions.filter((s) => dayKey(new Date(s.starts_at)) === selectedDate),
+    [sessions, selectedDate]
+  );
+
+  // Build a 6-week (42-cell) grid for the visible month, padded with
+  // days from the prior/next month so the grid always starts on Sunday.
+  const calendarCells = useMemo(() => {
+    const firstDay = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), 1);
+    const startWeekday = firstDay.getDay();
+    const gridStart = new Date(firstDay);
+    gridStart.setDate(firstDay.getDate() - startWeekday);
+    const cells = [];
+    for (let i = 0; i < 42; i += 1) {
+      const cellDate = new Date(gridStart);
+      cellDate.setDate(gridStart.getDate() + i);
+      cells.push({
+        key: dayKey(cellDate),
+        date: cellDate,
+        inMonth: cellDate.getMonth() === currentMonth.getMonth(),
+      });
+    }
+    return cells;
+  }, [currentMonth]);
+
+  const monthLabel = currentMonth.toLocaleDateString([], { month: "long", year: "numeric" });
+
+  /** Load study sessions overlapping the visible 6-week grid for the current user. */
+  const loadSessions = async () => {
+    if (!userEmail) return;
+    const first = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), 1);
+    const start = new Date(first);
+    start.setDate(first.getDate() - first.getDay());
+    const end = new Date(start);
+    end.setDate(start.getDate() + 42);
+
+    const params = new URLSearchParams({
+      user_email: userEmail,
+      range_start: start.toISOString(),
+      range_end: end.toISOString(),
+    });
+    const response = await fetch(`${API_BASE}/study-sessions?${params.toString()}`);
+    if (!response.ok) throw new Error("Failed to load study sessions");
+    const data = await response.json();
+    setSessions(data);
+  };
+
+  useEffect(() => {
+    loadSessions().catch((err) => setSessionStatus(err.message));
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [userEmail, currentMonth]);
+
+  const toInputDateTime = (d) => {
+    const pad = (n) => String(n).padStart(2, "0");
+    return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(
+      d.getMinutes()
+    )}`;
+  };
+
+  /**
+   * Validate the session form, POST a new session, and on success
+   * reset the title and bump the start/end pickers an hour forward
+   * so chained creation feels natural. Group sessions require an
+   * active group selection.
+   */
+  const createStudySession = async () => {
+    if (!userEmail) return;
+    if (!sessionTitle.trim() || !sessionStart || !sessionEnd) {
+      setSessionStatus("Please provide title, start time, and end time.");
+      return;
+    }
+    if (sessionType === "group" && !selectedGroupId) {
+      setSessionStatus("Select a group for group study sessions.");
+      return;
+    }
+
+    setSessionStatus("Creating session...");
+    try {
+      const response = await fetch(`${API_BASE}/study-sessions`, {
+        method: "POST",
+        headers: { "Content-Type": "application/json" },
+        body: JSON.stringify({
+          creator_email: userEmail,
+          session_type: sessionType,
+          title: sessionTitle.trim(),
+          starts_at: new Date(sessionStart).toISOString(),
+          ends_at: new Date(sessionEnd).toISOString(),
+          group_id: sessionType === "group" ? Number(selectedGroupId) : null,
+        }),
+      });
+      if (!response.ok) {
+        const err = await response.json().catch(() => ({}));
+        throw new Error(err.detail || "Failed to create session");
+      }
+      const created = await response.json();
+      setSessionStatus("Session created.");
+      setSessionTitle("");
+      const nextStart = new Date(new Date(created.starts_at).getTime() + 60 * 60 * 1000);
+      const nextEnd = new Date(new Date(created.starts_at).getTime() + 2 * 60 * 60 * 1000);
+      setSessionStart(toInputDateTime(nextStart));
+      setSessionEnd(toInputDateTime(nextEnd));
+      await loadSessions();
+    } catch (err) {
+      setSessionStatus(err.message || "Failed creating session");
+    }
+  };
+
+  return (
+    <div className="page with-navbar">
+      <div className="calendar-layout">
+        <div className="calendar-main">
+          <div className="month-nav">
+            <button
+              className="month-nav-btn"
+              type="button"
+              onClick={() =>
+                setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1))
+              }
+            >
+              Prev
+            </button>
+            <div className="month-title">{monthLabel}</div>
+            <button
+              className="month-nav-btn"
+              type="button"
+              onClick={() =>
+                setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1))
+              }
+            >
+              Next
+            </button>
+          </div>
+
+          <div className="calendar-grid">
+            {["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].map((d) => (
+              <div key={d} className="calendar-dow">
+                {d}
+              </div>
+            ))}
+            {calendarCells.map((cell) => {
+              const sessionCount = sessionsByDay[cell.key] || 0;
+              const suggestionCount = suggestionsByDay[cell.key] || 0;
+              const isSelected = selectedDate === cell.key;
+              const className = [
+                "calendar-day",
+                cell.inMonth ? "" : "outside",
+                isSelected ? "selected" : "",
+              ]
+                .join(" ")
+                .trim();
+              return (
+                <button
+                  key={cell.key}
+                  type="button"
+                  onClick={() => setSelectedDate(cell.key)}
+                  className={className}
+                >
+                  <div className="calendar-day-num">{cell.date.getDate()}</div>
+                  <div className="calendar-day-slots">
+                    {sessionCount > 0
+                      ? `${sessionCount} session${sessionCount > 1 ? "s" : ""}`
+                      : suggestionCount > 0
+                      ? `${suggestionCount} slot${suggestionCount > 1 ? "s" : ""}`
+                      : " "}
+                  </div>
+                </button>
+              );
+            })}
+          </div>
+
+          <p className="calendar-day-summary">
+            {selectedDaySuggestions.length > 0
+              ? `${selectedDaySuggestions.length} suggested slot(s) on ${new Date(selectedDate).toLocaleDateString()}.`
+              : `No meetings suggested yet for ${new Date(selectedDate).toLocaleDateString()}.`}
+          </p>
+        </div>
+
+        <div className="calendar-sidebar">
+          <div>
+            <h2 style={{ marginBottom: "12px" }}>Calendar / Study Groups</h2>
+            <p style={{ color: "var(--text-secondary)", marginBottom: "16px" }}>
+              Privacy mode: only busy/free blocks are synced. Event titles and details are never stored.
+            </p>
+
+            <div className="sidebar-section">
+              <label>Create Study Session</label>
+              <div className="mode-toggle">
+                <button
+                  type="button"
+                  className={`mode-btn ${sessionType === "solo" ? "active" : ""}`}
+                  onClick={() => setSessionType("solo")}
+                >
+                  Solo Study Session
+                </button>
+                <button
+                  type="button"
+                  className={`mode-btn ${sessionType === "group" ? "active" : ""}`}
+                  onClick={() => setSessionType("group")}
+                >
+                  Group Study Session
+                </button>
+              </div>
+              <div className="sidebar-row">
+                <input
+                  type="text"
+                  placeholder={sessionType === "group" ? "Group session title" : "Solo session title"}
+                  value={sessionTitle}
+                  onChange={(e) => setSessionTitle(e.target.value)}
+                  className="sidebar-input"
+                />
+              </div>
+              <div className="sidebar-row">
+                <input
+                  type="datetime-local"
+                  value={sessionStart}
+                  onChange={(e) => setSessionStart(e.target.value)}
+                  className="sidebar-input"
+                />
+              </div>
+              <div className="sidebar-row">
+                <input
+                  type="datetime-local"
+                  value={sessionEnd}
+                  onChange={(e) => setSessionEnd(e.target.value)}
+                  className="sidebar-input"
+                />
+              </div>
+              {sessionType === "group" && (
+                <p style={{ color: "var(--text-secondary)", marginTop: "6px" }}>
+                  Group sessions use the selected group above.
+                </p>
+              )}
+              <div className="sidebar-row">
+                <button className="btn-submit" type="button" onClick={createStudySession}>
+                  Create
+                </button>
+              </div>
+              {sessionStatus && (
+                <p style={{ color: "var(--text-secondary)", marginTop: "6px" }}>{sessionStatus}</p>
+              )}
+            </div>
+
+            <div className="sidebar-section">
+              <label>Signed in as</label>
+              <div style={{ marginTop: "6px", color: "var(--text-secondary)" }}>
+                {userEmail || "No user"}
+              </div>
+            </div>
+
+            <div className="sidebar-section">
+              <label>Create a Study Group</label>
+              <div className="sidebar-row">
+                <input
+                  type="text"
+                  placeholder="e.g., CSDS 393 Midterm Prep"
+                  value={groupName}
+                  onChange={(e) => setGroupName(e.target.value)}
+                  className="sidebar-input"
+                />
+                <button className="btn-submit" type="button" onClick={createGroup}>
+                  Create
+                </button>
+              </div>
+            </div>
+
+            <div className="sidebar-section">
+              <label>Join by Group ID</label>
+              <div className="sidebar-row">
+                <input
+                  type="number"
+                  placeholder="Group ID"
+                  value={joinGroupId}
+                  onChange={(e) => setJoinGroupId(e.target.value)}
+                  className="sidebar-input"
+                />
+                <button className="btn-submit" type="button" onClick={joinGroup}>
+                  Join
+                </button>
+              </div>
+            </div>
+
+            <div className="sidebar-section">
+              <label>Your Groups</label>
+              <select
+                value={selectedGroupId}
+                onChange={(e) => setSelectedGroupId(e.target.value)}
+                className="sidebar-select"
+              >
+                <option value="">Select a group</option>
+                {groups.map((group) => (
+                  <option key={group.id} value={group.id}>
+                    #{group.id} - {group.name}
+                  </option>
+                ))}
+              </select>
+              {selectedGroup && (
+                <p style={{ color: "var(--text-secondary)", marginTop: "6px" }}>
+                  Selected: {selectedGroup.name} (ID: {selectedGroup.id})
+                </p>
+              )}
+            </div>
+
+            <div className="sidebar-section">
+              <label>Sync Window (Days Ahead)</label>
+              <input
+                type="number"
+                min="1"
+                max="60"
+                value={daysAhead}
+                onChange={(e) => setDaysAhead(Number(e.target.value) || 14)}
+                className="sidebar-input"
+              />
+            </div>
+
+            <div className="sidebar-section">
+              <label>Meeting Duration (Minutes)</label>
+              <input
+                type="number"
+                min="15"
+                max="240"
+                step="15"
+                value={meetingMinutes}
+                onChange={(e) => setMeetingMinutes(Number(e.target.value) || 60)}
+                className="sidebar-input"
+              />
+            </div>
+
+            <div style={{ display: "flex", gap: "8px", marginBottom: "16px", flexWrap: "wrap" }}>
+              <button className="btn-google" type="button" onClick={syncGoogleBusyTimes}>
+                Sync Busy Times from Google
+              </button>
+              <button className="btn-submit" type="button" onClick={loadSuggestions} disabled={!selectedGroupId || loadingSuggestions}>
+                {loadingSuggestions ? "Loading..." : "Find Best Meeting Times"}
+              </button>
+            </div>
+
+            {groupStatus && (
+              <p style={{ color: "var(--text-secondary)", marginBottom: "8px" }}>{groupStatus}</p>
+            )}
+            {syncStatus && (
+              <p style={{ color: "var(--text-secondary)", marginBottom: "12px" }}>{syncStatus}</p>
+            )}
+
+            <div style={{ marginBottom: "16px" }}>
+              <h3 style={{ marginBottom: "8px" }}>Sessions On Selected Day</h3>
+              {selectedDaySessions.length === 0 ? (
+                <p style={{ color: "var(--text-secondary)" }}>No sessions created for this day yet.</p>
+              ) : (
+                <div className="suggestion-list">
+                  {selectedDaySessions.map((session, idx) => (
+                    <div key={`${session.id || idx}-${session.starts_at}`} className="suggestion-item">
+                      <div style={{ fontWeight: 700 }}>{session.title}</div>
+                      <div style={{ color: "var(--text-secondary)", marginTop: "2px" }}>
+                        {fmt(session.starts_at)} - {fmt(session.ends_at)}
+                      </div>
+                      <div style={{ color: "var(--text-secondary)", marginTop: "2px" }}>
+                        {session.session_type === "group" ? "Group Session" : "Solo Session"}
+                      </div>
+                    </div>
+                  ))}
+                </div>
+              )}
+            </div>
+
+            <div>
+              <h3 style={{ marginBottom: "8px" }}>Suggested Slots</h3>
+              {selectedDaySuggestions.length === 0 ? (
+                <p style={{ color: "var(--text-secondary)" }}>
+                  No suggestions for this day. Sync your busy times and click "Find Best Meeting Times".
+                </p>
+              ) : (
+                <div className="suggestion-list">
+                  {selectedDaySuggestions.map((slot, idx) => (
+                    <div key={`${slot.starts_at}-${idx}`} className="suggestion-item">
+                      <div style={{ fontWeight: 700 }}>
+                        {fmt(slot.starts_at)} - {fmt(slot.ends_at)}
+                      </div>
+                      <div style={{ color: "var(--text-secondary)", marginTop: "2px" }}>
+                        {slot.available_count}/{slot.total_members} members available
+                      </div>
+                    </div>
+                  ))}
+                </div>
+              )}
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default CalendarPage;
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.5 on Wed Apr 29 2026 18:41:17 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/frontend/ChatPage.js.html b/docs/frontend/ChatPage.js.html new file mode 100644 index 0000000..a324de7 --- /dev/null +++ b/docs/frontend/ChatPage.js.html @@ -0,0 +1,354 @@ + + + + + JSDoc: Source: ChatPage.js + + + + + + + + + + +
+ +

Source: ChatPage.js

+ + + + + + +
+
+
/**
+ * Chat page used in two modes selected by the `isGlobal` prop:
+ *
+ * - Global inbox (`isGlobal=true`, mounted at /inbox): every
+ *   conversation the current user belongs to, across courses and DMs.
+ * - Class chat (`isGlobal=false`, mounted at /class/:courseId/chat):
+ *   the auto-provisioned course-wide group chat plus 1:1 DMs with
+ *   classmates.
+ *
+ * @module ChatPage
+ */
+import { useEffect, useState, useCallback} from "react";
+import { onAuthStateChanged } from "firebase/auth";
+import { useParams } from "react-router-dom";
+import "./LoginPage.css";
+import "./ChatPage.css";
+import { auth } from "./firebase";
+
+const ROLE_PRIORITY = {
+  Admin: 0,
+  TA: 1,
+  Student: 2,
+};
+
+const ChatPage = ({ isGlobal = true }) => {
+  const { courseId } = useParams();
+  const [authUser, setAuthUser] = useState(null);
+  const [userProfile, setUserProfile] = useState(null);
+  const [loading, setLoading] = useState(true);
+
+  const [users, setUsers] = useState([]);
+  const [selectedUser, setSelectedUser] = useState(null);
+  const [messages, setMessages] = useState([]);
+  const [newMessage, setNewMessage] = useState("");
+  const [sending, setSending] = useState(false);
+
+  // Load current user + profile
+  useEffect(() => {
+    const unsubscribe = onAuthStateChanged(auth, async (user) => {
+      if (user) {
+        setAuthUser(user);
+        try {
+          // 1. Load User Profile
+          const profileRes = await fetch(
+            `http://localhost:8000/user/${user.uid}`,
+          );
+          if (profileRes.ok) {
+            const profileData = await profileRes.json();
+            setUserProfile(profileData);
+          }
+
+          // 2. Logic Switch: Global Inbox vs. Class Roster
+          if (isGlobal) {
+            // GLOBAL VIEW: Fetch only active conversations from across all classes
+            const inboxRes = await fetch(
+              `http://localhost:8000/conversations/inbox/global?user_uid=${user.uid}`,
+            );
+            if (inboxRes.ok) {
+              const activeConversations = await inboxRes.json();
+              setUsers(activeConversations);
+              if (activeConversations.length > 0)
+                setSelectedUser(activeConversations[0]);
+            }
+          } else {
+            // CLASS VIEW: Fetch all students/TAs in the system so you can start new chats
+            const usersRes = await fetch("http://localhost:8000/users");
+            if (usersRes.ok) {
+              const allUsers = await usersRes.json();
+              // Filter out yourself
+              let filtered = allUsers.filter(
+                (u) => u.firebase_uid !== user.uid,
+              );
+
+              // Create the Class Chat sentinel for this specific course
+              const classChatEntry = {
+                firebase_uid: `GROUP_${courseId}`, // Unique ID for this course group
+                full_name: "Class Discussion",
+                role: "Public Channel",
+                is_group: true,
+                course_id: courseId,
+              };
+
+              setUsers([classChatEntry, ...filtered]);
+              setSelectedUser(classChatEntry); // Default to the class group chat
+            }
+          }
+        } catch (err) {
+          console.error("Error loading chat roster:", err);
+        }
+      }
+      setLoading(false);
+    });
+    return () => unsubscribe();
+  }, [isGlobal, courseId]);
+
+  /**
+   * Fetch the message history for the conversation between
+   * `currentUser` and `otherUser` (or the course group chat if
+   * `otherUser.is_group`). Course context comes from the conversation
+   * itself when in the global inbox, falling back to the URL
+   * `:courseId` on the class page.
+   */
+  const loadMessages = useCallback(async (currentUser, otherUser) => {
+    if (!currentUser || !otherUser) return;
+    try {
+      const params = new URLSearchParams();
+      params.append("user1", currentUser.uid);
+      params.append("user2", otherUser.firebase_uid);
+      params.append("is_group", otherUser.is_group || false);
+
+      // Use the course ID from the user object (Global) OR the URL (Class Page)
+      const activeCourseId = otherUser.course_id || courseId;
+      if (activeCourseId) {
+        params.append("course_id", activeCourseId);
+      }
+
+      const res = await fetch(
+        `http://localhost:8000/messages?${params.toString()}`,
+      );
+      if (res.ok) {
+        const data = await res.json();
+        setMessages(data);
+      }
+    } catch (err) {
+      console.error("Error loading messages:", err);
+    }
+  }, [courseId]);
+
+  // Initial + polling load of messages when selectedUser changes
+  useEffect(() => {
+    if (!authUser || !selectedUser) return;
+    loadMessages(authUser, selectedUser);
+    const interval = setInterval(() => {
+      loadMessages(authUser, selectedUser);
+    }, 3000);
+    return () => clearInterval(interval);
+  }, [authUser, selectedUser, loadMessages]);
+
+  /**
+   * Send the current draft message to the selected conversation.
+   * Uses the conversation's own course id when present (so global
+   * inbox replies stay in the right course thread); falls back to
+   * the URL `:courseId` for class-page sends. For group chats the
+   * receiver UID is the sentinel `"GROUP"`.
+   */
+  const handleSendMessage = async (e) => {
+    e.preventDefault();
+    if (!authUser || !selectedUser || !newMessage.trim()) return;
+
+    setSending(true);
+    try {
+      // Determine correct course context for the message
+      const targetCourseId = selectedUser.course_id || courseId;
+
+      const payload = {
+        sender_uid: authUser.uid,
+        content: newMessage.trim(),
+        course_id: parseInt(targetCourseId || 0),
+        receiver_uid: selectedUser.is_group
+          ? "GROUP"
+          : selectedUser.firebase_uid,
+        is_group: selectedUser.is_group || false,
+      };
+
+      const res = await fetch("http://localhost:8000/messages", {
+        method: "POST",
+        headers: { "Content-Type": "application/json" },
+        body: JSON.stringify(payload),
+      });
+
+      if (res.ok) {
+        setNewMessage("");
+        loadMessages(authUser, selectedUser);
+      }
+    } catch (err) {
+      console.error("Error sending message:", err);
+    } finally {
+      setSending(false);
+    }
+  };
+
+  if (loading) {
+    return (
+      <div className="page with-navbar">
+        <div className="dm-page">
+          <div className="dm-shell">
+            <div className="dm-sidebar">
+              <div className="dm-brand">
+                <h1 className="tagline">
+                  Discussion <span className="highlight">Forum</span>
+                </h1>
+                <p className="tagline-sub">Loading chat...</p>
+              </div>
+            </div>
+            <div className="dm-main">
+              <div className="dm-main-empty">Loading…</div>
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="page with-navbar">
+      <div className="dm-page">
+        <div className="dm-shell">
+          <aside className="dm-sidebar">
+            <div className="dm-sidebar-header">
+              <h2 className="dm-sidebar-title">Messages</h2>
+              {userProfile && (
+                <p className="dm-sidebar-sub">
+                  {userProfile.full_name} · {userProfile.role}
+                </p>
+              )}
+            </div>
+
+            <div className="dm-list-label">Active Conversations</div>
+            <div className="dm-user-list">
+              {users.length === 0 && (
+                <p className="dm-empty-text">No active chats in this view.</p>
+              )}
+              {users.map((u) => {
+                const isActive = selectedUser?.firebase_uid === u.firebase_uid;
+                return (
+                  <button
+                    key={u.firebase_uid}
+                    type="button"
+                    className={`dm-user-row ${isActive ? "active" : ""}`}
+                    onClick={() => setSelectedUser(u)}
+                  >
+                    <div className="dm-avatar">
+                      {u.full_name?.charAt(0).toUpperCase() || "U"}
+                    </div>
+                    <div className="dm-user-meta">
+                      {/* Name will now include (Course Code) from the backend */}
+                      <div className="dm-user-name">{u.full_name}</div>
+                      <div className="dm-user-role">
+                        {u.role} {u.course_code ? `· ${u.course_code}` : ""}
+                      </div>
+                    </div>
+                  </button>
+                );
+              })}
+            </div>
+          </aside>
+
+          <section className="dm-main">
+            <header className="dm-main-header">
+              {selectedUser && (
+                <div className="dm-main-user">
+                  <div className="dm-main-name">{selectedUser.full_name}</div>
+                  <div className="dm-main-role">
+                    {selectedUser.role}{" "}
+                    {selectedUser.course_code
+                      ? `· ${selectedUser.course_code}`
+                      : ""}
+                  </div>
+                </div>
+              )}
+            </header>
+
+            <div className="dm-messages">
+              {messages.map((m) => {
+                const isMe = m.sender_uid === authUser?.uid;
+                return (
+                  <div
+                    key={m.id}
+                    className={`dm-message-row ${isMe ? "me" : "them"}`}
+                  >
+                    {!isMe && selectedUser?.is_group && (
+                      <span className="dm-sender-label">{m.sender_name}</span>
+                    )}
+                    <div className="dm-bubble">{m.content}</div>
+                  </div>
+                );
+              })}
+            </div>
+
+            <form className="dm-input-bar" onSubmit={handleSendMessage}>
+              <input
+                type="text"
+                placeholder="Message..."
+                value={newMessage}
+                onChange={(e) => setNewMessage(e.target.value)}
+                disabled={!selectedUser || sending}
+              />
+              <button
+                type="submit"
+                className="btn-submit chat-send-btn"
+                disabled={!newMessage.trim()}
+              >
+                Send
+              </button>
+            </form>
+          </section>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default ChatPage;
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.5 on Wed Apr 29 2026 18:41:17 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/frontend/ClassHeader.js.html b/docs/frontend/ClassHeader.js.html new file mode 100644 index 0000000..b966300 --- /dev/null +++ b/docs/frontend/ClassHeader.js.html @@ -0,0 +1,196 @@ + + + + + JSDoc: Source: ClassHeader.js + + + + + + + + + + +
+ +

Source: ClassHeader.js

+ + + + + + +
+
+
/**
+ * Class-scoped header shown above ClassLayout pages.
+ *
+ * Reads the `:courseId` URL param, fetches the user's enrolled courses
+ * to populate a class-switcher dropdown, and renders the secondary tab
+ * bar (Summary / Library / Chat / Study Sessions) for the current class.
+ *
+ * @module ClassHeader
+ */
+import { useState, useEffect } from "react";
+import { NavLink, useParams, useNavigate } from "react-router-dom";
+import { auth } from "./firebase";
+import "./ClassHeader.css";
+
+const ClassHeader = () => {
+  const { courseId } = useParams();
+  const navigate = useNavigate();
+  const [showDropdown, setShowDropdown] = useState(false);
+  const [courses, setCourses] = useState([]);
+  const [currentCourse, setCurrentCourse] = useState(null);
+
+  // Fetch user's courses for the dropdown
+  useEffect(() => {
+    const fetchCourses = async () => {
+      try {
+        const user = auth.currentUser;
+        if (!user) return;
+
+        const response = await fetch(`http://localhost:8000/users/${user.uid}/courses`);
+        if (response.ok) {
+          const data = await response.json();
+          setCourses(data);
+          
+          // Find current course
+          const current = data.find((c) => c.id === parseInt(courseId));
+          if (current) {
+            setCurrentCourse(current);
+          }
+        }
+      } catch (err) {
+        console.error("Error fetching courses for dropdown:", err);
+      }
+    };
+
+    if (courseId) {
+      fetchCourses();
+    }
+  }, [courseId]);
+
+  const handleCourseSwitch = (newCourseId) => {
+    // Navigate to summary page of the new course
+    navigate(`/class/${newCourseId}/summary`);
+    setShowDropdown(false);
+  };
+
+  return (
+    <div className="class-header-container">
+      {/* Class Title with Dropdown */}
+      <div className="class-header-top">
+        <div className="class-title-with-dropdown">
+          <h2 className="class-title">
+            {currentCourse?.name || "Course"}
+          </h2>
+          <button
+            className="dropdown-trigger"
+            onClick={() => setShowDropdown(!showDropdown)}
+            title="Switch classes"
+          >
+            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+              <polyline points="6 9 12 15 18 9"></polyline>
+            </svg>
+          </button>
+
+          {/* Dropdown Menu */}
+          {showDropdown && (
+            <div className="class-dropdown">
+              <div className="dropdown-label">Switch Class</div>
+              {courses.length > 0 ? (
+                <div className="dropdown-list">
+                  {courses.map((course) => (
+                    <button
+                      key={course.id}
+                      className={`dropdown-item ${
+                        course.id === currentCourse?.id ? "active" : ""
+                      }`}
+                      onClick={() => handleCourseSwitch(course.id)}
+                    >
+                      <span className="course-icon">
+                        {course.course_code[0]}
+                      </span>
+                      <div className="course-info">
+                        <div className="course-name">{course.name}</div>
+                        <div className="course-code">{course.course_code}</div>
+                      </div>
+                      {course.id === currentCourse?.id && (
+                        <span className="check-mark">✓</span>
+                      )}
+                    </button>
+                  ))}
+                </div>
+              ) : (
+                <div className="dropdown-empty">No classes found</div>
+              )}
+            </div>
+          )}
+        </div>
+
+        {/* Course Code and Quick Info */}
+        <div className="class-quick-info">
+          <span className="course-code-badge">{currentCourse?.course_code}</span>
+        </div>
+      </div>
+
+      {/* Secondary Navigation (Tabs) */}
+      <nav className="secondary-navbar">
+        <NavLink
+          to={`/class/${courseId}/summary`}
+          className={({ isActive }) => `sec-nav-link ${isActive ? "active" : ""}`}
+        >
+          Summary
+        </NavLink>
+        <NavLink
+          to={`/class/${courseId}/resources`}
+          className={({ isActive }) => `sec-nav-link ${isActive ? "active" : ""}`}
+        >
+          Library
+        </NavLink>
+        <NavLink
+          to={`/class/${courseId}/chat`}
+          className={({ isActive }) => `sec-nav-link ${isActive ? "active" : ""}`}
+        >
+          Chat
+        </NavLink>
+        <NavLink
+          to={`/class/${courseId}/schedule`}
+          className={({ isActive }) => `sec-nav-link ${isActive ? "active" : ""}`}
+        >
+          Study Sessions
+        </NavLink>
+      </nav>
+    </div>
+  );
+};
+
+export default ClassHeader;
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.5 on Wed Apr 29 2026 18:41:17 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/frontend/DashboardPage.js.html b/docs/frontend/DashboardPage.js.html new file mode 100644 index 0000000..2c9d5c9 --- /dev/null +++ b/docs/frontend/DashboardPage.js.html @@ -0,0 +1,576 @@ + + + + + JSDoc: Source: DashboardPage.js + + + + + + + + + + +
+ +

Source: DashboardPage.js

+ + + + + + +
+
+
/**
+ * Dashboard page used in two modes selected by the `isClassScoped` prop:
+ *
+ * - Personal (`isClassScoped=false`, mounted at /dashboard): profile
+ *   editing (name, email, password re-auth) plus, for TA/Admin users,
+ *   a moderation queue of flagged posts.
+ * - Class summary (`isClassScoped=true`, mounted at
+ *   /class/:courseId/summary): upcoming sessions and aggregate study
+ *   stats for the active course.
+ *
+ * @module DashboardPage
+ */
+import { useState, useEffect, useCallback } from "react";
+import { auth } from "./firebase";
+import {
+  onAuthStateChanged,
+  EmailAuthProvider,
+  reauthenticateWithCredential,
+  verifyBeforeUpdateEmail,
+  updatePassword,
+} from "firebase/auth";
+import { useParams } from "react-router-dom";
+import "./DashboardPage.css";
+
+const API_BASE = "http://localhost:8000";
+
+const DashboardPage = ({ isClassScoped = false }) => {
+  const { courseId } = useParams();
+  const [userProfile, setUserProfile] = useState(null);
+  const [loading, setLoading] = useState(true);
+  const [showModal, setShowModal] = useState(false);
+  const [editData, setEditData] = useState({
+    name: "",
+    email: "",
+    role: "",
+    newPass: "",
+    confirmPass: "",
+    currentPass: "",
+  });
+  const [needsReauth, setNeedsReauth] = useState(false);
+  const [flaggedPosts, setFlaggedPosts] = useState([]);
+  const [classSummary, setClassSummary] = useState({
+    upcomingSessions: [],
+    completedHours: 0,
+    completedSessions: 0,
+  });
+  const [classSummaryLoading, setClassSummaryLoading] = useState(false);
+
+  const fetchProfile = useCallback(async (firebase_uid) => {
+    try {
+      const response = await fetch(
+        `${API_BASE}/user/${firebase_uid}`,
+      );
+      if (response.ok) {
+        const data = await response.json();
+        setUserProfile(data);
+        setEditData((prev) => ({
+          ...prev,
+          name: data.full_name,
+          email: data.email,
+          role: data.role,
+        }));
+      }
+    } catch (err) {
+      console.error("Error fetching profile:", err);
+    }
+  }, []);
+
+  const fetchFlaggedPosts = useCallback(async () => {
+    try {
+      const res = await fetch(`${API_BASE}/posts/flagged`);
+      if (res.ok) {
+        const data = await res.json();
+        setFlaggedPosts(data);
+      }
+    } catch (err) {
+      console.error("Error fetching flagged posts:", err);
+    }
+  }, []);
+
+  /**
+   * Load the upcoming sessions and aggregate study stats for the
+   * current course (class-scoped mode only). Resets to empty values
+   * outside class mode or when the user/profile isn't ready yet.
+   */
+  const fetchClassSummary = useCallback(async () => {
+    if (!isClassScoped || !courseId || !userProfile?.email) {
+      setClassSummary({
+        upcomingSessions: [],
+        completedHours: 0,
+        completedSessions: 0,
+      });
+      return;
+    }
+
+    setClassSummaryLoading(true);
+    try {
+      const params = new URLSearchParams({
+        requester_email: userProfile.email,
+      });
+      const response = await fetch(
+        `${API_BASE}/study-sessions/course/${courseId}/summary?${params.toString()}`,
+      );
+
+      if (!response.ok) {
+        throw new Error("Failed to load class dashboard summary");
+      }
+
+      const data = await response.json();
+      setClassSummary({
+        upcomingSessions: data.upcoming_sessions || [],
+        completedHours: data.completed_hours || 0,
+        completedSessions: data.completed_sessions || 0,
+      });
+    } catch (err) {
+      console.error("Error fetching class dashboard summary:", err);
+      setClassSummary({
+        upcomingSessions: [],
+        completedHours: 0,
+        completedSessions: 0,
+      });
+    } finally {
+      setClassSummaryLoading(false);
+    }
+  }, [courseId, isClassScoped, userProfile?.email]);
+
+  /** Format a session's start/end Date pair as a single localized label. */
+  const formatSessionTime = (startsAt, endsAt) => {
+    const start = new Date(startsAt);
+    const end = new Date(endsAt);
+    const dayLabel = start.toLocaleDateString([], {
+      month: "short",
+      day: "numeric",
+      weekday: "short",
+    });
+    const timeLabel = `${start.toLocaleTimeString([], {
+      hour: "numeric",
+      minute: "2-digit",
+    })} - ${end.toLocaleTimeString([], {
+      hour: "numeric",
+      minute: "2-digit",
+    })}`;
+
+    return `${dayLabel} • ${timeLabel}`;
+  };
+
+  /** Moderator action: clear a post's flag and remove it from the queue. */
+  const handleDismissFlag = async (postId) => {
+    try {
+      const res = await fetch(
+        `${API_BASE}/posts/${postId}/dismiss-flag`,
+        {
+          method: "POST",
+        },
+      );
+      if (res.ok) {
+        setFlaggedPosts((prev) => prev.filter((p) => p.id !== postId));
+      }
+    } catch (err) {
+      console.error("Dismiss failed", err);
+    }
+  };
+
+  /** Moderator action: confirm and permanently delete a flagged post. */
+  const handleDeletePost = async (postId) => {
+    if (
+      !window.confirm("Are you sure you want to delete this flagged content?")
+    )
+      return;
+    try {
+      const res = await fetch(
+        `${API_BASE}/posts/${postId}?user_uid=${userProfile.firebase_uid}`,
+        { method: "DELETE" },
+      );
+      if (res.ok) {
+        setFlaggedPosts((prev) => prev.filter((p) => p.id !== postId));
+      }
+    } catch (err) {
+      console.error("Delete failed", err);
+    }
+  };
+
+  useEffect(() => {
+    const unsubscribe = onAuthStateChanged(auth, async (user) => {
+      if (user) {
+        await fetchProfile(user.uid);
+      }
+      setLoading(false);
+    });
+    return () => unsubscribe();
+  }, [fetchProfile]);
+
+  useEffect(() => {
+    if (
+      userProfile &&
+      (userProfile.role === "TA" || userProfile.role === "Admin")
+    ) {
+      fetchFlaggedPosts();
+    }
+  }, [userProfile, fetchFlaggedPosts]);
+
+  useEffect(() => {
+    fetchClassSummary();
+  }, [fetchClassSummary]);
+
+  /**
+   * Submit profile edits from the modal.
+   *
+   * Email and password changes trigger a two-step flow: the first
+   * submit flips the form into "needs current password" mode, and the
+   * second submit re-authenticates with `EmailAuthProvider` before
+   * applying `updatePassword` / `verifyBeforeUpdateEmail`. Name and
+   * role changes are pushed to the StudySync backend in a single PUT
+   * regardless of the re-auth path.
+   */
+  const handleUpdate = async (e) => {
+    e.preventDefault();
+    const user = auth.currentUser;
+    const isChangingEmail = editData.email !== userProfile.email;
+    const isChangingPass = editData.newPass !== "";
+
+    try {
+      if ((isChangingEmail || isChangingPass) && !needsReauth) {
+        setNeedsReauth(true);
+        return;
+      }
+      if (needsReauth) {
+        const credential = EmailAuthProvider.credential(
+          user.email,
+          editData.currentPass,
+        );
+        await reauthenticateWithCredential(user, credential);
+      }
+      if (isChangingPass) {
+        if (editData.newPass !== editData.confirmPass)
+          throw new Error("Passwords do not match.");
+        await updatePassword(user, editData.newPass);
+      }
+      if (isChangingEmail) {
+        await verifyBeforeUpdateEmail(user, editData.email);
+        alert("Verification email sent!");
+      }
+
+      const response = await fetch(
+        `${API_BASE}/user/${user.uid}/update`,
+        {
+          method: "PUT",
+          headers: { "Content-Type": "application/json" },
+          body: JSON.stringify({
+            firebase_uid: user.uid,
+            email: userProfile.email,
+            full_name: editData.name,
+            role: editData.role,
+          }),
+        },
+      );
+
+      if (response.ok) {
+        setUserProfile({
+          ...userProfile,
+          full_name: editData.name,
+          role: editData.role,
+        });
+        setShowModal(false);
+        setNeedsReauth(false);
+      }
+    } catch (err) {
+      alert(err.message || "Failed to update profile.");
+      if (err.code === "auth/wrong-password") setNeedsReauth(true);
+    }
+  };
+
+  if (loading) return <div className="page">Loading...</div>;
+
+  const pageTitle = isClassScoped ? "Class Dashboard" : userProfile?.full_name;
+
+  return (
+    <div className="dashboard-container">
+      <div className="dashboard-content">
+        <header className="dashboard-header">
+          <h1 className="page-title">{pageTitle}</h1>
+          {!isClassScoped && (
+            <>
+              <div className="info-bubbles">
+                <div className="feature-pill">
+                  <div className="dot" />
+                  {userProfile?.email}
+                </div>
+                <div className="feature-pill">
+                  <div className="dot" />
+                  {userProfile?.role}
+                </div>
+                <div className="feature-pill">
+                  <div className="dot" />
+                  GCal: {userProfile?.gcal_connected ? "Connected" : "Not"}
+                </div>
+              </div>
+              <button className="btn-subtle" onClick={() => setShowModal(true)}>
+                Update personal info
+              </button>
+            </>
+          )}
+        </header>
+
+        {/* MODERATION SECTION */}
+        {flaggedPosts.length > 0 &&
+          (userProfile?.role === "TA" || userProfile?.role === "Admin") && (
+            <div className="moderation-alert-box">
+              <div className="mod-alert-header">
+                <h3>
+                  ⚠️ Urgent: {flaggedPosts.length} Flagged Posts Need Review
+                </h3>
+              </div>
+              <div className="moderation-container">
+                {flaggedPosts.map((post) => (
+                  <div key={post.id} className="flagged-post-wrapper">
+                    <div className="flagged-post-card">
+                      <div className="post-header">
+                        <div className="post-author-info">
+                          <div className="author-avatar">
+                            {post.author_name
+                              ? post.author_name.charAt(0).toUpperCase()
+                              : "?"}
+                          </div>
+                          <div className="author-details">
+                            <div className="author-name">
+                              {post.author_name}
+                            </div>
+                            <span className={`author-role ${post.author_role}`}>
+                              {post.author_role}
+                            </span>
+                          </div>
+                        </div>
+                        <div className="post-timestamp">
+                          {new Date(post.created_at).toLocaleDateString()}
+                        </div>
+                      </div>
+
+                      <div className="post-content">
+                        <h3 className="post-title">{post.title}</h3>
+                        {post.description && (
+                          <p className="post-description">{post.description}</p>
+                        )}
+                        {post.resource_link && (
+                          <a
+                            href={post.resource_link}
+                            target="_blank"
+                            rel="noopener noreferrer"
+                            className="post-resource-link"
+                          >
+                            🔗 {post.resource_link}
+                          </a>
+                        )}
+                      </div>
+
+                      {/* Action Row centered within the card */}
+                      <div className="moderation-actions-footer">
+                        <button
+                          className="btn-subtle dismiss-btn"
+                          onClick={() => handleDismissFlag(post.id)}
+                        >
+                          Dismiss Flag
+                        </button>
+                        <button
+                          className="btn-primary delete-btn"
+                          onClick={() => handleDeletePost(post.id)}
+                        >
+                          Delete Post
+                        </button>
+                      </div>
+                    </div>
+                  </div>
+                ))}
+              </div>
+            </div>
+          )}
+
+        {/* UPDATE PROFILE MODAL */}
+        {showModal && !isClassScoped && (
+          <div className="modal-overlay">
+            <div className="modal-card">
+              <h3>Update Profile</h3>
+              <form onSubmit={handleUpdate}>
+                <div className="field">
+                  <label>Full Name</label>
+                  <input
+                    type="text"
+                    value={editData.name}
+                    onChange={(e) =>
+                      setEditData({ ...editData, name: e.target.value })
+                    }
+                  />
+                </div>
+                <div className="field">
+                  <label>Role</label>
+                  <select
+                    value={editData.role}
+                    onChange={(e) =>
+                      setEditData({ ...editData, role: e.target.value })
+                    }
+                  >
+                    <option value="Student">Student</option>
+                    <option value="TA">Teaching Assistant</option>
+                    <option value="Admin">Administrator</option>
+                  </select>
+                </div>
+
+                <hr className="divider-line" />
+                <div className="field">
+                  <label>New Password</label>
+                  <input
+                    type="password"
+                    value={editData.newPass}
+                    onChange={(e) =>
+                      setEditData({ ...editData, newPass: e.target.value })
+                    }
+                  />
+                </div>
+                {editData.newPass && (
+                  <div className="field">
+                    <label>Confirm New Password</label>
+                    <input
+                      type="password"
+                      value={editData.confirmPass}
+                      onChange={(e) =>
+                        setEditData({
+                          ...editData,
+                          confirmPass: e.target.value,
+                        })
+                      }
+                    />
+                  </div>
+                )}
+
+                {needsReauth && (
+                  <div className="reauth-box">
+                    <p>Enter current password to confirm changes:</p>
+                    <div className="field">
+                      <input
+                        type="password"
+                        placeholder="Current Password"
+                        value={editData.currentPass}
+                        onChange={(e) =>
+                          setEditData({
+                            ...editData,
+                            currentPass: e.target.value,
+                          })
+                        }
+                        required
+                      />
+                    </div>
+                  </div>
+                )}
+
+                <div className="modal-actions">
+                  <button
+                    type="button"
+                    className="btn-subtle"
+                    onClick={() => {
+                      setShowModal(false);
+                      setNeedsReauth(false);
+                    }}
+                  >
+                    Cancel
+                  </button>
+                  <button type="submit" className="btn-primary">
+                    {needsReauth ? "Confirm Changes" : "Save Changes"}
+                  </button>
+                </div>
+              </form>
+            </div>
+          </div>
+        )}
+
+        <div className="dashboard-grid">
+          <div className="card">
+            <h3>Upcoming Meetings</h3>
+            {isClassScoped ? (
+              classSummaryLoading ? (
+                <p className="tagline-sub">Loading upcoming meetings...</p>
+              ) : classSummary.upcomingSessions.length > 0 ? (
+                <div className="dashboard-session-list">
+                  {classSummary.upcomingSessions.map((session) => (
+                    <div key={session.id} className="dashboard-session-item">
+                      <div className="dashboard-session-title">{session.title}</div>
+                      <div className="dashboard-session-meta">
+                        {formatSessionTime(session.starts_at, session.ends_at)}
+                      </div>
+                    </div>
+                  ))}
+                </div>
+              ) : (
+                <p className="tagline-sub">No upcoming meetings for this course yet.</p>
+              )
+            ) : (
+              <p className="tagline-sub">No meetings scheduled yet.</p>
+            )}
+          </div>
+          <div className="card">
+            <h3>Study Stats</h3>
+            {isClassScoped ? (
+              classSummaryLoading ? (
+                <p className="tagline-sub">Loading study stats...</p>
+              ) : (
+                <div className="study-stat-block">
+                  <div className="study-stat-value">
+                    {classSummary.completedHours.toFixed(1)} hours
+                  </div>
+                  <p className="tagline-sub">
+                    Completed so far in this course across {classSummary.completedSessions} session
+                    {classSummary.completedSessions === 1 ? "" : "s"}.
+                  </p>
+                </div>
+              )
+            ) : (
+              <p className="tagline-sub">Usage tracking coming soon.</p>
+            )}
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default DashboardPage;
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.5 on Wed Apr 29 2026 18:41:17 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/frontend/HomePage.js.html b/docs/frontend/HomePage.js.html new file mode 100644 index 0000000..a9225db --- /dev/null +++ b/docs/frontend/HomePage.js.html @@ -0,0 +1,287 @@ + + + + + JSDoc: Source: HomePage.js + + + + + + + + + + +
+ +

Source: HomePage.js

+ + + + + + +
+
+
/**
+ * Home page (post-login landing) — shows the user's enrolled classes
+ * and lets them join an existing course by code or, for TA/Admin
+ * users, create a new course. Each course tile links into the
+ * class-scoped routes under `/class/:courseId/...`.
+ *
+ * @module HomePage
+ */
+import React, { useState, useEffect, useCallback } from "react";
+import { useNavigate } from "react-router-dom";
+import axios from "axios";
+import { auth } from "./firebase";
+import { onAuthStateChanged } from "firebase/auth";
+import "./HomePage.css";
+
+const HomePage = () => {
+  const [courses, setCourses] = useState([]);
+  const [userProfile, setUserProfile] = useState(null);
+  const [showJoinModal, setShowJoinModal] = useState(false);
+  const [showCreateModal, setShowCreateModal] = useState(false);
+  const [courseCode, setCourseCode] = useState("");
+  const [newCourseName, setNewCourseName] = useState("");
+  const [newCourseCode, setNewCourseCode] = useState("");
+  const navigate = useNavigate();
+
+  // Fetch user profile to check for TA/Admin roles
+  const fetchProfile = useCallback(async (firebase_uid) => {
+    try {
+      const response = await fetch(`http://localhost:8000/user/${firebase_uid}`);
+      if (response.ok) {
+        const data = await response.json();
+        setUserProfile(data);
+      }
+    } catch (err) {
+      console.error("Error fetching profile:", err);
+    }
+  }, []);
+
+  const fetchUserCourses = useCallback(async (firebase_uid) => {
+    if (!firebase_uid) return;
+    try {
+      const res = await axios.get(
+        `http://localhost:8000/users/${firebase_uid}/courses`
+      );
+      setCourses(res.data);
+    } catch (err) {
+      console.error("Error fetching courses", err);
+    }
+  }, []);
+
+  useEffect(() => {
+    const unsubscribe = onAuthStateChanged(auth, (user) => {
+      if (user) {
+        fetchProfile(user.uid);
+        fetchUserCourses(user.uid);
+      }
+    });
+    return () => unsubscribe();
+  }, [fetchUserCourses, fetchProfile]);
+
+  /**
+   * Submit the join-course modal: enroll the current user in the
+   * course identified by `courseCode`. On success, close the modal
+   * and refresh the user's course list. Failure is surfaced via
+   * alert (typical case: bad code or already enrolled).
+   */
+  const handleJoinCourse = async (e) => {
+    e.preventDefault();
+    const user = auth.currentUser;
+    if (!user) return;
+
+    try {
+      await axios.post(
+        `http://localhost:8000/courses/join?course_code=${courseCode}&firebase_uid=${user.uid}`
+      );
+      setCourseCode("");
+      setShowJoinModal(false);
+      fetchUserCourses(user.uid);
+    } catch (err) {
+      alert("Invalid course code or already enrolled.");
+    }
+  };
+
+  /**
+   * Submit the create-course modal (TA/Admin only). Owner is the
+   * current Firebase UID. Surfaces the backend's `detail` message on
+   * failure (e.g. duplicate course code).
+   */
+  const handleCreateCourse = async (e) => {
+    e.preventDefault();
+    const user = auth.currentUser;
+    if (!user) return;
+
+    try {
+      await axios.post("http://localhost:8000/courses", {
+        name: newCourseName,
+        course_code: newCourseCode,
+        owner_id: user.uid, // Use Firebase UID
+      });
+      setNewCourseName("");
+      setNewCourseCode("");
+      setShowCreateModal(false);
+      fetchUserCourses(user.uid);
+    } catch (err) {
+      const errorMessage =
+        err.response?.data?.detail || "Failed to create course.";
+      alert(errorMessage);
+      console.error("Course creation error:", err.response?.data);
+    }
+  };
+
+  return (
+    <div className="home-content">
+      <header className="page-header">
+        <h1 className="page-title">My Classes</h1>
+        <div style={{ display: "flex", gap: "12px" }}>
+          <button
+            className="btn-primary"
+            onClick={() => setShowJoinModal(true)}
+          >
+            + Join Class
+          </button>
+
+          {(userProfile?.role === "TA" || userProfile?.role === "Admin") && (
+            <button
+              className="btn-subtle"
+              onClick={() => setShowCreateModal(true)}
+            >
+              + Create Class
+            </button>
+          )}
+        </div>
+      </header>
+
+      <div className="home-grid">
+        {courses.map((course) => (
+          <div
+            key={course.id}
+            className="home-card"
+            onClick={() => navigate(`/class/${course.id}/summary`)}
+          >
+            <div className="home-card-icon">{course.course_code[0]}</div>
+            <h3 className="home-card-title">{course.name}</h3>
+            <p className="home-card-description">{course.course_code}</p>
+          </div>
+        ))}
+
+        {courses.length === 0 && (
+          <div className="no-classes">
+            <p>
+              You haven't joined any classes yet. Use a code to get started!
+            </p>
+          </div>
+        )}
+      </div>
+
+      {/* Join Class Modal */}
+      {showJoinModal && (
+        <div className="modal-overlay">
+          <div className="modal-card">
+            <h2 className="brand-name">Join a New Class</h2>
+            <p className="tagline-sub">Enter your class code below</p>
+            <form onSubmit={handleJoinCourse}>
+              <div className="field">
+                <input
+                  type="text"
+                  placeholder="Enter Course Code (e.g. CSDS393)"
+                  value={courseCode}
+                  onChange={(e) => setCourseCode(e.target.value)}
+                  required
+                />
+              </div>
+              <div className="modal-actions">
+                <button
+                  type="button"
+                  className="btn-subtle"
+                  onClick={() => setShowJoinModal(false)}
+                >
+                  Cancel
+                </button>
+                <button type="submit" className="btn-primary">
+                  Join
+                </button>
+              </div>
+            </form>
+          </div>
+        </div>
+      )}
+
+      {/* Create Class Modal */}
+      {showCreateModal && (
+        <div className="modal-overlay">
+          <div className="modal-card">
+            <h2 className="brand-name">Create Workspace</h2>
+            <p className="tagline-sub">Set up a new class environment</p>
+            <form onSubmit={handleCreateCourse}>
+              <div className="field">
+                <label>Course Name</label>
+                <input
+                  type="text"
+                  placeholder="e.g. Data Science"
+                  value={newCourseName}
+                  onChange={(e) => setNewCourseName(e.target.value)}
+                  required
+                />
+              </div>
+              <div className="field">
+                <label>Course Code</label>
+                <input
+                  type="text"
+                  placeholder="e.g. CSDS393"
+                  value={newCourseCode}
+                  onChange={(e) => setNewCourseCode(e.target.value)}
+                  required
+                />
+              </div>
+              <div className="modal-actions">
+                <button
+                  type="button"
+                  className="btn-subtle"
+                  onClick={() => setShowCreateModal(false)}
+                >
+                  Cancel
+                </button>
+                <button type="submit" className="btn-primary">
+                  Create
+                </button>
+              </div>
+            </form>
+          </div>
+        </div>
+      )}
+    </div>
+  );
+};
+
+export default HomePage;
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.5 on Wed Apr 29 2026 18:41:17 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/frontend/LoginPage.js.html b/docs/frontend/LoginPage.js.html new file mode 100644 index 0000000..4886056 --- /dev/null +++ b/docs/frontend/LoginPage.js.html @@ -0,0 +1,531 @@ + + + + + JSDoc: Source: LoginPage.js + + + + + + + + + + +
+ +

Source: LoginPage.js

+ + + + + + +
+
+
/**
+ * Login / sign-up page.
+ *
+ * Handles email-and-password sign-in, account creation (with a role
+ * picker for new users), Google sign-in, and password reset. After a
+ * successful auth, the new/returning user is upserted into the
+ * StudySync backend via /sync-user before navigating to /home.
+ *
+ * @module LoginPage
+ */
+import { useState, useEffect } from "react";
+import "./LoginPage.css";
+import { useNavigate } from "react-router-dom";
+
+import {
+  signInWithEmailAndPassword,
+  createUserWithEmailAndPassword,
+  updateProfile,
+  GoogleAuthProvider,
+  signInWithPopup,
+  sendPasswordResetEmail,
+} from "firebase/auth";
+
+import { auth } from "./firebase.js";
+
+const LoginPage = () => {
+  const navigate = useNavigate();
+  const [isLogin, setIsLogin] = useState(true);
+  const [showPassword, setShowPassword] = useState(false);
+
+  // --- STATES FOR DATABASE SYNC ---
+  const [showRoleSelection, setShowRoleSelection] = useState(false);
+  const [tempUser, setTempUser] = useState(null);
+  const [errorMessage, setErrorMessage] = useState("");
+
+  const [formData, setFormData] = useState({
+    name: "",
+    email: "",
+    password: "",
+  });
+  const [focused, setFocused] = useState(null);
+
+  useEffect(() => {
+    return () => {
+      setFormData({ name: "", email: "", password: "" });
+      setErrorMessage("");
+    };
+  }, []);
+
+  const handleChange = (e) => {
+    setFormData({ ...formData, [e.target.name]: e.target.value });
+  };
+
+  // --- SYNC FUNCTION ---
+  /**
+   * Upsert the just-authenticated Firebase user into the StudySync
+   * Postgres database via /sync-user, then navigate to /home. Falls
+   * back to displayName / "New User" if no name was provided.
+   *
+   * @param {Object} user - Firebase user object (from firebase/auth).
+   * @param {string} role - Role to record (Student / TA / Admin).
+   * @param {string|null} name - Optional display name override.
+   */
+  const syncWithBackend = async (user, role, name) => {
+    try {
+      const response = await fetch("http://localhost:8000/sync-user", {
+        method: "POST",
+        headers: { "Content-Type": "application/json" },
+        body: JSON.stringify({
+          firebase_uid: user.uid,
+          email: user.email,
+          full_name: name || user.displayName || "New User",
+          role: role,
+        }),
+      });
+      if (response.ok) {
+        navigate("/home");
+      } else {
+        const data = await response.json();
+        alert(data.detail || "Database sync failed.");
+      }
+    } catch (err) {
+      console.error("Backend error:", err);
+      alert("Could not connect to the server.");
+    }
+  };
+
+  /**
+   * Handle the email/password form for both sign-in and sign-up.
+   *
+   * - Sign-in path: authenticate, then sync to backend as "Student" by
+   *   default and route to /home.
+   * - Sign-up path: create the Firebase account, set displayName from
+   *   the form, then surface the role-selection step before syncing.
+   *
+   * Maps common Firebase auth error codes to friendlier messages
+   * shown via `setErrorMessage`.
+   */
+  const handleSubmit = async (e) => {
+    e.preventDefault();
+    setErrorMessage("");
+
+    try {
+      if (isLogin) {
+        const userCred = await signInWithEmailAndPassword(
+          auth,
+          formData.email,
+          formData.password,
+        );
+
+        await syncWithBackend(
+          userCred.user,
+          "Student",
+          userCred.user.displayName,
+        );
+      } else {
+        const userCred = await createUserWithEmailAndPassword(
+          auth,
+          formData.email,
+          formData.password,
+        );
+        if (formData.name?.trim()) {
+          await updateProfile(userCred.user, {
+            displayName: formData.name.trim(),
+          });
+        }
+        setTempUser(userCred.user);
+        setShowRoleSelection(true);
+      }
+    } catch (err) {
+      console.error(err);
+      const code = err?.code || "";
+
+      // If they already exist in Firebase but Postgres failed last time:
+      if (code === "auth/email-already-in-use" && !isLogin) {
+        setErrorMessage(
+          "Account exists in Auth system. Please Sign In to sync your profile.",
+        );
+        return;
+      }
+
+      // Mapping errors to the state
+      const friendly =
+        code === "auth/invalid-credential"
+          ? "Invalid email or password."
+          : code === "auth/user-not-found"
+            ? "No account found for that email."
+            : code === "auth/wrong-password"
+              ? "Wrong password."
+              : code === "auth/email-already-in-use"
+                ? "That email is already in use."
+                : code === "auth/weak-password"
+                  ? "Password is too weak (try 6+ chars)."
+                  : err?.message || "Auth failed.";
+
+      setErrorMessage(friendly);
+    }
+  };
+
+  /**
+   * Sign in via the Google popup. Always routes through role
+   * selection afterward so first-time Google users still get an
+   * explicit role and a Postgres row, even though Firebase already
+   * has them.
+   */
+  const handleGoogle = async () => {
+    try {
+      const provider = new GoogleAuthProvider();
+      const userCred = await signInWithPopup(auth, provider);
+      // Trigger role selection for Google sign-in to ensure they are in PostgreSQL
+      setTempUser(userCred.user);
+      setShowRoleSelection(true);
+    } catch (err) {
+      console.error(err);
+      alert(err?.message || "Google sign-in failed.");
+    }
+  };
+
+  /** Send a Firebase password-reset email to the address in the form. */
+  const handleForgotPassword = async (e) => {
+    e.preventDefault();
+    if (!formData.email) {
+      alert("Enter your email first, then click 'Forgot password?'.");
+      return;
+    }
+    try {
+      await sendPasswordResetEmail(auth, formData.email);
+      alert("Password reset email sent!");
+    } catch (err) {
+      console.error(err);
+      alert(err?.message || "Could not send reset email.");
+    }
+  };
+
+  return (
+    <div className="page">
+      <div className="blob blob-1" />
+      <div className="blob blob-2" />
+      <div className="blob blob-3" />
+
+      <div className="left-panel">
+        <div className="brand-logo">
+          <div className="logo-icon">
+            <svg viewBox="0 0 24 24">
+              <path d="M12 2L2 7l10 5 10-5-10-5z" />
+              <path d="M2 17l10 5 10-5" />
+              <path d="M2 12l10 5 10-5" />
+            </svg>
+          </div>
+          <div className="brand-name">
+            Study<span>Sync</span>
+          </div>
+        </div>
+        <h1 className="tagline">
+          Learn together, <br />
+          <span className="highlight">grow faster.</span>
+        </h1>
+        <p className="tagline-sub">
+          A single workspace to sync notes, share materials, and collaborate.
+        </p>
+        <div className="features">
+          <div className="feature-pill">
+            <div className="dot" />
+            Shared Notebooks
+          </div>
+          <div className="feature-pill">
+            <div className="dot" />
+            Live Collaboration
+          </div>
+          <div className="feature-pill">
+            <div className="dot" />
+            Material Sharing
+          </div>
+          <div className="feature-pill">
+            <div className="dot" />
+            Study Groups
+          </div>
+        </div>
+      </div>
+
+      <div className="right-panel">
+        <div className="card">
+          {!showRoleSelection ? (
+            <>
+              <div className="tabs">
+                <button
+                  type="button"
+                  className={`tab ${isLogin ? "active" : ""}`}
+                  onClick={() => {
+                    setIsLogin(true);
+                    setFormData({ name: "", email: "", password: "" });
+                    setErrorMessage("");
+                  }}
+                >
+                  Sign In
+                </button>
+                <button
+                  type="button"
+                  className={`tab ${!isLogin ? "active" : ""}`}
+                  onClick={() => {
+                    setIsLogin(false);
+                    setFormData({ name: "", email: "", password: "" });
+                    setErrorMessage("");
+                  }}
+                >
+                  Sign Up
+                </button>
+              </div>
+
+              <form onSubmit={handleSubmit}>
+                {!isLogin && (
+                  <div
+                    className={`field ${focused === "name" ? "is-focused" : ""}`}
+                  >
+                    <label>Full Name</label>
+                    <div className="input-wrap">
+                      <svg className="icon" viewBox="0 0 24 24">
+                        <circle cx="12" cy="8" r="4" />
+                        <path d="M4 20c0-4 4-6 8-6s8 2 8 6" />
+                      </svg>
+                      <input
+                        type="text"
+                        name="name"
+                        placeholder="Alex Johnson"
+                        value={formData.name}
+                        onChange={handleChange}
+                        onFocus={() => setFocused("name")}
+                        onBlur={() => setFocused(null)}
+                      />
+                    </div>
+                  </div>
+                )}
+                <div
+                  className={`field ${focused === "email" ? "is-focused" : ""}`}
+                >
+                  <label>Email</label>
+                  <div className="input-wrap">
+                    <svg className="icon" viewBox="0 0 24 24">
+                      <rect x="2" y="4" width="20" height="16" rx="2" />
+                      <path d="M22 7l-10 6L2 7" />
+                    </svg>
+                    <input
+                      type="email"
+                      name="email"
+                      placeholder="you@university.edu"
+                      value={formData.email}
+                      onChange={handleChange}
+                      onFocus={() => setFocused("email")}
+                      onBlur={() => setFocused(null)}
+                    />
+                  </div>
+                </div>
+                <div
+                  className={`field ${focused === "password" ? "is-focused" : ""}`}
+                >
+                  <label>Password</label>
+                  <div className="input-wrap">
+                    <svg className="icon" viewBox="0 0 24 24">
+                      <rect x="3" y="11" width="18" height="11" rx="2" />
+                      <path d="M7 11V7a5 5 0 0 1 10 0v4" />
+                    </svg>
+
+                    <input
+                      type={showPassword ? "text" : "password"}
+                      name="password"
+                      placeholder={isLogin ? "••••••••" : "Create a password"}
+                      value={formData.password}
+                      onChange={handleChange}
+                      onFocus={() => setFocused("password")}
+                      onBlur={() => setFocused(null)}
+                      autoComplete="current-password"
+                    />
+
+                    <button
+                      type="button"
+                      className="password-toggle-icon"
+                      onClick={() => setShowPassword(!showPassword)}
+                      aria-label={
+                        showPassword ? "Hide password" : "Show password"
+                      }
+                    >
+                      {showPassword ? (
+                        <svg
+                          viewBox="0 0 24 24"
+                          width="20"
+                          height="20"
+                          fill="none"
+                          stroke="currentColor"
+                          strokeWidth="2"
+                        >
+                          <path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>
+                          <line x1="1" y1="1" x2="23" y2="23"></line>
+                        </svg>
+                      ) : (
+                        <svg
+                          viewBox="0 0 24 24"
+                          width="20"
+                          height="20"
+                          fill="none"
+                          stroke="currentColor"
+                          strokeWidth="2"
+                        >
+                          <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
+                          <circle cx="12" cy="12" r="3"></circle>
+                        </svg>
+                      )}
+                    </button>
+                  </div>
+                </div>
+                {isLogin && (
+                  <div className="forgot">
+                    <button
+                      type="button"
+                      className="btn-subtle-link"
+                      onClick={handleForgotPassword}
+                    >
+                      Forgot password?
+                    </button>
+                  </div>
+                )}
+                {errorMessage && (
+                  <div
+                    className="error-text"
+                    style={{
+                      color: "#ff4d4d",
+                      fontSize: "0.85rem",
+                      marginBottom: "15px",
+                      textAlign: "center",
+                      fontWeight: "500",
+                    }}
+                  >
+                    {errorMessage}
+                  </div>
+                )}
+
+                <button type="submit" className="btn-submit">
+                  {isLogin ? "Sign In" : "Create Account"}
+                </button>
+              </form>
+
+              <div className="divider">
+                <div className="divider-line" />
+                <span>or</span>
+                <div className="divider-line" />
+              </div>
+              <button
+                className="btn-google"
+                type="button"
+                onClick={handleGoogle}
+              >
+                Continue with Google
+              </button>
+
+              <div className="auth-footer">
+                {isLogin ? (
+                  <>
+                    Don't have an account?{" "}
+                    <button
+                      type="button"
+                      className="btn-subtle-link"
+                      onClick={() => setIsLogin(false)}
+                    >
+                      Sign up
+                    </button>
+                  </>
+                ) : (
+                  <>
+                    Already have an account?{" "}
+                    <button
+                      type="button"
+                      className="btn-subtle-link"
+                      onClick={() => setIsLogin(true)}
+                    >
+                      Sign in
+                    </button>
+                  </>
+                )}
+              </div>
+            </>
+          ) : (
+            <div className="role-selection">
+              <h2 className="role-title">One Last Step!</h2>
+              <p className="tagline-sub">
+                Are you a Student, TA, or Administrator?
+              </p>
+              <div className="role-options">
+                <button
+                  className="role-option-btn"
+                  onClick={() =>
+                    syncWithBackend(tempUser, "Student", formData.name)
+                  }
+                >
+                  <div className="dot" />
+                  Student
+                </button>
+                <button
+                  className="role-option-btn"
+                  onClick={() => syncWithBackend(tempUser, "TA", formData.name)}
+                >
+                  <div className="dot" />
+                  Teaching Assistant
+                </button>
+                <button
+                  className="role-option-btn"
+                  onClick={() =>
+                    syncWithBackend(tempUser, "Admin", formData.name)
+                  }
+                >
+                  <div className="dot" />
+                  Administrator
+                </button>
+              </div>
+            </div>
+          )}
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default LoginPage;
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.5 on Wed Apr 29 2026 18:41:17 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/frontend/Navbar.js.html b/docs/frontend/Navbar.js.html new file mode 100644 index 0000000..927e4d7 --- /dev/null +++ b/docs/frontend/Navbar.js.html @@ -0,0 +1,141 @@ + + + + + JSDoc: Source: Navbar.js + + + + + + + + + + +
+ +

Source: Navbar.js

+ + + + + + +
+
+
/**
+ * Top-of-page global navigation.
+ *
+ * Renders the StudySync brand, the three top-level links (Classes /
+ * Discussion / Personal), and a logout button.
+ *
+ * @module Navbar
+ */
+import { useState } from "react";
+import { Link, useLocation, useNavigate } from "react-router-dom";
+import { auth } from "./firebase";
+import { signOut } from "firebase/auth";
+import "./Navbar.css";
+
+const Navbar = () => {
+  const location = useLocation();
+  const navigate = useNavigate();
+  const [showLogoutModal, setShowLogoutModal] = useState(false);
+
+  // Helper to check if a path is active, ignoring nested class routes for global links
+  const isActive = (path) => location.pathname === path;
+
+  /**
+   * Sign the user out of Firebase, clear cached identity, and bounce
+   * back to the login page. Errors are logged but not surfaced to the
+   * user.
+   */
+  const handleLogout = async () => {
+    try {
+      await signOut(auth);
+      localStorage.removeItem("userEmail");
+      navigate("/");
+    } catch (error) {
+      console.error("Logout failed:", error);
+    }
+  };
+
+  return (
+    <nav className="navbar">
+      <div className="navbar-brand">
+        <Link to="/home" className="navbar-logo">
+          Study<span>Sync</span>
+        </Link>
+      </div>
+      <div className="navbar-links">
+        {/* GLOBAL NAVIGATION ONLY */}
+        <Link
+          to="/home"
+          className={`navbar-link ${isActive("/home") ? "active" : ""}`}
+        >
+          Classes
+        </Link>
+        <Link
+          to="/inbox"
+          className={`navbar-link ${isActive("/inbox") ? "active" : ""}`}
+        >
+          Discussion
+        </Link>
+        <Link
+          to="/dashboard"
+          className={`navbar-link ${isActive("/dashboard") ? "active" : ""}`}
+        >
+          Personal
+        </Link>
+
+        <button
+          className="navbar-logout-btn"
+          onClick={() => setShowLogoutModal(true)}
+        >
+          <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" strokeWidth="2">
+            <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9" />
+          </svg>
+        </button>
+      </div>
+
+      {showLogoutModal && (
+        <div className="modal-overlay">
+          <div className="modal-card logout-modal">
+            <h3>Are you sure you want to log out?</h3>
+            <div className="modal-actions">
+              <button className="btn-subtle-link" onClick={() => setShowLogoutModal(false)}>Cancel</button>
+              <button className="btn-submit logout-confirm" onClick={handleLogout}>Log out</button>
+            </div>
+          </div>
+        </div>
+      )}
+    </nav>
+  );
+};
+
+export default Navbar;
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.5 on Wed Apr 29 2026 18:41:17 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/frontend/ProtectedRoute.js.html b/docs/frontend/ProtectedRoute.js.html new file mode 100644 index 0000000..9010575 --- /dev/null +++ b/docs/frontend/ProtectedRoute.js.html @@ -0,0 +1,83 @@ + + + + + JSDoc: Source: ProtectedRoute.js + + + + + + + + + + +
+ +

Source: ProtectedRoute.js

+ + + + + + +
+
+
/**
+ * Auth-guard wrapper for routes that require a signed-in Firebase user.
+ * Renders nothing while the auth state is resolving, redirects to "/"
+ * if the user is signed out, and otherwise renders its children.
+ *
+ * @module ProtectedRoute
+ */
+import { useEffect, useState } from "react";
+import { Navigate } from "react-router-dom";
+import { onAuthStateChanged } from "firebase/auth";
+import { auth } from "./firebase";
+
+/**
+ * @param {{ children: React.ReactNode }} props
+ * @returns {React.ReactElement|null} The protected children, a redirect, or null while loading.
+ */
+export default function ProtectedRoute({ children }) {
+  const [user, setUser] = useState(null);
+  const [loading, setLoading] = useState(true);
+
+  useEffect(() => {
+    const unsub = onAuthStateChanged(auth, (u) => {
+      setUser(u);
+      setLoading(false);
+    });
+    return () => unsub();
+  }, []);
+
+  if (loading) return null; 
+  if (!user) return <Navigate to="/" replace />;
+
+  return children;
+}
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.5 on Wed Apr 29 2026 18:41:17 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/frontend/ResourcesPage.js.html b/docs/frontend/ResourcesPage.js.html new file mode 100644 index 0000000..1ed1c17 --- /dev/null +++ b/docs/frontend/ResourcesPage.js.html @@ -0,0 +1,565 @@ + + + + + JSDoc: Source: ResourcesPage.js + + + + + + + + + + +
+ +

Source: ResourcesPage.js

+ + + + + + +
+
+
/**
+ * Class library / resource board (mounted at /class/:courseId/resources).
+ *
+ * Lists resource posts for the current course, supports creating new
+ * posts (title, description, optional link), upvoting and downvoting,
+ * flagging for moderation, and deletion by the original author or
+ * privileged users.
+ *
+ * @module ResourcesPage
+ */
+import { useState, useEffect, useCallback } from "react";
+import { auth } from "./firebase";
+import { useParams } from "react-router-dom";
+import "./LoginPage.css";
+import "./ResourcesPage.css";
+
+const ResourcesPage = () => {
+  const { courseId } = useParams();
+  const [posts, setPosts] = useState([]);
+  const [loading, setLoading] = useState(true);
+  const [error, setError] = useState(null);
+  const [currentUser, setCurrentUser] = useState(null);
+  const [showForm, setShowForm] = useState(false);
+  const [formError, setFormError] = useState(null);
+  const [isSubmitting, setIsSubmitting] = useState(false);
+  const [formData, setFormData] = useState({
+    title: "",
+    description: "",
+    resource_link: "",
+  });
+
+  // Get current user info
+  useEffect(() => {
+    const unsubscribe = auth.onAuthStateChanged(async (firebaseUser) => {
+      if (firebaseUser) {
+        try {
+          const response = await fetch(
+            `http://localhost:8000/user/${firebaseUser.uid}`,
+          );
+          if (response.ok) {
+            const userData = await response.json();
+            setCurrentUser({
+              uid: firebaseUser.uid,
+              email: firebaseUser.email,
+              ...userData,
+            });
+          }
+        } catch (err) {
+          console.error("Error fetching user profile:", err);
+        }
+      }
+    });
+
+    return () => unsubscribe();
+  }, []);
+
+  /**
+   * Load posts for the current course. Passing the user's UID lets
+   * the backend annotate each post with `user_vote` so we can render
+   * the correct upvote/downvote highlight.
+   */
+  const fetchPosts = useCallback(async () => {
+    try {
+      setLoading(true);
+      setError(null);
+      const params = new URLSearchParams();
+      if (courseId) {
+        params.append("course_id", courseId);
+      }
+      if (currentUser?.uid) {
+        params.append("current_user_uid", currentUser.uid);
+      }
+      const response = await fetch(
+        `http://localhost:8000/posts?${params.toString()}`,
+      );
+      if (!response.ok) throw new Error("Failed to fetch posts");
+      const data = await response.json();
+      setPosts(data);
+    } catch (err) {
+      setError("Failed to load posts. Please try again.");
+      console.error("Error fetching posts:", err);
+    } finally {
+      setLoading(false);
+    }
+  }, [courseId, currentUser?.uid]);
+
+  /** Flag a post for TA/Admin review and optimistically mark it flagged in the local list. */
+  const handleFlagPost = async (postId) => {
+    if (!window.confirm("Are you sure you want to flag this post for review?"))
+      return;
+    try {
+      const res = await fetch(`http://localhost:8000/posts/${postId}/flag`, {
+        method: "POST",
+      });
+      if (res.ok) {
+        alert("Post has been flagged for TA review.");
+        // Optional: update local state to show it's flagged
+        setPosts((prev) =>
+          prev.map((p) => (p.id === postId ? { ...p, is_flagged: true } : p)),
+        );
+      }
+    } catch (err) {
+      console.error("Flagging failed", err);
+    }
+  };
+
+  // Initial fetch and refresh when user changes
+  useEffect(() => {
+    if (currentUser) {
+      fetchPosts();
+    }
+  }, [currentUser, courseId, fetchPosts]);
+
+  // Handle form input change
+  const handleFormChange = (e) => {
+    const { name, value } = e.target;
+    setFormData((prev) => ({
+      ...prev,
+      [name]: value,
+    }));
+    setFormError(null);
+  };
+
+  /**
+   * Validate and submit the new-post form: title required, optional
+   * description must be ≥5 chars, optional resource link must start
+   * with http(s)://. On success, close the form and refresh the list.
+   */
+  const handleCreatePost = async (e) => {
+    e.preventDefault();
+
+    // Validate fields
+    if (!formData.title.trim()) {
+      setFormError("Please enter a title for your post.");
+      return;
+    }
+    if (
+      formData.resource_link &&
+      !/^https?:\/\//i.test(formData.resource_link.trim())
+    ) {
+      setFormError("Resource link must start with http:// or https://");
+      return;
+    }
+    if (formData.description && formData.description.trim().length < 5) {
+      setFormError("Description must be at least 5 characters if provided.");
+      return;
+    }
+    setFormError(null);
+
+    try {
+      setIsSubmitting(true);
+      // Query parameters for metadata
+      const params = new URLSearchParams({
+        course_id: courseId || 0,
+        author_uid: currentUser.uid,
+      });
+
+      // JSON body for post content
+      const response = await fetch(
+        `http://localhost:8000/posts?${params.toString()}`,
+        {
+          method: "POST",
+          headers: { "Content-Type": "application/json" },
+          body: JSON.stringify({
+            title: formData.title.trim(),
+            description: formData.description.trim() || null,
+            resource_link: formData.resource_link.trim() || null,
+          }),
+        },
+      );
+
+      if (!response.ok) throw new Error("Failed to create post");
+
+      setFormData({ title: "", description: "", resource_link: "" });
+      setShowForm(false);
+      await fetchPosts();
+    } catch (err) {
+      setFormError(err.message);
+    } finally {
+      setIsSubmitting(false);
+    }
+  };
+
+  /**
+   * Cast or clear a vote on a post (`newVote` is +1, -1, or 0).
+   * Optimistically updates the local score using the previous user
+   * vote; refetches from the backend if the request fails so the UI
+   * doesn't drift from server truth.
+   */
+  const handleVote = async (postId, newVote) => {
+    if (!currentUser) return;
+    try {
+      const response = await fetch(
+        `http://localhost:8000/posts/${postId}/vote?user_uid=${currentUser.uid}&vote=${newVote}`,
+        { method: "POST" },
+      );
+      if (!response.ok) throw new Error("Failed to update vote");
+      // Optimistically update the post
+      setPosts((prevPosts) =>
+        prevPosts.map((post) => {
+          if (post.id === postId) {
+            let score = post.score - (post.user_vote || 0) + newVote;
+            return {
+              ...post,
+              score,
+              user_vote: newVote,
+            };
+          }
+          return post;
+        }),
+      );
+    } catch (err) {
+      console.error("Error updating vote:", err);
+      await fetchPosts();
+    }
+  };
+
+  /** Render an ISO timestamp as "just now" / "Nm ago" / "Nh ago" / "Nd ago" / fallback date. */
+  const formatDate = (isoString) => {
+    const date = new Date(isoString);
+    const now = new Date();
+    const diffMs = now - date;
+    const diffMins = Math.floor(diffMs / 60000);
+    const diffHours = Math.floor(diffMs / 3600000);
+    const diffDays = Math.floor(diffMs / 86400000);
+
+    if (diffMins < 1) return "just now";
+    if (diffMins < 60) return `${diffMins}m ago`;
+    if (diffHours < 24) return `${diffHours}h ago`;
+    if (diffDays < 7) return `${diffDays}d ago`;
+
+    return date.toLocaleDateString();
+  };
+
+  // Get author initial for avatar
+  const getInitial = (name) => {
+    return name ? name.charAt(0).toUpperCase() : "?";
+  };
+
+  /** Delete a post after a confirm prompt; backend enforces author/role permissions. */
+  const handleDeletePost = async (postId) => {
+    if (!window.confirm("Are you sure you want to delete this resource?"))
+      return;
+
+    try {
+      const response = await fetch(
+        `http://localhost:8000/posts/${postId}?user_uid=${currentUser.uid}`,
+        { method: "DELETE" },
+      );
+      if (!response.ok) throw new Error("Failed to delete post");
+      await fetchPosts();
+    } catch (err) {
+      alert(err.message);
+    }
+  };
+
+  return (
+    <div className="resources-page">
+      {/* Ambient blobs */}
+      <div className="blob blob-1" />
+      <div className="blob blob-2" />
+      <div className="blob blob-3" />
+
+      <div className="resources-wrapper">
+        <div className="resources-left">
+          <h1 className="tagline">
+            Resource <span className="highlight">Library</span>
+          </h1>
+          <p className="tagline-sub">
+            Access shared materials and study resources
+          </p>
+        </div>
+
+        <div className="resources-right">
+          {/* Post Creation Form */}
+          {currentUser && (
+            <div className="post-creation-form">
+              {!showForm ? (
+                <button
+                  onClick={() => setShowForm(true)}
+                  style={{
+                    width: "100%",
+                    padding: "12px 16px",
+                    background: "var(--navy-input)",
+                    border: "1px solid var(--border)",
+                    borderRadius: "10px",
+                    color: "var(--text-placeholder)",
+                    cursor: "pointer",
+                    fontSize: "14px",
+                    transition: "all 0.25s ease",
+                  }}
+                  onMouseEnter={(e) => {
+                    e.target.style.borderColor = "var(--teal)";
+                    e.target.style.color = "var(--text-secondary)";
+                  }}
+                  onMouseLeave={(e) => {
+                    e.target.style.borderColor = "var(--border)";
+                    e.target.style.color = "var(--text-placeholder)";
+                  }}
+                >
+                  Share a resource...
+                </button>
+              ) : (
+                <form onSubmit={handleCreatePost}>
+                  <h3 className="form-title">Create New Post</h3>
+
+                  <div className="form-field">
+                    <label>Title *</label>
+                    <input
+                      type="text"
+                      name="title"
+                      placeholder="e.g., Calculus Study Guide"
+                      value={formData.title}
+                      onChange={handleFormChange}
+                      maxLength={200}
+                    />
+                  </div>
+
+                  <div className="form-field">
+                    <label>Description</label>
+                    <textarea
+                      name="description"
+                      placeholder="Describe the resource or add notes..."
+                      value={formData.description}
+                      onChange={handleFormChange}
+                      maxLength={500}
+                    />
+                  </div>
+
+                  <div className="form-field">
+                    <label>Resource Link</label>
+                    <input
+                      type="url"
+                      name="resource_link"
+                      placeholder="https://example.com"
+                      value={formData.resource_link}
+                      onChange={handleFormChange}
+                    />
+                  </div>
+
+                  {formError && (
+                    <div className="error-message">{formError}</div>
+                  )}
+
+                  <div className="form-buttons">
+                    <button
+                      type="button"
+                      onClick={() => {
+                        setShowForm(false);
+                        setFormData({
+                          title: "",
+                          description: "",
+                          resource_link: "",
+                        });
+                        setFormError(null);
+                      }}
+                      className="btn-cancel"
+                      disabled={isSubmitting}
+                    >
+                      Cancel
+                    </button>
+                    <button
+                      type="submit"
+                      className="btn-post"
+                      disabled={isSubmitting}
+                    >
+                      {isSubmitting ? "Posting..." : "Post Resource"}
+                    </button>
+                  </div>
+                </form>
+              )}
+            </div>
+          )}
+
+          {/* Posts Feed */}
+          {error && <div className="error-message">{error}</div>}
+
+          {loading ? (
+            <div className="loading-state">
+              <div className="loading-spinner"></div>
+              Loading resources...
+            </div>
+          ) : posts.length === 0 ? (
+            <div className="empty-state">
+              <div className="empty-state-icon">📚</div>
+              <h3 className="empty-state-title">No resources yet</h3>
+              <p className="empty-state-text">
+                Be the first to share a resource with the community!
+              </p>
+            </div>
+          ) : (
+            <div className="posts-feed">
+              {posts.map((post) => (
+                <div key={post.id} className="post-card">
+                  <div className="post-upvote-section">
+                    {/* Upvote Button */}
+                    <button
+                      className={`upvote-button ${post.user_vote === 1 ? "active-vote" : ""}`}
+                      onClick={() =>
+                        handleVote(post.id, post.user_vote === 1 ? 0 : 1)
+                      }
+                      title={post.user_vote === 1 ? "Remove upvote" : "Upvote"}
+                      aria-label={
+                        post.user_vote === 1 ? "Remove upvote" : "Upvote"
+                      }
+                    >
+                      ▲
+                    </button>
+
+                    {/* Vote Count */}
+                    <div
+                      className={`upvote-count 
+      ${post.score < 0 ? "negative-score" : ""} 
+      ${post.user_vote !== 0 ? "active-highlight" : ""}`}
+                    >
+                      {post.score}
+                    </div>
+
+                    {/* Downvote Button */}
+                    <button
+                      className={`upvote-button ${post.user_vote === -1 ? "active-vote" : ""}`}
+                      onClick={() =>
+                        handleVote(post.id, post.user_vote === -1 ? 0 : -1)
+                      }
+                      title={
+                        post.user_vote === -1 ? "Remove downvote" : "Downvote"
+                      }
+                      aria-label={
+                        post.user_vote === -1 ? "Remove downvote" : "Downvote"
+                      }
+                    >
+                      ▼
+                    </button>
+                  </div>
+
+                  <div className="post-content">
+                    <div className="post-header">
+                      <div className="post-author-info">
+                        <div className="author-avatar">
+                          {getInitial(post.author_name)}
+                        </div>
+                        <div className="author-details">
+                          <div className="author-name">{post.author_name}</div>
+                          <span className={`author-role ${post.author_role}`}>
+                            {post.author_role}
+                          </span>
+                        </div>
+                      </div>
+                      <div
+                        style={{
+                          display: "flex",
+                          alignItems: "center",
+                          gap: "12px",
+                        }}
+                      >
+                        <div className="post-timestamp">
+                          {formatDate(post.created_at)}
+                        </div>
+                        {/* ONLY SHOW FOR TA/ADMIN */}
+                        {(currentUser?.role === "TA" ||
+                          currentUser?.role === "Admin") && (
+                          <button
+                            onClick={() => handleDeletePost(post.id)}
+                            className="icon-btn-outline delete-outline"
+                            title="Delete Post"
+                          >
+                            🗑
+                          </button>
+                        )}
+
+                        {/* FLAG CONTENT BUTTON */}
+                        <button
+                          onClick={() => handleFlagPost(post.id)}
+                          className={`icon-btn-outline flag-outline ${post.is_flagged ? "active" : ""}`}
+                          disabled={post.is_flagged}
+                          title={
+                            post.is_flagged
+                              ? "Flagged for Review"
+                              : "Flag Content"
+                          }
+                        >
+                          {post.is_flagged ? "🚩" : "🏳"}
+                        </button>
+                      </div>
+                    </div>
+
+                    <h3 className="post-title">{post.title}</h3>
+
+                    {post.description && (
+                      <p className="post-description">{post.description}</p>
+                    )}
+
+                    {post.resource_link && (
+                      <a
+                        href={post.resource_link}
+                        target="_blank"
+                        rel="noopener noreferrer"
+                        className="post-resource-link"
+                      >
+                        🔗 {post.resource_link}
+                      </a>
+                    )}
+                  </div>
+                </div>
+              ))}
+            </div>
+          )}
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default ResourcesPage;
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.5 on Wed Apr 29 2026 18:41:17 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/frontend/SchedulePage.js.html b/docs/frontend/SchedulePage.js.html new file mode 100644 index 0000000..ddeb16a --- /dev/null +++ b/docs/frontend/SchedulePage.js.html @@ -0,0 +1,1049 @@ + + + + + JSDoc: Source: SchedulePage.js + + + + + + + + + + +
+ +

Source: SchedulePage.js

+ + + + + + +
+
+
/**
+ * Class-scoped study-session scheduler (mounted at /class/:courseId/schedule).
+ *
+ * Lets students browse and create solo or group study sessions for the
+ * current course, sync availability from Google Calendar via Google
+ * Identity Services, and view a weekly calendar grid. Helper utilities
+ * at the top of the file (`loadGoogleScript`, `getWeekRange`,
+ * `formatHourLabel`, …) are used throughout the component to keep the
+ * render logic readable.
+ *
+ * @module SchedulePage
+ */
+import "./SchedulePage.css";
+import { useEffect, useState, useCallback } from "react";
+import { auth } from "./firebase";
+import { useParams } from "react-router-dom";
+
+const API_BASE = process.env.REACT_APP_API_BASE_URL || "http://127.0.0.1:8000";
+const GOOGLE_CLIENT_ID = process.env.REACT_APP_GOOGLE_CLIENT_ID;
+
+/**
+ * Lazily inject the Google Identity Services script and resolve once
+ * `window.google.accounts.oauth2` is available. Reuses the existing
+ * <script> tag if one was added earlier so the script only loads once
+ * per page.
+ *
+ * @returns {Promise<void>}
+ */
+const loadGoogleScript = () =>
+  new Promise((resolve, reject) => {
+    if (window.google?.accounts?.oauth2) {
+      resolve();
+      return;
+    }
+
+    const existing = document.querySelector('script[data-google-identity="1"]');
+    if (existing) {
+      existing.addEventListener("load", () => resolve());
+      existing.addEventListener("error", () => reject(new Error("Could not load Google Identity script")));
+      return;
+    }
+
+    const script = document.createElement("script");
+    script.src = "https://accounts.google.com/gsi/client";
+    script.async = true;
+    script.defer = true;
+    script.dataset.googleIdentity = "1";
+    script.onload = () => resolve();
+    script.onerror = () => reject(new Error("Could not load Google Identity script"));
+    document.body.appendChild(script);
+  });
+
+/**
+ * Return the [Sunday, next Sunday) range covering the week that
+ * contains `date`, with the start aligned to local midnight.
+ *
+ * @param {Date} date - Any date in the target week.
+ * @returns {{ start: Date, end: Date }} Half-open week boundaries.
+ */
+const getWeekRange = (date) => {
+  const start = new Date(date);
+  start.setHours(0, 0, 0, 0);
+  start.setDate(start.getDate() - start.getDay());
+
+  const end = new Date(start);
+  end.setDate(end.getDate() + 7);
+
+  return { start, end };
+};
+
+/** Format an hour-of-day (0–23) as a localized "h:mm AM/PM" label. */
+const formatHourLabel = (hour) =>
+  new Date(2000, 0, 1, hour).toLocaleTimeString([], {
+    hour: "numeric",
+    minute: "2-digit",
+  });
+
+/** Normalize a free-text query for case-insensitive substring matching. */
+const normalizeSearchValue = (value) => value.trim().toLowerCase();
+
+/**
+ * Format a Date as the value an `<input type="datetime-local">` expects
+ * (`YYYY-MM-DDTHH:MM` in local time). Used to pre-fill the meeting form.
+ */
+const toLocalDateTimeInputValue = (date) => {
+  const year = date.getFullYear();
+  const month = `${date.getMonth() + 1}`.padStart(2, "0");
+  const day = `${date.getDate()}`.padStart(2, "0");
+  const hours = `${date.getHours()}`.padStart(2, "0");
+  const minutes = `${date.getMinutes()}`.padStart(2, "0");
+  return `${year}-${month}-${day}T${hours}:${minutes}`;
+};
+
+const SchedulePage = () => {
+  const { courseId } = useParams();
+  const isClassScoped = Boolean(courseId);
+  const [gcalConnected, setGcalConnected] = useState(false);
+  const [classmates, setClassmates] = useState([]);
+  const [selectedClassmates, setSelectedClassmates] = useState([]);
+  const [calendarView, setCalendarView] = useState("mine");
+  const [availabilityByEmail, setAvailabilityByEmail] = useState({});
+  const [showScheduleMeetingModal, setShowScheduleMeetingModal] = useState(false);
+  const [editingSession, setEditingSession] = useState(null);
+  const [status, setStatus] = useState("");
+  const [loading, setLoading] = useState(false);
+  const [courses, setCourses] = useState([]);
+  const [activeCourse, setActiveCourse] = useState(null);
+  const [currentWeek, setCurrentWeek] = useState(new Date());
+  const [compareSearchQuery, setCompareSearchQuery] = useState("");
+  const [meetingSearchQuery, setMeetingSearchQuery] = useState("");
+  const [upcomingSessions, setUpcomingSessions] = useState([]);
+
+  const user = auth.currentUser;
+  const userEmail = user?.email || "";
+
+  const [meetingForm, setMeetingForm] = useState({
+    title: "",
+    startTime: "",
+    endTime: "",
+    courseId: courseId || "",
+  });
+
+  /** Clear the meeting form fields, classmate selection, and edit state. */
+  const resetMeetingForm = useCallback(() => {
+    setMeetingForm({
+      title: "",
+      startTime: "",
+      endTime: "",
+      courseId: courseId || "",
+    });
+    setSelectedClassmates([]);
+    setMeetingSearchQuery("");
+    setEditingSession(null);
+  }, [courseId]);
+
+  /**
+   * Request a Google OAuth access token with the calendar.readonly scope.
+   * Pass `prompt="consent"` on the first connection to force the picker;
+   * pass `""` for silent refresh on subsequent calls.
+   *
+   * @param {string} [prompt] - Google Identity Services prompt mode.
+   * @returns {Promise<string>} Access token usable against the Calendar API.
+   */
+  const getGoogleAccessToken = useCallback(async (prompt = "") => {
+    if (!GOOGLE_CLIENT_ID) {
+      throw new Error("Missing REACT_APP_GOOGLE_CLIENT_ID in frontend env");
+    }
+
+    await loadGoogleScript();
+
+    return new Promise((resolve, reject) => {
+      const tokenClient = window.google.accounts.oauth2.initTokenClient({
+        client_id: GOOGLE_CLIENT_ID,
+        scope: "https://www.googleapis.com/auth/calendar.readonly",
+        callback: (response) => {
+          if (response?.access_token) {
+            resolve(response.access_token);
+            return;
+          }
+          reject(new Error(response?.error || "Google OAuth failed"));
+        },
+      });
+
+      tokenClient.requestAccessToken({ prompt });
+    });
+  }, []);
+
+  /**
+   * Load busy blocks for the given emails over the currently displayed
+   * week and store them in `availabilityByEmail` for the calendar grid.
+   *
+   * @param {string[]} emails - Emails whose availability should be fetched.
+   */
+  const fetchAvailability = useCallback(async (emails) => {
+    if (!userEmail || emails.length === 0) {
+      setAvailabilityByEmail({});
+      return;
+    }
+
+    try {
+      const { start, end } = getWeekRange(currentWeek);
+      const params = new URLSearchParams({
+        time_min: start.toISOString(),
+        time_max: end.toISOString(),
+      });
+      emails.forEach((email) => params.append("user_emails", email));
+
+      const response = await fetch(`${API_BASE}/availability?${params.toString()}`);
+      if (!response.ok) {
+        const error = await response.json();
+        throw new Error(error.detail || "Failed to load availability");
+      }
+
+      const data = await response.json();
+      setAvailabilityByEmail(data.availability || {});
+    } catch (error) {
+      console.error("Error fetching availability:", error);
+      setStatus(`Error: ${error.message}`);
+    }
+  }, [currentWeek, userEmail]);
+
+  /**
+   * Pull busy times from the user's Google Calendar via freeBusy and
+   * push them to the StudySync backend. Refreshes availability for the
+   * currently displayed week on success.
+   *
+   * @param {string} [prompt] - "consent" on first connect, "" for refresh.
+   */
+  const syncGoogleCalendar = useCallback(async (prompt = "") => {
+    if (!userEmail) return;
+
+    setLoading(true);
+    setStatus(prompt === "consent" ? "Connecting to Google Calendar..." : "Refreshing Google Calendar...");
+
+    try {
+      const token = await getGoogleAccessToken(prompt);
+      const { start, end } = getWeekRange(currentWeek);
+
+      const freeBusyResponse = await fetch("https://www.googleapis.com/calendar/v3/freeBusy", {
+        method: "POST",
+        headers: {
+          Authorization: `Bearer ${token}`,
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify({
+          timeMin: start.toISOString(),
+          timeMax: end.toISOString(),
+          items: [{ id: "primary" }],
+        }),
+      });
+
+      if (!freeBusyResponse.ok) {
+        throw new Error("Failed to read Google Calendar busy times");
+      }
+
+      const freeBusyJson = await freeBusyResponse.json();
+      const busySlots =
+        freeBusyJson?.calendars?.primary?.busy?.map((slot) => ({
+          starts_at: slot.start,
+          ends_at: slot.end,
+        })) || [];
+
+      const backendResponse = await fetch(`${API_BASE}/availability/sync`, {
+        method: "POST",
+        headers: { "Content-Type": "application/json" },
+        body: JSON.stringify({
+          user_email: userEmail,
+          starts_at: start.toISOString(),
+          ends_at: end.toISOString(),
+          timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
+          source: "google_calendar",
+          busy_slots: busySlots,
+        }),
+      });
+
+      if (!backendResponse.ok) {
+        const error = await backendResponse.json();
+        throw new Error(error.detail || "Failed to sync Google Calendar");
+      }
+
+      setGcalConnected(true);
+      setStatus(`Synced ${busySlots.length} busy blocks from Google Calendar`);
+      await fetchAvailability([userEmail, ...selectedClassmates]);
+    } catch (error) {
+      console.error("Error syncing Google Calendar:", error);
+      setStatus(`Error: ${error.message}`);
+    } finally {
+      setLoading(false);
+    }
+  }, [currentWeek, fetchAvailability, getGoogleAccessToken, selectedClassmates, userEmail]);
+
+  /** Ask the backend whether this user has a stored Google Calendar token. */
+  const checkGcalConnection = useCallback(async () => {
+    if (!userEmail) return;
+
+    try {
+      const response = await fetch(
+        `${API_BASE}/availability/connected?user_email=${encodeURIComponent(userEmail)}`
+      );
+      if (!response.ok) {
+        throw new Error("Failed to check Google Calendar status");
+      }
+      const data = await response.json();
+      setGcalConnected(Boolean(data.connected));
+    } catch (error) {
+      console.error("Error checking GCal connection:", error);
+    }
+  }, [userEmail]);
+
+  /**
+   * Load the user's enrolled courses and the roster of classmates to
+   * compare schedules with. In class-scoped mode the list is restricted
+   * to members of the active course; otherwise every StudySync user is
+   * available as a potential study partner.
+   */
+  const fetchClassmates = useCallback(async () => {
+    if (!user?.uid) return;
+
+    try {
+      const response = await fetch(`${API_BASE}/users/${user.uid}/courses`);
+      if (response.ok) {
+        const enrolledCourses = await response.json();
+        const visibleCourses = isClassScoped
+          ? enrolledCourses.filter((course) => course.id === parseInt(courseId, 10))
+          : enrolledCourses;
+
+        setCourses(visibleCourses);
+        setActiveCourse(isClassScoped ? visibleCourses[0] || null : null);
+        if (isClassScoped && visibleCourses.length > 0) {
+          setMeetingForm((current) => ({
+            ...current,
+            courseId: `${visibleCourses[0].id}`,
+          }));
+        }
+
+        const classmatesUrl = isClassScoped
+          ? `${API_BASE}/courses/${courseId}/members`
+          : `${API_BASE}/users`;
+        const usersResponse = await fetch(classmatesUrl);
+        if (!usersResponse.ok) {
+          throw new Error("Failed to load classmates");
+        }
+
+        const users = await usersResponse.json();
+        setClassmates(users.filter((u) => u.email !== userEmail));
+      }
+    } catch (error) {
+      console.error("Error fetching classmates:", error);
+    }
+  }, [courseId, isClassScoped, user?.uid, userEmail]);
+
+  /**
+   * Load the next 14 days of study sessions for the active class so
+   * the upcoming-sessions sidebar stays in sync. No-op outside
+   * class-scoped mode.
+   */
+  const fetchUpcomingSessions = useCallback(async () => {
+    if (!isClassScoped || !courseId || !userEmail) {
+      setUpcomingSessions([]);
+      return;
+    }
+
+    try {
+      const rangeStart = new Date();
+      const rangeEnd = new Date(rangeStart.getTime() + 14 * 24 * 60 * 60 * 1000);
+      const params = new URLSearchParams({
+        range_start: rangeStart.toISOString(),
+        range_end: rangeEnd.toISOString(),
+        requester_email: userEmail,
+      });
+      const response = await fetch(
+        `${API_BASE}/study-sessions/course/${courseId}?${params.toString()}`
+      );
+      if (!response.ok) {
+        throw new Error("Failed to load upcoming class sessions");
+      }
+
+      const sessions = await response.json();
+      setUpcomingSessions(sessions);
+    } catch (error) {
+      console.error("Error fetching class sessions:", error);
+    }
+  }, [courseId, isClassScoped, userEmail]);
+
+  /**
+   * Submit the meeting form: POST a new study session, or PUT an update
+   * if `editingSession` is set. The session is "group" when classmates
+   * are selected and "solo" otherwise. Refreshes upcoming sessions and
+   * availability so the calendar reflects the new block.
+   */
+  const handleScheduleMeeting = async () => {
+    if (!meetingForm.title || !meetingForm.startTime || !meetingForm.endTime || !meetingForm.courseId) {
+      setStatus("Please fill in all required fields");
+      return;
+    }
+
+    try {
+      setLoading(true);
+      const response = await fetch(
+        editingSession
+          ? `${API_BASE}/study-sessions/${editingSession.id}`
+          : `${API_BASE}/study-sessions`,
+        {
+          method: editingSession ? "PUT" : "POST",
+          headers: { "Content-Type": "application/json" },
+          body: JSON.stringify({
+            title: meetingForm.title,
+            course_id: parseInt(meetingForm.courseId, 10),
+            session_type: selectedClassmates.length > 0 ? "group" : "solo",
+            starts_at: new Date(meetingForm.startTime).toISOString(),
+            ends_at: new Date(meetingForm.endTime).toISOString(),
+            creator_email: userEmail,
+            invitees: selectedClassmates,
+          }),
+        }
+      );
+
+      if (response.ok) {
+        setStatus(
+          editingSession
+            ? "Study meeting updated."
+            : selectedClassmates.length > 0
+              ? "Class session scheduled. It is now visible in StudySync for this class."
+              : "Session scheduled to your StudySync calendar for this class."
+        );
+        setShowScheduleMeetingModal(false);
+        resetMeetingForm();
+        await fetchUpcomingSessions();
+        await fetchAvailability([userEmail, ...selectedClassmates]);
+        setTimeout(() => setStatus(""), 3000);
+      } else {
+        const error = await response.json();
+        setStatus(`Error: ${error.detail}`);
+      }
+    } catch (error) {
+      console.error("Error scheduling meeting:", error);
+      setStatus(`Error: ${error.message}`);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  /**
+   * Pre-fill the meeting form with an existing session and open the
+   * modal in edit mode. Only the session's creator can edit; other
+   * users get a no-op.
+   */
+  const openEditMeetingModal = useCallback((session) => {
+    if (session.creator_email !== userEmail) {
+      return;
+    }
+
+    setEditingSession(session);
+    setMeetingForm({
+      title: session.title,
+      startTime: toLocalDateTimeInputValue(new Date(session.starts_at)),
+      endTime: toLocalDateTimeInputValue(new Date(session.ends_at)),
+      courseId: `${session.course_id}`,
+    });
+    setSelectedClassmates(session.invitees || []);
+    setMeetingSearchQuery("");
+    setShowScheduleMeetingModal(true);
+  }, [userEmail]);
+
+  // Initial load: GCal status, classmate roster, upcoming class sessions.
+  useEffect(() => {
+    checkGcalConnection();
+    fetchClassmates();
+    fetchUpcomingSessions();
+  }, [checkGcalConnection, fetchClassmates, fetchUpcomingSessions]);
+
+  // Re-fetch availability whenever the user changes week or selects a
+  // different classmate to compare against.
+  useEffect(() => {
+    if (!userEmail) return;
+    fetchAvailability([userEmail, ...selectedClassmates]);
+  }, [currentWeek, fetchAvailability, selectedClassmates, userEmail]);
+
+  // Drop any selected classmates that disappear from the roster (e.g.
+  // when switching from global to class-scoped view).
+  useEffect(() => {
+    const validEmails = new Set(classmates.map((classmate) => classmate.email));
+    setSelectedClassmates((current) => current.filter((email) => validEmails.has(email)));
+  }, [classmates]);
+
+  /** Return the seven Date objects that make up the currently displayed week. */
+  const getWeekDates = () => {
+    const { start } = getWeekRange(currentWeek);
+    return Array.from({ length: 7 }, (_, index) => {
+      const date = new Date(start);
+      date.setDate(start.getDate() + index);
+      return date;
+    });
+  };
+
+  const getBusyBlocks = (email) => availabilityByEmail[email] || [];
+
+  /**
+   * True if any of the given users has a busy block overlapping the
+   * one-hour slot starting at `hour` on `date`.
+   */
+  const isBusyForEmails = (emails, date, hour) => {
+    const slotStart = new Date(date);
+    slotStart.setHours(hour, 0, 0, 0);
+    const slotEnd = new Date(slotStart);
+    slotEnd.setHours(hour + 1);
+
+    return emails.some((email) =>
+      getBusyBlocks(email).some((block) => {
+        const busyStart = new Date(block.starts_at);
+        const busyEnd = new Date(block.ends_at);
+        return busyStart < slotEnd && busyEnd > slotStart;
+      })
+    );
+  };
+
+  /**
+   * Render the 7-day × 14-hour calendar grid (8am–9pm), shading each
+   * cell free or busy based on `emailsToCheck`. In Compare view,
+   * clicking a free slot pre-fills the meeting form and opens the
+   * scheduling modal.
+   */
+  const renderAvailabilityGrid = (emailsToCheck) => {
+    const weekDates = getWeekDates();
+    const hours = Array.from({ length: 14 }, (_, i) => i + 8);
+
+    return (
+      <div className="availability-grid">
+        <div className="grid-header">
+          <div className="time-header"></div>
+          {weekDates.map((date) => (
+            <div key={date.toDateString()} className="day-header">
+              {date.toLocaleDateString("en-US", { weekday: "short", month: "numeric", day: "numeric" })}
+            </div>
+          ))}
+        </div>
+        {hours.map((hour) => (
+          <div key={hour} className="grid-row">
+            <div className="time-cell">{formatHourLabel(hour)}</div>
+            {weekDates.map((date) => {
+              const isFree = !isBusyForEmails(emailsToCheck, date, hour);
+              return (
+                <div
+                  key={`${date.toDateString()}-${hour}`}
+                  className={`time-slot ${isFree ? "free" : "busy"}`}
+                  onClick={() => {
+                    if (!isFree || calendarView !== "compare") return;
+
+                    const startTime = new Date(date);
+                    startTime.setHours(hour, 0, 0, 0);
+                    const endTime = new Date(startTime);
+                    endTime.setHours(hour + 1);
+
+                    setMeetingForm((current) => ({
+                      ...current,
+                      startTime: toLocalDateTimeInputValue(startTime),
+                      endTime: toLocalDateTimeInputValue(endTime),
+                    }));
+                    setShowScheduleMeetingModal(true);
+                  }}
+                  title={isFree ? "Free" : "Busy"}
+                >
+                  <span className="time-slot-label">{isFree ? "Free" : "Busy"}</span>
+                </div>
+              );
+            })}
+          </div>
+        ))}
+      </div>
+    );
+  };
+
+  const compareEmails = [userEmail, ...selectedClassmates];
+  const filteredCompareClassmates = classmates.filter((classmate) => {
+    const query = normalizeSearchValue(compareSearchQuery);
+    if (!query) return true;
+
+    return [classmate.full_name, classmate.email].some((value) =>
+      value?.toLowerCase().includes(query)
+    );
+  });
+  const filteredMeetingClassmates = classmates.filter((classmate) => {
+    const query = normalizeSearchValue(meetingSearchQuery);
+    if (!query) return true;
+
+    return [classmate.full_name, classmate.email].some((value) =>
+      value?.toLowerCase().includes(query)
+    );
+  });
+
+  return (
+    <div className="schedule-page">
+      <div className="schedule-container">
+        <div className="calendar-section">
+          <div className="calendar-controls">
+            <div className="calendar-header">
+              <h2>{isClassScoped ? "Class Schedule & Study Partners" : "Schedule & Find Study Partners"}</h2>
+            </div>
+            <div className="calendar-toolbar">
+              <button
+                className={`view-btn ${calendarView === "mine" ? "active" : ""}`}
+                onClick={() => setCalendarView("mine")}
+              >
+                My Calendar
+              </button>
+              <button
+                className={`view-btn ${calendarView === "compare" ? "active" : ""}`}
+                onClick={() => setCalendarView("compare")}
+              >
+                Compare & Schedule
+              </button>
+              <button
+                className="nav-btn"
+                onClick={() => setCurrentWeek(new Date(currentWeek.getTime() - 7 * 24 * 60 * 60 * 1000))}
+              >
+                ← Prev Week
+              </button>
+              <button className="nav-btn" onClick={() => setCurrentWeek(new Date())}>
+                This Week
+              </button>
+              <button
+                className="nav-btn"
+                onClick={() => setCurrentWeek(new Date(currentWeek.getTime() + 7 * 24 * 60 * 60 * 1000))}
+              >
+                Next Week →
+              </button>
+            </div>
+          </div>
+
+          <div className="calendar-view">
+            {!gcalConnected ? (
+              <div className="schedule-empty-state">
+                <h3>Connect Google Calendar to Get Started</h3>
+                <p>Connect your calendar to sync your busy times and compare schedules with classmates</p>
+              </div>
+            ) : calendarView === "mine" ? (
+              <div className="schedule-panel">
+                <h3>Your Availability This Week</h3>
+                <p className="schedule-intro">
+                  Green blocks mean you are open during that hour. Busy blocks reflect the Google Calendar events you synced for this week.
+                </p>
+                {renderAvailabilityGrid([userEmail])}
+              </div>
+            ) : (
+              <div className="schedule-panel">
+                <h3>Find Common Free Times</h3>
+                <p className="schedule-intro">
+                  {isClassScoped
+                    ? "Pick classmates in this class to highlight hours where everyone selected is available."
+                    : "Pick classmates below to highlight hours where everyone selected is available."}
+                </p>
+                <div className="compare-section">
+                  <label>
+                    {isClassScoped
+                      ? `Select classmates in ${activeCourse?.course_code || "this class"}:`
+                      : "Select classmates to compare calendars with:"}
+                  </label>
+                  <input
+                    type="search"
+                    className="classmate-search-input"
+                    value={compareSearchQuery}
+                    onChange={(e) => setCompareSearchQuery(e.target.value)}
+                    placeholder="Search classmates by name or email"
+                  />
+                  <div className="compare-classmates">
+                    {filteredCompareClassmates.length > 0 ? (
+                      filteredCompareClassmates.map((classmate) => (
+                        <label key={classmate.email} className="classmate-option">
+                          <input
+                            type="checkbox"
+                            checked={selectedClassmates.includes(classmate.email)}
+                            onChange={(e) => {
+                              if (e.target.checked) {
+                                setSelectedClassmates((current) => [...current, classmate.email]);
+                              } else {
+                                setSelectedClassmates((current) =>
+                                  current.filter((email) => email !== classmate.email)
+                                );
+                              }
+                            }}
+                          />
+                          <span>{classmate.full_name}</span>
+                        </label>
+                      ))
+                    ) : (
+                      <p className="classmate-search-empty">No classmates match that search.</p>
+                    )}
+                  </div>
+                </div>
+                {selectedClassmates.length > 0 ? (
+                  <div className="compare-grid-wrap">
+                    <p className="compare-note">
+                      Green means everyone selected is free. Red means at least one person is busy.
+                    </p>
+                    {renderAvailabilityGrid(compareEmails)}
+                  </div>
+                ) : (
+                  <p>Select classmates above to see common free times</p>
+                )}
+              </div>
+            )}
+          </div>
+        </div>
+
+        <div className="sidebar">
+          <div className="sidebar-card google-calendar-section">
+              <div className="card-title">
+                <svg className="card-icon" fill="currentColor" viewBox="0 0 24 24">
+                  <path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11z" />
+                </svg>
+                Google Calendar
+              </div>
+            {gcalConnected ? (
+              <>
+                <div className="connection-status status-connected">✓ Synced</div>
+                <button onClick={() => syncGoogleCalendar("")} disabled={loading}>
+                  {loading ? "Syncing..." : "Refresh This Week"}
+                </button>
+              </>
+            ) : (
+              <button onClick={() => syncGoogleCalendar("consent")} disabled={loading}>
+                {loading ? "Connecting..." : "Connect Google Calendar"}
+              </button>
+            )}
+          </div>
+
+          <div className="sidebar-card">
+            <div className="card-title">
+              <svg className="card-icon" fill="currentColor" viewBox="0 0 24 24">
+                <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm3.5-9c.83 0 1.5-.67 1.5-1.5S16.33 8 15.5 8 14 8.67 14 9.5s.67 1.5 1.5 1.5zm-7 0c.83 0 1.5-.67 1.5-1.5S9.33 8 8.5 8 7 8.67 7 9.5 7.67 11 8.5 11zm3.5 6.5c2.33 0 4.31-1.46 5.11-3.5H6.89c.8 2.04 2.78 3.5 5.11 3.5z" />
+              </svg>
+              Quick Actions
+            </div>
+            <div className="quick-actions">
+              <button
+                className="action-btn"
+                onClick={() => {
+                  resetMeetingForm();
+                  setShowScheduleMeetingModal(true);
+                }}
+              >
+                + Schedule Meeting
+              </button>
+            </div>
+          </div>
+
+          {isClassScoped && (
+            <div className="sidebar-card">
+              <div className="card-title">
+                <svg className="card-icon" fill="currentColor" viewBox="0 0 24 24">
+                  <path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5s-3 1.34-3 3 1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5C15 14.17 10.33 13 8 13zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.98 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z" />
+                </svg>
+                Class List
+              </div>
+              {classmates.length > 0 ? (
+                <div className="class-roster-list">
+                  {classmates.map((classmate) => {
+                    const isSelected = selectedClassmates.includes(classmate.email);
+                    return (
+                      <button
+                        key={classmate.email}
+                        type="button"
+                        className={`class-roster-item ${isSelected ? "selected" : ""}`}
+                        onClick={() => {
+                          setSelectedClassmates((current) =>
+                            isSelected
+                              ? current.filter((email) => email !== classmate.email)
+                              : [...current, classmate.email]
+                          );
+                          setCalendarView("compare");
+                        }}
+                      >
+                        <div className="class-roster-main">
+                          <div className="class-roster-name">{classmate.full_name}</div>
+                          <div className="class-roster-meta">{classmate.email}</div>
+                        </div>
+                        <div className="class-roster-role">{classmate.role || "Student"}</div>
+                      </button>
+                    );
+                  })}
+                </div>
+              ) : (
+                <p className="class-session-empty">
+                  No classmates found yet for this course.
+                </p>
+              )}
+            </div>
+          )}
+
+          {isClassScoped && (
+            <div className="sidebar-card">
+              <div className="card-title">
+                <svg className="card-icon" fill="currentColor" viewBox="0 0 24 24">
+                  <path d="M7 2v2H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-2V2h-2v2H9V2H7zm12 8H5v8h14v-8z" />
+                </svg>
+                Upcoming For This Class
+              </div>
+              {upcomingSessions.length > 0 ? (
+                <div className="class-session-list">
+                  {upcomingSessions.slice(0, 5).map((session) => (
+                    <div
+                      key={session.id}
+                      className={`class-session-item ${session.creator_email === userEmail ? "editable" : ""}`}
+                    >
+                      <div className="class-session-title">{session.title}</div>
+                      <div className="class-session-meta">
+                        {new Date(session.starts_at).toLocaleString([], {
+                          month: "short",
+                          day: "numeric",
+                          hour: "numeric",
+                          minute: "2-digit",
+                        })}
+                      </div>
+                      {session.creator_email === userEmail && (
+                        <button
+                          type="button"
+                          className="session-edit-btn"
+                          onClick={() => openEditMeetingModal(session)}
+                        >
+                          Edit
+                        </button>
+                      )}
+                    </div>
+                  ))}
+                </div>
+              ) : (
+                <p className="class-session-empty">No upcoming class sessions yet.</p>
+              )}
+            </div>
+          )}
+
+          {status && (
+            <div
+              className={`status-message ${
+                status.includes("Error")
+                  ? "status-error"
+                  : status.includes("Synced") || status.includes("connected")
+                    ? "status-success"
+                    : "status-loading"
+              }`}
+            >
+              {status}
+            </div>
+          )}
+        </div>
+      </div>
+
+      {showScheduleMeetingModal && (
+        <div className="modal-overlay" onClick={() => setShowScheduleMeetingModal(false)}>
+          <div className="modal-card" onClick={(e) => e.stopPropagation()}>
+            <div className="modal-header">
+              <div>
+                <h3>{editingSession ? "Edit Study Meeting" : "Schedule Study Meeting"}</h3>
+                <p className="modal-subtitle">
+                  {editingSession
+                    ? "Update the meeting details and invited classmates."
+                    : "Pick a time and create a StudySync session that classmates in this course can see."}
+                </p>
+              </div>
+              <button
+                type="button"
+                className="modal-close-btn"
+                aria-label="Close schedule meeting dialog"
+                onClick={() => setShowScheduleMeetingModal(false)}
+              >
+                ×
+              </button>
+            </div>
+            {status && (
+              <div
+                className={`status-message ${
+                  status.includes("Error") ? "status-error" : "status-success"
+                }`}
+              >
+                {status}
+              </div>
+            )}
+            <div className="modal-section">
+              <div className="modal-section-title">Meeting Details</div>
+              <div className="form-group">
+                <label>Meeting Title *</label>
+                <input
+                  type="text"
+                  value={meetingForm.title}
+                  onChange={(e) =>
+                    setMeetingForm({ ...meetingForm, title: e.target.value })
+                  }
+                  placeholder="e.g., Midterm Study Group"
+                />
+              </div>
+              {isClassScoped ? (
+                <div className="form-group">
+                  <label>Course</label>
+                  <div className="locked-course-field">
+                    {courses[0]
+                      ? `${courses[0].course_code} - ${courses[0].name}`
+                      : "Current class"}
+                  </div>
+                </div>
+              ) : (
+                <div className="form-group">
+                  <label>Course *</label>
+                  <select
+                    value={meetingForm.courseId}
+                    onChange={(e) =>
+                      setMeetingForm({ ...meetingForm, courseId: e.target.value })
+                    }
+                  >
+                    <option value="">Select a course</option>
+                    {courses.map((course) => (
+                      <option key={course.id} value={course.id}>
+                        {course.course_code} - {course.name}
+                      </option>
+                    ))}
+                  </select>
+                </div>
+              )}
+              <div className="form-row">
+                <div className="form-group">
+                  <label>Start Time *</label>
+                  <input
+                    type="datetime-local"
+                    value={meetingForm.startTime}
+                    onChange={(e) =>
+                      setMeetingForm({ ...meetingForm, startTime: e.target.value })
+                    }
+                  />
+                </div>
+                <div className="form-group">
+                  <label>End Time *</label>
+                  <input
+                    type="datetime-local"
+                    value={meetingForm.endTime}
+                    onChange={(e) =>
+                      setMeetingForm({ ...meetingForm, endTime: e.target.value })
+                    }
+                  />
+                </div>
+              </div>
+            </div>
+            <div className="modal-section">
+              <div className="modal-section-title">Invite Classmates</div>
+              <div className="form-group">
+                <label>Tag classmates (optional)</label>
+                <input
+                  type="search"
+                  className="classmate-search-input"
+                  value={meetingSearchQuery}
+                  onChange={(e) => setMeetingSearchQuery(e.target.value)}
+                  placeholder="Search classmates to tag in this session"
+                />
+                <p className="invite-help-text">
+                  This only helps you plan the session in StudySync. It will not send Google Calendar invites.
+                </p>
+              </div>
+              <div className="modal-classmate-list">
+                {filteredMeetingClassmates.length > 0 ? (
+                  filteredMeetingClassmates.map((classmate) => (
+                    <label key={classmate.email} className="classmate-option">
+                      <input
+                        type="checkbox"
+                        checked={selectedClassmates.includes(classmate.email)}
+                        onChange={(e) => {
+                          if (e.target.checked) {
+                            setSelectedClassmates((current) => [...current, classmate.email]);
+                          } else {
+                            setSelectedClassmates((current) =>
+                              current.filter((email) => email !== classmate.email)
+                            );
+                          }
+                        }}
+                      />
+                      <span>{classmate.full_name}</span>
+                    </label>
+                  ))
+                ) : (
+                  <p className="classmate-search-empty">No classmates match that search.</p>
+                )}
+              </div>
+              <div className="selected-attendees">
+                <div className="selected-attendees-title">Selected invitees</div>
+                <div className="attendee-list">
+                  {selectedClassmates.length > 0 ? (
+                    selectedClassmates.map((email) => (
+                      <div key={email} className="attendee-tag">
+                        {email}
+                      </div>
+                    ))
+                  ) : (
+                    <p className="classmate-search-empty">
+                      No classmates selected. This session will still appear in the class schedule.
+                    </p>
+                  )}
+                </div>
+              </div>
+            </div>
+            <div className="modal-actions">
+              <button
+                className="btn-subtle-link"
+                onClick={() => {
+                  setShowScheduleMeetingModal(false);
+                  resetMeetingForm();
+                }}
+              >
+                Cancel
+              </button>
+              <button className="btn-submit" onClick={handleScheduleMeeting} disabled={loading}>
+                {loading ? (editingSession ? "Saving..." : "Scheduling...") : (editingSession ? "Save Changes" : "Schedule Meeting")}
+              </button>
+            </div>
+          </div>
+        </div>
+      )}
+    </div>
+  );
+};
+
+export default SchedulePage;
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.5 on Wed Apr 29 2026 18:41:17 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/frontend/firebase.js.html b/docs/frontend/firebase.js.html new file mode 100644 index 0000000..e24fbd5 --- /dev/null +++ b/docs/frontend/firebase.js.html @@ -0,0 +1,70 @@ + + + + + JSDoc: Source: firebase.js + + + + + + + + + + +
+ +

Source: firebase.js

+ + + + + + +
+
+
/**
+ * Firebase client setup. Reads project config from REACT_APP_FIREBASE_*
+ * environment variables and exports a single shared `auth` instance.
+ *
+ * @module firebase
+ */
+import { initializeApp } from "firebase/app";
+import { getAuth } from "firebase/auth";
+
+const firebaseConfig = {
+  apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
+  authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
+  projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
+  storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
+  messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
+  appId: process.env.REACT_APP_FIREBASE_APP_ID,
+};
+
+const app = initializeApp(firebaseConfig);
+export const auth = getAuth(app);
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.5 on Wed Apr 29 2026 18:41:17 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/frontend/fonts/OpenSans-Bold-webfont.eot b/docs/frontend/fonts/OpenSans-Bold-webfont.eot new file mode 100644 index 0000000..5d20d91 Binary files /dev/null and b/docs/frontend/fonts/OpenSans-Bold-webfont.eot differ diff --git a/docs/frontend/fonts/OpenSans-Bold-webfont.svg b/docs/frontend/fonts/OpenSans-Bold-webfont.svg new file mode 100644 index 0000000..3ed7be4 --- /dev/null +++ b/docs/frontend/fonts/OpenSans-Bold-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/frontend/fonts/OpenSans-Bold-webfont.woff b/docs/frontend/fonts/OpenSans-Bold-webfont.woff new file mode 100644 index 0000000..1205787 Binary files /dev/null and b/docs/frontend/fonts/OpenSans-Bold-webfont.woff differ diff --git a/docs/frontend/fonts/OpenSans-BoldItalic-webfont.eot b/docs/frontend/fonts/OpenSans-BoldItalic-webfont.eot new file mode 100644 index 0000000..1f639a1 Binary files /dev/null and b/docs/frontend/fonts/OpenSans-BoldItalic-webfont.eot differ diff --git a/docs/frontend/fonts/OpenSans-BoldItalic-webfont.svg b/docs/frontend/fonts/OpenSans-BoldItalic-webfont.svg new file mode 100644 index 0000000..6a2607b --- /dev/null +++ b/docs/frontend/fonts/OpenSans-BoldItalic-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/frontend/fonts/OpenSans-BoldItalic-webfont.woff b/docs/frontend/fonts/OpenSans-BoldItalic-webfont.woff new file mode 100644 index 0000000..ed760c0 Binary files /dev/null and b/docs/frontend/fonts/OpenSans-BoldItalic-webfont.woff differ diff --git a/docs/frontend/fonts/OpenSans-Italic-webfont.eot b/docs/frontend/fonts/OpenSans-Italic-webfont.eot new file mode 100644 index 0000000..0c8a0ae Binary files /dev/null and b/docs/frontend/fonts/OpenSans-Italic-webfont.eot differ diff --git a/docs/frontend/fonts/OpenSans-Italic-webfont.svg b/docs/frontend/fonts/OpenSans-Italic-webfont.svg new file mode 100644 index 0000000..e1075dc --- /dev/null +++ b/docs/frontend/fonts/OpenSans-Italic-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/frontend/fonts/OpenSans-Italic-webfont.woff b/docs/frontend/fonts/OpenSans-Italic-webfont.woff new file mode 100644 index 0000000..ff652e6 Binary files /dev/null and b/docs/frontend/fonts/OpenSans-Italic-webfont.woff differ diff --git a/docs/frontend/fonts/OpenSans-Light-webfont.eot b/docs/frontend/fonts/OpenSans-Light-webfont.eot new file mode 100644 index 0000000..1486840 Binary files /dev/null and b/docs/frontend/fonts/OpenSans-Light-webfont.eot differ diff --git a/docs/frontend/fonts/OpenSans-Light-webfont.svg b/docs/frontend/fonts/OpenSans-Light-webfont.svg new file mode 100644 index 0000000..11a472c --- /dev/null +++ b/docs/frontend/fonts/OpenSans-Light-webfont.svg @@ -0,0 +1,1831 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/frontend/fonts/OpenSans-Light-webfont.woff b/docs/frontend/fonts/OpenSans-Light-webfont.woff new file mode 100644 index 0000000..e786074 Binary files /dev/null and b/docs/frontend/fonts/OpenSans-Light-webfont.woff differ diff --git a/docs/frontend/fonts/OpenSans-LightItalic-webfont.eot b/docs/frontend/fonts/OpenSans-LightItalic-webfont.eot new file mode 100644 index 0000000..8f44592 Binary files /dev/null and b/docs/frontend/fonts/OpenSans-LightItalic-webfont.eot differ diff --git a/docs/frontend/fonts/OpenSans-LightItalic-webfont.svg b/docs/frontend/fonts/OpenSans-LightItalic-webfont.svg new file mode 100644 index 0000000..431d7e3 --- /dev/null +++ b/docs/frontend/fonts/OpenSans-LightItalic-webfont.svg @@ -0,0 +1,1835 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/frontend/fonts/OpenSans-LightItalic-webfont.woff b/docs/frontend/fonts/OpenSans-LightItalic-webfont.woff new file mode 100644 index 0000000..43e8b9e Binary files /dev/null and b/docs/frontend/fonts/OpenSans-LightItalic-webfont.woff differ diff --git a/docs/frontend/fonts/OpenSans-Regular-webfont.eot b/docs/frontend/fonts/OpenSans-Regular-webfont.eot new file mode 100644 index 0000000..6bbc3cf Binary files /dev/null and b/docs/frontend/fonts/OpenSans-Regular-webfont.eot differ diff --git a/docs/frontend/fonts/OpenSans-Regular-webfont.svg b/docs/frontend/fonts/OpenSans-Regular-webfont.svg new file mode 100644 index 0000000..25a3952 --- /dev/null +++ b/docs/frontend/fonts/OpenSans-Regular-webfont.svg @@ -0,0 +1,1831 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/frontend/fonts/OpenSans-Regular-webfont.woff b/docs/frontend/fonts/OpenSans-Regular-webfont.woff new file mode 100644 index 0000000..e231183 Binary files /dev/null and b/docs/frontend/fonts/OpenSans-Regular-webfont.woff differ diff --git a/docs/frontend/index.html b/docs/frontend/index.html new file mode 100644 index 0000000..2c53afc --- /dev/null +++ b/docs/frontend/index.html @@ -0,0 +1,65 @@ + + + + + JSDoc: Home + + + + + + + + + + +
+ +

Home

+ + + + + + + + +

+ + + + + + + + + + + + + + + + + + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.5 on Wed Apr 29 2026 18:41:17 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/docs/frontend/index.js.html b/docs/frontend/index.js.html new file mode 100644 index 0000000..0a3a606 --- /dev/null +++ b/docs/frontend/index.js.html @@ -0,0 +1,71 @@ + + + + + JSDoc: Source: index.js + + + + + + + + + + +
+ +

Source: index.js

+ + + + + + +
+
+
/**
+ * React entry point. Mounts &lt;App /&gt; into the #root element and wires up
+ * the optional Create React App web-vitals reporter.
+ *
+ * @module index
+ */
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import './index.css';
+import App from './App';
+import reportWebVitals from './reportWebVitals';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(
+  <React.StrictMode>
+    <App />
+  </React.StrictMode>
+);
+
+reportWebVitals();
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.5 on Wed Apr 29 2026 18:41:17 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/frontend/module-App.html b/docs/frontend/module-App.html new file mode 100644 index 0000000..13a8bee --- /dev/null +++ b/docs/frontend/module-App.html @@ -0,0 +1,173 @@ + + + + + JSDoc: Module: App + + + + + + + + + + +
+ +

Module: App

+ + + + + + +
+ +
+ + + + + +
+ +
+
+ + +
Top-level router for StudySync. + +Routes split into two layout groups behind a ProtectedRoute auth guard: + - GlobalLayout: Navbar only (Home, personal Dashboard, global Inbox). + - ClassLayout: Navbar + ClassHeader tabs (Summary, Resources, Chat, + Schedule), all scoped to a `:courseId` URL param. + +The login page at "/" is the only unauthenticated route; anything +unmatched redirects to "/home".
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.5 on Wed Apr 29 2026 18:41:17 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/docs/frontend/module-CalendarPage.html b/docs/frontend/module-CalendarPage.html new file mode 100644 index 0000000..d24e122 --- /dev/null +++ b/docs/frontend/module-CalendarPage.html @@ -0,0 +1,170 @@ + + + + + JSDoc: Module: CalendarPage + + + + + + + + + + +
+ +

Module: CalendarPage

+ + + + + + +
+ +
+ + + + + +
+ +
+
+ + +
Standalone calendar / availability page. + +Lets the user sync their Google Calendar busy times into StudySync, +create or join study groups, request meeting-time suggestions based +on group availability, and create solo or group study sessions on a +monthly calendar grid.
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.5 on Wed Apr 29 2026 18:41:17 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/docs/frontend/module-ChatPage.html b/docs/frontend/module-ChatPage.html new file mode 100644 index 0000000..e49d825 --- /dev/null +++ b/docs/frontend/module-ChatPage.html @@ -0,0 +1,171 @@ + + + + + JSDoc: Module: ChatPage + + + + + + + + + + +
+ +

Module: ChatPage

+ + + + + + +
+ +
+ + + + + +
+ +
+
+ + +
Chat page used in two modes selected by the `isGlobal` prop: + +- Global inbox (`isGlobal=true`, mounted at /inbox): every + conversation the current user belongs to, across courses and DMs. +- Class chat (`isGlobal=false`, mounted at /class/:courseId/chat): + the auto-provisioned course-wide group chat plus 1:1 DMs with + classmates.
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.5 on Wed Apr 29 2026 18:41:17 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/docs/frontend/module-ClassHeader.html b/docs/frontend/module-ClassHeader.html new file mode 100644 index 0000000..260eda1 --- /dev/null +++ b/docs/frontend/module-ClassHeader.html @@ -0,0 +1,169 @@ + + + + + JSDoc: Module: ClassHeader + + + + + + + + + + +
+ +

Module: ClassHeader

+ + + + + + +
+ +
+ + + + + +
+ +
+
+ + +
Class-scoped header shown above ClassLayout pages. + +Reads the `:courseId` URL param, fetches the user's enrolled courses +to populate a class-switcher dropdown, and renders the secondary tab +bar (Summary / Library / Chat / Study Sessions) for the current class.
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.5 on Wed Apr 29 2026 18:41:17 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/docs/frontend/module-DashboardPage.html b/docs/frontend/module-DashboardPage.html new file mode 100644 index 0000000..6e5833d --- /dev/null +++ b/docs/frontend/module-DashboardPage.html @@ -0,0 +1,172 @@ + + + + + JSDoc: Module: DashboardPage + + + + + + + + + + +
+ +

Module: DashboardPage

+ + + + + + +
+ +
+ + + + + +
+ +
+
+ + +
Dashboard page used in two modes selected by the `isClassScoped` prop: + +- Personal (`isClassScoped=false`, mounted at /dashboard): profile + editing (name, email, password re-auth) plus, for TA/Admin users, + a moderation queue of flagged posts. +- Class summary (`isClassScoped=true`, mounted at + /class/:courseId/summary): upcoming sessions and aggregate study + stats for the active course.
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.5 on Wed Apr 29 2026 18:41:17 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/docs/frontend/module-HomePage.html b/docs/frontend/module-HomePage.html new file mode 100644 index 0000000..5726c91 --- /dev/null +++ b/docs/frontend/module-HomePage.html @@ -0,0 +1,168 @@ + + + + + JSDoc: Module: HomePage + + + + + + + + + + +
+ +

Module: HomePage

+ + + + + + +
+ +
+ + + + + +
+ +
+
+ + +
Home page (post-login landing) — shows the user's enrolled classes +and lets them join an existing course by code or, for TA/Admin +users, create a new course. Each course tile links into the +class-scoped routes under `/class/:courseId/...`.
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.5 on Wed Apr 29 2026 18:41:17 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/docs/frontend/module-LoginPage.html b/docs/frontend/module-LoginPage.html new file mode 100644 index 0000000..0c7b9ed --- /dev/null +++ b/docs/frontend/module-LoginPage.html @@ -0,0 +1,170 @@ + + + + + JSDoc: Module: LoginPage + + + + + + + + + + +
+ +

Module: LoginPage

+ + + + + + +
+ +
+ + + + + +
+ +
+
+ + +
Login / sign-up page. + +Handles email-and-password sign-in, account creation (with a role +picker for new users), Google sign-in, and password reset. After a +successful auth, the new/returning user is upserted into the +StudySync backend via /sync-user before navigating to /home.
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.5 on Wed Apr 29 2026 18:41:17 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/docs/frontend/module-Navbar.html b/docs/frontend/module-Navbar.html new file mode 100644 index 0000000..d820c88 --- /dev/null +++ b/docs/frontend/module-Navbar.html @@ -0,0 +1,168 @@ + + + + + JSDoc: Module: Navbar + + + + + + + + + + +
+ +

Module: Navbar

+ + + + + + +
+ +
+ + + + + +
+ +
+
+ + +
Top-of-page global navigation. + +Renders the StudySync brand, the three top-level links (Classes / +Discussion / Personal), and a logout button.
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.5 on Wed Apr 29 2026 18:41:17 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/docs/frontend/module-ProtectedRoute.html b/docs/frontend/module-ProtectedRoute.html new file mode 100644 index 0000000..39e2ba5 --- /dev/null +++ b/docs/frontend/module-ProtectedRoute.html @@ -0,0 +1,167 @@ + + + + + JSDoc: Module: ProtectedRoute + + + + + + + + + + +
+ +

Module: ProtectedRoute

+ + + + + + +
+ +
+ + + + + +
+ +
+
+ + +
Auth-guard wrapper for routes that require a signed-in Firebase user. +Renders nothing while the auth state is resolving, redirects to "/" +if the user is signed out, and otherwise renders its children.
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.5 on Wed Apr 29 2026 18:41:17 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/docs/frontend/module-ResourcesPage.html b/docs/frontend/module-ResourcesPage.html new file mode 100644 index 0000000..d8384d7 --- /dev/null +++ b/docs/frontend/module-ResourcesPage.html @@ -0,0 +1,170 @@ + + + + + JSDoc: Module: ResourcesPage + + + + + + + + + + +
+ +

Module: ResourcesPage

+ + + + + + +
+ +
+ + + + + +
+ +
+
+ + +
Class library / resource board (mounted at /class/:courseId/resources). + +Lists resource posts for the current course, supports creating new +posts (title, description, optional link), upvoting and downvoting, +flagging for moderation, and deletion by the original author or +privileged users.
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.5 on Wed Apr 29 2026 18:41:17 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/docs/frontend/module-SchedulePage.html b/docs/frontend/module-SchedulePage.html new file mode 100644 index 0000000..f12767a --- /dev/null +++ b/docs/frontend/module-SchedulePage.html @@ -0,0 +1,710 @@ + + + + + JSDoc: Module: SchedulePage + + + + + + + + + + +
+ +

Module: SchedulePage

+ + + + + + +
+ +
+ + + + + +
+ +
+
+ + +
Class-scoped study-session scheduler (mounted at /class/:courseId/schedule). + +Lets students browse and create solo or group study sessions for the +current course, sync availability from Google Calendar via Google +Identity Services, and view a weekly calendar grid. Helper utilities +at the top of the file (`loadGoogleScript`, `getWeekRange`, +`formatHourLabel`, …) are used throughout the component to keep the +render logic readable.
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +

Methods

+ + + + + + + +

(inner) formatHourLabel()

+ + + + + + +
+ Format an hour-of-day (0–23) as a localized "h:mm AM/PM" label. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(inner) getWeekRange(date) → {Object}

+ + + + + + +
+ Return the [Sunday, next Sunday) range covering the week that +contains `date`, with the start aligned to local midnight. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
date + + +Date + + + + Any date in the target week.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Half-open week boundaries. +
+ + + +
+
+ Type +
+
+ +Object + + +
+
+ + + + + + + + + + + + + +

(inner) loadGoogleScript() → {Promise.<void>}

+ + + + + + +
+ Lazily inject the Google Identity Services script and resolve once +`window.google.accounts.oauth2` is available. Reuses the existing + + + + \ No newline at end of file diff --git a/docs/frontend/module-firebase.html b/docs/frontend/module-firebase.html new file mode 100644 index 0000000..1324c97 --- /dev/null +++ b/docs/frontend/module-firebase.html @@ -0,0 +1,166 @@ + + + + + JSDoc: Module: firebase + + + + + + + + + + +
+ +

Module: firebase

+ + + + + + +
+ +
+ + + + + +
+ +
+
+ + +
Firebase client setup. Reads project config from REACT_APP_FIREBASE_* +environment variables and exports a single shared `auth` instance.
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.5 on Wed Apr 29 2026 18:41:17 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/docs/frontend/module-index.html b/docs/frontend/module-index.html new file mode 100644 index 0000000..9c8b71f --- /dev/null +++ b/docs/frontend/module-index.html @@ -0,0 +1,166 @@ + + + + + JSDoc: Module: index + + + + + + + + + + +
+ +

Module: index

+ + + + + + +
+ +
+ + + + + +
+ +
+
+ + +
React entry point. Mounts <App /> into the #root element and wires up +the optional Create React App web-vitals reporter.
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.5 on Wed Apr 29 2026 18:41:17 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/docs/frontend/scripts/linenumber.js b/docs/frontend/scripts/linenumber.js new file mode 100644 index 0000000..4354785 --- /dev/null +++ b/docs/frontend/scripts/linenumber.js @@ -0,0 +1,25 @@ +/*global document */ +(() => { + const source = document.getElementsByClassName('prettyprint source linenums'); + let i = 0; + let lineNumber = 0; + let lineId; + let lines; + let totalLines; + let anchorHash; + + if (source && source[0]) { + anchorHash = document.location.hash.substring(1); + lines = source[0].getElementsByTagName('li'); + totalLines = lines.length; + + for (; i < totalLines; i++) { + lineNumber++; + lineId = `line${lineNumber}`; + lines[i].id = lineId; + if (lineId === anchorHash) { + lines[i].className += ' selected'; + } + } + } +})(); diff --git a/docs/frontend/scripts/prettify/Apache-License-2.0.txt b/docs/frontend/scripts/prettify/Apache-License-2.0.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/docs/frontend/scripts/prettify/Apache-License-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/frontend/scripts/prettify/lang-css.js b/docs/frontend/scripts/prettify/lang-css.js new file mode 100644 index 0000000..041e1f5 --- /dev/null +++ b/docs/frontend/scripts/prettify/lang-css.js @@ -0,0 +1,2 @@ +PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", +/^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); diff --git a/docs/frontend/scripts/prettify/prettify.js b/docs/frontend/scripts/prettify/prettify.js new file mode 100644 index 0000000..eef5ad7 --- /dev/null +++ b/docs/frontend/scripts/prettify/prettify.js @@ -0,0 +1,28 @@ +var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; +(function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= +[],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), +l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, +q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, +q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, +"");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), +a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} +for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], +"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], +H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], +J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ +I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), +["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", +/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), +["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", +hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= +!k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p th:last-child { border-right: 1px solid #ddd; } + +.ancestors, .attribs { color: #999; } +.ancestors a, .attribs a +{ + color: #999 !important; + text-decoration: none; +} + +.clear +{ + clear: both; +} + +.important +{ + font-weight: bold; + color: #950B02; +} + +.yes-def { + text-indent: -1000px; +} + +.type-signature { + color: #aaa; +} + +.name, .signature { + font-family: Consolas, Monaco, 'Andale Mono', monospace; +} + +.details { margin-top: 14px; border-left: 2px solid #DDD; } +.details dt { width: 120px; float: left; padding-left: 10px; padding-top: 6px; } +.details dd { margin-left: 70px; } +.details ul { margin: 0; } +.details ul { list-style-type: none; } +.details li { margin-left: 30px; padding-top: 6px; } +.details pre.prettyprint { margin: 0 } +.details .object-value { padding-top: 0; } + +.description { + margin-bottom: 1em; + margin-top: 1em; +} + +.code-caption +{ + font-style: italic; + font-size: 107%; + margin: 0; +} + +.source +{ + border: 1px solid #ddd; + width: 80%; + overflow: auto; +} + +.prettyprint.source { + width: inherit; +} + +.source code +{ + font-size: 100%; + line-height: 18px; + display: block; + padding: 4px 12px; + margin: 0; + background-color: #fff; + color: #4D4E53; +} + +.prettyprint code span.line +{ + display: inline-block; +} + +.prettyprint.linenums +{ + padding-left: 70px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.prettyprint.linenums ol +{ + padding-left: 0; +} + +.prettyprint.linenums li +{ + border-left: 3px #ddd solid; +} + +.prettyprint.linenums li.selected, +.prettyprint.linenums li.selected * +{ + background-color: lightyellow; +} + +.prettyprint.linenums li * +{ + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; +} + +.params .name, .props .name, .name code { + color: #4D4E53; + font-family: Consolas, Monaco, 'Andale Mono', monospace; + font-size: 100%; +} + +.params td.description > p:first-child, +.props td.description > p:first-child +{ + margin-top: 0; + padding-top: 0; +} + +.params td.description > p:last-child, +.props td.description > p:last-child +{ + margin-bottom: 0; + padding-bottom: 0; +} + +.disabled { + color: #454545; +} diff --git a/docs/frontend/styles/prettify-jsdoc.css b/docs/frontend/styles/prettify-jsdoc.css new file mode 100644 index 0000000..5a2526e --- /dev/null +++ b/docs/frontend/styles/prettify-jsdoc.css @@ -0,0 +1,111 @@ +/* JSDoc prettify.js theme */ + +/* plain text */ +.pln { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* string content */ +.str { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a keyword */ +.kwd { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a comment */ +.com { + font-weight: normal; + font-style: italic; +} + +/* a type name */ +.typ { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* a literal value */ +.lit { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* punctuation */ +.pun { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* lisp open bracket */ +.opn { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* lisp close bracket */ +.clo { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a markup tag name */ +.tag { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a markup attribute name */ +.atn { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a markup attribute value */ +.atv { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a declaration */ +.dec { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a variable name */ +.var { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* a function name */ +.fun { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* Specify class=linenums on a pre to get line numbering */ +ol.linenums { + margin-top: 0; + margin-bottom: 0; +} diff --git a/docs/frontend/styles/prettify-tomorrow.css b/docs/frontend/styles/prettify-tomorrow.css new file mode 100644 index 0000000..b6f92a7 --- /dev/null +++ b/docs/frontend/styles/prettify-tomorrow.css @@ -0,0 +1,132 @@ +/* Tomorrow Theme */ +/* Original theme - https://github.com/chriskempson/tomorrow-theme */ +/* Pretty printing styles. Used with prettify.js. */ +/* SPAN elements with the classes below are added by prettyprint. */ +/* plain text */ +.pln { + color: #4d4d4c; } + +@media screen { + /* string content */ + .str { + color: #718c00; } + + /* a keyword */ + .kwd { + color: #8959a8; } + + /* a comment */ + .com { + color: #8e908c; } + + /* a type name */ + .typ { + color: #4271ae; } + + /* a literal value */ + .lit { + color: #f5871f; } + + /* punctuation */ + .pun { + color: #4d4d4c; } + + /* lisp open bracket */ + .opn { + color: #4d4d4c; } + + /* lisp close bracket */ + .clo { + color: #4d4d4c; } + + /* a markup tag name */ + .tag { + color: #c82829; } + + /* a markup attribute name */ + .atn { + color: #f5871f; } + + /* a markup attribute value */ + .atv { + color: #3e999f; } + + /* a declaration */ + .dec { + color: #f5871f; } + + /* a variable name */ + .var { + color: #c82829; } + + /* a function name */ + .fun { + color: #4271ae; } } +/* Use higher contrast and text-weight for printable form. */ +@media print, projection { + .str { + color: #060; } + + .kwd { + color: #006; + font-weight: bold; } + + .com { + color: #600; + font-style: italic; } + + .typ { + color: #404; + font-weight: bold; } + + .lit { + color: #044; } + + .pun, .opn, .clo { + color: #440; } + + .tag { + color: #006; + font-weight: bold; } + + .atn { + color: #404; } + + .atv { + color: #060; } } +/* Style */ +/* +pre.prettyprint { + background: white; + font-family: Consolas, Monaco, 'Andale Mono', monospace; + font-size: 12px; + line-height: 1.5; + border: 1px solid #ccc; + padding: 10px; } +*/ + +/* Specify class=linenums on a pre to get line numbering */ +ol.linenums { + margin-top: 0; + margin-bottom: 0; } + +/* IE indents via margin-left */ +li.L0, +li.L1, +li.L2, +li.L3, +li.L4, +li.L5, +li.L6, +li.L7, +li.L8, +li.L9 { + /* */ } + +/* Alternate shading for lines */ +li.L1, +li.L3, +li.L5, +li.L7, +li.L9 { + /* */ } diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..292a6e7 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,80 @@ + + + + + + StudySync — Documentation + + + +
+

StudySync Documentation

+

Generated reference for the StudySync codebase. Rebuild with npm run docs.

+ + + +
CSDS 393 — StudySync
+
+ + diff --git a/frontend/jsdoc.json b/frontend/jsdoc.json new file mode 100644 index 0000000..73da642 --- /dev/null +++ b/frontend/jsdoc.json @@ -0,0 +1,15 @@ +{ + "source": { + "include": ["src"], + "includePattern": "\\.js$", + "excludePattern": "(\\.test\\.js$|setupTests\\.js$|reportWebVitals\\.js$|node_modules)" + }, + "opts": { + "recurse": true, + "destination": "../docs/frontend" + }, + "templates": { + "cleverLinks": true, + "monospaceLinks": false + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 69030a6..0664218 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,6 +19,9 @@ "react-router-dom": "^7.13.1", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" + }, + "devDependencies": { + "jsdoc": "^4.0.5" } }, "node_modules/@adobe/css-tools": { @@ -67,6 +70,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -716,6 +720,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.28.6.tgz", "integrity": "sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, @@ -1599,6 +1604,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-module-imports": "^7.28.6", @@ -2522,6 +2528,7 @@ "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.8.tgz", "integrity": "sha512-WiE9uCGRLUnShdjb9iP20sA3ToWrBbNXr14/N5mow7Nls9dmKgfGaGX5cynLvrltxq2OrDLh1VDNaUgsnS/k/g==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", @@ -2588,6 +2595,7 @@ "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.8.tgz", "integrity": "sha512-4De6SUZ36zozl9kh5rZSxKWULpgty27rMzZ6x+xkoo7+NWyhWyFdsdvhFsWhTw/9GGj0wXIcbTjwHYCUIUuHyg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@firebase/app": "0.14.8", "@firebase/component": "0.7.0", @@ -2603,7 +2611,8 @@ "version": "0.9.3", "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/@firebase/auth": { "version": "1.12.0", @@ -3060,6 +3069,7 @@ "integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" }, @@ -3554,6 +3564,19 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdoc/salty": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.12.tgz", + "integrity": "sha512-TuB0x50EoAvEX/UEWITd8Mkn3WhiTjSvbTMCLj0BhsQEl5iUzjXdA0bETEVpTk+5TGTLR6QktI9H4hLviVeaAQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash": "^4.18.1" + }, + "engines": { + "node": ">=v12.0.0" + } + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -4091,6 +4114,7 @@ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -4413,6 +4437,32 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "license": "MIT" }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -4577,6 +4627,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", @@ -4630,6 +4681,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -4999,6 +5051,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5097,6 +5150,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6033,6 +6087,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6211,6 +6266,19 @@ "node": ">=4" } }, + "node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -7866,6 +7934,7 @@ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -10639,6 +10708,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^27.5.1", "import-local": "^3.0.2", @@ -11536,6 +11606,7 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -11559,6 +11630,76 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "node_modules/jsdoc": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.5.tgz", + "integrity": "sha512-P4C6MWP9yIlMiK8nwoZvxN84vb6MsnXcHuy7XzVOvQoCizWX5JFCBsWIIWKXBltpoRZXddUOVQmCTOZt9yDj9g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^14.1.1", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^14.1.0", + "markdown-it-anchor": "^8.6.7", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsdoc/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jsdoc/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jsdoc/node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, "node_modules/jsdom": { "version": "16.7.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", @@ -11736,6 +11877,16 @@ "node": ">=0.10.0" } }, + "node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -11819,6 +11970,16 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/loader-runner": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", @@ -11859,9 +12020,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash.camelcase": { @@ -11987,6 +12148,68 @@ "tmpl": "1.0.5" } }, + "node_modules/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "dev": true, + "license": "Unlicense", + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -12002,6 +12225,13 @@ "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", "license": "CC0-1.0" }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -12885,6 +13115,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -14019,6 +14250,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -14297,6 +14529,16 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", @@ -14408,6 +14650,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -14539,6 +14782,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -14563,6 +14807,7 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -14901,6 +15146,16 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "license": "MIT" }, + "node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -15057,6 +15312,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -15299,6 +15555,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -16543,22 +16800,6 @@ } } }, - "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/tapable": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", @@ -16776,6 +17017,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -16944,6 +17186,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "license": "(MIT OR CC0-1.0)", + "peer": true, "engines": { "node": ">=10" }, @@ -17061,6 +17304,13 @@ "node": ">=4.2.0" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -17373,6 +17623,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -17444,6 +17695,7 @@ "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", "license": "MIT", + "peer": true, "dependencies": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -17857,6 +18109,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -18160,6 +18413,13 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "license": "MIT" }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/frontend/package.json b/frontend/package.json index a06194b..9a2f3a2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,7 +19,11 @@ "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", - "eject": "react-scripts eject" + "eject": "react-scripts eject", + "docs": "jsdoc -c jsdoc.json" + }, + "devDependencies": { + "jsdoc": "^4.0.5" }, "eslintConfig": { "extends": [ @@ -40,4 +44,3 @@ ] } } - \ No newline at end of file diff --git a/frontend/src/App.js b/frontend/src/App.js index ab87a21..d873046 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,3 +1,16 @@ +/** + * Top-level router for StudySync. + * + * Routes split into two layout groups behind a ProtectedRoute auth guard: + * - GlobalLayout: Navbar only (Home, personal Dashboard, global Inbox). + * - ClassLayout: Navbar + ClassHeader tabs (Summary, Resources, Chat, + * Schedule), all scoped to a `:courseId` URL param. + * + * The login page at "/" is the only unauthenticated route; anything + * unmatched redirects to "/home". + * + * @module App + */ import { BrowserRouter as Router, Routes, Route, Navigate, Outlet } from "react-router-dom"; import LoginPage from "./LoginPage"; import HomePage from "./HomePage"; diff --git a/frontend/src/CalendarPage.js b/frontend/src/CalendarPage.js index d060ada..c2f59e2 100644 --- a/frontend/src/CalendarPage.js +++ b/frontend/src/CalendarPage.js @@ -1,3 +1,13 @@ +/** + * Standalone calendar / availability page. + * + * Lets the user sync their Google Calendar busy times into StudySync, + * create or join study groups, request meeting-time suggestions based + * on group availability, and create solo or group study sessions on a + * monthly calendar grid. + * + * @module CalendarPage + */ import "./LoginPage.css"; import "./CalendarPage.css"; import { useEffect, useMemo, useState } from "react"; @@ -46,6 +56,7 @@ const CalendarPage = () => { [groups, selectedGroupId] ); + /** Lazily inject the Google Identity Services script and resolve once it is ready. */ const loadGoogleScript = () => new Promise((resolve, reject) => { if (window.google?.accounts?.oauth2) { @@ -68,6 +79,7 @@ const CalendarPage = () => { document.body.appendChild(script); }); + /** Prompt the user for a Google OAuth token with calendar.readonly scope. */ const getGoogleAccessToken = async () => { await loadGoogleScript(); return new Promise((resolve, reject) => { @@ -86,6 +98,7 @@ const CalendarPage = () => { }); }; + /** Load the user's study groups; auto-select the first group if none is selected yet. */ const fetchGroups = async () => { if (!userEmail) return; const response = await fetch( @@ -104,6 +117,7 @@ const CalendarPage = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [userEmail]); + /** Create a new study group from the form input and switch the active selection to it. */ const createGroup = async () => { if (!groupName.trim()) return; setGroupStatus("Creating group..."); @@ -127,6 +141,7 @@ const CalendarPage = () => { } }; + /** Join an existing group by numeric id, then select it as the active group. */ const joinGroup = async () => { const id = Number(joinGroupId); if (!id) return; @@ -147,6 +162,11 @@ const CalendarPage = () => { } }; + /** + * Pull busy ranges from Google Calendar's freeBusy endpoint over + * the next `daysAhead` days and POST them to the backend so other + * StudySync features can see when this user is unavailable. + */ const syncGoogleBusyTimes = async () => { if (!userEmail) return; setSyncStatus("Connecting to Google Calendar..."); @@ -201,6 +221,11 @@ const CalendarPage = () => { } }; + /** + * Ask the backend for meeting-time suggestions for the active group + * over the next `daysAhead` days, restricted to working hours + * (8am–10pm) and `meetingMinutes` long, in the user's local TZ. + */ const loadSuggestions = async () => { if (!selectedGroupId) return; setLoadingSuggestions(true); @@ -269,6 +294,8 @@ const CalendarPage = () => { [sessions, selectedDate] ); + // Build a 6-week (42-cell) grid for the visible month, padded with + // days from the prior/next month so the grid always starts on Sunday. const calendarCells = useMemo(() => { const firstDay = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), 1); const startWeekday = firstDay.getDay(); @@ -289,6 +316,7 @@ const CalendarPage = () => { const monthLabel = currentMonth.toLocaleDateString([], { month: "long", year: "numeric" }); + /** Load study sessions overlapping the visible 6-week grid for the current user. */ const loadSessions = async () => { if (!userEmail) return; const first = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), 1); @@ -320,6 +348,12 @@ const CalendarPage = () => { )}`; }; + /** + * Validate the session form, POST a new session, and on success + * reset the title and bump the start/end pickers an hour forward + * so chained creation feels natural. Group sessions require an + * active group selection. + */ const createStudySession = async () => { if (!userEmail) return; if (!sessionTitle.trim() || !sessionStart || !sessionEnd) { diff --git a/frontend/src/ChatPage.js b/frontend/src/ChatPage.js index 72370b3..770e1ac 100644 --- a/frontend/src/ChatPage.js +++ b/frontend/src/ChatPage.js @@ -1,3 +1,14 @@ +/** + * Chat page used in two modes selected by the `isGlobal` prop: + * + * - Global inbox (`isGlobal=true`, mounted at /inbox): every + * conversation the current user belongs to, across courses and DMs. + * - Class chat (`isGlobal=false`, mounted at /class/:courseId/chat): + * the auto-provisioned course-wide group chat plus 1:1 DMs with + * classmates. + * + * @module ChatPage + */ import { useEffect, useState, useCallback} from "react"; import { onAuthStateChanged } from "firebase/auth"; import { useParams } from "react-router-dom"; @@ -82,6 +93,13 @@ const ChatPage = ({ isGlobal = true }) => { return () => unsubscribe(); }, [isGlobal, courseId]); + /** + * Fetch the message history for the conversation between + * `currentUser` and `otherUser` (or the course group chat if + * `otherUser.is_group`). Course context comes from the conversation + * itself when in the global inbox, falling back to the URL + * `:courseId` on the class page. + */ const loadMessages = useCallback(async (currentUser, otherUser) => { if (!currentUser || !otherUser) return; try { @@ -118,7 +136,13 @@ const ChatPage = ({ isGlobal = true }) => { return () => clearInterval(interval); }, [authUser, selectedUser, loadMessages]); - // 2. Use conversation-specific course IDs + /** + * Send the current draft message to the selected conversation. + * Uses the conversation's own course id when present (so global + * inbox replies stay in the right course thread); falls back to + * the URL `:courseId` for class-page sends. For group chats the + * receiver UID is the sentinel `"GROUP"`. + */ const handleSendMessage = async (e) => { e.preventDefault(); if (!authUser || !selectedUser || !newMessage.trim()) return; diff --git a/frontend/src/ClassHeader.js b/frontend/src/ClassHeader.js index 36935c0..12deccb 100644 --- a/frontend/src/ClassHeader.js +++ b/frontend/src/ClassHeader.js @@ -1,3 +1,12 @@ +/** + * Class-scoped header shown above ClassLayout pages. + * + * Reads the `:courseId` URL param, fetches the user's enrolled courses + * to populate a class-switcher dropdown, and renders the secondary tab + * bar (Summary / Library / Chat / Study Sessions) for the current class. + * + * @module ClassHeader + */ import { useState, useEffect } from "react"; import { NavLink, useParams, useNavigate } from "react-router-dom"; import { auth } from "./firebase"; diff --git a/frontend/src/DashboardPage.js b/frontend/src/DashboardPage.js index b350078..9ba5af2 100644 --- a/frontend/src/DashboardPage.js +++ b/frontend/src/DashboardPage.js @@ -1,3 +1,15 @@ +/** + * Dashboard page used in two modes selected by the `isClassScoped` prop: + * + * - Personal (`isClassScoped=false`, mounted at /dashboard): profile + * editing (name, email, password re-auth) plus, for TA/Admin users, + * a moderation queue of flagged posts. + * - Class summary (`isClassScoped=true`, mounted at + * /class/:courseId/summary): upcoming sessions and aggregate study + * stats for the active course. + * + * @module DashboardPage + */ import { useState, useEffect, useCallback } from "react"; import { auth } from "./firebase"; import { @@ -66,6 +78,11 @@ const DashboardPage = ({ isClassScoped = false }) => { } }, []); + /** + * Load the upcoming sessions and aggregate study stats for the + * current course (class-scoped mode only). Resets to empty values + * outside class mode or when the user/profile isn't ready yet. + */ const fetchClassSummary = useCallback(async () => { if (!isClassScoped || !courseId || !userProfile?.email) { setClassSummary({ @@ -107,6 +124,7 @@ const DashboardPage = ({ isClassScoped = false }) => { } }, [courseId, isClassScoped, userProfile?.email]); + /** Format a session's start/end Date pair as a single localized label. */ const formatSessionTime = (startsAt, endsAt) => { const start = new Date(startsAt); const end = new Date(endsAt); @@ -126,6 +144,7 @@ const DashboardPage = ({ isClassScoped = false }) => { return `${dayLabel} • ${timeLabel}`; }; + /** Moderator action: clear a post's flag and remove it from the queue. */ const handleDismissFlag = async (postId) => { try { const res = await fetch( @@ -142,6 +161,7 @@ const DashboardPage = ({ isClassScoped = false }) => { } }; + /** Moderator action: confirm and permanently delete a flagged post. */ const handleDeletePost = async (postId) => { if ( !window.confirm("Are you sure you want to delete this flagged content?") @@ -183,6 +203,16 @@ const DashboardPage = ({ isClassScoped = false }) => { fetchClassSummary(); }, [fetchClassSummary]); + /** + * Submit profile edits from the modal. + * + * Email and password changes trigger a two-step flow: the first + * submit flips the form into "needs current password" mode, and the + * second submit re-authenticates with `EmailAuthProvider` before + * applying `updatePassword` / `verifyBeforeUpdateEmail`. Name and + * role changes are pushed to the StudySync backend in a single PUT + * regardless of the re-auth path. + */ const handleUpdate = async (e) => { e.preventDefault(); const user = auth.currentUser; diff --git a/frontend/src/HomePage.js b/frontend/src/HomePage.js index 92d8532..8547c1d 100644 --- a/frontend/src/HomePage.js +++ b/frontend/src/HomePage.js @@ -1,3 +1,11 @@ +/** + * Home page (post-login landing) — shows the user's enrolled classes + * and lets them join an existing course by code or, for TA/Admin + * users, create a new course. Each course tile links into the + * class-scoped routes under `/class/:courseId/...`. + * + * @module HomePage + */ import React, { useState, useEffect, useCallback } from "react"; import { useNavigate } from "react-router-dom"; import axios from "axios"; @@ -50,6 +58,12 @@ const HomePage = () => { return () => unsubscribe(); }, [fetchUserCourses, fetchProfile]); + /** + * Submit the join-course modal: enroll the current user in the + * course identified by `courseCode`. On success, close the modal + * and refresh the user's course list. Failure is surfaced via + * alert (typical case: bad code or already enrolled). + */ const handleJoinCourse = async (e) => { e.preventDefault(); const user = auth.currentUser; @@ -67,6 +81,11 @@ const HomePage = () => { } }; + /** + * Submit the create-course modal (TA/Admin only). Owner is the + * current Firebase UID. Surfaces the backend's `detail` message on + * failure (e.g. duplicate course code). + */ const handleCreateCourse = async (e) => { e.preventDefault(); const user = auth.currentUser; diff --git a/frontend/src/LoginPage.js b/frontend/src/LoginPage.js index 1840518..a9e62ad 100644 --- a/frontend/src/LoginPage.js +++ b/frontend/src/LoginPage.js @@ -1,3 +1,13 @@ +/** + * Login / sign-up page. + * + * Handles email-and-password sign-in, account creation (with a role + * picker for new users), Google sign-in, and password reset. After a + * successful auth, the new/returning user is upserted into the + * StudySync backend via /sync-user before navigating to /home. + * + * @module LoginPage + */ import { useState, useEffect } from "react"; import "./LoginPage.css"; import { useNavigate } from "react-router-dom"; @@ -42,6 +52,15 @@ const LoginPage = () => { }; // --- SYNC FUNCTION --- + /** + * Upsert the just-authenticated Firebase user into the StudySync + * Postgres database via /sync-user, then navigate to /home. Falls + * back to displayName / "New User" if no name was provided. + * + * @param {Object} user - Firebase user object (from firebase/auth). + * @param {string} role - Role to record (Student / TA / Admin). + * @param {string|null} name - Optional display name override. + */ const syncWithBackend = async (user, role, name) => { try { const response = await fetch("http://localhost:8000/sync-user", { @@ -66,6 +85,17 @@ const LoginPage = () => { } }; + /** + * Handle the email/password form for both sign-in and sign-up. + * + * - Sign-in path: authenticate, then sync to backend as "Student" by + * default and route to /home. + * - Sign-up path: create the Firebase account, set displayName from + * the form, then surface the role-selection step before syncing. + * + * Maps common Firebase auth error codes to friendlier messages + * shown via `setErrorMessage`. + */ const handleSubmit = async (e) => { e.preventDefault(); setErrorMessage(""); @@ -127,6 +157,12 @@ const LoginPage = () => { } }; + /** + * Sign in via the Google popup. Always routes through role + * selection afterward so first-time Google users still get an + * explicit role and a Postgres row, even though Firebase already + * has them. + */ const handleGoogle = async () => { try { const provider = new GoogleAuthProvider(); @@ -140,6 +176,7 @@ const LoginPage = () => { } }; + /** Send a Firebase password-reset email to the address in the form. */ const handleForgotPassword = async (e) => { e.preventDefault(); if (!formData.email) { diff --git a/frontend/src/Navbar.js b/frontend/src/Navbar.js index d6037d2..8ecd923 100644 --- a/frontend/src/Navbar.js +++ b/frontend/src/Navbar.js @@ -1,3 +1,11 @@ +/** + * Top-of-page global navigation. + * + * Renders the StudySync brand, the three top-level links (Classes / + * Discussion / Personal), and a logout button. + * + * @module Navbar + */ import { useState } from "react"; import { Link, useLocation, useNavigate } from "react-router-dom"; import { auth } from "./firebase"; @@ -12,10 +20,15 @@ const Navbar = () => { // Helper to check if a path is active, ignoring nested class routes for global links const isActive = (path) => location.pathname === path; + /** + * Sign the user out of Firebase, clear cached identity, and bounce + * back to the login page. Errors are logged but not surfaced to the + * user. + */ const handleLogout = async () => { try { await signOut(auth); - localStorage.removeItem("userEmail"); + localStorage.removeItem("userEmail"); navigate("/"); } catch (error) { console.error("Logout failed:", error); diff --git a/frontend/src/ProtectedRoute.js b/frontend/src/ProtectedRoute.js index 5b64d77..a0cf4b1 100644 --- a/frontend/src/ProtectedRoute.js +++ b/frontend/src/ProtectedRoute.js @@ -1,8 +1,19 @@ +/** + * Auth-guard wrapper for routes that require a signed-in Firebase user. + * Renders nothing while the auth state is resolving, redirects to "/" + * if the user is signed out, and otherwise renders its children. + * + * @module ProtectedRoute + */ import { useEffect, useState } from "react"; import { Navigate } from "react-router-dom"; import { onAuthStateChanged } from "firebase/auth"; -import { auth } from "./firebase"; +import { auth } from "./firebase"; +/** + * @param {{ children: React.ReactNode }} props + * @returns {React.ReactElement|null} The protected children, a redirect, or null while loading. + */ export default function ProtectedRoute({ children }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); diff --git a/frontend/src/ResourcesPage.js b/frontend/src/ResourcesPage.js index 5c6bcb1..bb7e247 100644 --- a/frontend/src/ResourcesPage.js +++ b/frontend/src/ResourcesPage.js @@ -1,3 +1,13 @@ +/** + * Class library / resource board (mounted at /class/:courseId/resources). + * + * Lists resource posts for the current course, supports creating new + * posts (title, description, optional link), upvoting and downvoting, + * flagging for moderation, and deletion by the original author or + * privileged users. + * + * @module ResourcesPage + */ import { useState, useEffect, useCallback } from "react"; import { auth } from "./firebase"; import { useParams } from "react-router-dom"; @@ -44,7 +54,11 @@ const ResourcesPage = () => { return () => unsubscribe(); }, []); - // Fetch posts for this course + /** + * Load posts for the current course. Passing the user's UID lets + * the backend annotate each post with `user_vote` so we can render + * the correct upvote/downvote highlight. + */ const fetchPosts = useCallback(async () => { try { setLoading(true); @@ -70,6 +84,7 @@ const ResourcesPage = () => { } }, [courseId, currentUser?.uid]); + /** Flag a post for TA/Admin review and optimistically mark it flagged in the local list. */ const handleFlagPost = async (postId) => { if (!window.confirm("Are you sure you want to flag this post for review?")) return; @@ -106,7 +121,11 @@ const ResourcesPage = () => { setFormError(null); }; - // Handle post submission + /** + * Validate and submit the new-post form: title required, optional + * description must be ≥5 chars, optional resource link must start + * with http(s)://. On success, close the form and refresh the list. + */ const handleCreatePost = async (e) => { e.preventDefault(); @@ -162,7 +181,12 @@ const ResourcesPage = () => { } }; - // Handle voting: vote = +1 (upvote), -1 (downvote), 0 (neutral) + /** + * Cast or clear a vote on a post (`newVote` is +1, -1, or 0). + * Optimistically updates the local score using the previous user + * vote; refetches from the backend if the request fails so the UI + * doesn't drift from server truth. + */ const handleVote = async (postId, newVote) => { if (!currentUser) return; try { @@ -191,7 +215,7 @@ const ResourcesPage = () => { } }; - // Format timestamp + /** Render an ISO timestamp as "just now" / "Nm ago" / "Nh ago" / "Nd ago" / fallback date. */ const formatDate = (isoString) => { const date = new Date(isoString); const now = new Date(); @@ -213,6 +237,7 @@ const ResourcesPage = () => { return name ? name.charAt(0).toUpperCase() : "?"; }; + /** Delete a post after a confirm prompt; backend enforces author/role permissions. */ const handleDeletePost = async (postId) => { if (!window.confirm("Are you sure you want to delete this resource?")) return; diff --git a/frontend/src/SchedulePage.js b/frontend/src/SchedulePage.js index bf7d127..e2867a0 100644 --- a/frontend/src/SchedulePage.js +++ b/frontend/src/SchedulePage.js @@ -1,3 +1,15 @@ +/** + * Class-scoped study-session scheduler (mounted at /class/:courseId/schedule). + * + * Lets students browse and create solo or group study sessions for the + * current course, sync availability from Google Calendar via Google + * Identity Services, and view a weekly calendar grid. Helper utilities + * at the top of the file (`loadGoogleScript`, `getWeekRange`, + * `formatHourLabel`, …) are used throughout the component to keep the + * render logic readable. + * + * @module SchedulePage + */ import "./SchedulePage.css"; import { useEffect, useState, useCallback } from "react"; import { auth } from "./firebase"; @@ -6,6 +18,14 @@ import { useParams } from "react-router-dom"; const API_BASE = process.env.REACT_APP_API_BASE_URL || "http://127.0.0.1:8000"; const GOOGLE_CLIENT_ID = process.env.REACT_APP_GOOGLE_CLIENT_ID; +/** + * Lazily inject the Google Identity Services script and resolve once + * `window.google.accounts.oauth2` is available. Reuses the existing + *