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

MarkUsProject / Markus / 17109290822

20 Aug 2025 08:16PM UTC coverage: 91.797% (-0.09%) from 91.884%
17109290822

Pull #7602

github

web-flow
Merge a552a9ddb into 899c1431b
Pull Request #7602: Added loading icon for instructor table

699 of 1486 branches covered (47.04%)

Branch coverage included in aggregate %.

10 of 14 new or added lines in 5 files covered. (71.43%)

107 existing lines in 7 files now uncovered.

42140 of 45181 relevant lines covered (93.27%)

118.79 hits per line

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

59.17
/app/javascript/Components/assignment_summary_table.jsx
1
import React from "react";
2
import {getMarkingStates, selectFilter} from "./Helpers/table_helpers";
3

4
import DownloadTestResultsModal from "./Modals/download_test_results_modal";
5
import LtiGradeModal from "./Modals/send_lti_grades_modal";
6
import {createColumnHelper} from "@tanstack/react-table";
7
import Table from "./table/table";
8

9
export class AssignmentSummaryTable extends React.Component {
10
  constructor(props) {
11
    super(props);
9✔
12
    const markingStates = getMarkingStates([]);
8✔
13
    this.state = {
8✔
14
      data: [],
15
      criteriaColumns: [],
16
      loading: true,
17
      num_assigned: 0,
18
      num_marked: 0,
19
      enable_test: false,
20
      marking_states: markingStates,
21
      markingStateFilter: "all",
22
      showDownloadTestsModal: false,
23
      showLtiGradeModal: false,
24
      lti_deployments: [],
25
      columnFilters: [{id: "inactive", value: false}],
26
      inactiveGroupsCount: 0,
27
    };
28
  }
29

30
  componentDidMount() {
31
    this.fetchData();
8✔
32
  }
33

34
  getColumns = () => {
8✔
35
    const columnHelper = createColumnHelper();
19✔
36

37
    const fixedColumns = [
19✔
38
      columnHelper.accessor("inactive", {
39
        id: "inactive",
40
      }),
41
      columnHelper.accessor("group_name", {
42
        id: "group_name",
43
        header: () => I18n.t("activerecord.models.group.one"),
38✔
44
        size: 100,
45
        enableResizing: true,
46
        cell: props => {
47
          if (props.row.original.result_id) {
22!
48
            const path = Routes.edit_course_result_path(
22✔
49
              this.props.course_id,
50
              props.row.original.result_id
51
            );
52
            return (
22✔
53
              <a href={path}>
54
                {props.getValue()}
55
                {this.memberDisplay(props.getValue(), props.row.original.members)}
56
              </a>
57
            );
58
          } else {
UNCOV
59
            return (
×
60
              <span>
61
                {props.row.original.group_name}
62
                {this.memberDisplay(props.row.original.group_name, props.row.original.members)}
63
              </span>
64
            );
65
          }
66
        },
67
        filterFn: (row, columnId, filterValue) => {
UNCOV
68
          if (filterValue) {
×
UNCOV
69
            filterValue = filterValue.toLowerCase();
×
70
            // Check group name
UNCOV
71
            if (row.original.group_name.toLowerCase().includes(filterValue)) {
×
UNCOV
72
              return true;
×
73
            }
74

75
            // Check member names (first three values of each "member" array)
UNCOV
76
            const member_matches = row.original.members.some(member =>
×
UNCOV
77
              member.slice(0, 3).some(name => name.toLowerCase().includes(filterValue))
×
78
            );
79

UNCOV
80
            if (member_matches) {
×
UNCOV
81
              return true;
×
82
            }
83

84
            // Check grader user names
UNCOV
85
            return row.original.graders.some(grader => grader.toLowerCase().includes(filterValue));
×
86
          } else {
UNCOV
87
            return true;
×
88
          }
89
        },
90
      }),
91
      columnHelper.accessor("marking_state", {
92
        header: () => I18n.t("activerecord.attributes.result.marking_state"),
38✔
93
        accessorKey: "marking_state",
94
        size: 100,
95
        enableResizing: true,
96
        cell: props => props.getValue(),
22✔
97
        filterFn: "equalsString",
98
        meta: {
99
          filterVariant: "select",
100
        },
101
        filterAllOptionText:
102
          I18n.t("all") +
103
          (this.state.markingStateFilter === "all"
19!
104
            ? ` (${Object.values(this.state.marking_states).reduce((a, b) => a + b)})`
104✔
105
            : ""),
106
        filterOptions: [
107
          {
108
            value: "before_due_date",
109
            text:
110
              I18n.t("submissions.state.before_due_date") +
111
              (["before_due_date", "all"].includes(this.state.markingStateFilter)
19!
112
                ? ` (${this.state.marking_states["before_due_date"]})`
113
                : ""),
114
          },
115
          {
116
            value: "not_collected",
117
            text:
118
              I18n.t("submissions.state.not_collected") +
119
              (["not_collected", "all"].includes(this.state.markingStateFilter)
19!
120
                ? ` (${this.state.marking_states["not_collected"]})`
121
                : ""),
122
          },
123
          {
124
            value: "incomplete",
125
            text:
126
              I18n.t("submissions.state.in_progress") +
127
              (["incomplete", "all"].includes(this.state.markingStateFilter)
19!
128
                ? ` (${this.state.marking_states["incomplete"]})`
129
                : ""),
130
          },
131
          {
132
            value: "complete",
133
            text:
134
              I18n.t("submissions.state.complete") +
135
              (["complete", "all"].includes(this.state.markingStateFilter)
19!
136
                ? ` (${this.state.marking_states["complete"]})`
137
                : ""),
138
          },
139
          {
140
            value: "released",
141
            text:
142
              I18n.t("submissions.state.released") +
143
              (["released", "all"].includes(this.state.markingStateFilter)
19!
144
                ? ` (${this.state.marking_states["released"]})`
145
                : ""),
146
          },
147
          {
148
            value: "remark",
149
            text:
150
              I18n.t("submissions.state.remark_requested") +
151
              (["remark", "all"].includes(this.state.markingStateFilter)
19!
152
                ? ` (${this.state.marking_states["remark"]})`
153
                : ""),
154
          },
155
        ],
156
        Filter: selectFilter,
157
      }),
158
      columnHelper.accessor("tags", {
159
        header: () => I18n.t("activerecord.models.tag.other"),
38✔
160
        size: 90,
161
        enableResizing: true,
162
        cell: props => (
163
          <ul className="tag-list">
22✔
164
            {props.row.original.tags.map(tag => (
165
              <li key={`${props.row.original._id}-${tag}`} className="tag-element">
×
166
                {tag}
167
              </li>
168
            ))}
169
          </ul>
170
        ),
171
        minWidth: 80,
172
        enableSorting: false,
173
        filterFn: (row, columnId, filterValue) => {
UNCOV
174
          if (filterValue) {
×
175
            // Check tag names
176
            return row.original.tags.some(tag => tag.includes(filterValue));
×
177
          } else {
UNCOV
178
            return true;
×
179
          }
180
        },
181
      }),
182
      columnHelper.accessor("final_grade", {
183
        header: () => I18n.t("results.total_mark"),
38✔
184
        size: 100,
185
        enableResizing: true,
186
        cell: props => {
187
          if (props.row.original.final_grade || props.row.original.final_grade === 0) {
22!
188
            const max_mark = Math.round(props.row.original.max_mark * 100) / 100;
22✔
189
            return props.row.original.final_grade + " / " + max_mark;
22✔
190
          } else {
UNCOV
191
            return "";
×
192
          }
193
        },
194
        meta: {className: "number"},
195
        enableColumnFilter: false,
196
        sortDescFirst: true,
197
      }),
198
    ];
199

200
    const criteriaColumns = this.state.criteriaColumns.map(col =>
19✔
201
      columnHelper.accessor(col.accessor, {
9✔
202
        id: col.id,
203
        header: () => col.Header,
19✔
204
        size: col.size || 100,
18✔
205
        meta: {
206
          className: col.className,
207
          headerClassName: col.headerClassName,
208
        },
209
        enableColumnFilter: col.enableColumnFilter,
210
        sortDescFirst: col.sortDescFirst,
211
      })
212
    );
213

214
    const bonusColumn = columnHelper.accessor("total_extra_marks", {
19✔
215
      header: () => I18n.t("activerecord.models.extra_mark.other"),
38✔
216
      size: 100,
217
      enableResizing: true,
218
      meta: {className: "number"},
219
      enableColumnFilter: false,
220
      sortDescFirst: true,
221
    });
222

223
    return [...fixedColumns, ...criteriaColumns, bonusColumn];
19✔
224
  };
225

226
  toggleShowInactiveGroups = showInactiveGroups => {
8✔
227
    let columnFilters = this.state.columnFilters.filter(group => group.id !== "inactive");
3✔
228

229
    if (!showInactiveGroups) {
3✔
230
      columnFilters.push({id: "inactive", value: false});
1✔
231
    }
232

233
    this.setState({columnFilters});
3✔
234
  };
235

236
  memberDisplay = (group_name, members) => {
8✔
237
    if (members.length !== 0 && !(members.length === 1 && members[0][0] === group_name)) {
22!
238
      return (
22✔
239
        " (" +
240
        members
241
          .map(member => {
242
            return member[0];
44✔
243
          })
244
          .join(", ") +
245
        ")"
246
      );
247
    }
248
  };
249

250
  fetchData = () => {
8✔
251
    return fetch(
8✔
252
      Routes.summary_course_assignment_path(this.props.course_id, this.props.assignment_id),
253
      {
254
        headers: {
255
          Accept: "application/json",
256
        },
257
      }
258
    )
259
      .then(response => {
260
        if (response.ok) {
8!
261
          return response.json();
8✔
262
        }
263
      })
264
      .then(res => {
265
        res.criteriaColumns.forEach(col => {
8✔
266
          col.enableColumnFilter = false;
6✔
267
          col.sortDescFirst = true;
6✔
268
        });
269

270
        let inactive_groups_count = 0;
8✔
271
        res.data.forEach(group => {
8✔
272
          if (group.members.length && group.members.every(member => member[3])) {
18✔
273
            group.inactive = true;
6✔
274
            inactive_groups_count++;
6✔
275
          } else {
276
            group.inactive = false;
6✔
277
          }
278
        });
279

280
        const processedData = this.processData(res.data);
8✔
281
        const markingStates = getMarkingStates(processedData);
8✔
282
        this.setState({
8✔
283
          data: processedData,
284
          criteriaColumns: res.criteriaColumns,
285
          num_assigned: res.numAssigned,
286
          num_marked: res.numMarked,
287
          enable_test: res.enableTest,
288
          loading: false,
289
          marking_states: markingStates,
290
          lti_deployments: res.ltiDeployments,
291
          inactiveGroupsCount: inactive_groups_count,
292
        });
293
      });
294
  };
295

296
  processData(data) {
297
    data.forEach(row => {
8✔
298
      switch (row.marking_state) {
12!
299
        case "not_collected":
UNCOV
300
          row.marking_state = I18n.t("submissions.state.not_collected");
×
UNCOV
301
          break;
×
302
        case "incomplete":
UNCOV
303
          row.marking_state = I18n.t("submissions.state.in_progress");
×
UNCOV
304
          break;
×
305
        case "complete":
UNCOV
306
          row.marking_state = I18n.t("submissions.state.complete");
×
UNCOV
307
          break;
×
308
        case "released":
309
          row.marking_state = I18n.t("submissions.state.released");
12✔
310
          break;
12✔
311
        case "remark":
UNCOV
312
          row.marking_state = I18n.t("submissions.state.remark_requested");
×
UNCOV
313
          break;
×
314
        case "before_due_date":
UNCOV
315
          row.marking_state = I18n.t("submissions.state.before_due_date");
×
316
          break;
×
317
        default:
318
          // should not get here
UNCOV
319
          row.marking_state = row.original.marking_state;
×
320
      }
321
    });
322
    return data;
8✔
323
  }
324

325
  onFilteredChange = (filtered, column) => {
8✔
UNCOV
326
    const summaryTable = this.wrappedInstance;
×
UNCOV
327
    if (column.id != "marking_state") {
×
UNCOV
328
      const markingStates = getMarkingStates(summaryTable.state.sortedData);
×
UNCOV
329
      this.setState({marking_states: markingStates});
×
330
    } else {
UNCOV
331
      const markingStateFilter = filtered.find(filter => filter.id == "marking_state").value;
×
UNCOV
332
      this.setState({markingStateFilter: markingStateFilter});
×
333
    }
334
  };
335

336
  onDownloadTestsModal = () => {
8✔
337
    this.setState({showDownloadTestsModal: true});
×
338
  };
339

340
  onLtiGradeModal = () => {
8✔
UNCOV
341
    this.setState({showLtiGradeModal: true});
×
342
  };
343

344
  render() {
345
    const {data} = this.state;
19✔
346
    let ltiButton;
347
    if (this.state.lti_deployments.length > 0) {
19!
UNCOV
348
      ltiButton = (
×
349
        <button type="submit" name="sync_grades" onClick={this.onLtiGradeModal}>
350
          {I18n.t("lti.sync_grades_lms")}
351
        </button>
352
      );
353
    }
354

355
    let displayInactiveGroupsTooltip = "";
19✔
356

357
    if (this.state.inactiveGroupsCount !== null) {
19!
358
      displayInactiveGroupsTooltip = `${I18n.t("activerecord.attributes.grouping.inactive_groups", {
19✔
359
        count: this.state.inactiveGroupsCount,
360
      })}`;
361
    }
362

363
    return (
19✔
364
      <div>
365
        <div style={{display: "inline-block"}}>
366
          <div className="progress">
367
            <meter
368
              value={this.state.num_marked}
369
              min={0}
370
              max={this.state.num_assigned}
371
              low={this.state.num_assigned * 0.35}
372
              high={this.state.num_assigned * 0.75}
373
              optimum={this.state.num_assigned}
374
            >
375
              {this.state.num_marked}/{this.state.num_assigned}
376
            </meter>
377
            {this.state.num_marked}/{this.state.num_assigned}&nbsp;
378
            {I18n.t("submissions.state.complete")}
379
          </div>
380
        </div>
381
        <div className="rt-action-box">
382
          <input
383
            id="show_inactive_groups"
384
            name="show_inactive_groups"
385
            type="checkbox"
386
            onChange={e => this.toggleShowInactiveGroups(e.target.checked)}
3✔
387
            className={"hide-user-checkbox"}
388
            data-testid={"show_inactive_groups"}
389
          />
390
          <label
391
            title={displayInactiveGroupsTooltip}
392
            htmlFor="show_inactive_groups"
393
            data-testid={"show_inactive_groups_tooltip"}
394
          >
395
            {I18n.t("submissions.groups.display_inactive")}
396
          </label>
397
          {this.props.is_instructor && (
23✔
398
            <>
399
              <form
400
                action={Routes.summary_course_assignment_path({
401
                  course_id: this.props.course_id,
402
                  id: this.props.assignment_id,
403
                  format: "csv",
404
                  _options: true,
405
                })}
406
                method="get"
407
              >
408
                <button type="submit" name="download">
409
                  {I18n.t("download")}
410
                </button>
411
              </form>
412
              {this.state.enable_test && (
5✔
413
                <button type="submit" name="download_tests" onClick={this.onDownloadTestsModal}>
414
                  {I18n.t("download_the", {
415
                    item: I18n.t("activerecord.models.test_result.other"),
416
                  })}
417
                </button>
418
              )}
419
              {ltiButton}
420
            </>
421
          )}
422
        </div>
423
        <Table
424
          data={data}
425
          columns={this.getColumns()}
426
          initialState={{
427
            sorting: [{id: "group_name"}],
428
          }}
429
          columnFilters={this.state.columnFilters}
430
          getRowCanExpand={() => true}
44✔
431
          renderSubComponent={renderSubComponent}
432
          loading={this.state.loading}
433
        />
434
        <DownloadTestResultsModal
435
          course_id={this.props.course_id}
436
          assignment_id={this.props.assignment_id}
437
          isOpen={this.state.showDownloadTestsModal}
UNCOV
438
          onRequestClose={() => this.setState({showDownloadTestsModal: false})}
×
439
          onSubmit={() => {}}
440
        />
441
        <LtiGradeModal
442
          isOpen={this.state.showLtiGradeModal}
UNCOV
443
          onRequestClose={() => this.setState({showLtiGradeModal: false})}
×
444
          lti_deployments={this.state.lti_deployments}
445
          assignment_id={this.props.assignment_id}
446
          course_id={this.props.course_id}
447
        />
448
      </div>
449
    );
450
  }
451
}
452

453
const renderSubComponent = ({row}) => {
1✔
454
  return (
1✔
455
    <div>
456
      <h4>{I18n.t("activerecord.models.ta", {count: row.original.graders.length})}</h4>
457
      <ul>
458
        {row.original.graders.map(grader => {
459
          return (
1✔
460
            <li key={grader[0]}>
461
              ({grader[0]}) {grader[1]} {grader[2]}
462
            </li>
463
          );
464
        })}
465
      </ul>
466
    </div>
467
  );
468
};
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

© 2025 Coveralls, Inc