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

MarkUsProject / Markus / 17864546036

19 Sep 2025 04:49PM UTC coverage: 91.83% (+0.004%) from 91.826%
17864546036

Pull #7663

github

web-flow
Merge d8ebcdb65 into 857462fab
Pull Request #7663: Convert Create Group modal to React component

693 of 1482 branches covered (46.76%)

Branch coverage included in aggregate %.

12 of 13 new or added lines in 1 file covered. (92.31%)

46 existing lines in 5 files now uncovered.

42302 of 45338 relevant lines covered (93.3%)

119.16 hits per line

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

88.37
/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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

347
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