• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MarkUsProject / Markus / 20143075828

11 Dec 2025 06:18PM UTC coverage: 91.513%. Remained the same
20143075828

Pull #7763

github

web-flow
Merge 9f55e660a into 3421ef3b2
Pull Request #7763: Release 2.9.0

914 of 1805 branches covered (50.64%)

Branch coverage included in aggregate %.

1584 of 1666 new or added lines in 108 files covered. (95.08%)

573 existing lines in 35 files now uncovered.

43650 of 46892 relevant lines covered (93.09%)

121.63 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.62
/app/javascript/Components/Result/flexible_criterion_input.jsx
1
import React, {useState, useEffect, useRef} from "react";
2
import PropTypes from "prop-types";
3
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
4

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

7
export default function FlexibleCriterionInput({
8
  active,
9
  annotations,
10
  bonus,
11
  description,
12
  destroyMark,
13
  expanded,
14
  findDeductiveAnnotation,
15
  id,
16
  mark,
17
  max_mark,
18
  name,
19
  oldMark,
20
  override,
21
  released_to_students,
22
  revertToAutomaticDeductions,
23
  setActive,
24
  toggleExpanded,
25
  unassigned,
26
  updateMark,
27
}) {
28
  const [rawText, setRawText] = useState(mark === null ? "" : String(mark));
67✔
29
  const [invalid, setInvalid] = useState(false);
67✔
30
  const typing_timer = useRef(undefined);
67✔
31
  const inputRef = useRef(null);
67✔
32

33
  const listDeductions = () => {
67✔
34
    let label = I18n.t("annotations.list_deductions");
67✔
35
    let deductiveAnnotations = annotations.filter(a => {
67✔
36
      return !!a.deduction && a.criterion_id === id && !a.is_remark;
51✔
37
    });
38

39
    if (deductiveAnnotations.length === 0) {
67✔
40
      return "";
64✔
41
    }
42

43
    let hyperlinkedDeductions = deductiveAnnotations.map((a, index) => {
3✔
44
      let full_path = a.path ? a.path + "/" + a.filename : a.filename;
3!
45
      return (
3✔
46
        <span key={a.id}>
47
          <a
48
            onClick={() =>
49
              findDeductiveAnnotation(full_path, a.submission_file_id, a.line_start, a.id)
1✔
50
            }
51
            href="#"
52
            className={"red-text"}
53
          >
54
            {"-" + a.deduction}
55
          </a>
56
          {index !== deductiveAnnotations.length - 1 ? ", " : ""}
3!
57
        </span>
58
      );
59
    });
60

61
    if (override) {
3✔
62
      label = "(" + I18n.t("results.overridden_deductions") + ") " + label;
2✔
63
    }
64

65
    return (
3✔
66
      <div className={"mark-deductions"}>
67
        {label}
68
        {hyperlinkedDeductions}
69
      </div>
70
    );
71
  };
72

73
  const deleteManualMarkLink = () => {
67✔
74
    if (!released_to_students && !unassigned) {
67✔
75
      if (annotations.some(a => !!a.deduction && a.criterion_id === id) && override) {
64✔
76
        return (
2✔
77
          <a
78
            href="#"
79
            className="flexible-revert"
80
            onClick={_ => revertToAutomaticDeductions(id)}
1✔
81
            style={{float: "right"}}
82
          >
83
            {I18n.t("results.cancel_override")}
84
          </a>
85
        );
86
      } else if (mark !== null && override) {
62✔
87
        return (
1✔
88
          <a href="#" onClick={e => destroyMark(e, id)} style={{float: "right"}}>
1✔
89
            {I18n.t("helpers.submit.delete", {
90
              model: I18n.t("activerecord.models.mark.one"),
91
            })}
92
          </a>
93
        );
94
      }
95
    }
96
    return "";
64✔
97
  };
98

99
  const renderOldMark = () => {
67✔
100
    if (oldMark === undefined || oldMark.mark === undefined) {
67✔
101
      return null;
65✔
102
    }
103
    let label = String(oldMark.mark);
2✔
104

105
    if (oldMark.override) {
2✔
106
      label = `(${I18n.t("results.overridden_deductions")}) ${label}`;
1✔
107
    }
108

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

112
  const handleChange = event => {
67✔
113
    if (typing_timer.current) {
19✔
114
      clearTimeout(typing_timer.current);
13✔
115
    }
116

117
    const inputMark = parseFloat(event.target.value);
19✔
118
    if (event.target.value !== "" && isNaN(inputMark)) {
19✔
119
      setRawText(event.target.value);
11✔
120
      setInvalid(true);
11✔
121
    } else if (inputMark === mark) {
8!
122
      // This can happen if the user types a decimal point at the end of the input.
NEW
123
      setRawText(event.target.value);
×
NEW
124
      setInvalid(false);
×
125
    } else if (inputMark > max_mark) {
8✔
126
      setRawText(event.target.value);
3✔
127
      setInvalid(true);
3✔
128
    } else {
129
      setRawText(event.target.value);
5✔
130
      setInvalid(false);
5✔
131

132
      typing_timer.current = setTimeout(() => {
5✔
133
        updateMark(id, isNaN(inputMark) ? null : inputMark);
2!
134
      }, 300);
135
    }
136
  };
137

138
  useEffect(() => {
67✔
139
    setRawText(mark === null ? "" : String(mark));
27✔
140
    setInvalid(false);
27✔
141
  }, [mark]);
142

143
  const unassignedClass = unassigned ? "unassigned" : "";
67✔
144
  const expandedClass = expanded ? "expanded" : "collapsed";
67✔
145

146
  // Auto-expand if not already when active
147
  useEffect(() => {
67✔
148
    if (active && !expanded) {
35✔
149
      toggleExpanded();
5✔
150
    }
151
  }, [active, expanded]);
152

153
  useEffect(() => {
67✔
154
    if (active && inputRef.current && expanded) {
35✔
155
      // Focus at end of input
156
      inputRef.current.focus();
5✔
157
      inputRef.current.setSelectionRange(
5✔
158
        inputRef.current.value.length,
159
        inputRef.current.value.length
160
      );
161
    }
162
  }, [active, expanded]);
163

164
  let markElement;
165
  if (released_to_students) {
67✔
166
    // Student view
167
    markElement = mark;
1✔
168
  } else {
169
    markElement = (
66✔
170
      <input
171
        ref={inputRef}
172
        className={invalid ? "invalid" : ""}
66✔
173
        type="text"
174
        size={4}
175
        value={rawText}
176
        onChange={handleChange}
177
        disabled={unassigned}
178
      />
179
    );
180
  }
181

182
  return (
67✔
183
    <li
184
      id={`flexible_criterion_${id}`}
185
      className={`flexible_criterion ${expandedClass} ${unassignedClass} ${active ? "active-criterion" : ""}`}
67✔
186
      onClick={setActive}
187
    >
188
      <div data-testid={id}>
189
        <div className="criterion-name" onClick={toggleExpanded}>
190
          <FontAwesomeIcon
191
            className="chevron-expandable"
192
            icon={expanded ? "fa-chevron-up" : "fa-chevron-down"}
67✔
193
          />
194
          {name}
195
          {bonus && ` (${I18n.t("activerecord.attributes.criterion.bonus")})`}
68✔
196
          {deleteManualMarkLink()}
197
        </div>
198
        <div
199
          className="criterion-description"
200
          dangerouslySetInnerHTML={{__html: safe_marked(description)}}
201
        />
202
        <span className="mark">
203
          {markElement}
204
          &nbsp;/&nbsp;
205
          {max_mark}
206
        </span>
207
        {listDeductions()}
208
        {renderOldMark()}
209
      </div>
210
    </li>
211
  );
212
}
213

214
FlexibleCriterionInput.propTypes = {
1✔
215
  annotations: PropTypes.arrayOf(PropTypes.object).isRequired,
216
  bonus: PropTypes.bool,
217
  description: PropTypes.string.isRequired,
218
  destroyMark: PropTypes.func.isRequired,
219
  expanded: PropTypes.bool.isRequired,
220
  findDeductiveAnnotation: PropTypes.func.isRequired,
221
  id: PropTypes.number.isRequired,
222
  mark: PropTypes.number,
223
  max_mark: PropTypes.number.isRequired,
224
  oldMark: PropTypes.object,
225
  override: PropTypes.bool,
226
  released_to_students: PropTypes.bool.isRequired,
227
  revertToAutomaticDeductions: PropTypes.func.isRequired,
228
  toggleExpanded: PropTypes.func.isRequired,
229
  unassigned: PropTypes.bool.isRequired,
230
  updateMark: PropTypes.func.isRequired,
231
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc