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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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 )
+
+ View Source
+
+
+
+
2167 def _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>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ def
+ get_db ():
+
+ View Source
+
+
+
+ 17 def 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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:
+
+
+
+
+
+
+ View Source
+
+
+
1 """
+ 2 High-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 """
+ 12 from database import Base
+ 13 from sqlalchemy import Column , Integer , String , DateTime , Enum , ForeignKey , Text , Boolean
+ 14 from sqlalchemy.orm import relationship
+ 15 import datetime
+ 16 import enum
+ 17
+ 18
+ 19 class 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
+ 26 class 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
+ 51 class 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
+ 70 class 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
+ 83 class 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
+102 class 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
+116 class 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
+131 class 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
+155 class 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
+167 class 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
+185 class 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
+208 class 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
+220 class 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
+239 class 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 ):
+
+ View Source
+
+
+
+
20 class 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.
+
+
+
+
+
+
+
+
+
+
+ class
+ User (sqlalchemy.orm.decl_api._DynamicAttributesType ,
+ sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]] ):
+
+ View Source
+
+
+
+
27 class 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.
+
+
+
+
+
+
+
+
+
+ google_calendar_token
+
+
+
+
+
+
+
+ conversation_participants
+
+
+
+
+
+ courses_created
+
+
+
+
+
+
+
+
+
+ class
+ Course (sqlalchemy.orm.decl_api._DynamicAttributesType ,
+ sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]] ):
+
+ View Source
+
+
+
+
52 class 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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ class
+ Enrollment (sqlalchemy.orm.decl_api._DynamicAttributesType ,
+ sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]] ):
+
+ View Source
+
+
+
+
71 class 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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ class
+ Conversation (sqlalchemy.orm.decl_api._DynamicAttributesType ,
+ sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]] ):
+
+ View Source
+
+
+
+
84 class 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ class
+ ConversationParticipant (sqlalchemy.orm.decl_api._DynamicAttributesType ,
+ sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]] ):
+
+ View Source
+
+
+
+
103 class 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.
+
+
+
+
+
+
+ conversation_id
+
+
+
+
+
+
+
+
+
+
+
+ class
+ Message (sqlalchemy.orm.decl_api._DynamicAttributesType ,
+ sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]] ):
+
+ View Source
+
+
+
+
117 class 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" )
+
+
+
+
+
+
+
+ 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.
+
+
+
+
+
+
+ conversation_id
+
+
+
+
+
+
+
+
+
+
+
+
+ class
+ Post (sqlalchemy.orm.decl_api._DynamicAttributesType ,
+ sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]] ):
+
+ View Source
+
+
+
+
132 class 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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ class
+ PostVote (sqlalchemy.orm.decl_api._DynamicAttributesType ,
+ sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]] ):
+
+ View Source
+
+
+
+
156 class 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.
+
+
+
+
+
+
+
+
+
+
+
+
+ class
+ StudyGroup (sqlalchemy.orm.decl_api._DynamicAttributesType ,
+ sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]] ):
+
+ View Source
+
+
+
+
168 class 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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ class
+ StudySession (sqlalchemy.orm.decl_api._DynamicAttributesType ,
+ sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]] ):
+
+ View Source
+
+
+
+
186 class 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" )
+
+
+
+
+
+
+
+ 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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ class
+ StudyGroupMember (sqlalchemy.orm.decl_api._DynamicAttributesType ,
+ sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]] ):
+
+ View Source
+
+
+
+
209 class 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.
+
+
+
+
+
+
+
+
+
+
+
+
+ class
+ UserAvailability (sqlalchemy.orm.decl_api._DynamicAttributesType ,
+ sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]] ):
+
+ View Source
+
+
+
+
221 class 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.
+
+
+
+
+
+
+
+
+
+
+ study_session_id
+
+
+
+
+
+
+
+
+ class
+ StudySessionInvitee (sqlalchemy.orm.decl_api._DynamicAttributesType ,
+ sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]] ):
+
+ View Source
+
+
+
+
240 class 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.
+
+
+
+
+
+
+ study_session_id
+
+
+
+
+
+
+
+
+
+
+
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\nWhen 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\nSets attributes on the constructed instance using the names and\nvalues in kwargs.
\n\nOnly 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\nUser \u2014 a person (student, TA, or admin). \nCourse + Enrollment \u2014 a class and its roster. \nPost + PostVote \u2014 discussion/resource posts and their votes. \nStudyGroup + StudyGroupMember \u2014 long-lived groups inside a course. \nStudySession + StudySessionInvitee \u2014 scheduled meetings (solo or group). \nUserAvailability \u2014 busy/free blocks, typically synced from Google Calendar. \nConversation + 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\nHolds 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\nSets attributes on the constructed instance using the names and\nvalues in kwargs.
\n\nOnly 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\nSets attributes on the constructed instance using the names and\nvalues in kwargs.
\n\nOnly 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\nSets attributes on the constructed instance using the names and\nvalues in kwargs.
\n\nOnly 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\nOptionally 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\nSets attributes on the constructed instance using the names and\nvalues in kwargs.
\n\nOnly 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\nSets attributes on the constructed instance using the names and\nvalues in kwargs.
\n\nOnly 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\nSets attributes on the constructed instance using the names and\nvalues in kwargs.
\n\nOnly 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\nscore 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\nSets attributes on the constructed instance using the names and\nvalues in kwargs.
\n\nOnly 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\nSets attributes on the constructed instance using the names and\nvalues in kwargs.
\n\nOnly 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\nHolds 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\nSets attributes on the constructed instance using the names and\nvalues in kwargs.
\n\nOnly 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\nsession_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\nSets attributes on the constructed instance using the names and\nvalues in kwargs.
\n\nOnly 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\nSets attributes on the constructed instance using the names and\nvalues in kwargs.
\n\nOnly 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\nMost 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\nSets attributes on the constructed instance using the names and\nvalues in kwargs.
\n\nOnly 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\nSets attributes on the constructed instance using the names and\nvalues in kwargs.
\n\nOnly 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;
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+ 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;
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+ 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;
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+ 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;
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+ 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;
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+ 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;
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+ 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;
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+ 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;
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+ 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;
+}
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+ 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;
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+ 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;
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+ 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);
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+ 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 <App /> 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();
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+ 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:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+ 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:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+ 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:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+ 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:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+ 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:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+ 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:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+ 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:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+ 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:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+ 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:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+ 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:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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:
+
+
+
+
+
+
+ Name
+
+
+ Type
+
+
+
+
+
+ Description
+
+
+
+
+
+
+
+
+ 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:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+ 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:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+ 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;c
122||(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",/^[^]+/],["dec",/^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^