Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
- Fixed Rack deprecation warnings by updating HTTP status code symbols (#7675)
- Refactored "Reuse Groups" functionality to use React modal and relocated button to action box row (#7688)
- Updated pre-commit `rubocop-rails` version to 2.33.4 (#7691)
- Refactored MarksPanel child components and converted the components into hook-based function components

## [v2.8.2]

Expand Down
97 changes: 97 additions & 0 deletions app/javascript/Components/Result/checkbox_criterion_input.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React from "react";
import PropTypes from "prop-types";

import safe_marked from "../../common/safe_marked";

export default function CheckboxCriterionInput({
bonus,
description,
destroyMark,
expanded,
id,
mark,
max_mark,
name,
oldMark,
released_to_students,
toggleExpanded,
unassigned,
updateMark,
}) {
const unassignedClass = unassigned ? "unassigned" : "";
const expandedClass = expanded ? "expanded" : "collapsed";

return (
<li
id={`checkbox_criterion_${id}`}
className={`checkbox_criterion ${expandedClass} ${unassignedClass}`}
>
<div>
<div className="criterion-name" onClick={toggleExpanded}>
<div className={expanded ? "arrow-up" : "arrow-down"} style={{float: "left"}} />
{name}
{bonus && ` (${I18n.t("activerecord.attributes.criterion.bonus")})`}
{!released_to_students && !unassigned && mark !== null && (
<a href="#" onClick={e => destroyMark(e, id)} style={{float: "right"}}>
{I18n.t("helpers.submit.delete", {
model: I18n.t("activerecord.models.mark.one"),
})}
</a>
)}
</div>
<div>
{!released_to_students && (
<span className="checkbox-criterion-inputs">
<label onClick={() => updateMark(id, max_mark)} className={`check_correct_${id}`}>
<input
type="radio"
readOnly={true}
checked={mark === max_mark}
disabled={released_to_students || unassigned}
/>
{I18n.t("checkbox_criteria.answer_yes")}
</label>
<label onClick={() => updateMark(id, 0)} className={`check_no_${id}`}>
<input
type="radio"
readOnly={true}
checked={mark === 0}
disabled={released_to_students || unassigned}
/>
{I18n.t("checkbox_criteria.answer_no")}
</label>
</span>
)}
<span className="mark">
{mark === null ? "-" : mark}
&nbsp;/&nbsp;
{max_mark}
</span>
</div>
{oldMark !== undefined && oldMark.mark !== undefined && (
<div className="old-mark">{`(${I18n.t("results.remark.old_mark")}: ${
oldMark.mark
})`}</div>
)}
<div
className="criterion-description"
dangerouslySetInnerHTML={{__html: safe_marked(description)}}
/>
</div>
</li>
);
}

CheckboxCriterionInput.propTypes = {
description: PropTypes.string.isRequired,
destroyMark: PropTypes.func.isRequired,
expanded: PropTypes.bool.isRequired,
id: PropTypes.number.isRequired,
mark: PropTypes.number,
max_mark: PropTypes.number.isRequired,
oldMark: PropTypes.object,
released_to_students: PropTypes.bool.isRequired,
toggleExpanded: PropTypes.func.isRequired,
unassigned: PropTypes.bool.isRequired,
updateMark: PropTypes.func.isRequired,
};
204 changes: 204 additions & 0 deletions app/javascript/Components/Result/flexible_criterion_input.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import React, {useState, useEffect, useRef} from "react";
import PropTypes from "prop-types";

import safe_marked from "../../common/safe_marked";

export default function FlexibleCriterionInput({
annotations,
bonus,
description,
destroyMark,
expanded,
findDeductiveAnnotation,
id,
mark,
max_mark,
name,
oldMark,
override,
released_to_students,
revertToAutomaticDeductions,
toggleExpanded,
unassigned,
updateMark,
}) {
const [rawText, setRawText] = useState(mark === null ? "" : String(mark));
const [invalid, setInvalid] = useState(false);
const typing_timer = useRef(undefined);

const listDeductions = () => {
let label = I18n.t("annotations.list_deductions");
let deductiveAnnotations = annotations.filter(a => {
return !!a.deduction && a.criterion_id === id && !a.is_remark;
});

if (deductiveAnnotations.length === 0) {
return "";
}

let hyperlinkedDeductions = deductiveAnnotations.map((a, index) => {
let full_path = a.path ? a.path + "/" + a.filename : a.filename;
return (
<span key={a.id}>
<a
onClick={() =>
findDeductiveAnnotation(full_path, a.submission_file_id, a.line_start, a.id)
}
href="#"
className={"red-text"}
>
{"-" + a.deduction}
</a>
{index !== deductiveAnnotations.length - 1 ? ", " : ""}
</span>
);
});

if (override) {
label = "(" + I18n.t("results.overridden_deductions") + ") " + label;
}

return (
<div className={"mark-deductions"}>
{label}
{hyperlinkedDeductions}
</div>
);
};

const deleteManualMarkLink = () => {
if (!released_to_students && !unassigned) {
if (annotations.some(a => !!a.deduction && a.criterion_id === id) && override) {
return (
<a
href="#"
className="flexible-revert"
onClick={_ => revertToAutomaticDeductions(id)}
style={{float: "right"}}
>
{I18n.t("results.cancel_override")}
</a>
);
} else if (mark !== null && override) {
return (
<a href="#" onClick={e => destroyMark(e, id)} style={{float: "right"}}>
{I18n.t("helpers.submit.delete", {
model: I18n.t("activerecord.models.mark.one"),
})}
</a>
);
}
}
return "";
};

const renderOldMark = () => {
if (oldMark === undefined || oldMark.mark === undefined) {
return null;
}
let label = String(oldMark.mark);

if (oldMark.override) {
label = `(${I18n.t("results.overridden_deductions")}) ${label}`;
}

return <div className="old-mark">{`(${I18n.t("results.remark.old_mark")}: ${label})`}</div>;
};

const handleChange = event => {
if (typing_timer.current) {
clearTimeout(typing_timer.current);
}

const inputMark = parseFloat(event.target.value);
if (event.target.value !== "" && isNaN(inputMark)) {
setRawText(event.target.value);
setInvalid(true);
} else if (inputMark === mark) {
// This can happen if the user types a decimal point at the end of the input.
setRawText(event.target.value);
setInvalid(false);
} else if (inputMark > max_mark) {
setRawText(event.target.value);
setInvalid(true);
} else {
setRawText(event.target.value);
setInvalid(false);

typing_timer.current = setTimeout(() => {
updateMark(id, isNaN(inputMark) ? null : inputMark);
}, 300);
}
};

useEffect(() => {
setRawText(mark === null ? "" : String(mark));
setInvalid(false);
}, [mark]);

const unassignedClass = unassigned ? "unassigned" : "";
const expandedClass = expanded ? "expanded" : "collapsed";

let markElement;
if (released_to_students) {
// Student view
markElement = mark;
} else {
markElement = (
<input
className={invalid ? "invalid" : ""}
type="text"
size={4}
value={rawText}
onChange={handleChange}
disabled={unassigned}
/>
);
}

return (
<li
id={`flexible_criterion_${id}`}
className={`flexible_criterion ${expandedClass} ${unassignedClass}`}
>
<div data-testid={id}>
<div className="criterion-name" onClick={toggleExpanded}>
<div className={expanded ? "arrow-up" : "arrow-down"} style={{float: "left"}} />
{name}
{bonus && ` (${I18n.t("activerecord.attributes.criterion.bonus")})`}
{deleteManualMarkLink()}
</div>
<div
className="criterion-description"
dangerouslySetInnerHTML={{__html: safe_marked(description)}}
/>
<span className="mark">
{markElement}
&nbsp;/&nbsp;
{max_mark}
</span>
{listDeductions()}
{renderOldMark()}
</div>
</li>
);
}

FlexibleCriterionInput.propTypes = {
annotations: PropTypes.arrayOf(PropTypes.object).isRequired,
bonus: PropTypes.bool,
description: PropTypes.string.isRequired,
destroyMark: PropTypes.func.isRequired,
expanded: PropTypes.bool.isRequired,
findDeductiveAnnotation: PropTypes.func.isRequired,
id: PropTypes.number.isRequired,
mark: PropTypes.number,
max_mark: PropTypes.number.isRequired,
oldMark: PropTypes.object,
override: PropTypes.bool,
released_to_students: PropTypes.bool.isRequired,
revertToAutomaticDeductions: PropTypes.func.isRequired,
toggleExpanded: PropTypes.func.isRequired,
unassigned: PropTypes.bool.isRequired,
updateMark: PropTypes.func.isRequired,
};
Loading