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

MarkUsProject / Markus / 17016853900

17 Aug 2025 04:45AM UTC coverage: 91.848% (-0.04%) from 91.884%
17016853900

Pull #7630

github

web-flow
Merge 56bd226f6 into 785f735a0
Pull Request #7630: Migrate assignment summary page to react-table v8

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: {headerClassName: col.headerClassName},
205
        enableColumnFilter: col.enableColumnFilter,
206
        sortDescFirst: col.sortDescFirst,
207
      })
208
    );
209

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

219
    return [...fixedColumns, ...criteriaColumns, bonusColumn];
19✔
220
  };
221

222
  toggleShowInactiveGroups = showInactiveGroups => {
8✔
223
    let columnFilters = this.state.columnFilters.filter(group => group.id !== "inactive");
3✔
224

225
    if (!showInactiveGroups) {
3✔
226
      columnFilters.push({id: "inactive", value: false});
1✔
227
    }
228

229
    this.setState({columnFilters});
3✔
230
  };
231

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

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

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

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

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

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

332
  onDownloadTestsModal = () => {
8✔
UNCOV
333
    this.setState({showDownloadTestsModal: true});
×
334
  };
335

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

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

351
    let displayInactiveGroupsTooltip = "";
19✔
352

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

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

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