├── .github └── workflows │ └── shared-build-deploy.yaml ├── .gitignore ├── LICENSE ├── README.md ├── backend ├── .dockerignore ├── Dockerfile ├── docker-compose.yaml ├── manage.py ├── nginx-proddev.conf ├── officehoursqueue │ ├── __init__.py │ ├── asgi.py │ ├── celery.py │ ├── routing.py │ ├── settings │ │ ├── __init__.py │ │ ├── base.py │ │ ├── ci.py │ │ ├── development.py │ │ ├── proddev.py │ │ ├── production.py │ │ └── staging.py │ ├── templates │ │ ├── emails │ │ │ ├── base.html │ │ │ ├── course_added.html │ │ │ └── course_invitation.html │ │ └── redoc.html │ ├── urls.py │ └── wsgi.py ├── ohq │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── backends.py │ ├── filters.py │ ├── invite.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ ├── archive.py │ │ │ ├── calculatewaittimes.py │ │ │ ├── course_stat.py │ │ │ ├── createcourse.py │ │ │ ├── populate.py │ │ │ ├── queue_daily_stat.py │ │ │ ├── queue_heatmap_stat.py │ │ │ └── user_stat.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20200816_1727.py │ │ ├── 0003_auto_20200822_1116.py │ │ ├── 0004_auto_20200825_1344.py │ │ ├── 0005_auto_20201016_1702.py │ │ ├── 0006_auto_20210105_2000.py │ │ ├── 0007_announcement.py │ │ ├── 0008_auto_20210119_2218.py │ │ ├── 0009_auto_20210201_2224.py │ │ ├── 0010_auto_20210405_1720.py │ │ ├── 0010_auto_20210407_0145.py │ │ ├── 0011_merge_20210415_2110.py │ │ ├── 0012_queue_require_video_chat_url_on_questions.py │ │ ├── 0013_auto_20210924_2056.py │ │ ├── 0014_question_student_descriptor.py │ │ ├── 0015_question_templates.py │ │ ├── 0016_auto_20211008_2136.py │ │ ├── 0017_auto_20211031_1615.py │ │ ├── 0018_auto_20220125_0344.py │ │ ├── 0019_auto_20211114_1800.py │ │ ├── 0020_auto_20240326_0226.py │ │ ├── 0021_queue_question_timer_enabled_and_more.py │ │ └── __init__.py │ ├── models.py │ ├── pagination.py │ ├── permissions.py │ ├── queues.py │ ├── routing.py │ ├── schemas.py │ ├── serializers.py │ ├── sms.py │ ├── statistics.py │ ├── tasks.py │ ├── urls.py │ └── views.py ├── pyproject.toml ├── scripts │ └── asgi-run ├── setup.cfg ├── tests │ ├── __init__.py │ └── ohq │ │ ├── __init__.py │ │ ├── test_backends.py │ │ ├── test_commands.py │ │ ├── test_filters.py │ │ ├── test_health.py │ │ ├── test_invite.py │ │ ├── test_live.py │ │ ├── test_migrations.py │ │ ├── test_models.py │ │ ├── test_permissions.py │ │ ├── test_queues.py │ │ ├── test_serializers.py │ │ ├── test_sms.py │ │ ├── test_tasks.py │ │ └── test_views.py └── uv.lock ├── frontend ├── .dockerignore ├── .editorconfig ├── .eslintrc.json ├── .prettierrc.json ├── .projectile ├── Dockerfile ├── components │ ├── Auth │ │ └── AuthPrompt.tsx │ ├── Calendar │ │ ├── CalendarCommon.tsx │ │ ├── DashboardCalendar │ │ │ ├── EventCard.tsx │ │ │ └── EventSidebar.tsx │ │ ├── InstructorCalendar │ │ │ ├── InstructorCalendar.tsx │ │ │ └── InstructorCalendarModals.tsx │ │ ├── StudentCalendar │ │ │ └── StudentCalendar.tsx │ │ └── calendarUtils.ts │ ├── Changelog │ │ ├── changelogfile.md │ │ └── index.tsx │ ├── Course │ │ ├── Analytics │ │ │ ├── Analytics.tsx │ │ │ ├── Cards │ │ │ │ ├── AnalyticsCard.tsx │ │ │ │ └── SummaryCards.tsx │ │ │ ├── Heatmaps │ │ │ │ ├── Averages.tsx │ │ │ │ └── Heatmap.tsx │ │ │ └── mockData.tsx │ │ ├── Announcements.tsx │ │ ├── CourseSettings │ │ │ ├── CourseForm.module.css │ │ │ ├── CourseForm.tsx │ │ │ └── CourseSettings.tsx │ │ ├── CourseSidebarInstructorList.tsx │ │ ├── CourseSidebarNav.tsx │ │ ├── CourseWrapper.tsx │ │ ├── InstructorQueuePage │ │ │ ├── ClearQueueModal.tsx │ │ │ ├── FinishConfirmModal.tsx │ │ │ ├── InstructorQueuePage.tsx │ │ │ ├── InstructorQueues.tsx │ │ │ ├── MessageQuestionModal.tsx │ │ │ ├── QuestionCard.tsx │ │ │ ├── QuestionTimer.tsx │ │ │ ├── Questions.tsx │ │ │ ├── Queue.tsx │ │ │ ├── QueueCreate │ │ │ │ └── QueueCreate.tsx │ │ │ ├── QueueFormFields.tsx │ │ │ ├── QueueMenuItem.tsx │ │ │ ├── QueuePin.tsx │ │ │ ├── QueueSettings │ │ │ │ ├── QueueSettings.tsx │ │ │ │ └── QueueSettingsForm.tsx │ │ │ ├── RejectQuestionModal.tsx │ │ │ ├── Tags.tsx │ │ │ ├── aol.mp3 │ │ │ ├── notification.mp3 │ │ │ └── timeupsound.mp3 │ │ ├── Roster │ │ │ ├── ChangeRoleDropdown.tsx │ │ │ ├── Invites │ │ │ │ ├── AddForm.tsx │ │ │ │ └── InviteModal.tsx │ │ │ ├── RemoveButton.tsx │ │ │ ├── Roster.tsx │ │ │ └── RosterForm.tsx │ │ ├── StudentQueuePage │ │ │ ├── DeleteQuestionModal.tsx │ │ │ ├── EditQuestionModal.tsx │ │ │ ├── LastQuestionCard.tsx │ │ │ ├── QuestionCard.tsx │ │ │ ├── QuestionForm.tsx │ │ │ ├── QueueMenuItem.tsx │ │ │ ├── StudentQueue.tsx │ │ │ ├── StudentQueuePage.tsx │ │ │ └── StudentQueues.tsx │ │ └── Summary │ │ │ ├── Summary.tsx │ │ │ └── SummaryForm.tsx │ ├── Guide │ │ ├── InstructorGuide.tsx │ │ ├── InstructorGuideContent.tsx │ │ ├── StudentGuide.tsx │ │ ├── StudentGuideContent.tsx │ │ ├── index.tsx │ │ └── utils.tsx │ ├── Home │ │ ├── AccountSettings │ │ │ ├── AccountForm.tsx │ │ │ ├── AccountSettings.tsx │ │ │ ├── VerificationForm.tsx │ │ │ ├── VerificationModal.module.css │ │ │ └── VerificationModal.tsx │ │ ├── Dashboard │ │ │ ├── Cards │ │ │ │ ├── AddCard.tsx │ │ │ │ ├── ArchivedCourseCard.tsx │ │ │ │ └── CourseCard.tsx │ │ │ ├── Dashboard.tsx │ │ │ ├── Forms │ │ │ │ ├── AddStudentForm.tsx │ │ │ │ └── CreateCourseForm.tsx │ │ │ ├── InstructorCourses.tsx │ │ │ ├── Messages │ │ │ │ └── tips.json │ │ │ ├── Modals │ │ │ │ ├── ModalAddInstructorCourse.tsx │ │ │ │ ├── ModalAddStudentCourse.tsx │ │ │ │ ├── ModalLeaveStudentCourse.tsx │ │ │ │ ├── ModalRedirectAddCourse.tsx │ │ │ │ └── ModalShowNewChanges.tsx │ │ │ └── StudentCourses.tsx │ │ ├── Home.tsx │ │ └── HomeSidebar.tsx │ ├── SignOut │ │ └── index.tsx │ └── common │ │ ├── AboutModal.tsx │ │ ├── Footer.tsx │ │ └── ui │ │ ├── LinkedText.tsx │ │ └── ResponsiveIconButton.tsx ├── constants.ts ├── context │ └── auth.tsx ├── csrf.ts ├── global.d.ts ├── hooks │ ├── data-fetching │ │ ├── account.ts │ │ ├── analytics.ts │ │ ├── calendar.ts │ │ ├── course.ts │ │ ├── dashboard.ts │ │ ├── questionsummary.ts │ │ └── resources.ts │ ├── debounce.ts │ └── player.ts ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── _error.tsx │ ├── calendar.tsx │ ├── changelog.tsx │ ├── courses │ │ └── [course] │ │ │ ├── analytics.tsx │ │ │ ├── calendar.tsx │ │ │ ├── index.tsx │ │ │ ├── roster.tsx │ │ │ ├── settings.tsx │ │ │ └── summary.tsx │ ├── faq.tsx │ ├── health.tsx │ ├── index.tsx │ └── settings.tsx ├── public │ ├── answer-queue-1.png │ ├── create-course-1.png │ ├── create-course-2.png │ ├── create-course-3.png │ ├── favicon.ico │ ├── invite-members-1.png │ ├── join-course-1.png │ ├── join-course-2.png │ ├── joining-oh-1.png │ ├── notifications-1.png │ ├── ohq-login.png │ ├── ohq.png │ ├── open-queue-1.png │ ├── open-queue-2.png │ ├── put-queue-1.png │ ├── remove-queue-1.png │ ├── vercel.svg │ └── while-in-queue-1.png ├── server.js ├── styles │ ├── index.css │ └── landingpage.module.css ├── tsconfig.json ├── tsconfig.tsbuildinfo ├── types.tsx ├── utils │ ├── enums.tsx │ ├── fetch.tsx │ ├── ga │ │ ├── googleAnalytics.ts │ │ └── withGA.tsx │ ├── gippage.ts │ ├── index.tsx │ ├── notifications.ts │ ├── protectpage.tsx │ ├── redirect.ts │ ├── sentry.tsx │ └── staffcheck.ts └── yarn.lock ├── k8s ├── .gitignore ├── cdk8s.yaml ├── main.ts ├── package.json ├── tsconfig.json └── yarn.lock └── ohq.code-workspace /.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Penn Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /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/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM pennlabs/django-base:11d476546bd11c7a499e0e93be8db6af035d360f-3.11 2 | 3 | LABEL maintainer="Penn Labs" 4 | 5 | # Install uv 6 | COPY --from=ghcr.io/astral-sh/uv@sha256:2381d6aa60c326b71fd40023f921a0a3b8f91b14d5db6b90402e65a635053709 /uv /uvx /bin/ 7 | 8 | # Copy project dependencies 9 | COPY pyproject.toml uv.lock /app/ 10 | 11 | # Install dependencies 12 | RUN uv sync --frozen --no-dev --no-install-project --python $(which python); \ 13 | ln -s /app/.venv/bin/uwsgi /usr/local/bin/uwsgi; \ 14 | ln -s /app/.venv/bin/gunicorn /usr/local/bin/gunicorn 15 | 16 | # Make installed binaries available for POSIX compliant scripts 17 | ENV PATH="/app/.venv/bin:$PATH" 18 | 19 | # Patch for scripts that use a hard-coded path to python (django-run, asgi-run) 20 | ENV PYTHONPATH="/app/.venv/lib/python3.11/site-packages/:$PYTHONPATH" 21 | 22 | # Copy project files 23 | COPY . /app/ 24 | 25 | ENV DJANGO_SETTINGS_MODULE officehoursqueue.settings.production 26 | ENV SECRET_KEY 'temporary key just to build the docker image' 27 | 28 | # Copy custom asgi-run 29 | COPY ./scripts/asgi-run /usr/local/bin/ 30 | 31 | # Collect static files 32 | RUN python3 /app/manage.py collectstatic --noinput 33 | -------------------------------------------------------------------------------- /backend/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | db: 5 | image: postgres 6 | environment: 7 | - POSTGRES_DB=postgres 8 | - POSTGRES_USER=postgres 9 | - POSTGRES_PASSWORD=postgres 10 | ports: 11 | - "5432:5432" 12 | volumes: 13 | - ./postgres:/var/lib/postgresql/data 14 | redis: 15 | image: redis:4.0 16 | ports: 17 | - "6379:6379" 18 | # api, dashboard, and admin requests for proddev 19 | proddev-backend-wsgi: 20 | depends_on: 21 | - db 22 | - redis 23 | profiles: 24 | - proddev 25 | build: 26 | context: . 27 | dockerfile: Dockerfile 28 | ports: 29 | - "8001:80" 30 | environment: 31 | - DATABASE_URL=postgres://postgres:postgres@db:5432/postgres 32 | - DJANGO_SETTINGS_MODULE=officehoursqueue.settings.proddev 33 | command: sh -c "python manage.py migrate && { DJANGO_SUPERUSER_PASSWORD=root python manage.py createsuperuser --no-input --username root --email root@pennlabs.org; /usr/local/bin/django-run; }" 34 | # Web socket requests for proddev 35 | proddev-backend-asgi: 36 | depends_on: 37 | - db 38 | - redis 39 | profiles: 40 | - proddev 41 | build: 42 | context: . 43 | dockerfile: Dockerfile 44 | ports: 45 | - "8002:80" 46 | environment: 47 | - DATABASE_URL=postgres://postgres:postgres@db:5432/postgres 48 | - DJANGO_SETTINGS_MODULE=officehoursqueue.settings.proddev 49 | command: sh -c "python manage.py migrate && { DJANGO_SUPERUSER_PASSWORD=root python manage.py createsuperuser --no-input --username root --email root@pennlabs.org; /usr/local/bin/asgi-run; }" 50 | # frontend for proddev 51 | proddev-frontend: 52 | profiles: 53 | - proddev 54 | build: 55 | context: ../frontend 56 | dockerfile: ../frontend/Dockerfile 57 | ports: 58 | - "8003:80" 59 | # Reverse proxy for routing requests to the various proddev servers based on the path 60 | nginx: 61 | image: nginx:latest 62 | depends_on: 63 | - proddev-backend-wsgi 64 | - proddev-backend-asgi 65 | - proddev-frontend 66 | profiles: 67 | - proddev 68 | ports: 69 | - "8000:80" 70 | volumes: 71 | - ./nginx-proddev.conf:/etc/nginx/nginx.conf:ro 72 | -------------------------------------------------------------------------------- /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/nginx-proddev.conf: -------------------------------------------------------------------------------- 1 | # Reverse proxy configuration for emulating what will actually be run in production as described in /k8s/main.ts 2 | 3 | events { } 4 | 5 | http { 6 | server { 7 | listen 80; 8 | 9 | # Frontend is served unless overridden by other locations 10 | location / { 11 | proxy_pass http://proddev-frontend:3000; 12 | proxy_set_header Host $host; 13 | proxy_set_header X-Real-IP $remote_addr; 14 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 15 | } 16 | 17 | # The wsgi backend is used on routes starting with '/api', '/admin', or '/assets' 18 | location ~ ^/(api|admin|assets) { 19 | proxy_pass http://proddev-backend-wsgi:80; 20 | proxy_set_header Host $host; 21 | proxy_set_header X-Real-IP $remote_addr; 22 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 23 | } 24 | 25 | # The asgi backend is used for websocket requests on routes starting with '/api/ws' 26 | location /api/ws { 27 | proxy_pass http://proddev-backend-asgi:80; 28 | proxy_set_header Host $host; 29 | proxy_set_header X-Real-IP $remote_addr; 30 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 31 | 32 | # For web sockets 33 | proxy_http_version 1.1; 34 | proxy_set_header Upgrade $http_upgrade; 35 | proxy_set_header Connection "Upgrade"; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/officehoursqueue/__init__.py: -------------------------------------------------------------------------------- 1 | from officehoursqueue.celery import app as celery_app 2 | 3 | 4 | __all__ = ("celery_app",) 5 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /backend/officehoursqueue/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/office-hours-queue/7fdcdaeba0b83600950edfbdfb155ba62cd2febd/backend/officehoursqueue/settings/__init__.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /backend/officehoursqueue/settings/production.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import sentry_sdk 4 | from sentry_sdk.integrations.celery import CeleryIntegration 5 | from sentry_sdk.integrations.django import DjangoIntegration 6 | 7 | from officehoursqueue.settings.base import * # noqa: F401, F403 8 | from officehoursqueue.settings.base import DOMAINS, REDIS_URL 9 | 10 | 11 | DEBUG = False 12 | 13 | # Honour the 'X-Forwarded-Proto' header for request.is_secure() 14 | SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") 15 | 16 | # Allow production host headers 17 | ALLOWED_HOSTS = DOMAINS 18 | 19 | # Make sure SECRET_KEY is set to a secret in production 20 | SECRET_KEY = os.environ.get("SECRET_KEY", None) 21 | 22 | # Sentry settings 23 | SENTRY_URL = os.environ.get("SENTRY_URL", "") 24 | sentry_sdk.init(dsn=SENTRY_URL, integrations=[CeleryIntegration(), DjangoIntegration()]) 25 | 26 | # DLA settings 27 | PLATFORM_ACCOUNTS = {"ADMIN_PERMISSION": "ohq_admin"} 28 | 29 | # Email client settings 30 | EMAIL_HOST = os.getenv("SMTP_HOST") 31 | EMAIL_PORT = int(os.getenv("SMTP_PORT", 587)) 32 | EMAIL_HOST_USER = os.getenv("SMTP_USERNAME") 33 | EMAIL_HOST_PASSWORD = os.getenv("SMTP_PASSWORD") 34 | EMAIL_USE_TLS = True 35 | 36 | # Redis Channel Layer 37 | CHANNEL_LAYERS = { 38 | "default": { 39 | "BACKEND": "channels_redis.core.RedisChannelLayer", 40 | "CONFIG": {"hosts": [REDIS_URL]}, 41 | }, 42 | } 43 | -------------------------------------------------------------------------------- /backend/officehoursqueue/settings/staging.py: -------------------------------------------------------------------------------- 1 | from officehoursqueue.settings.base import * # noqa: F401, F403 2 | -------------------------------------------------------------------------------- /backend/officehoursqueue/templates/emails/base.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 63 | 64 | 65 | 66 | 67 |You have been added to {{ course }} as a {{ role }}.
8 | 9 |Click below to view this course's office hours queues!
10 | 11 |You have been invited to join {{ course }} as a {{ role }}.
8 | 9 |Click below to create your account!
10 | 11 |= NextPage
& { 4 | getInitialProps: (ctx: NextPageContext) => P | Promise
;
5 | };
6 |
--------------------------------------------------------------------------------
/frontend/utils/index.tsx:
--------------------------------------------------------------------------------
1 | import { ALLOWED_LINKS } from "../constants";
2 | import { Membership, Kind, User } from "../types";
3 |
4 | export function isValidEmail(email: string) {
5 | const pattern =
6 | /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
7 | return pattern.test(email);
8 | }
9 |
10 | export function isValidVideoChatURL(url: string) {
11 | try {
12 | // URL constructor does not prevent "http://www.zoom.us Meeting ID: ..."
13 | const pattern =
14 | /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)$/;
15 |
16 | if (!pattern.test(url)) {
17 | return false;
18 | }
19 |
20 | const urlObject = new URL(url);
21 | return ALLOWED_LINKS.reduce(
22 | (acc: boolean, link: string) =>
23 | acc ||
24 | urlObject.hostname === link ||
25 | urlObject.hostname.endsWith(`.${link}`),
26 | false
27 | );
28 | } catch (e) {
29 | return false;
30 | }
31 | }
32 |
33 | export function roleSortFunc(a: Kind, b: Kind) {
34 | const order = ["PROFESSOR", "HEAD_TA", "TA", "STUDENT"];
35 | return order.indexOf(a) - order.indexOf(b);
36 | }
37 |
38 | export function leadershipSortFunc(a: Membership, b: Membership) {
39 | if (a.kind !== b.kind) {
40 | return roleSortFunc(a.kind, b.kind);
41 | }
42 | if (a.user.firstName !== b.user.firstName) {
43 | return a.user.firstName < b.user.firstName ? -1 : 1;
44 | }
45 | if (a.user.lastName !== b.user.lastName) {
46 | return a.user.lastName < b.user.lastName ? -1 : 1;
47 | }
48 | if (a.user.email < b.user.email) {
49 | return a.user.email < b.user.email ? -1 : 1;
50 | }
51 | return 0;
52 | }
53 |
54 | export function getFullName(user: User): string {
55 | return `${user.firstName} ${user.lastName}`;
56 | }
57 |
--------------------------------------------------------------------------------
/frontend/utils/notifications.ts:
--------------------------------------------------------------------------------
1 | import { logException } from "./sentry";
2 |
3 | export function browserSupportsNotifications() {
4 | return typeof window !== "undefined" && "Notification" in window;
5 | }
6 |
7 | export function askNotificationPermissions() {
8 | if (browserSupportsNotifications()) {
9 | try {
10 | Notification.requestPermission();
11 | } catch (e) {
12 | logException(e);
13 | }
14 | }
15 | }
16 |
17 | export function playNotification(message: string) {
18 | if (browserSupportsNotifications()) {
19 | try {
20 | /* eslint-disable-next-line */
21 | new Notification("Alert", {
22 | body: message,
23 | icon: "../favicon.ico",
24 | });
25 | } catch (e) {
26 | logException(e);
27 | }
28 | }
29 | }
30 |
31 | export function checkPermissions() {
32 | return localStorage && localStorage.getItem("notifs") === "false";
33 | }
34 |
--------------------------------------------------------------------------------
/frontend/utils/protectpage.tsx:
--------------------------------------------------------------------------------
1 | import { NextPageContext } from "next";
2 | import { AuthProps } from "../context/auth";
3 | import { User } from "../types";
4 | import nextRedirect from "./redirect";
5 | import { GIPPage } from "./gippage";
6 |
7 | export function withProtectPage