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

MarkUsProject / Markus / 17040575291

18 Aug 2025 12:27PM UTC coverage: 91.848% (-0.04%) from 91.884%
17040575291

push

github

web-flow
Migrated assignment summary table to react-table v8 (#7630)

683 of 1458 branches covered (46.84%)

Branch coverage included in aggregate %.

53 of 79 new or added lines in 2 files covered. (67.09%)

1 existing line in 1 file now uncovered.

42121 of 45145 relevant lines covered (93.3%)

118.42 hits per line

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

59.52
/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 {
NEW
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) => {
NEW
68
          if (filterValue) {
×
69
            // Check group name
NEW
70
            if (row.original.group_name.includes(filterValue)) {
×
NEW
71
              return true;
×
72
            }
73

74
            // Check member names
NEW
75
            const member_matches = row.original.members.some(member =>
×
NEW
76
              member.some(name => name.includes(filterValue))
×
77
            );
78

NEW
79
            if (member_matches) {
×
NEW
80
              return true;
×
81
            }
82

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

354
    let displayInactiveGroupsTooltip = "";
19✔
355

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

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

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