Skip to content
Snippets Groups Projects
Commit 14898990 authored by Janis Daniel Dähne's avatar Janis Daniel Dähne
Browse files

- added function to create an exercise test with asset

parent 1e6861db
No related branches found
No related tags found
No related merge requests found
...@@ -462,6 +462,12 @@ export const lang_en: LangObj = { ...@@ -462,6 +462,12 @@ export const lang_en: LangObj = {
"Save project": "Save project", "Save project": "Save project",
"Export exercise": "Export exercise", "Export exercise": "Export exercise",
"Import exercise": "Import exercise", "Import exercise": "Import exercise",
"Export tests": "Export tests",
"Import tests": "Import tests",
"Importing tests ...": "Importing tests ...",
"Imports tests from a json file and saves the tests to the backend": "Imports tests from a json file and saves the tests to the backend",
"At least one test could not be created": "At least one test could not be created",
"... all tests were created and saved": "... all tests were created and saved",
"Lock exercise permanently" : "Lock exercise permanently", "Lock exercise permanently" : "Lock exercise permanently",
"Locked permanently" : "Locked permanently", "Locked permanently" : "Locked permanently",
"Exercise is lock": "Exercise is lock", "Exercise is lock": "Exercise is lock",
......
...@@ -473,6 +473,12 @@ export interface LangObj { ...@@ -473,6 +473,12 @@ export interface LangObj {
"Save project": string "Save project": string
"Export exercise": string "Export exercise": string
"Import exercise": string "Import exercise": string
"Export tests": string
"Import tests": string
"Importing tests ...": string
"Imports tests from a json file and saves the tests to the backend": string
"At least one test could not be created": string
"... all tests were created and saved": string
"Lock exercise permanently": string "Lock exercise permanently": string
"Locked permanently": string "Locked permanently": string
"Exercise is lock": string "Exercise is lock": string
......
...@@ -6,7 +6,7 @@ import * as genericLayer from './genericLayer' ...@@ -6,7 +6,7 @@ import * as genericLayer from './genericLayer'
import { import {
EditorExerciseForBackend, EditorExerciseForBackend,
EditorExerciseFromBackend, EditorExerciseFromBackend,
ExerciseFromBackendWithData, ExerciseFromBackendWithData, ExerciseTestFromBackend, ExerciseTestFromFrontendWithData,
FilePreviewFromBackend, FilePreviewFromBackend,
FileWithData FileWithData
} from "../types/exerciseEditor"; } from "../types/exerciseEditor";
...@@ -165,3 +165,10 @@ export async function detachFileFromExerciseTest(exerciseId: number, testId: num ...@@ -165,3 +165,10 @@ export async function detachFileFromExerciseTest(exerciseId: number, testId: num
`${controllerPrefixExerciseEditor}/upload/detach/tests/${exerciseId}/${testId}/${fileReferenceId}` `${controllerPrefixExerciseEditor}/upload/detach/tests/${exerciseId}/${testId}/${fileReferenceId}`
) )
} }
export async function createExerciseTestWithTestAsset(exerciseId: number, test: ExerciseTestFromFrontendWithData): Promise<ExerciseTestFromBackend> {
return genericLayer.update<ExerciseTestFromFrontendWithData, ExerciseTestFromBackend>(callingName,
`${controllerPrefixExerciseEditor}/create/test/${exerciseId}`, test
)
}
...@@ -15,8 +15,8 @@ import {InputOnChangeEvent} from "../../../types/reactEvents"; ...@@ -15,8 +15,8 @@ import {InputOnChangeEvent} from "../../../types/reactEvents";
import {readFileAsText} from "../../../helpers/fileReader"; import {readFileAsText} from "../../../helpers/fileReader";
import Logger from "../../../helpers/logger"; import Logger from "../../../helpers/logger";
import { import {
EditorExerciseForBackend, EditorExerciseFromBackend, ExerciseFromBackendWithData, EditorExerciseForBackend, EditorExerciseFromBackend, ExerciseFromBackendWithData, ExerciseTestForBackend,
ExerciseTestFromBackend, ExerciseTestFromBackend, ExerciseTestFromBackendWithData, ExerciseTestFromFrontendWithData,
TestAssetFromBackendWithData TestAssetFromBackendWithData
} from "../../../types/exerciseEditor"; } from "../../../types/exerciseEditor";
import {setEditorExercise} from "../../../state/actions/exerciseEditorSite/exerciseEditorSiteActions"; import {setEditorExercise} from "../../../state/actions/exerciseEditorSite/exerciseEditorSiteActions";
...@@ -34,12 +34,18 @@ import {notifyError, notifyInfo, notifySuccess} from "../../../helpers/notificat ...@@ -34,12 +34,18 @@ import {notifyError, notifyInfo, notifySuccess} from "../../../helpers/notificat
import * as mousetrap from "mousetrap"; import * as mousetrap from "mousetrap";
import {getEditorExercise, getEditorExerciseWithData} from "../../../communicationLayer/exerciseEditorLayer"; import {
createExerciseTestWithTestAsset,
getEditorExercise,
getEditorExerciseWithData,
} from "../../../communicationLayer/exerciseEditorLayer";
import {askDialog, errorDialog} from "../../../helpers/dialogHelper"; import {askDialog, errorDialog} from "../../../helpers/dialogHelper";
import {mergeEditorExerciseTestAssetContents} from "../../../helpers/convertersAndTransformers"; import {mergeEditorExerciseTestAssetContents} from "../../../helpers/convertersAndTransformers";
import {getI18n, getRawI18n} from "../../../../i18n/i18nRoot"; import {getI18n, getRawI18n} from "../../../../i18n/i18nRoot";
import {ErrorHelper} from '../../../helpers/errorHelper' import {ErrorHelper} from '../../../helpers/errorHelper'
import {HelpPopup} from '../../helpers/helpPopup' import {HelpPopup} from '../../helpers/helpPopup'
import {setEditorTests} from "../../../state/actions/exerciseEditorSite/subSets/editorExerciseTestsActions";
import {MyPopup} from "../../helpers/myPopup";
declare var require: any declare var require: any
const fileSaver = require('file-saver'); const fileSaver = require('file-saver');
...@@ -89,6 +95,7 @@ const mapDispatchToProps = (dispatch: Dispatch) => bindActionCreators({ ...@@ -89,6 +95,7 @@ const mapDispatchToProps = (dispatch: Dispatch) => bindActionCreators({
createEditorExerciseAsync, createEditorExerciseAsync,
updateEditorExerciseAsync, updateEditorExerciseAsync,
lockEditorExercisePermanentlyAsync, lockEditorExercisePermanentlyAsync,
setEditorTests,
}, dispatch) }, dispatch)
...@@ -98,6 +105,7 @@ type Props = typeof stateProps & typeof dispatchProps; ...@@ -98,6 +105,7 @@ type Props = typeof stateProps & typeof dispatchProps;
const importJsonFileExerciseId = 'exerciseEditor-file-import' const importJsonFileExerciseId = 'exerciseEditor-file-import'
const importJsonFileExerciseTestsId = 'exerciseEditor-tests-file-import'
class HeaderBar extends React.Component<Props, any> { class HeaderBar extends React.Component<Props, any> {
...@@ -152,7 +160,7 @@ class HeaderBar extends React.Component<Props, any> { ...@@ -152,7 +160,7 @@ class HeaderBar extends React.Component<Props, any> {
} }
if (this.props.isNoDialogDisplayed === false) { if (this.props.isNoDialogDisplayed === false) {
return return false
} }
if (this.props.editorExercise.id <= 0) { if (this.props.editorExercise.id <= 0) {
...@@ -166,7 +174,7 @@ class HeaderBar extends React.Component<Props, any> { ...@@ -166,7 +174,7 @@ class HeaderBar extends React.Component<Props, any> {
if (!created) { if (!created) {
notifyError(getI18n(this.props.langId, 'Could not save exercise')) notifyError(getI18n(this.props.langId, 'Could not save exercise'))
return return false
} }
//because we only get the new id back (intentionally) //because we only get the new id back (intentionally)
...@@ -189,12 +197,13 @@ class HeaderBar extends React.Component<Props, any> { ...@@ -189,12 +197,13 @@ class HeaderBar extends React.Component<Props, any> {
if (!success) { if (!success) {
notifyError(getI18n(this.props.langId, 'Could not update exercise')) notifyError(getI18n(this.props.langId, 'Could not update exercise'))
return return false
} }
notifySuccess(getI18n(this.props.langId, 'Updated exercise')) notifySuccess(getI18n(this.props.langId, 'Updated exercise'))
} }
return true
} }
...@@ -272,6 +281,146 @@ class HeaderBar extends React.Component<Props, any> { ...@@ -272,6 +281,146 @@ class HeaderBar extends React.Component<Props, any> {
</div>) </div>)
const rightArea = (<div> const rightArea = (<div>
<div className="clickable mar-right"
onClick={async () => {
//when we create a new exercise (not copying) then the test asset contents are not loaded...
// noinspection TsLint
let exerciseToExport: ExerciseFromBackendWithData | EditorExerciseFromBackend = this.props.editorExercise
if (this.props.editorExercise.id > 0) {
notifyInfo(getI18n(this.props.langId, 'Now loading test assets (before exporting)...'))
try {
//load the full exercise but only take the test asset contents
const exerciseWithTestContents = await getEditorExerciseWithData(this.props.editorExercise.id)
exerciseToExport = mergeEditorExerciseTestAssetContents({
...this.props.editorExercise,
numReleases: 0, // set to 0 because when we import we create a new exercise... and we can only create templates if we have no releases
tests: this.props.editorExercise.tests,
},
exerciseWithTestContents.tests
)
} catch (err) {
ErrorHelper.makeServerErrorDialog(err, this.props.langId, 'Server error', 'Could not load test assets')
return
}
} else {
//when we create a new exercise the test assets are still stored at frontend (in memory)
}
const value = toJSONAndReplaceIds(exerciseToExport.tests)
// noinspection TsLint
let fileName = `${this.props.editorExercise.exerciseProperties.displayName.trim()}_tests.json`
if (!fileName) {
fileName = "exercise_tests.json"
}
const blob = new Blob([value], {type: "application/json;charset=utf-8"});
fileSaver.saveAs(blob, fileName)
}}
>
<Icon name="upload"/>
<span>
{getI18n(this.props.langId, 'Export tests')}
</span>
</div>
<div className={`${this.props.editorExercise.id <= 0 ? 'div-disabled' : 'clickable'}`}
onClick={() => {
const inputEl = document.getElementById(importJsonFileExerciseTestsId)
inputEl.click()}}>
<Icon name="download"/>
<span className="mar-right">
{getI18n(this.props.langId, 'Import tests')}
</span>
<input id={importJsonFileExerciseTestsId} type="file" className="collapsed" accept="application/json"
disabled={this.props.editorExercise.id <= 0}
onChange={async (e: InputOnChangeEvent) => {
const files: FileList = e.currentTarget.files
//only allowed for already saved exercises
//because we need to save here and this would redirect/reload the page --> our data is gone
if (this.props.editorExercise.id <= 0) {
return
}
// noinspection TsLint
let text: string = null
try {
text = await readFileAsText(files[0])
} catch (err) {
Logger.error(`exercise editor: ${err.message}`)
}
if (!text) {
Logger.warn(`exercise editor: something went wrong reading the file`)
return
}
notifyInfo(getI18n(this.props.langId, "Importing tests ..."))
//TODO must this is insecure....
//check if the parsed has every needed props
// noinspection TsLint
let parsed = JSON.parse(text) as ExerciseTestFromBackendWithData[]
let mapped: ReadonlyArray<ExerciseTestFromFrontendWithData> = parsed.map(p => ({
id: p.id,
displayIndex: -1, //index in array matters in setEditorTests not value here
displayName: p.displayName,
isSubmitTest: p.isSubmitTest,
maxPoints: p.maxPoints,
testTypeId: p.testTypeId,
content: p.content,
testSettings: {
...p.testSettings,
},
files: p.files.map(k => ({
id: k.id,
mimeType: k.mimeType,
size: k.size,
displayName: k.displayName,
content: k.content
}))
}))
/* tslint:disable-next-line */
const createdTests: ExerciseTestFromBackend[] = []
for (let i = 0; i < mapped.length; i++) {
let mappedTest = mapped[i]
try {
const result = await createExerciseTestWithTestAsset(this.props.editorExercise.id, mappedTest)
createdTests.push(result)
} catch(err) {
Logger.error(`exercise editor: ${err.message}`)
notifyError(getI18n(this.props.langId, 'At least one test could not be created'))
return
}
}
this.props.setEditorTests([...this.props.editorExercise.tests, ...createdTests])
notifySuccess(getI18n(this.props.langId, "... all tests were created and saved"))
}}
/>
</div>
<HelpPopup defaultText={getI18n(this.props.langId,'Imports tests from a json file and saves the tests to the backend')}/>
<SimpleVDivider/>
<div className="clickable" <div className="clickable"
onClick={async () => { onClick={async () => {
......
...@@ -13,7 +13,7 @@ import Logger from './helpers/logger' ...@@ -13,7 +13,7 @@ import Logger from './helpers/logger'
* y - breaking changes / new features * y - breaking changes / new features
* z - fixes, small changes * z - fixes, small changes
*/ */
export const versionString = '2.19.5' export const versionString = '2.20.0'
export const supportMail = 'yapex@informatik.uni-halle.de' export const supportMail = 'yapex@informatik.uni-halle.de'
......
...@@ -121,4 +121,4 @@ export function getCopyAssetUrlText(asset: FileWithData): string { ...@@ -121,4 +121,4 @@ export function getCopyAssetUrlText(asset: FileWithData): string {
//from https://stackoverflow.com/questions/1144783/how-to-replace-all-occurrences-of-a-string-in-javascript //from https://stackoverflow.com/questions/1144783/how-to-replace-all-occurrences-of-a-string-in-javascript
function replaceAll(value: string, search: string, replacement: string): string { function replaceAll(value: string, search: string, replacement: string): string {
return value.split(search).join(replacement); return value.split(search).join(replacement);
}; }
...@@ -37,7 +37,7 @@ export function toJSONAndReplaceIds(obj: any): string { ...@@ -37,7 +37,7 @@ export function toJSONAndReplaceIds(obj: any): string {
*/ */
function replaceIdsTransformer(key: string, value: any): any { function replaceIdsTransformer(key: string, value: any): any {
if (key === 'id') { if (key === 'id') {
return getNextId().toString() return getNextId() //.toString()
} }
return value return value
......
...@@ -115,6 +115,10 @@ export interface ExerciseFromBackendWithData extends EditorExerciseBase { ...@@ -115,6 +115,10 @@ export interface ExerciseFromBackendWithData extends EditorExerciseBase {
readonly creatorId: number | null readonly creatorId: number | null
} }
export interface ExerciseTestFromFrontendWithData extends ExerciseTestBase {
readonly files: ReadonlyArray<TestAssetFromFrontendWithData>
}
/** /**
* the properties for an exercise (in the backend these are part of the exercise but not in the frontend (have own panel) * the properties for an exercise (in the backend these are part of the exercise but not in the frontend (have own panel)
* --> no id * --> no id
...@@ -334,6 +338,17 @@ export interface ExerciseTestFromBackend extends ExerciseTestBase { ...@@ -334,6 +338,17 @@ export interface ExerciseTestFromBackend extends ExerciseTestBase {
readonly files: ReadonlyArray<FilePreviewFromBackend> readonly files: ReadonlyArray<FilePreviewFromBackend>
} }
//almost same as TestAssetBase but with content
export interface TestAssetFromFrontendWithData {
readonly id: number
readonly displayName: string
readonly mimeType: string
readonly size: number
//bytes
readonly content: ReadonlyArray<number>
}
/** /**
* a test from the frontend with asset data * a test from the frontend with asset data
*/ */
......
...@@ -117,7 +117,7 @@ export interface ExercisePreviewFromBackend { ...@@ -117,7 +117,7 @@ export interface ExercisePreviewFromBackend {
readonly isReleased: boolean readonly isReleased: boolean
/** /**
* will be zero if no test was ran yet * will be zero if no test has been run
*/ */
readonly passedNormalTestsCount: number readonly passedNormalTestsCount: number
readonly maxNormalTestsCount: number readonly maxNormalTestsCount: number
......
...@@ -62,7 +62,7 @@ export interface AssessmentFullBase { ...@@ -62,7 +62,7 @@ export interface AssessmentFullBase {
* the summed max reachable submit test points * the summed max reachable submit test points
* this is even set for dummy assessments * this is even set for dummy assessments
*/ */
readonly maxSubmitTestPoints: number readonly maxSubmitTestPoints: number | null
/** /**
* the summed points from the normal test * the summed points from the normal test
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment