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

MarkUsProject / Markus / 16927721670

13 Aug 2025 04:43AM UTC coverage: 91.928% (+0.04%) from 91.884%
16927721670

Pull #7630

github

web-flow
Merge ab72a2d04 into 50db86a75
Pull Request #7630: Migrate assignment summary page to react-table v8

667 of 1421 branches covered (46.94%)

Branch coverage included in aggregate %.

43 of 51 new or added lines in 3 files covered. (84.31%)

1 existing line in 1 file now uncovered.

42111 of 45113 relevant lines covered (93.35%)

118.5 hits per line

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

73.95
/app/javascript/Components/assignment_summary_table.jsx
1
import React from "react";
2
import {markingStateColumn, getMarkingStates} 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
const renderSubComponent = ({row}) => {
1✔
10
  return (
1✔
11
    <div>
12
      <h4>{I18n.t("activerecord.models.ta", {count: 2})}</h4>
13
      <ul>
14
        {row.original.graders.map(grader => {
NEW
15
          return (
×
16
            <li key={grader[0]}>
17
              ({grader[0]}) {grader[1]} {grader[2]}
18
            </li>
19
          );
20
        })}
21
      </ul>
22
    </div>
23
  );
24
};
25

26
export class AssignmentSummaryTable extends React.Component {
27
  constructor(props) {
28
    super(props);
9✔
29
    const markingStates = getMarkingStates([]);
8✔
30
    this.state = {
8✔
31
      data: [],
32
      criteriaColumns: [],
33
      loading: true,
34
      num_assigned: 0,
35
      num_marked: 0,
36
      enable_test: false,
37
      marking_states: markingStates,
38
      markingStateFilter: "all",
39
      showDownloadTestsModal: false,
40
      showLtiGradeModal: false,
41
      lti_deployments: [],
42
      columnFilters: [{id: "inactive", value: false}],
43
      inactiveGroupsCount: 0,
44
    };
45
  }
46

47
  componentDidMount() {
48
    this.fetchData();
8✔
49
  }
50

51
  getColumns = () => {
8✔
52
    const columnHelper = createColumnHelper();
19✔
53

54
    const expanderColumn = columnHelper.display({
19✔
55
      id: "expander",
56
      header: () => null,
38✔
57
      size: 32,
58
      cell: ({row}) => {
59
        return row.getCanExpand() ? (
22!
60
          <button
61
            {...{
62
              onClick: row.getToggleExpandedHandler(),
63
              style: {
64
                all: "unset",
65
                cursor: "pointer",
66
                display: "flex",
67
                alignItems: "center",
68
                justifyContent: "center",
69
                width: "100%",
70
                height: "100%",
71
                fontSize: "0.7rem",
72
              },
73
            }}
74
          >
75
            {row.getIsExpanded() ? "â–¼" : "â–¶"}
22✔
76
          </button>
77
        ) : (
78
          " "
79
        );
80
      },
81
    });
82

83
    const fixedColumns = [
19✔
84
      columnHelper.accessor("inactive", {
85
        id: "inactive",
86
      }),
87
      columnHelper.accessor("group_name", {
88
        id: "group_name",
89
        header: () => I18n.t("activerecord.models.group.one"),
38✔
90
        size: 100,
91
        enableResizing: true,
92
        cell: info => {
93
          if (info.row.original.result_id) {
22!
94
            const path = Routes.edit_course_result_path(
22✔
95
              this.props.course_id,
96
              info.row.original.result_id
97
            );
98
            return (
22✔
99
              <a href={path}>
100
                {info.row.original.group_name}
101
                {this.memberDisplay(info.row.original.group_name, info.row.original.members)}
102
              </a>
103
            );
104
          } else {
NEW
105
            return (
×
106
              <span>
107
                {info.row.original.group_name}
108
                {this.memberDisplay(info.row.original.group_name, info.row.original.members)}
109
              </span>
110
            );
111
          }
112
        },
113
      }),
114
      markingStateColumn(this.state.marking_states, this.state.markingStateFilter),
115
      columnHelper.accessor("tags", {
116
        header: () => I18n.t("activerecord.models.tag.other"),
38✔
117
        size: 90,
118
        enableResizing: true,
119
        cell: info => (
120
          <ul className="tag-list">
22✔
121
            {info.row.original.tags.map(tag => (
NEW
122
              <li key={`${info.row.original._id}-${tag}`} className="tag-element">
×
123
                {tag}
124
              </li>
125
            ))}
126
          </ul>
127
        ),
128
        minWidth: 80,
129
        enableSorting: false,
130
        filterFn: (row, columnId, filterValue) => {
NEW
131
          if (filterValue) {
×
132
            // Check tag names
NEW
133
            return row.original.tags.some(tag => tag.includes(filterValue));
×
134
          } else {
NEW
135
            return true;
×
136
          }
137
        },
138
      }),
139
      columnHelper.accessor("final_grade", {
140
        header: () => I18n.t("results.total_mark"),
38✔
141
        size: 100,
142
        enableResizing: true,
143
        cell: info => {
144
          if (info.row.original.final_grade || info.row.original.final_grade === 0) {
22!
145
            const max_mark = Math.round(info.row.original.max_mark * 100) / 100;
22✔
146
            return info.row.original.final_grade + " / " + max_mark;
22✔
147
          } else {
NEW
148
            return "";
×
149
          }
150
        },
151
        meta: {className: "number"},
152
        enableColumnFilter: false,
153
        sortDescFirst: true,
154
      }),
155
    ];
156

157
    const criteriaColumns = this.state.criteriaColumns.map(col =>
19✔
158
      columnHelper.accessor(col.accessor, {
9✔
159
        id: col.id,
160
        header: () => col.Header,
19✔
161
        size: 100,
162
        enableResizing: true,
163
        cell: value => value.getValue(),
22✔
164
        meta: {className: "number"},
165
        enableColumnFilter: false,
166
      })
167
    );
168

169
    const bonusColumn = columnHelper.accessor("total_extra_marks", {
19✔
170
      header: () => I18n.t("activerecord.models.extra_mark.other"),
38✔
171
      size: 100,
172
      enableResizing: true,
173
      cell: info => info.getValue(),
22✔
174
      meta: {className: "number"},
175
      enableColumnFilter: false,
176
      sortDescFirst: true,
177
    });
178

179
    return [expanderColumn, ...fixedColumns, ...criteriaColumns, bonusColumn];
19✔
180
  };
181

182
  toggleShowInactiveGroups = showInactiveGroups => {
8✔
183
    let columnFilters = this.state.columnFilters.filter(group => group.id !== "inactive");
3✔
184

185
    if (!showInactiveGroups) {
3✔
186
      columnFilters.push({id: "inactive", value: false});
1✔
187
    }
188

189
    this.setState({columnFilters});
3✔
190
  };
191

192
  memberDisplay = (group_name, members) => {
8✔
193
    if (members.length !== 0 && !(members.length === 1 && members[0][0] === group_name)) {
22!
194
      return (
22✔
195
        " (" +
196
        members
197
          .map(member => {
198
            return member[0];
44✔
199
          })
200
          .join(", ") +
201
        ")"
202
      );
203
    }
204
  };
205

206
  fetchData = () => {
8✔
207
    return fetch(
8✔
208
      Routes.summary_course_assignment_path(this.props.course_id, this.props.assignment_id),
209
      {
210
        headers: {
211
          Accept: "application/json",
212
        },
213
      }
214
    )
215
      .then(response => {
216
        if (response.ok) {
8!
217
          return response.json();
8✔
218
        }
219
      })
220
      .then(res => {
221
        res.criteriaColumns.forEach(col => {
8✔
222
          col.enableColumnFilter = false;
6✔
223
          col.sortDescFirst = true;
6✔
224
        });
225

226
        let inactive_groups_count = 0;
8✔
227
        res.data.forEach(group => {
8✔
228
          if (group.members.length && group.members.every(member => member[3])) {
18✔
229
            group.inactive = true;
6✔
230
            inactive_groups_count++;
6✔
231
          } else {
232
            group.inactive = false;
6✔
233
          }
234
        });
235

236
        const markingStates = getMarkingStates(res.data);
8✔
237
        this.setState({
8✔
238
          data: res.data,
239
          criteriaColumns: res.criteriaColumns,
240
          num_assigned: res.numAssigned,
241
          num_marked: res.numMarked,
242
          enable_test: res.enableTest,
243
          loading: false,
244
          marking_states: markingStates,
245
          lti_deployments: res.ltiDeployments,
246
          inactiveGroupsCount: inactive_groups_count,
247
        });
248
      });
249
  };
250

251
  onFilteredChange = (filtered, column) => {
8✔
252
    const summaryTable = this.wrappedInstance;
×
253
    if (column.id != "marking_state") {
×
254
      const markingStates = getMarkingStates(summaryTable.state.sortedData);
×
255
      this.setState({marking_states: markingStates});
×
256
    } else {
257
      const markingStateFilter = filtered.find(filter => filter.id == "marking_state").value;
×
258
      this.setState({markingStateFilter: markingStateFilter});
×
259
    }
260
  };
261

262
  onDownloadTestsModal = () => {
8✔
UNCOV
263
    this.setState({showDownloadTestsModal: true});
×
264
  };
265

266
  onLtiGradeModal = () => {
8✔
267
    this.setState({showLtiGradeModal: true});
×
268
  };
269

270
  render() {
271
    const {data} = this.state;
19✔
272
    let ltiButton;
273
    if (this.state.lti_deployments.length > 0) {
19!
274
      ltiButton = (
×
275
        <button type="submit" name="sync_grades" onClick={this.onLtiGradeModal}>
276
          {I18n.t("lti.sync_grades_lms")}
277
        </button>
278
      );
279
    }
280

281
    let displayInactiveGroupsTooltip = "";
19✔
282

283
    if (this.state.inactiveGroupsCount !== null) {
19!
284
      displayInactiveGroupsTooltip = `${I18n.t("activerecord.attributes.grouping.inactive_groups", {
19✔
285
        count: this.state.inactiveGroupsCount,
286
      })}`;
287
    }
288

289
    return (
19✔
290
      <div>
291
        <div style={{display: "inline-block"}}>
292
          <div className="progress">
293
            <meter
294
              value={this.state.num_marked}
295
              min={0}
296
              max={this.state.num_assigned}
297
              low={this.state.num_assigned * 0.35}
298
              high={this.state.num_assigned * 0.75}
299
              optimum={this.state.num_assigned}
300
            >
301
              {this.state.num_marked}/{this.state.num_assigned}
302
            </meter>
303
            {this.state.num_marked}/{this.state.num_assigned}&nbsp;
304
            {I18n.t("submissions.state.complete")}
305
          </div>
306
        </div>
307
        <div className="rt-action-box">
308
          <input
309
            id="show_inactive_groups"
310
            name="show_inactive_groups"
311
            type="checkbox"
312
            onChange={e => this.toggleShowInactiveGroups(e.target.checked)}
3✔
313
            className={"hide-user-checkbox"}
314
            data-testid={"show_inactive_groups"}
315
          />
316
          <label
317
            title={displayInactiveGroupsTooltip}
318
            htmlFor="show_inactive_groups"
319
            data-testid={"show_inactive_groups_tooltip"}
320
          >
321
            {I18n.t("submissions.groups.display_inactive")}
322
          </label>
323
          {this.props.is_instructor && (
23✔
324
            <>
325
              <form
326
                action={Routes.summary_course_assignment_path({
327
                  course_id: this.props.course_id,
328
                  id: this.props.assignment_id,
329
                  format: "csv",
330
                  _options: true,
331
                })}
332
                method="get"
333
              >
334
                <button type="submit" name="download">
335
                  {I18n.t("download")}
336
                </button>
337
              </form>
338
              {this.state.enable_test && (
5✔
339
                <button type="submit" name="download_tests" onClick={this.onDownloadTestsModal}>
340
                  {I18n.t("download_the", {
341
                    item: I18n.t("activerecord.models.test_result.other"),
342
                  })}
343
                </button>
344
              )}
345
              {ltiButton}
346
            </>
347
          )}
348
        </div>
349
        <Table
350
          data={data}
351
          columns={this.getColumns()}
352
          initialState={{
353
            sorting: [{id: "group_name"}],
354
          }}
355
          columnFilters={this.state.columnFilters}
356
          getRowCanExpand={() => true}
44✔
357
          renderSubComponent={renderSubComponent}
358
          loading={this.state.loading}
359
        />
360
        <DownloadTestResultsModal
361
          course_id={this.props.course_id}
362
          assignment_id={this.props.assignment_id}
363
          isOpen={this.state.showDownloadTestsModal}
364
          onRequestClose={() => this.setState({showDownloadTestsModal: false})}
×
365
          onSubmit={() => {}}
366
        />
367
        <LtiGradeModal
368
          isOpen={this.state.showLtiGradeModal}
369
          onRequestClose={() => this.setState({showLtiGradeModal: false})}
×
370
          lti_deployments={this.state.lti_deployments}
371
          assignment_id={this.props.assignment_id}
372
          course_id={this.props.course_id}
373
        />
374
      </div>
375
    );
376
  }
377
}
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