Skip to content
import { Compartment, EditorState } from '@codemirror/state';
import {
EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter,
drawSelection, rectangularSelection, dropCursor, highlightSpecialChars,
highlightTrailingWhitespace,
} from '@codemirror/view';
import {
defaultKeymap, history, historyKeymap, indentWithTab,
} from '@codemirror/commands';
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
import {
HighlightStyle,
syntaxHighlighting, indentOnInput, bracketMatching,
foldGutter, foldKeymap, indentUnit, StreamLanguage,
} from '@codemirror/language';
import {
autocompletion, completionKeymap,
closeBrackets, closeBracketsKeymap,
} from '@codemirror/autocomplete';
import { python } from '@codemirror/lang-python';
import { markdown } from '@codemirror/lang-markdown';
import { yaml as yamlStreamParser } from '@codemirror/legacy-modes/mode/yaml';
import { tags } from '@lezer/highlight';
import { vim, Vim } from '@replit/codemirror-vim';
import { emacs } from '@replit/codemirror-emacs';
let anyEditorChangedFlag = false;
export function anyEditorChanged() {
return anyEditorChangedFlag;
}
export function resetAnyEditorChanged() {
anyEditorChangedFlag = false;
}
const myListener = new Compartment();
const rlDefaultKeymap = [
...closeBracketsKeymap,
...defaultKeymap,
...searchKeymap,
...historyKeymap,
...completionKeymap,
...foldKeymap,
indentWithTab,
];
// Based on https://discuss.codemirror.net/t/dynamic-light-mode-dark-mode-how/4709/5
// Use a class highlight style, so we can handle things in CSS.
const highlightStyle = HighlightStyle.define([
{ tag: tags.atom, class: 'cmt-atom' },
{ tag: tags.comment, class: 'cmt-comment' },
{ tag: tags.keyword, class: 'cmt-keyword' },
{ tag: tags.literal, class: 'cmt-literal' },
{ tag: tags.number, class: 'cmt-number' },
{ tag: tags.operator, class: 'cmt-operator' },
{ tag: tags.separator, class: 'cmt-separator' },
{ tag: tags.string, class: 'cmt-string' },
]);
const yaml = () => StreamLanguage.define(yamlStreamParser);
const defaultExtensionsBase = [
lineNumbers(),
history(),
foldGutter(),
indentOnInput(),
drawSelection(),
EditorState.allowMultipleSelections.of(true),
dropCursor(),
syntaxHighlighting(highlightStyle, { fallback: true }),
bracketMatching(),
closeBrackets(),
autocompletion(),
rectangularSelection(),
highlightActiveLine(),
highlightActiveLineGutter(),
highlightSelectionMatches(),
highlightSpecialChars(),
highlightTrailingWhitespace(),
];
// based on https://codemirror.net/docs/migration/
export function editorFromTextArea(textarea, extensions, autofocus, additionalKeys) {
// vim/emacs must come before other extensions
extensions.push(
...defaultExtensionsBase,
keymap.of([
...rlDefaultKeymap,
...additionalKeys,
]),
EditorView.updateListener.of((viewUpdate) => {
if (viewUpdate.docChanged) {
anyEditorChangedFlag = true;
}
}),
myListener.of(EditorView.updateListener.of(
() => { },
)),
);
if (textarea.disabled || textarea.readOnly) {
extensions.push(
EditorState.readOnly.of(true),
EditorView.editable.of(false),
);
}
const view = new EditorView({ doc: textarea.value, extensions });
textarea.parentNode.insertBefore(view.dom, textarea);
// eslint-disable-next-line no-param-reassign
textarea.style.display = 'none';
if (textarea.form) {
textarea.form.addEventListener('submit', () => {
// eslint-disable-next-line no-param-reassign
textarea.value = view.state.doc.toString();
});
}
textarea.classList.add('rl-managed-by-codemirror');
if (autofocus) {
document.addEventListener('DOMContentLoaded', () => {
view.focus();
});
}
return view;
}
export function setListener(view, fn) {
view.dispatch({
effects: myListener.reconfigure(
EditorView.updateListener.of(fn),
),
});
}
Vim.defineEx('write', 'w', (cm) => {
const form = cm.cm6.dom.closest('form');
if (form) {
// prefer 'submit' over 'save' on flow pages
let submitButton = form.querySelector("input[type='submit'][name='submit']");
if (submitButton) {
anyEditorChangedFlag = false;
submitButton.click();
return;
}
submitButton = form.querySelector("input[type='submit']");
if (submitButton) {
anyEditorChangedFlag = false;
submitButton.click();
}
}
});
export {
EditorState,
EditorView,
indentUnit,
vim, emacs,
python, markdown, yaml,
};
/* eslint-disable func-names */
import './base';
import jQuery from 'jquery';
import datatables from 'datatables.net';
import datatablesBs from 'datatables.net-bs5/js/dataTables.bootstrap5';
import 'datatables.net-bs5/css/dataTables.bootstrap5.css';
import datatablesFixedColumns from 'datatables.net-fixedcolumns/js/dataTables.fixedColumns';
import 'datatables.net-fixedcolumns-bs5/css/fixedColumns.bootstrap5.css';
import 'datatables.net-fixedcolumns-bs5/js/fixedColumns.bootstrap5';
/* eslint-disable camelcase, import/extensions */
import language_es_CO from 'datatables.net-plugins/i18n/es-CO.mjs';
import language_es_CL from 'datatables.net-plugins/i18n/es-CL.mjs';
import language_es_MX from 'datatables.net-plugins/i18n/es-MX.mjs';
import language_ca from 'datatables.net-plugins/i18n/ca.mjs';
import language_en_GB from 'datatables.net-plugins/i18n/en-GB.mjs';
import language_pt_BR from 'datatables.net-plugins/i18n/pt-BR.mjs';
import language_it_IT from 'datatables.net-plugins/i18n/it-IT.mjs';
import language_fr_FR from 'datatables.net-plugins/i18n/fr-FR.mjs';
import language_nl_NL from 'datatables.net-plugins/i18n/nl-NL.mjs';
import language_fa from 'datatables.net-plugins/i18n/fa.mjs';
import language_ru from 'datatables.net-plugins/i18n/ru.mjs';
import language_pl from 'datatables.net-plugins/i18n/pl.mjs';
import language_zh from 'datatables.net-plugins/i18n/zh.mjs';
import language_he from 'datatables.net-plugins/i18n/he.mjs';
import language_ar from 'datatables.net-plugins/i18n/ar.mjs';
import language_ro from 'datatables.net-plugins/i18n/ro.mjs';
import language_lv from 'datatables.net-plugins/i18n/lv.mjs';
import language_vi from 'datatables.net-plugins/i18n/vi.mjs';
import language_es_ES from 'datatables.net-plugins/i18n/es-ES.mjs';
import language_km from 'datatables.net-plugins/i18n/km.mjs';
import language_sr_SP from 'datatables.net-plugins/i18n/sr-SP.mjs';
import language_lt from 'datatables.net-plugins/i18n/lt.mjs';
import language_cs from 'datatables.net-plugins/i18n/cs.mjs';
import language_sk from 'datatables.net-plugins/i18n/sk.mjs';
import language_sv_SE from 'datatables.net-plugins/i18n/sv-SE.mjs';
import language_id from 'datatables.net-plugins/i18n/id.mjs';
import language_tr from 'datatables.net-plugins/i18n/tr.mjs';
import language_pt_PT from 'datatables.net-plugins/i18n/pt-PT.mjs';
import language_de_DE from 'datatables.net-plugins/i18n/de-DE.mjs';
import language_sq from 'datatables.net-plugins/i18n/sq.mjs';
import language_da from 'datatables.net-plugins/i18n/da.mjs';
import language_zh_HANT from 'datatables.net-plugins/i18n/zh-HANT.mjs';
import language_bn from 'datatables.net-plugins/i18n/bn.mjs';
import language_hu from 'datatables.net-plugins/i18n/hu.mjs';
import language_th from 'datatables.net-plugins/i18n/th.mjs';
import language_eu from 'datatables.net-plugins/i18n/eu.mjs';
import language_no_NB from 'datatables.net-plugins/i18n/no-NB.mjs';
import language_hr from 'datatables.net-plugins/i18n/hr.mjs';
import language_uz from 'datatables.net-plugins/i18n/uz.mjs';
import language_el from 'datatables.net-plugins/i18n/el.mjs';
import language_gl from 'datatables.net-plugins/i18n/gl.mjs';
import language_uk from 'datatables.net-plugins/i18n/uk.mjs';
import language_ka from 'datatables.net-plugins/i18n/ka.mjs';
import language_bg from 'datatables.net-plugins/i18n/bg.mjs';
import language_sl from 'datatables.net-plugins/i18n/sl.mjs';
import language_az_AZ from 'datatables.net-plugins/i18n/az-AZ.mjs';
/* eslint-enable camelcase, import/extensions */
function stripLanguageTag(localeId) {
const hyphenPos = localeId.indexOf('-');
if (hyphenPos !== -1) {
return localeId.substring(0, hyphenPos);
}
return null;
}
function addFallbacks(tbl) {
const newtbl = { ...tbl };
Object.keys(tbl).forEach((localeId) => {
const strippedId = stripLanguageTag(localeId);
if (strippedId) {
if (!(strippedId in newtbl)) {
newtbl[strippedId] = tbl[localeId];
}
}
});
return newtbl;
}
/* eslint-disable camelcase, quote-props */
const i18nTables = addFallbacks({
'es-CO': language_es_CO,
'es-CL': language_es_CL,
'es-MX': language_es_MX,
'ca': language_ca,
'en-GB': language_en_GB,
'pt-BR': language_pt_BR,
'it-IT': language_it_IT,
'fr-FR': language_fr_FR,
'nl-NL': language_nl_NL,
'fa': language_fa,
'ru': language_ru,
'pl': language_pl,
'zh': language_zh,
'he': language_he,
'ar': language_ar,
'ro': language_ro,
'lv': language_lv,
'vi': language_vi,
'es-ES': language_es_ES,
'km': language_km,
'sr-SP': language_sr_SP,
'lt': language_lt,
'cs': language_cs,
'sk': language_sk,
'sv-SE': language_sv_SE,
'id': language_id,
'tr': language_tr,
'pt-PT': language_pt_PT,
'de-DE': language_de_DE,
'sq': language_sq,
'da': language_da,
'zh-HANT': language_zh_HANT,
'bn': language_bn,
'hu': language_hu,
'th': language_th,
'eu': language_eu,
'no-NB': language_no_NB,
'hr': language_hr,
'uz': language_uz,
'el': language_el,
'gl': language_gl,
'uk': language_uk,
'ka': language_ka,
'bg': language_bg,
'sl': language_sl,
'az-AZ': language_az_AZ,
});
/* eslint-enable camelcase, quote-props */
datatables(window, jQuery);
datatablesBs(window, jQuery);
datatablesFixedColumns(window, jQuery);
// eslint-disable-next-line import/prefer-default-export
export function getI18nTable(localeId) {
if (localeId in i18nTables) {
return i18nTables[localeId];
}
const strippedId = stripLanguageTag(localeId);
if (strippedId in i18nTables) {
return i18nTables[strippedId];
}
return null;
}
// {{{ custom sort
function removeTags(s) {
return s.replace(/(<([^>]+)>)/g, '');
}
jQuery.extend(jQuery.fn.dataTableExt.oSort, {
'name-asc': function (s1, s2) {
return removeTags(s1).localeCompare(removeTags(s2));
},
'name-desc': function (s1, s2) {
return removeTags(s2).localeCompare(removeTags(s1));
},
});
// }}}
// raw list of datatables translations with 'good' completion, from
// https://datatables.net/plug-ins/i18n/:
// es-CO
// es-CL
// es-MX
// ca
// en-GB
// pt-BR
// it-IT
// fr-FR
// nl-NL
// fa
// ru
// pl
// zh
// he
// ar
// ro
// lv
// vi
// es-ES
// km
// sr-SP
// lt
// cs
// sk
// sv-SE
// id
// tr
// pt-PT
// de-DE
// sq
// da
// zh-HANT
// bn
// hu
// th
// eu
// no-NB
// hr
// uz
// el
// gl
// uk
// ka
// bg
// sl
// az-AZ
// vim: foldmethod=marker
import { Calendar } from '@fullcalendar/core';
import dayGridPlugin from '@fullcalendar/daygrid';
import timeGridPlugin from '@fullcalendar/timegrid';
import listPlugin from '@fullcalendar/list';
import allLocales from '@fullcalendar/core/locales-all';
/* eslint-disable-next-line import/prefer-default-export */
export function setupCalendar(domEl, events, initialDate, locale) {
const calendar = new Calendar(domEl, {
plugins: [
dayGridPlugin,
timeGridPlugin,
listPlugin,
],
locales: allLocales,
initialView: 'dayGridMonth',
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,listWeek',
},
initialDate,
events,
locale,
});
calendar.render();
}
// This ensures that the '$' and jQuery aliases are set before other modules
// use them.
import jQuery from 'jquery';
window.$ = jQuery;
window.jQuery = jQuery;
// Copyright (C) 2017 University of Illinois Board of Trustees
// Copyright (C) 2024 Marijn Haverbeke
// Contains parts of
// https://github.com/ProseMirror/prosemirror-markdown/blob/99b6f0a6c377a2c010320f4fdd883e4868aaf122/src/from_markdown.ts
// Used under the MIT license.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import 'katex/dist/katex.min.css';
// prosemirror imports
import { Schema, Node, Slice } from 'prosemirror-model';
import { EditorView } from 'prosemirror-view';
import { EditorState, Plugin, PluginKey } from 'prosemirror-state';
import { schema as basicSchema } from 'prosemirror-schema-basic';
import { addListNodes } from 'prosemirror-schema-list';
import {
chainCommands, deleteSelection, selectNodeBackward, joinBackward,
} from 'prosemirror-commands';
import { keymap } from 'prosemirror-keymap';
import { inputRules } from 'prosemirror-inputrules';
import 'prosemirror-view/style/prosemirror.css';
import 'prosemirror-menu/style/menu.css';
import { exampleSetup } from 'prosemirror-example-setup';
import 'prosemirror-example-setup/style/style.css';
import { MarkdownParser } from 'prosemirror-markdown';
import MarkdownIt from 'markdown-it';
import markdownItMath from '@vscode/markdown-it-katex';
import {
mathPlugin, mathBackspaceCmd, insertMathCmd, mathSerializer,
makeBlockMathInputRule, makeInlineMathInputRule,
REGEX_INLINE_MATH_DOLLARS, REGEX_BLOCK_MATH_DOLLARS,
// I've no idea why eslint can't find the module; rollup can.
// eslint-disable-next-line import/no-unresolved
} from '@benrbray/prosemirror-math';
import '@benrbray/prosemirror-math/dist/prosemirror-math.css';
let anyEditorChangedFlag = false;
export function anyEditorChanged() {
return anyEditorChangedFlag;
}
export function resetAnyEditorChanged() {
anyEditorChangedFlag = false;
}
const schema = new Schema({
nodes: addListNodes(basicSchema.spec.nodes, 'paragraph block*', 'block')
.remove('image')
.addToEnd('math_inline', {
group: 'inline math',
content: 'text*', // important!
inline: true, // important!
atom: true, // important!
toDOM: () => ['math-inline', { class: 'math-node' }, 0],
parseDOM: [{
tag: 'math-inline', // important!
}],
})
.addToEnd('math_display', {
group: 'block math',
content: 'text*', // important!
atom: true, // important!
code: true, // important!
toDOM: () => ['math-display', { class: 'math-node' }, 0],
parseDOM: [{
tag: 'math-display', // important!
}],
}),
marks: basicSchema.spec.marks,
});
const inlineMathInputRule = makeInlineMathInputRule(
REGEX_INLINE_MATH_DOLLARS,
schema.nodes.math_inline,
);
const blockMathInputRule = makeBlockMathInputRule(
REGEX_BLOCK_MATH_DOLLARS,
schema.nodes.math_display,
);
const readonlyPlugin = new Plugin({
key: new PluginKey('readonly'),
// Allow selections but prevent any other changes
filterTransaction: (transaction) => transaction.docChanged === false,
});
const changeListenerPlugin = new Plugin({
key: new PluginKey('readonly'),
filterTransaction: (transaction) => {
if (transaction.docChanged) {
anyEditorChangedFlag = true;
}
return true;
},
});
// {{{ handle markdown paste
function listIsTight(tokens, i) {
// eslint-disable-next-line no-plusplus, no-param-reassign
while (++i < tokens.length) {
if (tokens[i].type !== 'list_item_open') return tokens[i].hidden;
}
return false;
}
function markdownToProsemirrorParser() {
const mdit = MarkdownIt('commonmark', { html: false }).use(markdownItMath, {
enableBareBlocks: true,
});
return new MarkdownParser(schema, mdit, {
blockquote: { block: 'blockquote' },
paragraph: { block: 'paragraph' },
list_item: { block: 'list_item' },
bullet_list: {
block: 'bullet_list',
getAttrs: (_, tokens, i) => ({ tight: listIsTight(tokens, i) }),
},
ordered_list: {
block: 'ordered_list',
getAttrs: (tok, tokens, i) => ({
order: +tok.attrGet('start') || 1,
tight: listIsTight(tokens, i),
}),
},
heading: {
block: 'heading',
getAttrs: (tok) => ({ level: +tok.tag.slice(1) }),
},
code_block: { block: 'code_block', noCloseToken: true },
fence: {
block: 'code_block',
getAttrs: (tok) => ({ params: tok.info || '' }),
noCloseToken: true,
},
hr: { node: 'horizontal_rule' },
hardbreak: { node: 'hard_break' },
em: { mark: 'em' },
strong: { mark: 'strong' },
link: {
mark: 'link',
getAttrs: (tok) => ({
href: tok.attrGet('href'),
title: tok.attrGet('title') || null,
}),
},
code_inline: { mark: 'code', noCloseToken: true },
math_inline: { block: 'math_inline', noCloseToken: true },
math_block: { block: 'math_display', noCloseToken: true },
});
}
const pasteMarkdownPlugin = new Plugin({
props: {
handlePaste(view, event/* , slice */) {
const clipboardText = event.clipboardData.getData('text/plain');
if (!clipboardText) return false;
const doc = markdownToProsemirrorParser().parse(clipboardText);
const transaction = view.state.tr.replaceSelection(new Slice(doc.content, 0, 0));
view.dispatch(transaction);
return true;
},
},
});
// }}}
// eslint-disable-next-line import/prefer-default-export
export function editorFromTextArea(textarea, autofocus) {
const plugins = [
...exampleSetup({ schema }),
mathPlugin,
pasteMarkdownPlugin,
keymap({
'Mod-Space': insertMathCmd(schema.nodes.math_inline),
Backspace: chainCommands(
deleteSelection,
mathBackspaceCmd,
joinBackward,
selectNodeBackward,
),
}),
inputRules({ rules: [inlineMathInputRule, blockMathInputRule] }),
];
if (textarea.disabled || textarea.readOnly) {
plugins.push(readonlyPlugin);
}
// Change listener should be after readonly.
plugins.push(changeListenerPlugin);
let docJson = null;
if (textarea.value) {
docJson = JSON.parse(textarea.value);
}
let doc;
if (docJson) {
doc = Node.fromJSON(schema, docJson);
} else {
doc = schema.topNodeType.createAndFill();
}
const state = EditorState.create({ schema, plugins, doc });
const editorElt = document.createElement('div');
editorElt.classList.add('rl-prosemirror-container');
const view = new EditorView(editorElt, {
state,
clipboardTextSerializer: mathSerializer.serializeSlice,
});
if (autofocus) {
document.addEventListener('DOMContentLoaded', () => {
view.focus();
});
}
textarea.parentNode.insertBefore(editorElt, textarea);
// eslint-disable-next-line no-param-reassign
textarea.style.display = 'none';
if (textarea.form) {
textarea.form.addEventListener('submit', () => {
// eslint-disable-next-line no-param-reassign
textarea.value = JSON.stringify(view.state.doc.toJSON());
});
}
textarea.classList.add('rl-managed-by-prosemirror');
return view;
}
import jQuery from 'jquery';
export function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; ++i) { /* eslint-disable-line no-plusplus */
const cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) { /* eslint-disable-line prefer-template */
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
// {{{ file upload support
function embedUploadedFileViewer(mimeType, dataUrl) {
jQuery('#file_upload_viewer_div').html(
`<object id="file_upload_viewer" data='${dataUrl}' type='${mimeType}' style='width:100%; height: 80vh;'>
<p>(
Your browser reported itself unable to render <tt>${mimeType}</tt> inline.
)</p>
</object>`,
);
}
function matchUploadDataURL(dataUrl) {
// take apart data URL
const parts = dataUrl.match(/data:([^;]*)(;base64)?,([0-9A-Za-z+/]+)/);
return {
mimeType: parts[1],
encoding: parts[2],
base64Data: parts[3],
};
}
function convertUploadDataUrlToObjectUrl(dataUrlParts) {
// https://code.google.com/p/chromium/issues/detail?id=69227#37
const isWebKit = /WebKit/.test(navigator.userAgent);
if (isWebKit) {
// assume base64 encoding
const binStr = atob(dataUrlParts.base64Data);
// convert to binary in ArrayBuffer
const buf = new ArrayBuffer(binStr.length);
const view = new Uint8Array(buf);
for (let i = 0; i < view.length; i += 1) {
view[i] = binStr.charCodeAt(i);
}
const blob = new Blob([view], { type: dataUrlParts.mimeType });
return webkitURL.createObjectURL(blob); // eslint-disable-line no-undef
}
return null;
}
export function enablePreviewForFileUpload() {
let dataUrl = document.getElementById('file_upload_download_link').href;
const dataUrlParts = matchUploadDataURL(dataUrl);
const objUrl = convertUploadDataUrlToObjectUrl(dataUrlParts);
if (objUrl) {
dataUrl = objUrl;
}
jQuery('#file_upload_download_link').attr('href', dataUrl);
if (dataUrlParts.mimeType === 'application/pdf') {
embedUploadedFileViewer(dataUrlParts.mimeType, dataUrl);
}
}
// }}}
// {{{ grading ui: next/previous points field
// based on https://codemirror.net/addon/search/searchcursor.js (MIT)
const pointsRegexp = /\[pts:/g;
export function goToNextPointsField(view) {
pointsRegexp.lastIndex = view.state.selection.main.head;
const match = pointsRegexp.exec(view.state.doc.toString());
if (match) {
view.dispatch({ selection: { anchor: match.index + match[0].length } });
return true;
}
return false;
}
// based on https://stackoverflow.com/a/274094
function regexLastMatch(string, regex, startpos) {
if (!regex.global) {
throw new Error('Passed regex not global');
}
let start;
if (typeof (startpos) === 'undefined') {
start = string.length;
} else if (startpos < 0) {
start = 0;
} else {
start = startpos;
}
const stringToWorkWith = string.substring(0, start);
let match;
let lastMatch = null;
// eslint-disable-next-line no-param-reassign
regex.lastIndex = 0;
// eslint-disable-next-line no-cond-assign
while ((match = regex.exec(stringToWorkWith)) != null) {
lastMatch = match;
// eslint-disable-next-line no-param-reassign
regex.lastIndex = match.index + 1;
}
return lastMatch;
}
export function goToPreviousPointsField(view) {
const match = regexLastMatch(
view.state.doc.toString(),
pointsRegexp,
// "[pts:" is five characters
view.state.selection.main.head - 5,
);
if (match) {
view.dispatch({ selection: { anchor: match.index + match[0].length } });
return true;
}
return false;
}
// }}}
// {{{ grading UI: points spec processing
function parseFloatRobust(s) {
const result = Number.parseFloat(s);
if (Number.isNaN(result)) {
throw new Error(`Numeral not understood: ${s}`);
}
return result;
}
export function parsePointsSpecs(feedbackText) {
const result = [];
const pointsRegex = /\[pts:\s*([^\]]*)\]/g;
const pointsBodyRegex = /^([-0-9.]*)\s*((?:\/\s*[-0-9.]*)?)\s*((?:#[a-zA-Z_]\w*)?)\s*$/;
// eslint-disable-next-line no-constant-condition
while (true) {
const bodyMatch = pointsRegex.exec(feedbackText);
if (bodyMatch === null) {
break;
}
const pointsBody = bodyMatch[1];
const match = pointsBody.match(pointsBodyRegex);
if (match === null) {
throw new Error(`Points spec not understood: '${pointsBody}'`);
}
const [_fullMatch, pointsStr, maxPointsStr, identifierStr] = match;
let points = null;
if (pointsStr.length) {
points = parseFloatRobust(pointsStr);
}
let maxPoints = null;
if (maxPointsStr.length) {
maxPoints = parseFloatRobust(maxPointsStr.substring(1));
if (maxPoints <= 0) {
throw new Error(`Point denominator must be positive: '${pointsBody}'`);
}
}
let identifier = null;
if (identifierStr.length) {
identifier = identifierStr;
}
result.push({
points,
maxPoints,
identifier,
matchStart: bodyMatch.index,
matchLength: bodyMatch[0].length,
});
}
return result;
}
// }}}
// http://stackoverflow.com/a/30558011
const SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
// Match everything outside of normal chars and " (quote character)
const NON_ALPHANUMERIC_REGEXP = /([^#-~| |!])/g;
export function encodeEntities(value) {
return value
.replace(/&/g, '&amp;')
.replace(SURROGATE_PAIR_REGEXP, (val) => {
const hi = val.charCodeAt(0);
const low = val.charCodeAt(1);
return `&#${((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000};`;
})
.replace(NON_ALPHANUMERIC_REGEXP, (val) => `&#${val.charCodeAt(0)};`)
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
export function truncateText(s, length) {
if (s.length > length) {
return `${s.slice(0, length)}...`;
}
return s;
}
// vim: foldmethod=marker
# See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/
import os # noqa: F401
import os.path as path
_BASEDIR = path.dirname(path.abspath(__file__))
# {{{ database and site
SECRET_KEY = '<CHANGE ME TO SOME RANDOM STRING ONCE IN PRODUCTION>'
SECRET_KEY = "<CHANGE ME TO SOME RANDOM STRING ONCE IN PRODUCTION>"
ALLOWED_HOSTS = [
"relate.example.com",
"relate.example.edu",
]
# Configure the following as url as above.
RELATE_BASE_URL = "http://YOUR/RELATE/SITE/DOMAIN"
from django.utils.translation import gettext_noop # noqa
# Uncomment this to configure the site name of your relate instance.
# If not configured, "RELATE" will be used as default value.
# Use gettext_noop() if you want it to be discovered as an i18n literal
# for translation.
# RELATE_CUTOMIZED_SITE_NAME = gettext_noop("My RELATE")
# Uncomment this to use a real database. If left commented out, a local SQLite3
# database will be used, which is not recommended for production use.
#
# DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.postgresql_psycopg2',
# 'NAME': 'relate',
# 'USER': 'relate',
# 'PASSWORD': '<PASSWORD>',
# 'HOST': '127.0.0.1',
# 'PORT': '5432',
# "default": {
# "ENGINE": "django.db.backends.postgresql",
# "NAME": "relate",
# "USER": "relate",
# "PASSWORD": '<PASSWORD>',
# "HOST": '127.0.0.1',
# "PORT": '5432',
# }
# }
......@@ -38,9 +52,9 @@ RELATE_BASE_URL = "http://YOUR/RELATE/SITE/DOMAIN"
# broken in Python 33, as of 2016-08-01.
#
# CACHES = {
# 'default': {
# 'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache',
# 'LOCATION': '127.0.0.1:11211',
# "default": {
# "BACKEND": "django.core.cache.backends.memcached.PyLibMCCache",
# "LOCATION": '127.0.0.1:11211',
# }
# }
......@@ -49,6 +63,22 @@ DEBUG = True
TIME_ZONE = "America/Chicago"
# RELATE needs a message broker for long-running tasks.
#
# See here for options:
# http://docs.celeryproject.org/en/latest/userguide/configuration.html#broker-url
#
# The dev server will run fine without this, but any tasks that require
# queueing will just appear to hang. On Debian/Ubuntu, the following line
# should be enough to satisfy this requirement.
#
# apt-get install rabbitmq-server
CELERY_BROKER_URL = "amqp://"
# Set both of these to true if serving your site exclusively via HTTPS.
SESSION_COOKIE_SECURE = False
CSRF_COOKIE_SECURE = False
# }}}
# {{{ git storage
......@@ -56,22 +86,30 @@ TIME_ZONE = "America/Chicago"
# Your course git repositories will be stored under this directory.
# Make sure it's writable by your web user.
#
# The default below makes them sit side-by-side with your relate checkout,
# which makes sense for development, but you probably want to change this
# in production.
#
# The 'course identifiers' you enter will be directory names below this root.
# The "course identifiers" you enter will be directory names below this root.
#GIT_ROOT = "/some/where"
GIT_ROOT = ".."
# GIT_ROOT = "/some/where"
GIT_ROOT = path.join(_BASEDIR, "git-roots")
# }}}
# {{{ bulk storage
from django.core.files.storage import FileSystemStorage
# This must be a subclass of django.core.storage.Storage.
# This should *not* be MEDIA_ROOT, and the corresponding directory/storage location
# should *not* be accessible under a URL.
RELATE_BULK_STORAGE = FileSystemStorage(path.join(_BASEDIR, "bulk-storage"))
# }}}
# {{{ email
EMAIL_HOST = '127.0.0.1'
EMAIL_HOST_USER = ''
EMAIL_HOST_PASSWORD = ''
EMAIL_HOST = "127.0.0.1"
EMAIL_HOST_USER = ""
EMAIL_HOST_PASSWORD = ""
EMAIL_PORT = 25
EMAIL_USE_TLS = False
......@@ -85,15 +123,15 @@ ADMINS = (
)
# If your email service do not allow nonauthorized sender, uncomment the following
# statement and change the configurations above accordingly, noticing that all
# statement and change the configurations above accordingly, noticing that all
# emails will be sent using the EMAIL_ settings above.
#RELATE_EMAIL_SMTP_ALLOW_NONAUTHORIZED_SENDER = False
# RELATE_EMAIL_SMTP_ALLOW_NONAUTHORIZED_SENDER = False
# Advanced email settings if you want to configure multiple SMTPs for different
# purpose/type of emails. It is also very useful when
# "RELATE_EMAIL_SMTP_ALLOW_NONAUTHORIZED_SENDER" is False.
# If you want to enable this functionality, set the next line to True, and edit
# the next block with your cofigurations.
# the next block with your configurations.
RELATE_ENABLE_MULTIPLE_SMTP = False
if RELATE_ENABLE_MULTIPLE_SMTP:
......@@ -102,49 +140,58 @@ if RELATE_ENABLE_MULTIPLE_SMTP:
# For automatic email sent by site.
"robot": {
# You can use your preferred email backend.
'backend': 'djcelery_email.backends.CeleryEmailBackend',
'host': 'smtp.gmail.com',
'username': 'blah@blah.com',
'password': 'password',
'port': 587,
'use_tls': True,
"backend": "djcelery_email.backends.CeleryEmailBackend",
"host": "smtp.gmail.com",
"username": "blah@blah.com",
"password": "password",
"port": 587,
"use_tls": True,
},
# For emails that expect no reply for recipients, e.g., registration,
# reset password, etc.
"no_reply": {
'host': 'smtp.gmail.com',
'username': 'blah@blah.com',
'password': 'password',
'port': 587,
'use_tls': True,
"host": "smtp.gmail.com",
"username": "blah@blah.com",
"password": "password",
"port": 587,
"use_tls": True,
},
# For sending notifications like submission of flow sessions.
"notification": {
'host': 'smtp.gmail.com',
'username': 'blah@blah.com',
'password': 'password',
'port': 587,
'use_tls': True,
"host": "smtp.gmail.com",
"username": "blah@blah.com",
"password": "password",
"port": 587,
"use_tls": True,
},
# For sending feedback email to students in grading interface.
"grader_feedback": {
'host': 'smtp.gmail.com',
'username': 'blah@blah.com',
'password': 'password',
'port': 587,
'use_tls': True,
"host": "smtp.gmail.com",
"username": "blah@blah.com",
"password": "password",
"port": 587,
"use_tls": True,
},
# For student to send email to course staff in flow pages
"student_interact": {
'host': 'smtp.gmail.com',
'username': 'blah@blah.com',
'password': 'password',
'port': 587,
'use_tls': True,
"host": "smtp.gmail.com",
"username": "blah@blah.com",
"password": "password",
"port": 587,
"use_tls": True,
},
# For enrollment request email sent to course instructors
"enroll": {
"host": "smtp.gmail.com",
"username": "blah@blah.com",
"password": "password",
"port": 587,
"use_tls": True,
},
}
......@@ -155,6 +202,7 @@ if RELATE_ENABLE_MULTIPLE_SMTP:
NOTIFICATION_EMAIL_FROM = "Notification <notification_example@example.com>"
GRADER_FEEDBACK_EMAIL_FROM = "Feedback <feedback_example@example.com>"
STUDENT_INTERACT_EMAIL_FROM = "interaction <feedback_example@example.com>"
ENROLLMENT_EMAIL_FROM = "Enrollment <enroll@example.com>"
# }}}
......@@ -168,6 +216,7 @@ RELATE_SESSION_RESTART_COOLDOWN_SECONDS = 10
# {{{ sign-in methods
RELATE_SIGN_IN_BY_EMAIL_ENABLED = True
RELATE_SIGN_IN_BY_USERNAME_ENABLED = True
RELATE_REGISTRATION_ENABLED = False
RELATE_SIGN_IN_BY_EXAM_TICKETS_ENABLED = True
......@@ -175,66 +224,173 @@ RELATE_SIGN_IN_BY_EXAM_TICKETS_ENABLED = True
# See saml_config.py.example for help.
RELATE_SIGN_IN_BY_SAML2_ENABLED = False
RELATE_SOCIAL_AUTH_BACKENDS = (
# See https://python-social-auth.readthedocs.io/en/latest/
# for full list.
# "social_core.backends.google.GoogleOAuth2",
# CAUTION: Relate uses emails returned by the backend to match
# users. Only use backends that return verified emails.
)
# Your Google "Client ID"
# SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = ''
# Your Google "Client Secret"
# SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = ''
SOCIAL_AUTH_GOOGLE_OAUTH2_USE_UNIQUE_USER_ID = True
# When registering your OAuth2 app (and consent screen) with Google,
# specify the following authorized redirect URI:
# https://sitename.edu/social-auth/complete/google-oauth2/
# Blacklist these domains for social auth. This may be useful if there
# is a canonical way (e.g. SAML2) for members of that domain to
# sign in.
# RELATE_SOCIAL_AUTH_BLACKLIST_EMAIL_DOMAINS = {
# "illinois.edu": "Must use SAML2 to sign in."
# }
# }}}
# {{{ editable institutional id before verification?
# If set to False, user won't be able to edit institutional ID
# after submission. Set to False only when you trust your students
# or you don't want to verfiy insitutional ID they submit.
# or you don't want to verify insitutional ID they submit.
RELATE_EDITABLE_INST_ID_BEFORE_VERIFICATION = True
# If set to False, these fields will be hidden in the user profile form.
RELATE_SHOW_INST_ID_FORM = True
RELATE_SHOW_EDITOR_FORM = True
# }}}
# Whether disable "markdown.extensions.codehilite" when rendering page markdown.
# Default to True, as enable it sometimes crashes for some pages with code fences.
# For this reason, there will be a warning when the attribute is set to False when
# starting the server.
# RELATE_DISABLE_CODEHILITE_MARKDOWN_EXTENSION = True
# {{{ user full_name format
# RELATE's default full_name format is "'%s %s' % (first_name, last_name)",
# you can override it by supply a customized method/fuction, with
# "firstname" and "lastname" as its paramaters, and return a string.
# "firstname" and "lastname" as its parameters, and return a string.
# For example, you can define it like this:
#<code>
# def my_fullname_format(firstname, lastname)
# <code>
# def my_fullname_format(firstname, lastname):
# return "%s%s" % (last_name, first_name)
#</code>
# </code>
# and then uncomment the following line and enable it with:
#RELATE_USER_FULL_NAME_FORMAT_METHOD = my_fullname_format
# RELATE_USER_FULL_NAME_FORMAT_METHOD = my_fullname_format
# You can also import it from your custom module.
# You can also import it from your custom module, or use a dotted path of the
# method, i.e.:
# RELATE_USER_FULL_NAME_FORMAT_METHOD = "path.to.my_fullname_format"
# }}}
# {{{ system email appelation priority
# {{{ system email appellation priority
# RELATE's default email appelation of the receiver is a ordered list:
# RELATE's default email appellation of the receiver is a ordered list:
# ["first_name", "email", "username"], when first_name is not None
# (e.g, first_name = "Foo"), the email will be opened
# by "Dear Foo,". If first_name is None, then email will be used
# as appelation, so on and so forth.
# as appellation, so on and so forth.
# you can override the appelation priority by supply a customized list
# named RELATE_EMAIL_APPELATION_PRIORITY_LIST. The available
# you can override the appellation priority by supply a customized list
# named relate_email_appellation_priority_list. The available
# elements include first_name, last_name, get_full_name, email and
# username.
# RELATE_EMAIL_APPELATION_PRIORITY_LIST = [
# RELATE_EMAIL_APPELLATION_PRIORITY_LIST = [
# "full_name", "first_name", "email", "username"]
# }}}
# {{{ custom method for masking user profile
# When a participation, for example, teaching assistant, has limited access to
# students' profile (i.e., has_permission(pperm.view_participant_masked_profile)),
# a built-in mask method (which is based on pk of user instances) is used be
# default. The mask method can be overridden by the following a custom method, with
# user as the args.
# RELATE_USER_PROFILE_MASK_METHOD = "path.tomy_method
# For example, you can define it like this:
# <code>
# def my_mask_method(user):
# return "User_%s" % str(user.pk + 100)
# </code>
# and then uncomment the following line and enable it with:
# RELATE_USER_PROFILE_MASK_METHOD = my_mask_method
# You can also import it from your custom module, or use a dotted path of the
# method, i.e.:
# RELATE_USER_PROFILE_MASK_METHOD = "path.to.my_mask_method"
# }}}
# {{{ extra checks
# This allow user to add customized startup checks for user-defined modules
# using Django's system checks (https://docs.djangoproject.com/en/dev/ref/checks/)
# For example, define a `my_check_func in `my_module` with
# <code>
# def my_check_func(app_configs, **kwargs):
# return [list of error]
# </code>
# The configuration should be
# RELATE_STARTUP_CHECKS_EXTRA = ["my_module.my_check_func"]
# i.e., Each item should be the path to an importable check function.
# RELATE_STARTUP_CHECKS_EXTRA = []
# }}}
# {{{ overriding built-in templates
# Uncomment the following to enable templates overriding. It should be configured
# as a list/tuple of path(s).
# For example, if you the templates are in a folder named "my_templates" in the
# root dir of the project, with base.html (project template), course_base.html,
# and sign-in-email.txt (app templates) etc., are the templates you want to
# override, the structure of the files should look like:
# ...
# relate/
# local_settings.py
# my_templates/
# base.html
# ...
# course/
# course_base.html
# sign-in-email.txt
# ...
#
# import os.path
# RELATE_OVERRIDE_TEMPLATES_DIRS = [
# os.path.join(os.path.dirname(__file__), "my_templates"),
# os.path.join(os.path.dirname(__file__), "my_other_templates")
# ]
# }}}
# {{{ docker
# A string containing the image ID of the docker image to be used to run
# student Python code. Docker should download the image on first run.
RELATE_DOCKER_RUNPY_IMAGE = "inducer/relate-runpy-i386"
RELATE_DOCKER_RUNPY_IMAGE = "inducer/relate-runcode-python-amd64"
# A URL pointing to the Docker command interface which RELATE should use
# to spawn containers for student code.
RELATE_DOCKER_URL = "unix://var/run/docker.sock"
# for podman
# RELATE_DOCKER_URL = f"unix://run/user/{os.getuid()}/podman/podman.sock"
RELATE_DOCKER_TLS_CONFIG = None
......@@ -269,11 +425,27 @@ RELATE_SITE_ANNOUNCEMENT = None
# }}}
# Uncomment this to enable i18n, change 'en-us' to locale name your language.
# Uncomment this to enable i18n, change "en-us" to locale name your language.
# Make sure you have generated, translate and compile the message file of your
# language. If commented, RELATE will use default language 'en-us'.
#LANGUAGE_CODE='en-us'
# language. If commented, RELATE will use default language "en-us".
# LANGUAGE_CODE = "en-us"
# You can (and it's recommended to) override Django's built-in LANGUAGES settings
# if you want to filter languages allowed for course-specific languages.
# The format of languages should be a list/tuple of 2-tuples:
# (language_code, language_description). If there are entries with the same
# language_code, language_description will be using the one which comes latest.
# .If LANGUAGES is not configured, django.conf.global_settings.LANGUAGES will be
# used.
# Note: make sure LANGUAGE_CODE you used is also in LANGUAGES, if it is not
# the default "en-us". Otherwise translation of that language will not work.
# LANGUAGES = [
# ("en", "English"),
# ("zh-hans", "Simplified Chinese"),
# ("de", "German"),
# ]
# {{{ exams and testing
......@@ -299,6 +471,17 @@ RELATE_SITE_ANNOUNCEMENT = None
# "exams_only": True,
# },
# }
#
# # Automatically get denied facilities from PrairieTest
# result = {}
#
# from prairietest.utils import denied_ip_networks_at
# pt_facilities_networks = denied_ip_networks_at(now_datetime)
# for (course_id, facility_name), networks in pt_facilities_networks.items():
# fdata = result.setdefault(facility_name, {})
# fdata["exams_only"] = True
# fdata["ip_ranges"] = [*fdata.get("ip_ranges", []), *networks]
# return result
RELATE_FACILITIES = {
......@@ -320,86 +503,92 @@ RELATE_TICKET_MINUTES_VALID_AFTER_USE = 12*60
if RELATE_SIGN_IN_BY_SAML2_ENABLED:
from os import path
import saml2.saml
_BASEDIR = path.dirname(path.abspath(__file__))
_BASE_URL = 'https://relate.cs.illinois.edu'
import saml2.saml
_BASE_URL = "https://relate.cs.illinois.edu"
# see saml2-keygen.sh in this directory
_SAML_KEY_FILE = path.join(_BASEDIR, 'saml-config', 'sp-key.pem')
_SAML_CERT_FILE = path.join(_BASEDIR, 'saml-config', 'sp-cert.pem')
_SAML_KEY_FILE = path.join(_BASEDIR, "saml-config", "sp-key.pem")
_SAML_CERT_FILE = path.join(_BASEDIR, "saml-config", "sp-cert.pem")
SAML_ATTRIBUTE_MAPPING = {
'eduPersonPrincipalName': ('username',),
'iTrustUIN': ('institutional_id',),
'mail': ('email',),
'givenName': ('first_name', ),
'sn': ('last_name', ),
"eduPersonPrincipalName": ("username",),
"iTrustUIN": ("institutional_id",),
"mail": ("email",),
"givenName": ("first_name", ),
"sn": ("last_name", ),
}
SAML_DJANGO_USER_MAIN_ATTRIBUTE = 'username'
SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP = '__iexact'
SAML_DJANGO_USER_MAIN_ATTRIBUTE = "username"
SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP = "__iexact"
saml_idp = {
# Find the entity ID of your IdP and make this the key here:
"urn:mace:incommon:uiuc.edu": {
"single_sign_on_service": {
# Add the POST and REDIRECT bindings for the sign on service here:
saml2.BINDING_HTTP_POST:
"https://shibboleth.illinois.edu/idp/profile/SAML2/POST/SSO",
saml2.BINDING_HTTP_REDIRECT:
"https://shibboleth.illinois.edu/idp/profile/SAML2/Redirect/SSO",
},
"single_logout_service": {
# And the REDIRECT binding for the logout service here:
saml2.BINDING_HTTP_REDIRECT:
"https://shibboleth.illinois.edu/idp/logout.jsp",
},
},
}
SAML_CONFIG = {
# full path to the xmlsec1 binary programm
'xmlsec_binary': '/usr/bin/xmlsec1',
# full path to the xmlsec1 binary program
"xmlsec_binary": "/usr/bin/xmlsec1",
# your entity id, usually your subdomain plus the url to the metadata view
# (usually no need to change)
'entityid': _BASE_URL + '/saml2/metadata/',
"entityid": _BASE_URL + "/saml2/metadata/",
# directory with attribute mapping
# (already populated with samples from djangosaml2, usually no need to
# change)
'attribute_map_dir': path.join(_BASEDIR, 'saml-config', 'attribute-maps'),
"attribute_map_dir": path.join(_BASEDIR, "saml-config", "attribute-maps"),
'allow_unknown_attributes': True,
"allow_unknown_attributes": True,
# this block states what services we provide
'service': {
'sp': {
'name': 'RELATE SAML2 SP',
'name_id_format': saml2.saml.NAMEID_FORMAT_TRANSIENT,
'endpoints': {
"service": {
"sp": {
"name": "RELATE SAML2 SP",
# Django sets SameSite attribute on session cookies,
# which causes problems. Work around that, for now.
# https://github.com/peppelinux/djangosaml2/issues/143#issuecomment-633694504
"allow_unsolicited": True,
"name_id_format": saml2.saml.NAMEID_FORMAT_TRANSIENT,
"endpoints": {
# url and binding to the assertion consumer service view
# do not change the binding or service name
'assertion_consumer_service': [
(_BASE_URL + '/saml2/acs/',
"assertion_consumer_service": [
(_BASE_URL + "/saml2/acs/",
saml2.BINDING_HTTP_POST),
],
# url and binding to the single logout service view
# do not change the binding or service name
'single_logout_service': [
(_BASE_URL + '/saml2/ls/',
"single_logout_service": [
(_BASE_URL + "/saml2/ls/",
saml2.BINDING_HTTP_REDIRECT),
(_BASE_URL + '/saml2/ls/post',
(_BASE_URL + "/saml2/ls/post",
saml2.BINDING_HTTP_POST),
],
},
# attributes that this project needs to identify a user
'required_attributes': ['uid'],
"required_attributes": ["uid"],
# attributes that may be useful to have but not required
'optional_attributes': ['eduPersonAffiliation'],
# in this section the list of IdPs we talk to are defined
'idp': {
# Find the entity ID of your IdP and make this the key here:
'urn:mace:incommon:uiuc.edu': {
'single_sign_on_service': {
# Add the POST and REDIRECT bindings for the sign on service here:
saml2.BINDING_HTTP_POST:
'https://shibboleth.illinois.edu/idp/profile/SAML2/POST/SSO',
saml2.BINDING_HTTP_REDIRECT:
'https://shibboleth.illinois.edu/idp/profile/SAML2/Redirect/SSO',
},
'single_logout_service': {
# And the REDIRECT binding for the logout service here:
saml2.BINDING_HTTP_REDIRECT:
'https://shibboleth.illinois.edu/idp/logout.jsp', # noqa
},
},
},
"optional_attributes": ["eduPersonAffiliation"],
"idp": saml_idp,
},
},
......@@ -410,44 +599,44 @@ if RELATE_SIGN_IN_BY_SAML2_ENABLED:
# This particular file is public and lives at
# https://discovery.itrust.illinois.edu/itrust-metadata/itrust-metadata.xml
'metadata': {
'local': [path.join(_BASEDIR, 'saml-config', 'itrust-metadata.xml')],
"metadata": {
"local": [path.join(_BASEDIR, "saml-config", "itrust-metadata.xml")],
},
# set to 1 to output debugging information
'debug': 1,
"debug": 1,
# certificate and key
'key_file': _SAML_KEY_FILE,
'cert_file': _SAML_CERT_FILE,
"key_file": _SAML_KEY_FILE,
"cert_file": _SAML_CERT_FILE,
'encryption_keypairs': [
"encryption_keypairs": [
{
'key_file': _SAML_KEY_FILE,
'cert_file': _SAML_CERT_FILE,
"key_file": _SAML_KEY_FILE,
"cert_file": _SAML_CERT_FILE,
}
],
# own metadata settings
'contact_person': [
{'given_name': 'Andreas',
'sur_name': 'Kloeckner',
'company': 'CS - University of Illinois',
'email_address': 'andreask@illinois.edu',
'contact_type': 'technical'},
{'given_name': 'Andreas',
'sur_name': 'Kloeckner',
'company': 'CS - University of Illinois',
'email_address': 'andreask@illinois.edu',
'contact_type': 'administrative'},
"contact_person": [
{"given_name": "Andreas",
"sur_name": "Kloeckner",
"company": "CS - University of Illinois",
"email_address": "andreask@illinois.edu",
"contact_type": "technical"},
{"given_name": "Andreas",
"sur_name": "Kloeckner",
"company": "CS - University of Illinois",
"email_address": "andreask@illinois.edu",
"contact_type": "administrative"},
],
# you can set multilanguage information here
'organization': {
'name': [('RELATE', 'en')],
'display_name': [('RELATE', 'en')],
'url': [(_BASE_URL, 'en')],
"organization": {
"name": [("RELATE", "en")],
"display_name": [("RELATE", "en")],
"url": [(_BASE_URL, "en")],
},
'valid_for': 24, # how long is our metadata valid
"valid_for": 24, # how long is our metadata valid
}
# }}}
......
source diff could not be displayed: it is too large. Options to address this: view the blob.
source diff could not be displayed: it is too large. Options to address this: view the blob.
#!/usr/bin/env python
from __future__ import annotations
import os
import sys
def get_local_test_settings_file(argv):
assert argv[1] == "test"
assert "manage.py" in argv[0]
local_settings_dir = os.path.split(argv[0])[0]
assert os.path.isfile(os.path.join(local_settings_dir, "manage.py"))
from django.core.management import CommandError, CommandParser
parser = CommandParser(
usage="%(prog)s subcommand [options] [args]",
add_help=False)
parser.add_argument("--local_test_settings",
dest="local_test_settings")
options, _args = parser.parse_known_args(argv)
if options.local_test_settings is None:
local_settings_file = "local_settings_example.py"
else:
local_settings_file = options.local_test_settings
if os.path.split(local_settings_file)[0] == "":
local_settings_file = os.path.join(
local_settings_dir, local_settings_file)
if os.path.abspath(local_settings_file) == os.path.abspath(
os.path.join(local_settings_dir, "local_settings.py")):
raise CommandError(
"Using production local_settings for tests is not "
"allowed due to security reason."
)
if not os.path.isfile(local_settings_file):
raise CommandError(
f"file '{local_settings_file}' does not exist"
)
return local_settings_file
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "relate.settings")
from django.core.management import execute_from_command_line
if sys.argv[1] == "test":
local_settings_file = get_local_test_settings_file(sys.argv)
os.environ["RELATE_LOCAL_TEST_SETTINGS"] = local_settings_file
execute_from_command_line(sys.argv)
source diff could not be displayed: it is too large. Options to address this: view the blob.
{
"dependencies": {
"@benrbray/prosemirror-math": "^1.0.0",
"@codemirror/autocomplete": "^6.18.3",
"@codemirror/commands": "^6.7.1",
"@codemirror/lang-markdown": "^6.2.0",
"@codemirror/lang-python": "^6.1.6",
"@codemirror/language": "^6.10.8",
"@codemirror/legacy-modes": "^6.4.2",
"@codemirror/search": "^6.5.8",
"@codemirror/state": "^6.4.1",
"@codemirror/view": "^6.34",
"@fullcalendar/core": "^6.1.15",
"@fullcalendar/daygrid": "^6.1.15",
"@fullcalendar/list": "^6.1.15",
"@fullcalendar/timegrid": "^6.1.15",
"@lezer/highlight": "^1.2.1",
"@popperjs/core": "^2.11.2",
"@replit/codemirror-emacs": "^6.1.0",
"@replit/codemirror-vim": "^6.2.1",
"@vscode/markdown-it-katex": "^1.1.0",
"bootstrap": "^5.3.3",
"bootstrap-icons": "^1.8.1",
"datatables.net": "^2.1.8",
"datatables.net-bs5": "^2.1.8",
"datatables.net-fixedcolumns": "^5.0.4",
"datatables.net-fixedcolumns-bs5": "^5.0.4",
"datatables.net-plugins": "^2.1.7",
"htmx.org": "^2",
"jquery": "^3.7.1",
"jstree": "^3.3.17",
"katex": "^0.16.11",
"markdown-it": "^14.1.0",
"mathjax": "^3.2.2",
"prosemirror-commands": "^1.6.2",
"prosemirror-example-setup": "^1.2.3",
"prosemirror-inputrules": "^1.4.0",
"prosemirror-keymap": "^1.2.2",
"prosemirror-markdown": "^1.13.1",
"prosemirror-menu": "^1.2.4",
"prosemirror-model": "^1.23.0",
"prosemirror-schema-basic": "^1.2.3",
"prosemirror-schema-list": "^1.4.1",
"prosemirror-state": "^1.4.3",
"prosemirror-view": "^1.37.0",
"select2": "^4.0.5",
"select2-bootstrap-theme": "^0.1.0-beta.10",
"video.js": "^8.21.0"
},
"//devDependencies": {
"sass": "https://github.com/twbs/bootstrap/issues/40849"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-node-resolve": "^16.0.0",
"@rollup/plugin-replace": "^6.0.2",
"@rollup/plugin-terser": "^0.4.4",
"autoprefixer": "^10.4.0",
"eslint": "^8.3.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.31.0",
"less": "^4.1.2",
"npm-run-all": "^4.1.5",
"rollup": "^4.24.0",
"rollup-plugin-gzip": "^4.0.1",
"rollup-plugin-styler": "^1.8.0",
"sass": "<1.77",
"serve": "^14.2.4"
},
"scripts": {
"build": "rollup --config",
"dev": "rollup --config --watch"
}
}
\ No newline at end of file
source diff could not be displayed: it is too large. Options to address this: view the blob.
from __future__ import annotations
__copyright__ = "Copyright (C) 2024 University of Illinois Board of Trustees"
__license__ = """
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
from typing import TYPE_CHECKING
from django import forms, http
from django.contrib import admin
from django.db.models import QuerySet
from django.urls import reverse
from django.utils.safestring import mark_safe
from accounts.models import User
from course.constants import participation_permission as pperm
from prairietest.models import AllowEvent, DenyEvent, Facility, MostRecentDenyEvent
if TYPE_CHECKING:
from accounts.models import User
class FacilityAdminForm(forms.ModelForm):
class Meta:
model = Facility
fields = "__all__"
widgets = {
"secret": forms.PasswordInput(render_value=True),
}
@admin.register(Facility)
class FacilityAdmin(admin.ModelAdmin):
def get_queryset(self, request: http.HttpRequest) -> QuerySet:
assert request.user.is_authenticated
qs = super().get_queryset(request)
if request.user.is_superuser:
return qs
from course.admin import _filter_course_linked_obj_for_user
return _filter_course_linked_obj_for_user(qs, request.user)
def webhook_url(self, obj: Facility) -> str:
url = reverse(
"prairietest:webhook",
args=(obj.course.identifier, obj.identifier),
)
return mark_safe(
f"<tt>https://YOUR-HOST{url}</tt> Make sure to include the trailing slash!")
list_display = ["course", "identifier", "webhook_url"]
list_display_links = ["identifier"]
list_filter = ["course", "identifier"]
form = FacilityAdminForm
def _filter_events_for_user(queryset: QuerySet, user: User) -> QuerySet:
if user.is_superuser:
return queryset
return queryset.filter(
facility__course__participations__user=user,
facility__course__participations__roles__permissions__permission=pperm.use_admin_interface)
@admin.register(AllowEvent)
class AllowEventAdmin(admin.ModelAdmin):
def get_queryset(self, request: http.HttpRequest) -> QuerySet:
assert request.user.is_authenticated
qs = super().get_queryset(request)
return _filter_events_for_user(qs, request.user)
list_display = [
"event_id", "facility", "user_uid", "start", "end", "exam_uuid"]
list_filter = ["facility", "user_uid", "exam_uuid"]
@admin.register(DenyEvent)
class DenyEventAdmin(admin.ModelAdmin):
def get_queryset(self, request: http.HttpRequest) -> QuerySet:
assert request.user.is_authenticated
qs = super().get_queryset(request)
return _filter_events_for_user(qs, request.user)
list_display = [
"event_id", "facility", "start", "end", "deny_uuid"]
list_filter = ["facility"]
@admin.register(MostRecentDenyEvent)
class MostRecentDenyEventAdmin(admin.ModelAdmin):
def get_queryset(self, request: http.HttpRequest) -> QuerySet:
assert request.user.is_authenticated
qs = super().get_queryset(request)
if request.user.is_superuser:
return qs
return qs.filter(
event__facility__course__participations__user=request.user,
event__facility__course__participations__roles__permissions__permission=pperm.use_admin_interface)
list_display = ["deny_uuid", "end"]
# Generated by Django 5.1 on 2024-09-13 04:22
import django.core.validators
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('course', '0121_alter_flowaccessexceptionentry_permission_and_more'),
]
operations = [
migrations.CreateModel(
name='Facility',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('identifier', models.CharField(db_index=True, max_length=200, unique=True, validators=[django.core.validators.RegexValidator('^(?P<test_facility_id>[a-zA-Z][a-zA-Z0-9_]*)$')])),
('description', models.TextField(blank=True, null=True)),
('secret', models.CharField(max_length=220)),
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course.course')),
],
options={
'verbose_name_plural': 'Facilities',
},
),
migrations.CreateModel(
name='DenyEvent',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('event_id', models.UUIDField()),
('created', models.DateTimeField(verbose_name='Created time')),
('received_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Received time')),
('deny_uuid', models.UUIDField()),
('start', models.DateTimeField(db_index=True, verbose_name='Start time')),
('end', models.DateTimeField(db_index=True, verbose_name='End time')),
('cidr_blocks', models.JSONField()),
('test_facility', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='prairietest.facility')),
],
),
migrations.CreateModel(
name='AllowEvent',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('event_id', models.UUIDField()),
('created', models.DateTimeField(verbose_name='Created time')),
('received_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Received time')),
('user_uid', models.CharField(max_length=200)),
('user_uin', models.CharField(max_length=200)),
('exam_uuid', models.UUIDField()),
('start', models.DateTimeField(verbose_name='Start time')),
('end', models.DateTimeField(verbose_name='End time')),
('cidr_blocks', models.JSONField()),
('test_facility', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='prairietest.facility')),
],
),
migrations.CreateModel(
name='MostRecentDenyEvent',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('deny_uuid', models.UUIDField(unique=True)),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='prairietest.denyevent')),
],
),
migrations.AddIndex(
model_name='facility',
index=models.Index(fields=['course', 'identifier'], name='prairietest_course__2525b9_idx'),
),
migrations.AddIndex(
model_name='denyevent',
index=models.Index(fields=['deny_uuid', 'created'], name='prairietest_deny_uu_bbbbf1_idx'),
),
migrations.AddIndex(
model_name='denyevent',
index=models.Index(fields=['deny_uuid', 'start'], name='prairietest_deny_uu_b13822_idx'),
),
migrations.AddIndex(
model_name='denyevent',
index=models.Index(fields=['deny_uuid', 'end'], name='prairietest_deny_uu_3c9537_idx'),
),
migrations.AddIndex(
model_name='allowevent',
index=models.Index(fields=['user_uid', 'exam_uuid', 'start'], name='prairietest_user_ui_e93827_idx'),
),
migrations.AddIndex(
model_name='allowevent',
index=models.Index(fields=['user_uid', 'exam_uuid', 'end'], name='prairietest_user_ui_e11aa4_idx'),
),
]
# Generated by Django 5.1 on 2024-09-13 15:39
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('prairietest', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='mostrecentdenyevent',
name='end',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='End time'),
preserve_default=False,
),
migrations.AlterField(
model_name='denyevent',
name='end',
field=models.DateTimeField(verbose_name='End time'),
),
migrations.AlterField(
model_name='denyevent',
name='start',
field=models.DateTimeField(verbose_name='Start time'),
),
migrations.AddIndex(
model_name='mostrecentdenyevent',
index=models.Index(fields=['end'], name='prairietest_end_31b76d_idx'),
),
]
# Generated by Django 5.1 on 2024-10-09 19:43
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('prairietest', '0002_mostrecentdenyevent_end_alter_denyevent_end_and_more'),
]
operations = [
migrations.RenameField(
model_name='allowevent',
old_name='test_facility',
new_name='facility',
),
migrations.RenameField(
model_name='denyevent',
old_name='test_facility',
new_name='facility',
),
migrations.AlterField(
model_name='facility',
name='identifier',
field=models.CharField(db_index=True, max_length=200, unique=True, validators=[django.core.validators.RegexValidator('^(?P<facility_id>[a-zA-Z][a-zA-Z0-9_]*)$')]),
),
]