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

MarkUsProject / Markus / 29662909844

18 Jul 2026 10:08PM UTC coverage: 90.356%. First build
29662909844

Pull #8067

github

web-flow
Merge b5fbfae23 into 35deb772c
Pull Request #8067: V2.10.1

1156 of 2380 branches covered (48.57%)

Branch coverage included in aggregate %.

1187 of 1268 new or added lines in 107 files covered. (93.61%)

46952 of 50863 relevant lines covered (92.31%)

133.31 hits per line

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

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

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

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

33
  componentDidMount() {
34
    this.fetchData();
11✔
35
  }
36

37
  getColumns = (criteriaColumns, marking_states, markingStateFilter) => {
11✔
38
    const columnHelper = createColumnHelper();
22✔
39

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

78
            // Check member names (first three values of each "member" array)
79
            const member_matches = row.original.members.some(member =>
4✔
80
              member.slice(0, 3).some(name => name.toLowerCase().includes(filterValue))
12✔
81
            );
82

83
            if (member_matches) {
4!
84
              return true;
×
85
            }
86

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

205
    const criteriaColumnDefs = criteriaColumns.map(col =>
22✔
206
      columnHelper.accessor(col.accessor, {
6✔
207
        id: col.id,
208
        header: () => col.Header,
6✔
209
        size: col.size || 100,
12✔
210
        meta: {
211
          className: col.className,
212
          headerClassName: col.headerClassName,
213
        },
214
        enableColumnFilter: col.enableColumnFilter,
215
        sortDescFirst: col.sortDescFirst,
216
      })
217
    );
218

219
    const bonusColumn = columnHelper.accessor("total_extra_marks", {
22✔
220
      header: () => I18n.t("activerecord.models.extra_mark.other"),
22✔
221
      size: 100,
222
      enableResizing: true,
223
      meta: {className: "number"},
224
      enableColumnFilter: false,
225
      sortDescFirst: true,
226
    });
227

228
    return [...fixedColumns, ...criteriaColumnDefs, bonusColumn];
22✔
229
  };
230

231
  toggleShowInactiveGroups = showInactiveGroups => {
11✔
232
    let columnFilters = this.state.columnFilters.filter(group => group.id !== "inactive");
3✔
233

234
    if (!showInactiveGroups) {
3✔
235
      columnFilters.push({id: "inactive", value: false});
1✔
236
    }
237

238
    this.setState({columnFilters});
3✔
239
  };
240

241
  memberDisplay = (group_name, members) => {
11✔
242
    if (members.length !== 0 && !(members.length === 1 && members[0][0] === group_name)) {
15!
243
      return (
15✔
244
        " (" +
245
        members
246
          .map(member => {
247
            return member[0];
23✔
248
          })
249
          .join(", ") +
250
        ")"
251
      );
252
    }
253
  };
254

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

275
        let inactive_groups_count = 0;
11✔
276
        res.data.forEach(group => {
11✔
277
          if (group.members.length && group.members.every(member => member[3])) {
24✔
278
            group.inactive = true;
6✔
279
            inactive_groups_count++;
6✔
280
          } else {
281
            group.inactive = false;
12✔
282
          }
283
        });
284

285
        const processedData = this.processData(res.data);
11✔
286
        const markingStates = getMarkingStates(processedData);
11✔
287
        this.setState({
11✔
288
          data: processedData,
289
          criteriaColumns: res.criteriaColumns,
290
          num_assigned: res.numAssigned,
291
          num_marked: res.numMarked,
292
          enable_test: res.enableTest,
293
          loading: false,
294
          marking_states: markingStates,
295
          lti_deployments: res.ltiDeployments,
296
          inactiveGroupsCount: inactive_groups_count,
297
          columns: this.getColumns(
298
            res.criteriaColumns,
299
            markingStates,
300
            this.state.markingStateFilter
301
          ),
302
        });
303
      });
304
  };
305

306
  processData(data) {
307
    data.forEach(row => {
11✔
308
      switch (row.marking_state) {
18!
309
        case "not_collected":
310
          row.marking_state = I18n.t("submissions.state.not_collected");
×
311
          break;
×
312
        case "incomplete":
313
          row.marking_state = I18n.t("submissions.state.in_progress");
×
314
          break;
×
315
        case "complete":
316
          row.marking_state = I18n.t("submissions.state.complete");
6✔
317
          break;
6✔
318
        case "released":
319
          row.marking_state = I18n.t("submissions.state.released");
12✔
320
          break;
12✔
321
        case "remark":
322
          row.marking_state = I18n.t("submissions.state.remark_requested");
×
323
          break;
×
324
        case "before_due_date":
325
          row.marking_state = I18n.t("submissions.state.before_due_date");
×
326
          break;
×
327
        default:
328
          // should not get here
329
          row.marking_state = row.original.marking_state;
×
330
      }
331
    });
332
    return data;
11✔
333
  }
334

335
  onFilteredChange = (filtered, column) => {
11✔
336
    const summaryTable = this.wrappedInstance;
×
337
    if (column.id != "marking_state") {
×
338
      const markingStates = getMarkingStates(summaryTable.state.sortedData);
×
339
      this.setState({
×
340
        marking_states: markingStates,
341
        columns: this.getColumns(
342
          this.state.criteriaColumns,
343
          markingStates,
344
          this.state.markingStateFilter
345
        ),
346
      });
347
    } else {
348
      const markingStateFilter = filtered.find(filter => filter.id == "marking_state").value;
×
349
      this.setState({
×
350
        markingStateFilter,
351
        columns: this.getColumns(
352
          this.state.criteriaColumns,
353
          this.state.marking_states,
354
          markingStateFilter
355
        ),
356
      });
357
    }
358
  };
359

360
  onDownloadTestsModal = () => {
11✔
361
    this.setState({showDownloadTestsModal: true});
×
362
  };
363

364
  onGradesUploadModal = () => {
11✔
NEW
365
    this.setState({showGradesUploadModal: true});
×
366
  };
367

368
  onLtiGradeModal = () => {
11✔
369
    this.setState({showLtiGradeModal: true});
×
370
  };
371

372
  render() {
373
    const {data} = this.state;
29✔
374
    let ltiButton;
375
    if (this.state.lti_deployments.length > 0) {
29!
376
      ltiButton = (
×
377
        <button type="submit" name="sync_grades" onClick={this.onLtiGradeModal}>
378
          {I18n.t("lti.sync_grades_lms")}
379
        </button>
380
      );
381
    }
382

383
    let displayInactiveGroupsTooltip = "";
29✔
384

385
    if (this.state.inactiveGroupsCount !== null) {
29!
386
      displayInactiveGroupsTooltip = `${I18n.t("activerecord.attributes.grouping.inactive_groups", {
29✔
387
        count: this.state.inactiveGroupsCount,
388
      })}`;
389
    }
390

391
    return (
29✔
392
      <div>
393
        <div style={{display: "inline-block"}}>
394
          <div className="progress">
395
            <meter
396
              value={this.state.num_marked}
397
              min={0}
398
              max={this.state.num_assigned}
399
              low={this.state.num_assigned * 0.35}
400
              high={this.state.num_assigned * 0.75}
401
              optimum={this.state.num_assigned}
402
            >
403
              {this.state.num_marked}/{this.state.num_assigned}
404
            </meter>
405
            {this.state.num_marked}/{this.state.num_assigned}&nbsp;
406
            {I18n.t("submissions.state.complete")}
407
          </div>
408
        </div>
409
        <div className="rt-action-box">
410
          <input
411
            id="show_inactive_groups"
412
            name="show_inactive_groups"
413
            type="checkbox"
414
            onChange={e => this.toggleShowInactiveGroups(e.target.checked)}
3✔
415
            className={"hide-user-checkbox"}
416
            data-testid={"show_inactive_groups"}
417
          />
418
          <label
419
            title={displayInactiveGroupsTooltip}
420
            htmlFor="show_inactive_groups"
421
            data-testid={"show_inactive_groups_tooltip"}
422
          >
423
            {I18n.t("submissions.groups.display_inactive")}
424
          </label>
425
          {this.props.is_instructor && (
33✔
426
            <>
427
              <form
428
                action={Routes.summary_course_assignment_path({
429
                  course_id: this.props.course_id,
430
                  id: this.props.assignment_id,
431
                  format: "csv",
432
                  _options: true,
433
                })}
434
                method="get"
435
              >
436
                <button type="submit" name="download">
437
                  {I18n.t("download")}
438
                </button>
439
              </form>
440
              <button type="button" name="upload" onClick={this.onGradesUploadModal}>
441
                <i className="fa-solid fa-upload" aria-hidden="true" />
442
                {I18n.t("upload")}
443
              </button>
444
              {this.state.enable_test && (
5✔
445
                <button type="submit" name="download_tests" onClick={this.onDownloadTestsModal}>
446
                  {I18n.t("download_the", {
447
                    item: I18n.t("activerecord.models.test_result.other"),
448
                  })}
449
                </button>
450
              )}
451
              {ltiButton}
452
            </>
453
          )}
454
        </div>
455
        <Table
456
          data={data}
457
          columns={this.state.columns}
458
          initialState={{
459
            sorting: [{id: "group_name"}],
460
          }}
461
          columnFilters={this.state.columnFilters}
462
          onColumnFiltersChange={updaterOrValue => {
463
            this.setState(prevState => {
4✔
464
              const newFilters =
465
                typeof updaterOrValue === "function"
4!
466
                  ? updaterOrValue(prevState.columnFilters)
467
                  : updaterOrValue;
468
              return {columnFilters: newFilters};
4✔
469
            });
470
          }}
471
          getRowCanExpand={() => true}
30✔
472
          renderSubComponent={renderSubComponent}
473
          loading={this.state.loading}
474
        />
475
        <AssignmentGradesUploadModal
476
          course_id={this.props.course_id}
477
          assignment_id={this.props.assignment_id}
478
          encodings={this.props.encodings || []}
58✔
479
          isOpen={this.state.showGradesUploadModal}
NEW
480
          onRequestClose={() => this.setState({showGradesUploadModal: false})}
×
481
        />
482
        <DownloadTestResultsModal
483
          course_id={this.props.course_id}
484
          assignment_id={this.props.assignment_id}
485
          isOpen={this.state.showDownloadTestsModal}
486
          onRequestClose={() => this.setState({showDownloadTestsModal: false})}
×
487
          onSubmit={() => {}}
488
        />
489
        <LtiGradeModal
490
          isOpen={this.state.showLtiGradeModal}
491
          onRequestClose={() => this.setState({showLtiGradeModal: false})}
×
492
          lti_deployments={this.state.lti_deployments}
493
          assignment_id={this.props.assignment_id}
494
          course_id={this.props.course_id}
495
        />
496
      </div>
497
    );
498
  }
499
}
500

501
const renderSubComponent = ({row}) => {
1✔
502
  return (
1✔
503
    <div>
504
      <h4>{I18n.t("activerecord.models.ta", {count: row.original.graders.length})}</h4>
505
      <ul>
506
        {row.original.graders.map(grader => {
507
          return (
1✔
508
            <li key={grader[0]}>
509
              ({grader[0]}) {grader[1]} {grader[2]}
510
            </li>
511
          );
512
        })}
513
      </ul>
514
    </div>
515
  );
516
};
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