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

MarkUsProject / Markus / 14231858563

02 Apr 2025 11:54PM UTC coverage: 91.87% (-0.007%) from 91.877%
14231858563

Pull #7480

github

web-flow
Merge 0626f3558 into bc8c49fad
Pull Request #7480: Enable deletion of a student from a course

630 of 1371 branches covered (45.95%)

Branch coverage included in aggregate %.

43 of 48 new or added lines in 8 files covered. (89.58%)

15 existing lines in 1 file now uncovered.

41506 of 44494 relevant lines covered (93.28%)

120.28 hits per line

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

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

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

21
  componentDidMount() {
22
    this.fetchData();
17✔
23
  }
24

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

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

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

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

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

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

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

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

242
  inputChanged = event => {
24✔
243
    this.setState({[event.target.name]: event.target.value});
×
244
  };
245

246
  actionChanged = event => {
24✔
247
    this.setState({action: event.target.value});
3✔
248
  };
249

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

286
    return (
31✔
287
      <form onSubmit={this.props.onSubmit} data-testid={"student_action_box"}>
288
        <select
289
          value={this.state.action}
290
          onChange={this.actionChanged}
291
          data-testid={"student_action_box_select"}
292
        >
293
          <option value="give_grace_credits">
294
            {I18n.t("students.instructor_actions.give_grace_credits")}
295
          </option>
296
          <option value="update_section">
297
            {I18n.t("students.instructor_actions.update_section")}
298
          </option>
299
          <option value="hide">{I18n.t("students.instructor_actions.mark_inactive")}</option>
300
          <option value="unhide">{I18n.t("students.instructor_actions.mark_active")}</option>
301
        </select>
302
        {optionalInputBox}
303
        <input type="submit" disabled={this.props.disabled} value={I18n.t("apply")} />
304
        <input type="hidden" name="authenticity_token" value={this.props.authenticity_token} />
305
      </form>
306
    );
307
  };
308
}
309
StudentsActionBox.propTypes = {
2✔
310
  onSubmit: PropTypes.func,
311
  disabled: PropTypes.bool,
312
  authenticity_token: PropTypes.string,
313
  sections: PropTypes.object,
314
};
315

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

323
let StudentTable = withSelection(RawStudentTable);
2✔
324
function makeStudentTable(elem, props) {
325
  const root = createRoot(elem);
×
326
  root.render(<StudentTable {...props} />);
×
327
}
328
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

© 2025 Coveralls, Inc