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

MarkUsProject / Markus / 16993661674

15 Aug 2025 03:46PM UTC coverage: 91.898% (+0.01%) from 91.884%
16993661674

Pull #7630

github

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

672 of 1433 branches covered (46.89%)

Branch coverage included in aggregate %.

44 of 61 new or added lines in 3 files covered. (72.13%)

1 existing line in 1 file now uncovered.

42112 of 45123 relevant lines covered (93.33%)

118.47 hits per line

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

64.62
/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: row.original.graders.length})}</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 fixedColumns = [
19✔
55
      columnHelper.accessor("inactive", {
56
        id: "inactive",
57
      }),
58
      columnHelper.accessor("group_name", {
59
        id: "group_name",
60
        header: () => I18n.t("activerecord.models.group.one"),
38✔
61
        size: 100,
62
        enableResizing: true,
63
        cell: props => {
64
          if (props.row.original.result_id) {
22!
65
            const path = Routes.edit_course_result_path(
22✔
66
              this.props.course_id,
67
              props.row.original.result_id
68
            );
69
            return (
22✔
70
              <a href={path}>
71
                {props.getValue()}
72
                {this.memberDisplay(props.getValue(), props.row.original.members)}
73
              </a>
74
            );
75
          } else {
NEW
76
            return (
×
77
              <span>
78
                {props.row.original.group_name}
79
                {this.memberDisplay(props.row.original.group_name, props.row.original.members)}
80
              </span>
81
            );
82
          }
83
        },
84
        filterFn: (row, columnId, filterValue) => {
NEW
85
          if (filterValue) {
×
86
            // Check group name
NEW
87
            if (row.original.group_name.includes(filterValue)) {
×
NEW
88
              return true;
×
89
            }
90

91
            // Check member names
NEW
92
            const member_matches = row.original.members.some(member =>
×
NEW
93
              member.some(name => name.includes(filterValue))
×
94
            );
95

NEW
96
            if (member_matches) {
×
NEW
97
              return true;
×
98
            }
99

100
            // Check grader user names
NEW
101
            return row.original.graders.some(grader => grader.includes(filterValue));
×
102
          } else {
NEW
103
            return true;
×
104
          }
105
        },
106
      }),
107
      markingStateColumn(this.state.marking_states, this.state.markingStateFilter),
108
      columnHelper.accessor("tags", {
109
        header: () => I18n.t("activerecord.models.tag.other"),
38✔
110
        size: 90,
111
        enableResizing: true,
112
        cell: props => (
113
          <ul className="tag-list">
22✔
114
            {props.row.original.tags.map(tag => (
NEW
115
              <li key={`${props.row.original._id}-${tag}`} className="tag-element">
×
116
                {tag}
117
              </li>
118
            ))}
119
          </ul>
120
        ),
121
        minWidth: 80,
122
        enableSorting: false,
123
        filterFn: (row, columnId, filterValue) => {
NEW
124
          if (filterValue) {
×
125
            // Check tag names
NEW
126
            return row.original.tags.some(tag => tag.includes(filterValue));
×
127
          } else {
NEW
128
            return true;
×
129
          }
130
        },
131
      }),
132
      columnHelper.accessor("final_grade", {
133
        header: () => I18n.t("results.total_mark"),
38✔
134
        size: 100,
135
        enableResizing: true,
136
        cell: props => {
137
          if (props.row.original.final_grade || props.row.original.final_grade === 0) {
22!
138
            const max_mark = Math.round(props.row.original.max_mark * 100) / 100;
22✔
139
            return props.row.original.final_grade + " / " + max_mark;
22✔
140
          } else {
NEW
141
            return "";
×
142
          }
143
        },
144
        meta: {className: "number"},
145
        enableColumnFilter: false,
146
        sortDescFirst: true,
147
      }),
148
    ];
149

150
    const criteriaColumns = this.state.criteriaColumns.map(col =>
19✔
151
      columnHelper.accessor(col.accessor, {
9✔
152
        id: col.id,
153
        header: () => col.Header,
19✔
154
        size: col.size || 100,
18✔
155
        enableResizing: col.enableResizing !== undefined ? col.enableResizing : true,
9!
156
        cell: props => props.getValue(),
22✔
157
        meta: {className: col.className},
158
        enableColumnFilter: col.enableColumnFilter,
159
        sortDescFirst: col.sortDescFirst,
160
      })
161
    );
162

163
    const bonusColumn = columnHelper.accessor("total_extra_marks", {
19✔
164
      header: () => I18n.t("activerecord.models.extra_mark.other"),
38✔
165
      size: 100,
166
      enableResizing: true,
167
      meta: {className: "number"},
168
      enableColumnFilter: false,
169
      sortDescFirst: true,
170
    });
171

172
    return [...fixedColumns, ...criteriaColumns, bonusColumn];
19✔
173
  };
174

175
  toggleShowInactiveGroups = showInactiveGroups => {
8✔
176
    let columnFilters = this.state.columnFilters.filter(group => group.id !== "inactive");
3✔
177

178
    if (!showInactiveGroups) {
3✔
179
      columnFilters.push({id: "inactive", value: false});
1✔
180
    }
181

182
    this.setState({columnFilters});
3✔
183
  };
184

185
  memberDisplay = (group_name, members) => {
8✔
186
    if (members.length !== 0 && !(members.length === 1 && members[0][0] === group_name)) {
22!
187
      return (
22✔
188
        " (" +
189
        members
190
          .map(member => {
191
            return member[0];
44✔
192
          })
193
          .join(", ") +
194
        ")"
195
      );
196
    }
197
  };
198

199
  fetchData = () => {
8✔
200
    return fetch(
8✔
201
      Routes.summary_course_assignment_path(this.props.course_id, this.props.assignment_id),
202
      {
203
        headers: {
204
          Accept: "application/json",
205
        },
206
      }
207
    )
208
      .then(response => {
209
        if (response.ok) {
8!
210
          return response.json();
8✔
211
        }
212
      })
213
      .then(res => {
214
        res.criteriaColumns.forEach(col => {
8✔
215
          col.enableColumnFilter = false;
6✔
216
          col.sortDescFirst = true;
6✔
217
        });
218

219
        let inactive_groups_count = 0;
8✔
220
        res.data.forEach(group => {
8✔
221
          if (group.members.length && group.members.every(member => member[3])) {
18✔
222
            group.inactive = true;
6✔
223
            inactive_groups_count++;
6✔
224
          } else {
225
            group.inactive = false;
6✔
226
          }
227
        });
228

229
        const markingStates = getMarkingStates(res.data);
8✔
230
        this.setState({
8✔
231
          data: res.data,
232
          criteriaColumns: res.criteriaColumns,
233
          num_assigned: res.numAssigned,
234
          num_marked: res.numMarked,
235
          enable_test: res.enableTest,
236
          loading: false,
237
          marking_states: markingStates,
238
          lti_deployments: res.ltiDeployments,
239
          inactiveGroupsCount: inactive_groups_count,
240
        });
241
      });
242
  };
243

244
  onFilteredChange = (filtered, column) => {
8✔
245
    const summaryTable = this.wrappedInstance;
×
246
    if (column.id != "marking_state") {
×
247
      const markingStates = getMarkingStates(summaryTable.state.sortedData);
×
248
      this.setState({marking_states: markingStates});
×
249
    } else {
250
      const markingStateFilter = filtered.find(filter => filter.id == "marking_state").value;
×
251
      this.setState({markingStateFilter: markingStateFilter});
×
252
    }
253
  };
254

255
  onDownloadTestsModal = () => {
8✔
UNCOV
256
    this.setState({showDownloadTestsModal: true});
×
257
  };
258

259
  onLtiGradeModal = () => {
8✔
260
    this.setState({showLtiGradeModal: true});
×
261
  };
262

263
  render() {
264
    const {data} = this.state;
19✔
265
    let ltiButton;
266
    if (this.state.lti_deployments.length > 0) {
19!
267
      ltiButton = (
×
268
        <button type="submit" name="sync_grades" onClick={this.onLtiGradeModal}>
269
          {I18n.t("lti.sync_grades_lms")}
270
        </button>
271
      );
272
    }
273

274
    let displayInactiveGroupsTooltip = "";
19✔
275

276
    if (this.state.inactiveGroupsCount !== null) {
19!
277
      displayInactiveGroupsTooltip = `${I18n.t("activerecord.attributes.grouping.inactive_groups", {
19✔
278
        count: this.state.inactiveGroupsCount,
279
      })}`;
280
    }
281

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