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

MarkUsProject / Markus / 24593804825

18 Apr 2026 01:29AM UTC coverage: 91.723%. Remained the same
24593804825

Pull #7910

github

web-flow
Merge f948f7160 into 51d085c06
Pull Request #7910: Refactored tables to cache column definitions

955 of 1849 branches covered (51.65%)

Branch coverage included in aggregate %.

43 of 82 new or added lines in 4 files covered. (52.44%)

99 existing lines in 4 files now uncovered.

45267 of 48544 relevant lines covered (93.25%)

130.53 hits per line

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

69.14
/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);
12✔
12
    const markingStates = getMarkingStates([]);
11✔
13
    this.state = {
11✔
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
      columns: this.getColumns([], markingStates, "all"),
28
    };
29
  }
30

31
  componentDidMount() {
32
    this.fetchData();
11✔
33
  }
34

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

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

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

81
            if (member_matches) {
4!
82
              return true;
×
83
            }
84

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

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

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

226
    return [...fixedColumns, ...criteriaColumnDefs, bonusColumn];
22✔
227
  };
228

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

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

236
    this.setState({columnFilters});
3✔
237
  };
238

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

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

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

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

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

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

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

362
  onLtiGradeModal = () => {
11✔
363
    this.setState({showLtiGradeModal: true});
×
364
  };
365

366
  render() {
367
    const {data} = this.state;
29✔
368
    let ltiButton;
369
    if (this.state.lti_deployments.length > 0) {
29!
370
      ltiButton = (
×
371
        <button type="submit" name="sync_grades" onClick={this.onLtiGradeModal}>
372
          {I18n.t("lti.sync_grades_lms")}
373
        </button>
374
      );
375
    }
376

377
    let displayInactiveGroupsTooltip = "";
29✔
378

379
    if (this.state.inactiveGroupsCount !== null) {
29!
380
      displayInactiveGroupsTooltip = `${I18n.t("activerecord.attributes.grouping.inactive_groups", {
29✔
381
        count: this.state.inactiveGroupsCount,
382
      })}`;
383
    }
384

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

484
const renderSubComponent = ({row}) => {
1✔
485
  return (
1✔
486
    <div>
487
      <h4>{I18n.t("activerecord.models.ta", {count: row.original.graders.length})}</h4>
488
      <ul>
489
        {row.original.graders.map(grader => {
490
          return (
1✔
491
            <li key={grader[0]}>
492
              ({grader[0]}) {grader[1]} {grader[2]}
493
            </li>
494
          );
495
        })}
496
      </ul>
497
    </div>
498
  );
499
};
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