├── backend ├── ohq │ ├── __init__.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ ├── calculatewaittimes.py │ │ │ ├── archive.py │ │ │ ├── queue_heatmap_stat.py │ │ │ ├── queue_daily_stat.py │ │ │ ├── user_stat.py │ │ │ ├── course_stat.py │ │ │ └── createcourse.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0011_merge_20210415_2110.py │ │ ├── 0015_question_templates.py │ │ ├── 0010_auto_20210407_0145.py │ │ ├── 0004_auto_20200825_1344.py │ │ ├── 0018_auto_20220125_0344.py │ │ ├── 0017_auto_20211031_1615.py │ │ ├── 0014_question_student_descriptor.py │ │ ├── 0013_auto_20210924_2056.py │ │ ├── 0005_auto_20201016_1702.py │ │ ├── 0016_auto_20211008_2136.py │ │ ├── 0021_queue_question_timer_enabled_and_more.py │ │ ├── 0003_auto_20200822_1116.py │ │ ├── 0010_auto_20210405_1720.py │ │ ├── 0008_auto_20210119_2218.py │ │ ├── 0006_auto_20210105_2000.py │ │ ├── 0012_queue_require_video_chat_url_on_questions.py │ │ ├── 0007_announcement.py │ │ ├── 0020_auto_20240326_0226.py │ │ └── 0019_auto_20211114_1800.py │ ├── apps.py │ ├── routing.py │ ├── pagination.py │ ├── backends.py │ ├── admin.py │ ├── tasks.py │ ├── sms.py │ ├── queues.py │ ├── filters.py │ ├── invite.py │ └── urls.py ├── tests │ ├── __init__.py │ └── ohq │ │ ├── __init__.py │ │ ├── test_health.py │ │ ├── test_backends.py │ │ └── test_invite.py ├── officehoursqueue │ ├── settings │ │ ├── __init__.py │ │ ├── staging.py │ │ ├── ci.py │ │ ├── development.py │ │ ├── proddev.py │ │ └── production.py │ ├── __init__.py │ ├── wsgi.py │ ├── templates │ │ ├── emails │ │ │ ├── course_invitation.html │ │ │ ├── course_added.html │ │ │ └── base.html │ │ └── redoc.html │ ├── asgi.py │ ├── routing.py │ ├── celery.py │ └── urls.py ├── .dockerignore ├── scripts │ └── asgi-run ├── setup.cfg ├── manage.py ├── Dockerfile ├── nginx-proddev.conf ├── pyproject.toml └── docker-compose.yaml ├── frontend ├── .projectile ├── global.d.ts ├── .prettierrc.json ├── public │ ├── ohq.png │ ├── favicon.ico │ ├── ohq-login.png │ ├── joining-oh-1.png │ ├── open-queue-1.png │ ├── open-queue-2.png │ ├── put-queue-1.png │ ├── answer-queue-1.png │ ├── create-course-1.png │ ├── create-course-2.png │ ├── create-course-3.png │ ├── join-course-1.png │ ├── join-course-2.png │ ├── notifications-1.png │ ├── remove-queue-1.png │ ├── invite-members-1.png │ ├── while-in-queue-1.png │ └── vercel.svg ├── .editorconfig ├── components │ ├── Course │ │ ├── CourseSettings │ │ │ ├── CourseForm.module.css │ │ │ └── CourseSettings.tsx │ │ ├── InstructorQueuePage │ │ │ ├── aol.mp3 │ │ │ ├── timeupsound.mp3 │ │ │ ├── notification.mp3 │ │ │ ├── FinishConfirmModal.tsx │ │ │ ├── QuestionTimer.tsx │ │ │ ├── Tags.tsx │ │ │ ├── ClearQueueModal.tsx │ │ │ ├── Questions.tsx │ │ │ └── QueueSettings │ │ │ │ └── QueueSettings.tsx │ │ ├── Analytics │ │ │ ├── Cards │ │ │ │ └── AnalyticsCard.tsx │ │ │ ├── Analytics.tsx │ │ │ └── Heatmaps │ │ │ │ ├── Averages.tsx │ │ │ │ └── Heatmap.tsx │ │ ├── Roster │ │ │ ├── ChangeRoleDropdown.tsx │ │ │ ├── Invites │ │ │ │ ├── AddForm.tsx │ │ │ │ └── InviteModal.tsx │ │ │ ├── RemoveButton.tsx │ │ │ └── RosterForm.tsx │ │ ├── StudentQueuePage │ │ │ ├── StudentQueuePage.tsx │ │ │ ├── DeleteQuestionModal.tsx │ │ │ └── QueueMenuItem.tsx │ │ └── CourseSidebarInstructorList.tsx │ ├── Home │ │ ├── AccountSettings │ │ │ ├── VerificationModal.module.css │ │ │ ├── AccountSettings.tsx │ │ │ └── VerificationModal.tsx │ │ ├── Home.tsx │ │ └── Dashboard │ │ │ ├── Modals │ │ │ ├── ModalShowNewChanges.tsx │ │ │ ├── ModalRedirectAddCourse.tsx │ │ │ ├── ModalLeaveStudentCourse.tsx │ │ │ └── ModalAddStudentCourse.tsx │ │ │ ├── Messages │ │ │ └── tips.json │ │ │ ├── Cards │ │ │ ├── AddCard.tsx │ │ │ └── ArchivedCourseCard.tsx │ │ │ └── Forms │ │ │ └── AddStudentForm.tsx │ ├── SignOut │ │ └── index.tsx │ ├── common │ │ ├── ui │ │ │ ├── LinkedText.tsx │ │ │ └── ResponsiveIconButton.tsx │ │ ├── AboutModal.tsx │ │ └── Footer.tsx │ ├── Changelog │ │ └── changelogfile.md │ ├── Guide │ │ ├── utils.tsx │ │ ├── StudentGuide.tsx │ │ └── InstructorGuide.tsx │ ├── Auth │ │ └── AuthPrompt.tsx │ └── Calendar │ │ ├── calendarUtils.ts │ │ └── DashboardCalendar │ │ └── EventCard.tsx ├── utils │ ├── gippage.ts │ ├── ga │ │ ├── googleAnalytics.ts │ │ └── withGA.tsx │ ├── redirect.ts │ ├── sentry.tsx │ ├── notifications.ts │ ├── staffcheck.ts │ ├── protectpage.tsx │ ├── index.tsx │ ├── enums.tsx │ └── fetch.tsx ├── next-env.d.ts ├── .dockerignore ├── pages │ ├── faq.tsx │ ├── changelog.tsx │ ├── calendar.tsx │ ├── settings.tsx │ ├── health.tsx │ ├── _document.tsx │ ├── _app.tsx │ ├── _error.tsx │ ├── courses │ │ └── [course] │ │ │ ├── calendar.tsx │ │ │ ├── analytics.tsx │ │ │ └── settings.tsx │ └── index.tsx ├── Dockerfile ├── hooks │ ├── debounce.ts │ ├── player.ts │ └── data-fetching │ │ ├── resources.ts │ │ ├── dashboard.ts │ │ ├── account.ts │ │ └── questionsummary.ts ├── styles │ ├── landingpage.module.css │ └── index.css ├── csrf.ts ├── tsconfig.json ├── next.config.js ├── constants.ts ├── server.js ├── context │ └── auth.tsx └── .eslintrc.json ├── k8s ├── .gitignore ├── cdk8s.yaml ├── package.json └── tsconfig.json ├── .gitignore ├── LICENSE └── ohq.code-workspace /backend/ohq/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/.projectile: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/tests/ohq/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/ohq/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/ohq/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/ohq/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/officehoursqueue/settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /k8s/.gitignore: -------------------------------------------------------------------------------- 1 | *.d.ts 2 | *.js 3 | node_modules 4 | dist/ 5 | -------------------------------------------------------------------------------- /k8s/cdk8s.yaml: -------------------------------------------------------------------------------- 1 | language: typescript 2 | app: node main.js 3 | -------------------------------------------------------------------------------- /frontend/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.mp3"; 2 | declare module "*.md"; 3 | -------------------------------------------------------------------------------- /frontend/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /backend/officehoursqueue/settings/staging.py: -------------------------------------------------------------------------------- 1 | from officehoursqueue.settings.base import * # noqa: F401, F403 2 | -------------------------------------------------------------------------------- /frontend/public/ohq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/office-hours-queue/HEAD/frontend/public/ohq.png -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{js, jsx, ts, tsx}] 4 | indent_style = space 5 | indent_size = 4 6 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/office-hours-queue/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /backend/ohq/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class OhqConfig(AppConfig): 5 | name = "ohq" 6 | -------------------------------------------------------------------------------- /frontend/public/ohq-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/office-hours-queue/HEAD/frontend/public/ohq-login.png -------------------------------------------------------------------------------- /frontend/public/joining-oh-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/office-hours-queue/HEAD/frontend/public/joining-oh-1.png -------------------------------------------------------------------------------- /frontend/public/open-queue-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/office-hours-queue/HEAD/frontend/public/open-queue-1.png -------------------------------------------------------------------------------- /frontend/public/open-queue-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/office-hours-queue/HEAD/frontend/public/open-queue-2.png -------------------------------------------------------------------------------- /frontend/public/put-queue-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/office-hours-queue/HEAD/frontend/public/put-queue-1.png -------------------------------------------------------------------------------- /frontend/public/answer-queue-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/office-hours-queue/HEAD/frontend/public/answer-queue-1.png -------------------------------------------------------------------------------- /frontend/public/create-course-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/office-hours-queue/HEAD/frontend/public/create-course-1.png -------------------------------------------------------------------------------- /frontend/public/create-course-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/office-hours-queue/HEAD/frontend/public/create-course-2.png -------------------------------------------------------------------------------- /frontend/public/create-course-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/office-hours-queue/HEAD/frontend/public/create-course-3.png -------------------------------------------------------------------------------- /frontend/public/join-course-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/office-hours-queue/HEAD/frontend/public/join-course-1.png -------------------------------------------------------------------------------- /frontend/public/join-course-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/office-hours-queue/HEAD/frontend/public/join-course-2.png -------------------------------------------------------------------------------- /frontend/public/notifications-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/office-hours-queue/HEAD/frontend/public/notifications-1.png -------------------------------------------------------------------------------- /frontend/public/remove-queue-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/office-hours-queue/HEAD/frontend/public/remove-queue-1.png -------------------------------------------------------------------------------- /backend/officehoursqueue/__init__.py: -------------------------------------------------------------------------------- 1 | from officehoursqueue.celery import app as celery_app 2 | 3 | 4 | __all__ = ("celery_app",) 5 | -------------------------------------------------------------------------------- /frontend/components/Course/CourseSettings/CourseForm.module.css: -------------------------------------------------------------------------------- 1 | .department-input input { 2 | text-transform: uppercase; 3 | } 4 | 5 | -------------------------------------------------------------------------------- /frontend/public/invite-members-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/office-hours-queue/HEAD/frontend/public/invite-members-1.png -------------------------------------------------------------------------------- /frontend/public/while-in-queue-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/office-hours-queue/HEAD/frontend/public/while-in-queue-1.png -------------------------------------------------------------------------------- /frontend/components/Course/InstructorQueuePage/aol.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/office-hours-queue/HEAD/frontend/components/Course/InstructorQueuePage/aol.mp3 -------------------------------------------------------------------------------- /frontend/components/Course/InstructorQueuePage/timeupsound.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/office-hours-queue/HEAD/frontend/components/Course/InstructorQueuePage/timeupsound.mp3 -------------------------------------------------------------------------------- /frontend/components/Course/InstructorQueuePage/notification.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/office-hours-queue/HEAD/frontend/components/Course/InstructorQueuePage/notification.mp3 -------------------------------------------------------------------------------- /frontend/utils/gippage.ts: -------------------------------------------------------------------------------- 1 | import { NextPageContext, NextPage } from "next"; 2 | 3 | export type GIPPage

= NextPage

& { 4 | getInitialProps: (ctx: NextPageContext) => P | Promise

; 5 | }; 6 | -------------------------------------------------------------------------------- /frontend/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /backend/ohq/routing.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from ohq.urls import realtime_router 4 | 5 | 6 | websocket_urlpatterns = [ 7 | path("api/ws/subscribe/", realtime_router.as_consumer().as_asgi(), name="subscriptions"), 8 | ] 9 | -------------------------------------------------------------------------------- /backend/officehoursqueue/settings/ci.py: -------------------------------------------------------------------------------- 1 | from officehoursqueue.settings.base import * # noqa: F401, F403 2 | 3 | 4 | TEST_RUNNER = "xmlrunner.extra.djangotestrunner.XMLTestRunner" 5 | TEST_OUTPUT_VERBOSE = 2 6 | TEST_OUTPUT_DIR = "test-results" 7 | -------------------------------------------------------------------------------- /frontend/components/Home/AccountSettings/VerificationModal.module.css: -------------------------------------------------------------------------------- 1 | .react-code-input input[type=number]::-webkit-inner-spin-button, 2 | .react-code-input input[type=number]::-webkit-outer-spin-button { 3 | -webkit-appearance: none; 4 | margin: 0; 5 | } 6 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | # Docker 2 | Dockerfile 3 | .dockerignore 4 | 5 | # git 6 | .circleci 7 | .git 8 | .gitignore 9 | .gitmodules 10 | **/*.md 11 | LICENSE 12 | 13 | # Misc 14 | .coverage 15 | **/__pycache__/ 16 | tests/ 17 | postgres/ 18 | .venv 19 | -------------------------------------------------------------------------------- /backend/ohq/pagination.py: -------------------------------------------------------------------------------- 1 | from rest_framework.pagination import PageNumberPagination 2 | 3 | 4 | class QuestionSearchPagination(PageNumberPagination): 5 | """ 6 | Custom pagination for QuestionListView. 7 | """ 8 | 9 | page_size = 20 10 | -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | # Docker 2 | Dockerfile 3 | .dockerignore 4 | 5 | # git 6 | .circleci 7 | .git 8 | .gitignore 9 | .gitmodules 10 | **/*.md 11 | !components/Changelog/changelogfile.md 12 | LICENSE 13 | 14 | # Misc 15 | node_modules/ 16 | .next/ 17 | -------------------------------------------------------------------------------- /backend/scripts/asgi-run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Django Migrate 4 | /usr/local/bin/python3 /app/manage.py migrate --noinput 5 | 6 | # Switch to project folder 7 | cd /app/ 8 | 9 | # Run Uvicorn through Gunicorn 10 | exec /usr/local/bin/gunicorn -b 0.0.0.0:80 -w 4 -k uvicorn.workers.UvicornWorker officehoursqueue.asgi:application 11 | -------------------------------------------------------------------------------- /backend/ohq/migrations/0011_merge_20210415_2110.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.7 on 2021-04-15 21:10 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("ohq", "0010_auto_20210407_0145"), 10 | ("ohq", "0010_auto_20210405_1720"), 11 | ] 12 | 13 | operations = [] 14 | -------------------------------------------------------------------------------- /backend/tests/ohq/test_health.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.urls import reverse 3 | 4 | class HealthTestCase(TestCase): 5 | def test_health(self): 6 | url = reverse("health") 7 | resp = self.client.get(url) 8 | self.assertEqual(resp.status_code, 200) 9 | self.assertEqual(resp.data, {"message": "OK"}) 10 | -------------------------------------------------------------------------------- /frontend/components/Home/Home.tsx: -------------------------------------------------------------------------------- 1 | import { Grid } from "semantic-ui-react"; 2 | import HomeSidebar from "./HomeSidebar"; 3 | 4 | const Home = ({ children, ...props }) => { 5 | return ( 6 | 7 | 8 | {children} 9 | 10 | ); 11 | }; 12 | 13 | export default Home; 14 | -------------------------------------------------------------------------------- /backend/ohq/management/commands/calculatewaittimes.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from ohq.queues import calculate_wait_times 4 | 5 | 6 | class Command(BaseCommand): 7 | help = "Calculates the estimated wait times of all unarchived queues." 8 | 9 | def handle(self, *args, **kwargs): 10 | calculate_wait_times() 11 | self.stdout.write("Updated estimated queue wait times!") 12 | -------------------------------------------------------------------------------- /frontend/pages/faq.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import Home from "../components/Home/Home"; 3 | import Guide from "../components/Guide"; 4 | 5 | const GuidePage = () => { 6 | return ( 7 | <> 8 | 9 | OHQ | FAQ 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default GuidePage; 19 | -------------------------------------------------------------------------------- /frontend/utils/ga/googleAnalytics.ts: -------------------------------------------------------------------------------- 1 | import ReactGA from "react-ga"; 2 | 3 | const prod = process.env.NODE_ENV === "production"; 4 | 5 | export const initGA = () => { 6 | if (prod) { 7 | ReactGA.initialize("UA-21029575-18"); 8 | } 9 | }; 10 | 11 | export const logPageView = () => { 12 | if (prod) { 13 | ReactGA.set({ page: window.location.pathname }); 14 | ReactGA.pageview(window.location.pathname); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/ohq/migrations/0015_question_templates.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations, models 2 | 3 | 4 | class Migration(migrations.Migration): 5 | 6 | dependencies = [ 7 | ("ohq", "0014_question_student_descriptor"), 8 | ] 9 | 10 | operations = [ 11 | migrations.AddField( 12 | model_name="queue", 13 | name="question_template", 14 | field=models.TextField(blank=True, default=""), 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /backend/ohq/migrations/0010_auto_20210407_0145.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.7 on 2021-04-07 01:45 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("ohq", "0009_auto_20210201_2224"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="announcement", 15 | name="content", 16 | field=models.TextField(), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-buster-slim 2 | 3 | LABEL maintainer="Penn Labs" 4 | 5 | WORKDIR /app/ 6 | 7 | # Copy project dependencies 8 | COPY package.json /app/ 9 | COPY yarn.lock /app/ 10 | 11 | # Install project dependencies 12 | RUN yarn install --frozen-lockfile --production=true 13 | 14 | # Copy project files 15 | COPY . /app/ 16 | 17 | # Disable telemetry back to zeit 18 | ENV NEXT_TELEMETRY_DISABLED=1 19 | 20 | # Build project 21 | RUN yarn build 22 | 23 | CMD ["yarn", "start"] 24 | -------------------------------------------------------------------------------- /backend/ohq/migrations/0004_auto_20200825_1344.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1 on 2020-08-25 17:44 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("ohq", "0003_auto_20200822_1116"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="queue", 15 | name="estimated_wait_time", 16 | field=models.IntegerField(default=-1), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/ohq/migrations/0018_auto_20220125_0344.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.7 on 2022-01-25 03:44 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("ohq", "0017_auto_20211031_1615"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="course", 15 | name="course_title", 16 | field=models.CharField(max_length=100), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /frontend/pages/changelog.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Head from "next/head"; 3 | import Home from "../components/Home/Home"; 4 | import Changelog from "../components/Changelog"; 5 | 6 | const LandingPage = () => { 7 | return ( 8 | <> 9 | 10 | OHQ | Changelog 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default LandingPage; 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | frontend/package-lock.json 2 | frontend/yarn-error.log 3 | 4 | # IDE files 5 | .vscode/ 6 | .idea/ 7 | 8 | # Python files 9 | __pycache__/ 10 | *.pyc 11 | 12 | # Distribution 13 | build/ 14 | dist/ 15 | *.egg-info/ 16 | 17 | # Code testing/coverage 18 | .tox 19 | test-results/ 20 | .coverage 21 | htmlcov/ 22 | 23 | # Test database 24 | db.sqlite3 25 | postgres 26 | 27 | # Mac 28 | .DS_Store 29 | 30 | # React 31 | node_modules/ 32 | .next/ 33 | .log 34 | 35 | # Firebase credentials 36 | ohq-firebase-*.json 37 | -------------------------------------------------------------------------------- /backend/officehoursqueue/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for officehoursqueue project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | 15 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "officehoursqueue.settings.production") 16 | 17 | application = get_wsgi_application() 18 | -------------------------------------------------------------------------------- /backend/ohq/migrations/0017_auto_20211031_1615.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.7 on 2021-10-31 16:15 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("ohq", "0016_auto_20211008_2136"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="queue", 15 | name="pin", 16 | field=models.CharField(blank=True, max_length=50, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/ohq/migrations/0014_question_student_descriptor.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.7 on 2021-10-03 17:13 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("ohq", "0013_auto_20210924_2056"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="question", 15 | name="student_descriptor", 16 | field=models.CharField(blank=True, max_length=255, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/officehoursqueue/templates/emails/course_invitation.html: -------------------------------------------------------------------------------- 1 | {% extends 'emails/base.html' %} 2 | 3 | {% block content %} 4 | 5 |

Invitation to join {{ course }} OHQ

6 | 7 |

You have been invited to join {{ course }} as a {{ role }}.

8 | 9 |

Click below to create your account!

10 | 11 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /backend/officehoursqueue/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for officehoursqueue project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | import django 13 | from channels.routing import get_default_application 14 | 15 | 16 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "officehoursqueue.settings.production") 17 | django.setup() 18 | application = get_default_application() 19 | -------------------------------------------------------------------------------- /backend/officehoursqueue/templates/emails/course_added.html: -------------------------------------------------------------------------------- 1 | {% extends 'emails/base.html' %} 2 | 3 | {% block content %} 4 | 5 |

You've been added to {{ course }} OHQ

6 | 7 |

You have been added to {{ course }} as a {{ role }}.

8 | 9 |

Click below to view this course's office hours queues!

10 | 11 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /frontend/pages/calendar.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import Home from "../components/Home/Home"; 3 | import Calendar from "../components/Calendar/StudentCalendar/StudentCalendar"; 4 | import { withAuth } from "../context/auth"; 5 | 6 | const CalendarPage = () => { 7 | return ( 8 | <> 9 | 10 | OHQ | Calendar 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default withAuth(CalendarPage); 20 | -------------------------------------------------------------------------------- /frontend/hooks/debounce.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | 3 | function useDebounce( 4 | f: (...args: T) => void, 5 | interval: number 6 | ): (...args: T) => void { 7 | const ref = useRef(null); 8 | 9 | const debouncedF = (...args: T): void => { 10 | if (ref.current) { 11 | clearTimeout(ref.current); 12 | } 13 | ref.current = setTimeout(() => { 14 | f(...args); 15 | }, interval); 16 | }; 17 | 18 | return debouncedF; 19 | } 20 | 21 | export default useDebounce; 22 | -------------------------------------------------------------------------------- /frontend/pages/settings.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import { withAuth } from "../context/auth"; 3 | import Home from "../components/Home/Home"; 4 | import AccountSettings from "../components/Home/AccountSettings/AccountSettings"; 5 | 6 | const LandingPage = () => { 7 | return ( 8 | <> 9 | 10 | OHQ | Account Settings 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default withAuth(LandingPage); 20 | -------------------------------------------------------------------------------- /backend/officehoursqueue/routing.py: -------------------------------------------------------------------------------- 1 | from channels.auth import AuthMiddlewareStack 2 | from channels.routing import ProtocolTypeRouter, URLRouter 3 | from channels.security.websocket import AllowedHostsOriginValidator 4 | 5 | import ohq.routing 6 | import ohq.urls # DO NOT DELETE THIS IMPORT! 7 | 8 | 9 | # Django REST Live requires urls too be imported from the async entrypoint. 10 | 11 | application = ProtocolTypeRouter( 12 | { 13 | "websocket": AllowedHostsOriginValidator( 14 | AuthMiddlewareStack(URLRouter(ohq.routing.websocket_urlpatterns)) 15 | ) 16 | } 17 | ) 18 | -------------------------------------------------------------------------------- /frontend/pages/health.tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps } from "next"; 2 | 3 | const HealthPage = () => { 4 | return
OK
; 5 | }; 6 | 7 | export const getServerSideProps: GetServerSideProps = async ({ req }) => { 8 | const userAgent = req.headers["user-agent"] || ""; 9 | 10 | if (userAgent !== "service-status") { 11 | return { 12 | redirect: { 13 | destination: "/", 14 | permanent: false, 15 | }, 16 | }; 17 | } 18 | 19 | return { 20 | props: {}, 21 | }; 22 | }; 23 | 24 | export default HealthPage; 25 | -------------------------------------------------------------------------------- /frontend/components/SignOut/index.tsx: -------------------------------------------------------------------------------- 1 | import { Menu, Icon } from "semantic-ui-react"; 2 | 3 | const SignOutButton = () => { 4 | return ( 5 | 15 | 16 | Sign Out 17 | 18 | ); 19 | }; 20 | 21 | export default SignOutButton; 22 | -------------------------------------------------------------------------------- /backend/ohq/migrations/0013_auto_20210924_2056.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.7 on 2021-09-24 20:56 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("ohq", "0012_queue_require_video_chat_url_on_questions"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="course", 15 | name="require_video_chat_url_on_questions", 16 | ), 17 | migrations.RemoveField( 18 | model_name="course", 19 | name="video_chat_enabled", 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /backend/officehoursqueue/settings/development.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from officehoursqueue.settings.base import * # noqa: F401, F403 4 | from officehoursqueue.settings.base import INSTALLED_APPS, MIDDLEWARE 5 | 6 | 7 | # Development extensions 8 | INSTALLED_APPS += ["django_extensions", "debug_toolbar"] 9 | 10 | MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware"] + MIDDLEWARE 11 | INTERNAL_IPS = ["127.0.0.1"] 12 | 13 | # Allow http callback for DLA 14 | os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" 15 | 16 | # Use the console for email in development 17 | EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" 18 | -------------------------------------------------------------------------------- /backend/ohq/backends.py: -------------------------------------------------------------------------------- 1 | from accounts.backends import LabsUserBackend 2 | 3 | from ohq.models import Membership, MembershipInvite 4 | 5 | 6 | class OHQBackend(LabsUserBackend): 7 | """ 8 | A custom DLA backend that converts Membership Invites into Memberships on user creation. 9 | """ 10 | 11 | def post_authenticate(self, user, created, dictionary): 12 | if created: 13 | invites = MembershipInvite.objects.filter(email__istartswith=f"{user.username}@") 14 | 15 | for invite in invites: 16 | Membership.objects.create(course=invite.course, kind=invite.kind, user=user) 17 | 18 | invites.delete() 19 | user.save() 20 | -------------------------------------------------------------------------------- /backend/ohq/migrations/0005_auto_20201016_1702.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.1 on 2020-10-16 21:02 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("ohq", "0004_auto_20200825_1344"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="question", 15 | name="note", 16 | field=models.CharField(blank=True, max_length=255, null=True), 17 | ), 18 | migrations.AddField( 19 | model_name="question", 20 | name="resolved_note", 21 | field=models.BooleanField(default=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /backend/ohq/migrations/0016_auto_20211008_2136.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.7 on 2021-10-08 21:36 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("ohq", "0015_question_templates"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="queue", 15 | name="pin_enabled", 16 | field=models.BooleanField(default=False), 17 | ), 18 | migrations.AddField( 19 | model_name="queue", 20 | name="pin", 21 | field=models.CharField(blank=True, default="", max_length=5, null=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /frontend/styles/landingpage.module.css: -------------------------------------------------------------------------------- 1 | .about-landing { 2 | position: fixed; 3 | bottom: 0; 4 | right: 0; 5 | margin: 10px 5px 15px 10px; 6 | } 7 | 8 | @media only screen and (max-width: 767px) { 9 | .about-dashboard { 10 | position: absolute; 11 | bottom: 0; 12 | margin: 12px 12px 12px 22px; 13 | } 14 | } 15 | 16 | @media only screen and (min-width: 768px) { 17 | .about-dashboard { 18 | position: fixed; 19 | bottom: 0; 20 | margin: 12px 12px 12px 13px; 21 | } 22 | } 23 | 24 | .about p { 25 | cursor: pointer; 26 | color: #4285f5; 27 | text-decoration: underline; 28 | font-size: 10pt; 29 | font-weight: 800; 30 | } 31 | -------------------------------------------------------------------------------- /backend/officehoursqueue/settings/proddev.py: -------------------------------------------------------------------------------- 1 | # Django config based off of production config, but with minor changes to ensure it works on dev machines 2 | 3 | from officehoursqueue.settings.production import * 4 | 5 | import officehoursqueue.settings.base as base 6 | 7 | # No https on dev machines 8 | SECURE_PROXY_SSL_HEADER = () 9 | 10 | # Prevents request rejection on dev machines 11 | ALLOWED_HOSTS = ["*"] 12 | 13 | # Use local login instead of UPenn's 14 | PLATFORM_ACCOUNTS = base.PLATFORM_ACCOUNTS 15 | 16 | # Allow http callback for DLA 17 | os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" 18 | 19 | # Use the console for email in development 20 | EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" 21 | -------------------------------------------------------------------------------- /frontend/csrf.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @returns {string | boolean} The CSRF token used by the Django REST Framework 3 | */ 4 | const getCsrf = () => { 5 | const result = 6 | document.cookie && 7 | document.cookie 8 | .split("; ") 9 | .reduce( 10 | (acc, cookie) => 11 | acc || 12 | (cookie.substring(0, "csrftoken".length + 1) === 13 | "csrftoken=" && 14 | decodeURIComponent( 15 | cookie.substring("csrftoken=".length) 16 | )), 17 | null 18 | ); 19 | return result; 20 | }; 21 | 22 | export default getCsrf; 23 | -------------------------------------------------------------------------------- /backend/setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 100 3 | exclude = .venv, migrations 4 | inline-quotes = double 5 | 6 | [isort] 7 | default_section = THIRDPARTY 8 | known_first_party = ohq, officehoursqueue 9 | line_length = 100 10 | lines_after_imports = 2 11 | multi_line_output = 3 12 | include_trailing_comma = True 13 | use_parentheses = True 14 | 15 | # [coverage:run] 16 | # omit = */tests/*, */migrations/*, */settings/*, */asgi.py, */wsgi.py, */apps.py, */schemas.py, */.venv/*, manage.py, */management/commands/populate.py 17 | # source = . 18 | 19 | [uwsgi] 20 | http-socket = :80 21 | chdir = /app/ 22 | module = officehoursqueue.wsgi:application 23 | master = true 24 | static-map = /assets=/app/static 25 | processes = 5 26 | -------------------------------------------------------------------------------- /frontend/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | const Document = () => { 4 | return ( 5 | 6 | 7 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default Document; 23 | -------------------------------------------------------------------------------- /frontend/utils/redirect.ts: -------------------------------------------------------------------------------- 1 | import Router from "next/router"; 2 | import { NextPageContext } from "next"; 3 | 4 | export default function nextRedirect( 5 | { req, res }: NextPageContext, 6 | condition: (url: string) => boolean, 7 | redirectUrl: string 8 | ) { 9 | // if redirect is called server side 10 | if (req && res) { 11 | // if request doesn't have an associated URL or the condition 12 | // on the url applies, we redirect 13 | if (!req.url || condition(req.url)) { 14 | res.writeHead(302, { Location: redirectUrl }); 15 | res.end(); 16 | } 17 | } else if (condition(window.location.pathname)) { 18 | Router.replace(redirectUrl); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backend/ohq/migrations/0021_queue_question_timer_enabled_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.3 on 2024-10-11 21:08 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("ohq", "0020_auto_20240326_0226"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="queue", 15 | name="question_timer_enabled", 16 | field=models.BooleanField(default=False), 17 | ), 18 | migrations.AddField( 19 | model_name="queue", 20 | name="question_timer_start_time", 21 | field=models.IntegerField(blank=True, null=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "strictNullChecks": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "noEmit": true, 15 | "esModuleInterop": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "jsx": "preserve", 21 | "incremental": true 22 | }, 23 | "exclude": [ 24 | "node_modules" 25 | ], 26 | "include": [ 27 | "next-env.d.ts", 28 | "**/*.ts", 29 | "**/*.tsx" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /backend/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "officehoursqueue.settings.development") 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == "__main__": 21 | main() 22 | -------------------------------------------------------------------------------- /backend/officehoursqueue/templates/redoc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ReDoc 5 | 6 | 7 | 8 | 9 | 10 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /backend/officehoursqueue/celery.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from celery import Celery 4 | from django.conf import settings 5 | 6 | 7 | # set the default Django settings module for the 'celery' program. 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "officehoursqueue.settings.development") 9 | 10 | app = Celery("officehoursqueue", broker=settings.MESSAGE_BROKER_URL) 11 | 12 | # Using a string here means the worker doesn't have to serialize 13 | # the configuration object to child processes. 14 | # - namespace='CELERY' means all celery-related configuration keys 15 | # should have a `CELERY_` prefix. 16 | app.config_from_object("django.conf:settings", namespace="CELERY") 17 | 18 | # Load task modules from all registered Django app configs. 19 | app.autodiscover_tasks() 20 | -------------------------------------------------------------------------------- /backend/ohq/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from ohq.models import ( 4 | Announcement, 5 | Course, 6 | CourseStatistic, 7 | Membership, 8 | MembershipInvite, 9 | Profile, 10 | Question, 11 | Queue, 12 | QueueStatistic, 13 | Semester, 14 | Tag, 15 | UserStatistic, 16 | ) 17 | 18 | 19 | admin.site.register(Course) 20 | admin.site.register(CourseStatistic) 21 | admin.site.register(Membership) 22 | admin.site.register(MembershipInvite) 23 | admin.site.register(Profile) 24 | admin.site.register(Question) 25 | admin.site.register(Queue) 26 | admin.site.register(Semester) 27 | admin.site.register(QueueStatistic) 28 | admin.site.register(Announcement) 29 | admin.site.register(Tag) 30 | admin.site.register(UserStatistic) 31 | -------------------------------------------------------------------------------- /k8s/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "k8s", 3 | "version": "1.0.0", 4 | "main": "main.js", 5 | "types": "main.ts", 6 | "license": "Apache-2.0", 7 | "private": true, 8 | "scripts": { 9 | "import": "cdk8s import", 10 | "synth": "cdk8s synth", 11 | "compile": "tsc", 12 | "watch": "tsc -w", 13 | "test": "jest", 14 | "build": "npm run compile && npm run synth", 15 | "upgrade": "npm i cdk8s@latest cdk8s-cli@latest", 16 | "upgrade:next": "npm i cdk8s@next cdk8s-cli@next" 17 | }, 18 | "dependencies": { 19 | "@pennlabs/kittyhawk": "^1.1.11", 20 | "cdk8s": "^2.2.63", 21 | "constructs": "^10.0.110" 22 | }, 23 | "devDependencies": { 24 | "@types/jest": "^26.0.24", 25 | "@types/node": "^14.18.12", 26 | "jest": "^26.6.3", 27 | "ts-jest": "^26.5.6", 28 | "typescript": "^4.6.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /frontend/components/Home/Dashboard/Modals/ModalShowNewChanges.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Button, Modal } from "semantic-ui-react"; 3 | import Changelog from "../../../Changelog"; 4 | 5 | interface ModalShowNewChangesProps { 6 | openModal: boolean; 7 | setOpen: (boolean) => void; 8 | } 9 | 10 | const ModalShowNewChanges = (props: ModalShowNewChangesProps) => { 11 | const { openModal, setOpen } = props; 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default ModalShowNewChanges; 25 | -------------------------------------------------------------------------------- /frontend/components/common/ui/LinkedText.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | import Linkify from "react-linkify"; 3 | 4 | interface LinkedTextProps { 5 | text: string; 6 | } 7 | 8 | const LinkedText = ({ text }: LinkedTextProps) => { 9 | const componentDecorator = ( 10 | decoratedHref: string, 11 | decoratedText: string, 12 | key: number 13 | ) => ( 14 | 20 | {decoratedText} 21 | 22 | ); 23 | 24 | const linkified = ( 25 | {text} 26 | ); 27 | return linkified; 28 | }; 29 | 30 | export default LinkedText; 31 | -------------------------------------------------------------------------------- /frontend/styles/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", 4 | "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", 5 | "Helvetica Neue", sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | 15 | #__next { 16 | /* Grid components use negative margin of 1rem */ 17 | /* We can consider fixing them, since negative margins might not*/ 18 | /* necessarily be good practice, but it is really hard to get the*/ 19 | /* same behavior back so we do this as a "quick hack" */ 20 | height: calc(100% - 1rem); 21 | width: 100%; 22 | display: flex; 23 | } 24 | -------------------------------------------------------------------------------- /frontend/utils/sentry.tsx: -------------------------------------------------------------------------------- 1 | let Sentry; 2 | 3 | const SENTRY_URL = 4 | "https://8bac880d12c34567b17d8af1ea4ea0d1@sentry.pennlabs.org/18"; 5 | const dev = process.env.NODE_ENV !== "production"; 6 | 7 | if (process.browser) { 8 | // If the code is running in user's browser 9 | Sentry = require("@sentry/browser"); 10 | } else { 11 | // If code is running on the server 12 | Sentry = require("@sentry/node"); 13 | } 14 | 15 | if (!dev) { 16 | Sentry.init({ dsn: SENTRY_URL }); 17 | } 18 | 19 | export function logException(ex: Error, context?: any): void { 20 | Sentry.captureException(ex, { 21 | extra: context, 22 | }); 23 | window.console && console.error && console.error(ex); // eslint-disable-line no-console 24 | } 25 | 26 | export function logMessage(msg: string): void { 27 | Sentry.captureMessage(msg); 28 | } 29 | -------------------------------------------------------------------------------- /backend/ohq/tasks.py: -------------------------------------------------------------------------------- 1 | from celery import shared_task 2 | 3 | from ohq.models import Question 4 | from ohq.sms import sendUpNextNotification 5 | 6 | 7 | @shared_task(name="ohq.tasks.sendUpNextNotificationTask") 8 | def sendUpNextNotificationTask(queue_id): 9 | """ 10 | Send an SMS notification to the 3rd person in a queue if they have verified their phone number 11 | and the queue was at least 4 people long when they joined it. 12 | """ 13 | 14 | questions = Question.objects.filter(queue=queue_id, status=Question.STATUS_ASKED).order_by( 15 | "time_asked" 16 | ) 17 | if questions.count() >= 3: 18 | question = questions[2] 19 | user = question.asked_by 20 | if question.should_send_up_soon_notification and user.profile.sms_verified: 21 | sendUpNextNotification(user, question.queue.course) 22 | -------------------------------------------------------------------------------- /frontend/components/Home/AccountSettings/AccountSettings.tsx: -------------------------------------------------------------------------------- 1 | import { Segment, Header, Grid } from "semantic-ui-react"; 2 | import AccountForm from "./AccountForm"; 3 | import Footer from "../../common/Footer"; 4 | 5 | const AccountSettings = () => { 6 | return ( 7 | 11 | 12 | 13 |
Account Settings
14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 |