From 432efc3e8994de2f6c8b8f189dc43e8b6bf86d79 Mon Sep 17 00:00:00 2001 From: Lucas Welscher Date: Sun, 27 Oct 2024 20:27:32 +0100 Subject: [PATCH 1/5] Development: Upgrade markdown library to markdown-it (#9354) --- angular.json | 4 +- docs/user/markdown-support.rst | 10 +- package-lock.json | 221 ++++++++++-------- package.json | 15 +- ...rogramming-exercise-plant-uml.extension.ts | 106 ++++----- .../programming-exercise-task.extension.ts | 27 +-- ...gramming-exercise-instruction.component.ts | 4 +- .../short-answer-question-util.service.ts | 1 + src/main/webapp/app/index.d.ts | 9 +- .../ArtemisTextReplacementPlugin.ts | 24 ++ .../artemis-showdown-extension-wrapper.ts | 15 -- .../markdown-editor-monaco.component.html | 2 +- .../webapp/app/shared/markdown.service.ts | 10 +- .../shared/pipes/html-for-markdown.pipe.ts | 6 +- .../shared/util/markdown.conversion.util.ts | 93 +++++--- ...short-answer-question-util.service.spec.ts | 2 +- .../text-unit/text-unit.component.spec.ts | 2 +- .../posting-content-part.component.spec.ts | 2 +- .../spec/helpers/sample/problemStatement.json | 4 +- .../spec/service/markdown.service.spec.ts | 28 +++ 20 files changed, 322 insertions(+), 263 deletions(-) create mode 100644 src/main/webapp/app/shared/markdown-editor/extensions/ArtemisTextReplacementPlugin.ts delete mode 100644 src/main/webapp/app/shared/markdown-editor/extensions/artemis-showdown-extension-wrapper.ts diff --git a/angular.json b/angular.json index 008ac75d13bf..d9535f656cd2 100644 --- a/angular.json +++ b/angular.json @@ -44,9 +44,7 @@ "react-is", "rfdc", "shallowequal", - "showdown-highlight", - "showdown-katex", - "showdown", + "markdown-it-class", "smoothscroll-polyfill", "sockjs-client", "use-sync-external-store/shim", diff --git a/docs/user/markdown-support.rst b/docs/user/markdown-support.rst index c28d50fa2786..9ac5b6f5a56f 100644 --- a/docs/user/markdown-support.rst +++ b/docs/user/markdown-support.rst @@ -9,7 +9,7 @@ Markdown Support `Markdown `__ is an easy-to-read, easy-to-write syntax for formatting plain text. -A markdown playground can be found `here `__. +A markdown playground can be found `here `__. Artemis extends the basic `Markdown `__ syntax to support Artemis-specific features. This Artemis flavored Markdown is used to format text content across the platform using an integrated markdown editor. @@ -52,9 +52,9 @@ Markdown is also supported in the context of :ref:`communicating` Supported Syntax ^^^^^^^^^^^^^^^^ -The integrated markdown editor uses `Showdown `__. A quick description of the supported syntax can be found `here `__. +The integrated markdown editor uses `MarkdownIt `__. A quick description of the supported syntax can be found `here `__. -The following Showdown extensions are activated: +The following Plugins are activated: -- `Showdown Katex `__ to render LaTeX math and AsciiMath using KaTeX. -- `Showdown Highlight `__ for syntax highlighting in code blocks. +- `MarkdownIt Katex `__ to render LaTeX math and AsciiMath using KaTeX. +- `MarkdownIt HighlightJS `__ for syntax highlighting in code blocks. diff --git a/package-lock.json b/package-lock.json index 9ab71995dbff..74e3625253d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "@swimlane/ngx-charts": "20.5.0", "@swimlane/ngx-graph": "8.4.0", "@vscode/codicons": "0.0.36", + "@vscode/markdown-it-katex": "1.1.0", "bootstrap": "5.3.3", "compare-versions": "6.1.1", "core-js": "3.38.1", @@ -54,6 +55,9 @@ "js-video-url-parser": "0.5.1", "jszip": "3.10.1", "lodash-es": "4.17.21", + "markdown-it": "14.1.0", + "markdown-it-class": "1.0.0", + "markdown-it-highlightjs": "4.2.0", "mobile-drag-drop": "3.0.0-rc.0", "monaco-editor": "0.52.0", "ngx-infinite-scroll": "18.0.0", @@ -62,15 +66,13 @@ "pdfjs-dist": "4.7.76", "posthog-js": "1.176.0", "rxjs": "7.8.1", - "showdown": "2.1.0", - "showdown-highlight": "3.1.0", - "showdown-katex": "0.6.0", "simple-statistics": "7.8.7", "smoothscroll-polyfill": "0.4.4", "sockjs-client": "1.6.1", "split.js": "1.6.5", "ts-cacheable": "1.0.10", "tslib": "2.8.0", + "turndown": "7.2.0", "uuid": "10.0.0", "webstomp-client": "1.2.6", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", @@ -93,11 +95,12 @@ "@types/dompurify": "3.0.5", "@types/jest": "29.5.14", "@types/lodash-es": "4.17.12", + "@types/markdown-it": "14.1.2", "@types/node": "22.7.9", "@types/papaparse": "5.3.15", - "@types/showdown": "2.0.6", "@types/smoothscroll-polyfill": "0.3.4", "@types/sockjs-client": "1.5.4", + "@types/turndown": "5.0.5", "@types/uuid": "10.0.0", "@typescript-eslint/eslint-plugin": "8.11.0", "@typescript-eslint/parser": "8.11.0", @@ -5262,6 +5265,12 @@ "node": ">=6" } }, + "node_modules/@mixmark-io/domino": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz", + "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==", + "license": "BSD-2-Clause" + }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", @@ -7325,6 +7334,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/lodash": { "version": "4.17.12", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.12.tgz", @@ -7342,6 +7358,24 @@ "@types/lodash": "*" } }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -7466,13 +7500,6 @@ "@types/send": "*" } }, - "node_modules/@types/showdown": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/showdown/-/showdown-2.0.6.tgz", - "integrity": "sha512-pTvD/0CIeqe4x23+YJWlX2gArHa8G0J0Oh6GKaVXV7TAeickpkkZiNOgFcFcmLQ5lB/K0qBJL1FtRYltBfbGCQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/smoothscroll-polyfill": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/@types/smoothscroll-polyfill/-/smoothscroll-polyfill-0.3.4.tgz", @@ -7518,6 +7545,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/turndown": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.5.tgz", + "integrity": "sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", @@ -7774,6 +7808,15 @@ "integrity": "sha512-wsNOvNMMJ2BY8rC2N2MNBG7yOowV3ov8KlvUE/AiVUlHKTfWsw3OgAOQduX7h0Un6GssKD3aoTVH+TF3DSQwKQ==", "license": "CC-BY-4.0" }, + "node_modules/@vscode/markdown-it-katex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vscode/markdown-it-katex/-/markdown-it-katex-1.1.0.tgz", + "integrity": "sha512-9cF2eJpsJOEs2V1cCAoJW/boKz9GQQLvZhNvI030K90z6ZE9lRGc9hDVvKut8zdFO2ObjwylPXXXVYvTdP2O2Q==", + "license": "MIT", + "dependencies": { + "katex": "^0.16.4" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", @@ -8333,7 +8376,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/aria-query": { @@ -10986,7 +11028,6 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "devOptional": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -12966,15 +13007,6 @@ "node": ">= 0.4" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, "node_modules/highlight.js": { "version": "11.10.0", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.10.0.tgz", @@ -13038,17 +13070,6 @@ "integrity": "sha512-9SQg9oLQSAOZb8rO17mRNPkVB95QRh6iLY5J0Dbc/cgeoBT+XJBK/6XrQqfd+vxUVRjdctW+sfgYqgYzi0vg9g==", "license": "ISC" }, - "node_modules/html-encoder-decoder": { - "version": "1.3.10", - "resolved": "https://registry.npmjs.org/html-encoder-decoder/-/html-encoder-decoder-1.3.10.tgz", - "integrity": "sha512-18SjgzQZ9U1mxb96rjcWgWMnTlEzNj2lU2wAU7OeUobdIWXTS6lOGc6419eLhMlX24sNQYDyQfgkSXWjyq/Ilg==", - "license": "MIT", - "dependencies": { - "he": "^1.1.0", - "iterate-object": "^1.3.2", - "regex-escape": "^3.4.2" - } - }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", @@ -13912,12 +13933,6 @@ "node": ">=8" } }, - "node_modules/iterate-object": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/iterate-object/-/iterate-object-1.3.4.tgz", - "integrity": "sha512-4dG1D1x/7g8PwHS9aK6QV5V94+ZvyP4+d19qDv43EzImmrndysIl4prmJ1hWWIGCqrZHyaHBm6BSEWHOLnpoNw==", - "license": "MIT" - }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -16321,6 +16336,15 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/lint-staged": { "version": "15.2.10", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.10.tgz", @@ -16983,12 +17007,50 @@ "tmpl": "1.0.5" } }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-class": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-class/-/markdown-it-class-1.0.0.tgz", + "integrity": "sha512-CVDYqSgmErLAqInwWu8WmAR2nX6MMIBIt8LB6qg8DNldca9+aoC6ZyuY0lvBMsaTSHNFJRkcHVR1XjLw9nr9qQ==", + "license": "MIT" + }, + "node_modules/markdown-it-highlightjs": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/markdown-it-highlightjs/-/markdown-it-highlightjs-4.2.0.tgz", + "integrity": "sha512-NC7pXE8KkOl6xWJVRNt8p6wgJVznXKsE0HgYGdk6DD2tn1l4L9f0ALf3VIoGVkotNU1uGQatSxfBF1zZPUMmuQ==", + "license": "Unlicense", + "dependencies": { + "highlight.js": "^11.9.0" + } + }, "node_modules/material-colors": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==", "license": "ISC" }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, "node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -19547,6 +19609,15 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -19898,12 +19969,6 @@ "@babel/runtime": "^7.8.4" } }, - "node_modules/regex-escape": { - "version": "3.4.10", - "resolved": "https://registry.npmjs.org/regex-escape/-/regex-escape-3.4.10.tgz", - "integrity": "sha512-qEqf7uzW+iYcKNLMDFnMkghhQBnGdivT6KqVQyKsyjSWnoFyooXVnxrw9dtv3AFLnD6VBGXxtZGAQNFGFTnCqA==", - "license": "MIT" - }, "node_modules/regex-parser": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", @@ -20800,57 +20865,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/showdown": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/showdown/-/showdown-2.1.0.tgz", - "integrity": "sha512-/6NVYu4U819R2pUIk79n67SYgJHWCce0a5xTP979WbNp0FL9MN1I1QK662IDU1b6JzKTvmhgI7T7JYIxBi3kMQ==", - "license": "MIT", - "dependencies": { - "commander": "^9.0.0" - }, - "bin": { - "showdown": "bin/showdown.js" - }, - "funding": { - "type": "individual", - "url": "https://www.paypal.me/tiviesantos" - } - }, - "node_modules/showdown-highlight": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/showdown-highlight/-/showdown-highlight-3.1.0.tgz", - "integrity": "sha512-wrTxtE63L/bpW5A2Uy/AO1gblXnNHK/cDL6LszECOoCdMJKWTj0/4n4I/pmqub+3H3KCPVDDvtXpCArnT/heFA==", - "license": "MIT", - "dependencies": { - "highlight.js": "^11.5.0", - "html-encoder-decoder": "^1.3.9", - "showdown": "^2.0.3" - } - }, - "node_modules/showdown-katex": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/showdown-katex/-/showdown-katex-0.6.0.tgz", - "integrity": "sha512-eEOipJjqMxRJ+e69WlA7XENhFZzKhNl12csey0iLd4QbLzGF61+FBxNPhEZFz9wICYTJNfyqNgLSqmm8Uj0fGA==", - "license": "MIT", - "dependencies": { - "katex": "^0.10.0" - }, - "engines": { - "node": "*" - }, - "peerDependencies": { - "showdown": "^1.4.3" - } - }, - "node_modules/showdown/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || >=14" - } - }, "node_modules/side-channel": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", @@ -22244,6 +22258,15 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/turndown": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.0.tgz", + "integrity": "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A==", + "license": "MIT", + "dependencies": { + "@mixmark-io/domino": "^2.2.0" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -22362,6 +22385,12 @@ "typescript-compare": "^0.0.2" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", diff --git a/package.json b/package.json index c195b3503caa..04ad311c7f9a 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@swimlane/ngx-charts": "20.5.0", "@swimlane/ngx-graph": "8.4.0", "@vscode/codicons": "0.0.36", + "@vscode/markdown-it-katex": "1.1.0", "bootstrap": "5.3.3", "compare-versions": "6.1.1", "core-js": "3.38.1", @@ -57,6 +58,9 @@ "js-video-url-parser": "0.5.1", "jszip": "3.10.1", "lodash-es": "4.17.21", + "markdown-it": "14.1.0", + "markdown-it-class": "1.0.0", + "markdown-it-highlightjs": "4.2.0", "mobile-drag-drop": "3.0.0-rc.0", "monaco-editor": "0.52.0", "ngx-infinite-scroll": "18.0.0", @@ -65,15 +69,13 @@ "pdfjs-dist": "4.7.76", "posthog-js": "1.176.0", "rxjs": "7.8.1", - "showdown": "2.1.0", - "showdown-highlight": "3.1.0", - "showdown-katex": "0.6.0", "simple-statistics": "7.8.7", "smoothscroll-polyfill": "0.4.4", "sockjs-client": "1.6.1", "split.js": "1.6.5", "ts-cacheable": "1.0.10", "tslib": "2.8.0", + "turndown": "7.2.0", "uuid": "10.0.0", "webstomp-client": "1.2.6", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", @@ -102,13 +104,9 @@ }, "express": "5.0.1", "jsdom": "25.0.1", - "katex": "0.16.11", "postcss": "8.4.47", "rimraf": "6.0.1", "semver": "7.6.3", - "showdown-katex": { - "showdown": "2.1.0" - }, "tough-cookie": "5.0.0", "vite": "5.4.10", "webpack-dev-middleware": "7.4.2", @@ -134,11 +132,12 @@ "@types/dompurify": "3.0.5", "@types/jest": "29.5.14", "@types/lodash-es": "4.17.12", + "@types/markdown-it": "14.1.2", "@types/node": "22.7.9", "@types/papaparse": "5.3.15", - "@types/showdown": "2.0.6", "@types/smoothscroll-polyfill": "0.3.4", "@types/sockjs-client": "1.5.4", + "@types/turndown": "5.0.5", "@types/uuid": "10.0.0", "@typescript-eslint/eslint-plugin": "8.11.0", "@typescript-eslint/parser": "8.11.0", diff --git a/src/main/webapp/app/exercises/programming/shared/instructions-render/extensions/programming-exercise-plant-uml.extension.ts b/src/main/webapp/app/exercises/programming/shared/instructions-render/extensions/programming-exercise-plant-uml.extension.ts index 2c0d4036369b..886520d929f4 100644 --- a/src/main/webapp/app/exercises/programming/shared/instructions-render/extensions/programming-exercise-plant-uml.extension.ts +++ b/src/main/webapp/app/exercises/programming/shared/instructions-render/extensions/programming-exercise-plant-uml.extension.ts @@ -1,20 +1,19 @@ import { Injectable } from '@angular/core'; import { ProgrammingExerciseTestCase } from 'app/entities/programming/programming-exercise-test-case.model'; +import { ArtemisTextReplacementPlugin } from 'app/shared/markdown-editor/extensions/ArtemisTextReplacementPlugin'; +import { escapeStringForUseInRegex } from 'app/shared/util/global.utils'; import { Subject } from 'rxjs'; import { tap } from 'rxjs/operators'; -import { escapeStringForUseInRegex } from 'app/shared/util/global.utils'; import { ProgrammingExerciseInstructionService, TestCaseState } from 'app/exercises/programming/shared/instructions-render/service/programming-exercise-instruction.service'; import { ProgrammingExercisePlantUmlService } from 'app/exercises/programming/shared/instructions-render/service/programming-exercise-plant-uml.service'; -import { ArtemisShowdownExtensionWrapper } from 'app/shared/markdown-editor/extensions/artemis-showdown-extension-wrapper'; import { Result } from 'app/entities/result.model'; -import { ShowdownExtension } from 'showdown'; import DOMPurify from 'dompurify'; // This regex is the same as in the server: ProgrammingExerciseTaskService.java const testsColorRegex = /testsColor\((\s*[^()\s]+(\([^()]*\))?)\)/g; @Injectable({ providedIn: 'root' }) -export class ProgrammingExercisePlantUmlExtensionWrapper implements ArtemisShowdownExtensionWrapper { +export class ProgrammingExercisePlantUmlExtensionWrapper extends ArtemisTextReplacementPlugin { private latestResult?: Result; private testCases?: ProgrammingExerciseTestCase[]; private injectableElementsFoundSubject = new Subject<() => void>(); @@ -25,7 +24,9 @@ export class ProgrammingExercisePlantUmlExtensionWrapper implements ArtemisShowd constructor( private programmingExerciseInstructionService: ProgrammingExerciseInstructionService, private plantUmlService: ProgrammingExercisePlantUmlService, - ) {} + ) { + super(); + } /** * Sets latest result according to parameter. @@ -67,7 +68,6 @@ export class ProgrammingExercisePlantUmlExtensionWrapper implements ArtemisShowd } /** - * Creates and returns an extension to current exercise. * The extension provides a custom rendering mechanism for embedded plantUml diagrams. * The mechanism works as follows: * 1) Find (multiple) embedded plantUml diagrams based on a regex (startuml, enduml). @@ -76,55 +76,49 @@ export class ProgrammingExercisePlantUmlExtensionWrapper implements ArtemisShowd * 4) Send the plantUml content to the server for rendering a svg (the result will be cached for performance reasons) * 5) Inject the computed svg for the plantUml (from the server) into the plantUml div container based on the unique placeholder id (see step 2) */ - getExtension() { - const extension: ShowdownExtension = { - type: 'lang', - filter: (text: string) => { - const idPlaceholder = '%idPlaceholder%'; - // E.g. [task][Implement BubbleSort](testBubbleSort) - const plantUmlRegex = /@startuml([^@]*)@enduml/g; - // E.g. Implement BubbleSort, testBubbleSort - const plantUmlContainer = `
`; - // Replace test status markers. - const plantUmls = text.match(plantUmlRegex) ?? []; - // Assign unique ids to uml data structure at the beginning. - const plantUmlsIndexed = plantUmls.map((plantUml) => { - const nextIndex = this.plantUmlIndex; - // increase the global unique index so that the next plantUml gets a unique global id - this.plantUmlIndex++; - return { plantUmlId: nextIndex, plantUml }; - }); - // custom markdown to html rendering: replace the plantUml in the markdown with a simple
container with a unique id placeholder - // with the global unique id so that we can find the plantUml later on, when it was rendered, and then inject the 'actual' inner html (actually a svg image) - const replacedText = plantUmlsIndexed.reduce((acc: string, umlIndexed: { plantUmlId: number; plantUml: string }): string => { - return acc.replace(new RegExp(escapeStringForUseInRegex(umlIndexed.plantUml), 'g'), plantUmlContainer.replace(idPlaceholder, umlIndexed.plantUmlId.toString())); - }, text); - // before we send the plantUml to the server for rendering, we need to inject the current test status so that the colors can be adapted - // (green == implemented, red == not yet implemented, grey == unknown) - const plantUmlsValidated = plantUmlsIndexed.map((plantUmlIndexed: { plantUmlId: number; plantUml: string }) => { - plantUmlIndexed.plantUml = plantUmlIndexed.plantUml.replace(testsColorRegex, (match: any, capture: string) => { - const tests = this.programmingExerciseInstructionService.convertTestListToIds(capture, this.testCases); - const { testCaseState } = this.programmingExerciseInstructionService.testStatusForTask(tests, this.latestResult); - switch (testCaseState) { - case TestCaseState.SUCCESS: - return 'green'; - case TestCaseState.FAIL: - return 'red'; - default: - return 'grey'; - } - }); - return plantUmlIndexed; - }); - // send the adapted plantUml to the server for rendering and inject the result into the html DOM based on the unique plantUml id - this.injectableElementsFoundSubject.next(() => { - plantUmlsValidated.forEach((plantUmlIndexed: { plantUmlId: number; plantUml: string }) => { - this.loadAndInjectPlantUml(plantUmlIndexed.plantUml, plantUmlIndexed.plantUmlId); - }); - }); - return replacedText; - }, - }; - return extension; + replaceText(text: string): string { + const idPlaceholder = '%idPlaceholder%'; + // E.g. [task][Implement BubbleSort](testBubbleSort) + const plantUmlRegex = /@startuml([^@]*)@enduml/g; + // E.g. Implement BubbleSort, testBubbleSort + const plantUmlContainer = `
`; + // Replace test status markers. + const plantUmls = text.match(plantUmlRegex) ?? []; + // Assign unique ids to uml data structure at the beginning. + const plantUmlsIndexed = plantUmls.map((plantUml) => { + const nextIndex = this.plantUmlIndex; + // increase the global unique index so that the next plantUml gets a unique global id + this.plantUmlIndex++; + return { plantUmlId: nextIndex, plantUml }; + }); + // custom markdown to html rendering: replace the plantUml in the markdown with a simple
container with a unique id placeholder + // with the global unique id so that we can find the plantUml later on, when it was rendered, and then inject the 'actual' inner html (actually a svg image) + const replacedText = plantUmlsIndexed.reduce((acc: string, umlIndexed: { plantUmlId: number; plantUml: string }): string => { + return acc.replace(new RegExp(escapeStringForUseInRegex(umlIndexed.plantUml), 'g'), plantUmlContainer.replace(idPlaceholder, umlIndexed.plantUmlId.toString())); + }, text); + // before we send the plantUml to the server for rendering, we need to inject the current test status so that the colors can be adapted + // (green == implemented, red == not yet implemented, grey == unknown) + const plantUmlsValidated = plantUmlsIndexed.map((plantUmlIndexed: { plantUmlId: number; plantUml: string }) => { + plantUmlIndexed.plantUml = plantUmlIndexed.plantUml.replace(testsColorRegex, (match: string, capture: string) => { + const tests = this.programmingExerciseInstructionService.convertTestListToIds(capture, this.testCases); + const { testCaseState } = this.programmingExerciseInstructionService.testStatusForTask(tests, this.latestResult); + switch (testCaseState) { + case TestCaseState.SUCCESS: + return 'green'; + case TestCaseState.FAIL: + return 'red'; + default: + return 'grey'; + } + }); + return plantUmlIndexed; + }); + // send the adapted plantUml to the server for rendering and inject the result into the html DOM based on the unique plantUml id + this.injectableElementsFoundSubject.next(() => { + plantUmlsValidated.forEach((plantUmlIndexed: { plantUmlId: number; plantUml: string }) => { + this.loadAndInjectPlantUml(plantUmlIndexed.plantUml, plantUmlIndexed.plantUmlId); + }); + }); + return replacedText; } } diff --git a/src/main/webapp/app/exercises/programming/shared/instructions-render/extensions/programming-exercise-task.extension.ts b/src/main/webapp/app/exercises/programming/shared/instructions-render/extensions/programming-exercise-task.extension.ts index 5e7b54444a14..c6cbc4f1a2a2 100644 --- a/src/main/webapp/app/exercises/programming/shared/instructions-render/extensions/programming-exercise-task.extension.ts +++ b/src/main/webapp/app/exercises/programming/shared/instructions-render/extensions/programming-exercise-task.extension.ts @@ -1,8 +1,7 @@ import { Injectable, ViewContainerRef } from '@angular/core'; import { TaskArrayWithExercise } from 'app/exercises/programming/shared/instructions-render/task/programming-exercise-task.model'; -import { ArtemisShowdownExtensionWrapper } from 'app/shared/markdown-editor/extensions/artemis-showdown-extension-wrapper'; +import { ArtemisTextReplacementPlugin } from 'app/shared/markdown-editor/extensions/ArtemisTextReplacementPlugin'; import { Observable, Subject } from 'rxjs'; -import { ShowdownExtension } from 'showdown'; /** * Regular expression for finding tasks. @@ -18,15 +17,12 @@ import { ShowdownExtension } from 'showdown'; export const taskRegex = /\[task]\[([^[\]]+)]\(((?:[^(),]+(?:\([^()]*\)[^(),]*)?(?:,[^(),]+(?:\([^()]*\)[^(),]*)?)*)?)\)/g; @Injectable({ providedIn: 'root' }) -export class ProgrammingExerciseTaskExtensionWrapper implements ArtemisShowdownExtensionWrapper { +export class ProgrammingExerciseTaskExtensionWrapper extends ArtemisTextReplacementPlugin { // We don't have a provider for ViewContainerRef, so we pass it from ProgrammingExerciseInstructionComponent viewContainerRef: ViewContainerRef; private testsForTaskSubject = new Subject(); private injectableElementsFoundSubject = new Subject<() => void>(); - - constructor() {} - /** * Subscribes to injectableElementsFoundSubject. */ @@ -35,23 +31,12 @@ export class ProgrammingExerciseTaskExtensionWrapper implements ArtemisShowdownE } /** - * Creates and returns an extension to current exercise. - * The task regex is coupled to the value used in ProgrammingExerciseTaskService in the server and - * `TaskCommand` in the client + * The task regex is coupled to the value used in ProgrammingExerciseTaskService in the server + * and `TaskCommand` in the client * If you change the regex, make sure to change it in all places! */ - getExtension() { - const extension: ShowdownExtension = { - type: 'lang', - filter: (problemStatement: string) => { - return this.createTasks(problemStatement); - }, - }; - return extension; - } - - public createTasks(problemStatement: string): string { - return problemStatement.replace(taskRegex, (match) => { + replaceText(text: string): string { + return text.replace(taskRegex, (match) => { return this.escapeTaskSpecialCharactersForMarkdown(match); }); } diff --git a/src/main/webapp/app/exercises/programming/shared/instructions-render/programming-exercise-instruction.component.ts b/src/main/webapp/app/exercises/programming/shared/instructions-render/programming-exercise-instruction.component.ts index b0bd830496a9..9eac396fa2bc 100644 --- a/src/main/webapp/app/exercises/programming/shared/instructions-render/programming-exercise-instruction.component.ts +++ b/src/main/webapp/app/exercises/programming/shared/instructions-render/programming-exercise-instruction.component.ts @@ -16,7 +16,7 @@ import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { ThemeService } from 'app/core/theme/theme.service'; import { ProgrammingExerciseTestCase } from 'app/entities/programming/programming-exercise-test-case.model'; import { ProgrammingExerciseGradingService } from 'app/exercises/programming/manage/services/programming-exercise-grading.service'; -import { ShowdownExtension } from 'showdown'; +import type { PluginSimple } from 'markdown-it'; import { catchError, filter, map, mergeMap, switchMap, tap } from 'rxjs/operators'; import { Observable, Subscription, merge, of } from 'rxjs'; import { ProgrammingExercise } from 'app/entities/programming/programming-exercise.model'; @@ -80,7 +80,7 @@ export class ProgrammingExerciseInstructionComponent implements OnChanges, OnDes public renderedMarkdown: SafeHtml; private injectableContentForMarkdownCallbacks: Array<() => void> = []; - markdownExtensions: ShowdownExtension[]; + markdownExtensions: PluginSimple[]; private injectableContentFoundSubscription: Subscription; private tasksSubscription: Subscription; private generateHtmlSubscription: Subscription; diff --git a/src/main/webapp/app/exercises/quiz/shared/short-answer-question-util.service.ts b/src/main/webapp/app/exercises/quiz/shared/short-answer-question-util.service.ts index c9cf7bc98076..6e6aac7e24eb 100644 --- a/src/main/webapp/app/exercises/quiz/shared/short-answer-question-util.service.ts +++ b/src/main/webapp/app/exercises/quiz/shared/short-answer-question-util.service.ts @@ -373,6 +373,7 @@ export class ShortAnswerQuestionUtil { if (firstWord === '') { continue; } + const firstWordIndex = element.indexOf(firstWord); const whitespace = ' '.repeat(this.getIndentation(originalTextParts[i][0]).length); formattedTextParts[i][0] = [element.substring(0, firstWordIndex), whitespace, element.substring(firstWordIndex).trim()].join(''); diff --git a/src/main/webapp/app/index.d.ts b/src/main/webapp/app/index.d.ts index 44ce332f5e6b..41fc280ef9ff 100644 --- a/src/main/webapp/app/index.d.ts +++ b/src/main/webapp/app/index.d.ts @@ -1,9 +1,4 @@ -declare module 'showdown-katex' { - const main: () => ShowDownExtension; - export = main; -} - -declare module 'showdown-highlight' { - const main: ({ pre: boolean }) => ShowDownExtension; +declare module 'markdown-it-class' { + const main: (md: MarkdownIt) => void; export = main; } diff --git a/src/main/webapp/app/shared/markdown-editor/extensions/ArtemisTextReplacementPlugin.ts b/src/main/webapp/app/shared/markdown-editor/extensions/ArtemisTextReplacementPlugin.ts new file mode 100644 index 000000000000..0a913251fc6d --- /dev/null +++ b/src/main/webapp/app/shared/markdown-editor/extensions/ArtemisTextReplacementPlugin.ts @@ -0,0 +1,24 @@ +import type MarkdownIt from 'markdown-it'; +import type { PluginSimple } from 'markdown-it'; + +/** + * Markdown-It plugin that allows replacing text in the raw markdown before tokenizing. + * See more about Markdown-It plugins here: https://github.com/markdown-it/markdown-it/tree/master/docs + */ +export abstract class ArtemisTextReplacementPlugin { + getExtension(): PluginSimple { + return (md: MarkdownIt): void => { + md.core.ruler.before('normalize', 'artemis_text_replacement', (state) => { + // Perform the replacement on the raw markdown text + state.src = this.replaceText(state.src); + }); + }; + } + + /** + * Performs text replacement on the raw markdown before parsing. + * @param text The raw markdown text. + * @returns The modified markdown text after replacements. + */ + abstract replaceText(text: string): string; +} diff --git a/src/main/webapp/app/shared/markdown-editor/extensions/artemis-showdown-extension-wrapper.ts b/src/main/webapp/app/shared/markdown-editor/extensions/artemis-showdown-extension-wrapper.ts deleted file mode 100644 index ce716b886c4e..000000000000 --- a/src/main/webapp/app/shared/markdown-editor/extensions/artemis-showdown-extension-wrapper.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ShowdownExtension } from 'showdown'; -import { Observable } from 'rxjs'; - -/** - * The idea of this interface is to provide more information for an extension. - * By implementing the interface, the extension can use data that is in the closure of the class (e.g. this.latestResult). - * 1) The component that uses the extension can request it from the wrapper class by using getExtension. - * 2) In some cases it might also be necessary to inject content after the html is loaded, as async data fetching is necessary. - * Therefore, the component can subscribe for injectable elements. - * - */ -export interface ArtemisShowdownExtensionWrapper { - getExtension: () => ShowdownExtension; - subscribeForInjectableElementsFound: () => Observable<() => void>; -} diff --git a/src/main/webapp/app/shared/markdown-editor/monaco/markdown-editor-monaco.component.html b/src/main/webapp/app/shared/markdown-editor/monaco/markdown-editor-monaco.component.html index f34057d6ca0a..9b9c54d262fb 100644 --- a/src/main/webapp/app/shared/markdown-editor/monaco/markdown-editor-monaco.component.html +++ b/src/main/webapp/app/shared/markdown-editor/monaco/markdown-editor-monaco.component.html @@ -40,7 +40,7 @@ > diff --git a/src/main/webapp/app/shared/markdown.service.ts b/src/main/webapp/app/shared/markdown.service.ts index 646b03cb1b81..24e2589c616a 100644 --- a/src/main/webapp/app/shared/markdown.service.ts +++ b/src/main/webapp/app/shared/markdown.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; -import { addCSSClass, htmlForMarkdown } from 'app/shared/util/markdown.conversion.util'; -import showdown from 'showdown'; +import { htmlForMarkdown } from 'app/shared/util/markdown.conversion.util'; +import type { PluginSimple } from 'markdown-it'; @Injectable({ providedIn: 'root' }) export class ArtemisMarkdownService { @@ -11,21 +11,21 @@ export class ArtemisMarkdownService { * Converts markdown into html, sanitizes it and then declares it as safe to bypass further security. * * @param {string} markdownText the original markdown text - * @param extensions to use for markdown parsing + * @param {PluginSimple[]} extensions to use for markdown parsing * @param {string[]} allowedHtmlTags to allow during sanitization * @param {string[]} allowedHtmlAttributes to allow during sanitization * @returns {string} the resulting html as a SafeHtml object that can be inserted into the angular template */ safeHtmlForMarkdown( markdownText?: string, - extensions: showdown.ShowdownExtension[] = [], + extensions: PluginSimple[] = [], allowedHtmlTags: string[] | undefined = undefined, allowedHtmlAttributes: string[] | undefined = undefined, ): SafeHtml { if (!markdownText || markdownText === '') { return ''; } - const convertedString = htmlForMarkdown(markdownText, [...extensions, ...addCSSClass], allowedHtmlTags, allowedHtmlAttributes); + const convertedString = htmlForMarkdown(markdownText, extensions, allowedHtmlTags, allowedHtmlAttributes); return this.sanitizer.bypassSecurityTrustHtml(convertedString); } diff --git a/src/main/webapp/app/shared/pipes/html-for-markdown.pipe.ts b/src/main/webapp/app/shared/pipes/html-for-markdown.pipe.ts index 610faf20c0ca..660a5c0971dc 100644 --- a/src/main/webapp/app/shared/pipes/html-for-markdown.pipe.ts +++ b/src/main/webapp/app/shared/pipes/html-for-markdown.pipe.ts @@ -1,7 +1,7 @@ import { Pipe, PipeTransform } from '@angular/core'; -import { ShowdownExtension } from 'showdown'; import { SafeHtml } from '@angular/platform-browser'; import { ArtemisMarkdownService } from 'app/shared/markdown.service'; +import type { PluginSimple } from 'markdown-it'; @Pipe({ name: 'htmlForMarkdown', @@ -12,14 +12,14 @@ export class HtmlForMarkdownPipe implements PipeTransform { /** * Converts markdown into html, sanitizes it and then declares it as safe to bypass further security. * @param {string} markdown the original markdown text - * @param {ShowdownExtension[]} extensions to use for markdown parsing + * @param {PluginSimple[]} extensions to use for markdown parsing * @param {string[]} allowedHtmlTags to allow during sanitization * @param {string[]} allowedHtmlAttributes to allow during sanitization * @returns {string} the resulting html as a SafeHtml object that can be inserted into the angular template */ transform( markdown?: string, - extensions: ShowdownExtension[] = [], + extensions: PluginSimple[] = [], allowedHtmlTags: string[] | undefined = undefined, allowedHtmlAttributes: string[] | undefined = undefined, ): SafeHtml { diff --git a/src/main/webapp/app/shared/util/markdown.conversion.util.ts b/src/main/webapp/app/shared/util/markdown.conversion.util.ts index 658574db088b..d5166b70d690 100644 --- a/src/main/webapp/app/shared/util/markdown.conversion.util.ts +++ b/src/main/webapp/app/shared/util/markdown.conversion.util.ts @@ -1,24 +1,41 @@ -import showdown from 'showdown'; -import showdownKatex from 'showdown-katex'; -import showdownHighlight from 'showdown-highlight'; +import { ArtemisTextReplacementPlugin } from 'app/shared/markdown-editor/extensions/ArtemisTextReplacementPlugin'; import DOMPurify, { Config } from 'dompurify'; +import type { PluginSimple } from 'markdown-it'; +import markdownIt from 'markdown-it'; +import markdownItClass from 'markdown-it-class'; +import markdownItKatex from '@vscode/markdown-it-katex'; +import markdownItHighlightjs from 'markdown-it-highlightjs'; +import TurndownService from 'turndown'; /** - * showdown will add the classes to the converted html - * see: https://github.com/showdownjs/showdown/wiki/Add-default-classes-for-each-HTML-element + * Add these classes to the converted html. */ const classMap: { [key: string]: string } = { table: 'table', }; -/** - * extension to add css classes to html tags - * see: https://github.com/showdownjs/showdown/wiki/Add-default-classes-for-each-HTML-element - */ -export const addCSSClass = Object.keys(classMap).map((key) => ({ - type: 'output', - regex: new RegExp(`<${key}(.*)>`, 'g'), - replace: `<${key} class="${classMap[key]}" $1>`, -})); + +// An inline math formula has some other characters before or after the formula and uses $$ as delimiters +const inlineFormulaRegex = /(?:.+\$\$[^\$]+\$\$)|(?:\$\$[^\$]+\$\$.+)/g; + +class FormulaCompatibilityPlugin extends ArtemisTextReplacementPlugin { + replaceText(text: string): string { + return text + .split('\n') + .map((line) => { + if (line.match(inlineFormulaRegex)) { + line = line.replace(/\$\$/g, '$'); + } + if (line.includes('\\\\begin') || line.includes('\\\\end')) { + line = line.replaceAll('\\\\begin', '\\begin').replaceAll('\\\\end', '\\end'); + } + return line; + }) + .join('\n'); + } +} +const formulaCompatibilityPlugin = new FormulaCompatibilityPlugin(); + +const turndownService = new TurndownService(); /** * Converts markdown into html (string) and sanitizes it. Does NOT declare it as safe to bypass further security @@ -32,24 +49,37 @@ export const addCSSClass = Object.keys(classMap).map((key) => ({ */ export function htmlForMarkdown( markdownText?: string, - extensions: showdown.ShowdownExtension[] = [], + extensions: PluginSimple[] = [], allowedHtmlTags: string[] | undefined = undefined, allowedHtmlAttributes: string[] | undefined = undefined, ): string { if (!markdownText || markdownText === '') { return ''; } - const converter = new showdown.Converter({ - parseImgDimensions: true, - headerLevelStart: 3, - simplifiedAutoLink: true, - strikethrough: true, - tables: true, - openLinksInNewWindow: true, - backslashEscapesHTMLTags: true, - extensions: [...extensions, showdownKatex(), showdownHighlight({ pre: true }), ...addCSSClass], + + const md = markdownIt({ + html: true, + linkify: true, + breaks: false, // Avoid line breaks after tasks }); - const html = converter.makeHtml(markdownText); + for (const extension of extensions) { + md.use(extension); + } + + // Add default extensions (Code Highlight, Latex) + md.use(markdownItHighlightjs) + .use(formulaCompatibilityPlugin.getExtension()) + .use(markdownItKatex, { + enableMathInlineInHtml: true, + }) + .use(markdownItClass, classMap); + let markdownRender = md.render(markdownText); + if (markdownRender.endsWith('\n')) { + // Keep legacy behavior from showdown where the output does not end with \n. + // This is needed because e.g. for quiz questions, we render the markdown in multiple small parts and then concatenate them. + markdownRender = markdownRender.slice(0, -1); + } + const purifyParameters = {} as Config; // Prevents sanitizer from deleting id purifyParameters['ADD_TAGS'] = ['testid']; @@ -59,18 +89,9 @@ export function htmlForMarkdown( if (allowedHtmlAttributes) { purifyParameters['ALLOWED_ATTR'] = allowedHtmlAttributes; } - return DOMPurify.sanitize(html, purifyParameters) as string; + return DOMPurify.sanitize(markdownRender, purifyParameters) as string; } export function markdownForHtml(htmlText: string): string { - const converter = new showdown.Converter({ - parseImgDimensions: true, - headerLevelStart: 3, - simplifiedAutoLink: true, - strikethrough: true, - tables: true, - openLinksInNewWindow: true, - backslashEscapesHTMLTags: true, - }); - return converter.makeMarkdown(htmlText); + return turndownService.turndown(htmlText); } diff --git a/src/test/javascript/spec/component/exercises/quiz/short-answer-question-util.service.spec.ts b/src/test/javascript/spec/component/exercises/quiz/short-answer-question-util.service.spec.ts index da7e4949bbb3..47aeba9f7fd9 100644 --- a/src/test/javascript/spec/component/exercises/quiz/short-answer-question-util.service.spec.ts +++ b/src/test/javascript/spec/component/exercises/quiz/short-answer-question-util.service.spec.ts @@ -216,7 +216,7 @@ describe('ShortAnswerQuestionUtil', () => { const originalTextParts2 = [['`random code`'], ['` some more code`', '[-spot 1]'], ['`last code paragraph`']]; const formattedTextParts2 = [ ['

random code

'], - ['

    some more code

', '

[-spot 1]

'], + ['

    some more code

', '

[-spot 1]

'], ['

last code paragraph

'], ]; expect(service.transformTextPartsIntoHTML(originalTextParts2)).toEqual(formattedTextParts2); diff --git a/src/test/javascript/spec/component/lecture-unit/text-unit/text-unit.component.spec.ts b/src/test/javascript/spec/component/lecture-unit/text-unit/text-unit.component.spec.ts index dc8ffa8669af..3326a8e91514 100644 --- a/src/test/javascript/spec/component/lecture-unit/text-unit/text-unit.component.spec.ts +++ b/src/test/javascript/spec/component/lecture-unit/text-unit/text-unit.component.spec.ts @@ -27,7 +27,7 @@ describe('TextUnitComponent', () => { visibleToStudents: true, }; - const exampleHtml = '

Sample Markdown

'; + const exampleHtml = '

Sample Markdown

'; beforeEach(async () => { await TestBed.configureTestingModule({ diff --git a/src/test/javascript/spec/component/shared/metis/posting-content/posting-content-part.component.spec.ts b/src/test/javascript/spec/component/shared/metis/posting-content/posting-content-part.component.spec.ts index 81e073bf43a6..424567dd6518 100644 --- a/src/test/javascript/spec/component/shared/metis/posting-content/posting-content-part.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/posting-content/posting-content-part.component.spec.ts @@ -94,7 +94,7 @@ describe('PostingContentPartComponent', () => { expect(markdownRenderedTexts).toHaveLength(2); // check that the paragraph right before the reference and the paragraph right after have the class `inline-paragraph` expect(markdownRenderedTexts![0].innerHTML).toInclude('

Be aware

'); - expect(markdownRenderedTexts![0].innerHTML).toInclude('

I want to reference the following Post

'); // last paragraph before reference + expect(markdownRenderedTexts![0].innerHTML).toInclude('

I want to reference the following Post

'); // last paragraph before reference expect(markdownRenderedTexts![1].innerHTML).toInclude('

in my content,

'); // first paragraph after reference expect(markdownRenderedTexts![1].innerHTML).toInclude('

does it actually work?

'); diff --git a/src/test/javascript/spec/helpers/sample/problemStatement.json b/src/test/javascript/spec/helpers/sample/problemStatement.json index 125a7c25d5c9..6a9297f35013 100644 --- a/src/test/javascript/spec/helpers/sample/problemStatement.json +++ b/src/test/javascript/spec/helpers/sample/problemStatement.json @@ -7,8 +7,8 @@ "problemStatementBothFailedRendered": "
    \n
  1. Implement Bubble Sort: artemisApp.editor.testStatusLabels.noResult
    \nImplement the method performSort(List<Date>) in the class BubbleSort. Make sure to follow the Bubble Sort algorithm exactly.
  2. \n
  3. Implement Merge Sort: artemisApp.editor.testStatusLabels.noResult
    \nImplement the method performSort(List<Date>) in the class MergeSort. Make sure to follow the Merge Sort algorithm exactly.
  4. \n
\n", "problemStatementBothFailedHtml": "
    \n
  1. Implement Bubble Sort: artemisApp.editor.testStatusLabels.testFailing
    \nImplement the method performSort(List<Date>) in the class BubbleSort. Make sure to follow the Bubble Sort algorithm exactly.
  2. \n
  3. Implement Merge Sort: artemisApp.editor.testStatusLabels.testPassing
    \nImplement the method performSort(List<Date>) in the class MergeSort. Make sure to follow the Merge Sort algorithm exactly.
  4. \n
\n", "problemStatementBubbleSortFailsRendered": "
    \n
  1. Implement Bubble Sort: artemisApp.editor.testStatusLabels.noResult
    \nImplement the method performSort(List<Date>) in the class BubbleSort. Make sure to follow the Bubble Sort algorithm exactly.
  2. \n
  3. Implement Merge Sort: artemisApp.editor.testStatusLabels.noResult
    \nImplement the method performSort(List<Date>) in the class MergeSort. Make sure to follow the Merge Sort algorithm exactly.
  4. \n
\n", - "problemStatementBubbleSortNotExecutedHtml": "
    \n
  1. Implement Bubble SortartemisApp.editor.testStatusLabels.totalTestsPassing: [{\"totalTests\":1,\"passedTests\":0}]
    \nImplement the method performSort(List<Date>) in the class BubbleSort. Make sure to follow the Bubble Sort algorithm exactly.
  2. \n
  3. Implement Merge SortartemisApp.editor.testStatusLabels.totalTestsPassing: [{\"totalTests\":1,\"passedTests\":1}]
    \nImplement the method performSort(List<Date>) in the class MergeSort. Make sure to follow the Merge Sort algorithm exactly.
  4. \n
", + "problemStatementBubbleSortNotExecutedHtml": "
    \n
  1. Implement Bubble SortartemisApp.editor.testStatusLabels.totalTestsPassing: [{\"totalTests\":1,\"passedTests\":0}]
    \nImplement the method performSort(List<Date>) in the class BubbleSort. Make sure to follow the Bubble Sort algorithm exactly.
  2. \n
  3. Implement Merge SortartemisApp.editor.testStatusLabels.totalTestsPassing: [{\"totalTests\":1,\"passedTests\":1}]
    \nImplement the method performSort(List<Date>) in the class MergeSort. Make sure to follow the Merge Sort algorithm exactly.
  4. \n
", "problemStatementEmptySecondTask": "1. [task][Bubble Sort](1) \n Implement the method. \n 2. [task][Merge Sort]() \n Implement the method.", - "problemStatementEmptySecondTaskNotExecutedHtml": "
    \n
  1. Bubble SortartemisApp.editor.testStatusLabels.totalTestsPassing: [{\"totalTests\":1,\"passedTests\":1}]
    \nImplement the method.
  2. \n
  3. Merge SortartemisApp.editor.testStatusLabels.noTests
    \nImplement the method.
  4. \n
", + "problemStatementEmptySecondTaskNotExecutedHtml": "
    \n
  1. Bubble SortartemisApp.editor.testStatusLabels.totalTestsPassing: [{\"totalTests\":1,\"passedTests\":1}]
    \nImplement the method.
  2. \n
  3. Merge SortartemisApp.editor.testStatusLabels.noTests
    \nImplement the method.
  4. \n
", "problemStatementPlantUMLWithTest": "@startuml\nclass Policy {\n1)>+configure()\n2)>+testWithParenthesis()}\n@enduml" } diff --git a/src/test/javascript/spec/service/markdown.service.spec.ts b/src/test/javascript/spec/service/markdown.service.spec.ts index 6473a2b6405d..d33f0113eea9 100644 --- a/src/test/javascript/spec/service/markdown.service.spec.ts +++ b/src/test/javascript/spec/service/markdown.service.spec.ts @@ -108,4 +108,32 @@ describe('Markdown Service', () => { const safeMarkdownWithoutExtras = htmlForMarkdown(markdownString, [], [], []); expect(safeMarkdownWithoutExtras).toBe('Will this render blue?'); }); + + describe('formulaCompatibilityPlugin', () => { + it.each(['This is a formula $$E=mc^2$$ in text.', '$$a_1$$ formula at front', 'formula at back $$a_2$$'])('converts block formulas to inline formulas', (input) => { + const result = htmlForMarkdown(input); + expect(result).toContain(''); + expect(result).not.toContain('class="katex-block"'); + }); + + it('does not convert block formulas without surrounding text', () => { + const result = htmlForMarkdown('$$E=mc^2$$'); + expect(result).toContain('class="katex-block"'); + expect(result).toContain('display="block"'); + }); + + it('converts double-backslash LaTeX begin and end tags', () => { + const result = htmlForMarkdown('Here is some LaTeX: $$\\\\begin{equation}a^2 + b^2 = c^2\\\\end{equation}$$\n'); + expect(result).toContain(''); + expect(result).toContain('class="katex-html"'); + }); + + it('handles multiple formulas in the same text', () => { + const result = htmlForMarkdown('First formula $$a^2 + b^2 = c^2$$ and second formula $$E=mc^2$$.'); + const formulaCount = (result.match(/class="katex"/g) || []).length; + expect(formulaCount).toBe(2); + expect(result).not.toContain('class="katex-block"'); + expect(result).not.toContain('display="block"'); + }); + }); }); From 7346de1a79695d6b30949c17780c881c3c7e3b43 Mon Sep 17 00:00:00 2001 From: Florian Glombik <63976129+florian-glombik@users.noreply.github.com> Date: Sun, 27 Oct 2024 20:27:51 +0100 Subject: [PATCH 2/5] Development: Fix DOM event name conflicts (#9589) --- .../assessment-header.component.html | 6 +++--- .../assessment-header.component.ts | 10 ++++------ .../assessment-layout.component.html | 4 ++-- .../assessment-layout.component.ts | 6 ++---- .../complaints-student-view.component.html | 2 +- .../complaints/form/complaints-form.component.ts | 5 ++--- .../assess/file-upload-assessment.component.html | 4 ++-- .../file-upload-exercise-update.component.html | 2 +- .../modeling-assessment-editor.component.html | 4 ++-- .../modeling-exercise-update.component.html | 2 +- ...tor-tutor-assessment-container.component.html | 4 ++-- .../programming-exercise-update.component.html | 2 +- .../text-submission-assessment.component.html | 4 ++-- .../textblock-assessment-card.component.html | 2 +- .../textblock-feedback-editor.component.ts | 4 ++-- .../text-exercise-update.component.html | 2 +- .../forms/form-footer/form-footer.component.html | 2 +- .../forms/form-footer/form-footer.component.ts | 4 ++-- .../assessment-header.component.spec.ts | 16 ++++++++-------- .../complaints/complaints-form.component.spec.ts | 8 ++++---- 20 files changed, 44 insertions(+), 49 deletions(-) diff --git a/src/main/webapp/app/assessment/assessment-header/assessment-header.component.html b/src/main/webapp/app/assessment/assessment-header/assessment-header.component.html index fbf4de107932..47c600031279 100644 --- a/src/main/webapp/app/assessment/assessment-header/assessment-header.component.html +++ b/src/main/webapp/app/assessment/assessment-header/assessment-header.component.html @@ -115,7 +115,7 @@

diff --git a/src/main/webapp/app/forms/form-footer/form-footer.component.ts b/src/main/webapp/app/forms/form-footer/form-footer.component.ts index 378422878b70..bb2192728381 100644 --- a/src/main/webapp/app/forms/form-footer/form-footer.component.ts +++ b/src/main/webapp/app/forms/form-footer/form-footer.component.ts @@ -10,8 +10,8 @@ import { ButtonSize } from 'app/shared/components/button.component'; }) export class FormFooterComponent { @Output() save = new EventEmitter(); - // eslint-disable-next-line @angular-eslint/no-output-native - @Output() cancel = new EventEmitter(); + + @Output() onCancel = new EventEmitter(); @Input() isSaving: boolean = false; @Input() isDisabled: boolean = false; diff --git a/src/test/javascript/spec/component/assessment-shared/assessment-header.component.spec.ts b/src/test/javascript/spec/component/assessment-shared/assessment-header.component.spec.ts index a85e5e47bc16..aa0073961211 100644 --- a/src/test/javascript/spec/component/assessment-shared/assessment-header.component.spec.ts +++ b/src/test/javascript/spec/component/assessment-shared/assessment-header.component.spec.ts @@ -159,14 +159,14 @@ describe('AssessmentHeaderComponent', () => { saveButtonSpan.nativeElement.click(); expect(component.save.emit).toHaveBeenCalledOnce(); - jest.spyOn(component.submit, 'emit'); + jest.spyOn(component.onSubmit, 'emit'); submitButtonSpan.nativeElement.click(); - expect(component.submit.emit).toHaveBeenCalledOnce(); + expect(component.onSubmit.emit).toHaveBeenCalledOnce(); const cancelButtonSpan = fixture.debugElement.query(By.css('[jhiTranslate$=cancel]')); - jest.spyOn(component.cancel, 'emit'); + jest.spyOn(component.onCancel, 'emit'); cancelButtonSpan.nativeElement.click(); - expect(component.cancel.emit).toHaveBeenCalledOnce(); + expect(component.onCancel.emit).toHaveBeenCalledOnce(); }); it('should show override button when result is present', () => { @@ -189,9 +189,9 @@ describe('AssessmentHeaderComponent', () => { overrideAssessmentButtonSpan = fixture.debugElement.query(By.css('[jhiTranslate$=overrideAssessment]')); expect(overrideAssessmentButtonSpan).toBeTruthy(); - jest.spyOn(component.submit, 'emit'); + jest.spyOn(component.onSubmit, 'emit'); overrideAssessmentButtonSpan.nativeElement.click(); - expect(component.submit.emit).toHaveBeenCalledOnce(); + expect(component.onSubmit.emit).toHaveBeenCalledOnce(); }); it('should show next submission if assessor or instructor, result is present and no complaint', () => { @@ -345,7 +345,7 @@ describe('AssessmentHeaderComponent', () => { const eventMock = new KeyboardEvent('keydown', { ctrlKey: true, key: 'Enter' }); const spyOnControlAndEnter = jest.spyOn(component, 'submitOnControlAndEnter'); - const submitSpy = jest.spyOn(component.submit, 'emit'); + const submitSpy = jest.spyOn(component.onSubmit, 'emit'); document.dispatchEvent(eventMock); expect(spyOnControlAndEnter).toHaveBeenCalledOnce(); @@ -362,7 +362,7 @@ describe('AssessmentHeaderComponent', () => { const eventMock = new KeyboardEvent('keydown', { ctrlKey: true, key: 'Enter' }); const spyOnControlAndEnter = jest.spyOn(component, 'submitOnControlAndEnter'); - const submitSpy = jest.spyOn(component.submit, 'emit'); + const submitSpy = jest.spyOn(component.onSubmit, 'emit'); document.dispatchEvent(eventMock); expect(spyOnControlAndEnter).toHaveBeenCalledOnce(); diff --git a/src/test/javascript/spec/component/complaints/complaints-form.component.spec.ts b/src/test/javascript/spec/component/complaints/complaints-form.component.spec.ts index c0e144ca8828..488c740be038 100644 --- a/src/test/javascript/spec/component/complaints/complaints-form.component.spec.ts +++ b/src/test/javascript/spec/component/complaints/complaints-form.component.spec.ts @@ -84,7 +84,7 @@ describe('ComplaintsFormComponent', () => { it('should submit after complaint creation', () => { const createMock = jest.spyOn(complaintService, 'create').mockReturnValue(of({} as EntityResponseType)); - const submitSpy = jest.spyOn(component.submit, 'emit'); + const submitSpy = jest.spyOn(component.onSubmit, 'emit'); component.createComplaint(); expect(createMock).toHaveBeenCalledOnce(); expect(submitSpy).toHaveBeenCalledOnce(); @@ -93,7 +93,7 @@ describe('ComplaintsFormComponent', () => { it('should throw unknown error after complaint creation', () => { const createMock = jest.spyOn(complaintService, 'create').mockReturnValue(throwError(() => ({ status: 400 }))); - const submitSpy = jest.spyOn(component.submit, 'emit'); + const submitSpy = jest.spyOn(component.onSubmit, 'emit'); const errorSpy = jest.spyOn(alertService, 'error'); component.createComplaint(); expect(createMock).toHaveBeenCalledOnce(); @@ -104,7 +104,7 @@ describe('ComplaintsFormComponent', () => { it('should throw known error after complaint creation', () => { const error = { error: { errorKey: 'tooManyComplaints' } } as HttpErrorResponse; const createMock = jest.spyOn(complaintService, 'create').mockReturnValue(throwError(() => error)); - const submitSpy = jest.spyOn(component.submit, 'emit'); + const submitSpy = jest.spyOn(component.onSubmit, 'emit'); const errorSpy = jest.spyOn(alertService, 'error'); const numberOfComplaints = 42; component.maxComplaintsPerCourse = numberOfComplaints; @@ -120,7 +120,7 @@ describe('ComplaintsFormComponent', () => { component.exercise = courseExercise; component.ngOnInit(); - const submitSpy = jest.spyOn(component.submit, 'emit'); + const submitSpy = jest.spyOn(component.onSubmit, 'emit'); const errorSpy = jest.spyOn(alertService, 'error'); // 26 characters component.complaintText = 'abcdefghijklmnopqrstuvwxyz'; From efe7c16f542e7954ea31ef7c9063e9931e72b046 Mon Sep 17 00:00:00 2001 From: Mohamed Bilel Besrour <58034472+BBesrour@users.noreply.github.com> Date: Sun, 27 Oct 2024 20:45:07 +0100 Subject: [PATCH 3/5] Development: Fix hazelcast issue on server shutdown (#9602) --- .../service/feature/FeatureToggleService.java | 91 +++++++++++++++---- 1 file changed, 75 insertions(+), 16 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/feature/FeatureToggleService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/feature/FeatureToggleService.java index 83438c369cfd..3e2a906bee8b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/feature/FeatureToggleService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/feature/FeatureToggleService.java @@ -4,15 +4,19 @@ import java.util.List; import java.util.Map; +import java.util.Optional; -import jakarta.annotation.PostConstruct; - +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.annotation.Profile; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import com.hazelcast.core.HazelcastInstance; +import com.hazelcast.core.HazelcastInstanceNotActiveException; import de.tum.cit.aet.artemis.communication.service.WebsocketMessagingService; @@ -20,6 +24,8 @@ @Service public class FeatureToggleService { + private static final Logger log = LoggerFactory.getLogger(FeatureToggleService.class); + private static final String TOPIC_FEATURE_TOGGLES = "/topic/management/feature-toggles"; @Value("${artemis.science.event-logging.enable:false}") @@ -36,10 +42,22 @@ public FeatureToggleService(WebsocketMessagingService websocketMessagingService, this.hazelcastInstance = hazelcastInstance; } + private Optional> getFeatures() { + try { + if (isHazelcastRunning()) { + return Optional.ofNullable(features); + } + } + catch (HazelcastInstanceNotActiveException e) { + log.error("Failed to get features in {} as Hazelcast instance is not active anymore.", FeatureToggleService.class.getSimpleName()); + } + return Optional.empty(); + } + /** * Initialize relevant data from hazelcast */ - @PostConstruct + @EventListener(ApplicationReadyEvent.class) public void init() { // The map will automatically be distributed between all instances by Hazelcast. features = hazelcastInstance.getMap("features"); @@ -63,8 +81,10 @@ public void init() { * @param feature The feature that should be enabled */ public void enableFeature(Feature feature) { - features.put(feature, true); - sendUpdate(); + getFeatures().ifPresent(features -> { + features.put(feature, true); + sendUpdate(); + }); } /** @@ -73,23 +93,34 @@ public void enableFeature(Feature feature) { * @param feature The feature that should be disabled */ public void disableFeature(Feature feature) { - features.put(feature, false); - sendUpdate(); + getFeatures().ifPresent(features -> { + features.put(feature, false); + sendUpdate(); + }); } /** * Updates the given feature toggles and enables/disables the features based on the given map. Also notifies all clients * by sending a message via the websocket. * - * @param features A map of features (feature -> shouldBeActivated) + * @param updatedFeatures A map of features (feature -> shouldBeActivated) */ - public void updateFeatureToggles(final Map features) { - this.features.putAll(features); - sendUpdate(); + public void updateFeatureToggles(final Map updatedFeatures) { + getFeatures().ifPresent(features -> { + features.putAll(updatedFeatures); + sendUpdate(); + }); } private void sendUpdate() { - websocketMessagingService.sendMessage(TOPIC_FEATURE_TOGGLES, enabledFeatures()); + try { + if (isHazelcastRunning()) { + websocketMessagingService.sendMessage(TOPIC_FEATURE_TOGGLES, enabledFeatures()); + } + } + catch (HazelcastInstanceNotActiveException e) { + log.error("Failed to send features update in {} as Hazelcast instance is not active anymore.", FeatureToggleService.class.getSimpleName()); + } } /** @@ -99,8 +130,16 @@ private void sendUpdate() { * @return if the feature is enabled */ public boolean isFeatureEnabled(Feature feature) { - Boolean isEnabled = features.get(feature); - return Boolean.TRUE.equals(isEnabled); + try { + if (isHazelcastRunning()) { + Boolean isEnabled = features.get(feature); + return Boolean.TRUE.equals(isEnabled); + } + } + catch (HazelcastInstanceNotActiveException e) { + log.error("Failed to check if feature is enabled in FeatureToggleService as Hazelcast instance is not active any more."); + } + return false; } /** @@ -109,7 +148,15 @@ public boolean isFeatureEnabled(Feature feature) { * @return A list of enabled features */ public List enabledFeatures() { - return features.entrySet().stream().filter(feature -> Boolean.TRUE.equals(feature.getValue())).map(Map.Entry::getKey).toList(); + try { + if (isHazelcastRunning()) { + return features.entrySet().stream().filter(feature -> Boolean.TRUE.equals(feature.getValue())).map(Map.Entry::getKey).toList(); + } + } + catch (HazelcastInstanceNotActiveException e) { + log.error("Failed to retrieve enabled features update in FeatureToggleService as Hazelcast instance is not active any more."); + } + return List.of(); } /** @@ -118,6 +165,18 @@ public List enabledFeatures() { * @return A list of disabled features */ public List disabledFeatures() { - return features.entrySet().stream().filter(feature -> Boolean.FALSE.equals(feature.getValue())).map(Map.Entry::getKey).toList(); + try { + if (isHazelcastRunning()) { + return features.entrySet().stream().filter(feature -> Boolean.FALSE.equals(feature.getValue())).map(Map.Entry::getKey).toList(); + } + } + catch (HazelcastInstanceNotActiveException e) { + log.error("Failed to retrieve disabled features update in FeatureToggleService as Hazelcast instance is not active any more."); + } + return List.of(); + } + + private boolean isHazelcastRunning() { + return hazelcastInstance != null && hazelcastInstance.getLifecycleService().isRunning(); } } From 041d5c96360254b196a4d5126001f10809c19b72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20St=C3=B6hr?= <38322605+JohannesStoehr@users.noreply.github.com> Date: Sun, 27 Oct 2024 20:56:53 +0100 Subject: [PATCH 4/5] Development: Ensure correct @Repository annotation usage (#9610) --- .../LongFeedbackTextRepository.java | 6 +++ .../CourseCompetencyRepository.java | 6 +++ .../repository/PrerequisiteRepository.java | 8 ++- .../repository/CustomPostRepository.java | 6 +++ .../core/repository/AuthorityRepository.java | 7 +++ .../repository/MigrationChangeRepository.java | 7 +++ .../PersistenceAuditEventRepository.java | 5 ++ ...IrisTextExerciseChatSessionRepository.java | 5 ++ .../repository/BuildPlanRepository.java | 5 ++ .../repository/hestia/CodeHintRepository.java | 6 +++ .../ExerciseHintActivationRepository.java | 6 +++ ...ammingExerciseSolutionEntryRepository.java | 6 +++ .../ProgrammingExerciseTaskRepository.java | 35 +++---------- .../atlas/AbstractAtlasIntegrationTest.java | 4 +- .../util/PrerequisiteUtilService.java | 4 +- .../PrerequisiteTestRepository.java | 19 +++++++ ...ProgrammingIntegrationIndependentTest.java | 4 +- ...ProgrammingExerciseTaskTestRepository.java | 52 +++++++++++++++++++ .../util/ProgrammingExerciseTestService.java | 4 +- .../util/ProgrammingExerciseUtilService.java | 4 +- ...tractModuleRepositoryArchitectureTest.java | 9 ++++ 21 files changed, 167 insertions(+), 41 deletions(-) create mode 100644 src/test/java/de/tum/cit/aet/artemis/atlas/test_repository/PrerequisiteTestRepository.java create mode 100644 src/test/java/de/tum/cit/aet/artemis/programming/test_repository/ProgrammingExerciseTaskTestRepository.java diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/repository/LongFeedbackTextRepository.java b/src/main/java/de/tum/cit/aet/artemis/assessment/repository/LongFeedbackTextRepository.java index 6ad61c4ef7ff..87df115afc0d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/repository/LongFeedbackTextRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/repository/LongFeedbackTextRepository.java @@ -1,14 +1,20 @@ package de.tum.cit.aet.artemis.assessment.repository; +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + import java.util.List; import java.util.Optional; +import org.springframework.context.annotation.Profile; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; import de.tum.cit.aet.artemis.assessment.domain.LongFeedbackText; import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; +@Profile(PROFILE_CORE) +@Repository public interface LongFeedbackTextRepository extends ArtemisJpaRepository { @Query(""" diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/repository/CourseCompetencyRepository.java b/src/main/java/de/tum/cit/aet/artemis/atlas/repository/CourseCompetencyRepository.java index d8b66519355c..0d672f1bcbc2 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/repository/CourseCompetencyRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/repository/CourseCompetencyRepository.java @@ -1,14 +1,18 @@ package de.tum.cit.aet.artemis.atlas.repository; +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + import java.util.List; import java.util.Optional; import java.util.Set; import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.annotation.Profile; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; import de.tum.cit.aet.artemis.atlas.domain.LearningObject; import de.tum.cit.aet.artemis.atlas.domain.competency.CourseCompetency; @@ -21,6 +25,8 @@ /** * Spring Data JPA repository for the {@link CourseCompetency} entity. */ +@Profile(PROFILE_CORE) +@Repository public interface CourseCompetencyRepository extends ArtemisJpaRepository { @Query(""" diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/repository/PrerequisiteRepository.java b/src/main/java/de/tum/cit/aet/artemis/atlas/repository/PrerequisiteRepository.java index 9616c2a5f34b..5d7fc9c56e49 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/repository/PrerequisiteRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/repository/PrerequisiteRepository.java @@ -1,11 +1,15 @@ package de.tum.cit.aet.artemis.atlas.repository; +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + import java.util.List; import java.util.Optional; import java.util.Set; +import org.springframework.context.annotation.Profile; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; import de.tum.cit.aet.artemis.atlas.domain.competency.Prerequisite; import de.tum.cit.aet.artemis.core.domain.Course; @@ -14,10 +18,10 @@ /** * Spring Data JPA repository for the {@link Prerequisite} entity. */ +@Profile(PROFILE_CORE) +@Repository public interface PrerequisiteRepository extends ArtemisJpaRepository { - List findAllByCourseIdOrderById(long courseId); - @Query(""" SELECT p FROM Prerequisite p diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/repository/CustomPostRepository.java b/src/main/java/de/tum/cit/aet/artemis/communication/repository/CustomPostRepository.java index d40778fbaae6..db460a6b27c6 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/repository/CustomPostRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/repository/CustomPostRepository.java @@ -1,11 +1,17 @@ package de.tum.cit.aet.artemis.communication.repository; +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + +import org.springframework.context.annotation.Profile; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Repository; import de.tum.cit.aet.artemis.communication.domain.Post; +@Profile(PROFILE_CORE) +@Repository public interface CustomPostRepository { Page findPostIdsWithSpecification(Specification specification, Pageable pageable); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/repository/AuthorityRepository.java b/src/main/java/de/tum/cit/aet/artemis/core/repository/AuthorityRepository.java index 4e3f3f0466af..70a1078fbf7b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/repository/AuthorityRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/repository/AuthorityRepository.java @@ -1,13 +1,20 @@ package de.tum.cit.aet.artemis.core.repository; +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + import java.util.List; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Repository; + import de.tum.cit.aet.artemis.core.domain.Authority; import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; /** * Spring Data JPA repository for the Authority entity. */ +@Profile(PROFILE_CORE) +@Repository public interface AuthorityRepository extends ArtemisJpaRepository { /** diff --git a/src/main/java/de/tum/cit/aet/artemis/core/repository/MigrationChangeRepository.java b/src/main/java/de/tum/cit/aet/artemis/core/repository/MigrationChangeRepository.java index 71b6b9c1a8c4..12ff470bed9f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/repository/MigrationChangeRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/repository/MigrationChangeRepository.java @@ -1,7 +1,14 @@ package de.tum.cit.aet.artemis.core.repository; +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Repository; + import de.tum.cit.aet.artemis.core.domain.MigrationChangelog; import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; +@Profile(PROFILE_CORE) +@Repository public interface MigrationChangeRepository extends ArtemisJpaRepository { } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/repository/PersistenceAuditEventRepository.java b/src/main/java/de/tum/cit/aet/artemis/core/repository/PersistenceAuditEventRepository.java index 9c4c133fe6da..c2afe2117540 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/repository/PersistenceAuditEventRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/repository/PersistenceAuditEventRepository.java @@ -1,5 +1,6 @@ package de.tum.cit.aet.artemis.core.repository; +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.LOAD; import java.time.Instant; @@ -9,12 +10,14 @@ import jakarta.validation.constraints.NotNull; +import org.springframework.context.annotation.Profile; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; import de.tum.cit.aet.artemis.core.domain.PersistentAuditEvent; import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; @@ -22,6 +25,8 @@ /** * Spring Data JPA repository for the PersistentAuditEvent entity. */ +@Profile(PROFILE_CORE) +@Repository public interface PersistenceAuditEventRepository extends ArtemisJpaRepository { @EntityGraph(type = LOAD, attributePaths = { "data" }) diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisTextExerciseChatSessionRepository.java b/src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisTextExerciseChatSessionRepository.java index a8f76c5ff679..be8d6c3b4331 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisTextExerciseChatSessionRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisTextExerciseChatSessionRepository.java @@ -1,5 +1,6 @@ package de.tum.cit.aet.artemis.iris.repository; +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_IRIS; import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.LOAD; import java.util.Collections; @@ -7,10 +8,12 @@ import jakarta.validation.constraints.NotNull; +import org.springframework.context.annotation.Profile; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; import de.tum.cit.aet.artemis.core.domain.DomainObject; import de.tum.cit.aet.artemis.core.exception.EntityNotFoundException; @@ -21,6 +24,8 @@ * Repository interface for managing {@link IrisTextExerciseChatSession} entities. * Provides custom queries for finding text exercise chat sessions based on different criteria. */ +@Profile(PROFILE_IRIS) +@Repository public interface IrisTextExerciseChatSessionRepository extends ArtemisJpaRepository { /** diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/BuildPlanRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/BuildPlanRepository.java index 1a1cd6167bea..9e9c996a8e37 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/BuildPlanRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/BuildPlanRepository.java @@ -1,17 +1,22 @@ package de.tum.cit.aet.artemis.programming.repository; +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.LOAD; import java.util.Optional; +import org.springframework.context.annotation.Profile; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.build.BuildPlan; +@Profile(PROFILE_CORE) +@Repository public interface BuildPlanRepository extends ArtemisJpaRepository { @Query(""" diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/hestia/CodeHintRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/hestia/CodeHintRepository.java index 7c78408aedb6..9c4a03766832 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/hestia/CodeHintRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/hestia/CodeHintRepository.java @@ -1,12 +1,16 @@ package de.tum.cit.aet.artemis.programming.repository.hestia; +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + import java.util.Optional; import java.util.Set; import jakarta.validation.constraints.NotNull; +import org.springframework.context.annotation.Profile; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; import de.tum.cit.aet.artemis.core.exception.EntityNotFoundException; import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; @@ -15,6 +19,8 @@ /** * Spring Data repository for the CodeHint entity. */ +@Profile(PROFILE_CORE) +@Repository public interface CodeHintRepository extends ArtemisJpaRepository { Set findByExerciseId(Long exerciseId); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/hestia/ExerciseHintActivationRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/hestia/ExerciseHintActivationRepository.java index 5a24463cdc7f..c827a9b3052b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/hestia/ExerciseHintActivationRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/hestia/ExerciseHintActivationRepository.java @@ -1,14 +1,20 @@ package de.tum.cit.aet.artemis.programming.repository.hestia; +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + import java.util.Optional; import java.util.Set; +import org.springframework.context.annotation.Profile; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; import de.tum.cit.aet.artemis.programming.domain.hestia.ExerciseHintActivation; +@Profile(PROFILE_CORE) +@Repository public interface ExerciseHintActivationRepository extends ArtemisJpaRepository { @Query(""" diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/hestia/ProgrammingExerciseSolutionEntryRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/hestia/ProgrammingExerciseSolutionEntryRepository.java index 839a7d67dc49..14a03ed49c0a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/hestia/ProgrammingExerciseSolutionEntryRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/hestia/ProgrammingExerciseSolutionEntryRepository.java @@ -1,12 +1,16 @@ package de.tum.cit.aet.artemis.programming.repository.hestia; +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + import java.util.Optional; import java.util.Set; import jakarta.validation.constraints.NotNull; +import org.springframework.context.annotation.Profile; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; import de.tum.cit.aet.artemis.core.exception.EntityNotFoundException; import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; @@ -15,6 +19,8 @@ /** * Spring Data repository for the ProgrammingExerciseSolutionEntry entity. */ +@Profile(PROFILE_CORE) +@Repository public interface ProgrammingExerciseSolutionEntryRepository extends ArtemisJpaRepository { /** diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/hestia/ProgrammingExerciseTaskRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/hestia/ProgrammingExerciseTaskRepository.java index 432727c61a3e..778c0c811374 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/hestia/ProgrammingExerciseTaskRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/hestia/ProgrammingExerciseTaskRepository.java @@ -1,13 +1,17 @@ package de.tum.cit.aet.artemis.programming.repository.hestia; +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + import java.util.List; import java.util.Optional; import java.util.Set; import jakarta.validation.constraints.NotNull; +import org.springframework.context.annotation.Profile; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; import de.tum.cit.aet.artemis.core.exception.EntityNotFoundException; import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; @@ -16,37 +20,10 @@ /** * Spring Data repository for the ProgrammingExerciseTask entity. */ +@Profile(PROFILE_CORE) +@Repository public interface ProgrammingExerciseTaskRepository extends ArtemisJpaRepository { - Set findByExerciseId(Long exerciseId); - - /** - * Gets a task with its programming exercise, test cases and solution entries of the test cases - * - * @param entryId The id of the task - * @return The task with the given ID if found - * @throws EntityNotFoundException If no task with the given ID was found - */ - @NotNull - default ProgrammingExerciseTask findByIdWithTestCaseAndSolutionEntriesElseThrow(long entryId) throws EntityNotFoundException { - return getValueElseThrow(findByIdWithTestCaseAndSolutionEntries(entryId), entryId); - } - - /** - * Gets a task with its programming exercise, test cases and solution entries of the test cases - * - * @param entryId The id of the task - * @return The task with the given ID - */ - @Query(""" - SELECT t - FROM ProgrammingExerciseTask t - LEFT JOIN FETCH t.testCases tc - LEFT JOIN FETCH tc.solutionEntries - WHERE t.id = :entryId - """) - Optional findByIdWithTestCaseAndSolutionEntries(@Param("entryId") long entryId); - /** * Gets all tasks with its test cases and solution entries of the test case for a programming exercise * diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/AbstractAtlasIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/atlas/AbstractAtlasIntegrationTest.java index 744c27d2f937..f2513d50f2f5 100644 --- a/src/test/java/de/tum/cit/aet/artemis/atlas/AbstractAtlasIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/atlas/AbstractAtlasIntegrationTest.java @@ -14,13 +14,13 @@ import de.tum.cit.aet.artemis.atlas.repository.CompetencyRepository; import de.tum.cit.aet.artemis.atlas.repository.CourseCompetencyRepository; import de.tum.cit.aet.artemis.atlas.repository.KnowledgeAreaRepository; -import de.tum.cit.aet.artemis.atlas.repository.PrerequisiteRepository; import de.tum.cit.aet.artemis.atlas.repository.ScienceSettingRepository; import de.tum.cit.aet.artemis.atlas.repository.SourceRepository; import de.tum.cit.aet.artemis.atlas.repository.StandardizedCompetencyRepository; import de.tum.cit.aet.artemis.atlas.service.competency.CompetencyProgressService; import de.tum.cit.aet.artemis.atlas.test_repository.CompetencyProgressTestRepository; import de.tum.cit.aet.artemis.atlas.test_repository.LearningPathTestRepository; +import de.tum.cit.aet.artemis.atlas.test_repository.PrerequisiteTestRepository; import de.tum.cit.aet.artemis.atlas.test_repository.ScienceEventTestRepository; import de.tum.cit.aet.artemis.core.service.feature.FeatureToggleService; import de.tum.cit.aet.artemis.core.util.PageableSearchUtilService; @@ -74,7 +74,7 @@ public abstract class AbstractAtlasIntegrationTest extends AbstractSpringIntegra protected ScienceEventTestRepository scienceEventRepository; @Autowired - protected PrerequisiteRepository prerequisiteRepository; + protected PrerequisiteTestRepository prerequisiteRepository; @Autowired protected CompetencyJolRepository competencyJolRepository; diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/competency/util/PrerequisiteUtilService.java b/src/test/java/de/tum/cit/aet/artemis/atlas/competency/util/PrerequisiteUtilService.java index edf6e9378286..ac23fe4ef531 100644 --- a/src/test/java/de/tum/cit/aet/artemis/atlas/competency/util/PrerequisiteUtilService.java +++ b/src/test/java/de/tum/cit/aet/artemis/atlas/competency/util/PrerequisiteUtilService.java @@ -7,7 +7,7 @@ import org.springframework.stereotype.Service; import de.tum.cit.aet.artemis.atlas.domain.competency.Prerequisite; -import de.tum.cit.aet.artemis.atlas.repository.PrerequisiteRepository; +import de.tum.cit.aet.artemis.atlas.test_repository.PrerequisiteTestRepository; import de.tum.cit.aet.artemis.core.domain.Course; /** @@ -17,7 +17,7 @@ public class PrerequisiteUtilService { @Autowired - private PrerequisiteRepository prerequisiteRepository; + private PrerequisiteTestRepository prerequisiteRepository; /** * Creates and saves a Prerequisite competency for the given Course. diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/test_repository/PrerequisiteTestRepository.java b/src/test/java/de/tum/cit/aet/artemis/atlas/test_repository/PrerequisiteTestRepository.java new file mode 100644 index 000000000000..a3a8e54cbe3b --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/atlas/test_repository/PrerequisiteTestRepository.java @@ -0,0 +1,19 @@ +package de.tum.cit.aet.artemis.atlas.test_repository; + +import java.util.List; + +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Repository; + +import de.tum.cit.aet.artemis.atlas.domain.competency.Prerequisite; +import de.tum.cit.aet.artemis.atlas.repository.PrerequisiteRepository; + +/** + * Spring Data JPA repository for the {@link Prerequisite} entity. + */ +@Repository +@Primary +public interface PrerequisiteTestRepository extends PrerequisiteRepository { + + List findAllByCourseIdOrderById(long courseId); +} diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationIndependentTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationIndependentTest.java index 46749fbcd3ec..21791e5d24f7 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationIndependentTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationIndependentTest.java @@ -25,7 +25,6 @@ import de.tum.cit.aet.artemis.programming.repository.hestia.ExerciseHintActivationRepository; import de.tum.cit.aet.artemis.programming.repository.hestia.ExerciseHintRepository; import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseSolutionEntryRepository; -import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseTaskRepository; import de.tum.cit.aet.artemis.programming.repository.hestia.TestwiseCoverageReportEntryRepository; import de.tum.cit.aet.artemis.programming.repository.settings.IdeRepository; import de.tum.cit.aet.artemis.programming.repository.settings.UserIdeMappingRepository; @@ -38,6 +37,7 @@ import de.tum.cit.aet.artemis.programming.service.hestia.ExerciseHintService; import de.tum.cit.aet.artemis.programming.service.hestia.ProgrammingExerciseTaskService; import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseStudentParticipationTestRepository; +import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTaskTestRepository; import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestCaseTestRepository; import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingSubmissionTestRepository; @@ -79,7 +79,7 @@ public abstract class AbstractProgrammingIntegrationIndependentTest extends Abst protected ProgrammingExerciseStudentParticipationTestRepository programmingExerciseStudentParticipationRepository; @Autowired - protected ProgrammingExerciseTaskRepository taskRepository; + protected ProgrammingExerciseTaskTestRepository taskRepository; @Autowired protected ProgrammingExerciseTestCaseTestRepository testCaseRepository; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/test_repository/ProgrammingExerciseTaskTestRepository.java b/src/test/java/de/tum/cit/aet/artemis/programming/test_repository/ProgrammingExerciseTaskTestRepository.java new file mode 100644 index 000000000000..7d79da2bdcb6 --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/programming/test_repository/ProgrammingExerciseTaskTestRepository.java @@ -0,0 +1,52 @@ +package de.tum.cit.aet.artemis.programming.test_repository; + +import java.util.Optional; +import java.util.Set; + +import jakarta.validation.constraints.NotNull; + +import org.springframework.context.annotation.Primary; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import de.tum.cit.aet.artemis.core.exception.EntityNotFoundException; +import de.tum.cit.aet.artemis.programming.domain.hestia.ProgrammingExerciseTask; +import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseTaskRepository; + +/** + * Spring Data repository for the ProgrammingExerciseTask entity. + */ +@Repository +@Primary +public interface ProgrammingExerciseTaskTestRepository extends ProgrammingExerciseTaskRepository { + + Set findByExerciseId(Long exerciseId); + + /** + * Gets a task with its programming exercise, test cases and solution entries of the test cases + * + * @param entryId The id of the task + * @return The task with the given ID if found + * @throws EntityNotFoundException If no task with the given ID was found + */ + @NotNull + default ProgrammingExerciseTask findByIdWithTestCaseAndSolutionEntriesElseThrow(long entryId) throws EntityNotFoundException { + return getValueElseThrow(findByIdWithTestCaseAndSolutionEntries(entryId), entryId); + } + + /** + * Gets a task with its programming exercise, test cases and solution entries of the test cases + * + * @param entryId The id of the task + * @return The task with the given ID + */ + @Query(""" + SELECT t + FROM ProgrammingExerciseTask t + LEFT JOIN FETCH t.testCases tc + LEFT JOIN FETCH tc.solutionEntries + WHERE t.id = :entryId + """) + Optional findByIdWithTestCaseAndSolutionEntries(@Param("entryId") long entryId); +} diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseTestService.java b/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseTestService.java index 95ba2804b3b6..b8963f3decae 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseTestService.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseTestService.java @@ -136,7 +136,6 @@ import de.tum.cit.aet.artemis.programming.repository.BuildPlanRepository; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.cit.aet.artemis.programming.repository.StaticCodeAnalysisCategoryRepository; -import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseTaskRepository; import de.tum.cit.aet.artemis.programming.service.AutomaticProgrammingExerciseCleanupService; import de.tum.cit.aet.artemis.programming.service.GitService; import de.tum.cit.aet.artemis.programming.service.JavaTemplateUpgradeService; @@ -148,6 +147,7 @@ import de.tum.cit.aet.artemis.programming.service.vcs.VersionControlRepositoryPermission; import de.tum.cit.aet.artemis.programming.service.vcs.VersionControlService; import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseStudentParticipationTestRepository; +import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTaskTestRepository; import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestCaseTestRepository; import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingSubmissionTestRepository; @@ -228,7 +228,7 @@ public class ProgrammingExerciseTestService { private JavaTemplateUpgradeService javaTemplateUpgradeService; @Autowired - private ProgrammingExerciseTaskRepository programmingExerciseTaskRepository; + private ProgrammingExerciseTaskTestRepository programmingExerciseTaskRepository; @Autowired private ProgrammingExerciseTestCaseTestRepository programmingExerciseTestCaseRepository; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseUtilService.java b/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseUtilService.java index e1571792335a..7aa1d99e1c8f 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseUtilService.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseUtilService.java @@ -74,8 +74,8 @@ import de.tum.cit.aet.artemis.programming.repository.hestia.CodeHintRepository; import de.tum.cit.aet.artemis.programming.repository.hestia.ExerciseHintRepository; import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseSolutionEntryRepository; -import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseTaskRepository; import de.tum.cit.aet.artemis.programming.service.GitService; +import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTaskTestRepository; import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestCaseTestRepository; import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingSubmissionTestRepository; @@ -145,7 +145,7 @@ public class ProgrammingExerciseUtilService { private ExerciseHintRepository exerciseHintRepository; @Autowired - private ProgrammingExerciseTaskRepository programmingExerciseTaskRepository; + private ProgrammingExerciseTaskTestRepository programmingExerciseTaskRepository; @Autowired private ProgrammingExerciseSolutionEntryRepository solutionEntryRepository; diff --git a/src/test/java/de/tum/cit/aet/artemis/shared/architecture/module/AbstractModuleRepositoryArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/shared/architecture/module/AbstractModuleRepositoryArchitectureTest.java index 6e3887654163..fdc272e7877d 100644 --- a/src/test/java/de/tum/cit/aet/artemis/shared/architecture/module/AbstractModuleRepositoryArchitectureTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/shared/architecture/module/AbstractModuleRepositoryArchitectureTest.java @@ -20,6 +20,7 @@ import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.NoRepositoryBean; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import org.springframework.stereotype.Service; @@ -48,6 +49,14 @@ void shouldBeNamedRepository() { rule.allowEmptyShould(true).check(allClasses); } + @Test + void shouldBeAnnotatedRepository() { + ArchRule rule = classesOfThisModuleThat().haveSimpleNameEndingWith("Repository").and().areInterfaces().should().beAnnotatedWith(Repository.class).orShould() + .beAnnotatedWith(NoRepositoryBean.class).because("repositories should be annotated with @Repository or @NoRepositoryBean."); + // allow empty should since some modules do not have repositories + rule.allowEmptyShould(true).check(allClasses); + } + @Test void shouldBeInRepositoryPackage() { ArchRule rule = classesOfThisModuleThat().areAnnotatedWith(Repository.class).should().resideInAPackage("..repository..") From 981fe87f1089225d453cb1fdaf6d3617e623339d Mon Sep 17 00:00:00 2001 From: Asli Aykan <56061820+asliayk@users.noreply.github.com> Date: Sun, 27 Oct 2024 20:58:03 +0100 Subject: [PATCH 5/5] Communication: Fix content overflow in expanded thread view (#9474) --- .../course-conversations.component.html | 5 +---- .../course-conversations.component.scss | 17 ++--------------- .../conversation-thread-sidebar.component.scss | 17 +++++++++-------- .../conversation-thread-sidebar.component.ts | 4 ++-- .../answer-post-reactions-bar.component.html | 2 +- .../post-reactions-bar.component.html | 2 +- ...onversation-thread-sidebar.component.spec.ts | 12 ++++++++++++ 7 files changed, 28 insertions(+), 31 deletions(-) diff --git a/src/main/webapp/app/overview/course-conversations/course-conversations.component.html b/src/main/webapp/app/overview/course-conversations/course-conversations.component.html index 113ab1ca1cd8..1e7b44fa6fb7 100644 --- a/src/main/webapp/app/overview/course-conversations/course-conversations.component.html +++ b/src/main/webapp/app/overview/course-conversations/course-conversations.component.html @@ -62,10 +62,7 @@ } -
+
@if (!!postInThread) { +
@for (reactionMetaData of reactionMetaDataMap | keyvalue; track reactionMetaData) {