= (props) => {
14 | const {
15 | show,
16 | hasNext,
17 | hasPrev,
18 | next,
19 | prev,
20 | } = props;
21 | return (
22 |
25 |
34 |
43 |
44 | );
45 | };
46 | PaginationArrows.displayName = 'PaginationArrows';
47 |
48 | export default PaginationArrows;
49 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/components/Part.css:
--------------------------------------------------------------------------------
1 | .bodyitem:not(.HTML, .CodeSnippet):hover {
2 | background-color: #eeeeee;
3 | }
4 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/components/Tooltip.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Tooltip as BSTooltip,
4 | OverlayTrigger,
5 | OverlayTriggerProps,
6 | } from 'react-bootstrap';
7 |
8 | export interface TooltipProps {
9 | message: string;
10 | className?: string;
11 | showTooltip?: boolean;
12 | defaultShow?: boolean;
13 | placement?: OverlayTriggerProps['placement'];
14 | children: React.ReactElement;
15 | }
16 |
17 | const Tooltip: React.FC = (props) => {
18 | const {
19 | message,
20 | className,
21 | showTooltip = true,
22 | defaultShow,
23 | placement = 'bottom',
24 | children,
25 | } = props;
26 | const tooltip = showTooltip
27 | ? (
28 |
33 | {message}
34 |
35 | )
36 | : ((): JSX.Element => );
37 | return (
38 |
43 | {children}
44 |
45 | );
46 | };
47 |
48 | export default Tooltip;
49 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/components/navbar/Scratch.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Form } from 'react-bootstrap';
3 |
4 | interface ScratchProps {
5 | value: string;
6 | onChange?: (newVal: string) => void;
7 | disabled?: boolean;
8 | }
9 |
10 | const Scratch: React.FC = (props) => {
11 | const {
12 | value,
13 | onChange,
14 | disabled = false,
15 | } = props;
16 | return (
17 | {
21 | onChange(event.target.value);
22 | }}
23 | as="textarea"
24 | spellCheck={false}
25 | disabled={disabled}
26 | />
27 | );
28 | };
29 |
30 | export default Scratch;
31 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/components/navbar/index.css:
--------------------------------------------------------------------------------
1 | .sidebar-small {
2 | width: var(--sidebar-small);
3 | }
4 |
5 | .sidebar-expanded {
6 | width: var(--sidebar-expanded);
7 | }
8 |
9 | .collapse:not(.width):not(.show) {
10 | display: none;
11 | }
12 |
13 | .collapsing:not(.width) {
14 | position: relative;
15 | height: 0;
16 | /* overflow: hidden; */
17 | transition: height 0.35s ease-in-out;
18 | }
19 |
20 | @media (prefers-reduced-motion: reduce) {
21 | .collapsing {
22 | transition: none;
23 | }
24 | }
25 |
26 | .collapse.width, .collapsing.width {
27 | white-space: nowrap;
28 | /* overflow: hidden; */
29 | display: inline-block;
30 | height: unset; /* undoes bootstrap .collapsing height */
31 | transition: 0.35s width ease-in-out;
32 | }
33 |
34 | .collapse.width:not(.show) {
35 | display: none;
36 | height: unset;
37 | }
38 | .collapsing.width {
39 | position: relative;
40 | height: unset; /* undoes bootstrap .collapsing height */
41 | width: 0;
42 | }
43 |
44 | .collapse.width.show {
45 | width: calc(var(--sidebar-expanded) - 7em);
46 | }
47 |
48 |
49 | .blue-glow {
50 | /* a somewhat-transparent lightblue */
51 | background: radial-gradient(closest-side, #add8e688 40%,transparent);
52 | }
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/components/questions/Text.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Row, Col, Form,
4 | } from 'react-bootstrap';
5 | import { TextInfo, TextState } from '@student/exams/show/types';
6 | import HTML from '@student/exams/show/components/HTML';
7 |
8 | interface TextProps {
9 | info: TextInfo;
10 | value: TextState;
11 | onChange: (newVal: TextState) => void;
12 | disabled: boolean;
13 | }
14 |
15 | const Text: React.FC = (props) => {
16 | const {
17 | info,
18 | value,
19 | onChange,
20 | disabled,
21 | } = props;
22 | const { prompt } = info;
23 | return (
24 | <>
25 |
26 |
27 |
28 | {
36 | const elem = e.target as HTMLTextAreaElement;
37 | onChange(elem.value);
38 | }}
39 | />
40 |
41 |
42 | >
43 | );
44 | };
45 |
46 | export default Text;
47 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/containers/ExamShowContents.ts:
--------------------------------------------------------------------------------
1 | import ExamShowContents from '@student/exams/show/components/ExamShowContents';
2 | import { connect } from 'react-redux';
3 | import { saveSnapshot, submitExam } from '@student/exams/show/actions';
4 | import {
5 | ExamVersion,
6 | MSTP,
7 | MDTP,
8 | } from '@student/exams/show/types';
9 |
10 | interface OwnProps {
11 | examTakeUrl: string;
12 | }
13 |
14 | const mapStateToProps: MSTP<{exam: ExamVersion}, OwnProps> = (state) => ({
15 | exam: state.contents.exam,
16 | });
17 |
18 | const mapDispatchToProps: MDTP<{
19 | save: () => void;
20 | submit: (cleanup: () => void) => void;
21 | }, OwnProps> = (dispatch, ownProps) => ({
22 | save: (): void => {
23 | dispatch(saveSnapshot(ownProps.examTakeUrl));
24 | },
25 | submit: (cleanup: () => void): void => {
26 | dispatch(submitExam(ownProps.examTakeUrl, cleanup));
27 | },
28 | });
29 |
30 | export default connect(mapStateToProps, mapDispatchToProps)(ExamShowContents);
31 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/containers/ExamTaker.tsx:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import {
3 | LockdownStatus,
4 | MSTP,
5 | } from '@student/exams/show/types';
6 | import ExamTaker from '@student/exams/show/components/ExamTaker';
7 |
8 | const examTakerStateToProps: MSTP<{ ready: boolean }> = (state) => ({
9 | ready: (state.lockdown.status === LockdownStatus.LOCKED
10 | || state.lockdown.status === LockdownStatus.IGNORED)
11 | && !!state.lockdown.loaded,
12 | });
13 |
14 | export default connect(examTakerStateToProps)(ExamTaker);
15 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/containers/LockdownInfo.tsx:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { MSTP, LockdownStatus } from '@student/exams/show/types';
3 | import LockdownInfo from '@student/exams/show/components/LockdownInfo';
4 |
5 | const mapStateToProps: MSTP<{ status: LockdownStatus; message: string }> = (state) => {
6 | const { lockdown } = state;
7 | const { status, message } = lockdown;
8 | return {
9 | status,
10 | message,
11 | };
12 | };
13 |
14 | export default connect(mapStateToProps)(LockdownInfo);
15 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/containers/PaginationArrows.ts:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import {
3 | nextQuestion,
4 | prevQuestion,
5 | } from '@student/exams/show/actions';
6 | import PaginationArrows from '@student/exams/show/components/PaginationArrows';
7 | import { MDTP, MSTP } from '@student/exams/show/types';
8 |
9 | const mapStateToProps: MSTP<{
10 | show: boolean;
11 | hasNext: boolean;
12 | hasPrev: boolean;
13 | }> = (state) => ({
14 | show: state.pagination.paginated,
15 | hasNext: state.pagination.page !== state.pagination.pageCoords.length - 1,
16 | hasPrev: state.pagination.page !== 0,
17 | });
18 |
19 | const mapDispatchToProps: MDTP<{
20 | next: () => void;
21 | prev: () => void;
22 | }> = (dispatch) => ({
23 | next: (): void => {
24 | dispatch(nextQuestion());
25 | },
26 | prev: (): void => {
27 | dispatch(prevQuestion());
28 | },
29 | });
30 |
31 | export default connect(mapStateToProps, mapDispatchToProps)(PaginationArrows);
32 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/containers/ShowQuestion.ts:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { ExamTakerState, MSTP, MDTP } from '@student/exams/show/types';
3 | import ShowQuestion from '@student/exams/show/components/ShowQuestion';
4 | import { spyQuestion } from '@student/exams/show/actions';
5 |
6 | const mapStateToProps: MSTP<{
7 | paginated: boolean;
8 | selectedQuestion: number;
9 | selectedPart: number;
10 | }> = (state: ExamTakerState) => {
11 | const { pageCoords, paginated, page } = state.pagination;
12 | return {
13 | paginated,
14 | selectedQuestion: pageCoords[page].question,
15 | selectedPart: pageCoords[page].part,
16 | };
17 | };
18 |
19 | const mapDispatchToProps: MDTP<{
20 | spyQuestion: (question: number, pnum?: number) => void;
21 | }> = (dispatch) => ({
22 | spyQuestion: (question, part): void => {
23 | dispatch(spyQuestion({ question, part }));
24 | },
25 | });
26 |
27 | export default connect(mapStateToProps, mapDispatchToProps)(ShowQuestion);
28 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/containers/SnapshotInfo.tsx:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { ExamTakerState, MSTP, SnapshotStatus } from '@student/exams/show/types';
3 | import SnapshotInfo from '@student/exams/show/components/SnapshotInfo';
4 |
5 | const mapStateToProps: MSTP<{
6 | status: SnapshotStatus;
7 | message: string;
8 | }> = (state: ExamTakerState) => ({
9 | status: state.snapshot.status,
10 | message: state.snapshot.message,
11 | });
12 |
13 | export default connect(mapStateToProps)(SnapshotInfo);
14 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/containers/SubmitButton.ts:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { submitExam } from '@student/exams/show/actions';
3 | import SubmitButton from '@student/exams/show/components/SubmitButton';
4 | import { MDTP } from '@student/exams/show/types';
5 |
6 | interface OwnProps {
7 | examTakeUrl: string;
8 | cleanupBeforeSubmit: () => void;
9 | }
10 |
11 | const mapDispatchToProps: MDTP<{
12 | submit: () => void;
13 | }, OwnProps> = (dispatch, ownProps) => ({
14 | submit: (): void => {
15 | dispatch(submitExam(ownProps.examTakeUrl, ownProps.cleanupBeforeSubmit));
16 | },
17 | });
18 |
19 | export default connect(null, mapDispatchToProps)(SubmitButton);
20 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/containers/navbar/Scratch.ts:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { MSTP, MDTP } from '@student/exams/show/types';
3 | import Scratch from '@student/exams/show/components/navbar/Scratch';
4 | import { updateScratch } from '@student/exams/show/actions';
5 |
6 | const mapStateToProps: MSTP<{
7 | value: string;
8 | }> = (state) => ({
9 | value: state.contents.answers.scratch ?? '',
10 | });
11 |
12 | const mapDispatchToProps: MDTP<{
13 | onChange: (newVal: string) => void;
14 | }> = (dispatch) => ({
15 | onChange: (newVal): void => {
16 | dispatch(updateScratch(newVal));
17 | },
18 | });
19 |
20 | export default connect(mapStateToProps, mapDispatchToProps)(Scratch);
21 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/containers/navbar/index.ts:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import ExamNavbar from '@student/exams/show/components/navbar';
3 | import { MSTP, TimeInfo } from '@student/exams/show/types';
4 |
5 | const mapStateToProps: MSTP<{
6 | time: TimeInfo;
7 | }> = (state) => ({
8 | time: state.contents.time,
9 | });
10 |
11 | export default connect(mapStateToProps)(ExamNavbar);
12 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/containers/questions/AllThatApply.tsx:
--------------------------------------------------------------------------------
1 | import AllThatApply from '@student/exams/show/components/questions/AllThatApply';
2 | import { connectWithPath } from './connectors';
3 |
4 | export default connectWithPath(AllThatApply);
5 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/containers/questions/Code.ts:
--------------------------------------------------------------------------------
1 | import Code from '@student/exams/show/components/questions/Code';
2 | import { connectWithPath } from './connectors';
3 |
4 | export default connectWithPath(Code);
5 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/containers/questions/CodeTag.tsx:
--------------------------------------------------------------------------------
1 | import CodeTag from '@student/exams/show/components/questions/CodeTag';
2 | import { connectWithPath } from './connectors';
3 |
4 | export default connectWithPath(CodeTag);
5 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/containers/questions/Matching.tsx:
--------------------------------------------------------------------------------
1 | import Matching from '@student/exams/show/components/questions/Matching';
2 | import { connectWithPath } from './connectors';
3 |
4 | export default connectWithPath(Matching);
5 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/containers/questions/MultipleChoice.tsx:
--------------------------------------------------------------------------------
1 | import MultipleChoice from '@student/exams/show/components/questions/MultipleChoice';
2 | import { connectWithPath } from './connectors';
3 |
4 | export default connectWithPath(MultipleChoice);
5 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/containers/questions/Text.tsx:
--------------------------------------------------------------------------------
1 | import Text from '@student/exams/show/components/questions/Text';
2 | import { connectWithPath } from './connectors';
3 |
4 | export default connectWithPath(Text);
5 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/containers/questions/YesNo.ts:
--------------------------------------------------------------------------------
1 | import YesNo from '@student/exams/show/components/questions/YesNo';
2 | import { connectWithPath } from './connectors';
3 |
4 | export default connectWithPath(YesNo);
5 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/containers/scrollspy/connectors.ts:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { MSTP, MDTP } from '@student/exams/show/types';
3 | import { spyQuestion } from '@student/exams/show/actions';
4 |
5 | const mapStateToProps: MSTP<{
6 | paginated: boolean;
7 | selectedQuestion: number;
8 | selectedPart: number;
9 | waypointsActive: boolean;
10 | }> = (state) => {
11 | const {
12 | paginated,
13 | pageCoords,
14 | page,
15 | waypointsActive,
16 | } = state.pagination;
17 | return {
18 | paginated,
19 | selectedQuestion: pageCoords[page].question,
20 | selectedPart: pageCoords[page].part,
21 | waypointsActive,
22 | };
23 | };
24 |
25 | const mapDispatchToProps: MDTP<{
26 | spy: (question: number, pnum?: number) => void;
27 | }> = (dispatch) => ({
28 | spy: (question, part): void => {
29 | dispatch(spyQuestion({ question, part }));
30 | },
31 | });
32 |
33 | export default connect(mapStateToProps, mapDispatchToProps);
34 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/helpers.ts:
--------------------------------------------------------------------------------
1 | export function getCSRFToken(): string {
2 | const elem: HTMLMetaElement = document.querySelector('[name=csrf-token]');
3 | return elem?.content ?? '';
4 | }
5 |
6 | /**
7 | * Flash an element's background color for emphasis.
8 | */
9 | function pulse(elem: HTMLElement): void {
10 | const listener = (): void => {
11 | elem.removeEventListener('animationend', listener);
12 | elem.classList.remove('bg-pulse');
13 | };
14 | elem.addEventListener('animationend', listener);
15 | elem.classList.add('bg-pulse');
16 | }
17 |
18 | export function scrollToElem(id: string, smooth = true): void {
19 | setTimeout(() => {
20 | const elem = document.getElementById(id);
21 | if (!elem) { return; }
22 | const elemTop = elem.getBoundingClientRect().top + window.pageYOffset;
23 | window.scrollTo({
24 | left: 0,
25 | top: elemTop + 1,
26 | behavior: smooth ? 'smooth' : 'auto',
27 | });
28 | pulse(elem);
29 | });
30 | }
31 |
32 | export function scrollToQuestion(qnum: number, pnum?: number, smooth?: boolean): void {
33 | if (pnum !== undefined) {
34 | scrollToElem(`question-${qnum}-part-${pnum}`, smooth);
35 | } else {
36 | scrollToElem(`question-${qnum}`, smooth);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/reducers/contents.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ContentsState,
3 | ExamTakerAction,
4 | } from '@student/exams/show/types';
5 |
6 | export default (state: ContentsState = {
7 | exam: undefined,
8 | answers: {
9 | answers: [],
10 | scratch: '',
11 | },
12 | }, action: ExamTakerAction): ContentsState => {
13 | switch (action.type) {
14 | case 'LOAD_EXAM': {
15 | return {
16 | exam: action.exam,
17 | answers: action.answers,
18 | time: action.time,
19 | };
20 | }
21 | case 'UPDATE_ANSWER': {
22 | const { qnum, pnum, bnum } = action;
23 | const answers = [...state.answers.answers];
24 | answers[qnum] = [...answers[qnum]];
25 | answers[qnum][pnum] = [...answers[qnum][pnum]];
26 | answers[qnum][pnum][bnum] = action.val;
27 | return {
28 | ...state,
29 | answers: {
30 | ...state.answers,
31 | answers,
32 | },
33 | };
34 | }
35 | case 'UPDATE_SCRATCH':
36 | return {
37 | ...state,
38 | answers: {
39 | ...state.answers,
40 | scratch: action.val,
41 | },
42 | };
43 | default:
44 | return state;
45 | }
46 | };
47 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 |
3 | import lockdown from './lockdown';
4 | import contents from './contents';
5 | import pagination from './pagination';
6 | import snapshot from './snapshot';
7 |
8 | export default combineReducers({
9 | lockdown,
10 | contents,
11 | pagination,
12 | snapshot,
13 | });
14 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/reducers/lockdown.ts:
--------------------------------------------------------------------------------
1 | import {
2 | LockdownStatus,
3 | LockdownState,
4 | ExamTakerAction,
5 | } from '@student/exams/show/types';
6 |
7 | export default (state: LockdownState = {
8 | loaded: false,
9 | status: LockdownStatus.BEFORE,
10 | message: '',
11 | }, action: ExamTakerAction): LockdownState => {
12 | switch (action.type) {
13 | case 'IN_PROGRESS':
14 | return {
15 | ...state,
16 | status: LockdownStatus.IN_PROGRESS,
17 | message: 'Please wait...',
18 | };
19 | case 'LOCKDOWN_IGNORED':
20 | return {
21 | ...state,
22 | status: LockdownStatus.IGNORED,
23 | message: '',
24 | };
25 | case 'LOCKED_DOWN':
26 | return {
27 | ...state,
28 | status: LockdownStatus.LOCKED,
29 | message: '',
30 | };
31 | case 'LOCKDOWN_FAILED':
32 | return {
33 | ...state,
34 | status: LockdownStatus.FAILED,
35 | message: action.message,
36 | };
37 | case 'LOAD_EXAM':
38 | return {
39 | ...state,
40 | loaded: true,
41 | };
42 | default:
43 | return state;
44 | }
45 | };
46 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/reducers/snapshot.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ExamTakerAction,
3 | SnapshotState,
4 | SnapshotStatus,
5 | } from '@student/exams/show/types';
6 |
7 | export default function snapshot(state: SnapshotState = {
8 | status: SnapshotStatus.LOADING,
9 | message: '',
10 | }, action: ExamTakerAction): SnapshotState {
11 | switch (action.type) {
12 | case 'LOAD_EXAM':
13 | return {
14 | ...state,
15 | status: SnapshotStatus.SUCCESS,
16 | };
17 | case 'SNAPSHOT_SAVING':
18 | return {
19 | ...state,
20 | status: SnapshotStatus.LOADING,
21 | };
22 | case 'SNAPSHOT_SUCCESS':
23 | return {
24 | ...state,
25 | status: SnapshotStatus.SUCCESS,
26 | };
27 | case 'SNAPSHOT_FAILURE':
28 | return {
29 | ...state,
30 | status: SnapshotStatus.FAILURE,
31 | message: action.message,
32 | };
33 | case 'SNAPSHOT_FINISHED':
34 | return {
35 | ...state,
36 | status: SnapshotStatus.FINISHED,
37 | message: action.message,
38 | };
39 | default:
40 | return state;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/student/exams/show/store/index.ts:
--------------------------------------------------------------------------------
1 | import { applyMiddleware, createStore } from 'redux';
2 | import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly';
3 | import ReduxThunk from 'redux-thunk';
4 | import rootReducer from '@student/exams/show/reducers';
5 |
6 | const composeEnhancers = composeWithDevTools({
7 | trace: true,
8 | traceLimit: 25,
9 | });
10 |
11 | const reduxEnhancers = composeEnhancers(
12 | applyMiddleware(ReduxThunk),
13 | );
14 |
15 | export default createStore(rootReducer, reduxEnhancers);
16 |
--------------------------------------------------------------------------------
/app/packs/components/workflows/wdyr.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | // eslint-disable-next-line import/no-extraneous-dependencies
3 | import whyDidYouRender from '@welldone-software/why-did-you-render';
4 |
5 | whyDidYouRender(React, {
6 | trackAllPureComponents: true,
7 | });
8 |
--------------------------------------------------------------------------------
/app/packs/entrypoints/application.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | // This file is automatically compiled by Webpack, along with any other files
4 | // present in this directory. You're encouraged to place your actual application logic in
5 | // a relevant structure within app/javascript and only use these pack files to reference
6 | // that code so it'll be compiled.
7 |
8 | require('@rails/ujs').start();
9 | const componentRequireContext = require.context('components', true);
10 | const ReactRailsUJS = require('react_ujs');
11 |
12 | const images = require.context('../images', true);
13 |
14 | ReactRailsUJS.useContext(componentRequireContext);
15 |
--------------------------------------------------------------------------------
/app/packs/entrypoints/bootstrap.js:
--------------------------------------------------------------------------------
1 | import 'bootstrap/js/dist/alert';
2 | import 'bootstrap/js/dist/button';
3 | import 'bootstrap/js/dist/carousel';
4 | import 'bootstrap/js/dist/collapse';
5 | import 'bootstrap/js/dist/dropdown';
6 | import 'bootstrap/js/dist/index';
7 | import 'bootstrap/js/dist/modal';
8 | import 'bootstrap/js/dist/popover';
9 | import 'bootstrap/js/dist/scrollspy';
10 | import 'bootstrap/js/dist/tab';
11 | import 'bootstrap/js/dist/toast';
12 | import 'bootstrap/js/dist/tooltip';
13 | import 'bootstrap/js/dist/util';
14 |
15 | import './bootstrap.scss';
16 |
--------------------------------------------------------------------------------
/app/packs/entrypoints/bootstrap.scss:
--------------------------------------------------------------------------------
1 | @import "~bootstrap/scss/bootstrap";
2 | @import "~bootstrap/scss/functions";
3 | @import "~bootstrap/scss/variables";
4 | @import "~bootstrap/scss/mixins";
5 |
6 | html,
7 | body {
8 | height: 100%;
9 | }
10 |
11 | .form-signin {
12 | width: 100%;
13 | max-width: 330px;
14 | padding: 15px;
15 | margin: 0 auto;
16 |
17 | .form-check {
18 | margin-bottom: 2rem;
19 | }
20 |
21 | .form-control {
22 | position: relative;
23 | box-sizing: border-box;
24 | height: auto;
25 | padding: 10px;
26 | font-size: 16px;
27 | }
28 |
29 | .form-control:focus {
30 | z-index: 2;
31 | }
32 |
33 | .form-group {
34 | margin: 0;
35 | }
36 |
37 | input[type="password"] {
38 | margin-bottom: 10px;
39 | border-top-left-radius: 0;
40 | border-top-right-radius: 0;
41 | }
42 | }
43 |
44 | #username {
45 | margin-bottom: -1px;
46 | border-bottom-right-radius: 0;
47 | border-bottom-left-radius: 0;
48 | }
49 |
--------------------------------------------------------------------------------
/app/packs/images/navbar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeGrade/hourglass/12b7f02381b8445505bf9eadf0e48a4bb4bdfc50/app/packs/images/navbar.png
--------------------------------------------------------------------------------
/app/packs/images/site-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeGrade/hourglass/12b7f02381b8445505bf9eadf0e48a4bb4bdfc50/app/packs/images/site-icon.png
--------------------------------------------------------------------------------
/app/packs/rawimages/hourglass-dolphin.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeGrade/hourglass/12b7f02381b8445505bf9eadf0e48a4bb4bdfc50/app/packs/rawimages/hourglass-dolphin.jpg
--------------------------------------------------------------------------------
/app/packs/rawimages/hourglass-dolphin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeGrade/hourglass/12b7f02381b8445505bf9eadf0e48a4bb4bdfc50/app/packs/rawimages/hourglass-dolphin.png
--------------------------------------------------------------------------------
/app/packs/relay/data/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeGrade/hourglass/12b7f02381b8445505bf9eadf0e48a4bb4bdfc50/app/packs/relay/data/.keep
--------------------------------------------------------------------------------
/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= csrf_meta_tags %>
8 | <%= csp_meta_tag %>
9 |
10 | <% default_page_title = "#{params[:controller]} / #{params[:action]}" %>
11 | <%= @page_title || default_page_title %> - Hourglass
12 |
13 | <%= stylesheet_pack_tag 'application', 'bootstrap', media: 'all' %>
14 | <%= javascript_pack_tag 'application', 'bootstrap' %>
15 |
16 | <%= favicon_pack_tag 'site-icon.png', id: 'favicon' %>
17 |
18 |
19 |
20 | <%= yield %>
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/views/layouts/mailer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | load File.expand_path('spring', __dir__)
3 | APP_PATH = File.expand_path('../config/application', __dir__)
4 | require_relative '../config/boot'
5 | require 'rails/commands'
6 |
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | load File.expand_path('spring', __dir__)
3 | require_relative '../config/boot'
4 | require 'rake'
5 | Rake.application.run
6 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'fileutils'
3 |
4 | # path to your application root.
5 | APP_ROOT = File.expand_path('..', __dir__)
6 |
7 | def system!(*args)
8 | system(*args) || abort("\n== Command #{args} failed ==")
9 | end
10 |
11 | FileUtils.chdir APP_ROOT do
12 | # This script is a way to set up or update your development environment automatically.
13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome.
14 | # Add necessary setup steps to this file.
15 |
16 | puts '== Installing dependencies =='
17 | system! 'gem install bundler --conservative'
18 | system('bundle check') || system!('bundle install')
19 |
20 | # Install JavaScript dependencies
21 | system! 'bin/yarn'
22 |
23 | # puts "\n== Copying sample files =="
24 | # unless File.exist?('config/database.yml')
25 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml'
26 | # end
27 |
28 | puts "\n== Preparing database =="
29 | system! 'bin/rails db:prepare'
30 |
31 | puts "\n== Removing old logs and tempfiles =="
32 | system! 'bin/rails log:clear tmp:clear'
33 |
34 | puts "\n== Restarting application server =="
35 | system! 'bin/rails restart'
36 | end
37 |
--------------------------------------------------------------------------------
/bin/spring:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | if !defined?(Spring) && [nil, "development", "test"].include?(ENV["RAILS_ENV"])
3 | gem "bundler"
4 | require "bundler"
5 |
6 | # Load Spring without loading other gems in the Gemfile, for speed.
7 | Bundler.locked_gems&.specs&.find { |spec| spec.name == "spring" }&.tap do |spring|
8 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
9 | gem "spring", spring.version
10 | require "spring/binstub"
11 | rescue Gem::LoadError
12 | # Ignore when Spring is not installed.
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/bin/webpack:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
4 | ENV["NODE_ENV"] ||= "development"
5 |
6 | require "pathname"
7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
8 | Pathname.new(__FILE__).realpath)
9 |
10 | require "bundler/setup"
11 |
12 | require "webpacker"
13 | require "webpacker/webpack_runner"
14 |
15 | APP_ROOT = File.expand_path("..", __dir__)
16 | Dir.chdir(APP_ROOT) do
17 | Webpacker::WebpackRunner.run(ARGV)
18 | end
19 |
--------------------------------------------------------------------------------
/bin/webpack-dev-server:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
4 | ENV["NODE_ENV"] ||= "development"
5 |
6 | require "pathname"
7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
8 | Pathname.new(__FILE__).realpath)
9 |
10 | require "bundler/setup"
11 |
12 | require "webpacker"
13 | require "webpacker/dev_server_runner"
14 |
15 | APP_ROOT = File.expand_path("..", __dir__)
16 | Dir.chdir(APP_ROOT) do
17 | Webpacker::DevServerRunner.run(ARGV)
18 | end
19 |
--------------------------------------------------------------------------------
/bin/yarn:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | APP_ROOT = File.expand_path("..", __dir__)
4 | Dir.chdir(APP_ROOT) do
5 | yarn = ENV["PATH"].split(File::PATH_SEPARATOR).
6 | select { |dir| File.expand_path(dir) != __dir__ }.
7 | product(["yarn", "yarnpkg", "yarn.cmd", "yarn.ps1"]).
8 | map { |dir, file| File.expand_path(file, dir) }.
9 | find { |file| File.executable?(file) }
10 |
11 | if yarn
12 | exec yarn, *ARGV
13 | else
14 | $stderr.puts "Yarn executable was not detected in the system."
15 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
16 | exit 1
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # This file is used by Rack-based servers to start the application.
4 |
5 | require_relative 'config/environment'
6 |
7 | run Rails.application
8 | Rails.application.load_server
9 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
4 |
5 | require 'bundler/setup' # Set up gems listed in the Gemfile.
6 | require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
7 |
--------------------------------------------------------------------------------
/config/cable.yml:
--------------------------------------------------------------------------------
1 | # development:
2 | # adapter: async
3 | #
4 | # test:
5 | # adapter: test
6 | #
7 | # production:
8 | # adapter: redis
9 | # url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
10 | # channel_prefix: hourglass_production
11 |
12 | production:
13 | adapter: postgresql
14 |
15 | development:
16 | adapter: postgresql
17 |
18 | test:
19 | adapter: postgresql
20 |
--------------------------------------------------------------------------------
/config/credentials.yml.enc:
--------------------------------------------------------------------------------
1 | Kl4b+RxwNsqGlBkwgKFd6oA2dqq+VCvryUaQytIEqhKyYLFUeoHcXoQ3GzI8siOBm21Tuz7NzG6hTukMfnJcuXvj/t8Fpba+UdWxjjlbYE6ooleEHj9/E4Arx8mNi3exxmddTh+NEErSt74sBxJCr5ohCvqlBJoOTu3cBLmHh4S9NkOiylE/UdKupwCH0hv+jmZJYpTpitsAl16QtK5mwDELFxP1iaBoFl/ICC5mMyAg+0F0WwkqxnVlhIiAQVBFexeGjzoM8jdw+bRthYpOSJdqNeC6T5jujTL3fuawIRfFo1AwjjIFGiZnV2Wb5sCW8j5SVZzTAhliFGNwYABouBV5jdD77zoXeBkvcAJy9A7VVWPB5XzdmaTAsk4SVga+c3lmeIyG4MKGw43GM/i7BLHx1ug6d41bs21x--ur/fG9mCEoMmGg8a--xEuwdt3f7ew3Z8cfU8S55Q==
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Load the Rails application.
4 | require_relative 'application'
5 |
6 | # Initialize the Rails application.
7 | Rails.application.initialize!
8 |
--------------------------------------------------------------------------------
/config/initializers/application_controller_renderer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | # Be sure to restart your server when you modify this file.
3 |
4 | # ActiveSupport::Reloader.to_prepare do
5 | # ApplicationController.renderer.defaults.merge!(
6 | # http_host: 'example.org',
7 | # https: false
8 | # )
9 | # end
10 |
--------------------------------------------------------------------------------
/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | # Be sure to restart your server when you modify this file.
3 |
4 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
5 | # Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) }
6 |
7 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code
8 | # by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'".
9 | Rails.backtrace_cleaner.remove_silencers! if ENV['BACKTRACE']
10 |
--------------------------------------------------------------------------------
/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Specify a serializer for the signed and encrypted cookie jars.
6 | # Valid options are :json, :marshal, and :hybrid.
7 | Rails.application.config.action_dispatch.cookies_serializer = :json
8 |
--------------------------------------------------------------------------------
/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Configure sensitive parameters which will be filtered from the log file.
6 | Rails.application.config.filter_parameters += [
7 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
8 | ]
9 |
--------------------------------------------------------------------------------
/config/initializers/graphql_schema_updater.rb:
--------------------------------------------------------------------------------
1 | reloader = ActiveSupport::FileUpdateChecker.new([], {
2 | Rails.root.join('app/graphql/types').to_s => ['rb'],
3 | Rails.root.join('app/graphql/mutations').to_s => ['rb'],
4 | }) do
5 | HourglassSchema.write_json!
6 | HourglassSchema.write_graphql!
7 | end
8 |
9 | Rails.application.config.to_prepare do
10 | reloader.execute_if_updated
11 | end
12 |
13 | Rails.application.config.after_initialize do
14 | HourglassSchema.write_json!
15 | HourglassSchema.write_graphql!
16 | end
17 |
18 | if File.exists?(Rails.root.join('config/schemas/graphql-queries.json'))
19 | STATIC_GRAPHQL_QUERIES = JSON.parse(File.read(Rails.root.join('config/schemas/graphql-queries.json')))
20 | KNOWN_GRAPHQL_QUERIES = STATIC_GRAPHQL_QUERIES.invert
21 | else
22 | STATIC_GRAPHQL_QUERIES = {}
23 | KNOWN_GRAPHQL_QUERIES = STATIC_GRAPHQL_QUERIES.invert
24 | end
25 |
26 | if Rails.env.production?
27 | STATIC_GRAPHQL_QUERIES.values.each(&:freeze)
28 | STATIC_GRAPHQL_QUERIES.freeze
29 | KNOWN_GRAPHQL_QUERIES.values.each(&:freeze)
30 | KNOWN_GRAPHQL_QUERIES.freeze
31 | end
32 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | # Be sure to restart your server when you modify this file.
3 |
4 | # Add new inflection rules using the following format. Inflections
5 | # are locale specific, and you may define rules for as many different
6 | # locales as you wish. All of these examples are active by default:
7 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
8 | # inflect.plural /^(ox)$/i, '\1en'
9 | # inflect.singular /^(ox)en/i, '\1'
10 | # inflect.irregular 'person', 'people'
11 | # inflect.uncountable %w( fish sheep )
12 | # end
13 |
14 | # These inflection rules are supported but not enabled by default:
15 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
16 | # inflect.acronym 'RESTful'
17 | # end
18 |
--------------------------------------------------------------------------------
/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | # Be sure to restart your server when you modify this file.
3 |
4 | # Add new mime types for use in respond_to blocks:
5 | # Mime::Type.register "text/richtext", :rtf
6 |
--------------------------------------------------------------------------------
/config/initializers/permissions_policy.rb:
--------------------------------------------------------------------------------
1 | # Define an application-wide HTTP permissions policy. For further
2 | # information see https://developers.google.com/web/updates/2018/06/feature-policy
3 | #
4 | # Rails.application.config.permissions_policy do |f|
5 | # f.camera :none
6 | # f.gyroscope :none
7 | # f.microphone :none
8 | # f.usb :none
9 | # f.fullscreen :self
10 | # f.payment :self, "https://secure.example.com"
11 | # end
12 |
--------------------------------------------------------------------------------
/config/initializers/secret_token.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 | # Your secret key for verifying the integrity of signed cookies.
3 | # If you change this key, all old signed cookies will become invalid!
4 | # Make sure the secret is at least 30 characters and all random,
5 | # no regular words or you'll be exposed to dictionary attacks.
6 |
7 | if Rails.env.production?
8 | require 'securerandom'
9 | key_file = File.expand_path("~/.rails_key").to_s
10 | unless File.exists?(key_file)
11 | kk = File.open(key_file, 'wb')
12 | 6.times do
13 | kk.write(SecureRandom.urlsafe_base64)
14 | end
15 | kk.close
16 | end
17 | if Rails.version.to_f <= 5.0
18 | Hourglass::Application.config.secret_token = File.open(key_file).read
19 | else
20 | Hourglass::Application.config.secret_key_base = File.open(key_file).read
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # This file contains settings for ActionController::ParamsWrapper which
6 | # is enabled by default.
7 |
8 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
9 | ActiveSupport.on_load(:action_controller) do
10 | wrap_parameters format: [:json]
11 | end
12 |
13 | # To enable root element in JSON for ActiveRecord objects.
14 | # ActiveSupport.on_load(:active_record) do
15 | # self.include_root_in_json = true
16 | # end
17 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # The following keys must be escaped otherwise they will not be retrieved by
20 | # the default I18n backend:
21 | #
22 | # true, false, on, off, yes, no
23 | #
24 | # Instead, surround them with single quotes.
25 | #
26 | # en:
27 | # 'true': 'foo'
28 | #
29 | # To learn more, please read the Rails Internationalization guide
30 | # available at https://guides.rubyonrails.org/i18n.html.
31 |
32 | en:
33 | hello: "Hello world"
34 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Rails.application.routes.draw do
4 | mount ActionCable.server => '/cable'
5 | post '/graphql', to: 'graphql#execute'
6 | get '/graphiql', to: 'graphql#graphiql' if Rails.env.development?
7 |
8 | namespace :api, shallow: true do
9 | namespace :professor do
10 | resources :exams, param: 'exam_id', only: [] do
11 | member do
12 | resources :versions, param: 'version_id', only: [] do
13 | collection do
14 | post :import
15 | end
16 | member do
17 | get :export_file
18 | get :export_archive
19 | end
20 | end
21 | end
22 | end
23 | end
24 |
25 | namespace :student do
26 | resources :exams, param: 'exam_id', only: [] do
27 | member do
28 | post :take
29 | end
30 | end
31 | end
32 | end
33 |
34 | devise_for :users, skip: [:registrations, :passwords], controllers: {
35 | omniauth_callbacks: 'users/omniauth_callbacks',
36 | }
37 |
38 | root to: 'main#index'
39 | get '*path', to: 'main#index'
40 | end
41 |
--------------------------------------------------------------------------------
/config/spring.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Spring.watch(
4 | '.ruby-version',
5 | '.rbenv-vars',
6 | 'tmp/restart.txt',
7 | 'tmp/caching-dev.txt'
8 | )
9 |
--------------------------------------------------------------------------------
/config/webpack/development.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'
2 |
3 | const webpackConfig = require('./base')
4 |
5 | module.exports = webpackConfig
6 |
--------------------------------------------------------------------------------
/config/webpack/production.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = process.env.NODE_ENV || 'production'
2 |
3 | const { merge } = require('@rails/webpacker');
4 | const webpackConfig = require('./base')
5 |
6 | module.exports = merge(
7 | {
8 | module: {
9 | rules: [
10 | {
11 | test: /graphiql|wdyr/,
12 | use: [{ loader: 'ignore-loader' }],
13 | },
14 | ],
15 | },
16 | },
17 | webpackConfig,
18 | );
19 |
20 | module.exports = webpackConfig
21 |
--------------------------------------------------------------------------------
/config/webpack/test.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'
2 |
3 | const webpackConfig = require('./base')
4 |
5 | module.exports = webpackConfig
6 |
--------------------------------------------------------------------------------
/db/migrate/20201022000727_add_unique_bottlenose_id_index_to_section.rb:
--------------------------------------------------------------------------------
1 | class AddUniqueBottlenoseIdIndexToSection < ActiveRecord::Migration[6.0]
2 | def change
3 | add_index :sections, [:bottlenose_id], unique: true
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20210301005626_remove_rubrics_from_exam_info.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Removes rubric information from exam version's `info` field.
4 | # This information is now schematized in the database.
5 | class RemoveRubricsFromExamInfo < ActiveRecord::Migration[6.0]
6 | def up
7 | ExamVersion.transaction do
8 | ExamVersion.all.each do |ev|
9 | ev.update(info: ev.info.reject { |k| k == 'rubrics' })
10 | end
11 | end
12 | end
13 |
14 | def down
15 | raise 'NOT IMPLEMENTED YET'
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/db/migrate/20210331000943_change_questions_to_student_questions.rb:
--------------------------------------------------------------------------------
1 | class ChangeQuestionsToStudentQuestions < ActiveRecord::Migration[6.0]
2 | def change
3 | rename_table "questions", "student_questions"
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20210519203405_mark_preset_comments_order_required.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Marks `order` as required for preset comments.
4 | class MarkPresetCommentsOrderRequired < ActiveRecord::Migration[6.0]
5 | def up
6 | PresetComment.all.group_by(&:rubric_preset_id).each do |_, comments|
7 | max_order = comments.map(&:order).compact.max || -1
8 | missing_order_comments = comments.filter { |c| c.order.nil? }
9 | missing_order_comments.each_with_index do |comment, index|
10 | comment.update(order: max_order + index + 1)
11 | end
12 | end
13 | change_column :preset_comments, :order, :integer, null: false
14 | end
15 |
16 | def down
17 | change_column :preset_comments, :order, :integer, null: true
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/db/migrate/20210521115135_create_rubric_closure_tree.rb:
--------------------------------------------------------------------------------
1 | class CreateRubricClosureTree < ActiveRecord::Migration[6.0]
2 | def up
3 | create_table :rubric_tree_paths do |t|
4 | t.references :ancestor, null: false, foreign_key: { to_table: :rubrics }
5 | t.references :descendant, null: false, foreign_key: { to_table: :rubrics }
6 | t.integer :path_length, null: false
7 | t.index [:ancestor_id, :descendant_id], unique: true
8 | end
9 | all_rubrics = Rubric.all.map { |r| [r.id, r] }.to_h
10 | all_rubrics.each do |r_id, r|
11 | RubricTreePath.create(ancestor: r, descendant: r, path_length: 0)
12 | parent = all_rubrics[r.parent_section_id]
13 | path_length = 1
14 | while parent
15 | RubricTreePath.create(ancestor: parent, descendant: r, path_length: path_length)
16 | parent = all_rubrics[parent.parent_section_id]
17 | path_length += 1
18 | end
19 | end
20 | end
21 |
22 | def down
23 | drop_table :rubric_tree_paths
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/db/migrate/20210521121346_remove_rubric_parent_section.rb:
--------------------------------------------------------------------------------
1 | class RemoveRubricParentSection < ActiveRecord::Migration[6.0]
2 | def up
3 | change_table :rubrics do |t|
4 | t.remove :parent_section_id
5 | end
6 | end
7 |
8 | def down
9 | change_table :rubrics do |t|
10 | t.references :parent_section, null: true, foreign_key: { to_table: :rubrics }
11 | end
12 | RubricTreePath.where(path_length: 1) do |link|
13 | link.descendant.update(parent_section_id: link.ancestor_id)
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/db/migrate/20210523182059_remove_info_from_exam_versions.rb:
--------------------------------------------------------------------------------
1 | class RemoveInfoFromExamVersions < ActiveRecord::Migration[6.0]
2 | def up
3 | change_table :exam_versions do |t|
4 | t.remove :info
5 | end
6 | end
7 |
8 | def down
9 | change_table :exam_versions do |t|
10 | t.jsonb :info, null: false, default: {placeholder: true}
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/db/migrate/20210911223649_create_terms.rb:
--------------------------------------------------------------------------------
1 | class CreateTerms < ActiveRecord::Migration[6.0]
2 | def up
3 | create_table :terms do |t|
4 | t.integer :semester, null: false
5 | t.integer :year, null: false
6 | t.boolean :archived, default: false, null: false
7 |
8 | t.timestamps
9 | t.index ["semester", "year"], name: "index_terms_on_semester_and_year", unique: true
10 | end
11 | empty_term = Term.create(semester: Term.semesters["fall"], year: 2000)
12 | change_table :courses do |t|
13 | t.references :term, foreign_key: true
14 | end
15 | change_column_null :courses, :term_id, false, empty_term.id
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/db/migrate/20220911215356_add_times_to_exam_versions.rb:
--------------------------------------------------------------------------------
1 | class AddTimesToExamVersions < ActiveRecord::Migration[6.1]
2 | def change
3 | add_column :exam_versions, :start_time, :datetime, null: true
4 | add_column :exam_versions, :end_time, :datetime, null: true
5 | add_column :exam_versions, :duration, :integer, null: true
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20231106011324_add_updated_index_to_snapshots.rb:
--------------------------------------------------------------------------------
1 | class AddUpdatedIndexToSnapshots < ActiveRecord::Migration[6.1]
2 | def change
3 | change_table :snapshots do |t|
4 | t.index :created_at
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20231116195043_add_pins_to_students_registrations.rb:
--------------------------------------------------------------------------------
1 | class AddPinsToStudentsRegistrations < ActiveRecord::Migration[6.1]
2 | def change
3 | change_table :exam_versions do |t|
4 | t.string :pin_nonce, null: true
5 | t.integer :pin_strength, null: false, default: 6
6 | end
7 | change_table :accommodations do |t|
8 | t.string :policy_exemptions, default: "", null: false
9 | end
10 | change_table :registrations do |t|
11 | t.integer :login_attempt_count, null: false, default: 0
12 | t.boolean :pin_validated, null: false, default: false
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/db/migrate/20240531182212_add_notes_to_grading_lock.rb:
--------------------------------------------------------------------------------
1 | class AddNotesToGradingLock < ActiveRecord::Migration[6.1]
2 | def change
3 | change_table :grading_locks do |t|
4 | t.string :notes, null: false, default: ""
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | # This file should contain all the record creation needed to seed the database with its default values.
3 | # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup).
4 | #
5 | # Examples:
6 | #
7 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
8 | # Character.create(name: 'Luke', movie: movies.first)
9 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "nixpkgs": {
4 | "locked": {
5 | "lastModified": 1637010500,
6 | "narHash": "sha256-CVcZs8QP0Y2NbsizhgI/P42FqdeqXNe4h11W1Wk1aFY=",
7 | "owner": "nixos",
8 | "repo": "nixpkgs",
9 | "rev": "5e15d5da4abb74f0dd76967044735c70e94c5af1",
10 | "type": "github"
11 | },
12 | "original": {
13 | "owner": "nixos",
14 | "repo": "nixpkgs",
15 | "rev": "5e15d5da4abb74f0dd76967044735c70e94c5af1",
16 | "type": "github"
17 | }
18 | },
19 | "nixpkgs-ruby": {
20 | "locked": {
21 | "lastModified": 1637010500,
22 | "narHash": "sha256-CVcZs8QP0Y2NbsizhgI/P42FqdeqXNe4h11W1Wk1aFY=",
23 | "owner": "nixos",
24 | "repo": "nixpkgs",
25 | "rev": "5e15d5da4abb74f0dd76967044735c70e94c5af1",
26 | "type": "github"
27 | },
28 | "original": {
29 | "owner": "nixos",
30 | "repo": "nixpkgs",
31 | "rev": "5e15d5da4abb74f0dd76967044735c70e94c5af1",
32 | "type": "github"
33 | }
34 | },
35 | "root": {
36 | "inputs": {
37 | "nixpkgs": "nixpkgs",
38 | "nixpkgs-ruby": "nixpkgs-ruby"
39 | }
40 | }
41 | },
42 | "root": "root",
43 | "version": 7
44 | }
45 |
--------------------------------------------------------------------------------
/hourglass.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 | root /home/app/hourglass/public;
4 | passenger_enabled on;
5 | passenger_user app;
6 | passenger_app_root /home/app/hourglass;
7 |
8 | passenger_env_var RAILS_ENV production;
9 | passenger_env_var NODE_ENV production;
10 | passenger_env_var SECRET_KEY_BASE aaaaa;
11 | passenger_env_var HOURGLASS_DATABASE_HOST postgres;
12 | passenger_env_var BOTTLENOSE_URL http://bottlenose;
13 | passenger_env_var BOTTLENOSE_APP_ID YYdiyTMC4HRH9WpTvrFmpRHAf8xY09c67woaNzbI1OQ;
14 | passenger_env_var BOTTLENOSE_APP_SECRET RIu1dprvaQ5swuvtYXOvqNvbt3CbANaAr5KA1Eg-cpk;
15 |
16 | passenger_app_type rack;
17 | passenger_startup_file /home/app/hourglass/config.ru;
18 |
19 | location /cable {
20 | passenger_app_group_name hourglass_websocket;
21 | passenger_force_max_concurrent_requests_per_process 0;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/lib/assets/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeGrade/hourglass/12b7f02381b8445505bf9eadf0e48a4bb4bdfc50/lib/assets/.keep
--------------------------------------------------------------------------------
/lib/audit.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # An Auditing logger
4 | class Audit
5 | @@log = Logger.new(File.open(Rails.root.join('log', "audit-#{Rails.env}.log"),
6 | File::WRONLY | File::APPEND | File::CREAT))
7 | def self.log(msg)
8 | @@log.info("#{Time.now}: #{msg}")
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/lib/devise/strategies/debug_login.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # From https://insights.kyan.com/devise-authentication-strategies-a1a6b4e2b891
4 | module Devise
5 | module Strategies
6 | # A Devise login mechanism to allow locally testing username/passwords, without LDAP
7 | class DebugLogin < Authenticatable
8 | def authenticate!
9 | user = User.find_by(username: params[:user][:username])
10 | if user &&
11 | params[:user][:password].present? &&
12 | Devise::Encryptor.compare(user.class, user.encrypted_password, params[:user][:password])
13 | success!(user)
14 | else
15 | fail('Did not recognize username/password') # rubocop:disable Style/SignalException
16 | end
17 | end
18 |
19 | def valid?
20 | params[:user] && params[:user][:username] && params[:user][:password]
21 | end
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/tasks/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeGrade/hourglass/12b7f02381b8445505bf9eadf0e48a4bb4bdfc50/lib/tasks/.keep
--------------------------------------------------------------------------------
/lib/tasks/factory_bot_lint.rake:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | namespace :factory_bot do
4 | desc 'Verify that all FactoryBot factories are valid'
5 | task lint: :environment do
6 | require 'factory_bot_rails'
7 | if Rails.env.test?
8 | conn = ActiveRecord::Base.connection
9 | conn.transaction do
10 | FactoryBot.lint
11 | raise ActiveRecord::Rollback
12 | end
13 | else
14 | system("bundle exec rake factory_bot:lint RAILS_ENV='test'")
15 | fail if $?.exitstatus.nonzero?
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/tasks/graphql_schema_update.rake:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | namespace :graphql do
4 | desc 'Update schema.json file'
5 | task update_schema: :environment do
6 | HourglassSchema.write_json!
7 | HourglassSchema.write_graphql!
8 | HourglassSchema.ensure_queries_file!
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/log/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeGrade/hourglass/12b7f02381b8445505bf9eadf0e48a4bb4bdfc50/log/.keep
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('postcss-import'),
4 | require('postcss-flexbugs-fixes'),
5 | require('postcss-preset-env')({
6 | autoprefixer: {
7 | flexbox: 'no-2009'
8 | },
9 | stage: 3
10 | })
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeGrade/hourglass/12b7f02381b8445505bf9eadf0e48a4bb4bdfc50/public/apple-touch-icon-precomposed.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeGrade/hourglass/12b7f02381b8445505bf9eadf0e48a4bb4bdfc50/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeGrade/hourglass/12b7f02381b8445505bf9eadf0e48a4bb4bdfc50/public/favicon.ico
--------------------------------------------------------------------------------
/public/find_x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeGrade/hourglass/12b7f02381b8445505bf9eadf0e48a4bb4bdfc50/public/find_x.jpg
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 |
--------------------------------------------------------------------------------
/relay.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // ...
3 | // Configuration options accepted by the `relay-compiler` command-line tool and `babel-plugin-relay`.
4 | src: './app/packs/components',
5 | schema: './app/packs/relay/data/schema.graphql',
6 | exclude: ['**/node_modules/**', '**/__generated__/**'],
7 | language: 'typescript',
8 | noFutureProofEnums: true,
9 | customScalars: {
10 | ISO8601DateTime: 'string',
11 | },
12 | persistConfig: {
13 | file: "./config/schemas/graphql-queries.json",
14 | algorithm: "MD5",
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/shell.nix:
--------------------------------------------------------------------------------
1 | (import (fetchTarball {
2 | url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz";
3 | sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2";
4 | }) {
5 | src = ./.;
6 | }).shellNix
7 |
--------------------------------------------------------------------------------
/test/application_system_test_case.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
6 | include Devise::Test::IntegrationHelpers
7 | DRIVER = if ENV['DRIVER']
8 | ENV['DRIVER'].to_sym
9 | else
10 | :headless_chrome
11 | end
12 | driven_by :selenium, using: DRIVER, screen_size: [1400, 1400]
13 |
14 | def with_resize_to(width, height)
15 | old_width, old_height = page.current_window.size
16 | page.current_window.resize_to(width, height)
17 | begin
18 | yield
19 | ensure
20 | page.current_window.resize_to(old_width, old_height)
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/test/channels/graphql_channel_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class GraphqlChannelTest < ActionCable::Channel::TestCase
6 | # test "subscribes" do
7 | # subscribe
8 | # assert subscription.confirmed?
9 | # end
10 | end
11 |
--------------------------------------------------------------------------------
/test/controllers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeGrade/hourglass/12b7f02381b8445505bf9eadf0e48a4bb4bdfc50/test/controllers/.keep
--------------------------------------------------------------------------------
/test/controllers/main_controller_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class MainControllerTest < ActionDispatch::IntegrationTest
6 | test 'should get home' do
7 | get root_url
8 | assert_redirected_to new_user_session_path
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/test/factories/accommodation.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :accommodation do
5 | registration
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/test/factories/anomaly.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :anomaly do
5 | registration
6 | reason { 'Left fullscreen.' }
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/test/factories/course.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :course do
5 | sequence(:title, 101) { |n| "Computing #{n}" }
6 | term
7 | last_sync { '2020-05-22 14:03:53' }
8 | active { true }
9 | sequence(:bottlenose_id)
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/test/factories/exam.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :exam do
5 | course
6 | name { 'CS2500 Midterm' }
7 | duration { 30.minutes }
8 | start_time { DateTime.now }
9 | end_time { DateTime.now + 3.hours }
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/test/factories/exam_announcement.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :exam_announcement do
5 | exam
6 | body { "Hello all students. You are taking #{exam.name}." }
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/test/factories/grading_check.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :grading_check do
5 | transient do
6 | staff_registration { create(:staff_registration) }
7 | end
8 |
9 | registration
10 | creator { staff_registration.user }
11 |
12 | question { registration.exam_version.db_questions.find_by(index: 0) }
13 | part { question.parts.find_by(index: 0) }
14 | body_item { part.body_items.find_by(index: 0) }
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/test/factories/grading_comment.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :grading_comment do
5 | transient do
6 | staff_registration { create(:staff_registration) }
7 | end
8 |
9 | registration
10 | creator { staff_registration.user }
11 |
12 | message { 'You answered incorrectly.' }
13 |
14 | points { 10 }
15 |
16 | question { registration.exam_version.db_questions.find_by(index: 0) }
17 | part { question.parts.find_by(index: 0) }
18 | body_item { part.body_items.find_by(index: 0) }
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/test/factories/grading_lock.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :grading_lock do
5 | transient do
6 | staff_registration { create(:staff_registration) }
7 | end
8 |
9 | registration
10 | grader { staff_registration.user }
11 |
12 | question { registration.exam_version.db_questions.find_by(index: 0) }
13 | part { question.parts.find_by(index: 0) }
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/test/factories/message.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :message do
5 | transient do
6 | prof_reg { create(:professor_course_registration, course: registration.exam.course) }
7 | end
8 |
9 | sender { prof_reg.user }
10 | registration
11 | body { 'Read the directions for that question more carefully..' }
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/test/factories/proctor_registration.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :proctor_registration do
5 | user
6 | exam
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/test/factories/professor_course_registrations.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :professor_course_registration do
5 | course
6 | user
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/test/factories/registration.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :registration do
5 | transient do
6 | student_registration { create(:student_registration, course: exam_version.course) }
7 | end
8 |
9 | user { student_registration.user }
10 | exam_version
11 |
12 | # Student starts at the start of their window
13 | trait :early_start do
14 | start_time { accommodated_start_time }
15 | end
16 |
17 | # Student starts 1/8 of the way into their window
18 | trait :normal_start do
19 | start_time { accommodated_start_time + (accommodated_duration / 8.0) }
20 | end
21 |
22 | # Student starts with 1/4 of their time remaining in their window
23 | trait :late_start do
24 | start_time { accommodated_end_time - (accommodated_duration / 4.0) }
25 | end
26 |
27 | trait :done do
28 | early_start
29 | end_time { start_time + effective_duration }
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/test/factories/room.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :room do
5 | transient do
6 | sequence(:room_number, 200)
7 | end
8 |
9 | exam
10 | name { "Richards #{room_number}" }
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/test/factories/room_announcement.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :room_announcement do
5 | room
6 | body { "Hello, all students in #{room.name}!" }
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/test/factories/section.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :section do
5 | lecture
6 | course
7 | sequence(:bottlenose_id)
8 |
9 | trait :lecture do
10 | title { 'Lecture (TF 11:45-1:25)' }
11 | end
12 |
13 | trait :lab do
14 | title { 'Lab (F 1:25-3:15)' }
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/factories/snapshot.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :snapshot do
5 | registration
6 | answers { registration.exam_version.default_answers }
7 |
8 | trait :long_answers do
9 | answers do
10 | JSON.parse(Rails.root.join('test/fixtures/files/long-snapshot.json').read)
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/test/factories/staff_registration.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :staff_registration do
5 | user
6 | section
7 |
8 | trait :ta do
9 | ta { true }
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/test/factories/student_question.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :student_question do
5 | registration
6 | body { 'Am I allowed to use toBinaryString?' }
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/test/factories/student_registration.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :student_registration do
5 | transient do
6 | course { create(:course) }
7 | end
8 |
9 | user
10 | section { create(:section, course: course) }
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/test/factories/term.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :term do
5 | sequence(:year, 2000)
6 | semester { Term.semesters.values.sample }
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/test/factories/upload.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative '../helpers/upload'
4 |
5 | FactoryBot.define do
6 | factory :upload do
7 | cs2500_v1
8 |
9 | trait :blank do
10 | transient do
11 | file_name { 'blank' }
12 | end
13 | end
14 |
15 | trait :cs2500_v1 do
16 | transient do
17 | file_name { 'cs2500midterm-v1' }
18 | end
19 | end
20 |
21 | trait :cs2500_v2 do
22 | transient do
23 | file_name { 'cs2500midterm-v2' }
24 | end
25 | end
26 |
27 | trait :cs3500_v1 do
28 | transient do
29 | file_name { 'cs3500final-v1' }
30 | end
31 | end
32 |
33 | trait :cs3500_v2 do
34 | transient do
35 | file_name { 'cs3500final-v2' }
36 | end
37 | end
38 |
39 | trait :extra_credit do
40 | transient do
41 | file_name { 'extra-credits' }
42 | end
43 | end
44 |
45 | initialize_with do
46 | UploadTestHelper.with_test_uploaded_fixture_zip file_name do |real_upload|
47 | Upload.new(real_upload)
48 | end
49 | end
50 |
51 | to_create do
52 | # deliberately do nothing
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/test/factories/user.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :user do
5 | transient do
6 | sequence :num
7 | end
8 |
9 | username { "user#{num}" }
10 | encrypted_password { Devise::Encryptor.digest(User, username) }
11 | display_name { username }
12 | email { "#{username}@localhost.localdomain" }
13 | nuid { 100_000_000 + num }
14 |
15 | factory :admin do
16 | admin { true }
17 | display_name { 'Admin' }
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/test/factories/version_announcement.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :version_announcement do
5 | exam_version
6 | body { "Hello all students, welcome to #{exam_version.exam.name}!" }
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/test/fixtures/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeGrade/hourglass/12b7f02381b8445505bf9eadf0e48a4bb4bdfc50/test/fixtures/.keep
--------------------------------------------------------------------------------
/test/fixtures/files/blank/exam.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | policies: []
3 | contents:
4 | instructions: ""
5 | questions:
6 | - description: "Placeholder"
7 | separateSubparts: false
8 | parts:
9 | - description: "Placeholder"
10 | points: 1
11 | body:
12 | - "Placeholder"
13 |
--------------------------------------------------------------------------------
/test/fixtures/files/cs2500midterm-v1/files/q1/all/src/packageone/Example.java:
--------------------------------------------------------------------------------
1 | package packageone;
2 |
3 | class Main {
4 | public static void main(String[] args) {
5 | System.out.println("hello!");
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/test/fixtures/files/cs2500midterm-v1/files/q1/all/src/packagetwo/Example2.java:
--------------------------------------------------------------------------------
1 | package packagetwo;
2 |
3 | ~ro:1:s~// This should be locked to the left
4 | // and continues to here
5 | ~ro:1:e~class Foo {
6 |
7 | ~ro:2:s~// This should be locked in the middle~ro:2:e~
8 |
9 | ~ro:3:s~}~ro:3:e~
10 |
--------------------------------------------------------------------------------
/test/fixtures/files/cs2500midterm-v1/files/q1/p1/anything.txt:
--------------------------------------------------------------------------------
1 | Question 1 part one.
2 |
--------------------------------------------------------------------------------
/test/fixtures/files/cs2500midterm-v1/files/test.txt:
--------------------------------------------------------------------------------
1 | this is a file for the entire exam
2 |
--------------------------------------------------------------------------------
/test/fixtures/files/cs2500midterm-v2/files/q1/all/src/packageone/Example.java:
--------------------------------------------------------------------------------
1 | package packageone;
2 |
3 | class Main {
4 | public static void main(String[] args) {
5 | System.out.println("hello!");
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/test/fixtures/files/cs2500midterm-v2/files/q1/all/src/packagetwo/Example2.java:
--------------------------------------------------------------------------------
1 | package packagetwo;
2 |
3 | ~ro:1:s~// This should be locked to the left
4 | // and continues to here
5 | ~ro:1:e~class Foo {
6 |
7 | ~ro:2:s~// This should be locked in the middle~ro:2:e~
8 |
9 | ~ro:3:s~}~ro:3:e~
10 |
--------------------------------------------------------------------------------
/test/fixtures/files/cs2500midterm-v2/files/q1/p1/anything.txt:
--------------------------------------------------------------------------------
1 | Question 1 part one.
2 |
--------------------------------------------------------------------------------
/test/fixtures/files/cs2500midterm-v2/files/test.txt:
--------------------------------------------------------------------------------
1 | this is a file for the entire exam
2 |
--------------------------------------------------------------------------------
/test/fixtures/files/cs3500final-v1/exam.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | policies:
3 | - IGNORE_LOCKDOWN
4 | - TOLERATE_WINDOWED
5 | contents:
6 | instructions: Exam instructions here...
7 | questions:
8 | - description: "Something to answer."
9 | separateSubparts: false
10 | parts:
11 | - description: "A part."
12 | points: 1
13 | body:
14 | - "Some instructions here for the first part."
15 |
--------------------------------------------------------------------------------
/test/fixtures/files/cs3500final-v2/exam.yaml:
--------------------------------------------------------------------------------
1 | {
2 | "info": {
3 | "policies": [
4 | "IGNORE_LOCKDOWN",
5 | "TOLERATE_WINDOWED"
6 | ],
7 | "contents": {
8 | "instructions": "Exam instructions here...",
9 | "reference": [],
10 | "questions": [
11 | {
12 | "reference": [],
13 | "description": "Something to answer.",
14 | "separateSubparts": false,
15 | "parts": [
16 | {
17 | "reference": [],
18 | "description": "A part.",
19 | "points": 1.0,
20 | "body": [
21 | "Some instructions here for the first part."
22 | ]
23 | }
24 | ]
25 | }
26 | ]
27 | }
28 | },
29 | "files": []
30 | }
31 |
--------------------------------------------------------------------------------
/test/fixtures/files/tutorial/files/several/and/seek.java:
--------------------------------------------------------------------------------
1 | import tester.*;
2 |
3 | class Seek {
4 | int foundIt() {
5 | return 42;
6 | }
7 | }
--------------------------------------------------------------------------------
/test/fixtures/files/tutorial/files/several/hide.rkt:
--------------------------------------------------------------------------------
1 | (define (hide x)
2 | (* x 2))
--------------------------------------------------------------------------------
/test/fixtures/files/tutorial/files/singleFile.java:
--------------------------------------------------------------------------------
1 | import tester.*;
2 |
3 | class SingleFile {
4 | String hideAndSeek() {
5 | return "Found you!";
6 | }
7 | }
--------------------------------------------------------------------------------
/test/helpers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeGrade/hourglass/12b7f02381b8445505bf9eadf0e48a4bb4bdfc50/test/helpers/.keep
--------------------------------------------------------------------------------
/test/helpers/upload.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module UploadTestHelper
4 | def self.with_temp_zip(glob_path)
5 | ArchiveUtils.mktmpdir do |path|
6 | dir = Pathname.new path
7 | zip = dir.join("#{name}.zip")
8 | ArchiveUtils.create_zip zip, Dir.glob(glob_path)
9 | file = File.new(zip)
10 | yield file
11 | end
12 | end
13 |
14 | def with_test_uploaded_zip(name, mime_type = nil)
15 | with_temp_zip name do |f|
16 | yield Rack::Test::UploadedFile.new(f.path, mime_type, false)
17 | end
18 | end
19 |
20 | def self.with_temp_fixture_zip(name, &block)
21 | with_temp_zip(Rails.root.join('test', 'fixtures', 'files', name, '**'), &block)
22 | end
23 |
24 | def self.with_test_uploaded_fixture_zip(name, mime_type = nil)
25 | with_temp_fixture_zip name do |f|
26 | yield Rack::Test::UploadedFile.new(f.path, mime_type, false)
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/test/integration/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeGrade/hourglass/12b7f02381b8445505bf9eadf0e48a4bb4bdfc50/test/integration/.keep
--------------------------------------------------------------------------------
/test/mailers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeGrade/hourglass/12b7f02381b8445505bf9eadf0e48a4bb4bdfc50/test/mailers/.keep
--------------------------------------------------------------------------------
/test/models/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeGrade/hourglass/12b7f02381b8445505bf9eadf0e48a4bb4bdfc50/test/models/.keep
--------------------------------------------------------------------------------
/test/models/anomaly_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class AnomalyTest < ActiveSupport::TestCase
6 | test 'factory creates valid anomaly' do
7 | assert build(:anomaly).valid?
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/test/models/course_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class CourseTest < ActiveSupport::TestCase
6 | test 'course factory builds valid course' do
7 | assert build(:course).valid?
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/test/models/message_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class MessageTest < ActiveSupport::TestCase
6 | test 'factory creates valid messages' do
7 | reg = create(:registration)
8 | msg = build(:message, registration: reg)
9 | assert msg.valid?
10 | assert msg.save
11 | assert msg.sender.sent_messages.include? msg
12 | assert msg.registration.messages.include? msg
13 | end
14 |
15 | test 'should not save message without sender' do
16 | msg = build(:message, sender: nil)
17 | assert_not msg.save
18 | assert_match(/Sender must exist/, msg.errors.full_messages.to_sentence)
19 | end
20 |
21 | test 'students cannot send messages to other students' do
22 | ev = create(:exam_version)
23 | reg = build(:registration, exam_version: ev)
24 | reg2 = build(:registration, exam_version: ev)
25 | msg = build(
26 | :message,
27 | {
28 | sender: reg.user,
29 | registration: reg2,
30 | body: 'hi',
31 | },
32 | )
33 | assert_not msg.valid?
34 | assert_match(/must be a proctor or professor/, msg.errors[:sender].first)
35 | assert_not msg.save
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/test/models/question_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class QuestionTest < ActiveSupport::TestCase
6 | test 'factory builds valid question' do
7 | assert build(:student_question).valid?
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/test/models/room_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class RoomTest < ActiveSupport::TestCase
6 | test 'factory creates valid room' do
7 | assert build(:room).valid?
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/test/models/snapshot_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class SnapshotTest < ActiveSupport::TestCase
6 | # test "the truth" do
7 | # assert true
8 | # end
9 | end
10 |
--------------------------------------------------------------------------------
/test/models/upload_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class UploadTest < ActiveSupport::TestCase
6 | test 'confirm process_marks doesnt trim too much' do
7 | text = 'text'
8 | marked_code = "~ro:1:s~#{text}~ro:1:e~"
9 | 4.times do |num_trailing_lines|
10 | test_string = marked_code + ("\n" * num_trailing_lines)
11 | marks = MarksProcessor.process_marks(test_string)
12 | assert_equal [
13 | {
14 | from: { line: 0, ch: 0 },
15 | to: { line: 0, ch: text.length },
16 | options: { inclusiveLeft: true, inclusiveRight: num_trailing_lines < 2 },
17 | },
18 | ], marks[:marks]
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/test/models/user_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class UserTest < ActiveSupport::TestCase
6 | test 'standard user not an admin' do
7 | assert_not build(:user).admin?
8 | end
9 |
10 | test 'admin factory builds admins' do
11 | assert build(:admin).admin?
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/test/models/version_announcement_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class VersionAnnouncementTest < ActiveSupport::TestCase
6 | def setup
7 | @version = create(:exam_version)
8 | end
9 | test 'factory creates valid version announcement' do
10 | assert build(:version_announcement, exam_version: @version).valid?
11 | end
12 |
13 | test 'should save valid announcement' do
14 | announcement = build(:version_announcement, exam_version: @version)
15 | assert announcement.save
16 | end
17 |
18 | test 'should not save announcement without body' do
19 | announcement = build(:version_announcement, exam_version: @version, body: '')
20 | assert_not announcement.save
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/test/system/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeGrade/hourglass/12b7f02381b8445505bf9eadf0e48a4bb4bdfc50/test/system/.keep
--------------------------------------------------------------------------------
/tmp/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeGrade/hourglass/12b7f02381b8445505bf9eadf0e48a4bb4bdfc50/tmp/.keep
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": false,
4 | "emitDecoratorMetadata": true,
5 | "experimentalDecorators": true,
6 | "module": "esnext",
7 | "moduleResolution": "node",
8 | "sourceMap": true,
9 | "target": "esnext",
10 | "jsx": "preserve",
11 | "allowSyntheticDefaultImports": true,
12 | "baseUrl": "app/packs/components",
13 | "skipLibCheck": true,
14 | "paths": {
15 | "@hourglass/*": ["./*"],
16 | "@grading/*": ["./workflows/grading/*"],
17 | "@student/*": ["./workflows/student/*"],
18 | "@proctor/*": ["./workflows/proctor/*"],
19 | "@professor/*": ["./workflows/professor/*"]
20 | }
21 | },
22 | "exclude": ["**/*.spec.ts", "node_modules", "vendor", "public"],
23 | "compileOnSave": true
24 | }
25 |
--------------------------------------------------------------------------------
/vendor/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeGrade/hourglass/12b7f02381b8445505bf9eadf0e48a4bb4bdfc50/vendor/.keep
--------------------------------------------------------------------------------