├── .circleci ├── config.yml ├── env-students_cs_dev.enc └── env.enc ├── .dockerignore ├── .editorconfig ├── .env.sample ├── .githooks └── pre-commit ├── .gitignore ├── .idea ├── d0.iml ├── jsonSchemas.xml ├── modules.xml └── vcs.xml ├── .nvmrc ├── .prettierrc.js ├── .yarn └── releases │ └── yarn-1.18.0.cjs ├── .yarnrc ├── Dockerfile ├── LICENSE ├── README.md ├── deploy-image.sh ├── docker-compose.yml ├── docs ├── assets │ ├── admin-config-classlist.png │ ├── admin-config-deliv-config.png │ ├── admin-create-deliv.png │ ├── admin-force-regrade.png │ ├── admin-provisioning-options.png │ ├── admin-request-feedback.png │ ├── admin-silent-regrade.png │ ├── admin-view-dashboard.png │ ├── admin-view-grades.png │ ├── admin-view-results.png │ ├── admin-view-students.png │ ├── autograde-flow.svg │ ├── autograde-image-run.svg │ ├── autotest-options.png │ ├── circle-ci-key.png │ ├── classy-logged-in.png │ ├── classy-login-main.png │ ├── classy-network-layer.svg │ ├── commit-comment-build-failure.png │ ├── commit-comment-feedback.png │ ├── commit-comment-schedule.png │ ├── commit-comment-wait.png │ ├── dev-process.graffle │ ├── dockerfile-classy-admin-portal.png │ ├── invalid-classy-credentials.png │ ├── oauth-application-credentials.png │ ├── organization-profile.png │ ├── portal-admin-config.png │ ├── pull-request-to-upstream.svg │ ├── pulling-changes-to-fork.svg │ ├── subdirectory-dockerfile-classy.png │ ├── test-screenshot.png │ ├── ubcbot_gravatar.png │ └── vm-container-applications.svg ├── courses │ └── 310.md ├── developer │ ├── bootstrap.md │ ├── container.md │ ├── continuousintegration.md │ ├── contributing.md │ └── customization.md ├── instructor │ ├── autograde.md │ ├── autogradecreation.md │ ├── autotest.md │ ├── features.md │ ├── gettingstarted.md │ └── portal.md └── tech-staff │ ├── architecture.md │ ├── backups.md │ ├── envconfig.md │ ├── githubsetup.md │ ├── hardware.md │ ├── install.md │ ├── operatingclassy.md │ ├── semestertransitions.md │ ├── startstop.md │ ├── termtransitions.md │ └── updates.md ├── helper-scripts ├── bootstrap-plugin.sh ├── data_export_json.sh ├── make_test_org.py ├── set-docker-override.sh └── set-nginx-conf.sh ├── package.json ├── packages ├── autotest │ ├── Dockerfile │ ├── README.md │ ├── package.json │ ├── src │ │ ├── AutoTestDaemon.ts │ │ ├── autotest │ │ │ ├── AutoTest.ts │ │ │ ├── ClassPortal.ts │ │ │ ├── DataStore.ts │ │ │ ├── GradingJob.ts │ │ │ ├── Queue.ts │ │ │ └── mocks │ │ │ │ ├── MockClassPortal.ts │ │ │ │ ├── MockDataStore.ts │ │ │ │ └── MockGradingJob.ts │ │ ├── github │ │ │ ├── GitHubAutoTest.ts │ │ │ └── GitHubUtil.ts │ │ └── server │ │ │ ├── AutoTestRouteHandler.ts │ │ │ └── AutoTestServer.ts │ ├── test │ │ ├── AutoTestServerSpec.ts │ │ ├── ClassPortalSpec.ts │ │ ├── GitHubAutoTestSpec.ts │ │ ├── GitHubEventSpec.ts │ │ ├── GitHubServiceSpec.ts │ │ ├── GitHubUtilSpec.ts │ │ ├── GradingJobSpec.ts │ │ ├── MongoStoreSpec.ts │ │ ├── QueueSpec.ts │ │ ├── TestData.ts │ │ ├── githubAutoTestData │ │ │ ├── commentRecords.json │ │ │ ├── feedbackRecords.json │ │ │ ├── outputRecords.json │ │ │ ├── pushRecords.json │ │ │ └── pushes.json │ │ └── githubEvents │ │ │ ├── comment_master_bot_one-deliv.json │ │ │ ├── comment_master_bot_two-deliv.json │ │ │ ├── comment_master_bot_zero-deliv.json │ │ │ ├── comment_master_no-bot_no-deliv.json │ │ │ ├── comment_other-branch_bot_one-deliv.json │ │ │ ├── push_create-new-branch.json │ │ │ ├── push_delete-branch.json │ │ │ ├── push_master-branch.json │ │ │ └── push_other-branch.json │ └── tsconfig.json ├── common │ ├── src │ │ ├── Config.ts │ │ ├── Log.ts │ │ ├── Util.ts │ │ ├── commands │ │ │ ├── Command.ts │ │ │ └── GitRepository.ts │ │ └── types │ │ │ ├── AutoTestTypes.ts │ │ │ ├── CS340Types.ts │ │ │ ├── ContainerTypes.ts │ │ │ ├── PortalTypes.ts │ │ │ └── SDMMTypes.ts │ ├── test │ │ ├── GlobalSpec.ts │ │ ├── TestHarness.ts │ │ └── UtilSpec.ts │ └── tsconfig.json ├── portal │ ├── Dockerfile │ ├── backend │ │ ├── README.md │ │ ├── html │ │ │ └── index.html │ │ ├── package.json │ │ ├── src-util │ │ │ ├── DatabaseValidator.ts │ │ │ ├── FrontendDatasetGenerator.ts │ │ │ ├── GitHubCleaner.ts │ │ │ ├── InvokeAutoTest.ts │ │ │ ├── README.md │ │ │ ├── RepositoryUpdater.ts │ │ │ ├── TransformGrades.ts │ │ │ ├── TraverseResults.ts │ │ │ └── WebhookUpdater.ts │ │ ├── src │ │ │ ├── BackendDaemon.ts │ │ │ ├── Factory.ts │ │ │ ├── Types.ts │ │ │ ├── controllers │ │ │ │ ├── AdminController.ts │ │ │ │ ├── AuthController.ts │ │ │ │ ├── CourseController.ts │ │ │ │ ├── DatabaseController.ts │ │ │ │ ├── DeliverablesController.ts │ │ │ │ ├── GitHubActions.ts │ │ │ │ ├── GitHubController.ts │ │ │ │ ├── GradesController.ts │ │ │ │ ├── PersonController.ts │ │ │ │ ├── RepositoryController.ts │ │ │ │ ├── ResultsController.ts │ │ │ │ └── TeamController.ts │ │ │ └── server │ │ │ │ ├── BackendServer.ts │ │ │ │ ├── IREST.ts │ │ │ │ └── common │ │ │ │ ├── AdminRoutes.ts │ │ │ │ ├── AuthRoutes.ts │ │ │ │ ├── AutoTestRoutes.ts │ │ │ │ ├── CSVParser.ts │ │ │ │ ├── CSVPrairieLearnParser.ts │ │ │ │ ├── ClasslistAgent.ts │ │ │ │ └── GeneralRoutes.ts │ │ ├── test │ │ │ ├── CSVParserSpec.ts │ │ │ ├── CSVPrairieLearnParserSpec.ts │ │ │ ├── ClasslistAgentSpec.ts │ │ │ ├── FactorySpec.ts │ │ │ ├── controllers │ │ │ │ ├── AdminControllerSpec.ts │ │ │ │ ├── AuthControllerSpec.ts │ │ │ │ ├── CourseControllerSpec.ts │ │ │ │ ├── DatabaseControllerSpec.ts │ │ │ │ ├── DeliverablesControllerSpec.ts │ │ │ │ ├── GitHubActionSpec.ts │ │ │ │ ├── GitHubControllerSpec.ts │ │ │ │ ├── GradeControllerSpec.ts │ │ │ │ ├── PersonControllerSpec.ts │ │ │ │ ├── RepositoryControllerSpec.ts │ │ │ │ ├── ResultControllerSpec.ts │ │ │ │ ├── TeamControllerSpec.ts │ │ │ │ └── TestGitHubActions.ts │ │ │ ├── data │ │ │ │ ├── classlistDuplicateField.csv │ │ │ │ ├── classlistEmpty.csv │ │ │ │ ├── classlistEmptyField.csv │ │ │ │ ├── classlistEmptyNamePrefName.csv │ │ │ │ ├── classlistInvalid.csv │ │ │ │ ├── classlistTest.csv │ │ │ │ ├── classlistTestLarge.csv │ │ │ │ ├── classlistTestSingle.csv │ │ │ │ ├── classlistValidFirst.csv │ │ │ │ ├── classlistValidPrefName.csv │ │ │ │ ├── classlistValidUpdate.csv │ │ │ │ ├── gradesEmpty.csv │ │ │ │ ├── gradesInconsistent.csv │ │ │ │ ├── gradesInconsistent2.csv │ │ │ │ ├── gradesInvalid.csv │ │ │ │ ├── gradesValid.csv │ │ │ │ ├── gradesValidBucket.csv │ │ │ │ ├── gradesValidBucketCWL.csv │ │ │ │ ├── gradesValidBucketGithub.csv │ │ │ │ ├── prairieEmpty.csv │ │ │ │ ├── prairieValid.csv │ │ │ │ └── prairieValidUpload.csv │ │ │ ├── server │ │ │ │ ├── AdminRoutesSpec.ts │ │ │ │ ├── AuthRoutesSpec.ts │ │ │ │ ├── AutoTestRoutesSpec.ts │ │ │ │ └── GeneralRoutesSpec.ts │ │ │ └── xRunLast │ │ │ │ └── TestDatasetGeneratorSpec.ts │ │ └── tsconfig.json │ └── frontend │ │ ├── README.md │ │ ├── html │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── invalid.html │ │ ├── stdio.html │ │ └── style.css │ │ ├── package.json │ │ ├── src │ │ └── app │ │ │ ├── App.ts │ │ │ ├── Factory.ts │ │ │ ├── util │ │ │ ├── AuthHelper.ts │ │ │ ├── DashboardTable.ts │ │ │ ├── Network.ts │ │ │ ├── SortableTable.ts │ │ │ └── UI.ts │ │ │ └── views │ │ │ ├── AbstractStudentView.ts │ │ │ ├── AdminConfigTab.ts │ │ │ ├── AdminDashboardTab.ts │ │ │ ├── AdminDeleteGraderPage.ts │ │ │ ├── AdminDeletePage.ts │ │ │ ├── AdminDeliverablesTab.ts │ │ │ ├── AdminGradesTab.ts │ │ │ ├── AdminPage.ts │ │ │ ├── AdminProvisionPage.ts │ │ │ ├── AdminPullRequestsPage.ts │ │ │ ├── AdminResultsTab.ts │ │ │ ├── AdminStudentsTab.ts │ │ │ ├── AdminTeamsTab.ts │ │ │ ├── AdminView.ts │ │ │ ├── DockerListImageView.ts │ │ │ ├── IView.ts │ │ │ └── classy │ │ │ ├── ClassyAdminView.ts │ │ │ └── ClassyStudentView.ts │ │ ├── tsconfig.json │ │ └── webpack.config.js └── proxy │ ├── Dockerfile │ ├── README.md │ ├── nginx.rconf.default │ └── proxy.conf ├── plugins ├── default │ ├── docker │ │ └── docker-compose.override.yml │ ├── nginx │ │ └── nginx.rconf │ └── portal │ │ ├── backend │ │ ├── CustomCourseController.ts │ │ └── CustomCourseRoutes.ts │ │ └── frontend │ │ ├── CustomAdminView.ts │ │ ├── CustomStudentView.ts │ │ └── html │ │ ├── admin.html │ │ ├── custom.html │ │ ├── landing.html │ │ ├── login.html │ │ └── student.html └── example │ ├── docker │ └── docker-compose.override.yml │ ├── helloworld │ ├── Dockerfile │ ├── package.json │ └── serve_json.js │ ├── nginx │ └── nginx.rconf │ └── portal │ ├── backend │ ├── CustomCourseController.ts │ └── CustomCourseRoutes.ts │ └── frontend │ ├── CustomAdminView.ts │ ├── CustomStudentView.ts │ └── html │ ├── admin.html │ ├── custom.html │ ├── landing.html │ ├── login.html │ └── student.html ├── testOutput └── index.html ├── tsconfig.json ├── tslint.json └── yarn.lock /.circleci/env-students_cs_dev.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/.circleci/env-students_cs_dev.enc -------------------------------------------------------------------------------- /.circleci/env.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/.circleci/env.enc -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .circleci 2 | .vscode 3 | docs 4 | 5 | **/Dockerfile 6 | **/node_modules/ 7 | packages/**/src/**/*.js* 8 | packages/**/test 9 | packages/common/**/*.js* 10 | packages/portal/backend/src-util 11 | 12 | testOutput 13 | .dockerignore 14 | .editorconfig 15 | # This is required by the common package 16 | #.env 17 | .env.sample 18 | .git 19 | .gitignore 20 | .rysncexclude 21 | LICENSE 22 | README.md 23 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [*.ts] 10 | indent_style = tab 11 | indent_size = 4 12 | max_line_length = 140 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | yarn prettier:check && yarn build 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node 2 | **/node_modules/ 3 | 4 | # Yarn 5 | **/yarn-debug.log* 6 | **/yarn-error.log* 7 | **/.yarn-integrity 8 | 9 | # NYC 10 | **/.nyc_output 11 | **/coverage/ 12 | 13 | # WebStorm (https://www.jetbrains.com/help/idea/managing-projects-under-version-control.html) 14 | **/.idea/workspace.xml 15 | **/.idea/tasks.xml 16 | 17 | # Compiled source 18 | **/src/**/*.js 19 | **/src/**/*.map 20 | **/test/**/*.js 21 | **/test/**/*.map 22 | **/html/**/*.js 23 | **/html/**/*.map 24 | **/bin/ 25 | 26 | persist/queues/**.json 27 | 28 | packages/common/**/*.js 29 | packages/common/**/*.map 30 | packages/portal/backend/**/*.js 31 | packages/portal/backend/**/*.map 32 | packages/portal/frontend/**/*.js 33 | packages/portal/frontend/**/*.map 34 | 35 | ## Fork-specific resources 36 | packages/portal/backend/test/cs310/ 37 | packages/portal/frontend/html/cs310/ 38 | packages/portal/frontend/src/app/custom/CustomAdminView.ts 39 | packages/portal/frontend/src/app/custom/CustomStudentView.ts 40 | packages/portal/backend/src/custom/CustomCourseController.ts 41 | packages/portal/backend/src/custom/CustomCourseRoutes.ts 42 | 43 | # Plugins 44 | packages/portal/frontend/src/app/plugs/ 45 | 46 | .circleci/ 47 | 48 | # Webstorm 49 | **/.idea 50 | 51 | # Visual Studio 52 | .vscode/ 53 | 54 | # MacOS 55 | **/.DS_Store 56 | 57 | # Env 58 | **/.env 59 | **/.env* 60 | 61 | # Misc 62 | /data/ 63 | .rsyncexclude 64 | testOutput/ 65 | packages/autotest/testOutput/ 66 | packages/autotest/test/data/ 67 | 68 | # SSL 69 | ssl/ 70 | ssl.log 71 | 72 | # Excludes plugins aside default plugin 73 | plugins/* 74 | !plugins/default/ 75 | plugins/default/**/*.js 76 | plugins/default/**/*.js.map 77 | !plugins/example/ 78 | plugins/example/**/*.js 79 | plugins/example/**/*.js.map 80 | docker-compose.override.yml 81 | packages/proxy/nginx.rconf 82 | -------------------------------------------------------------------------------- /.idea/d0.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/jsonSchemas.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/carbon 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | // "tabWidth": 4, // set in .editorConfig 3 | // "useTabs": true, // set in .editorConfig 4 | // "endOfLine": "lf", // set in .editorConfig 5 | // "printWidth": 140, // set in .editorConfig 6 | 7 | "trailingComma": "es5", 8 | "bracketSpacing": true, 9 | "bracketSameLine": false, 10 | "arrowParens": "always", 11 | "semi": true, 12 | "singleQuote": false, 13 | "quoteProps": "as-needed", 14 | }; 15 | 16 | module.exports = config; 17 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | yarn-path ".yarn/releases/yarn-1.18.0.cjs" 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | WORKDIR /app 4 | COPY package.json tsconfig.json .env ./ 5 | COPY packages/common ./packages/common 6 | RUN yarn config set workspaces-experimental true \ 7 | && yarn global add typescript@2.6.2 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 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 | -------------------------------------------------------------------------------- /deploy-image.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Deploys the specified image (optionally from a pull request by specifying the id) 4 | 5 | GITHUB_TOKEN=$(grep GH_DOCKER_TOKEN .env | cut -d '=' -f2) 6 | URL="https://${GITHUB_TOKEN}@github.students.cs.ubc.ca/cpsc310/project-resources.git#" 7 | 8 | if [[ $2 == +([0-9]) ]]; then 9 | URL="${URL}pull/${2}/head" 10 | fi 11 | 12 | case "${1}" in 13 | grade) 14 | docker build --tag grader --file grade.dockerfile "${URL/%#/}" 15 | ;; 16 | ui) 17 | docker build --tag cpsc310reference_ui --file ui.dockerfile "${URL/%#/}" && \ 18 | docker-compose up --detach reference_ui 19 | ;; 20 | geo) 21 | docker build --tag cpsc310geocoder "${URL}:geocoder" 22 | docker-compose up --detach geolocation 23 | ;; 24 | *) 25 | echo $"Usage: $0 {grade|ui|geo} [PR#]" 26 | exit 1 27 | esac 28 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Before deploying, make sure you have done the following: 2 | # - created a user `classy` on the host 3 | # - installed SSL certificates for the host 4 | # - created a `.env` file in the same dir as this file and populated appropriately 5 | # - opened port 80 and port 443 (publicly) 6 | # 7 | # A few high-level notes about this config file: 8 | # - At deploy-time, all services have access to the values in the .env file; however, they are only accessible to the 9 | # running service if the env_file directive is specified. If you only need to pass a subset of the env vars, use the 10 | # environment directive and list only the var keys you need. 11 | # - As configured, only ports 80 and 443 are seen by the host; all other ports listed (with the expose directive) are 12 | # only accessible to linked services (i.e. those listed in the depends_on directive). If a service should be publicly 13 | # accessible, consider listing it in the proxy service instead of opening additional ports on the host. 14 | # - In general, services should be started as non-root users. Here, we launch services as the classy user (configured on 15 | # the host) using the user directive. 16 | # - Services specified here can be extended (and additional services can be added) by creating additional 17 | # docker-compose.yml files. See https://docs.docker.com/compose/extends/#example-use-case. 18 | 19 | # NOTE: Do not change the container names. They are used to refer to the service throughout the codebase in http requests. 20 | 21 | services: 22 | autotest: 23 | build: 24 | context: ./ 25 | dockerfile: ./packages/autotest/Dockerfile 26 | container_name: autotest 27 | depends_on: 28 | - db 29 | env_file: .env 30 | expose: 31 | - ${AUTOTEST_PORT} 32 | restart: always 33 | # for localhost testing: comment out the next field (user) 34 | # but ensure it is always UNCOMMENTED when committing 35 | user: "${UID}:${GID}" 36 | volumes: 37 | - "${HOST_DIR}:${PERSIST_DIR}" 38 | - "/var/run/docker.sock:/var/run/docker.sock" 39 | db: 40 | # mongo logs are almost never interesting 41 | # but if you have a problem, remove the logpath 42 | command: --slowms 250 --quiet --logpath /dev/null 43 | container_name: db 44 | environment: 45 | - MONGO_INITDB_ROOT_USERNAME 46 | - MONGO_INITDB_ROOT_PASSWORD 47 | ports: 48 | - "27017:27017" 49 | image: mongo:5.0.14 50 | restart: always 51 | user: "${UID}" 52 | volumes: 53 | - "${HOST_DIR}/db:/data/db" 54 | portal: 55 | build: 56 | args: 57 | - GH_BOT_USERNAME 58 | - GH_BOT_EMAIL 59 | - PLUGIN 60 | context: ./ 61 | dockerfile: ./packages/portal/Dockerfile 62 | container_name: portal 63 | depends_on: 64 | - db 65 | - autotest 66 | env_file: .env 67 | expose: 68 | - ${BACKEND_PORT} 69 | restart: always 70 | user: "${UID}" 71 | volumes: 72 | - "${HOST_SSL_CERT_PATH}:${SSL_CERT_PATH}" 73 | - "${HOST_SSL_KEY_PATH}:${SSL_KEY_PATH}" 74 | - "${HOST_DIR}:${PERSIST_DIR}:ro" 75 | proxy: 76 | build: 77 | args: 78 | - UID 79 | - SSL_CERT_PATH 80 | - SSL_KEY_PATH 81 | - BACKEND_PORT 82 | context: ./ 83 | dockerfile: ./packages/proxy/Dockerfile 84 | container_name: proxy 85 | depends_on: 86 | - portal 87 | ports: 88 | - "80:8080" 89 | - "443:8443" 90 | restart: always 91 | user: "${UID}" 92 | volumes: 93 | - "${HOST_SSL_CERT_PATH}:${SSL_CERT_PATH}" 94 | - "${HOST_SSL_KEY_PATH}:${SSL_KEY_PATH}" 95 | -------------------------------------------------------------------------------- /docs/assets/admin-config-classlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/admin-config-classlist.png -------------------------------------------------------------------------------- /docs/assets/admin-config-deliv-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/admin-config-deliv-config.png -------------------------------------------------------------------------------- /docs/assets/admin-create-deliv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/admin-create-deliv.png -------------------------------------------------------------------------------- /docs/assets/admin-force-regrade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/admin-force-regrade.png -------------------------------------------------------------------------------- /docs/assets/admin-provisioning-options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/admin-provisioning-options.png -------------------------------------------------------------------------------- /docs/assets/admin-request-feedback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/admin-request-feedback.png -------------------------------------------------------------------------------- /docs/assets/admin-silent-regrade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/admin-silent-regrade.png -------------------------------------------------------------------------------- /docs/assets/admin-view-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/admin-view-dashboard.png -------------------------------------------------------------------------------- /docs/assets/admin-view-grades.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/admin-view-grades.png -------------------------------------------------------------------------------- /docs/assets/admin-view-results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/admin-view-results.png -------------------------------------------------------------------------------- /docs/assets/admin-view-students.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/admin-view-students.png -------------------------------------------------------------------------------- /docs/assets/autotest-options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/autotest-options.png -------------------------------------------------------------------------------- /docs/assets/circle-ci-key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/circle-ci-key.png -------------------------------------------------------------------------------- /docs/assets/classy-logged-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/classy-logged-in.png -------------------------------------------------------------------------------- /docs/assets/classy-login-main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/classy-login-main.png -------------------------------------------------------------------------------- /docs/assets/commit-comment-build-failure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/commit-comment-build-failure.png -------------------------------------------------------------------------------- /docs/assets/commit-comment-feedback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/commit-comment-feedback.png -------------------------------------------------------------------------------- /docs/assets/commit-comment-schedule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/commit-comment-schedule.png -------------------------------------------------------------------------------- /docs/assets/commit-comment-wait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/commit-comment-wait.png -------------------------------------------------------------------------------- /docs/assets/dev-process.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/dev-process.graffle -------------------------------------------------------------------------------- /docs/assets/dockerfile-classy-admin-portal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/dockerfile-classy-admin-portal.png -------------------------------------------------------------------------------- /docs/assets/invalid-classy-credentials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/invalid-classy-credentials.png -------------------------------------------------------------------------------- /docs/assets/oauth-application-credentials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/oauth-application-credentials.png -------------------------------------------------------------------------------- /docs/assets/organization-profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/organization-profile.png -------------------------------------------------------------------------------- /docs/assets/portal-admin-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/portal-admin-config.png -------------------------------------------------------------------------------- /docs/assets/subdirectory-dockerfile-classy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/subdirectory-dockerfile-classy.png -------------------------------------------------------------------------------- /docs/assets/test-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/test-screenshot.png -------------------------------------------------------------------------------- /docs/assets/ubcbot_gravatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/docs/assets/ubcbot_gravatar.png -------------------------------------------------------------------------------- /docs/courses/310.md: -------------------------------------------------------------------------------- 1 | # CPSC 310 2 | 3 | ## Note: this document is stale; better docs exist [here](https://github.students.cs.ubc.ca/CPSC310/project-resources/blob/master/docs/README.md) and [here](https://github.students.cs.ubc.ca/CPSC310/classy-cs310-plugin). 4 | 5 | ### Viewing with the database (changes strongly discouraged) 6 | 7 | * Configure your SSH keys on `remote` and `cs310` 8 | * Open an SSH tunnel: ssh -J remote.cs.ubc.ca cs310.students.cs.ubc.ca -L 27017:127.0.0.1:27017 9 | * With Robo3T: connect to `localhost:27017`. Do not use SSH tunnel (in Robo) and just connect as if local. 10 | -------------------------------------------------------------------------------- /docs/developer/bootstrap.md: -------------------------------------------------------------------------------- 1 | # Bootstrapping Classy for Development 2 | 3 | Although Classy is containerized, configuring your development instance does not require building Docker containers. The [Classy](https://github.com/ubccpsc/classy) repository consists of two REST-based projects and a JavaScript front-end application that is served by on one of the REST APIs as static HTML content. These applications can be run separately, or together, in your IDE or from the command line in debugging mode. TypeScript source maps are produced during compilation for debugging the application during runtime. 4 | 5 | ## Software Dependencies 6 | 7 | The software dependencies that are currently used in production and recommended to work in development: 8 | 9 | - Node JS > v12.13.0 < v13 [Download](https://nodejs.org/en/download/) (or use `nvm`) 10 | - Yarn v1.19.1+ [Installation](https://yarnpkg.com/lang/en/docs/install) 11 | - Docker v19.03.4, build 9013bf583a [Install](https://docs.docker.com/install/) 12 | - IDE: JetBrain's Webstorm is recommended; VSCode is supported 13 | - MongoDB > 3.6.7 (Docker: `docker run -p 27017:27017 mongo`, or [Install](https://docs.mongodb.com/manual/installation/)) 14 | 15 | **NOTE**: MongoDB must be running before starting **AutoTest** or **Portal**. 16 | 17 | ## Environmental Config 18 | 19 | You will need to ensure the required environment variables, which you can see in `packages/common/Config.ts`, are set. This can be done by copying `.env.sample` to `.env` in the root of the project and modifying as needed. It is ***CRUCIAL*** that your `.env` file is never committed to version control. 20 | 21 | The sample configuration file includes a lot of documentation inline so [take a look](https://github.com/ubccpsc/classy/blob/main/.env.sample). 22 | 23 | ## GitHub Setup 24 | 25 | Classy manages administrators using GitHub teams. The GitHub organization that the course uses should have a `staff` and `admin` team. Users on the GitHub `staff` and `admin` teams will have access to the Classy Admin Portal, although users on the `staff` team will have greater privileges (e.g., the ability to configure the course). The bot user should be added as an owner of the organization. 26 | 27 | ## Install/Build 28 | 29 | To install Classy for development: 30 | 31 | 1. Type `git clone https://github.com/ORGNAME/classy` 32 | 2. `cd classy` to navigate inside the directory. 33 | 3. Inside the directory, type `yarn install` to fetch library dependencies. 34 | 4. Then type `yarn run build` to build the project. 35 | 36 | During the build step, a source-map was produced with the built code, which allows you to set breakpoints and debug in your IDE. 37 | 38 | 5. You are ready to run any of the applications (commands found in `package.json` files under respective application package directories). 39 | 40 | ## Running as dev 41 | 42 | There are a variety of services you may want to run independently while developing. 43 | Most will require configuring mongo to run in dev mode (see `DB_URL` in `.env`). 44 | The most common of these services can be invoked from the `classy/` directory through either the terminal or IDE: 45 | 46 | * Classy backend: `node -r tsconfig-paths/register packages/portal/backend/src/BackendDaemon.js` 47 | * Classy frontend: Instructions in `packages/portal/frontend/README.md` 48 | * Autotest backend: `node packages/autotest/src/AutoTestDaemon.js` 49 | 50 | Some handy dev scripts also exist; these can be found in `portal/backend/src-util/`; use these with care, many modify the database or GitHub repos in unrecoverable ways. 51 | 52 | The automated test suite is stored in: 53 | * `packages/autotest/test/` 54 | * `packages/portal/backend/test/` 55 | 56 | To run these in the IDE create a Mocha target in Webstorm with `-r tsconfig-paths/register` as the node options and `--exit` as the mocha options. 57 | To run these on the terminal, execute `yarn run test` in `packages/autotest/` or `packages/portal/backend/` 58 | 59 | ### Coverage 60 | 61 | The best way to run coverage locally is to execute `yarn run cover` in `classy/`. The coverage report will be generated in `testOutput/coverage/index.html`. NOTE: when executing locally, mocks are extensively used so the report will not be as comprehensive as executing on CI. 62 | 63 | ## QA Checklist 64 | 65 | More checks may need to be made depending on the nature of your work, but these are the recommended checks: 66 | 67 | 1. [ ] Portal Back-end compiles 68 | 2. [ ] Portal Front-end compiles 69 | 3. [ ] AutoTest compiles 70 | 4. [ ] CI tests pass for Portal Back-end 71 | 5. [ ] CI tests pass for AutoTest 72 | 6. [ ] Project containers build successfully (`docker compose build` and `docker compose up`) 73 | 74 | *NOTE*: 75 | 76 | - Items 1-5 can all be fulfilled by CircleCI integration. 77 | - Item 6 can only be done manually at this time. 78 | - Item 6 requires a properly-setup environmental file with SSL certificates. 79 | -------------------------------------------------------------------------------- /docs/developer/container.md: -------------------------------------------------------------------------------- 1 | # AutoTest Containers 2 | 3 | AutoTest uses course-specific containers to evaluate and execute student code for formative and summative assessment. This model gives course owners full control over how their student submissions are assessed and the kind of feedback that is provided to them. 4 | 5 | Usually AutoTest executes against all push events students make to GitHub (the most recent commit within a push), although students can request AutoTest to run on any commit within a push. The automatic execution is used to provide sanity checking feedback to students and is not meant to be used for grading (e.g., in CPSC 310 they are given warnings that their code does not build and that the test suite will not run). 6 | 7 | Containers are launched with the following input: 8 | 9 | ```typescript 10 | 11 | /** 12 | * Primary data structure that the course container is invoked with. 13 | */ 14 | export interface IContainerInput { 15 | delivId: string; // Specifies what delivId the commit should execute against. 16 | target: ICommitTarget; // Details about the push event that led to this request. 17 | containerConfig: AutoTestConfig; // Containers can usually ignore this. 18 | } 19 | 20 | export interface CommitTarget { 21 | delivId: string; 22 | repoId: string; 23 | 24 | cloneURL: string; // URL container should clone 25 | commitSHA: string; // commit container should checkout 26 | commitURL: string; 27 | 28 | postbackURL: string; // Containers can ignore this. 29 | timestamp: number; // Timestamp of push event (not the commit). GitHub can only enforce this timestamp so it is the one we must use. 30 | } 31 | 32 | /** 33 | * Description of the configuration parameters for the AutoTest container. 34 | * These can be specified per-deliverable in the Portal UI. 35 | */ 36 | export interface AutoTestConfig { 37 | dockerImage: string; 38 | studentDelay: number; 39 | maxExecTime: number; 40 | regressionDelivIds: string[]; 41 | custom: {}; 42 | } 43 | 44 | ``` 45 | 46 | And must provide the following output: 47 | 48 | ```typescript 49 | /** 50 | * Primary data structure that the course container returns. 51 | */ 52 | export interface IGradeReport { 53 | scoreOverall: number; // must be set 54 | scoreTest: number | null; // null means not valid for this report 55 | scoreCover: number | null; // null means not valid for this report 56 | 57 | // The semantics of these four categories are up to the container 58 | // we only differentiate them so the report UI can render them uniquely. 59 | // Set to [] for any unused property. 60 | passNames: string[]; 61 | failNames: string[]; 62 | errorNames: string[]; 63 | skipNames: string[]; 64 | 65 | // This is the text of the feedback (in markdown) that the container wants 66 | // to return to the user. 67 | feedback: string; 68 | 69 | // Enables custom values to be returned to the UI layer. 70 | // PLEASE: do not store large objects in here or it will 71 | // significantly impact the performance of the dashboard. 72 | // Use attachments instead for large bits of data you wish 73 | // to persist. 74 | custom: {}; 75 | } 76 | ``` 77 | 78 | There is also a mechanism for the container to return file-based output that can be viewed by course staff (TODO: document this). 79 | 80 | 81 | ## Developer Guide 82 | 83 | - If a container executes for an excessive amount of time, AutoTest with terminate the container by sending a SIGTERM. 84 | After a grace period, AutoTest will forcibly terminate the container with a SIGKILL. 85 | It is recommended that the _exec_ form of `CMD` and `ENTRYPOINT` are used to start the main process so that these signals are forwarded to the main process. 86 | 87 | - AutoTest will capture all output sent to `stdout` and `stderr` but will retain only a fixed amount of the most recent output. 88 | Output should be managed in the container to ensure necessary output is removed by AutoTest. 89 | 90 | - Containers should exit with code 0 unless they are unable to produce feedback. AutoTest will post a generic error message if the exit code is non-zero. 91 | -------------------------------------------------------------------------------- /docs/instructor/autogradecreation.md: -------------------------------------------------------------------------------- 1 | # Instructor AutoGrade Creation Manual 2 | 3 | Classy can automatically grade student code by running Docker containers that are designed to produce grade output data. `AutoTest` is an application inside Classy that starts a container each time a student pushes code to a repository. The student code is mounted to a disk volume inside the container, which allows a script to run with instructions on how to mark student code. When the script produces grade output data, the data is taken from the container, which AutoTest then sends to a database. Classy reads from the database to present the compiled grade records to instructors in the front-end application. 4 | 5 | 6 | 7 | To create an AutoGrade Container, follow the AutoGrade Technical Requirements Checklist. Your grading business logic should be implemented on top of the container technical requirements. Click on the headers in the checklist to learn more context about the technical requirements. 8 | 9 | ## AutoGrade Technical Requirements Checklist 10 | 11 | This checklist ensures that you have implemented key technical and business logic requirements that ensure your AutoGrade container is functional after it is built in an AutoTest environment: 12 | 13 | ### Container Input 14 | 15 | - [ ] Your grading logic assumes that the student code is found in the `/assn` path when the container runs. 16 | - [ ] Your grading logic assumes that the code in the `/assn` directory is checked out to the SHA of the last commit before the push. 17 | - [ ] If necessary for your course Business Logic, you implement the following environment variables: 18 | ASSIGNMENT: the deliverable name of the assignment that is running. 19 | EXEC_ID: an always unique execution SHA produced each time a container runs. 20 | 21 | ### Container Output 22 | 23 | - [ ] Your container logic assumes that output data is put in the `/output` path following the user-role sub-directory convention: 24 | ../admin/ 25 | ../staff/ 26 | ../student/ 27 | - [ ] /output/staff sub-directory contains: 28 | report.json grading file at the end of a grading run 29 | additional files that TAs and instructors need access to after the grading run 30 | - [ ] The report.json file is valid JSON that follows this Report Schema: https://github.com/ubccpsc/classy/blob/956e78328c14146e2246b89f1fe0c6e60cb689ed/packages/common/types/ContainerTypes.ts#L69-L106. 31 | - [ ] Your container logic assumes that if code stalls, encounters an infinite loop, or the container times out, Classy will provide this default report.json file: https://github.com/ubccpsc/classy/blob/main/packages/autotest/src/autotest/GradingJob.ts#L28-L40. 32 | - [ ] You container logic assumes that any data that is NOT output to the appropriate `/output` path WILL BE LOST FOREVER after a grading run finishes. 33 | 34 | ### Dockerfile 35 | 36 | - [ ] `FROM` directive is declared with an operating system and/or additional packages installed to run your business logic. 37 | - [ ] `RUN` chmod directive is declared that sets necessary permissions on files copied into your image* 38 | *644  (-rw-r — r — ) owned by root is default when copying files into an image using the COPY directive 39 | - [ ] `COPY` directive is declared to copy any files cloned from your Git repository to your container. 40 | - [ ] `CMD` directive is declared to trigger your AutoGrade grading logic each time the container is started by AutoTest 41 | 42 | Dockerfile GitHub Repository 43 | 44 | - [ ] Dockerfile is named 'Dockerfile' by default or customized. 45 | - [ ] Dockerfile is located in the root path of the filesystem of the Git repository or syntax to specify subdirectories in Classy clone address is understood. 46 | - [ ] Choose one: 47 | Git repository is publicly accessible to be cloned by Classy 48 | Git repository is privately accessible and a GitHub token has been given to technical staff to be added to the Classy environmental configuration file. 49 | - [ ] If sharing a Classy instance with instructors, the other instructors can also use the same GitHub token to configure their AutoGrade containers**. 50 | 51 | ** A Classy instance can only have a single GitHub clone token that clones AutoGrade repositories that contain Dockerfiles. Hence, this token must be shared by instructors of a course that offers more than one section within a single Classy instance. 52 | -------------------------------------------------------------------------------- /docs/instructor/autotest.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | Explains how a student or admin interacts with AutoTest on GitHub. 4 | 5 | ## User Types 6 | 7 | Classy manages administrators using GitHub teams within the GitHub organization that is assigned to the course. 8 | 9 | Two teams have access to the Classy Admin portal: 10 | 11 | - `staff` 12 | - `admin` 13 | 14 | Admin users may configure the course. Staff users are only able to help administer the course by viewing student repositories, AutoGrade container execution logs, and viewing grades. 15 | 16 | A bot user, *AutoBot* unless requested otherwise for a necessary use-case, will be added to the admin team. This gives *AutoBot* access to student repositories to allow for AutoGrade capabilities and giving grade feedback. 17 | 18 | ## Student AutoBot Commands 19 | 20 | AutoTest listens for `push` and `comment` events in repositories managed by AutoTest. AutoTest has the ability to start a container to grade or analyze code based on logic that an instructor has programmed into a Docker container. Currently, AutoTest is tightly integrated with GitHub, although it has been designed such that it could also receive grading requests through other means (e.g., through some form of REST-based invoker). The document below describes the current GitHub-oriented version of AutoTest. 21 | 22 | AutoTest can compute feedback either when a GitHub push event (e.g., a `git push`) is received or when a user makes a comment on a commit (e.g., they use the GitHub web interface to make a comment that references the AutoTest bot). The name of the bot is configurable, but we will use `@autobot` for the remainder of this document. These messages should take the form `@autobot [flags]`. For example `@autobot #d1` or `@autobot #d4`. All flags are sent to the grader image so graders can be customized as needed. Course staff should be sure to check that grader flags are only used by authorized users. 23 | 24 | ## Instructor AutoBot Commands 25 | 26 | Note: all flags are sent to the grader so the grading image can be extensively configured when it is invoked. Built-in flags that are often used by admins include: 27 | 28 | * `#force` Admin-user only. Forces the submission to be re-graded (e.g., purges the cached result if it exists and grades it again). (i.e. Used after updating the grading container to rerun on the same SHA.) 29 | 30 | * `#silent` Admin-user only. This is used to invoke the bot, but suppresses feedback. `#silent` is usually used in conjunction with `#force`. 31 | 32 | ## Avoiding Queue Pile-Ups 33 | 34 | AutoTest queues student assignments before they are marked by a grading container. In CPSC courses, it is normal for a large number of students to be enrolled in a course. If a large number of students are enrolled in a course and the students share the same deadline for an assignment, AutoTest may queue a larger number of assignments near the deadline. AutoTest can only concurrently mark a small number of assignments. The queue, therefore, may experience a pile-up of assignments that are waiting to be graded, as assignments leave the queue at a slower rate at which they enter the queue. 35 | 36 | When a queue pile-up occurs, AutoTest will continue to function. While AutoTest will continue to function,students must wait for their grade results. This wait may inconvenience students if they urgently need grade feedback to continue with the assignment. 37 | 38 | To minimize the potential of a queue pile-up: 39 | 40 | 1. An AutoGrade container should be optimized to perform grading as quickly as possible. 41 | - Any internal container initialization should be done during the container BUILD step when possible. 42 | - If possible, cache any upstream dependencies that require a download and installation. 43 | 2. A minimum grade feedback delay should be set that is appropriate for your deliverable. 44 | 45 | A minimum grade feedback delay is the amount of time that a student must wait between grade requests. Without modifying a container, the minimum delay is the easiest variable to change to minimize the chance of a queue pile-up. *The recommended minimum delay between grade requests is 15 minutes (300 seconds), but it most commonly set at 12 hours (43,200 seconds)*. 46 | 47 | If your class requires a shorter minimum grade feedback delay, a custom minimum can be set in the `.env` file `MINIMUM_STUDENT_DELAY` property, which requires access to the server configuration. Technical staff can assist. 48 | -------------------------------------------------------------------------------- /docs/instructor/gettingstarted.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Overview 4 | 5 | Classy requires some implementation work and learning before it can be used in a course. Here are some first steps to get you started: 6 | 7 | - [ ] Shadow a course that uses Classy before you begin to use it 8 | - [ ] Request [Classy for your Course](#requesting-classy-for-your-course) at least three weeks in advance of course start date. 9 | - [ ] Read about [Default Classy Plugin](#default-classy-plugin) and customization option. 10 | - [ ] Give tech staff a GitHub token to access [AutoGrade Dockerfile Repository](/docs/instructor/autograde.md#autograde-dockerfile-repository). 11 | 12 | Nice-to-have's: 13 | 14 | - [ ] Retain a TA to build-out Docker AutoGrade container. 15 | - [ ] Add container logging that allows for quick debugging of container logic. 16 | 17 | GitHub is integrated with Classy and GitHub requires that you use Git version control. Syllabus and course instructions will need to be updated to work with *Git* version control. 18 | 19 | ## Requesting Classy for Your Course 20 | 21 | You must formally request Classy by notifying *CPSC Technical Staff*. Classy takes approximately two weeks to set up because of software, hardware, and CPSC technical staff resources that are necessary to run the software while upholding privacy and data laws. 22 | 23 | Please start a discussion with CPSC Technical Staff as soon as you develop an interest in using Classy. 24 | 25 | ## Default Classy Plugin 26 | 27 | Classy comes with default MVC, course controller, and route logic. The default plugin is generic Classy logic and should meet the requirements of most courses. The default plugin can be copied to a new folder and completely customized by your course. For more information, visit the [Customization](/docs/developer/customization.md) section of the table of contents. 28 | 29 | ### Admin Students Panel 30 | 31 | *Displays basic student information.* 32 | 33 | 34 | 35 | ### Admin Results Panel 36 | 37 | *Displays basic the result record with the highest scores per single or team repository.* 38 | 39 | 40 | 41 | ### Admin Dashboard Panel 42 | 43 | *Displays the spread of grade, test coverage, and test score percentages per deliverable.* 44 | 45 | 46 | 47 | ### Admin Grades Panel 48 | 49 | *Displays student grades per deliverable with a summary of grade averages.* 50 | 51 | 52 | -------------------------------------------------------------------------------- /docs/instructor/portal.md: -------------------------------------------------------------------------------- 1 | # Portal Manual 2 | 3 | 4 | - [Portal Manual](#portal-manual) 5 | - [Overview](#overview) 6 | - [Classlist Enrollment](#classlist-enrollment) 7 | - [Deliverable Configuration](#deliverable-configuration) 8 | - [Distributing Assignments and Repository Creation](#distributing-assignments-and-repository-creation) 9 | 10 | 11 | ## Overview 12 | 13 | AutoTest configurations allow for the unique customization of course content. The configuration steps below give you basic introduction to steps for a typical course delivery. Click on the UI header of a configuration setting to see more detailed instructions. 14 | 15 | ## Classlist Enrollment 16 | 17 | Classy is integrated with Classlist to access student enrollment information. An instructor must update the classlist enrollment before Classy receives any student enrollment information. 18 | 19 | The **Admin Settings** view contains an **Update Classlist** button that automatically retrieves the current student information from Classlist. Add, update, and delete information will be viewable each time you click on **Update Classlist**. 20 | 21 | If more customizable classlist updates are necessary, a CSV may be uploaded with a custom classlist by the instructor. To upload a custom CSV, you will need to add the `ACCT`, `SNUM`, `CWL`, `LAST`, `FIRST`, and `LAB` headers to the CSV to produce a format accepted by Classy. 22 | 23 | Classlist API update and customizable classlist upload feature 24 | 25 | ## Deliverable Configuration 26 | 27 | A deliverable has many possible configurations that result in unique AutoTest behaviour, but most AutoTest behaviour is impossible without a deliverable. Creating a deliverable is *necessary* before one can: 28 | 29 | - provision repositories to students 30 | - receive AutoTest feedback on `commit` and `push` events 31 | - store grade information in Classy and display it on the Admin Grade Dashboard 32 | 33 | These prior three actions create the core scope of desirable AutoTest functionality. Hence, if you are learning how to use AutoTest and you are not sure where to start, always start by creating a deliverable. The **Admin Configuration Panel** will display the option of creating a deliverable: 34 | 35 | The option to create a new deliverable can be found at the top of the Admin Configuration Panel 36 | 37 | The **Admin Configuration Panel** will display a list of all deliverables. Clicking on the deliverable will open up a list of configuration settings for the deliverable: 38 | 39 | A list of configuration options appears when the deliverable is clicked on 40 | 41 | It is *mandatory* to include a `Deliverable Id` name. The `Deliverable Id` cannot be changed once it is created. All other configuration settings on the **Deliverable Configuration Panel** are *optional*. 42 | 43 | ## Distributing Assignments and Repository Creation 44 | 45 | As AutoTest deals with sensitive student information, all repositories that are provisioned will be private. Only staff and admin roles have access to private repositories. 46 | 47 | Repository Provisioning Pre-Requisites: 48 | 49 | - [Classlist Enrollment](#classlist-enrollment) 50 | - [Deliverable Creation/Configuration](#deliverable-configuration) 51 | 52 | `Provisioning Options` *should* be considered before provisioning repositories. It is likely that an instructor will want to distribute starter code as boilerplate material for a student, as it will give the assignment a structure that helps integrate it with autograding functionality: 53 | 54 | A panel of optional provisioning configurations under the Deliverable Configuration Panel 55 | 56 | Once the pre-requisites have been met and your provisioning options have been configured, an option to **provision** and **release** repositories can be found under **Admin Configuration Panel** by clicking on **Manage Repositories**. 57 | 58 | *Provisioning* repositories will create the GitHub repositories and place starter code in the assignment. Due to GitHub limitations, it takes approximately 5 seconds to distribute a repository. If your class has many students, you must leave enough time to provision the repositories (i.e. ~2 hours for a large class). 59 | 60 | *Releasing* repositories gives a student or team access to read and write to their GitHub repositories. This action is much quicker than repository provisioning and will likely not take longer than 2 minutes for an entire class. 61 | 62 | -------------------------------------------------------------------------------- /docs/tech-staff/architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | 4 | 5 | - [Architecture](#architecture) 6 | - [Overview](#overview) 7 | - [Network Layer](#network-layer) 8 | - [Application Layer](#application-layer) 9 | - [Data Layer](#data-layer) 10 | 11 | 12 | 13 | ## Overview 14 | 15 | Classy consists of three supporting applications: AutoTest, Portal Front-End, and Portal Back-End. Classy uses MongoDB as its data store. The applications are containerized, which means that they run in Docker containers. Each Classy instance is hosted on a single VM where the containerized applications share a single `.env` configuration file. 16 | 17 | Running Classy requires operations support to manage its integrated systems, SSL certificates, ongoing development, hardware requirements, and security updates. Classy has a moderate amount of complexity that requires instructions are accurately followed when bootstrapping integrated systems and customizing its configuration. 18 | 19 | ## Network Layer 20 | 21 | The network layer requires access to the internet to install, build, and run Classy. Docker Compose is a Docker orchestration tool that simplifies the installation and operation of Classy. 22 | 23 | If Docker is properly installed and the environment that is hosting Classy has access to the internet, minimal effort is needed to configure the network layer. 24 | 25 | 26 | 27 | ## Application Layer 28 | 29 | In staging and production environments, the application layer is broken into supporting applications that are Dockerized. The Portal and AutoTest applications are Node JS based applications that are hosted with Nginx routing and a NoSQL MongoDB database. 30 | 31 | 32 | 33 | ## Data Layer 34 | 35 | The data layer consists of the MongoDB database, which is containerized and started with the applications layer. 36 | 37 | MongoDB is mounted to share a volume on the host VM to ensure that data persists after the container is stopped and started. 38 | -------------------------------------------------------------------------------- /docs/tech-staff/backups.md: -------------------------------------------------------------------------------- 1 | # Backup Configuration 2 | 3 | 4 | - [Backup Configuration](#backup-configuration) 5 | - [Database](#database) 6 | - [AutoTest Executions](#autotest-executions) 7 | 8 | 9 | ## Database 10 | 11 | Add a cron job to back up the database daily at 0400. `MONGO_INITDB_ROOT_USERNAME` and `MONGO_INITDB_ROOT_PASSWORD` should 12 | match the values set in the .env file. 13 | 14 | ```bash 15 | echo '0 4 * * * root docker exec db mongodump --username MONGO_INITDB_ROOT_USERNAME --password MONGO_INITDB_ROOT_PASSWORD --gzip --archive > /var/opt/classy/backups/classydb.$(date +\%Y\%m\%dT\%H\%M\%S).gz' | sudo tee /etc/cron.d/backup-classy-db 16 | ``` 17 | 18 | **Restore:** To restore a backup you can use: 19 | ```bash 20 | cat BACKUP_NAME | docker exec -i db mongorestore --gzip --archive 21 | ``` 22 | 23 | Note: you can also use the additional options for [mongodump](https://docs.mongodb.com/manual/reference/program/mongodump/) 24 | and [mongorestore](https://docs.mongodb.com/manual/reference/program/mongorestore/) described in the docs. 25 | 26 | ## AutoTest Executions 27 | 28 | Archive old executions. AutoTest stores the output of each run on disk and, depending on the size of the output, can cause space issues. 29 | You can apply the following cron job (as root) that will archive (and then remove) runs more than a week old. 30 | Adapt as needed: this will run every Wednesday at 0300 and archive runs older than 7 days (based on last modified time); 31 | all runs are stored together in a single compressed tarball called `runs-TIMESTAMP.tar.gz` under `/cs/portal-backup/cs310.ugrad.cs.ubc.ca/classy`. 32 | 33 | ```bash 34 | echo '0 3 * * WED root cd /var/opt/classy/runs && find . ! -path . -type d -mtime +7 -print0 | tar -czvf /cs/portal-backup/cs310.ugrad.cs.ubc.ca/classy/runs-$(date +\%Y\%m\%dT\%H\%M\%S).tar.gz --remove-files --null -T -' | tee /etc/cron.d/archive-classy-runs 35 | ``` 36 | 37 | You can list the contents of the tarball using `tar -tvf FILENAME.tar.gz`. 38 | -------------------------------------------------------------------------------- /docs/tech-staff/envconfig.md: -------------------------------------------------------------------------------- 1 | # Environmental Config 2 | 3 | -------------------------------------------------------------------------------- /docs/tech-staff/hardware.md: -------------------------------------------------------------------------------- 1 | # Operations 2 | 3 | ## Production Environment 4 | 5 | Virtual Machines (VMs) to host Classy in a production environment are typically 100GB storage, 6 CPUs, and 16 GB RAM with a XFS filesystem built with the `dtype = 1` flag. The high disk space is due to the accumulation of assignment execution data. 6 | 7 | ## Staging Environment 8 | 9 | VMs to host Classy in a staging environment are typically 40GB, 3CPUs, 8GB of RAM with a XFS filesystem build with the `dtype = 1` flag. 10 | 11 | ## Customization 12 | 13 | If a course does not require autograding, the filesystem may be much smaller. Approximately 25GB of data should be sufficient to meet any requirements for a course that only wants to use the Classy features to provision repositories and upload grades. 14 | -------------------------------------------------------------------------------- /docs/tech-staff/operatingclassy.md: -------------------------------------------------------------------------------- 1 | # Build/Start/Stop Classy 2 | 3 | [Building Classy](#building-classy) 4 | [Starting Classy](#starting-classy) 5 | [Stopping Classy](#stopping-classy) 6 | 7 | Docker Compose is a Docker orchestration tool that is used to streamline basic operations tasks. Docker Compose commands can build, start, and stop Classy. Logs may also be compiled from multiple sources and viewed. 8 | 9 | ## Building Classy 10 | 11 | ```bash 12 | cd /opt/classy 13 | 14 | # Create a subnet that the grading containers will attach to. This makes it easier to set up firewall rules (above). 15 | docker network create --attachable --ip-range "172.28.5.0/24" --gateway "172.28.5.254" --subnet "172.28.0.0/16" grading_net 16 | 17 | # if plugin in .env file is not 'default', run the command to configure the new plugin 18 | ./helper-scripts/bootstrap-plugin.sh 19 | 20 | # then build Classy with Docker 21 | docker-compose build 22 | ``` 23 | 24 | You can also build just a subset of classy, which can save time; the main modules that need rebuilding are: 25 | 26 | ```bash 27 | docker-compose build portal autotest 28 | ``` 29 | 30 | ## Starting Classy 31 | 32 | Classy is a containerized application that requires containers are built before the application can be run. Building a container is necessary to make a copy of an image that Docker can run. 33 | 34 | If this is a new instance of Classy OR code has been updated, it is necessary to build or re-build Classy. See [Building Classy](#building-classy) for instructions on how to build Classy. 35 | 36 | ```bash 37 | # docker-compose commands must be run from the following directory 38 | cd /opt/classy 39 | ``` 40 | 41 | Start up everything: 42 | 43 | ``` 44 | bash 45 | docker-compose up --detach 46 | ``` 47 | 48 | You should now be able to open portal on the host you've installed classy on (e.g. ). 49 | The system should also be able to receive commit and comment events from GitHub and process them accordingly. 50 | 51 | If you want to start a single service, execute: (where `` is something like 'db') 52 | ```bash 53 | docker-compose up -d 54 | ``` 55 | 56 | If you want to run the db for testing, execute: 57 | ```bash 58 | docker run -p 27017:27017 mongo 59 | ``` 60 | 61 | If you want to run the db for development with persistent data, execute: 62 | ```bash 63 | docker run -p 27017:27017 -v /var/opt/classy/db:/data/db mongo 64 | ``` 65 | 66 | ## Stopping Classy 67 | 68 | To shut down everything: 69 | 70 | ```bash 71 | docker-compose down 72 | ``` 73 | ## Restarting Classy 74 | 75 | To restart Classy (this restarts the _current_ containers and is not sufficient if you are building new images): 76 | 77 | ```bash 78 | docker-compose restart 79 | ``` 80 | 81 | If you're restarting after updating images, use: 82 | 83 | ```bash 84 | docker-compose up -d 85 | ``` 86 | 87 | ## Viewing Classy Runtime Logs 88 | 89 | To view the logs while Classy is running: 90 | 91 | ```bash 92 | docker-compose logs 93 | ``` 94 | 95 | To watch the logs stream as Classy is executing: 96 | 97 | ```bash 98 | docker-compose logs --tail 1000 -f 99 | ``` 100 | 101 | To see only what the most recent logs say: 102 | 103 | ```bash 104 | docker-compose logs --tail 1000 105 | ``` 106 | 107 | ## Reclaiming disk space 108 | 109 | Docker is conservative about disk space and will not delete old containers when new ones are created. If your course is building many grading containers (in particular) these can add up and cause disk pressure on the system. If the host ever run out of disk, stale images can be pruned with: 110 | 111 | ```bash 112 | docker image prune 113 | ``` 114 | 115 | This is an alarming operation, and unfortunately there is no `--dry-run` option, but has been reliably used in the past and only seems to delete the 'excess' containers. 116 | -------------------------------------------------------------------------------- /docs/tech-staff/semestertransitions.md: -------------------------------------------------------------------------------- 1 | # Semester Transitions 2 | -------------------------------------------------------------------------------- /docs/tech-staff/startstop.md: -------------------------------------------------------------------------------- 1 | # Build/Start/Stop Classy 2 | 3 | 4 | - [Build/Start/Stop Classy](#buildstartstop-classy) 5 | - [Building Classy](#building-classy) 6 | - [Starting Classy](#starting-classy) 7 | - [Stopping Classy](#stopping-classy) 8 | 9 | 10 | 11 | Docker Compose is a Docker orchestration tool that is used to streamline basic operations tasks. Docker Compose commands can build, start, and stop Classy. Logs may also be compiled from multiple sources and vierwed. 12 | 13 | ## Building Classy 14 | 15 | ```bash 16 | cd /opt/classy 17 | 18 | # Create a subnet that the grading containers will attach to. This makes it easier to set up firewall rules (above). 19 | docker network create --attachable --ip-range "172.28.5.0/24" --gateway "172.28.5.254" --subnet "172.28.0.0/16" grading_net 20 | 21 | # Copy default front-end and back-end templates to customizable files needed to run Classy: 22 | ./helper-scripts/default-file-setup.sh 23 | # Or if you have yarn: (runs the same script, see package.json) 24 | yarn run pre-build 25 | 26 | docker-compose build 27 | ``` 28 | 29 | ## Starting Classy 30 | 31 | Classy is a containerized application that requires containers are built before the application can be run. Building a container is necessary to make a copy of an image that Docker can run. 32 | 33 | If this is a new instance of Classy OR code has been updated, it is necessary to build or re-build Classy. See [Building Classy](#building-classy) for instructions on how to build Classy. 34 | 35 | ```bash 36 | # docker-compose commands must be run from the following directory 37 | cd /opt/classy 38 | ``` 39 | 40 | Start up everything: 41 | 42 | ``` 43 | bash 44 | docker-compose up --detach 45 | ``` 46 | 47 | You should now be able to open portal on the host you've installed classy on (e.g. ). 48 | The system should also be able to receive commit and comment events from GitHub and process them accordingly. 49 | 50 | If you want to start a single service, execute: (where `` is something like 'db') 51 | ```bash 52 | docker-compose up -d 53 | ``` 54 | 55 | If you want to run the db for testing, execute: 56 | ```bash 57 | docker run -p 27017:27017 mongo 58 | ``` 59 | 60 | If you want to run the db for development with persistent data, execute: 61 | ```bash 62 | docker run -p 27017:27017 -v /var/opt/classy/db:/data/db mongo 63 | ``` 64 | 65 | ## Stopping Classy 66 | 67 | To shut down everything: 68 | 69 | ```bash 70 | docker-compose down 71 | ``` 72 | -------------------------------------------------------------------------------- /docs/tech-staff/termtransitions.md: -------------------------------------------------------------------------------- 1 | # Term Transitions 2 | 3 | Each Classy VM is assigned a hostname that is re-used for a course. As Classy is not designed to run multiple instances on a host, term transitions require an exact end-date. The end-date marks a time that data from the prior term will no longer be modified and can safely be archived on a network storage location without disruption to a course. 4 | 5 | [GitHub re-configuration](#github-term-transition-checklist) and [VM re-configuration](#vm-term-transition-checklist) must occur. 6 | 7 | ## Back-up and Archive Network Locations 8 | 9 | - Database: `/cs/portal-backup/$HOSTNAME/classy/$org/` 10 | - Grading runs: `/cs/portal-backup/$HOSTNAME/classy/$org/runs-$(date +\%Y\%m\%dT\%H\%M\%S)` 11 | 12 | ## Archive Data Database 13 | 14 | There are two types of data to archive: 15 | 16 | 1. MongoDB `gzip` data dump 17 | 2. Container grading run tarball 18 | 19 | MongoDB offers the `mongodump` tool to dump the database and automatically export the database dump in `gzip` format. The database username and password are required for this operation, which can be found in the `.env` file in the `/opt/classy` folder. 20 | 21 | The grading runs of a course consist of student assignments, log information, and results that are stored on the Classy VM filesystem in the path specified in the `HOST_DIR` attribute in the `.env` file. 22 | 23 | Two scripts can perform the database dump and grading run archive operations: 24 | 25 | - `/opt/classy-scripts/archive-classy-runs.sh` 26 | - `/opt/classy-scripts/backup-classy-db.sh` 27 | 28 | A CRON job is configured to automatically back up the database and grading run data. After the end-date of the semester transition, it is no longer necessary to retain the back-ups created by the CRON jobs. Hence, delete all the database and grading run back-ups *prior* to the last back-up and archive of the database. 29 | 30 | ## VM Term Transition Checklist 31 | 32 | - [ ] Staff and faculty have agreed on an end-date when Classy archiving can take place without disrupting course operations 33 | - [ ] Database has been backed-up to **Back-up storage location** 34 | - Script: `/opt/classy-scripts/backup-classy-db.sh` 35 | - [ ] Grading runs have been archived to **Archive storage location** 36 | - Script: `/opt/classy-scripts/archive-classy-runs.sh` 37 | - [ ] Once database has been backed-up, host MongoDB volume mount location has been deleted 38 | - Command: `rm -rf /var/opt/classy/db/*` 39 | - [ ] Once grading runs have been archived, host filesystem run have been deleted 40 | - Command: `rm -rf /var/opt/classy/runs/*` 41 | - [ ] ClassList API **sections** and **term** have been updated in `/opt/classy/.env` file. 42 | - Course sections found here: [Course Schedule](https://courses.students.ubc.ca/cs/courseschedule?pname=subjarea&tname=subj-department&dept=CPSC) 43 | - [ ] A new GitHub organization namespace has been created for the term (ie. **cpsc210-2019w-t1**) 44 | - Instructions: [Add Students and Staff to GitHub Organization](/docs/tech-staff/githubsetup.md#add-students-and-staff-to-github-organization) 45 | - [ ] Only the last two database and grading run back-ups are retained 46 | - All grading run and database back-ups prior to the last back-ups have been deleted 47 | 48 | ## GitHub Term Transition Checklist 49 | 50 | - [ ] If another course is being offered next term, configure a new GitHub organization for the term 51 | - [ ] A new OAuth application has been created under the new GitHub organization and integrated in the `/opt/classy/.env` file 52 | - Instructions: [Setup GitHub OAuth](/docs/tech-staff/githubsetup.md#setup-github-oauth) 53 | - [ ] An instructor may request additional GitHub configuration steps, such as adding TAs as organization owners 54 | -------------------------------------------------------------------------------- /docs/tech-staff/updates.md: -------------------------------------------------------------------------------- 1 | # Patching 2 | 3 | 4 | - [Patching](#patching) 5 | - [Operating System](#operating-system) 6 | - [Classy](#classy) 7 | 8 | 9 | ## Operating System 10 | 11 | Hardware is hosted on VM infrastructure that must be maintained with the latest security releases and updates. Patching the OS is based off of standard OS update procedures. 12 | 13 | If it is necessary to stop and start Classy, then follow the [2.3 Build/Start/Stop Classy](/docs/tech-staff/startstop.md) build, start, and stop steps in the README.md. 14 | 15 | ## Classy 16 | 17 | Patching Classy, or its software dependencies, requires [Stopping](/docs/tech-staff/startstop.md#stopping-classy) and [Starting](/docs/tech-staff/startstop.md#starting-classy) Classy. 18 | 19 | Classy is hosted on GitHub to manage version control. The Classy repository is cloned on a VM during installation. Git is installed on the VM operating system and the `.git` folder is left in the installation directory found in [Installation: Install Classy](/docs/tech-staff/install.md#install-files). 20 | 21 | Classy installations will use the `main` branch of a downstream repository that is assigned to the course, unless otherwise directed by the course instructor. Any upstream project changes from `https://github.com/ubccpsc/classy` will be merged by an instructor or developer. Hence, any updated code that is approved for a course will already be merged and ready to pull into the `main` branch of the downstream Classy project for a course. 22 | 23 | From *within the installation directory*, to pull in changes: 24 | 25 | - type `git status`. 26 | - If any changes are *staged* or *not staged* for a commit, then abort the update and check with the instructor for further instructions on how to proceed with the update. 27 | - It is *normal* to see a list of untracked files in a `custom` folder, which hosts customized front-end and back-end instructor logic. You are safe to proceed with the next step. 28 | - Type `git branch` to ensure that you are on the `main` branch of the project. 29 | - If you are not on the `main` branch, then abort the update and check with the instructor for further instructions on how to proceed with the update. 30 | - If no changes are *staged* or *not staged* for a commit in the `git status` command, then type `git pull`. The `pull` command will bring in the latest changes from the branch. 31 | - Re-build the project by typing `docker-compose build`. This will compile the code and Dockerize the applications. 32 | - If the build successfully completes, then type `docker-compose up -d` to run the application in detached mode. 33 | - Go to the http path of the application (ie. https://classy-dev.students.cs.ubc.ca) to ensure that the application is running as intended. 34 | 35 | In the case that software dependencies must be updated, then follow the [2.3 Starting/Stopping Classy](/docs/tech-staff/startstop.md) build, start, and stop steps in the `README.md` after updating the software. 36 | -------------------------------------------------------------------------------- /helper-scripts/bootstrap-plugin.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Pre-file to `docker-compose build` command when setting a plugin in Classy. 4 | # This file needs to run on Linux boxes without Node dependencies. Node is introduced 5 | # during the Docker build, but not before, in a production environment. 6 | 7 | # Run from root Classy project dir ie. /opt/classy/ 8 | 9 | ./helper-scripts/set-docker-override.sh 10 | 11 | ./helper-scripts/set-nginx-conf.sh 12 | -------------------------------------------------------------------------------- /helper-scripts/data_export_json.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Exports data in JSON array format to the output path you specified. 4 | ## Available tables for export: teams, results, people, course, deliverables, comments 5 | ## 6 | ## Example usage: ./data_export_json.sh output.json results "{ delivId: 'd2' }" 7 | 8 | printf "### Classy JSON Data Exporter v1\n" 9 | 10 | help='' 11 | quiet='' 12 | 13 | while getopts ":hq" opt; do 14 | case ${opt} in 15 | h ) help='true' 16 | ;; 17 | q ) quiet='true' 18 | ;; 19 | esac 20 | done 21 | shift $((OPTIND -1)) 22 | 23 | if [ "$help" == "true" ] 24 | then 25 | printf " 26 | This script exports data from MongoDB to a JSON file. If no arguments are supplied, the default export settings will be used. 27 | 28 | Default Export Settings: Exports the entire 'results' table to the ~/results.json destination file path. 29 | 30 | Example: ./data_export_json.sh 31 | 32 | Custom Export Settings: Will export matches of a query in a table to a specified destination file path. 33 | 34 | Example: ./data_export_json.sh output.json results \"{ delivId: 'd2' }\" 35 | 36 | Flags: 37 | --help or -h Displays the help menu 38 | --quiet or -q Continues without display prompt 39 | 40 | Custom arguments: 41 | \$1 - string: filename to export json to 42 | \$2 - string: table to export 43 | \$3 - string: optional query parameters in string ie. \"{ delivId: 'd2' }\n" 44 | 45 | exit 0 46 | fi 47 | 48 | user=`grep MONGO_INITDB_ROOT_USERNAME /opt/classy/.env | sed -e 's/^MONGO_INITDB_ROOT_USERNAME=//'` 49 | pw=`grep MONGO_INITDB_ROOT_PASSWORD /opt/classy/.env | sed -e 's/^MONGO_INITDB_ROOT_PASSWORD=//'` 50 | database=`grep NAME /opt/classy/.env -m 1 | sed -e 's/^NAME=//'` 51 | 52 | outputPath="$1" 53 | table="$2" 54 | query="$3" 55 | 56 | if [ -z "$1" ] 57 | then 58 | outputPath="results.json" 59 | fi 60 | 61 | if [ -z "$2" ] 62 | then 63 | table='results' 64 | fi 65 | 66 | if [ -z "$3" ] 67 | then 68 | query='' 69 | fi 70 | 71 | printf " 72 | Selected settings: 73 | 74 | Destination file path: $outputPath 75 | MongoDB table set as custom: $table 76 | Query: $query \n\n" 77 | 78 | while [[ true ]] && [[ $quiet != 'true' ]]; 79 | do 80 | read -p "Do you want to continue with data export operation? " yn 81 | case $yn in 82 | [Yy]* ) break;; 83 | [Nn]* ) exit;; 84 | * ) echo "Please answer 'yes' or 'no': ";; 85 | esac 86 | done 87 | 88 | docker exec db mongoexport --username="$user" --password="$pw" --db="$database" --collection="$table" --query="$query" --authenticationDatabase=admin --jsonArray > "$outputPath" 89 | -------------------------------------------------------------------------------- /helper-scripts/set-docker-override.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Pre to `docker-compose build`. Performs plugin configuration. 4 | # If docker-compose.override.yml file found, it will be copied to the root Classy folder to be 5 | # read by docker-compose at build time. 6 | 7 | plugin=`awk -F = '/^PLUGIN[[:space:]]*=/{gsub(/[[:space:]]/, "", $2); print $2}' ./.env` 8 | rootDir=`pwd` 9 | file="./plugins/$plugin/docker/docker-compose.override.yml" 10 | 11 | if [[ -f $file ]]; then 12 | echo "Docker-compose.override.yml file found in $plugin plugin" 13 | echo "Copying Docker override file to Classy root directory: $rootDir" 14 | cp $file ./ 15 | else 16 | echo "No docker-compose.override.yml found in $plugin plugin" 17 | fi 18 | -------------------------------------------------------------------------------- /helper-scripts/set-nginx-conf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Pre to `docker-compose build`. Performs plugin configuration. 4 | # If nginx.rconf file found, it will overwrite default Classy nginx.rconf 5 | 6 | plugin=`awk -F = '/^PLUGIN[[:space:]]*=/{gsub(/[[:space:]]/, "", $2); print $2}' ./.env` 7 | file="./plugins/$plugin/nginx/nginx.rconf" 8 | 9 | if [[ -f $file ]]; then 10 | echo "Nginx.rconf file found in $plugin plugin" 11 | echo "Overriding Classy/packages/proxy/nginx.rconf.default" 12 | cp $file ./packages/proxy/nginx.rconf 13 | echo "Classy/packages/proxy/nginx.rconf file written" 14 | else 15 | echo "$file not found." 16 | fi 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "classy", 4 | "description": "A GitHub service for grading commits and posting feedback.", 5 | "homepage": "https://github.com/ubccpsc/classy", 6 | "author": { 7 | "name": "Reid Holmes", 8 | "url": "https://cs.ubc.ca/~rtholmes" 9 | }, 10 | "contributors": [ 11 | { 12 | "name": "Nick Bradley", 13 | "email": "nick@ncbradley.com", 14 | "url": "https://www.ncbradley.com" 15 | } 16 | ], 17 | "license": "MIT", 18 | "version": "1.4.0", 19 | "engines": { 20 | "node": ">= 18.12 < 24" 21 | }, 22 | "workspaces": [ 23 | "packages/*", 24 | "packages/portal/frontend", 25 | "packages/portal/backend" 26 | ], 27 | "dependencies": { 28 | "dotenv": "4.0.0", 29 | "fs-extra": "5.0.0", 30 | "jszip": "3.8.0", 31 | "mongodb": "^4.17.0", 32 | "node-fetch": "^2.6.7", 33 | "restify": "^10.0.0" 34 | }, 35 | "devDependencies": { 36 | "@types/bson": "^4.2.0", 37 | "@types/dotenv": "4.0.2", 38 | "@types/fs-extra": "5.0.0", 39 | "@types/jszip": "3.1.6", 40 | "@types/mocha": "2.2.44", 41 | "@types/node": "^18.11.18", 42 | "@types/node-fetch": "^2.5.5", 43 | "@types/restify": "8.5.5", 44 | "chai": "^4.3.6", 45 | "jsonschema": "1.2.2", 46 | "mocha": "^9.2.2", 47 | "prettier": "^3.4.2", 48 | "ts-node": "4.1.0", 49 | "tslint": "^5.11.0", 50 | "typescript": "^4.9.4", 51 | "webpack": "^5.94.0" 52 | }, 53 | "resolutions": { 54 | "**/mem": "^4.0.0" 55 | }, 56 | "nyc": { 57 | "exclude": [ 58 | "**/*Spec.ts", 59 | "**/TestHarness.ts", 60 | "**/TestData.ts", 61 | "**/TestGitHubActions.ts" 62 | ] 63 | }, 64 | "scripts": { 65 | "postinstall": "git config core.hooksPath .githooks", 66 | "build": "tsc", 67 | "build:prod": "tsc --outDir bin --sourceMap false", 68 | "prettier:check": "prettier packages/**/*.ts --check", 69 | "prettier:fix": "prettier packages/**/*.ts --write", 70 | "cover": "nyc --reporter text --reporter html --report-dir ./testOutput/coverage yarn run test", 71 | "test": "mocha --require=dotenv/config --require tsconfig-paths/register --timeout 10000 --recursive --exit packages/portal/backend/test packages/autotest/test", 72 | "test:backend": "mocha --require=dotenv/config --require tsconfig-paths/register --timeout 10000 --recursive --exit packages/portal/backend/test", 73 | "test:autotest": "mocha --require=dotenv/config --require tsconfig-paths/register --timeout 10000 --recursive --exit packages/autotest/test", 74 | "XXX_MIGRATE_TO_ESLINT_lint": "tslint --project tsconfig.json" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /packages/autotest/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | RUN apk add --no-cache git 4 | 5 | WORKDIR /app 6 | 7 | # The common package requires the .env file directly so we have to pass it through 8 | COPY .env ./ 9 | COPY yarn.lock ./ 10 | COPY package.json tsconfig.json ./ 11 | COPY packages/common ./packages/common 12 | COPY packages/portal/backend ./packages/portal/backend 13 | COPY packages/autotest ./packages/autotest 14 | 15 | RUN yarn install --pure-lockfile --non-interactive --ignore-scripts \ 16 | && yarn tsc --sourceMap false \ 17 | && chmod -R a+rx /app 18 | 19 | CMD ["node", "--require", "/app/node_modules/tsconfig-paths/register", "/app/packages/autotest/src/AutoTestDaemon.js"] 20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/autotest/README.md: -------------------------------------------------------------------------------- 1 | 2 | # AutoTest 3 | 4 | 5 | ## Dev Instructions 6 | 7 | This assumes you're working with WebStorm. 8 | 9 | ## Testing 10 | 11 | 0) `yarn run install` 12 | 13 | 1) Configure WebStorm for testing (only needs to happen once): 14 | * Create `Mocha` execution profile 15 | * Node options: `-r dotenv/config -r tsconfig-paths/register` 16 | * Mocha package: `/packages/autotest/node_modules/mocha` 17 | * Extra Mocha options: `dotenv_config_path=/.env` 18 | * Test directory: `/packages/autotest/test` 19 | 20 | 2) Make sure `portal-backend` is running on `localhost:5000` 21 | 22 | 3) Run the tests. 23 | -------------------------------------------------------------------------------- /packages/autotest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "autotest", 3 | "description": "A GitHub service for grading commits and posting feedback.", 4 | "homepage": "https://github.ubc.ca/cpsc310/autotest", 5 | "author": { 6 | "name": "Reid Holmes", 7 | "url": "https://cs.ubc.ca/~rtholmes" 8 | }, 9 | "contributors": [ 10 | { 11 | "name": "Nick Bradley", 12 | "email": "nick@ncbradley.com", 13 | "url": "https://www.ncbradley.com" 14 | } 15 | ], 16 | "license": "MIT", 17 | "version": "0.4.0", 18 | "dependencies": { 19 | "dockerode": "^2.5.7", 20 | "dotenv": "4.0.0", 21 | "fs-extra": "5.0.0", 22 | "jszip": "^3.8.0", 23 | "mongodb": "^4.17.0", 24 | "restify": "^10.0.0", 25 | "ts-node": "^7.0.0" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "^7.0.0-0", 29 | "@types/dockerode": "^2.5.9", 30 | "coveralls": "^3.0.2", 31 | "xunit-viewer": "^7.1.5", 32 | "mocha": "^9.0.0", 33 | "mocha-junit-reporter": "^1.17.0", 34 | "nyc": "^15.1.0", 35 | "typescript": "^4.9.4" 36 | }, 37 | "scripts": { 38 | "build": "tsc", 39 | "build:prod": "tsc --outDir bin --sourceMap false", 40 | "lint": "tslint -c tslint.json 'src/**/*.ts' 'test/**/*.ts'", 41 | "test": "mocha --require=dotenv/config --require tsconfig-paths/register --timeout 10000", 42 | "testCI": "TS_NODE_PROJECT=../../tsconfig.json node ./node_modules/.bin/mocha --require tsconfig-paths/register --reporter mocha-junit-reporter --reporter-options mochaFile=../../testOutput/autotest/test/test-results.xml --timeout 10000 --recursive --exit", 43 | "cover": "nyc --reporter text --reporter html ./node_modules/mocha/bin/mocha --require=dotenv/config --require tsconfig-paths/register --timeout 10000 --exit", 44 | "coverCI": "./node_modules/.bin/nyc --reporter html --report-dir ../../testOutput/autotest/coverage --reporter=text-lcov yarn run testCI", 45 | "coveralls": "./node_modules/.bin/nyc report --report-dir ../../testOutput/autotest/coverage --reporter=text-lcov | ./node_modules/coveralls/bin/coveralls.js", 46 | "run:dev": "LOG_LEVEL=TRACE nohup node --require tsconfig-paths/register ./src/AutoTestDaemon.js &> nohup.out &", 47 | "run:prod": "LOG_LEVEL=INFO nohup node --require tsconfig-paths/register ./src/AutoTestDaemon.js &> nohup.out &", 48 | "autotest": "LOG_LEVEL=TRACE node --require tsconfig-paths/register ./src/AutoTestDaemon.js" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/autotest/src/AutoTestDaemon.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by rtholmes on 2016-06-19. 3 | */ 4 | 5 | import Config, { ConfigKey } from "@common/Config"; 6 | import Log from "@common/Log"; 7 | 8 | import AutoTestServer from "@autotest/server/AutoTestServer"; 9 | 10 | /** 11 | * Starts the server; does not listen to whether the start was successful. 12 | */ 13 | export class AutoTestDaemon { 14 | public initServer() { 15 | Log.info("AutoTestDaemon::initServer() - start"); 16 | 17 | const portNum = Number(Config.getInstance().getProp(ConfigKey.autotestPort)); 18 | 19 | // start server 20 | const s = new AutoTestServer(); 21 | s.setPort(portNum); 22 | s.start() 23 | .then(function (val: boolean) { 24 | Log.info("AutoTestDaemon::initServer() - started: " + val); 25 | }) 26 | .catch(function (err: Error) { 27 | Log.error("AutoTestDaemon::initServer() - ERROR: " + err.message); 28 | }); 29 | } 30 | } 31 | 32 | // This starts up the AutoTest system 33 | Log.info("AutoTest Daemon - starting"); 34 | const app = new AutoTestDaemon(); 35 | app.initServer(); 36 | 37 | Log.info("AutoTestDaemon - registering unhandled rejection"); 38 | 39 | /** 40 | * AutoTest instances are run without then/catch blocks since we have 41 | * no way of recovering anyway. This just gives us an opportunity to 42 | * log when a container has failed (which in practice is extremely 43 | * rare). 44 | */ 45 | process.on("unhandledRejection", (reason, p) => { 46 | try { 47 | Log.warn("AutoTestDaemon - unhandled promise rejection"); // in case next line fails 48 | // tslint:disable-next-line 49 | console.log("AutoTestDaemon - unhandled rejection at: ", p, "; reason:", reason); 50 | Log.error("AutoTestDaemon - unhandled promise rejection: " + JSON.stringify(reason)); 51 | } catch (err) { 52 | // eat any error 53 | } 54 | }); 55 | Log.info("AutoTestDaemon - registering unhandled rejection; done"); 56 | -------------------------------------------------------------------------------- /packages/autotest/src/autotest/mocks/MockGradingJob.ts: -------------------------------------------------------------------------------- 1 | import Log from "@common/Log"; 2 | import { AutoTestResult } from "@common/types/AutoTestTypes"; 3 | import { ContainerInput, ContainerOutput, ContainerState, GradeReport } from "@common/types/ContainerTypes"; 4 | import Util from "@common/Util"; 5 | 6 | import { GradingJob } from "../GradingJob"; 7 | 8 | export class MockGradingJob extends GradingJob { 9 | public static readonly JOB_WAIT = 200; 10 | 11 | constructor(input: ContainerInput) { 12 | super(input); 13 | } 14 | 15 | public async prepare(): Promise { 16 | return; 17 | } 18 | 19 | public async run(docker: any): Promise { 20 | try { 21 | Log.info( 22 | "MockGrader::execute() - start; repo: " + 23 | this.input.target.repoId + 24 | "; deliv: " + 25 | this.input.target.delivId + 26 | "; sha: " + 27 | Util.shaHuman(this.input.target.commitSHA) 28 | ); 29 | // const oracleToken = Config.getInstance().getProp(ConfigKey.githubOracleToken); 30 | // const dockerId = Config.getInstance().getProp(ConfigKey.dockerId); 31 | // const workspace = Config.getInstance().getProp(ConfigKey.workspace); 32 | 33 | // TODO: This should really become TestDocker.ts or something that can be instantiated 34 | // let timeout = 1000; 35 | // if (Config.getInstance().getProp(ConfigKey.name) === Config.getInstance().getProp(ConfigKey.testname)) { 36 | // timeout = 200; // do not slow down tests; do not need a lot to get out of order here 37 | // } 38 | 39 | await Util.delay(MockGradingJob.JOB_WAIT); // simulate the container taking longer than the rest of the process 40 | 41 | const gradeReport: GradeReport = { 42 | scoreOverall: 50, 43 | scoreTest: 50, 44 | scoreCover: 50, 45 | passNames: [], 46 | failNames: [], 47 | errorNames: [], 48 | skipNames: [], 49 | custom: {}, 50 | feedback: "Test execution complete.", 51 | result: "SUCCESS", 52 | attachments: [], 53 | }; 54 | 55 | const out: ContainerOutput = { 56 | // commitURL: this.input.pushInfo.commitURL, 57 | timestamp: Date.now(), 58 | report: gradeReport, 59 | // feedback: "Test execution complete.", 60 | postbackOnComplete: false, 61 | custom: {}, 62 | state: ContainerState.SUCCESS, 63 | graderTaskId: "", 64 | }; 65 | 66 | // just a hack to test postback events 67 | if (this.input.target.postbackURL === "POSTBACK") { 68 | Log.info("MockGrader::execute() - overriding for postback"); 69 | out.postbackOnComplete = true; 70 | out.report.feedback = "Build Problem Encountered."; 71 | out.report.result = "FAIL_COMPILE"; 72 | } 73 | 74 | const ret: AutoTestResult = { 75 | delivId: this.input.target.delivId, 76 | repoId: this.input.target.repoId, 77 | commitURL: this.input.target.commitURL, 78 | commitSHA: this.input.target.commitSHA, 79 | input: this.input, 80 | output: out, 81 | }; 82 | 83 | Log.info( 84 | "MockGrader::execute() - execution complete; repo: " + 85 | this.input.target.repoId + 86 | "; deliv: " + 87 | this.input.target.delivId + 88 | "; sha: " + 89 | Util.shaHuman(this.input.target.commitSHA) + 90 | "; feedback: " + 91 | ret.output.report.feedback 92 | ); 93 | return ret; 94 | } catch (err) { 95 | Log.error("MockGrader::execute() - ERROR: " + err); 96 | throw err; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /packages/autotest/src/server/AutoTestServer.ts: -------------------------------------------------------------------------------- 1 | import * as restify from "restify"; 2 | 3 | import Config from "@common/Config"; 4 | import Log from "@common/Log"; 5 | 6 | import AutoTestRouteHandler from "./AutoTestRouteHandler"; 7 | 8 | /** 9 | * This configures the endpoints for the AutoTest REST server. 10 | */ 11 | export default class AutoTestServer { 12 | private rest: restify.Server; 13 | private port: number; 14 | 15 | constructor() { 16 | Config.getInstance(); // for SSL params 17 | } 18 | 19 | /** 20 | * Stops the server. Returns a promise, so we know when the connections have 21 | * actually been fully closed and the port has been released. 22 | * 23 | * @returns {Promise} 24 | */ 25 | public async stop(): Promise { 26 | Log.info("AutoTestServer::close()"); 27 | const that = this; 28 | return new Promise(function (fulfill) { 29 | that.rest.close(function () { 30 | fulfill(true); 31 | }); 32 | }); 33 | } 34 | 35 | /** 36 | * Sets the port on this instance of a server 37 | * @returns {void} 38 | */ 39 | public setPort(portNum: number) { 40 | Log.info("AutoTestServer::setPort()"); 41 | this.port = portNum; 42 | } 43 | 44 | /** 45 | * Starts the server. Returns a promise with a boolean value. Promises are used 46 | * here because starting the server takes some time, and we want to know when it 47 | * is done (and if it worked). 48 | * 49 | * @returns {Promise} 50 | */ 51 | public start(): Promise { 52 | const that = this; 53 | return new Promise(function (fulfill, reject) { 54 | try { 55 | Log.info("AutoTestServer::start() - start"); 56 | 57 | that.rest = restify.createServer({ 58 | name: "AutoTest", 59 | // key: fs.readFileSync(Config.getInstance().getProp(ConfigKey.sslKeyPath)), 60 | // certificate: fs.readFileSync(Config.getInstance().getProp(ConfigKey.sslCertPath)) 61 | }); 62 | 63 | // support CORS 64 | that.rest.use(function crossOrigin(req: any, res: any, next: any) { 65 | res.header("Access-Control-Allow-Origin", "*"); 66 | res.header("Access-Control-Allow-Headers", "X-Requested-With"); 67 | return next(); 68 | }); 69 | 70 | // Return the queue stats (also makes sure the server is running) 71 | that.rest.get("/status", restify.plugins.queryParser(), AutoTestRouteHandler.getAutoTestStatus); 72 | 73 | // GitHub Webhook endpoint 74 | that.rest.post("/githubWebhook", restify.plugins.bodyParser(), AutoTestRouteHandler.postGithubHook); 75 | 76 | // AutoTest image creation / listing / removal endpoints 77 | that.rest.get("/docker/images", restify.plugins.queryParser(), AutoTestRouteHandler.getDockerImages); 78 | that.rest.del("/docker/image/:tag", restify.plugins.queryParser(), AutoTestRouteHandler.removeDockerImage); 79 | that.rest.post("/docker/image", restify.plugins.bodyParser(), AutoTestRouteHandler.postDockerImage); 80 | 81 | // Resource endpoint 82 | // that.rest.get("/resource/.*", restify.plugins.bodyParser(), AutoTestRouteHandler.getResource); 83 | 84 | that.rest.listen(that.port, function () { 85 | Log.info("AutoTestServer::start() - restify listening: " + that.rest.url); 86 | fulfill(true); 87 | }); 88 | 89 | that.rest.on("error", function (err: string) { 90 | // catches errors in restify start; unusual syntax due to internal node not using normal exceptions here 91 | Log.info("AutoTestServer::start() - restify ERROR: " + err); 92 | reject(err); 93 | }); 94 | } catch (err) { 95 | Log.error("AutoTestServer::start() - ERROR: " + err); 96 | reject(err); 97 | } 98 | }); 99 | } 100 | 101 | /** 102 | * Used in tests. 103 | * 104 | * @returns {AutoTestServer} 105 | */ 106 | public getServer(): restify.Server { 107 | Log.trace("AutoTestServer::getServer()"); 108 | return this.rest; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /packages/autotest/test/GitHubServiceSpec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import "mocha"; 3 | 4 | import "@common/GlobalSpec"; 5 | import Config, { ConfigKey } from "@common/Config"; 6 | import Log from "@common/Log"; 7 | 8 | import { GitHubUtil, IGitHubMessage } from "@autotest/github/GitHubUtil"; 9 | 10 | describe("GitHub Markdown Service", () => { 11 | Config.getInstance(); 12 | 13 | // tslint:disable-next-line 14 | const githubAPI = Config.getInstance().getProp(ConfigKey.githubAPI); 15 | const VALID_URL = githubAPI + "/repos/classytest/PostTestDoNotDelete/commits/c35a0e5968338a9757813b58368f36ddd64b063e/comments"; 16 | 17 | const TIMEOUT = 5000; 18 | 19 | // let gh: IGitHubService; 20 | 21 | const postbackVal = Config.getInstance().getProp(ConfigKey.postback); 22 | 23 | before(function () { 24 | // gh = new GitHubService(); 25 | 26 | // set postback to be true so we an actually validate this 27 | const config = Config.getInstance(); 28 | config.setProp(ConfigKey.postback, true); 29 | }); 30 | 31 | after(function () { 32 | // return postback val 33 | const config = Config.getInstance(); 34 | config.setProp(ConfigKey.postback, postbackVal); 35 | }); 36 | 37 | it("Should be able to post a valid message.", async function () { 38 | const post: IGitHubMessage = { 39 | url: VALID_URL, 40 | message: "Automated Test Suite Message", 41 | }; 42 | 43 | Log.test("Trying a valid url"); 44 | const res = await GitHubUtil.postMarkdownToGithub(post); 45 | if (res === true) { 46 | Log.test("Success (expected)"); 47 | expect(res).to.equal(true); 48 | } else { 49 | Log.test("Failure (unexpected)"); 50 | expect.fail(); 51 | } 52 | }).timeout(TIMEOUT * 2); 53 | 54 | it("Should fail when trying to post an invalid message.", async function () { 55 | const post: any = { 56 | url: VALID_URL, 57 | }; 58 | 59 | Log.test("Trying an invalid message"); 60 | 61 | const res = await GitHubUtil.postMarkdownToGithub(post); 62 | if (res === true) { 63 | Log.test("Success (unexpected): " + res); 64 | expect.fail(); 65 | } else { 66 | Log.test("Failure (expected)"); 67 | expect(res).to.equal(false); 68 | } 69 | }).timeout(TIMEOUT); 70 | }); 71 | -------------------------------------------------------------------------------- /packages/autotest/test/GitHubUtilSpec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import "mocha"; 3 | 4 | import Config from "@common/Config"; 5 | import Log from "@common/Log"; 6 | 7 | import Util from "@common/Util"; 8 | import "@common/GlobalSpec"; 9 | 10 | import { GitHubUtil } from "@autotest/github/GitHubUtil"; 11 | 12 | describe("GitHubUtil", () => { 13 | Config.getInstance(); 14 | 15 | before(() => { 16 | Log.test("GitHubUtilSpec::before"); 17 | }); 18 | 19 | after(() => { 20 | Log.test("GitHubUtilSpec::after"); 21 | }); 22 | 23 | it("Should be able to correctly parse deliv ids from a commit comment.", () => { 24 | let actual; 25 | 26 | actual = GitHubUtil.parseDeliverableFromComment("@ubcbot #d1", ["d1", "d2", "project"]); 27 | expect(actual).to.equal("d1"); 28 | 29 | actual = GitHubUtil.parseDeliverableFromComment("@ubcbot d1", ["d1", "d2", "project"]); 30 | expect(actual).to.be.null; 31 | 32 | actual = GitHubUtil.parseDeliverableFromComment("@ubcbot #d101", ["d101", "d2", "project"]); 33 | expect(actual).to.equal("d101"); 34 | 35 | actual = GitHubUtil.parseDeliverableFromComment("@ubcbot #a1", ["d1", "d2", "project"]); 36 | expect(actual).to.be.null; 37 | 38 | actual = GitHubUtil.parseDeliverableFromComment("@ubcbot #a1", ["d1", "d2", "project", "a1"]); 39 | expect(actual).to.equal("a1"); 40 | }); 41 | 42 | it("Should be able to find extra commands from a commit comment.", () => { 43 | let actual; 44 | 45 | actual = GitHubUtil.parseCommandsFromComment("@ubcbot #d1 #verbose"); 46 | expect(actual).to.deep.equal(["#d1", "#verbose"]); 47 | 48 | actual = GitHubUtil.parseCommandsFromComment("@ubcbot d1 verbose ## # ###"); 49 | expect(actual).to.deep.equal([]); 50 | 51 | actual = GitHubUtil.parseCommandsFromComment("@ubcbot #d101 #silent #force #verbose"); 52 | expect(actual).to.deep.equal(["#d101", "#silent", "#force", "#verbose"]); 53 | 54 | actual = GitHubUtil.parseCommandsFromComment("@ubcbot #force. #verbose. #force #silent\n"); 55 | expect(actual).to.deep.equal(["#force", "#verbose", "#silent"]); 56 | 57 | actual = GitHubUtil.parseCommandsFromComment("@ubcbot #forcefoo"); 58 | expect(actual).to.deep.equal(["#forcefoo"]); 59 | }); 60 | 61 | it("Should be able to correctly create human durations.", () => { 62 | const now = Date.now(); 63 | const oneSecond = now - 1000; 64 | const twoSeconds = now - 1000 * 2; 65 | const oneMinute = now - 60 * 1000; 66 | const oneMinuteHalf = now - 90 * 1000; 67 | const halfHour = now - 30 * 60 * 1000; 68 | const halfHourSecond = now - 30 * 60 * 1000 - 1000; 69 | const halfHourSeconds = now - 30 * 60 * 1000 - 2000; 70 | const oneHour = now - 60 * 60 * 1000; 71 | const oneHourHalf = now - 90 * 60 * 1000; 72 | const twoDays = now - 48 * 60 * 60 * 1000; 73 | const sixHundredHours = now - 600 * 60 * 60 * 1000; 74 | 75 | expect(Util.tookHuman(oneSecond)).to.equal("1 second"); 76 | expect(Util.tookHuman(twoSeconds)).to.equal("2 seconds"); 77 | expect(Util.tookHuman(oneMinute)).to.equal("1 minute"); 78 | expect(Util.tookHuman(oneMinuteHalf)).to.equal("1 minute and 30 seconds"); 79 | expect(Util.tookHuman(halfHour)).to.equal("30 minutes"); 80 | expect(Util.tookHuman(halfHourSecond)).to.equal("30 minutes and 1 second"); 81 | expect(Util.tookHuman(halfHourSeconds)).to.equal("30 minutes and 2 seconds"); 82 | expect(Util.tookHuman(oneHour)).to.equal("1 hour"); 83 | expect(Util.tookHuman(oneHourHalf)).to.equal("1 hour and 30 minutes"); 84 | expect(Util.tookHuman(twoDays)).to.equal("48 hours"); 85 | expect(Util.tookHuman(sixHundredHours)).to.equal("600 hours"); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /packages/autotest/test/GradingJobSpec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import * as Docker from "dockerode"; 3 | 4 | import { GradingJob } from "@autotest/autotest/GradingJob"; 5 | 6 | class ContainerMock extends Docker.Container { 7 | public timer: any; 8 | public waitTime: number = 0; 9 | private resolveWait: any; 10 | // private isRunning: boolean = false; 11 | // private isWaiting: boolean = false; 12 | 13 | public start(options?: {}): Promise { 14 | return null; 15 | } 16 | 17 | public stop(options?: {}): Promise { 18 | clearTimeout(this.timer); 19 | this.resolveWait({ StatusCode: 0 }); 20 | return Promise.resolve({ StatusCode: 0 }); 21 | } 22 | 23 | public wait(): Promise { 24 | return new Promise((resolve) => { 25 | if (this.waitTime <= 0) { 26 | resolve({ StatusCode: 0 }); 27 | } else { 28 | this.resolveWait = resolve; 29 | this.timer = setTimeout(() => { 30 | resolve({ StatusCode: 0 }); 31 | }, this.waitTime * 1000); 32 | } 33 | }); 34 | } 35 | } 36 | 37 | describe("GradingJob", function () { 38 | const containerMaxExecTime = 0.1; // seconds 39 | 40 | describe("#runContainer", function () { 41 | let container: ContainerMock; 42 | 43 | beforeEach(function () { 44 | container = new ContainerMock(null, "test-container"); 45 | }); 46 | 47 | it("Should return the exit code for the container.", async function () { 48 | let result: any; 49 | try { 50 | result = await GradingJob.runContainer(container, containerMaxExecTime); 51 | } catch (err) { 52 | result = err; 53 | } finally { 54 | expect(result).to.equal(0); 55 | } 56 | }); 57 | 58 | it("Should return -1 if the container is stopped.", async function () { 59 | container.waitTime = containerMaxExecTime + 0.05; 60 | let result: any; 61 | try { 62 | result = await GradingJob.runContainer(container, containerMaxExecTime); 63 | } catch (err) { 64 | result = err; 65 | } finally { 66 | expect(result).to.equal(-1); 67 | } 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /packages/autotest/test/QueueSpec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import "mocha"; 3 | 4 | import Log from "@common/Log"; 5 | import "@common/GlobalSpec"; 6 | 7 | import { Queue } from "@autotest/autotest/Queue"; 8 | 9 | import { TestData } from "./TestData"; 10 | import Util from "@common/Util"; 11 | import { ContainerInput } from "@common/types/ContainerTypes"; 12 | 13 | describe("Queue", () => { 14 | let q: Queue; 15 | 16 | before(function () { 17 | Log.test("QueueSpec::before"); 18 | }); 19 | 20 | beforeEach(function () { 21 | Log.test("QueueSpec::beforeEach"); 22 | q = new Queue("test"); 23 | }); 24 | 25 | after(function () { 26 | Log.test("QueueSpec::after"); 27 | q = new Queue("test"); 28 | }); 29 | 30 | it("Should work when empty.", () => { 31 | let res: any = q.indexOf(TestData.inputRecordA); 32 | expect(res).to.equal(-1); 33 | res = q.pop(); 34 | expect(res).to.be.null; 35 | expect(q.length()).to.equal(0); 36 | 37 | res = q.remove(TestData.inputRecordB); 38 | expect(res).to.equal(null); 39 | res = q.indexOf(TestData.inputRecordB); 40 | expect(res).to.equal(-1); 41 | expect(q.length()).to.equal(0); 42 | }); 43 | 44 | it("Should be able to push and pop jobs.", () => { 45 | expect(q.length()).to.equal(0); 46 | let res: any = q.push(TestData.inputRecordA); 47 | expect(res).to.equal(1); 48 | expect(q.length()).to.equal(1); 49 | 50 | expect(q.pop()).to.not.be.null; 51 | expect(q.length()).to.equal(0); 52 | 53 | res = q.push(TestData.inputRecordA); 54 | expect(res).to.equal(1); 55 | 56 | res = q.indexOf(TestData.inputRecordA); 57 | expect(res).to.equal(0); 58 | 59 | res = q.remove(TestData.inputRecordA); 60 | expect(res).to.not.be.null; 61 | expect(q.length()).to.equal(0); 62 | }); 63 | 64 | it("Should be able to get the number of jobs for a person.", () => { 65 | const r1 = Util.clone(TestData.inputRecordA) as ContainerInput; 66 | const r2 = Util.clone(TestData.inputRecordB) as ContainerInput; 67 | r1.target.personId = "person1"; 68 | r2.target.personId = "person1"; 69 | 70 | expect(q.numberJobsForPerson(r1)).to.equal(0); // nothing added, should return 0 71 | 72 | q.push(r1); 73 | expect(q.length()).to.equal(1); 74 | expect(q.numberJobsForPerson(r1)).to.equal(1); 75 | 76 | q.push(r2); 77 | expect(q.length()).to.equal(2); // should have 2 jobs on queue 78 | expect(q.numberJobsForPerson(r2)).to.equal(2); 79 | }); 80 | 81 | it("Should be able to replace the oldest job for a person.", () => { 82 | const r1 = Util.clone(TestData.inputRecordA) as ContainerInput; 83 | const r2 = Util.clone(TestData.inputRecordB) as ContainerInput; 84 | r1.target.personId = "person1"; 85 | r1.target.botMentioned = false; 86 | r2.target.personId = "person1"; 87 | r2.target.botMentioned = false; 88 | 89 | const res: number = q.push(r1); 90 | expect(res).to.equal(1); 91 | expect(q.length()).to.equal(1); 92 | 93 | q.replaceOldestForPerson(r2, false); 94 | expect(q.length()).to.equal(1); // should still be 1 95 | }); 96 | 97 | it("Should not replace the oldest job for a person if the oldest job mentions the bot.", () => { 98 | const r1 = Util.clone(TestData.inputRecordA) as ContainerInput; 99 | const r2 = Util.clone(TestData.inputRecordB) as ContainerInput; 100 | r1.target.personId = "person1"; 101 | r1.target.botMentioned = true; 102 | r2.target.personId = "person1"; 103 | r2.target.botMentioned = true; 104 | 105 | q.push(r1); 106 | expect(q.length()).to.equal(1); 107 | expect(q.numberJobsForPerson(r1)).to.equal(1); 108 | 109 | const job = q.replaceOldestForPerson(r2, true); 110 | expect(job).to.be.null; 111 | expect(q.length()).to.equal(2); 112 | expect(q.numberJobsForPerson(r1)).to.equal(2); // enqued rather than replaced 113 | }); 114 | 115 | it("Should enqueue a job for a person when they do not have one yet.", () => { 116 | const r1 = Util.clone(TestData.inputRecordA) as ContainerInput; 117 | const r2 = Util.clone(TestData.inputRecordB) as ContainerInput; 118 | r1.target.personId = "person1"; 119 | r2.target.personId = "person2"; // new job is for a different person 120 | 121 | const res: number = q.push(r1); 122 | expect(res).to.equal(1); 123 | expect(q.length()).to.equal(1); 124 | 125 | q.replaceOldestForPerson(r2, true); 126 | expect(q.length()).to.equal(2); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /packages/autotest/test/githubAutoTestData/commentRecords.json: -------------------------------------------------------------------------------- 1 | [{"botMentioned":true,"commitSHA":"abe1b0918b872997de4c4d2baf4c263f8d4c6dc2","commitURL":"https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/abe1b0918b872997de4c4d2baf4c263f8d4c6dc2","personId":"cs310test","org":"310","delivId":"d1","postbackURL":"EMPTY","timestamp":1516472873288},{"botMentioned":true,"commitSHA":"abe1b0918b872997de4c4d2baf4c263f8d4c6dc2","commitURL":"https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/abe1b0918b872997de4c4d2baf4c263f8d4c6dc2","personId":"cs310test","org":"310","delivId":"d1","postbackURL":"EMPTY","timestamp":1516472873288}] 2 | -------------------------------------------------------------------------------- /packages/autotest/test/githubAutoTestData/feedbackRecords.json: -------------------------------------------------------------------------------- 1 | [{"commitURL":"https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/abe1b0918b872997de4c4d2baf4c263f8d4c6dc2","org":"310","delivId":"d1","timestamp":1516472873288,"personId":"cs310test"},{"commitURL":"https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/abe1b0918b872997de4c4d2baf4c263f8d4c6dc2","org":"310","delivId":"d1","timestamp":1516472873288,"personId":"cs310test"}] 2 | -------------------------------------------------------------------------------- /packages/autotest/test/githubAutoTestData/outputRecords.json: -------------------------------------------------------------------------------- 1 | [{"commitURL":"https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/abe1b0918b872997de4c4d2baf4c263f8d4c6dc2","commitSHA":"abe1b0918b872997de4c4d2baf4c263f8d4c6dc2","input":{"org":"310","delivId":"d1","pushInfo":{"branch":"master","cloneURL":"","commitSHA":"abe1b0918b872997de4c4d2baf4c263f8d4c6dc2","commitURL":"https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/abe1b0918b872997de4c4d2baf4c263f8d4c6dc2","projectURL":"https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/","org":"CPSC310-2017W-T2","postbackURL":"EMPTY","repoId":"d0_team999","timestamp":1516472872288}},"output":{"commitURL":"https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/abe1b0918b872997de4c4d2baf4c263f8d4c6dc2","timestamp":1525211906080,"report":{"scoreOverall":50,"scoreTest":50,"scoreCover":50,"passNames":[],"failNames":[],"errorNames":[],"skipNames":[],"custom":[],"feedback":""},"feedback":"Test execution complete.","postbackOnComplete":false,"custom":{},"attachments":[],"state":"SUCCESS"}}] 2 | -------------------------------------------------------------------------------- /packages/autotest/test/githubAutoTestData/pushRecords.json: -------------------------------------------------------------------------------- 1 | [{"org":"310","delivId":"d1","pushInfo":{"branch":"master","cloneURL":"","commitSHA":"abe1b0918b872997de4c4d2baf4c263f8d4c6dc2","commitURL":"https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/abe1b0918b872997de4c4d2baf4c263f8d4c6dc2","projectURL":"https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/","org":"CPSC310-2017W-T2","postbackURL":"EMPTY","repoId":"d0_team999","timestamp":1516472872288}}] 2 | -------------------------------------------------------------------------------- /packages/autotest/test/githubAutoTestData/pushes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "branch": "master", 4 | "commitSHA": "abe1b0918b872997de4c4d2baf4c263f8d4c6dc2", 5 | "commitURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/abe1b0918b872997de4c4d2baf4c263f8d4c6dc2", 6 | "projectURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/", 7 | "postbackURL": "EMPTY", 8 | "repoId": "d0_team999", 9 | "personId": "user1", 10 | "timestamp": 1234567890 11 | }, 12 | { 13 | "branch": "master", 14 | "commitSHA": "bbe1b0918b872997de4c4d2baf4c263f8d4c6dc2", 15 | "commitURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/bbe1b0918b872997de4c4d2baf4c263f8d4c6dc2", 16 | "projectURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/", 17 | "postbackURL": "EMPTY", 18 | "repoId": "d0_team999", 19 | "personId": "user1", 20 | "timestamp": 2234567890 21 | }, 22 | { 23 | "branch": "master", 24 | "commitSHA": "cbe1b0918b872997de4c4d2baf4c263f8d4c6dc2", 25 | "commitURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/cbe1b0918b872997de4c4d2baf4c263f8d4c6dc2", 26 | "projectURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/", 27 | "postbackURL": "EMPTY", 28 | "repoId": "d0_team999", 29 | "personId": "user1", 30 | "timestamp": 3234567890 31 | }, 32 | { 33 | "branch": "master", 34 | "commitSHA": "dbe1b0918b872997de4c4d2baf4c263f8d4c6dc2", 35 | "commitURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/dbe1b0918b872997de4c4d2baf4c263f8d4c6dc2", 36 | "projectURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/", 37 | "postbackURL": "EMPTY", 38 | "repoId": "d0_team999", 39 | "personId": "user1", 40 | "timestamp": 4234567890 41 | }, 42 | { 43 | "branch": "master", 44 | "commitSHA": "ebe1b0918b872997de4c4d2baf4c263f8d4c6dc2", 45 | "commitURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/ebe1b0918b872997de4c4d2baf4c263f8d4c6dc2", 46 | "projectURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/", 47 | "postbackURL": "EMPTY", 48 | "repoId": "d0_team999", 49 | "personId": "user1", 50 | "timestamp": 5234567890 51 | }, 52 | { 53 | "branch": "master", 54 | "commitSHA": "fbe1b0918b872997de4c4d2baf4c263f8d4c6dc2", 55 | "commitURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/fbe1b0918b872997de4c4d2baf4c263f8d4c6dc2", 56 | "projectURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/", 57 | "postbackURL": "EMPTY", 58 | "repoId": "d0_team999", 59 | "personId": "user1", 60 | "timestamp": 6234567890 61 | }, 62 | { 63 | "branch": "master", 64 | "commitSHA": "gbe1b0918b872997de4c4d2baf4c263f8d4c6dc2", 65 | "commitURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/gbe1b0918b872997de4c4d2baf4c263f8d4c6dc2", 66 | "projectURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/", 67 | "postbackURL": "EMPTY", 68 | "repoId": "d0_team999", 69 | "personId": "user1", 70 | "timestamp": 7234567890 71 | }, 72 | { 73 | "branch": "master", 74 | "commitSHA": "hbe1b0918b872997de4c4d2baf4c263f8d4c6dc2", 75 | "commitURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/hbe1b0918b872997de4c4d2baf4c263f8d4c6dc2", 76 | "projectURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/", 77 | "postbackURL": "EMPTY", 78 | "repoId": "d0_team999", 79 | "personId": "user1", 80 | "timestamp": 8234567890 81 | }, 82 | { 83 | "branch": "master", 84 | "commitSHA": "ibe1b0918b872997de4c4d2baf4c263f8d4c6dc2", 85 | "commitURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/ibe1b0918b872997de4c4d2baf4c263f8d4c6dc2", 86 | "projectURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/", 87 | "postbackURL": "EMPTY", 88 | "repoId": "d0_team999", 89 | "personId": "user1", 90 | "timestamp": 9234567890 91 | } 92 | ] 93 | -------------------------------------------------------------------------------- /packages/autotest/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES6", 5 | "noImplicitAny": true, 6 | "removeComments": false, 7 | "preserveConstEnums": true, 8 | "sourceMap": true, 9 | "baseUrl": ".", 10 | "paths": { 11 | "@common/*": [ 12 | "../common/src/*", 13 | "../common/test/*" 14 | ], 15 | "@backend/*": [ 16 | "../portal/backend/src/*" 17 | ], 18 | "@autotest/*": [ 19 | "./src/*" 20 | ] 21 | }, 22 | "lib": [ 23 | "es7", 24 | "es2017.object", 25 | "dom" 26 | ], 27 | "typeRoots": [ 28 | "../../node_modules/@types" 29 | ] 30 | }, 31 | "include": [ 32 | "src/**/*.ts", 33 | "test/**/*.ts" 34 | ], 35 | "exclude": [ 36 | "node_modules" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /packages/common/src/Log.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-console */ 2 | 3 | export enum LogLevel { 4 | TRACE, 5 | INFO, 6 | WARN, 7 | ERROR, 8 | TEST, 9 | NONE, 10 | } 11 | 12 | let LOG_LEVEL: LogLevel; 13 | 14 | /** 15 | * Collection of logging methods. Useful for making the output easier to read and understand. 16 | */ 17 | export default class Log { 18 | public static Level: LogLevel = Log.parseLogLevel(); 19 | 20 | public static parseLogLevel(): LogLevel { 21 | try { 22 | // console.log("Log::parseLogLevel() - start; currently: " + Log.Level); 23 | let valToSwitch; 24 | if (typeof Log.Level === "undefined") { 25 | // if undefined, use .env; otherwise re-parse value 26 | valToSwitch = (process.env["LOG_LEVEL"] || "").toUpperCase(); 27 | } else { 28 | valToSwitch = Log.Level; 29 | } 30 | 31 | if (typeof valToSwitch !== "string") { 32 | LOG_LEVEL = Log.Level; 33 | // console.log("Log::parseLogLevel() - unchanged; current level: " + LOG_LEVEL); 34 | return LOG_LEVEL; 35 | } else { 36 | // if the value is not a string, it must be a LogLevel already 37 | // so we do not need to parse it again 38 | switch (valToSwitch) { 39 | case "TRACE": 40 | LOG_LEVEL = LogLevel.TRACE; 41 | break; 42 | case "INFO": 43 | LOG_LEVEL = LogLevel.INFO; 44 | break; 45 | case "WARN": 46 | LOG_LEVEL = LogLevel.WARN; 47 | break; 48 | case "ERROR": 49 | LOG_LEVEL = LogLevel.ERROR; 50 | break; 51 | case "TEST": 52 | LOG_LEVEL = LogLevel.TEST; 53 | break; 54 | case "NONE": 55 | LOG_LEVEL = LogLevel.NONE; 56 | break; 57 | default: 58 | LOG_LEVEL = LogLevel.TRACE; 59 | } 60 | // console.log("Log::parseLogLevel() - log level: " + LOG_LEVEL); 61 | Log.Level = LOG_LEVEL; 62 | return LOG_LEVEL; 63 | } 64 | } catch (err) { 65 | console.log(" Log::parseLogLevel() - ERROR; setting to TRACE"); 66 | Log.Level = LogLevel.TRACE; 67 | LOG_LEVEL = LogLevel.TRACE; 68 | return LOG_LEVEL; 69 | } 70 | } 71 | 72 | public static trace(...msg: any[]): void { 73 | if (Log.Level <= LogLevel.TRACE) { 74 | console.log(` ${Log.getCurrentTS()}`, ...msg); 75 | } 76 | } 77 | 78 | public static cmd(msg: string): void { 79 | if (Log.Level <= LogLevel.INFO) { 80 | console.info(`\`\`\`\n${msg}\n\`\`\``); 81 | } 82 | } 83 | 84 | public static info(...msg: any[]): void { 85 | if (Log.Level <= LogLevel.INFO) { 86 | console.info(` ${Log.getCurrentTS()}`, ...msg); 87 | } 88 | } 89 | 90 | public static warn(...msg: any[]): void { 91 | if (Log.Level <= LogLevel.WARN) { 92 | console.warn(` ${Log.getCurrentTS()}`, ...msg); 93 | } 94 | } 95 | 96 | public static error(...msg: any[]): void { 97 | if (Log.Level <= LogLevel.ERROR) { 98 | console.error(` ${Log.getCurrentTS()}`, ...msg); 99 | } 100 | } 101 | 102 | public static exception(...err: any[]): void { 103 | console.error(` ${Log.getCurrentTS()}`, ...err); 104 | } 105 | 106 | public static test(...msg: any[]): void { 107 | if (Log.Level <= LogLevel.TEST) { 108 | console.log(` ${Log.getCurrentTS()}`, ...msg); 109 | } 110 | } 111 | 112 | protected static getCurrentTS(): string { 113 | // 11/31/2024, 11:27:00 AM 114 | let dateStr = new Date().toLocaleString() + ":"; 115 | dateStr = dateStr.padEnd(23, " "); 116 | return dateStr; 117 | } 118 | } 119 | 120 | // enable log level changes to dynamically update 121 | Log.parseLogLevel(); 122 | -------------------------------------------------------------------------------- /packages/common/src/commands/Command.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess, spawn, SpawnOptions } from "child_process"; 2 | import Config from "../Config"; 3 | import Log from "../Log"; 4 | 5 | export type CommandResult = [number, any]; 6 | 7 | export interface ICommand { 8 | executeCommand(args: string[], options?: SpawnOptions): Promise; 9 | } 10 | 11 | export class Command implements ICommand { 12 | // Assign spawn to a member variable so we can substitute a mock when testing 13 | private readonly spawn: (command: string, args: string[], options: SpawnOptions) => ChildProcess; 14 | private readonly cmdName: string; 15 | 16 | constructor(name: string) { 17 | this.cmdName = name; 18 | this.spawn = spawn; 19 | } 20 | 21 | public async executeCommand(args: string[], options: SpawnOptions = {}): Promise { 22 | Log.trace(Config.sanitize(`Command::executeCommand(..) -> ${this.cmdName} ${args.join(" ")}`)); 23 | return new Promise((resolve, reject) => { 24 | let output: Buffer = Buffer.allocUnsafe(0); 25 | const cmd: ChildProcess = this.spawn(this.cmdName, args, options); 26 | cmd.on(`error`, (err) => { 27 | reject(err); 28 | }); 29 | cmd.stdout.on(`data`, (data: Buffer) => { 30 | output = Buffer.concat([output, data], output.length + data.length); 31 | }); 32 | cmd.stderr.on(`data`, (data: Buffer) => { 33 | output = Buffer.concat([output, data], output.length + data.length); 34 | }); 35 | cmd.on(`close`, (code, _signal) => { 36 | const out = output.toString().trim(); 37 | if (code === 0) { 38 | resolve([code, out]); 39 | } else { 40 | Log.warn(Config.sanitize(`Command::executeCommand(..) -> EXIT ${code}: ${this.cmdName} ${args.join(" ")}. ${out}`)); 41 | reject([code, Config.sanitize(out)]); 42 | } 43 | }); 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/common/src/commands/GitRepository.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { CommandResult } from "./Command"; 3 | import { Command } from "./Command"; 4 | 5 | interface Repository { 6 | /** 7 | * Wrapper for git checkout. 8 | * @param commit The commit SHA to check out. 9 | * @throws When the commit cannot be checked-out. 10 | */ 11 | checkout(commit: string): Promise; 12 | 13 | /** 14 | * Wrapper for git clone. Credentials with read permission on the repository should be specified in the url. 15 | * @param url The location, including credentials, of the repository. 16 | * @param dir The path on the local machine to clone the files. 17 | * @throws When the repository cannot be cloned. 18 | */ 19 | clone(url: string, dir: string): Promise; 20 | 21 | /** 22 | * Wrapper for git rev-parse HEAD. 23 | * @returns the full SHA for the most recent commit. 24 | */ 25 | getSha(): Promise; 26 | } 27 | 28 | /** 29 | * Wrapper for some git commands. 30 | */ 31 | export class GitRepository extends Command implements Repository { 32 | private readonly path: string; 33 | 34 | constructor(dir: string) { 35 | super("git"); 36 | this.path = path.resolve(dir); 37 | } 38 | 39 | public async checkout(commit: string): Promise { 40 | const args: string[] = ["checkout", commit]; 41 | return await this.executeCommand(args, { cwd: this.path }); 42 | } 43 | 44 | public async clone(url: string): Promise { 45 | const args: string[] = ["clone", url, this.path]; 46 | return await this.executeCommand(args, { env: { GIT_TERMINAL_PROMPT: "0" } }); 47 | } 48 | 49 | public async getSha(): Promise { 50 | const args: string[] = ["rev-parse", "HEAD"]; 51 | const commandResult = await this.executeCommand(args, { cwd: this.path }); 52 | return commandResult[1]; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/common/src/types/AutoTestTypes.ts: -------------------------------------------------------------------------------- 1 | import { ContainerInput, ContainerOutput } from "./ContainerTypes"; 2 | 3 | export interface IFeedbackGiven { 4 | personId: string; 5 | delivId: string; 6 | timestamp: number; 7 | commitURL: string; // for information only 8 | kind: string; // "standard" | "check" 9 | } 10 | 11 | /** 12 | * This is the result of an AutoTest run. It is constructed by the Grader 13 | * and sent back to Classy for querying on the backend as needed. 14 | * 15 | * There is some duplication in the record to enable easier querying. 16 | * 17 | */ 18 | export interface AutoTestResult { 19 | /** 20 | * Foreign key into Deliverables. 21 | * This lets us know what deliverable this run is scoring. 22 | * 23 | * (intentional duplication with input.delivId) 24 | */ 25 | delivId: string; 26 | 27 | /** 28 | * Foreign key into Repositories. 29 | * This helps us know what repository (and people) this run is for. 30 | * 31 | * (intentional duplication with input.pushInfo.repoId) 32 | */ 33 | repoId: string; 34 | 35 | commitURL: string; 36 | commitSHA: string; // can be used to index into the AutoTest collections (pushes, comments, & feedback) 37 | 38 | input: ContainerInput; // Prepared by AutoTest service 39 | output: ContainerOutput; // Returned by the Grader service 40 | } 41 | -------------------------------------------------------------------------------- /packages/common/src/types/CS340Types.ts: -------------------------------------------------------------------------------- 1 | // /** 2 | // * DEPRECATED 3 | // * 4 | // * 340 is not using Classy anymore, these can probably be removed. 5 | // */ 6 | // 7 | // /** 8 | // * Custom type definitions, to be placed inside the custom field 9 | // */ 10 | // 11 | // // Placed in Grade.custom 12 | // // Represents an Assignment grade, comprised of an arbitrary amount of Questions 13 | // // TODO [WISHLIST]: Remove AssignmentGrade from having studentID and assignmentID (redundant, grade already tracks this) 14 | // 15 | // export interface AssignmentGrade { 16 | // assignmentID: string; // Unique Assignment ID per course 17 | // studentID: string; // Unique Student ID per course 18 | // released: boolean; // status if assignment grade has been released or not 19 | // questions: QuestionGrade[]; // SubQuestions 20 | // } 21 | // 22 | // // Represents the grade for a question, comprised of an arbitrary amount of subQuestions 23 | // export interface QuestionGrade { 24 | // questionName: string; 25 | // commentName: string; 26 | // subQuestion: SubQuestionGrade[]; 27 | // } 28 | // 29 | // // Represents the grade for the subQuestion 30 | // export interface SubQuestionGrade { 31 | // sectionName: string; 32 | // grade: number; 33 | // graded: boolean; 34 | // feedback: string; 35 | // } 36 | // 37 | // // Placed in Deliverable.custom 38 | // export interface AssignmentInfo { 39 | // seedRepoURL: string; 40 | // seedRepoPath: string; 41 | // mainFilePath: string; 42 | // courseWeight: number; // should be a value between 0-1, relative to the final grade 43 | // status: AssignmentStatus; 44 | // rubric: AssignmentGradingRubric; 45 | // repositories: string[]; // Associated Repositories based on IDs 46 | // } 47 | // 48 | // // Placed in Repository.custom 49 | // export interface AssignmentRepositoryInfo { 50 | // assignmentId: string[]; 51 | // status: AssignmentStatus; 52 | // assignedTeams: string[]; // team.id[] 53 | // } 54 | // 55 | // // Represents a grading rubric 56 | // export interface AssignmentGradingRubric { 57 | // name: string; 58 | // comment: string; // placeholders for future use 59 | // questions: QuestionGradingRubric[]; 60 | // } 61 | // 62 | // // Represents a question rubric 63 | // export interface QuestionGradingRubric { 64 | // name: string; 65 | // comment: string; // placeholders for future use 66 | // subQuestions: SubQuestionGradingRubric[]; 67 | // } 68 | // 69 | // export interface SubQuestionGradingRubric { 70 | // name: string; 71 | // comment: string; 72 | // outOf: number; 73 | // weight: number; // score multiplier for the total grade 74 | // modifiers: any; // Custom modifiers - course dependant 75 | // } 76 | // 77 | // export enum AssignmentStatus { 78 | // // Repositories Status: 79 | // // Created | Pull | Push | 80 | // INACTIVE, // = 1, // | | | Repositories not created or viewable 81 | // CREATED, // = 2, // X | | | Repositories are created, not viewable 82 | // RELEASED, // = 3, // X | X | X | Created and viewable, with push access 83 | // CLOSED, // = 4, // X | X | | Created, viewable, no push access 84 | // } 85 | // 86 | // // Contains information about deliverables 87 | // export interface DeliverableInfo { 88 | // id: string; 89 | // minStudents: number; 90 | // maxStudents: number; 91 | // } 92 | // 93 | // /* 94 | // export interface CategoricalGradeRecord extends CategoricalRecord { 95 | // grade: number; 96 | // } 97 | // 98 | // export interface CategoricalRecord { 99 | // name: string; 100 | // comment: string; 101 | // } 102 | // */ 103 | -------------------------------------------------------------------------------- /packages/common/src/types/SDMMTypes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * NOTE: these were part of SDDM frontend and should be made to be more generic (e.g.m, StatusPayload) for other courses. 3 | */ 4 | import { FailurePayload } from "./PortalTypes"; 5 | 6 | export interface Payload { 7 | success?: ActionPayload | StatusPayload; // only set if defined 8 | failure?: FailurePayload; // only set if defined 9 | } 10 | 11 | export interface ActionPayload { 12 | message: string; 13 | status: StatusPayload; // if an action was successful we should send the current status 14 | } 15 | 16 | export interface StatusPayload { 17 | status: string; 18 | d0: GradePayload | null; 19 | d1: GradePayload | null; 20 | d2: GradePayload | null; 21 | d3: GradePayload | null; 22 | } 23 | 24 | /** 25 | * TODO: This type seems fundamentally broken and should be revisited. 26 | * We just need to make sure that any properties we add to it are things autoTest knows. 27 | */ 28 | export interface GradePayload { 29 | // delivId: string; // invariant; foreign key on Deliverable.id 30 | // personId: string; // TODO: who do we know who the grade is for? 31 | score: number; // grade: < 0 will mean "N/A" in the UI 32 | comment: string; 33 | 34 | urlName: string; // name associated with url (e.g., project name) 35 | URL: string; // commit URL if known, otherwise repo URL 36 | 37 | timestamp: number; // even if grade < 0 might as well return when the entry was made 38 | custom: any; 39 | } 40 | 41 | export enum SDMMStatus { 42 | D0PRE, 43 | D0, 44 | D1UNLOCKED, 45 | D1TEAMSET, 46 | D1, 47 | D2, 48 | D3PRE, 49 | D3, 50 | } 51 | -------------------------------------------------------------------------------- /packages/common/test/GlobalSpec.ts: -------------------------------------------------------------------------------- 1 | import "mocha"; 2 | 3 | import Config, { ConfigKey } from "@common/Config"; 4 | import Log from "@common/Log"; 5 | 6 | import { TestHarness } from "./TestHarness"; 7 | 8 | before(async () => { 9 | Log.info("GlobalSpec::before() - resetting Config.name and Config.org for test suite."); 10 | Config.getInstance().setProp(ConfigKey.name, Config.getInstance().getProp(ConfigKey.testname)); 11 | Config.getInstance().setProp(ConfigKey.org, Config.getInstance().getProp(ConfigKey.testorg)); 12 | }); 13 | 14 | after(() => { 15 | Log.info("GlobalSpec::after() - done."); 16 | // process.exit(); 17 | }); 18 | 19 | beforeEach(function () { 20 | TestHarness.testBefore(this); 21 | }); 22 | 23 | afterEach(function () { 24 | TestHarness.testAfter(this); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES6", 5 | "noImplicitAny": true, 6 | "removeComments": true, 7 | "preserveConstEnums": true, 8 | "sourceMap": true, 9 | "lib": [ 10 | "es7", 11 | "es2017.object", 12 | "dom" 13 | ], 14 | "typeRoots": [ 15 | "../../node_modules/@types" 16 | ], 17 | "baseUrl": ".", 18 | "paths": { 19 | "@common/*": [ 20 | "./src/*", 21 | "./test/*" 22 | ], 23 | "@backend/*": [ 24 | "../portal/backend/src/*" 25 | ], 26 | "@autotest/*": [ 27 | "../autotest/src/*" 28 | ] 29 | } 30 | }, 31 | "include": [ 32 | "./src/**/*.ts", 33 | "./test/**/*.ts" 34 | ], 35 | "exclude": [ 36 | "node_modules" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /packages/portal/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | ARG GH_BOT_EMAIL=classy@cs.ubc.ca 4 | ARG GH_BOT_USERNAME=classy 5 | ARG PLUGIN=default 6 | 7 | RUN apk add --no-cache git 8 | 9 | WORKDIR /app 10 | 11 | # The common package requires the .env file directly so we have to pass it through 12 | COPY .env ./ 13 | COPY yarn.lock ./ 14 | COPY package.json tsconfig.json .env ./ 15 | COPY packages/common ./packages/common 16 | COPY packages/portal ./packages/portal 17 | 18 | RUN yarn install --pure-lockfile --non-interactive --ignore-scripts \ 19 | && yarn tsc --sourceMap false 20 | 21 | # Webpack will do the frontend build 22 | COPY ./plugins/"${PLUGIN}"/portal/frontend ./plugins/"${PLUGIN}"/portal/frontend 23 | RUN cd packages/portal/frontend && yarn webpack && yarn webpack 24 | RUN chmod -R a+r /app \ 25 | && git config --system user.email "${GH_BOT_EMAIL}" \ 26 | && git config --system user.name "${GH_BOT_USERNAME}" 27 | 28 | # Typescript will build the backend 29 | COPY ./plugins/"${PLUGIN}"/portal/backend ./plugins/"${PLUGIN}"/portal/backend 30 | RUN cd ./plugins/"${PLUGIN}"/portal/backend && yarn run build 31 | 32 | CMD ["node", "--require", "/app/node_modules/tsconfig-paths/register", "/app/packages/portal/backend/src/BackendDaemon.js"] 33 | -------------------------------------------------------------------------------- /packages/portal/backend/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Portal BackendDaemon 3 | 4 | ## Dev Instructions 5 | 6 | Follow the one-time instructions below. Once those are done do the following: 7 | 8 | ### To run a dev instance 9 | 10 | * Install docker. 11 | * Install dependencies (`yarn run install`). 12 | * Make sure the `.env` is configured correctly (and is NOT added to version control). 13 | * Start mongo (`docker run -p 27017:27017 mongo`). 14 | * Build the backend (`yarn run build`). 15 | * Start the backend (`yarn run backend` in `backend/` or from WebStorm). 16 | * Start the frontend (`yarn run frontend` in `frontend/`). 17 | * In the browser, visit `https://localhost:3000/` 18 | * For sample data, execute `node -r tsconfig-paths/register packages/portal/backend/src-util/FrontendDatasetGenerator.js` 19 | 20 | ## To run the tests / coverage 21 | 22 | * In `portal/backend/`: 23 | * Build: `yarn run build` 24 | * Tests: `yarn run test` 25 | * Coverage: `yarn run cover` (reports are in `portal/backend/coverage/`). 26 | 27 | ### One-time setup 28 | 29 | * Create `classy/ssl/XXX` and `classy/ssl/XXX`. 30 | * Instructions for this are in `classy/README.md`. 31 | * Copy `classy/ssl/` into `classy/packages/portal/backend/ssl/`. 32 | 33 | * When configuring a WebStorm Run config: 34 | 35 | * Node parameters: `--require dotenv/config`. 36 | * JavaScript File: `src/server/BackendDaemon.js`. 37 | 38 | 39 | ## Configuring Webstorm 40 | 41 | * Configure WebStorm for testing (only needs to happen once): 42 | * Create `Mocha` execution profile 43 | * Node options: `--require dotenv/config -r tsconfig-paths/register` 44 | * Mocha package: `/packages/portal/backend/node_modules/mocha` 45 | * Extra Mocha options: `--exit` 46 | * Test directory: `/packages/portal/backend/test` (select `Include subdirectories`) 47 | 48 | * Configure WebStorm for interactive execution (only needs to happen once): 49 | * Create `Node.js` execution profile 50 | * Node options: `-r dotenv/config -r tsconfig-paths/register` 51 | * Working directory: `/packages/portal/backend` 52 | * JavaScript file: `src/BackendDaemon.js` 53 | -------------------------------------------------------------------------------- /packages/portal/backend/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Classy ERROR 9 | 10 | 11 |

The requested resource does not exist.

12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/portal/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "portal-backend", 3 | "version": "1.0.0", 4 | "description": "Classy Portal Backend", 5 | "main": "build/App.js", 6 | "scripts": { 7 | "test": "node --require dotenv/config --require tsconfig-paths/register ./node_modules/.bin/mocha --timeout 30000 --recursive --exit", 8 | "testCI": "TS_NODE_PROJECT=../../../tsconfig.json node ./node_modules/.bin/mocha --require tsconfig-paths/register --reporter mocha-junit-reporter --reporter-options mochaFile=../../../testOutput/backend/test/test-results.xml --timeout 5000 --recursive --exit", 9 | "cover": "nyc --reporter --require tsconfig-paths/register yarn run test", 10 | "coverCI": "./node_modules/.bin/nyc --reporter html --report-dir ../../../testOutput/backend/coverage --reporter=text-lcov yarn run testCI", 11 | "coveralls": "./node_modules/.bin/nyc report --report-dir ../../../testOutput/backend/coverage --reporter=text-lcov | coveralls", 12 | "codecov": "./node_modules/.bin/nyc report --report-dir ../../../testOutput/backend/coverage --reporter=json > ../../../testOutput/backend/coverage/coverage.json && codecov --token=$CODECOV_TOKEN", 13 | "build": "tsc", 14 | "backend": "node --require dotenv/config --require tsconfig-paths/register ./src/BackendDaemon.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/ubccpsc/classy.git" 19 | }, 20 | "author": "", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/ubccpsc/classy/issues" 24 | }, 25 | "homepage": "https://github.com/ubccpsc/classy", 26 | "dependencies": { 27 | "child-process-promise": "^2.2.1", 28 | "client-oauth2": "^4.2.1", 29 | "cookie": "^0.7.0", 30 | "core-js": "^3.1.3", 31 | "csv-parse": "^4.4.6", 32 | "dotenv": "^5.0.1", 33 | "fs-extra": "^5.0.0", 34 | "markdown-table": "^1.1.2", 35 | "mongodb": "^4.17.0", 36 | "parse-link-header": "^2.0.0", 37 | "restify": "^10.0.0", 38 | "source-map-loader": "^0.2.3", 39 | "supertest": "5.0.0-0", 40 | "tmp-promise": "^1.0.4", 41 | "ts-node": "^7.0.0", 42 | "tsconfig-paths": "^3.9.0", 43 | "tslint": "^5.11.0", 44 | "types": "^0.1.1" 45 | }, 46 | "devDependencies": { 47 | "@babel/core": "^7.0.0-0", 48 | "@types/chai": "^4.3.3", 49 | "@types/chai-as-promised": "^7.1.5", 50 | "@types/cookie": "^0.3.1", 51 | "@types/node": "^18.11.18", 52 | "@types/parse-link-header": "^2.0.0", 53 | "@types/supertest": "^2.0.8", 54 | "chai": "^4.3.6", 55 | "chai-as-promised": "^7.1.1", 56 | "codecov": "^3.0.4", 57 | "coveralls": "^3.0.2", 58 | "mocha": "^9.2.2", 59 | "mocha-junit-reporter": "^1.17.0", 60 | "nyc": "^15.1.0", 61 | "typescript": "^4.9.4", 62 | "xunit-viewer": "^7.1.5" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/portal/backend/src-util/GitHubCleaner.ts: -------------------------------------------------------------------------------- 1 | import Config, { ConfigKey } from "@common/Config"; 2 | import Log from "@common/Log"; 3 | import Util from "@common/Util"; 4 | 5 | import { GitHubActions } from "../src/controllers/GitHubActions"; 6 | import { TeamController } from "../src/controllers/TeamController"; 7 | 8 | /** 9 | * DANGER: You almost certainly do not use this file. 10 | * 11 | * This is an executable for munging GitHub orgs and the classy backend database. It will 12 | * delete database objects _AND_ their corresponding GitHub pieces resulting in unrecoverable 13 | * data loss. 14 | * 15 | * I"d really recommend closing this file now. Doing the wrong thing will ruin your 16 | * _whole_ day. And that is probably underestimating things. 17 | */ 18 | export class GitHubCleaner { 19 | private gha = GitHubActions.getInstance(true); 20 | // private tc = new TeamController(); 21 | 22 | private DRY_RUN = true; 23 | 24 | constructor() { 25 | Log.info("GitHubCleaner:: - start"); 26 | const config = Config.getInstance(); 27 | 28 | Log.warn("GitHubCleaner:: - ORGNAME: " + config.getProp(ConfigKey.org)); 29 | 30 | if (config.getProp(ConfigKey.org) !== config.getProp(ConfigKey.testorg)) { 31 | Log.error("GitHubCleaner:: - org is not the test org. You probably REALLY REALLY do not want to do this"); 32 | this.DRY_RUN = true; // force back to dry run 33 | } 34 | } 35 | 36 | public async run(): Promise { 37 | await this.cleanTeams(); 38 | await this.cleanRepositories(); 39 | } 40 | 41 | private async cleanTeams(): Promise { 42 | Log.info("GitHubCleaner::cleanTeams() - start"); 43 | 44 | const TEAMS_TO_KEEP = ["admin", "staff", "testrunners", "students"]; 45 | TEAMS_TO_KEEP.push(TeamController.ADMIN_NAME, TeamController.STAFF_NAME); 46 | const teams = await this.gha.listTeams(); 47 | const teamsToRemove = []; 48 | for (const team of teams) { 49 | if (TEAMS_TO_KEEP.indexOf(team.teamName) >= 0) { 50 | Log.info("GitHubCleaner::cleanTeams() - team to KEEP: " + team.teamName); 51 | } else { 52 | Log.info("GitHubCleaner::cleanTeams() - team to CLEAN: " + team.teamName); 53 | teamsToRemove.push(team); 54 | } 55 | } 56 | 57 | if (this.DRY_RUN === false) { 58 | Log.info("GitHubCleaner::cleanTeams() - DRY_RUN === false"); 59 | for (const team of teamsToRemove) { 60 | Log.info("GitHubCleaner::cleanTeams() - removing: " + team.teamName); 61 | // const teamNum = await this.tc.getTeamNumber(team.teamName); // await this.gha.getTeamNumber(team.teamName); 62 | // await this.gha.deleteTeam(teamNum); 63 | await this.gha.deleteTeam(team.teamName); 64 | Log.info("GitHubCleaner::cleanTeams() - done removing: " + team.teamName); 65 | } 66 | } 67 | 68 | Log.info("GitHubCleaner::cleanTeams() - done"); 69 | } 70 | 71 | private async cleanRepositories(): Promise { 72 | Log.info("GitHubCleaner::cleanRepositories() - start"); 73 | 74 | const REPOS_TO_KEEP = ["PostTestDoNotDelete", "PostTestDoNotDelete1"]; 75 | 76 | const reposToRemove = []; 77 | const repos = await this.gha.listRepos(); 78 | for (const repo of repos) { 79 | if (REPOS_TO_KEEP.indexOf(repo.repoName) >= 0) { 80 | Log.info("GitHubCleaner::cleanRepositories() - repo to KEEP: " + repo.repoName); 81 | } else { 82 | Log.info("GitHubCleaner::cleanRepositories() - repo to CLEAN: " + repo.repoName); 83 | reposToRemove.push(repo); 84 | } 85 | } 86 | 87 | if (this.DRY_RUN === false) { 88 | Log.info("GitHubCleaner::cleanRepositories() - DRY_RUN === false"); 89 | for (const repo of reposToRemove) { 90 | Log.info("GitHubCleaner::cleanRepositories() - removing: " + repo.repoName); 91 | await this.gha.deleteRepo(repo.repoName); 92 | Log.info("GitHubCleaner::cleanRepositories() - done removing: " + repo.repoName); 93 | } 94 | } 95 | 96 | Log.info("GitHubCleaner::cleanRepositories() - done"); 97 | } 98 | } 99 | 100 | const ghc = new GitHubCleaner(); 101 | const start = Date.now(); 102 | ghc.run() 103 | .then(function () { 104 | Log.info("GitHubCleaner::validate() - complete; took: " + Util.took(start)); 105 | }) 106 | .catch(function (err) { 107 | Log.error("GitHubCleaner::validate() - ERROR: " + err.message); 108 | process.exit(); 109 | }); 110 | -------------------------------------------------------------------------------- /packages/portal/backend/src-util/README.md: -------------------------------------------------------------------------------- 1 | # src-util 2 | 3 | This folder contains a series of programs that demonstrate how the backend can be programmatically manipulated if you need to do some batch-style backend modifications that would never make it into the UI. While several of these are included in `ubccpsc/classy` as examples, you probably just want to add your own files to your own course-specific fork. 4 | 5 | The full list is given below, but the most commonly used batch utilities are `InvokeAutoTest` and `TransformGrades`. 6 | 7 | * `DatabaseValidator`: Compares the GitHub org to the DB and lets you know if things are out of sync. Tries to fix the problems it encounters along the way. This is pretty dangerous though, so use with care. 8 | 9 | * `FrontendDatasetGenerator`: Generates data for Classy frontend testing. 10 | 11 | * `GitHubCleaner`: Batch deletion from DB and GitHub. You *REALLY* don't want to use this. 12 | 13 | * `InvokeAutoTest`: Batch invoke AutoTest on a specific set of commits. This is a pretty safe operation and is commonly used. 14 | 15 | * `TransformGrades`: Allows for post-hoc grade updates. This does modify the database, but can be helpful for changing grading rubrics etc. 16 | 17 | * `WebhookUpdater`: Updates the GitHub webhook addresses and secrets. While uncommon, this can be handy. 18 | -------------------------------------------------------------------------------- /packages/portal/backend/src/BackendDaemon.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by rtholmes on 2018-02-23. 3 | */ 4 | 5 | import Config from "@common/Config"; 6 | import Log from "@common/Log"; 7 | 8 | import BackendServer from "./server/BackendServer"; 9 | 10 | export class BackendDaemon { 11 | private server: BackendServer = null; 12 | 13 | constructor() { 14 | Log.info("BackendDaemon:: - start"); 15 | // App.config = Config; 16 | } 17 | 18 | public async start(): Promise { 19 | Log.info("BackendDaemon::start() - start"); 20 | 21 | // handle any config changes (specifically dev vs prod) 22 | if (this.server === null) { 23 | this.server = new BackendServer(true); // RELEASE: param should be true 24 | } 25 | 26 | try { 27 | await this.server.start(); 28 | Log.info("BackendDaemon::start() - server started"); 29 | return true; 30 | } catch (err) { 31 | Log.info("BackendDaemon::start() - server staring - ERROR: " + err); 32 | return false; 33 | } 34 | } 35 | 36 | public async stop(): Promise { 37 | Log.info("BackendDaemon::stop() - start"); 38 | 39 | // handle any config changes (specifically dev vs prod) 40 | if (this.server !== null) { 41 | return this.server 42 | .stop() 43 | .then(function () { 44 | Log.info("BackendDaemon::stop() - server stopped"); 45 | return true; 46 | }) 47 | .catch(function (err) { 48 | Log.info("BackendDaemon::stop() - server stopping - ERROR: " + err); 49 | return false; 50 | }); 51 | } else { 52 | Log.info("BackendDaemon::stop() - server not defined"); 53 | return false; 54 | } 55 | } 56 | } 57 | 58 | // This ends up starting the whole system 59 | Log.info("BackendDaemon - starting"); 60 | Config.getInstance(); 61 | const app = new BackendDaemon(); 62 | app.start() 63 | .then(function (success) { 64 | if (success === true) { 65 | Log.info("BackendDaemon - start success"); 66 | } else { 67 | Log.info("BackendDaemon - start failure"); 68 | } 69 | }) 70 | .catch(function (err) { 71 | Log.info("BackendDaemon - start ERROR: " + err); 72 | }); 73 | 74 | // Unhandled rejection checking code; this is not great, but is better than being surprised 75 | Log.info("BackendDaemon - registering unhandled rejection"); 76 | process.on("unhandledRejection", (reason) => { 77 | // , p 78 | try { 79 | Log.error("BackendDaemon - unhandled promise"); // in case next line fails 80 | // console.log("BackendDaemon - unhandled rejection at: ", p, "; reason:", reason); 81 | Log.error("BackendDaemon - unhandled promise: " + JSON.stringify(reason)); 82 | } catch (err) { 83 | // eat any error 84 | } 85 | }); 86 | Log.info("BackendDaemon - registering unhandled rejection; done"); 87 | // Promise.reject("foo"); 88 | -------------------------------------------------------------------------------- /packages/portal/backend/src/server/IREST.ts: -------------------------------------------------------------------------------- 1 | import * as restify from "restify"; 2 | 3 | export default interface IREST { 4 | // Restify cheatsheet (great resource): https://gist.github.com/LeCoupa/0664e885fd74152d1f90 5 | registerRoutes(server: restify.Server): void; 6 | } 7 | -------------------------------------------------------------------------------- /packages/portal/backend/test/CSVPrairieLearnParserSpec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import "mocha"; 3 | 4 | import Log from "@common/Log"; 5 | import { TestHarness } from "@common/TestHarness"; 6 | import "@common/GlobalSpec"; 7 | 8 | import { GradesController } from "@backend/controllers/GradesController"; 9 | import { CSVPrairieLearnParser } from "@backend/server/common/CSVPrairieLearnParser"; 10 | 11 | describe("CSVPrairieLearnParser", function () { 12 | before(async () => { 13 | await TestHarness.suiteBefore("CSVPrairieLearnParser"); 14 | await TestHarness.prepareAll(); 15 | }); 16 | 17 | it("Should be able to process an empty grade sheet", async function () { 18 | const path = __dirname + "/data/prairieEmpty.csv"; 19 | let rows = null; 20 | let ex = null; 21 | try { 22 | const csv = new CSVPrairieLearnParser(); 23 | rows = await csv.processGrades(TestHarness.ADMIN1.id, path); 24 | Log.test("# rows processed: " + rows.length); 25 | } catch (err) { 26 | ex = err; 27 | } 28 | expect(rows).to.be.null; 29 | expect(ex).to.not.be.null; 30 | }); 31 | 32 | it("Should be able to process a valid grade sheet", async function () { 33 | // check pre 34 | Log.test("check grades before"); 35 | const gc = new GradesController(); 36 | let grade = await gc.getGrade(TestHarness.USER1.id, TestHarness.DELIVID1); 37 | expect(grade.score).to.equal(100); 38 | grade = await gc.getGrade(TestHarness.USER2.id, TestHarness.DELIVID1); 39 | expect(grade.score).to.equal(100); 40 | grade = await gc.getGrade(TestHarness.USER3.id, TestHarness.DELIVID1); 41 | expect(grade).to.be.null; 42 | 43 | // do upload 44 | const path = __dirname + "/data/prairieValid.csv"; 45 | const csv = new CSVPrairieLearnParser(); 46 | Log.test("process sheet"); 47 | const rows = await csv.processGrades(TestHarness.ADMIN1.id, path); 48 | Log.test("# rows processed: " + rows.length); 49 | expect(rows).to.have.lengthOf(3); 50 | 51 | // validate outcome 52 | Log.test("check grades after"); 53 | grade = await gc.getGrade(TestHarness.USER1.id, TestHarness.DELIVID1); 54 | expect(grade.score).to.equal(79.38); 55 | grade = await gc.getGrade(TestHarness.USER2.id, TestHarness.DELIVID1); 56 | expect(grade.score).to.equal(65); 57 | grade = await gc.getGrade(TestHarness.USER3.id, TestHarness.DELIVID1); 58 | expect(grade.score).to.equal(13.75); 59 | }); 60 | 61 | it("Should not be able to process an invalid grade sheet", async function () { 62 | let rows = null; 63 | let ex = null; 64 | try { 65 | const path = __dirname + "/data/gradesInvalid.csv"; 66 | const csv = new CSVPrairieLearnParser(); 67 | rows = await csv.processGrades(TestHarness.ADMIN1.id, path); 68 | } catch (err) { 69 | ex = err; 70 | } 71 | expect(rows).to.be.null; 72 | expect(ex).to.not.be.null; 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /packages/portal/backend/test/FactorySpec.ts: -------------------------------------------------------------------------------- 1 | // import { use as chaiUse } from "chai"; 2 | import { expect, use as chaiUse } from "chai"; 3 | import * as chaiAsPromised from "chai-as-promised"; 4 | import "mocha"; 5 | 6 | import Log from "@common/Log"; 7 | import Config, { ConfigKey } from "@common/Config"; 8 | 9 | import { Factory } from "@backend/Factory"; 10 | 11 | import "@common/GlobalSpec"; 12 | 13 | chaiUse(chaiAsPromised); 14 | 15 | describe("Factory", function () { 16 | /** 17 | * These are all terrible tests and just make sure that _some_ object is returned. 18 | */ 19 | it("Can get the route handler for courses", async function () { 20 | let actual = Factory.getCustomRouteHandler("classytest"); 21 | expect(actual).to.not.be.null; 22 | 23 | actual = Factory.getCustomRouteHandler("cs310"); 24 | expect(actual).to.not.be.null; 25 | 26 | actual = null; 27 | let ex = null; 28 | try { 29 | actual = Factory.getCustomRouteHandler("INVALIDcourseNAME"); 30 | } catch (err) { 31 | ex = err; 32 | } 33 | expect(actual).to.not.be.null; // NoRouteHandler 34 | expect(ex).to.be.null; 35 | }); 36 | 37 | it("Can get the course controller for courses", async function () { 38 | // should be able to get our test controller 39 | const actual = await Factory.getCourseController(null, "classytest"); 40 | Log.test("Controller should not be null: " + actual); 41 | expect(actual).to.not.be.null; 42 | }); 43 | 44 | it("Invalid plugins should be handled gracefully", async function () { 45 | const pluginVal = Config.getInstance().getProp(ConfigKey.plugin); 46 | Config.getInstance().setProp(ConfigKey.plugin, "INVALIDPLUGIN"); 47 | 48 | await expect(Factory.getCustomRouteHandler("INVALID_PLUGIN")).to.eventually.throw; 49 | await expect(Factory.getCourseController(null, "INVALID_PLUGIN")).to.eventually.throw; 50 | await expect(Factory.getCourseController(undefined, undefined)).to.eventually.throw; 51 | 52 | Config.getInstance().setProp(ConfigKey.plugin, pluginVal); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /packages/portal/backend/test/controllers/DeliverablesControllerSpec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import "mocha"; 3 | 4 | import { DeliverableTransport } from "@common/types/PortalTypes"; 5 | import { TestHarness } from "@common/TestHarness"; 6 | 7 | import { DatabaseController } from "@backend/controllers/DatabaseController"; 8 | import { DeliverablesController } from "@backend/controllers/DeliverablesController"; 9 | import { Deliverable } from "@backend/Types"; 10 | 11 | import "@common/GlobalSpec"; // load first 12 | import "./DatabaseControllerSpec"; // run first 13 | 14 | describe("DeliverablesController", () => { 15 | let dc: DeliverablesController; 16 | 17 | let DELIV1: Deliverable = null; // delivs are complex so just use one for the whole suite 18 | 19 | before(async () => { 20 | await TestHarness.suiteBefore("DeliverablesController"); 21 | DELIV1 = TestHarness.createDeliverable(TestHarness.DELIVID1); 22 | }); 23 | 24 | beforeEach(() => { 25 | dc = new DeliverablesController(); 26 | }); 27 | 28 | after(() => { 29 | TestHarness.suiteAfter("DeliverablesController"); 30 | }); 31 | 32 | it("Should be able to get all deliverables, even if there are none.", async () => { 33 | const delivs = await dc.getAllDeliverables(); 34 | expect(delivs).to.have.lengthOf(0); 35 | }); 36 | 37 | it("Should be able to save a deliverable.", async () => { 38 | let delivs = await dc.getAllDeliverables(); 39 | expect(delivs).to.have.lengthOf(0); 40 | 41 | const valid = await dc.saveDeliverable(DELIV1); 42 | expect(valid).to.not.be.null; 43 | delivs = await dc.getAllDeliverables(); 44 | expect(delivs).to.have.lengthOf(1); 45 | expect(delivs[0].id).to.equal(DELIV1.id); 46 | }); 47 | 48 | it("Should update an existing deliverable.", async () => { 49 | let delivs = await dc.getAllDeliverables(); 50 | expect(delivs).to.have.lengthOf(1); 51 | 52 | const deliv2: Deliverable = Object.assign({}, DELIV1); 53 | deliv2.gradesReleased = true; 54 | deliv2.teamMinSize = 4; 55 | 56 | const valid = await dc.saveDeliverable(deliv2); 57 | expect(valid).to.not.be.null; 58 | delivs = await dc.getAllDeliverables(); 59 | expect(delivs).to.have.lengthOf(1); 60 | expect(delivs[0].gradesReleased).to.be.true; 61 | expect(delivs[0].teamMinSize).to.equal(4); 62 | }); 63 | 64 | it("Should be able to get a specific deliverable.", async () => { 65 | const deliv = await dc.getDeliverable(TestHarness.DELIVID1); 66 | expect(deliv).to.not.be.null; 67 | expect(deliv.id).to.equal(TestHarness.DELIVID1); 68 | }); 69 | 70 | it("Should be able to invalidate bad deliverables.", async () => { 71 | let deliv = await dc.validateDeliverableTransport(undefined); 72 | expect(deliv).to.not.be.null; 73 | expect(deliv).to.be.an("string"); 74 | 75 | deliv = await dc.validateDeliverableTransport(null); 76 | expect(deliv).to.not.be.null; 77 | expect(deliv).to.be.an("string"); 78 | 79 | deliv = await dc.validateDeliverableTransport({ id: "a" } as DeliverableTransport); 80 | expect(deliv).to.not.be.null; 81 | expect(deliv).to.be.an("string"); 82 | }); 83 | 84 | // this test should be last 85 | it("Should enforce minimum constraint on student delay in deliverables when saving.", async () => { 86 | const db = DatabaseController.getInstance(); 87 | 88 | const allPeople = await db.getPeople(); 89 | for (const p of allPeople) { 90 | await db.deletePerson(p); 91 | } 92 | 93 | let deliv = await dc.getDeliverable(TestHarness.DELIVID1); 94 | expect(deliv).to.not.be.null; 95 | 96 | deliv.autotest.studentDelay = 60; // 1 minute 97 | const valid = await dc.saveDeliverable(deliv); 98 | expect(valid).to.not.be.null; 99 | 100 | deliv = await dc.getDeliverable(TestHarness.DELIVID1); 101 | expect(deliv).to.not.be.null; 102 | expect(deliv.autotest.studentDelay).to.be.greaterThan(120); 103 | }); 104 | 105 | it("Should enforce custom entered minimum delay between grade requests", async () => { 106 | DatabaseController.getInstance(); 107 | let deliv = await dc.getDeliverable(TestHarness.DELIVID1); 108 | expect(deliv).to.not.be.null; 109 | deliv.autotest.studentDelay = 901; // 15 minutes, 1 second 110 | 111 | const valid = await dc.saveDeliverable(deliv); 112 | expect(valid).to.not.be.null; 113 | 114 | deliv = await dc.getDeliverable(TestHarness.DELIVID1); 115 | expect(deliv).to.not.be.null; 116 | expect(deliv.autotest.studentDelay).to.be.equal(901); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /packages/portal/backend/test/data/classlistDuplicateField.csv: -------------------------------------------------------------------------------- 1 | ACCT,SNUM,CWL,LAST,FIRST,LAB 2 | x8x1,12347423,dkl23TEST,AasdfFirstTest,AasdfLastTest,L2C 3 | x8x1,99992222,rthse2new,Arthse2,Zrthse2Last,L2C 4 | -------------------------------------------------------------------------------- /packages/portal/backend/test/data/classlistEmpty.csv: -------------------------------------------------------------------------------- 1 | ACCT,SNUM,CWL,LAST,FIRST,LAB 2 | -------------------------------------------------------------------------------- /packages/portal/backend/test/data/classlistEmptyField.csv: -------------------------------------------------------------------------------- 1 | ACCT,SNUM,CWL,LAST,FIRST,LAB 2 | a2b1,42383984,,DadsfasFirstTest,DasdfLastTest,L2J 3 | b6x4,48483222,badfasdTEST,CasdfFirstTest,CasdfLastTest, 4 | l2z2,29394944,e2klTEST,BasdfasFirstTest,BasdfLastTest,L2C 5 | x8x1,39292932,dkl23TEST,AasdfFirstTest,AasdfLastTest,L2C 6 | rthse2,99992222,rthse2new,Arthse2,Zrthse2Last,L2C 7 | -------------------------------------------------------------------------------- /packages/portal/backend/test/data/classlistEmptyNamePrefName.csv: -------------------------------------------------------------------------------- 1 | ACCT,SNUM,CWL,LAST,LAB 2 | a2b1,22992922,aardvarkTEST,DadsfasFirstTest,DasdfLastTest,L2J 3 | 4 | -------------------------------------------------------------------------------- /packages/portal/backend/test/data/classlistInvalid.csv: -------------------------------------------------------------------------------- 1 | ACCT,SNUM,CWL,LAST,FIRST 2 | a2b1,22992922,aardvarkTEST,DadsfasFirstTest,DasdfLastTest 3 | b6x4,48483222,badfasdTEST,CasdfFirstTest,CasdfLastTest 4 | l2z2,29394944,e2klTEST,BasdfasFirstTest,BasdfLastTest 5 | x8x1,39292932,dkl23TEST,AasdfFirstTest,AasdfLastTest 6 | -------------------------------------------------------------------------------- /packages/portal/backend/test/data/classlistTest.csv: -------------------------------------------------------------------------------- 1 | ACCT,SNUM,CWL,LAST,FIRST,LAB 2 | Atest-01,22992922,Atest-01,AadsfasFirstTest,AasdfLastTest,L2J 3 | Atest-02,48483222,Atest-02,BasdfFirstTest,BasdfLastTest, 4 | Atest-03,29394944,Atest-03,CasdfasFirstTest,CasdfLastTest,L2A 5 | Atest-04,39292932,Atest-04,DasdfFirstTest,DasdfLastTest,L2A 6 | -------------------------------------------------------------------------------- /packages/portal/backend/test/data/classlistTestSingle.csv: -------------------------------------------------------------------------------- 1 | ACCT,SNUM,CWL,LAST,FIRST,LAB 2 | Atest-01,22992922,Atest-01,AadsfasFirstTest,AasdfLastTest,L2TEST 3 | -------------------------------------------------------------------------------- /packages/portal/backend/test/data/classlistValidFirst.csv: -------------------------------------------------------------------------------- 1 | ACCT,SNUM,CWL,LAST,FIRST,LAB 2 | a2b1,22992922,aardvarkTEST2,DadsfasFirstTest,DasdfLastTest,L2J 3 | b6x4,48483222,badfasdTEST2,CasdfFirstTest,CasdfLastTest, 4 | l2z2,29394944,e2klTEST2,BasdfasFirstTest,BasdfLastTest,L2A 5 | x8x1,39292932,dkl23TEST2,AasdfFirstTest,AasdfLastTest,L2C 6 | rthse2,99992222,rthse22,Arthse2first,Zrthse2Last,L2A 7 | -------------------------------------------------------------------------------- /packages/portal/backend/test/data/classlistValidPrefName.csv: -------------------------------------------------------------------------------- 1 | ACCT,SNUM,CWL,LAST,PREF,LAB 2 | a2b1,22992922,aardvarkTEST,DadsfasFirstTestx,DasdfLastTest,L2J 3 | b6x4,48483222,badfasdTEST,CasdfFirstTestx,CasdfLastTest, 4 | l2z2,29394944,e2klTEST,BasdfasFirstTestx,BasdfLastTest,L2A 5 | x8x1,39292932,dkl23TEST,AasdfFirstTestx,AasdfLastTest,L2C 6 | rthse2,99992222,rthse2,Arthse2firstx,Zrthse2Last,L2A 7 | -------------------------------------------------------------------------------- /packages/portal/backend/test/data/classlistValidUpdate.csv: -------------------------------------------------------------------------------- 1 | ACCT,SNUM,CWL,LAST,FIRST,LAB 2 | a2b1,22992922,aardvarkTEST,DadsfasFirstTest,DasdfLastTest,L2J 3 | b6x4,48483222,badfasdTEST,CasdfFirstTest,CasdfLastTest, 4 | l2z2,29394944,e2klTEST,BasdfasFirstTest,BasdfLastTest,L2C 5 | x8x1,39292932,dkl23TEST,AasdfFirstTest,AasdfLastTest,L2C 6 | rthse2,99992222,rthse2new,Arthse2,Zrthse2Last,L2C 7 | -------------------------------------------------------------------------------- /packages/portal/backend/test/data/gradesEmpty.csv: -------------------------------------------------------------------------------- 1 | CSID,GRADE,COMMENT 2 | -------------------------------------------------------------------------------- /packages/portal/backend/test/data/gradesInconsistent.csv: -------------------------------------------------------------------------------- 1 | GITHUB,GRADE,COMMENT 2 | user1ID,92,csv comment 3 | user2ID,29,csv comment 4 | user3ID,19,csv comment 5 | -------------------------------------------------------------------------------- /packages/portal/backend/test/data/gradesInconsistent2.csv: -------------------------------------------------------------------------------- 1 | CWL,GRADE,COMMENT 2 | user1ID,92,csv comment 3 | user2ID,29,csv comment 4 | user3ID,19,csv comment 5 | -------------------------------------------------------------------------------- /packages/portal/backend/test/data/gradesInvalid.csv: -------------------------------------------------------------------------------- 1 | CSIDbadColumn,GRADE,COMMENT 2 | foo, 12, comment here -------------------------------------------------------------------------------- /packages/portal/backend/test/data/gradesValid.csv: -------------------------------------------------------------------------------- 1 | CSID,GRADE,COMMENT 2 | user1ID,92,csv comment 3 | user2ID,29,csv comment 4 | user3ID,19,csv comment 5 | -------------------------------------------------------------------------------- /packages/portal/backend/test/data/gradesValidBucket.csv: -------------------------------------------------------------------------------- 1 | CSID,GRADE,DISPLAY,COMMENT 2 | user1ID, 100,EXTENDING ,csv comment 3 | user2ID, 80,PROFICIENT,csv comment 4 | user3ID,0,N/A ,csv comment 5 | -------------------------------------------------------------------------------- /packages/portal/backend/test/data/gradesValidBucketCWL.csv: -------------------------------------------------------------------------------- 1 | CWL,GRADE,DISPLAY,COMMENT 2 | user1ID, 100,EXTENDING ,csv comment 3 | user2ID, 80,PROFICIENT,csv comment 4 | user3ID,0,N/A ,csv comment 5 | -------------------------------------------------------------------------------- /packages/portal/backend/test/data/gradesValidBucketGithub.csv: -------------------------------------------------------------------------------- 1 | GITHUB,GRADE,DISPLAY,COMMENT 2 | user1gh, 99,EXTENDING1 ,csv comment 3 | user2gh, 79,PROFICIENT1,csv comment 4 | user3gh,1,N/A1 ,csv comment 5 | -------------------------------------------------------------------------------- /packages/portal/backend/test/data/prairieEmpty.csv: -------------------------------------------------------------------------------- 1 | UID,UIN,Name,Role,P1,d0,d1,d2,Q3,Q4 2 | -------------------------------------------------------------------------------- /packages/portal/backend/test/data/prairieValid.csv: -------------------------------------------------------------------------------- 1 | UID,UIN,Name,Role,P1,Q0,d1,d2,d3,Q4 2 | user1gh@ubc.ca,O0QKX1ZE6B02,fName0 lName0,Student,,,79.375,,, 3 | user2gh@ubc.ca,RISPWAV14C05,fName1 lName1,Student,,,65,,, 4 | user3gh@ubc.ca,DMHA6KTX0Z05,fName2 lName2,Student,,,13.75,,, 5 | 6 | -------------------------------------------------------------------------------- /packages/portal/backend/test/data/prairieValidUpload.csv: -------------------------------------------------------------------------------- 1 | UID,UIN,Name,Role,P1,Q0,d1,d2,d3,Q4 2 | student-1@ubc.ca,O0QKX1ZE6B02,fName0 lName0,Student,,,,79.375,, 3 | student-2@ubc.ca,RISPWAV14C05,fName1 lName1,Student,,,,65,, 4 | student-3@ubc.ca,DMHA6KTX0Z05,fName2 lName2,Student,,,,13.75,, 5 | 6 | -------------------------------------------------------------------------------- /packages/portal/backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES6", 5 | "noImplicitAny": true, 6 | "removeComments": false, 7 | "preserveConstEnums": true, 8 | "sourceMap": true, 9 | "baseUrl": ".", 10 | "paths": { 11 | "@backend/*": [ 12 | "./src/*" 13 | ], 14 | "@common/*": [ 15 | "../../common/src/*", 16 | "../../common/test/*" 17 | ] 18 | }, 19 | "lib": [ 20 | "es7", 21 | "es2017.object", 22 | "dom" 23 | ], 24 | "typeRoots": [ 25 | "../../../node_modules/@types" 26 | ] 27 | }, 28 | "include": [ 29 | "src/**/*.ts", 30 | "test/**/*.ts" 31 | ], 32 | "exclude": [ 33 | "node_modules" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /packages/portal/frontend/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Portal Frontend 3 | 4 | 5 | 6 | ## Dev Instructions 7 | 8 | This assumes you're working with WebStorm. 9 | 10 | 1) Change into `portal/frontend/` 11 | 12 | 2) Run WebPack (bundles TS into JS for the browser): `webpack --watch` 13 | 14 | 3) Start `portal/backend` (probably in WebStorm so you can set breakpoints etc); details about configuring/running the backend can be found in `docs/developer/bootstrap.md` 15 | 16 | 4) Navigate to `https://localhost:3000` in your browser. This may require the `chrome://flags/#allow-insecure-localhost` be enabled, if you are using Chrome. 17 | 18 | -------------------------------------------------------------------------------- /packages/portal/frontend/html/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubccpsc/classy/13cb6b8812672149041d7ac03eefdf6c46d34c76/packages/portal/frontend/html/favicon.ico -------------------------------------------------------------------------------- /packages/portal/frontend/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Classy 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /packages/portal/frontend/html/invalid.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Classy - Invalid User 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 17 | 18 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
Invalid Classy Credentials
34 |
35 |
36 | 37 |
38 |
39 |

40 | GitHub username is not registered with the course. Please contact course staff. 41 |

42 |

43 | This can also happen if you changed your CWL during the term. If this is the case, we strongly urge you to 44 | consider changing it back until the end of term. 45 |

46 |
47 |
48 |
49 |
50 |
51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /packages/portal/frontend/html/style.css: -------------------------------------------------------------------------------- 1 | .list-header { 2 | padding-top: 4px; 3 | padding-bottom: 0; 4 | padding-left: 10px; 5 | padding-right: 0; 6 | background-color: #99c8ff; /* full shade (too dark): #0076ff */ 7 | color: #424242; 8 | } 9 | 10 | .mainBody { 11 | /* height: 100%; */ 12 | display: flex; 13 | flex-direction: column; 14 | align-items: center; 15 | justify-content: center; 16 | } 17 | 18 | .questionBody { 19 | /* Something goes here */ 20 | } 21 | 22 | .subQuestionBody { 23 | display: flex; 24 | flex-direction: row; 25 | margin-bottom: 25px; 26 | align-items: flex-start; 27 | } 28 | 29 | .subQuestionInfoBox { 30 | flex: 2; 31 | } 32 | 33 | .subQuestionTextBox { 34 | flex: 3; 35 | flex-direction: column; 36 | } 37 | 38 | .textboxLabel { 39 | flex: 1; 40 | margin-top: 0px; 41 | } 42 | 43 | .errorBox { 44 | color: red; 45 | } 46 | 47 | .redText { 48 | color: red; 49 | } 50 | 51 | #gradingSection { 52 | margin-top: 50px; 53 | margin-bottom: 100px; 54 | } 55 | 56 | .assignmentInfo { 57 | padding-left: 2em; 58 | padding-right: 1em; 59 | vertical-align: middle; 60 | } 61 | 62 | /** 63 | Settings Page 64 | */ 65 | 66 | .settingTextInput { 67 | background-color: lightsteelblue; 68 | } 69 | 70 | .noResultsRow { 71 | text-align: center; 72 | padding-top: 1em; 73 | padding-bottom: 1em; 74 | } 75 | 76 | /** 77 | Dashboard Page 78 | */ 79 | 80 | .dashHistoBucket { 81 | font-weight: bold; 82 | padding-right: 1em; 83 | } 84 | 85 | .dashHistoValue { 86 | padding-left: 2em; 87 | } 88 | 89 | .dashResultCell { 90 | /* nothing yet */ 91 | } 92 | 93 | .toast--danger { 94 | background-color: hsl(348, 100%, 61%); 95 | } 96 | 97 | .hidden { 98 | display: none; 99 | } 100 | 101 | /** 102 | Styles for all sortable tables. 103 | */ 104 | .sortableTable { 105 | margin-left: auto; 106 | margin-right: auto; 107 | border-collapse: collapse; 108 | user-select: auto; 109 | -webkit-user-select: auto; 110 | } 111 | 112 | .sortableHeader { 113 | user-select: auto; 114 | -webkit-user-select: auto; 115 | } 116 | 117 | .sortableTableRow { 118 | user-select: auto; 119 | -webkit-user-select: auto; 120 | } 121 | 122 | .sortableTableCell { 123 | user-select: auto; 124 | -webkit-user-select: auto; 125 | } 126 | 127 | .sortableTable th { 128 | background-color: white; 129 | position: sticky; 130 | top: 0; 131 | } 132 | 133 | .selectable { 134 | user-select: auto; 135 | -webkit-user-select: auto; 136 | } 137 | 138 | /** 139 | stdio viewer Page 140 | */ 141 | #stdioViewer { 142 | position: absolute; 143 | top: 0; 144 | right: 0; 145 | bottom: 0; 146 | left: 0; 147 | } 148 | 149 | .dialog { 150 | width: 80%; 151 | } 152 | 153 | #adminDockerBuildDialog-footer-container ons-bottom-toolbar { 154 | justify-content: center; 155 | align-items: center; 156 | display: flex; 157 | } 158 | 159 | #adminDockerBuildDialog-logs-text { 160 | max-height: 70vh; 161 | overflow-y: auto; 162 | } 163 | 164 | #adminDockerBuildDialog .dialog { 165 | height: 80vh; 166 | } 167 | 168 | /** 169 | CSS for Classlist API update change report 170 | */ 171 | #adminClasslistConfirmHeader ons-list { 172 | overflow-y: auto; 173 | height: 80vh; 174 | } 175 | 176 | #adminClasslistConfirmHeader ons-button { 177 | text-align: center; 178 | width: 100%; 179 | } 180 | -------------------------------------------------------------------------------- /packages/portal/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "portal-frontend", 3 | "version": "1.0.0", 4 | "description": "Classy Portal Frontend", 5 | "main": "app/index.html", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/ubccpsc/classy.git" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "bugs": { 13 | "url": "https://github.com/ubccpsc/classy/issues" 14 | }, 15 | "homepage": "https://github.com/ubccpsc/classy", 16 | "scripts": { 17 | "build": "tsc", 18 | "webpack": "NODE_OPTIONS=--openssl-legacy-provider webpack", 19 | "frontend": "NODE_OPTIONS=--openssl-legacy-provider webpack --watch" 20 | }, 21 | "dependencies": { 22 | "client-oauth2": "^4.1.0", 23 | "core-js": "^3.1.3", 24 | "flatpickr": "^4.5.1", 25 | "moment": "^2.29.4", 26 | "onsenui": "^2.12.2" 27 | }, 28 | "devDependencies": { 29 | "copy-webpack-plugin": "^6.2.1", 30 | "dotenv": "^8.2.0", 31 | "fs-extra": "^5.0.0", 32 | "source-map-loader": "^0.2.4", 33 | "ts-loader": "^9.4.2", 34 | "tslint": "^5.11.0", 35 | "typescript": "^4.9.4", 36 | "webpack": "^5.94.0", 37 | "tsconfig-paths-webpack-plugin": "^3.3.0", 38 | "webpack-cli": "^4.7.2" 39 | }, 40 | "resolutions": { 41 | "**/event-stream": "^4.0.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/portal/frontend/src/app/util/AuthHelper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by rtholmes on 2017-10-04. 3 | */ 4 | import Log from "@common/Log"; 5 | 6 | export class AuthHelper { 7 | private backendURL: string; 8 | private OPTIONS_HTTP_GET: object = { credentials: "include" }; 9 | 10 | constructor(backendURL: string) { 11 | Log.trace("AuthHelper:: - start"); 12 | this.backendURL = backendURL; 13 | } 14 | 15 | /** 16 | * @param {string} userrole that the current user should match 17 | */ 18 | public checkUserrole(userrole: string) { 19 | Log.trace("AuthHelper::checkUserRole() - start"); 20 | this.getCurrentUser() 21 | .then((data: any) => { 22 | if (data.response.user.userrole === userrole) { 23 | Log.trace("AuthHelper::checkUserrole() Valid userrole confirmed: " + userrole + "."); 24 | } else { 25 | this.updateAuthStatus(); 26 | } 27 | }) 28 | .catch((_err: any) => { 29 | Log.error("AuthHelper::checkUserrole() - end"); 30 | }); 31 | } 32 | 33 | public updateAuthStatus() { 34 | this.isLoggedIn() 35 | .then((data: any) => { 36 | // IsAuthenticatedResponse 37 | // console.log(data.response); 38 | Log.trace("AuthHelper::updateAuthStatus( ) - start"); 39 | const authStatus = localStorage.getItem("authStatus"); 40 | const UNAUTHENTICATED_STATUS = "unauthenticated"; 41 | if (data.response === false && authStatus !== UNAUTHENTICATED_STATUS) { 42 | Log.trace("AuthHelper::updateAuthStatus( unauthenticated )"); 43 | localStorage.setItem("authStatus", UNAUTHENTICATED_STATUS); 44 | location.reload(); 45 | } 46 | Log.trace("AuthHelper::updateAuthStatus( ) - end"); 47 | }) 48 | .catch((_err: Error) => { 49 | this.removeAuthStatus(); 50 | Log.error("AuthHelper::updateAuthStatus( ERROR ) - Logged out - Unauthenticated"); 51 | }); 52 | } 53 | 54 | private getCurrentUser(): Promise { 55 | const that = this; 56 | const url = that.backendURL + "/portal/currentUser"; // TODO: what is this route??? 57 | Log.trace("AuthHelper::getCurrentUser( " + url + " ) - start"); 58 | 59 | return fetch(url, that.OPTIONS_HTTP_GET) 60 | .then((data: any) => { 61 | if (data.status !== 200) { 62 | throw new Error("AuthHelper::getCurrentUser( " + url + " )"); 63 | } else { 64 | return data.json(); 65 | } 66 | }) 67 | .catch((err: Error) => { 68 | Log.error("AuthHelper::getCurrentUser( " + url + ") - ERROR " + err); 69 | }); 70 | } 71 | 72 | private removeAuthStatus() { 73 | localStorage.removeItem("authStatus"); 74 | } 75 | 76 | private isLoggedIn(): Promise { 77 | const that = this; 78 | const url = that.backendURL + "/portal/isAuthenticated"; // TODO: what is this route??? 79 | Log.trace("AuthHelper::isLoggedIn( " + url + " ) - start"); 80 | // const AUTHORIZED_STATUS: string = "authorized"; 81 | // const authStatus = String(localStorage.getItem("authStatus")); 82 | 83 | return fetch(url, that.OPTIONS_HTTP_GET) 84 | .then((data: any) => { 85 | if (data.status !== 200) { 86 | throw new Error("AuthHelper::isLoggedIn( " + that.backendURL + " )"); 87 | } else { 88 | return data.json(); 89 | } 90 | }) 91 | .catch((err: Error) => { 92 | Log.error("AuthHelper::handleRemote( " + that.backendURL + " ) - ERROR " + err); 93 | }); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /packages/portal/frontend/src/app/util/DashboardTable.ts: -------------------------------------------------------------------------------- 1 | import { SortableTable } from "./SortableTable"; 2 | 3 | export class DashboardTable extends SortableTable { 4 | public generate() { 5 | super.generate(); 6 | 7 | function toggle(event: MouseEvent) { 8 | const elem = event.currentTarget as Element; 9 | const normal = elem.querySelector("span.normalhistogram"); 10 | const clustered = elem.querySelector("span.clusteredhistogram"); 11 | if (clustered !== null) { 12 | normal.classList.toggle("hidden"); 13 | clustered.classList.toggle("hidden"); 14 | } 15 | } 16 | 17 | const tableDivs = document.querySelectorAll("div.histogramcontainer"); 18 | const tableDivArray = Array.prototype.slice.call(tableDivs, 0); 19 | for (const div of tableDivArray) { 20 | div.onclick = toggle; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/portal/frontend/src/app/views/AdminPage.ts: -------------------------------------------------------------------------------- 1 | import Log from "@common/Log"; 2 | 3 | import { Factory } from "../Factory"; 4 | import { IView } from "./IView"; 5 | 6 | import { UI } from "../util/UI"; 7 | 8 | export abstract class AdminPage implements IView { 9 | protected readonly remote: string | null = null; 10 | 11 | protected constructor(remote: string) { 12 | this.remote = remote; 13 | } 14 | 15 | /** 16 | * Initializes the view (e.g., wires up the buttons, fetches data, etc). 17 | * 18 | * @param opts 19 | * @returns {Promise} 20 | */ 21 | public abstract init(opts: any): Promise; 22 | 23 | public renderPage(pageName: string, opts: {}): void { 24 | Log.info("AdminPage::renderPage( " + pageName + ", ... ) - default implementation"); 25 | } 26 | 27 | /** 28 | * Pushes the page. If the page starts with ./ HTML prefix is not added. 29 | * 30 | * @param {string} pageName 31 | * @param {{}} opts 32 | * @returns {Promise} 33 | */ 34 | public pushPage(pageName: string, opts: {}): Promise { 35 | Log.info("AdminPage::pushPage( " + pageName + ", ... ) - start"); 36 | if (typeof opts !== "object") { 37 | opts = {}; 38 | } 39 | if (pageName.startsWith("./")) { 40 | pageName = pageName.substring(2); 41 | return UI.pushPage(pageName, opts); 42 | } else { 43 | const prefix = Factory.getInstance().getHTMLPrefix(); 44 | return UI.pushPage(prefix + "/" + pageName, opts); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/portal/frontend/src/app/views/IView.ts: -------------------------------------------------------------------------------- 1 | export interface IView { 2 | renderPage(pageName: string, opts: {}): void; 3 | 4 | pushPage(pageName: string, opts: {}): void; 5 | } 6 | -------------------------------------------------------------------------------- /packages/portal/frontend/src/app/views/classy/ClassyAdminView.ts: -------------------------------------------------------------------------------- 1 | import Log from "@common/Log"; 2 | 3 | import { AdminTabs, AdminView } from "../AdminView"; 4 | 5 | declare var ons: any; 6 | 7 | /** 8 | * This is a test implementation of the Classy admin features. 9 | */ 10 | export class ClassyAdminView extends AdminView { 11 | constructor(remoteUrl: string, tabs: AdminTabs) { 12 | Log.info("ClassyAdminView::(..)"); 13 | super(remoteUrl, tabs); 14 | } 15 | 16 | public renderPage(name: string, opts: any) { 17 | Log.info("ClassyAdminView::renderPage( " + name + ", ... ) - start; options: " + JSON.stringify(opts)); 18 | super.renderPage(name, opts); 19 | 20 | if (name === "AdminRoot") { 21 | Log.info("ClassyAdminView::renderPage(..) - augmenting tabs"); 22 | 23 | // this does not seem to work; it creates the tab on the menu, but it is not clickable 24 | // const tab = document.createElement("ons-tab"); 25 | // tab.setAttribute("page", "dashboard.html"); 26 | // tab.setAttribute("label", "Foo"); 27 | // tab.setAttribute("active", "true"); 28 | // tab.setAttribute("icon", "ion-ios-gear"); 29 | // tab.setAttribute("class", "tabbar__item tabbar--top__item"); 30 | // tab.setAttribute("modifier", "top"); 31 | // const tabbar = document.getElementById("adminTabbar"); 32 | // tabbar.children[1].appendChild(tab); 33 | 34 | Log.info("ClassyAdminView::renderPage(..) - augmenting tabs done."); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/portal/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES6", 5 | "noImplicitAny": true, 6 | "removeComments": true, 7 | "preserveConstEnums": true, 8 | "sourceMap": true, 9 | // only the frontend plugin code should use @frontend and @common 10 | // unless we adopt tsconfig-paths (to resolve these references at runtime) 11 | // the frontend does this with tsconfig-paths-webpack-plugin 12 | // TODO: refactor the type cycle that requires the @backend reference here 13 | "baseUrl": ".", 14 | "paths": { 15 | "@frontend/*": [ 16 | "../../portal/frontend/src/app/*" 17 | ], 18 | "@backend/*": [ 19 | "../../portal/backend/src/*" 20 | ], 21 | "@common/*": [ 22 | "../../common/src/*" 23 | ] 24 | }, 25 | "lib": [ 26 | "es7", 27 | "es2017.object", 28 | "dom" 29 | ], 30 | "typeRoots": [ 31 | "../../../node_modules/@types" 32 | ] 33 | }, 34 | "include": [ 35 | "src/**/*.ts", 36 | "test/**/*.ts", 37 | "../common/**/*.ts" 38 | ], 39 | "exclude": [ 40 | "node_modules" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /packages/portal/frontend/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | // read the env so we can copy custom resources (if needed) 4 | require("dotenv").config( 5 | {path: "../../../.env"} 6 | ); 7 | 8 | // copy plugin files so they are available to frontend 9 | const CopyPlugin = require("copy-webpack-plugin"); 10 | 11 | // handle @frontend and @common type import aliases from plugin 12 | const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); 13 | const { webpack, DefinePlugin } = require("webpack"); 14 | 15 | /** 16 | * Checks if plugin enabled in .env. Assume there _must_ be custom html as well. 17 | * 18 | * @returns {boolean} 19 | */ 20 | const pluginExists = () => { 21 | return process.env.PLUGIN ? true : false; 22 | } 23 | 24 | console.log("Preparing frontend for: " + process.env.NAME); 25 | 26 | 27 | if (process.env.PLUGIN !== "default") { 28 | console.log("Loading plugin: " + process.env.PLUGIN); 29 | } else { 30 | console.log("Loading Classy defaults..."); 31 | } 32 | 33 | module.exports = { 34 | 35 | // https://webpack.js.org/concepts/mode/ 36 | mode: "development", 37 | 38 | plugins: [ 39 | new DefinePlugin({ 40 | "process.env.LOG_LEVEL": JSON.stringify(process.env.LOG_LEVEL) || JSON.stringify("INFO") 41 | }), 42 | new CopyPlugin({ 43 | patterns: [ 44 | // Copy plugin frontend files if plugin enabled or copy default Classy logic into place 45 | // Docker and native compilation working dir: /classy/packages/portal/frontend 46 | // frontend/CustomStudentView.ts, CustomAdminView.ts, with their supporting files, 47 | // will be moved to their appropriate directories 48 | // 49 | // Course-specific plugins should be in classy/plugins/process.env.PLUGIN 50 | // When run, plugin will be copied to classy/packages/portal/frontend/app/plugs/ 51 | { 52 | from: "../../../plugins/" + process.env.PLUGIN + "/portal/frontend/", 53 | to: "../../src/app/plugs/", 54 | toType: "dir", 55 | force: true, 56 | noErrorOnMissing: false, 57 | force: true 58 | }, 59 | { 60 | from: "../../../plugins/" + process.env.PLUGIN + "/portal/frontend/html", 61 | // to: "../html/" + process.env.NAME, // puts it in ./html/{name} 62 | to: "../" + process.env.NAME, 63 | toType: "dir", 64 | noErrorOnMissing: false, 65 | force: true 66 | } 67 | ], 68 | }), 69 | ], 70 | 71 | entry: { 72 | portal: "./src/app/App.ts" 73 | }, 74 | 75 | output: { 76 | path: path.resolve(__dirname, "./html/js/"), 77 | publicPath: path.resolve(__dirname, "./html/js/"), 78 | filename: "portal.js" 79 | }, 80 | 81 | // Enable sourcemaps for debugging webpack"s output. 82 | devtool: "source-map", 83 | 84 | resolve: { 85 | // Add ".ts" and ".tsx" as resolvable extensions. 86 | extensions: [".ts", ".tsx", ".js", ".json"], 87 | plugins: [new TsconfigPathsPlugin({ 88 | configFile: "tsconfig.json" 89 | })] 90 | }, 91 | 92 | performance: { 93 | hints: false 94 | }, 95 | 96 | module: { 97 | rules: [ 98 | // All files with a ".ts" or ".tsx" extension will be handled by "ts-loader" or awesome-typescript-loader". 99 | {test: /\.tsx?$/, loader: "ts-loader"}, 100 | 101 | // All output ".js" files will have any sourcemaps re-processed by "source-map-loader". 102 | { 103 | enforce: "pre", 104 | test: /\.js$/, 105 | loader: "source-map-loader", 106 | exclude: [] 107 | } 108 | ] 109 | } 110 | }; 111 | -------------------------------------------------------------------------------- /packages/proxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.22-alpine 2 | 3 | # This is essentially just the official NGINX 4 | # image execpt we add a step to compile template 5 | # configuration files. The default version of NGINX 6 | # does not support env vars so we use the templates 7 | # to get the env var values into static configuration 8 | # files. Templates are processed by ERB (Embedded RuBy). 9 | 10 | # Specify any ENV VARs used in any of the *.rconf 11 | # configuration template files. These values need to 12 | # be available at build time since that is when the 13 | # template gets processed. 14 | 15 | # The static config produced from the template is shown 16 | # in the container's build output: it's a good idea to 17 | # check that all the <%= ENV["VAR_NAME"] %> tags have 18 | # been substituted. 19 | 20 | ARG UID 21 | ARG SSL_CERT_PATH 22 | ARG SSL_KEY_PATH 23 | ARG BACKEND_PORT 24 | ARG UI_PORT 25 | 26 | EXPOSE 8080 27 | EXPOSE 8443 28 | 29 | # TODO @ncbradley Use envsubst instead of ruby (unless we need more powerful templates) 30 | RUN apk add --no-cache ruby 31 | 32 | # Change permissions so we aren't running as root: http://pjdietz.com/2016/08/28/nginx-in-docker-without-root.html 33 | RUN touch /var/run/nginx.pid && \ 34 | chown -R ${UID} /var/run/nginx.pid && \ 35 | chown -R ${UID} /var/cache/nginx 36 | 37 | WORKDIR /etc/nginx 38 | COPY packages/proxy/proxy.conf packages/proxy/nginx.rconf* ./ 39 | RUN if test -e nginx.rconf; then erb nginx.rconf; else erb nginx.rconf.default; fi | tee nginx.conf \ 40 | && rm nginx.rconf* \ 41 | && apk del ruby 42 | 43 | USER ${UID} 44 | -------------------------------------------------------------------------------- /packages/proxy/README.md: -------------------------------------------------------------------------------- 1 | # Classy Router 2 | 3 | This service acts as an ingress controller, routing requests to the other services. 4 | In the [NGINX Microservices Architecture](https://www.nginx.com/blog/tag/nginx-microservices-reference-architecture/), this would be the (reverse) proxy in the [Proxy Model](https://www.nginx.com/blog/microservices-reference-architecture-nginx-proxy-model). 5 | 6 | # Notes 7 | The `nginx.rcof` is an embedded ruby (ERB) file that is processed when the Docker image is built. 8 | It is essentially a template with tags containing Ruby snippets that get processed by the _erb_ utility. 9 | This templating approach was used to allow build-time configuration via environment variables since nginx does not really support using environment variables directly. 10 | Before using the template approach, I tried the (perl) method suggested [here](https://hackerbox.io/articles/dockerised-nginx-env-vars/) but nginx does not expand variables in many of the directives (including `server_name` and `proxy_pass`) so this method won't work. 11 | 12 | Currently, the ports for the other services are fixed in the nginx configuration (and therefore must match the exposed ports in the Dockerfiles). 13 | -------------------------------------------------------------------------------- /packages/proxy/nginx.rconf.default: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 4096; ## Default: 1024 3 | } 4 | 5 | http { 6 | include /etc/nginx/proxy.conf; 7 | 8 | # Use simplified logging to better match standard Classy logs 9 | log_format classy '$time_local; $remote_addr: $request $status'; 10 | access_log /dev/stdout classy; 11 | 12 | server { 13 | listen 8080 default_server; 14 | listen [::]:8080; 15 | 16 | return 301 https://$host$request_uri; 17 | } 18 | server { 19 | listen 8443 default_server ssl; 20 | 21 | ssl_session_timeout 1d; 22 | ssl_session_cache shared:MozSSL:10m; # about 40000 sessions 23 | ssl_session_tickets off; 24 | 25 | ssl_protocols TLSv1.2 TLSv1.3; 26 | ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; 27 | ssl_prefer_server_ciphers off; 28 | 29 | # HSTS (ngx_http_headers_module is required) (63072000 seconds) 30 | add_header Strict-Transport-Security "max-age=63072000" always; 31 | 32 | # OCSP stapling 33 | ssl_stapling on; 34 | ssl_stapling_verify on; 35 | 36 | ssl_certificate <%= ENV["SSL_CERT_PATH"] %>; 37 | ssl_certificate_key <%= ENV["SSL_KEY_PATH"] %>; 38 | 39 | resolver 127.0.0.1; 40 | 41 | # pass requests to the portal service (which is automatically defined in the hosts file by docker) 42 | location / { 43 | # kill cache (https://stackoverflow.com/a/45285696) 44 | add_header Last-Modified $date_gmt; 45 | add_header Cache-Control 'no-store, no-cache'; 46 | if_modified_since off; 47 | expires off; 48 | etag off; 49 | proxy_pass "https://portal:<%= ENV["BACKEND_PORT"] %>/"; 50 | } 51 | 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/proxy/proxy.conf: -------------------------------------------------------------------------------- 1 | proxy_redirect off; 2 | proxy_set_header Host $host; 3 | proxy_set_header X-Real-IP $remote_addr; 4 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 5 | client_max_body_size 10m; 6 | client_body_buffer_size 128k; 7 | proxy_connect_timeout 90; 8 | proxy_send_timeout 90; 9 | proxy_read_timeout 90; 10 | # buffering needs to be off to support streaming for Docker image creation 11 | proxy_buffering off; 12 | -------------------------------------------------------------------------------- /plugins/default/docker/docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | # PLUGIN OVERRIDE NOTE: 2 | # This file is exactly the same as https://github.com/ubccpsc/classy/blob/master/docker-compose.yml 3 | 4 | # You can choose to override existing services in this file, by making changes to the service in this file, 5 | # or you can introduce new services here. 6 | 7 | version: "3.5" 8 | 9 | services: 10 | autotest: 11 | build: 12 | context: ./ 13 | dockerfile: ./packages/autotest/Dockerfile 14 | container_name: autotest 15 | depends_on: 16 | - db 17 | env_file: .env 18 | expose: 19 | - ${AUTOTEST_PORT} 20 | restart: always 21 | user: "${UID}:${GID}" 22 | volumes: 23 | - "${HOST_DIR}:${PERSIST_DIR}" 24 | - "/var/run/docker.sock:/var/run/docker.sock" 25 | db: 26 | command: --quiet --slowms 250 27 | container_name: db 28 | environment: 29 | - MONGO_INITDB_ROOT_USERNAME 30 | - MONGO_INITDB_ROOT_PASSWORD 31 | ports: 32 | - "27017:27017" 33 | image: mongo:5.0.14 34 | restart: always 35 | user: "${UID}" 36 | volumes: 37 | - /var/opt/classy/db:/data/db 38 | portal: 39 | build: 40 | args: 41 | - GH_BOT_USERNAME 42 | - GH_BOT_EMAIL 43 | - PLUGIN 44 | context: ./ 45 | dockerfile: ./packages/portal/Dockerfile 46 | container_name: portal 47 | depends_on: 48 | - db 49 | - autotest 50 | env_file: .env 51 | expose: 52 | - ${BACKEND_PORT} 53 | restart: always 54 | user: "${UID}" 55 | volumes: 56 | - "${HOST_SSL_CERT_PATH}:${SSL_CERT_PATH}" 57 | - "${HOST_SSL_KEY_PATH}:${SSL_KEY_PATH}" 58 | - "${HOST_DIR}:${PERSIST_DIR}:ro" 59 | proxy: 60 | build: 61 | args: 62 | - UID 63 | - SSL_CERT_PATH 64 | - SSL_KEY_PATH 65 | - BACKEND_PORT 66 | context: ./ 67 | dockerfile: ./packages/proxy/Dockerfile 68 | container_name: proxy 69 | depends_on: 70 | - portal 71 | ports: 72 | - "80:8080" 73 | - "443:8443" 74 | restart: always 75 | user: "${UID}" 76 | volumes: 77 | - "${HOST_SSL_CERT_PATH}:${SSL_CERT_PATH}" 78 | - "${HOST_SSL_KEY_PATH}:${SSL_KEY_PATH}" 79 | 80 | -------------------------------------------------------------------------------- /plugins/default/nginx/nginx.rconf: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 4096; ## Default: 1024 3 | } 4 | 5 | http { 6 | include /etc/nginx/proxy.conf; 7 | 8 | server { 9 | listen 8080 default_server; 10 | listen [::]:8080; 11 | 12 | return 301 https://$host$request_uri; 13 | } 14 | server { 15 | listen 8443 default_server ssl; 16 | 17 | ssl_session_timeout 1d; 18 | ssl_session_cache shared:MozSSL:10m; # about 40000 sessions 19 | ssl_session_tickets off; 20 | 21 | ssl_protocols TLSv1.2 TLSv1.3; 22 | ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; 23 | ssl_prefer_server_ciphers off; 24 | 25 | # HSTS (ngx_http_headers_module is required) (63072000 seconds) 26 | add_header Strict-Transport-Security "max-age=63072000" always; 27 | 28 | # OCSP stapling 29 | ssl_stapling on; 30 | ssl_stapling_verify on; 31 | 32 | ssl_certificate <%= ENV["SSL_CERT_PATH"] %>; 33 | ssl_certificate_key <%= ENV["SSL_KEY_PATH"] %>; 34 | 35 | resolver 127.0.0.1; 36 | 37 | # pass requests to the portal service (which is automatically defined in the hosts file by docker) 38 | location / { 39 | proxy_pass "https://portal:<%= ENV["BACKEND_PORT"] %>/"; 40 | } 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /plugins/default/portal/backend/CustomCourseController.ts: -------------------------------------------------------------------------------- 1 | import {CourseController} from "@backend/controllers/CourseController"; 2 | import {IGitHubController} from "@backend/controllers/GitHubController"; 3 | import {Deliverable, Person} from "@backend/Types"; 4 | import Log from "@common/Log"; 5 | export class DefaultCourseController extends CourseController { 6 | 7 | constructor(ghController: IGitHubController) { 8 | Log.trace("DefaultCourseController::"); 9 | super(ghController); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /plugins/default/portal/backend/CustomCourseRoutes.ts: -------------------------------------------------------------------------------- 1 | import * as restify from "restify"; 2 | 3 | import IREST from "@backend/server/IREST"; 4 | 5 | import Log from "@common/Log"; 6 | 7 | /** 8 | * This class should add any custom routes a course might need. 9 | * 10 | * Nothing should be added to this class. 11 | */ 12 | export default class DefaultCourseRoutes implements IREST { 13 | public registerRoutes(server: restify.Server) { 14 | Log.trace('DefaultCourseRoutes::registerRoutes()'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /plugins/default/portal/frontend/CustomAdminView.ts: -------------------------------------------------------------------------------- 1 | import Log from "@common/Log"; 2 | 3 | import {AdminTabs, AdminView} from "@frontend/views/AdminView"; 4 | 5 | /** 6 | * Stock Default Admin view 7 | */ 8 | export class DefaultAdminView extends AdminView { 9 | constructor(remoteUrl: string, tabs: AdminTabs) { 10 | Log.info("CustomAdminView::(..)"); 11 | super(remoteUrl, tabs); 12 | } 13 | 14 | public renderPage(name: string, opts: any) { 15 | Log.info('CustomAdminView::renderPage( ' + name + ', ... ) - start; options: ' + JSON.stringify(opts)); 16 | super.renderPage(name, opts); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /plugins/default/portal/frontend/html/custom.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | 21 | 24 | 25 | 79 | -------------------------------------------------------------------------------- /plugins/default/portal/frontend/html/landing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
UBC CPSC Classy Portal
4 | 5 |
6 | 7 | 8 | Logout 9 | 10 |
11 |
12 | 13 |
14 | 15 |
16 |
17 |

18 | This is the Classy management portal. 19 |

20 |
21 | 23 | 24 | Portal Login 25 | 26 |
27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /plugins/default/portal/frontend/html/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
GitHub Verification
4 |
5 |
6 | 7 |
8 |
9 | We use GitHub to authenticate your user session. This is required as all course resources are provisioned through 10 | the GitHub platform. We do not store any of your personal GitHub details; we only verify and store your GitHub 11 | username. Any repositories we provision will be within a private organization and will not appear on your public 12 | GitHub profile. 13 |
14 | 15 | 16 | Verify GitHub Credentials 17 | 18 |
19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /plugins/default/portal/frontend/html/student.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
Classy Student Portal
4 |
5 | 6 | 7 | Logout 8 | 9 |
10 |
11 | 12 | 13 | Grades 14 | 15 |
16 |
17 | Repositories 18 | 19 |
20 |
21 |
22 | Select Project Partner 23 | 24 |
25 | 26 | Specify project partner CWL: 27 |
28 |
29 | 30 | Create Team 31 |
32 |
33 | Specify your project partner here. They must be registered in your lab section and must not be on any other team. Please 34 | note: this will be your partner for the duration of the term and cannot be changed so make your choice carefully. Only 35 | one team member needs to do this. 36 |
37 |
38 |
39 | 40 |
41 | 42 | Specify deliverable: 43 |
44 |
45 | 46 | 47 |
48 |
49 | Select the deliverable that you would like to form a team for. Only deliverables configured for student team formation are listed. 50 |
51 |
52 |
53 | Current Teams 54 | 55 | You currently are not on any teams. 56 | 57 |
58 |
59 | 60 |
61 | -------------------------------------------------------------------------------- /plugins/example/docker/docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | # PLUGIN OVERRIDE NOTE: 2 | # This file is exactly the same as https://github.com/ubccpsc/classy/blob/master/docker-compose.yml 3 | 4 | # You can choose to override existing services in this file, by making changes to the service in this file, 5 | # or you can introduce new services here. 6 | 7 | version: "3.5" 8 | 9 | services: 10 | autotest: 11 | build: 12 | context: ./ 13 | dockerfile: ./packages/autotest/Dockerfile 14 | container_name: autotest 15 | depends_on: 16 | - db 17 | env_file: .env 18 | expose: 19 | - ${AUTOTEST_PORT} 20 | restart: always 21 | user: "${UID}:${GID}" 22 | volumes: 23 | - "${HOST_DIR}:${PERSIST_DIR}" 24 | - "/var/run/docker.sock:/var/run/docker.sock" 25 | db: 26 | command: --quiet --slowms 250 27 | container_name: db 28 | environment: 29 | - MONGO_INITDB_ROOT_USERNAME 30 | - MONGO_INITDB_ROOT_PASSWORD 31 | ports: 32 | - "27017:27017" 33 | image: mongo:5.0.14 34 | restart: always 35 | user: "${UID}" 36 | volumes: 37 | - /var/opt/classy/db:/data/db 38 | portal: 39 | build: 40 | args: 41 | - GH_BOT_USERNAME 42 | - GH_BOT_EMAIL 43 | - PLUGIN 44 | context: ./ 45 | dockerfile: ./packages/portal/Dockerfile 46 | container_name: portal 47 | depends_on: 48 | - db 49 | - autotest 50 | env_file: .env 51 | expose: 52 | - ${BACKEND_PORT} 53 | restart: always 54 | user: "${UID}" 55 | volumes: 56 | - "${HOST_SSL_CERT_PATH}:${SSL_CERT_PATH}" 57 | - "${HOST_SSL_KEY_PATH}:${SSL_KEY_PATH}" 58 | - "${HOST_DIR}:${PERSIST_DIR}:ro" 59 | proxy: 60 | build: 61 | args: 62 | - UID 63 | - SSL_CERT_PATH 64 | - SSL_KEY_PATH 65 | - BACKEND_PORT 66 | context: ./ 67 | dockerfile: ./packages/proxy/Dockerfile 68 | container_name: proxy 69 | depends_on: 70 | - portal 71 | ports: 72 | - "80:8080" 73 | - "443:8443" 74 | restart: always 75 | user: "${UID}" 76 | volumes: 77 | - "${HOST_SSL_CERT_PATH}:${SSL_CERT_PATH}" 78 | - "${HOST_SSL_KEY_PATH}:${SSL_KEY_PATH}" 79 | helloworld: 80 | env_file: .env 81 | build: 82 | context: ./ 83 | dockerfile: ./plugins/${PLUGIN}/helloworld/Dockerfile 84 | container_name: helloworld 85 | ports: 86 | - "3001:3001" 87 | depends_on: 88 | - portal 89 | restart: always 90 | user: "${UID}" 91 | -------------------------------------------------------------------------------- /plugins/example/helloworld/Dockerfile: -------------------------------------------------------------------------------- 1 | ## This can be any Docker image library. Does not have to be Node JS based. 2 | FROM node:18-alpine 3 | 4 | ARG PLUGIN=example 5 | 6 | WORKDIR /app 7 | 8 | COPY ./plugins/"${PLUGIN}"/helloworld ./packages/helloworld 9 | 10 | RUN cd ./packages/helloworld && npm install 11 | 12 | ## Port only discoverable by Docker services 13 | EXPOSE 3001 14 | 15 | CMD ["node", "/app/packages/helloworld/serve_json.js"] 16 | -------------------------------------------------------------------------------- /plugins/example/helloworld/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "helloworld", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "serve_json.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC" 11 | } 12 | -------------------------------------------------------------------------------- /plugins/example/helloworld/serve_json.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | console.log('Starting serve_json.js...'); 4 | 5 | //create a server object: 6 | http.createServer(function (req, res) { 7 | console.log('Request made'); 8 | res.setHeader('Content-Type', 'application/json'); 9 | res.end(JSON.stringify( 10 | [ 11 | 'This data', 12 | 'from the', 13 | 'HelloWorld! service', 14 | 'should appear on the front-end', 15 | ] 16 | )); 17 | }).listen(3001); //the server object listens on port 3001 18 | -------------------------------------------------------------------------------- /plugins/example/nginx/nginx.rconf: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 4096; ## Default: 1024 3 | } 4 | 5 | http { 6 | include /etc/nginx/proxy.conf; 7 | 8 | server { 9 | listen 8080 default_server; 10 | listen [::]:8080; 11 | 12 | return 301 https://$host$request_uri; 13 | } 14 | server { 15 | listen 8443 default_server ssl; 16 | 17 | ssl_session_timeout 1d; 18 | ssl_session_cache shared:MozSSL:10m; # about 40000 sessions 19 | ssl_session_tickets off; 20 | 21 | ssl_protocols TLSv1.2 TLSv1.3; 22 | ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; 23 | ssl_prefer_server_ciphers off; 24 | 25 | # HSTS (ngx_http_headers_module is required) (63072000 seconds) 26 | add_header Strict-Transport-Security "max-age=63072000" always; 27 | 28 | # OCSP stapling 29 | ssl_stapling on; 30 | ssl_stapling_verify on; 31 | 32 | ssl_certificate <%= ENV["SSL_CERT_PATH"] %>; 33 | ssl_certificate_key <%= ENV["SSL_KEY_PATH"] %>; 34 | 35 | resolver 127.0.0.1; 36 | 37 | # pass requests to the portal service (which is automatically defined in the hosts file by docker) 38 | location / { 39 | proxy_pass "https://portal:<%= ENV["BACKEND_PORT"] %>/"; 40 | } 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /plugins/example/portal/backend/CustomCourseController.ts: -------------------------------------------------------------------------------- 1 | import {CourseController} from "@backend/controllers/CourseController"; 2 | import {IGitHubController} from "@backend/controllers/GitHubController"; 3 | import {Deliverable, Person} from "@backend/Types"; 4 | 5 | import Log from "@common/Log"; 6 | 7 | import * as restify from "restify"; 8 | 9 | import fetch from 'node-fetch'; 10 | export class CustomCourseController extends CourseController { 11 | 12 | constructor(ghController: IGitHubController) { 13 | Log.trace("DefaultCourseController::"); 14 | super(ghController); 15 | } 16 | 17 | /** 18 | * Relays JSON data from your HelloWorld! Docker service to be consumed by front-end. 19 | * @param req 20 | * @param res 21 | * @param next 22 | */ 23 | public static getHelloWorldData(req: restify.Request, res: restify.Response, next: restify.Next) { 24 | fetch('http://helloworld:3001') 25 | .then((response) => { 26 | return response.json(); 27 | }) 28 | .then((data) => { 29 | res.send({success: {helloWorldData: data}}); 30 | }) 31 | .catch((err) => { 32 | // Careful not to send sensitive data in error 33 | // Likely want to create error handler 34 | res.send(err); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /plugins/example/portal/backend/CustomCourseRoutes.ts: -------------------------------------------------------------------------------- 1 | import * as restify from "restify"; 2 | import fetch from "node-fetch"; 3 | 4 | import Log from "@common/Log"; 5 | 6 | import IREST from "@backend/server/IREST"; 7 | 8 | import {CustomCourseController} from "./CustomCourseController"; 9 | 10 | /** 11 | * This class should add any custom routes a course might need. 12 | * 13 | * Nothing should be added to this class. 14 | */ 15 | 16 | export default class CustomCourseRoutes implements IREST { 17 | 18 | public registerRoutes(server: restify.Server) { 19 | Log.trace('CustomCourseRoutes::registerRoutes()'); 20 | 21 | // Create or import auth middleware where necessary (examples in AuthRoutes.ts, GeneralRoutes.ts) 22 | server.get('/portal/custom/helloWorld', CustomCourseController.getHelloWorldData); 23 | 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /plugins/example/portal/frontend/CustomAdminView.ts: -------------------------------------------------------------------------------- 1 | import Log from "@common/Log"; 2 | 3 | import {AdminTabs, AdminView} from "@frontend/views/AdminView"; 4 | 5 | /** 6 | * Stock Default Admin view 7 | */ 8 | export class DefaultAdminView extends AdminView { 9 | constructor(remoteUrl: string, tabs: AdminTabs) { 10 | Log.info("CustomAdminView::(..)"); 11 | super(remoteUrl, tabs); 12 | } 13 | 14 | public renderPage(name: string, opts: any) { 15 | Log.info('CustomAdminView::renderPage( ' + name + ', ... ) - start; options: ' + JSON.stringify(opts)); 16 | super.renderPage(name, opts); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /plugins/example/portal/frontend/html/custom.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | 21 | 24 | 25 | 79 | -------------------------------------------------------------------------------- /plugins/example/portal/frontend/html/landing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
UBC CPSC Classy Portal
4 | 5 |
6 | 7 | 8 | Logout 9 | 10 |
11 |
12 | 13 |
14 | 15 |
16 |
17 |

18 | This is the Classy management portal. 19 |

20 |
21 | 23 | 24 | Portal Login 25 | 26 |
27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /plugins/example/portal/frontend/html/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
GitHub Verification
4 |
5 |
6 | 7 |
8 |
9 | We use GitHub to authenticate your user session. This is required as all course resources are provisioned through 10 | the GitHub platform. We do not store any of your personal GitHub details; we only verify and store your GitHub 11 | username. Any repositories we provision will be within a private organization and will not appear on your public 12 | GitHub profile. 13 |
14 | 15 | 16 | Verify GitHub Credentials 17 | 18 |
19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /plugins/example/portal/frontend/html/student.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
Classy Student Portal
4 |
5 | 6 | 7 | Logout 8 | 9 |
10 |
11 | 12 | 13 |
14 | HelloWorld! Service New Feature 15 |
16 |
17 | 18 | JSON data from classy/plugins/example/helloworld/serve_json.js 19 | 20 | 21 |
22 |
23 | 24 | 25 | Grades 26 | 27 |
28 |
29 | Repositories 30 | 31 |
32 |
33 |
34 | Select Project Partner 35 | 36 |
37 | 38 | Specify project partner CWL: 39 |
40 |
41 | 42 | Create Team 43 |
44 |
45 | Specify your project partner here. They must be registered in your lab section and must not be on any other team. Please 46 | note: this will be your partner for the duration of the term and cannot be changed so make your choice carefully. Only 47 | one team member needs to do this. 48 |
49 |
50 |
51 | 52 |
53 | 54 | Specify deliverable: 55 |
56 |
57 | 58 | 59 |
60 |
61 | Select the deliverable that you would like to form a team for. Only deliverables configured for student team formation are listed. 62 |
63 |
64 |
65 | Current Teams 66 | 67 | You currently are not on any teams. 68 | 69 |
70 |
71 | 72 |
73 | -------------------------------------------------------------------------------- /testOutput/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Classy CI Artifacts 4 | 5 | 6 | 7 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES6", 5 | "noImplicitAny": true, 6 | "removeComments": true, 7 | "preserveConstEnums": true, 8 | "sourceMap": true, 9 | "baseUrl": ".", 10 | "paths": { 11 | "@common/*": [ 12 | "packages/common/src/*", 13 | "packages/common/test/*" 14 | ], 15 | "@frontend/*": [ 16 | "packages/portal/frontend/src/app/*" 17 | ], 18 | "@backend/*": [ 19 | "packages/portal/backend/src/*" 20 | ], 21 | "@autotest/*": [ 22 | "packages/autotest/src/*" 23 | ] 24 | }, 25 | "lib": [ 26 | "es7", 27 | "es2017.object", 28 | "es2018.AsyncIterable", 29 | "dom" 30 | ], 31 | "typeRoots": [ 32 | "./node_modules/@types" 33 | ] 34 | }, 35 | "exclude": [ 36 | "node_modules" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "indent": [ 5 | true, 6 | "tabs", 7 | 4 8 | ], 9 | "interface-name": [ 10 | false, 11 | "never-prefix" 12 | ], 13 | "max-line-length": [ 14 | true, 15 | 140 16 | ], 17 | "member-ordering": [ 18 | false, 19 | { 20 | "order": "fields-first" 21 | } 22 | ], 23 | "no-floating-promises": true, 24 | "no-string-literal": false, 25 | "no-unused-expression": false, 26 | "object-literal-shorthand": false, 27 | "object-literal-sort-keys": false, 28 | "only-arrow-functions": false, 29 | "ordered-imports": false, 30 | "quotemark": false, 31 | "space-before-function-paren": [ 32 | "error", 33 | { 34 | "anonymous": "always", 35 | "named": "never", 36 | "asyncArrow": "always" 37 | } 38 | ], 39 | "trailing-comma": "never", 40 | "variable-name": { 41 | "options": [ 42 | "ban-keywords", 43 | "check-format", 44 | "allow-pascal-case", 45 | "allow-leading-underscore" 46 | ] 47 | } 48 | } 49 | } 50 | --------------------------------------------------------------------------------