Skip to content

Commit f90d4de

Browse files
author
Martin Krulis
committed
Implementing visualization of score config associated with assignment solution.
1 parent 81501cb commit f90d4de

File tree

13 files changed

+317
-8
lines changed

13 files changed

+317
-8
lines changed

src/components/Solutions/EvaluationDetail/EvaluationDetail.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const EvaluationDetail = ({
1818
evaluationStatus,
1919
isDebug,
2020
viewResumbissions = false,
21+
showScoreDetail = null,
2122
}) => (
2223
<Box
2324
title={<FormattedMessage id="app.evaluationDetail.title.details" defaultMessage="Evaluation Details" />}
@@ -65,6 +66,12 @@ const EvaluationDetail = ({
6566
<b>
6667
<FormattedNumber style="percent" value={evaluation.score} />
6768
</b>
69+
{showScoreDetail && (
70+
<span className="pull-right clickable text-primary" onClick={showScoreDetail}>
71+
explain
72+
<Icon icon="calculator" gapLeft gapRight />
73+
</span>
74+
)}
6875
</td>
6976
</tr>
7077

@@ -158,6 +165,7 @@ EvaluationDetail.propTypes = {
158165
evaluationStatus: PropTypes.string.isRequired,
159166
isDebug: PropTypes.bool.isRequired,
160167
viewResumbissions: PropTypes.bool,
168+
showScoreDetail: PropTypes.func,
161169
};
162170

163171
export default EvaluationDetail;

src/components/Solutions/SolutionDetail/SolutionDetail.js

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import DownloadSolutionArchiveContainer from '../../../containers/DownloadSoluti
1313
import CommentThreadContainer from '../../../containers/CommentThreadContainer';
1414
import SourceCodeViewerContainer from '../../../containers/SourceCodeViewerContainer';
1515
import SubmissionEvaluations from '../SubmissionEvaluations';
16+
import { ScoreConfigInfoDialog } from '../../scoreConfig/ScoreConfigInfo';
1617
import ResourceRenderer from '../../helpers/ResourceRenderer';
1718

1819
import EvaluationDetail from '../EvaluationDetail';
@@ -24,12 +25,28 @@ import DateTime from '../../widgets/DateTime';
2425
import { safeGet, EMPTY_OBJ } from '../../../helpers/common';
2526

2627
class SolutionDetail extends Component {
27-
state = { openFileId: null, activeSubmissionId: null };
28+
state = { openFileId: null, activeSubmissionId: null, scoreDialogOpened: false };
29+
30+
setActiveSubmission = id => {
31+
this.props.fetchScoreConfigIfNeeded && this.props.fetchScoreConfigIfNeeded(id);
32+
this.setState({ activeSubmissionId: id });
33+
};
2834

2935
openFile = id => this.setState({ openFileId: id });
3036

3137
hideFile = () => this.setState({ openFileId: null });
3238

39+
openScoreDialog = () => {
40+
const activeSubmissionId =
41+
this.state.activeSubmissionId || safeGet(this.props.solution.lastSubmission, ['id'], null);
42+
if (activeSubmissionId) {
43+
this.props.fetchScoreConfigIfNeeded && this.props.fetchScoreConfigIfNeeded(activeSubmissionId);
44+
this.setState({ scoreDialogOpened: true });
45+
}
46+
};
47+
48+
closeScoreDialog = () => this.setState({ scoreDialogOpened: false });
49+
3350
render() {
3451
const {
3552
solution: {
@@ -53,6 +70,8 @@ class SolutionDetail extends Component {
5370
editNote = null,
5471
deleteEvaluation = null,
5572
refreshSolutionEvaluations = null,
73+
scoreConfigSelector = null,
74+
canResubmit = false,
5675
} = this.props;
5776

5877
const { openFileId } = this.state;
@@ -212,6 +231,7 @@ class SolutionDetail extends Component {
212231
evaluationStatus={evaluationStatus}
213232
isDebug={isDebug}
214233
viewResumbissions={permissionHints.viewResubmissions}
234+
showScoreDetail={this.openScoreDialog}
215235
/>
216236
)}
217237

@@ -242,7 +262,7 @@ class SolutionDetail extends Component {
242262
submissionId={id}
243263
evaluations={evaluations}
244264
activeSubmissionId={activeSubmissionId}
245-
onSelect={id => this.setState({ activeSubmissionId: id })}
265+
onSelect={this.setActiveSubmission}
246266
onDelete={permissionHints.deleteEvaluation ? deleteEvaluation : null}
247267
confirmDeleteLastSubmit
248268
/>
@@ -256,6 +276,15 @@ class SolutionDetail extends Component {
256276
</Row>
257277

258278
<SourceCodeViewerContainer show={openFileId !== null} fileId={openFileId} onHide={() => this.hideFile()} />
279+
280+
{activeSubmissionId && scoreConfigSelector && (
281+
<ScoreConfigInfoDialog
282+
show={this.state.scoreDialogOpened}
283+
onHide={this.closeScoreDialog}
284+
scoreConfig={scoreConfigSelector(activeSubmissionId)}
285+
canResubmit={canResubmit}
286+
/>
287+
)}
259288
</div>
260289
);
261290
}
@@ -287,6 +316,9 @@ SolutionDetail.propTypes = {
287316
editNote: PropTypes.func,
288317
deleteEvaluation: PropTypes.func,
289318
refreshSolutionEvaluations: PropTypes.func,
319+
scoreConfigSelector: PropTypes.func,
320+
fetchScoreConfigIfNeeded: PropTypes.func,
321+
canResubmit: PropTypes.bool,
290322
};
291323

292324
export default SolutionDetail;
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import { FormattedMessage } from 'react-intl';
4+
5+
import ScoreConfigInfoDefault from './ScoreConfigInfoDefault';
6+
import DateTime from '../../widgets/DateTime';
7+
import ScoreConfigInfoUniform from './ScoreConfigInfoUniform';
8+
import ScoreConfigInfoWeighted from './ScoreConfigInfoWeighted';
9+
10+
const knownCalculators = {
11+
uniform: ScoreConfigInfoUniform,
12+
weighted: ScoreConfigInfoWeighted,
13+
};
14+
15+
const ScoreConfigInfo = ({ scoreConfig, canResubmit = false }) => {
16+
const ScoreConfigCalculatorPresenter =
17+
(scoreConfig && scoreConfig.calculator && knownCalculators[scoreConfig.calculator]) || ScoreConfigInfoDefault;
18+
19+
return (
20+
<React.Fragment>
21+
{scoreConfig ? (
22+
<React.Fragment>
23+
<ScoreConfigCalculatorPresenter scoreConfig={scoreConfig} />
24+
<p className="small text-right text-nowrap text-muted em-padding-right">
25+
<FormattedMessage id="app.scoreConfigInfo.createdAt" defaultMessage="Configured at" />
26+
:&nbsp;
27+
<DateTime unixts={scoreConfig.createdAt} showRelative />
28+
</p>
29+
</React.Fragment>
30+
) : (
31+
<div className="callout callout-info">
32+
<h4>
33+
<FormattedMessage
34+
id="app.scoreConfigInfo.missingTitle"
35+
defaultMessage="The algorithm specification is missing"
36+
/>
37+
</h4>
38+
<p>
39+
<FormattedMessage
40+
id="app.scoreConfigInfo.missing"
41+
defaultMessage="The submission was evaluated before this feature was implemented. The overall correctness must have been computed as (possibly weighted) average of individual tests."
42+
/>
43+
</p>
44+
{canResubmit && (
45+
<p>
46+
<FormattedMessage
47+
id="app.scoreConfigInfo.missingButCanResubmit"
48+
defaultMessage="You may resubmit the solution and then the current correctness algorithm from the configuration of the assignment will be attached to the new submission (and incidently visible in this dialog)."
49+
/>
50+
</p>
51+
)}
52+
</div>
53+
)}
54+
</React.Fragment>
55+
);
56+
};
57+
58+
ScoreConfigInfo.propTypes = {
59+
scoreConfig: PropTypes.object,
60+
canResubmit: PropTypes.bool,
61+
};
62+
63+
export default ScoreConfigInfo;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import { Table } from 'react-bootstrap';
4+
import { FormattedMessage } from 'react-intl';
5+
6+
const ScoreConfigInfoDefault = ({ scoreConfig }) => (
7+
<Table>
8+
<tbody>
9+
<tr>
10+
<th className="text-nowrap shrink-col">
11+
<FormattedMessage id="app.scoreConfigInfo.calculator" defaultMessage="Algorithm" />:
12+
</th>
13+
<td>
14+
<code>{scoreConfig.calculator}</code>
15+
</td>
16+
</tr>
17+
<tr>
18+
<th className="text-nowrap shrink-col">
19+
<FormattedMessage id="app.scoreConfigInfo.rawConfig" defaultMessage="Raw configuration" />:
20+
</th>
21+
<td>
22+
<pre>
23+
<code>{JSON.stringify(scoreConfig.config, null, 2)}</code>
24+
</pre>
25+
</td>
26+
</tr>
27+
</tbody>
28+
</Table>
29+
);
30+
31+
ScoreConfigInfoDefault.propTypes = {
32+
scoreConfig: PropTypes.object,
33+
};
34+
35+
export default ScoreConfigInfoDefault;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import ImmutablePropTypes from 'react-immutable-proptypes';
4+
import { Modal } from 'react-bootstrap';
5+
import { FormattedMessage } from 'react-intl';
6+
7+
import ScoreConfigInfo from './ScoreConfigInfo';
8+
import ResourceRenderer from '../../helpers/ResourceRenderer';
9+
10+
const ScoreConfigInfoDialog = ({ show, onHide, scoreConfig, canResubmit = false }) => (
11+
<Modal show={show} backdrop="static" onHide={onHide} bsSize="large">
12+
<Modal.Header closeButton>
13+
<Modal.Title>
14+
<FormattedMessage id="app.scoreConfigInfo.dialogTitle" defaultMessage="Correctness Algorithm" />
15+
</Modal.Title>
16+
</Modal.Header>
17+
<Modal.Body>
18+
<ResourceRenderer resource={scoreConfig}>
19+
{scoreConfigJS => <ScoreConfigInfo scoreConfig={scoreConfigJS} canResubmit={canResubmit} />}
20+
</ResourceRenderer>
21+
</Modal.Body>
22+
</Modal>
23+
);
24+
25+
ScoreConfigInfoDialog.propTypes = {
26+
show: PropTypes.bool.isRequired,
27+
onHide: PropTypes.func.isRequired,
28+
scoreConfig: ImmutablePropTypes.map,
29+
canResubmit: PropTypes.bool,
30+
};
31+
32+
export default ScoreConfigInfoDialog;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import { FormattedMessage } from 'react-intl';
4+
5+
const ScoreConfigInfoUniform = ({ scoreConfig }) => (
6+
<div>
7+
<h4>
8+
<FormattedMessage id="app.scoreConfigInfoUniform.title" defaultMessage="Arithmetic average" />
9+
</h4>
10+
<p>
11+
<FormattedMessage
12+
id="app.scoreConfigInfoUniform.description"
13+
defaultMessage="The overall correctness is computed as a simple arithmetic average of the individual test results."
14+
/>
15+
</p>
16+
</div>
17+
);
18+
19+
ScoreConfigInfoUniform.propTypes = {
20+
scoreConfig: PropTypes.object,
21+
};
22+
23+
export default ScoreConfigInfoUniform;
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import { Table, Well } from 'react-bootstrap';
4+
import { FormattedMessage } from 'react-intl';
5+
6+
import { safeGet, EMPTY_OBJ } from '../../../helpers/common';
7+
8+
const ScoreConfigInfoWeighted = ({ scoreConfig }) => {
9+
const weightsObj = safeGet(scoreConfig, ['config', 'testWeights'], EMPTY_OBJ);
10+
const weights = Object.keys(weightsObj)
11+
.sort()
12+
.map(test => ({ test, weight: weightsObj[test] }));
13+
14+
return (
15+
<div>
16+
<h4>
17+
<FormattedMessage id="app.scoreConfigInfoWeighted.title" defaultMessage="Weighted arithmetic mean" />
18+
</h4>
19+
<p>
20+
<FormattedMessage
21+
id="app.scoreConfigInfoWeighted.description"
22+
defaultMessage="The overall correctness is computed as weighted average of the individual test results. The weights for individual tests are displayed below."
23+
/>
24+
</p>
25+
26+
{weights.length > 0 ? (
27+
<Table bordered striped hover>
28+
<thead>
29+
<tr>
30+
<th>
31+
<FormattedMessage id="app.scoreConfigInfoWeighted.test" defaultMessage="Test" />
32+
</th>
33+
<th>
34+
<FormattedMessage id="app.scoreConfigInfoWeighted.weight" defaultMessage="Weight" />
35+
</th>
36+
</tr>
37+
</thead>
38+
<tbody>
39+
{weights.map(({ test, weight }) => (
40+
<tr key={test}>
41+
<td className="text-nowrap">{test}</td>
42+
<td>{weight}</td>
43+
</tr>
44+
))}
45+
</tbody>
46+
</Table>
47+
) : (
48+
<Well>
49+
<FormattedMessage
50+
id="app.scoreConfigInfoWeighted.noTests"
51+
defaultMessage="There are no test weights specified in the configuration."
52+
/>
53+
</Well>
54+
)}
55+
</div>
56+
);
57+
};
58+
59+
ScoreConfigInfoWeighted.propTypes = {
60+
scoreConfig: PropTypes.object,
61+
};
62+
63+
export default ScoreConfigInfoWeighted;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import ScoreConfigInfo from './ScoreConfigInfo';
2+
import ScoreConfigInfoDialog from './ScoreConfigInfoDialog';
3+
export { ScoreConfigInfo, ScoreConfigInfoDialog };

src/locales/cs.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@
9999
"app.assignment.syncLocalizedTexts": "Lokalizované texty",
100100
"app.assignment.syncRequired": "Data úlohy jsou novější než data zadání. Úloha byla aktualizována {exerciseUpdated}, zatímco zadání bylo naposledy aktualizováno {assignmentUpdated}!",
101101
"app.assignment.syncRuntimeEnvironments": "Výběr běhového prostředí",
102-
"app.assignment.syncScoreCalculator": "Kalkulátor skóre",
103102
"app.assignment.syncScoreConfig": "Konfigurace skóre",
104103
"app.assignment.syncSupplementaryFiles": "Dodatečné soubory",
105104
"app.assignment.title": "Zadání úlohy",
@@ -1120,6 +1119,20 @@
11201119
"app.roles.supervisorStudents": "Vedoucí-studenti",
11211120
"app.roles.supervisors": "Vedoucí",
11221121
"app.roles.supervisorsEmpowered": "Zplnomocnění vedoucí",
1122+
"app.scoreConfigInfo.calculator": "Algoritmus",
1123+
"app.scoreConfigInfo.createdAt": "Konfigurace uložena",
1124+
"app.scoreConfigInfo.dialogTitle": "Výpočet správnosti",
1125+
"app.scoreConfigInfo.missing": "Vyhodnocení proběhlo před tím, než byla tato funkce implementována. Celková správnost řešení byla vypočtena jako průměr (případně vážený průměr) výsledků jednotlivých testů.",
1126+
"app.scoreConfigInfo.missingButCanResubmit": "Můžete nechat řešení znovu vyhodnotit s použitím aktuálního algoritmu pro výpočet správnosti. Použitý algoritmus pak bude asociován s vyhodnocením (díky čemuž pak bude zobrazen i v tomto dialogu).",
1127+
"app.scoreConfigInfo.missingTitle": "Specifikace algoritmu není dostupná",
1128+
"app.scoreConfigInfo.rawConfig": "Data konfigurace",
1129+
"app.scoreConfigInfoUniform.description": "Celková správnost je vypočtena jako aritmetický průměr výsledků jednotlivých testů.",
1130+
"app.scoreConfigInfoUniform.title": "Aritmetický průměr",
1131+
"app.scoreConfigInfoWeighted.description": "Celková správnost je vypočtena jako vážený průměr výsledků jednotlivých testů. Váhy jednotlivých testů jsou zobrazeny níže.",
1132+
"app.scoreConfigInfoWeighted.noTests": "V konfiguraci nejsou nastaveny žádné váhy testů.",
1133+
"app.scoreConfigInfoWeighted.test": "Test",
1134+
"app.scoreConfigInfoWeighted.title": "Vážený průměr",
1135+
"app.scoreConfigInfoWeighted.weight": "Váha",
11231136
"app.shadowAssignment.editSettings": "Upravit nastavení stínové úlohy",
11241137
"app.shadowAssignment.isBonus": "Bonusová úloha",
11251138
"app.shadowAssignment.isPublic": "Viditelná studentům",

0 commit comments

Comments
 (0)