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

MarkUsProject / Markus / 17254526227

27 Aug 2025 01:12AM UTC coverage: 91.814% (-0.03%) from 91.846%
17254526227

Pull #7602

github

web-flow
Merge 635cdd9fa into 3d2b5b1d1
Pull Request #7602: Added loading icon for instructor table

694 of 1476 branches covered (47.02%)

Branch coverage included in aggregate %.

11 of 12 new or added lines in 5 files covered. (91.67%)

31 existing lines in 2 files now uncovered.

42142 of 45179 relevant lines covered (93.28%)

118.8 hits per line

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

88.64
/app/javascript/Components/student_table.jsx
1
import React from "react";
2
import {createRoot} from "react-dom/client";
3
import PropTypes from "prop-types";
4

5
import {CheckboxTable, withSelection} from "./markus_with_selection_hoc";
6
import {selectFilter} from "./Helpers/table_helpers";
7
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
8
import {faPencil, faTrashCan} from "@fortawesome/free-solid-svg-icons";
9
import {ReactTableDefaults} from "react-table";
10

11
class RawStudentTable extends React.Component {
12
  constructor() {
13
    super();
20✔
14
    this.state = {
19✔
15
      data: {
16
        students: [],
17
        sections: {},
18
        counts: {all: 0, active: 0, inactive: 0},
19
      },
20
      loading: true,
21
    };
22
  }
23

24
  componentDidMount() {
25
    this.fetchData();
19✔
26
  }
27

28
  fetchData = () => {
19✔
29
    fetch(Routes.course_students_path(this.props.course_id), {
20✔
30
      headers: {
31
        Accept: "application/json",
32
      },
33
    })
34
      .then(response => {
35
        if (response.ok) {
19!
36
          return response.json();
19✔
37
        }
38
      })
39
      .then(res => {
40
        this.setState({
19✔
41
          data: res,
42
          loading: false,
43
          selection: [],
44
          selectAll: false,
45
        });
46
      });
47
  };
48

49
  /* Called when an action is run */
50
  onSubmit = event => {
19✔
51
    event.preventDefault();
1✔
52

53
    const data = {
1✔
54
      student_ids: this.props.selection,
55
      bulk_action: this.actionBox.state.action,
56
      grace_credits: this.actionBox.state.grace_credits,
57
      section: this.actionBox.state.section,
58
    };
59

60
    this.setState({loading: true});
1✔
61
    $.ajax({
1✔
62
      method: "patch",
63
      url: Routes.bulk_modify_course_students_path(this.props.course_id),
64
      data: data,
65
    }).then(this.fetchData);
66
  };
67

68
  removeStudent = student_id => {
19✔
69
    fetch(Routes.course_student_path(this.props.course_id, student_id), {
1✔
70
      method: "DELETE",
71
      headers: {
72
        "Content-Type": "application/json",
73
        "X-CSRF-Token": document.querySelector('[name="csrf-token"]').content,
74
      },
75
    })
76
      .then(response => {
77
        if (response.ok) {
1!
78
          this.fetchData();
1✔
79
        }
80
      })
81
      .catch(error => {
UNCOV
82
        console.error("Error removing student:", error);
×
83
      });
84
  };
85

86
  render() {
87
    const {data, loading} = this.state;
24✔
88

89
    return (
24✔
90
      <div data-testid={"raw_student_table"}>
91
        <StudentsActionBox
92
          ref={r => (this.actionBox = r)}
48✔
93
          sections={data.sections}
94
          disabled={this.props.selection.length === 0}
95
          onSubmit={this.onSubmit}
96
          authenticity_token={this.props.authenticity_token}
97
        />
98
        <CheckboxTable
99
          loading={this.state.loading}
100
          ref={r => (this.checkboxTable = r)}
48✔
101
          data={this.state.loading ? [] : data.students}
24✔
102
          columns={[
103
            {
104
              Header: I18n.t("activerecord.attributes.user.user_name"),
105
              accessor: "user_name",
106
              id: "user_name",
107
              minWidth: 120,
108
            },
109
            {
110
              Header: I18n.t("activerecord.attributes.user.first_name"),
111
              accessor: "first_name",
112
              minWidth: 120,
113
            },
114
            {
115
              Header: I18n.t("activerecord.attributes.user.last_name"),
116
              accessor: "last_name",
117
              minWidth: 120,
118
            },
119
            {
120
              Header: I18n.t("activerecord.attributes.user.email"),
121
              accessor: "email",
122
              minWidth: 150,
123
            },
124
            {
125
              Header: I18n.t("activerecord.attributes.user.id_number"),
126
              accessor: "id_number",
127
              minWidth: 90,
128
              className: "number",
129
            },
130
            {
131
              Header: I18n.t("activerecord.models.section", {count: 1}),
132
              accessor: "section",
133
              id: "section",
134
              Cell: ({value}) => {
135
                return data.sections[value] || "";
3✔
136
              },
137
              show: Boolean(data.sections),
138
              filterMethod: (filter, row) => {
139
                if (filter.value === "all") {
3✔
140
                  return true;
1✔
141
                } else {
142
                  return data.sections[row[filter.id]] === filter.value;
2✔
143
                }
144
              },
145
              Filter: selectFilter,
146
              filterOptions: Object.entries(data.sections).map(kv => ({
1✔
147
                value: kv[1],
148
                text: kv[1],
149
              })),
150
            },
151
            {
152
              Header: I18n.t("activerecord.attributes.user.grace_credits"),
153
              id: "grace_credits",
154
              accessor: "remaining_grace_credits",
155
              className: "number",
156
              Cell: row => `${row.value} / ${row.original.grace_credits}`,
3✔
157
              minWidth: 90,
158
              Filter: ({filter, onChange}) => (
159
                <input
29✔
UNCOV
160
                  onChange={event => onChange(event.target.valueAsNumber)}
×
161
                  type="number"
162
                  min={0}
163
                  value={filter ? filter.value : ""}
29!
164
                />
165
              ),
166
              filterMethod: (filter, row) => {
167
                return (
3✔
168
                  isNaN(filter.value) || filter.value === row._original.remaining_grace_credits
5✔
169
                );
170
              },
171
            },
172
            {
173
              Header: I18n.t("roles.active") + "?",
174
              accessor: "hidden",
175
              Cell: ({value}) => (value ? I18n.t("roles.inactive") : I18n.t("roles.active")),
3!
176
              filterMethod: (filter, row) => {
177
                if (filter.value === "all") {
5✔
178
                  return true;
1✔
179
                } else {
180
                  return (
4✔
181
                    (filter.value === "active" && !row[filter.id]) ||
11✔
182
                    (filter.value === "inactive" && row[filter.id])
183
                  );
184
                }
185
              },
186
              Filter: selectFilter,
187
              filterOptions: [
188
                {
189
                  value: "active",
190
                  text: `${I18n.t("roles.active")} (${this.state.data.counts.active})`,
191
                },
192
                {
193
                  value: "inactive",
194
                  text: `${I18n.t("roles.inactive")} (${this.state.data.counts.inactive})`,
195
                },
196
              ],
197
              filterAllOptionText: `${I18n.t("all")} (${this.state.data.counts.all})`,
198
            },
199
            {
200
              Header: I18n.t("actions"),
201
              accessor: "_id",
202
              Cell: data => (
203
                <>
3✔
204
                  <span>
205
                    <a
206
                      href={Routes.edit_course_student_path(this.props.course_id, data.value)}
207
                      aria-label={I18n.t("edit")}
208
                      title={I18n.t("edit")}
209
                    >
210
                      <FontAwesomeIcon icon={faPencil} />
211
                    </a>
212
                  </span>
213
                  &nbsp;|&nbsp;
214
                  <span>
215
                    <a
216
                      href="#"
217
                      onClick={() => this.removeStudent(data.value)}
1✔
218
                      aria-label={I18n.t("remove")}
219
                      title={I18n.t("remove")}
220
                    >
221
                      <FontAwesomeIcon icon={faTrashCan} />
222
                    </a>
223
                  </span>
224
                </>
225
              ),
226
              sortable: false,
227
              filterable: false,
228
            },
229
          ]}
230
          noDataText={I18n.t("students.empty_table")}
231
          getNoDataProps={() => ({
29✔
232
            loading,
233
            data,
234
          })}
235
          defaultSorted={[
236
            {
237
              id: "user_name",
238
            },
239
          ]}
240
          filterable
241
          {...this.props.getCheckboxProps()}
242
        />
243
      </div>
244
    );
245
  }
246
}
247

248
class StudentsActionBox extends React.Component {
249
  constructor() {
250
    super();
26✔
251
    this.state = {
26✔
252
      action: "give_grace_credits",
253
      grace_credits: 0,
254
      selected_section: "",
255
      button_disabled: false,
256
    };
257
  }
258

259
  inputChanged = event => {
26✔
UNCOV
260
    this.setState({[event.target.name]: event.target.value});
×
261
  };
262

263
  actionChanged = event => {
26✔
264
    this.setState({action: event.target.value});
3✔
265
  };
266

267
  render = () => {
26✔
268
    let optionalInputBox = null;
34✔
269
    if (this.state.action === "give_grace_credits") {
34✔
270
      optionalInputBox = (
32✔
271
        <input
272
          type="number"
273
          name="grace_credits"
274
          value={this.state.grace_credits}
275
          onChange={this.inputChanged}
276
        />
277
      );
278
    } else if (this.state.action === "update_section") {
2!
279
      if (Object.keys(this.props.sections).length > 0) {
2✔
280
        const section_options = Object.entries(this.props.sections).map(section => (
1✔
281
          <option key={section[0]} value={section[0]}>
2✔
282
            {section[1]}
283
          </option>
284
        ));
285
        optionalInputBox = (
1✔
286
          <select
287
            name="section"
288
            value={this.state.section}
289
            onChange={this.inputChanged}
290
            data-testid={"student_action_box_update_section"}
291
          >
292
            <option key={"none"} value={""}>
293
              {I18n.t("students.instructor_actions.no_section")}
294
            </option>
295
            {section_options}
296
          </select>
297
        );
298
      } else {
299
        optionalInputBox = <span>{I18n.t("sections.none")}</span>;
1✔
300
      }
301
    }
302

303
    return (
34✔
304
      <form onSubmit={this.props.onSubmit} data-testid={"student_action_box"}>
305
        <select
306
          value={this.state.action}
307
          onChange={this.actionChanged}
308
          data-testid={"student_action_box_select"}
309
        >
310
          <option value="give_grace_credits">
311
            {I18n.t("students.instructor_actions.give_grace_credits")}
312
          </option>
313
          <option value="update_section">
314
            {I18n.t("students.instructor_actions.update_section")}
315
          </option>
316
          <option value="hide">{I18n.t("students.instructor_actions.mark_inactive")}</option>
317
          <option value="unhide">{I18n.t("students.instructor_actions.mark_active")}</option>
318
        </select>
319
        {optionalInputBox}
320
        <input type="submit" disabled={this.props.disabled} value={I18n.t("apply")} />
321
        <input type="hidden" name="authenticity_token" value={this.props.authenticity_token} />
322
      </form>
323
    );
324
  };
325
}
326

327
StudentsActionBox.propTypes = {
2✔
328
  onSubmit: PropTypes.func,
329
  disabled: PropTypes.bool,
330
  authenticity_token: PropTypes.string,
331
  sections: PropTypes.object,
332
};
333

334
RawStudentTable.propTypes = {
2✔
335
  course_id: PropTypes.number,
336
  selection: PropTypes.array.isRequired,
337
  authenticity_token: PropTypes.string,
338
  getCheckboxProps: PropTypes.func.isRequired,
339
};
340

341
let StudentTable = withSelection(RawStudentTable);
2✔
342

343
function makeStudentTable(elem, props) {
344
  const root = createRoot(elem);
×
UNCOV
345
  root.render(<StudentTable {...props} />);
×
346
}
347

348
export {StudentTable, StudentsActionBox, makeStudentTable};
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