diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6f3f6abb19c7045df41cb9aa0833a9fd3a517aa0..544304bc019e2ff523ea14d2d02db82897f3ff30 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -69,22 +69,29 @@ jobs: formatter: runs-on: ubuntu-18.04 - container: - image: python:3.7 - name: Formatting steps: - name: Check out repository code uses: actions/checkout@v2 - - name: Install dependencies + - uses: actions/setup-python@v2 + with: + python-version: 3.7 + - name: Install Python dependencies run: pip install -r requirements-dev.txt + - name: Setup Node + uses: actions/setup-node@v2 + - name: Install Node dependencies + run: npm ci - name: Add localsettings run: cp evap/settings_test.py evap/localsettings.py - name: Check code formatting run: black --check evap - name: Check imports formatting run: isort . --check --diff + - run: ls -laR evap/static/ts + - name: Check TypeScript formatting + run: npx prettier --list-different --loglevel debug --config evap/static/ts/.prettierrc.json evap/static/ts/src backup-process: diff --git a/.gitignore b/.gitignore index d4ac1d1cbc3e63711edbbfc13738ff11cd736bed..ddeed6d5c38d321cb3e67f6436b3f417d206a2d3 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ htmlcov # pip puts editable packages here src +!evap/static/ts/.prettierrc.json !evap/static/ts/src node_modules diff --git a/evap/evaluation/management/commands/format.py b/evap/evaluation/management/commands/format.py index a1994d7d51016ce243fa2ed06e28bf1e50deed4e..f513276cf2f95d061b28617e5c8cb9169f9d591d 100644 --- a/evap/evaluation/management/commands/format.py +++ b/evap/evaluation/management/commands/format.py @@ -11,3 +11,4 @@ class Command(BaseCommand): def handle(self, *args, **options): subprocess.run(["black", "evap"], check=False) # nosec subprocess.run(["isort", "."], check=False) # nosec + subprocess.run(["npx", "prettier", "--write", "evap/static/ts/src"], check=False) # nosec diff --git a/evap/evaluation/tests/test_commands.py b/evap/evaluation/tests/test_commands.py index 4824fcd16ec9fe358b7fafba1c8bba7fa7fdb9e9..f0f23478782232cc07f16e562c5264569da658da 100644 --- a/evap/evaluation/tests/test_commands.py +++ b/evap/evaluation/tests/test_commands.py @@ -352,11 +352,12 @@ class TestFormatCommand(TestCase): @patch("subprocess.run") def test_formatters_called(self, mock_subprocess_run): management.call_command("format") - self.assertEqual(len(mock_subprocess_run.mock_calls), 2) + self.assertEqual(len(mock_subprocess_run.mock_calls), 3) mock_subprocess_run.assert_has_calls( [ call(["black", "evap"], check=False), call(["isort", "."], check=False), + call(["npx", "prettier", "--write", "evap/static/ts/src"], check=False), ] ) diff --git a/evap/static/ts/.prettierrc.json b/evap/static/ts/.prettierrc.json new file mode 100644 index 0000000000000000000000000000000000000000..d59f9815241ab1c9586226f793ead6443596cf06 --- /dev/null +++ b/evap/static/ts/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "tabWidth": 4, + "arrowParens": "avoid", + "trailingComma": "all", + "printWidth": 120 +} diff --git a/evap/static/ts/src/csrf-utils.ts b/evap/static/ts/src/csrf-utils.ts index b221865a44d58fa1a8eab91df6f68e258c8e754d..5300b1b03d59d37fcee61aa446acf6224f46db4a 100644 --- a/evap/static/ts/src/csrf-utils.ts +++ b/evap/static/ts/src/csrf-utils.ts @@ -1,7 +1,8 @@ // based on: https://docs.djangoproject.com/en/3.1/ref/csrf/#ajax function getCookie(name: string): string | null { if (document.cookie !== "") { - const cookie = document.cookie.split(";") + const cookie = document.cookie + .split(";") .map(cookie => cookie.trim()) .find(cookie => cookie.substring(0, name.length + 1) === `${name}=`); if (cookie) { @@ -19,7 +20,7 @@ function isMethodCsrfSafe(method: string): boolean { // setup ajax sending csrf token $.ajaxSetup({ - beforeSend: function(xhr: JQuery.jqXHR, settings: JQuery.AjaxSettings) { + beforeSend: function (xhr: JQuery.jqXHR, settings: JQuery.AjaxSettings) { const isMethodSafe = settings.method && isMethodCsrfSafe(settings.method); if (!isMethodSafe && !this.crossDomain) { xhr.setRequestHeader("X-CSRFToken", csrftoken); diff --git a/evap/static/ts/src/datagrid.ts b/evap/static/ts/src/datagrid.ts index 5262d356c5eed251d402e084d459d2cb8cb88d6d..8f0584c68d9c5ede9a89df6cf6539c3a239facf3 100644 --- a/evap/static/ts/src/datagrid.ts +++ b/evap/static/ts/src/datagrid.ts @@ -1,27 +1,27 @@ declare const Sortable: typeof import("sortablejs"); interface Row { - element: HTMLElement, - searchWords: string[], - filterValues: Map<string, string[]>, - orderValues: Map<string, string | number>, - isDisplayed: boolean, + element: HTMLElement; + searchWords: string[]; + filterValues: Map<string, string[]>; + orderValues: Map<string, string | number>; + isDisplayed: boolean; } interface State { - search: string, - filter: Map<string, string[]>, - order: [string, "asc" | "desc"][], + search: string; + filter: Map<string, string[]>; + order: [string, "asc" | "desc"][]; } interface BaseParameters { - storageKey: string, - searchInput: HTMLInputElement, + storageKey: string; + searchInput: HTMLInputElement; } interface DataGridParameters extends BaseParameters { - head: HTMLElement, - container: HTMLElement + head: HTMLElement; + container: HTMLElement; } abstract class DataGrid { @@ -33,7 +33,7 @@ abstract class DataGrid { private delayTimer: any | null; protected state: State; - protected constructor({storageKey, head, container, searchInput}: DataGridParameters) { + protected constructor({ storageKey, head, container, searchInput }: DataGridParameters) { this.storageKey = storageKey; this.sortableHeaders = new Map(); head.querySelectorAll<HTMLElement>(".col-order").forEach(header => { @@ -83,16 +83,19 @@ abstract class DataGrid { private static NUMBER_REGEX = /^[+-]?\d+(?:[.,]\d*)?$/; private fetchRows(): Row[] { - let rows = [...this.container.children].map(row => row as HTMLElement).map(row => { - const searchWords = this.findSearchableCells(row) - .flatMap(element => DataGrid.searchWordsOf(element.textContent!)); - return { - element: row, - searchWords, - filterValues: this.fetchRowFilterValues(row), - orderValues: this.fetchRowOrderValues(row), - } as Row; - }); + let rows = [...this.container.children] + .map(row => row as HTMLElement) + .map(row => { + const searchWords = this.findSearchableCells(row).flatMap(element => + DataGrid.searchWordsOf(element.textContent!), + ); + return { + element: row, + searchWords, + filterValues: this.fetchRowFilterValues(row), + orderValues: this.fetchRowOrderValues(row), + } as Row; + }); for (const column of this.sortableHeaders.keys()) { const orderValues = rows.map(row => row.orderValues.get(column) as string); const isNumericalColumn = orderValues.every(orderValue => DataGrid.NUMBER_REGEX.test(orderValue)); @@ -100,7 +103,7 @@ abstract class DataGrid { rows.forEach(row => { const numberString = (row.orderValues.get(column) as string).replace(",", "."); row.orderValues.set(column, parseFloat(numberString)); - }) + }); } } return rows; @@ -173,9 +176,7 @@ abstract class DataGrid { // Reflects changes to the rows to the DOM protected renderToDOM() { [...this.container.children].map(element => element as HTMLElement).forEach(element => element.remove()); - const elements = this.rows - .filter(row => row.isDisplayed) - .map(row => row.element); + const elements = this.rows.filter(row => row.isDisplayed).map(row => row.element); this.container.append(...elements); this.saveStateToStorage(); } @@ -206,15 +207,15 @@ abstract class DataGrid { } interface TableGridParameters extends BaseParameters { - table: HTMLTableElement, - resetSearch: HTMLButtonElement, + table: HTMLTableElement; + resetSearch: HTMLButtonElement; } // Table based data grid which uses its head and body export class TableGrid extends DataGrid { private resetSearch: HTMLButtonElement; - constructor({table, resetSearch, ...options}: TableGridParameters) { + constructor({ table, resetSearch, ...options }: TableGridParameters) { super({ head: table.querySelector("thead")!, container: table.querySelector("tbody")!, @@ -252,13 +253,13 @@ export class TableGrid extends DataGrid { } interface EvaluationGridParameters extends TableGridParameters { - filterButtons: HTMLButtonElement[], + filterButtons: HTMLButtonElement[]; } export class EvaluationGrid extends TableGrid { private filterButtons: HTMLButtonElement[]; - constructor({filterButtons, ...options}: EvaluationGridParameters) { + constructor({ filterButtons, ...options }: EvaluationGridParameters) { super(options); this.filterButtons = filterButtons; } @@ -295,8 +296,9 @@ export class EvaluationGrid extends TableGrid { } protected fetchRowFilterValues(row: HTMLElement): Map<string, string[]> { - const evaluationState = [...row.querySelectorAll<HTMLElement>("[data-filter]")] - .map(element => element.dataset.filter!); + const evaluationState = [...row.querySelectorAll<HTMLElement>("[data-filter]")].map( + element => element.dataset.filter!, + ); return new Map([["evaluationState", evaluationState]]); } @@ -315,13 +317,13 @@ export class EvaluationGrid extends TableGrid { } interface QuestionnaireParameters extends TableGridParameters { - updateUrl: string, + updateUrl: string; } export class QuestionnaireGrid extends TableGrid { private readonly updateUrl: string; - constructor({updateUrl, ...options}: QuestionnaireParameters) { + constructor({ updateUrl, ...options }: QuestionnaireParameters) { super(options); this.updateUrl = updateUrl; } @@ -338,35 +340,41 @@ export class QuestionnaireGrid extends TableGrid { } const questionnaireIndices = this.rows.map((row, index) => [$(row.element).data("id"), index]); $.post(this.updateUrl, Object.fromEntries(questionnaireIndices)); - } + }, }); } private reorderRow(oldPosition: number, newPosition: number) { - const displayedRows = this.rows.map((row, index) => ({row, index})) - .filter(({row}) => row.isDisplayed); + const displayedRows = this.rows.map((row, index) => ({ row, index })).filter(({ row }) => row.isDisplayed); this.rows.splice(displayedRows[oldPosition].index, 1); this.rows.splice(displayedRows[newPosition].index, 0, displayedRows[oldPosition].row); } } interface ResultGridParameters extends DataGridParameters { - filterCheckboxes: Map<string, {selector: string, checkboxes: HTMLInputElement[]}>, - sortColumnSelect: HTMLSelectElement, - sortOrderCheckboxes: HTMLInputElement[], - resetFilter: HTMLButtonElement, - resetOrder: HTMLButtonElement, + filterCheckboxes: Map<string, { selector: string; checkboxes: HTMLInputElement[] }>; + sortColumnSelect: HTMLSelectElement; + sortOrderCheckboxes: HTMLInputElement[]; + resetFilter: HTMLButtonElement; + resetOrder: HTMLButtonElement; } // Grid based data grid which has its container separated from its header export class ResultGrid extends DataGrid { - private readonly filterCheckboxes: Map<string, {selector: string, checkboxes: HTMLInputElement[]}>; + private readonly filterCheckboxes: Map<string, { selector: string; checkboxes: HTMLInputElement[] }>; private sortColumnSelect: HTMLSelectElement; private sortOrderCheckboxes: HTMLInputElement[]; private resetFilter: HTMLButtonElement; private resetOrder: HTMLButtonElement; - constructor({filterCheckboxes, sortColumnSelect, sortOrderCheckboxes, resetFilter, resetOrder, ...options}: ResultGridParameters) { + constructor({ + filterCheckboxes, + sortColumnSelect, + sortOrderCheckboxes, + resetFilter, + resetOrder, + ...options + }: ResultGridParameters) { super(options); this.filterCheckboxes = filterCheckboxes; this.sortColumnSelect = sortColumnSelect; @@ -377,7 +385,7 @@ export class ResultGrid extends DataGrid { public bindEvents() { super.bindEvents(); - for (const [name, {checkboxes}] of this.filterCheckboxes.entries()) { + for (const [name, { checkboxes }] of this.filterCheckboxes.entries()) { checkboxes.forEach(checkbox => { checkbox.addEventListener("change", () => { const values = checkboxes.filter(checkbox => checkbox.checked).map(elem => elem.value); @@ -413,21 +421,23 @@ export class ResultGrid extends DataGrid { const order = this.sortOrderCheckboxes.find(checkbox => checkbox.checked)!.value; if (order === "asc" || order === "desc") { if (column === "name-semester") { - this.sort([["name", order], ["semester", order]]); + this.sort([ + ["name", order], + ["semester", order], + ]); } else { this.sort([[column, order]]); } } } - protected findSearchableCells(row: HTMLElement): HTMLElement[] { return [...row.querySelectorAll<HTMLElement>(".evaluation-name, [data-col=responsible]")]; } protected fetchRowFilterValues(row: HTMLElement): Map<string, string[]> { let filterValues = new Map(); - for (const [name, {selector, checkboxes}] of this.filterCheckboxes.entries()) { + for (const [name, { selector, checkboxes }] of this.filterCheckboxes.entries()) { // To store filter values independent of the language, use the corresponding id from the checkbox const values = [...row.querySelectorAll(selector)] .map(element => element.textContent!.trim()) @@ -438,12 +448,15 @@ export class ResultGrid extends DataGrid { } protected get defaultOrder(): [string, "asc" | "desc"][] { - return [["name", "asc"], ["semester", "asc"]]; + return [ + ["name", "asc"], + ["semester", "asc"], + ]; } protected reflectFilterStateOnInputs() { super.reflectFilterStateOnInputs(); - for (const [name, {checkboxes}] of this.filterCheckboxes.entries()) { + for (const [name, { checkboxes }] of this.filterCheckboxes.entries()) { checkboxes.forEach(checkbox => { let isActive; if (this.state.filter.has(name)) { diff --git a/package-lock.json b/package-lock.json index 206271ac7bef72b53ac568409dee0f35e3f464e4..4b83b774ba13360f69e3b42181accff0056f72fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@types/sortablejs": "^1.3.0", "jest": "^27.3.1", "jest-environment-puppeteer": "^6.0.0", + "prettier": "^2.4.1", "puppeteer": "^10.4.0", "sass": "1.32.13", "ts-jest": "^27.0.7", @@ -6426,6 +6427,18 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz", + "integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/pretty-format": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", @@ -12653,6 +12666,12 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, + "prettier": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz", + "integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==", + "dev": true + }, "pretty-format": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", diff --git a/package.json b/package.json index ae7bee343a3852b5b0f4736ccf970a0f96f49811..ed919724738ba8bb70d51e1499cffc65fa56b574 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,11 @@ "@types/sortablejs": "^1.3.0", "jest": "^27.3.1", "jest-environment-puppeteer": "^6.0.0", + "prettier": "^2.4.1", "puppeteer": "^10.4.0", + "sass": "1.32.13", "ts-jest": "^27.0.7", - "typescript": "^4.4.4", - "sass": "1.32.13" + "typescript": "^4.4.4" }, "jest": { "testRunner": "jest-jasmine2",