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

MarkUsProject / Markus / 15466657172

05 Jun 2025 12:10PM UTC coverage: 91.953% (+0.001%) from 91.952%
15466657172

Pull #7557

github

web-flow
Merge 71c6473c1 into 055a2709b
Pull Request #7557: Design improvement for student, ta, and instructor tables

631 of 1371 branches covered (46.02%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

11 existing lines in 3 files now uncovered.

41944 of 44930 relevant lines covered (93.35%)

117.55 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 {tableNoDataComponent} from "./table_no_data";
8

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

22
  componentDidMount() {
23
    this.fetchData();
18✔
24
  }
25

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

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

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

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

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

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

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

233
class StudentsActionBox extends React.Component {
234
  constructor() {
235
    super();
25✔
236
    this.state = {
25✔
237
      action: "give_grace_credits",
238
      grace_credits: 0,
239
      selected_section: "",
240
      button_disabled: false,
241
    };
242
  }
243

244
  inputChanged = event => {
25✔
UNCOV
245
    this.setState({[event.target.name]: event.target.value});
×
246
  };
247

248
  actionChanged = event => {
25✔
249
    this.setState({action: event.target.value});
3✔
250
  };
251

252
  render = () => {
25✔
253
    let optionalInputBox = null;
33✔
254
    if (this.state.action === "give_grace_credits") {
33✔
255
      optionalInputBox = (
31✔
256
        <input
257
          type="number"
258
          name="grace_credits"
259
          value={this.state.grace_credits}
260
          onChange={this.inputChanged}
261
        />
262
      );
263
    } else if (this.state.action === "update_section") {
2!
264
      if (Object.keys(this.props.sections).length > 0) {
2✔
265
        const section_options = Object.entries(this.props.sections).map(section => (
1✔
266
          <option key={section[0]} value={section[0]}>
2✔
267
            {section[1]}
268
          </option>
269
        ));
270
        optionalInputBox = (
1✔
271
          <select
272
            name="section"
273
            value={this.state.section}
274
            onChange={this.inputChanged}
275
            data-testid={"student_action_box_update_section"}
276
          >
277
            <option key={"none"} value={""}>
278
              {I18n.t("students.instructor_actions.no_section")}
279
            </option>
280
            {section_options}
281
          </select>
282
        );
283
      } else {
284
        optionalInputBox = <span>{I18n.t("sections.none")}</span>;
1✔
285
      }
286
    }
287

288
    return (
33✔
289
      <form onSubmit={this.props.onSubmit} data-testid={"student_action_box"}>
290
        <select
291
          value={this.state.action}
292
          onChange={this.actionChanged}
293
          data-testid={"student_action_box_select"}
294
        >
295
          <option value="give_grace_credits">
296
            {I18n.t("students.instructor_actions.give_grace_credits")}
297
          </option>
298
          <option value="update_section">
299
            {I18n.t("students.instructor_actions.update_section")}
300
          </option>
301
          <option value="hide">{I18n.t("students.instructor_actions.mark_inactive")}</option>
302
          <option value="unhide">{I18n.t("students.instructor_actions.mark_active")}</option>
303
        </select>
304
        {optionalInputBox}
305
        <input type="submit" disabled={this.props.disabled} value={I18n.t("apply")} />
306
        <input type="hidden" name="authenticity_token" value={this.props.authenticity_token} />
307
      </form>
308
    );
309
  };
310
}
311

312
StudentsActionBox.propTypes = {
2✔
313
  onSubmit: PropTypes.func,
314
  disabled: PropTypes.bool,
315
  authenticity_token: PropTypes.string,
316
  sections: PropTypes.object,
317
};
318

319
RawStudentTable.propTypes = {
2✔
320
  course_id: PropTypes.number,
321
  selection: PropTypes.array.isRequired,
322
  authenticity_token: PropTypes.string,
323
  getCheckboxProps: PropTypes.func.isRequired,
324
};
325

326
let StudentTable = withSelection(RawStudentTable);
2✔
327

328
function makeStudentTable(elem, props) {
329
  const root = createRoot(elem);
×
UNCOV
330
  root.render(<StudentTable {...props} />);
×
331
}
332

333
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