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

MarkUsProject / Markus / 13122420635

03 Feb 2025 08:39PM UTC coverage: 91.849% (-0.02%) from 91.865%
13122420635

Pull #7393

github

web-flow
Merge f9da04898 into 0597f5222
Pull Request #7393: update remote_autotest_settings_id validation

624 of 1361 branches covered (45.85%)

Branch coverage included in aggregate %.

23 of 23 new or added lines in 2 files covered. (100.0%)

85 existing lines in 11 files now uncovered.

41281 of 44263 relevant lines covered (93.26%)

120.26 hits per line

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

41.8
/app/javascript/Components/starter_file_manager.jsx
1
import React, {Fragment} from "react";
2
import {createRoot} from "react-dom/client";
3
import FileManager from "./markus_file_manager";
4
import FileUploadModal from "./Modals/file_upload_modal";
5
import ReactTable from "react-table";
6
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
7
import {flashMessage} from "../common/flash";
8

9
function blurOnEnter(event) {
10
  if (event.key === "Enter") {
×
11
    document.activeElement.blur();
×
12
  }
13
}
14

15
class StarterFileManager extends React.Component {
16
  constructor(props) {
17
    super(props);
10✔
18
    this.state = {
10✔
19
      loading: true,
20
      dirUploadTarget: undefined,
21
      groupUploadTarget: undefined,
22
      showFileUploadModal: false,
23
      starterfileType: "simple",
24
      defaultStarterFileGroup: "",
25
      files: {},
26
      sections: {},
27
      form_changed: false,
28
      available_after_due: true,
29
    };
30
  }
31

32
  componentDidMount() {
33
    this.fetchData();
10✔
34
  }
35

36
  toggleFormChanged = value => {
10✔
37
    this.setState({form_changed: value});
×
38
  };
39

40
  fetchData = () => {
10✔
41
    fetch(
10✔
42
      Routes.populate_starter_file_manager_course_assignment_path(
43
        this.props.course_id,
44
        this.props.assignment_id
45
      ),
46
      {headers: {Accept: "application/json"}}
47
    )
48
      .then(reponse => {
49
        if (reponse.ok) {
10!
50
          return reponse.json();
10✔
51
        }
52
      })
53
      .then(res => this.setState({loading: false, ...res}));
10✔
54
  };
55

56
  createStarterFileGroup = () => {
10✔
57
    $.post({
×
58
      url: Routes.course_assignment_starter_file_groups_path(
59
        this.props.course_id,
60
        this.props.assignment_id
61
      ),
62
    }).then(this.fetchData);
63
  };
64

65
  deleteStarterFileGroup = starter_file_group_id => {
10✔
66
    $.ajax({
×
67
      url: Routes.course_starter_file_group_path(this.props.course_id, starter_file_group_id),
68
      method: "DELETE",
69
    }).then(this.fetchData);
70
  };
71

72
  handleDeleteFile = (groupUploadTarget, fileKeys) => {
10✔
73
    $.post({
×
74
      url: Routes.update_files_course_starter_file_group_path(
75
        this.props.course_id,
76
        groupUploadTarget
77
      ),
78
      data: {delete_files: fileKeys},
79
    })
80
      .then(() => this.setState({groupUploadTarget: undefined}))
×
81
      .then(this.fetchData);
82
  };
83

84
  handleCreateFiles = (groupUploadTarget, files, path, unzip) => {
10✔
85
    const prefix = path || this.state.dirUploadTarget || "";
×
86
    let data = new FormData();
×
87
    Array.from(files).forEach(f => data.append("new_files[]", f, f.name));
×
88
    data.append("path", prefix);
×
89
    data.append("unzip", unzip);
×
90
    $.post({
×
91
      url: Routes.update_files_course_starter_file_group_path(
92
        this.props.course_id,
93
        groupUploadTarget
94
      ),
95
      data: data,
96
      processData: false, // tell jQuery not to process the data
97
      contentType: false, // tell jQuery not to set contentType
98
    })
99
      .then(this.fetchData)
100
      .fail(jqXHR => {
101
        if (jqXHR.getResponseHeader("x-message-error") == null) {
×
102
          flashMessage(I18n.t("upload_errors.generic"), "error");
×
103
        }
104
      })
105
      .always(() =>
106
        this.setState({
×
107
          showFileUploadModal: false,
108
          dirUploadTarget: undefined,
109
          groupUploadTarget: undefined,
110
        })
111
      );
112
  };
113

114
  handleCreateFolder = (groupUploadTarget, folderKey) => {
10✔
115
    $.post({
×
116
      url: Routes.update_files_course_starter_file_group_path(
117
        this.props.course_id,
118
        groupUploadTarget
119
      ),
120
      data: {new_folders: [folderKey]},
121
    }).then(this.fetchData);
122
  };
123

124
  handleDeleteFolder = (groupUploadTarget, folderKeys) => {
10✔
125
    $.post({
×
126
      url: Routes.update_files_course_starter_file_group_path(
127
        this.props.course_id,
128
        groupUploadTarget
129
      ),
130
      data: {delete_folders: folderKeys},
131
    }).then(this.fetchData);
132
  };
133

134
  openUploadModal = (groupUploadTarget, uploadTarget) => {
10✔
135
    this.setState({
×
136
      showFileUploadModal: true,
137
      dirUploadTarget: uploadTarget,
138
      groupUploadTarget: groupUploadTarget,
139
    });
140
  };
141

142
  changeGroupName = (groupUploadTarget, original_name, event) => {
10✔
143
    const new_name = event.target.value;
×
144
    if (!!new_name && original_name !== new_name) {
×
145
      $.ajax({
×
146
        type: "PUT",
147
        url: Routes.course_starter_file_group_path(this.props.course_id, groupUploadTarget),
148
        data: {name: new_name},
149
      }).then(this.fetchData);
150
    }
151
  };
152

153
  saveStateChanges = () => {
10✔
154
    const data = {
×
155
      assignment: {
156
        starter_file_type: this.state.starterfileType,
157
        default_starter_file_group_id: this.state.defaultStarterFileGroup,
158
        starter_files_after_due: this.state.available_after_due,
159
      },
160
      sections: this.state.sections,
161
      starter_file_groups: this.state.files.map(data => {
162
        let {files, ...rest} = data;
×
163
        return rest;
×
164
      }),
165
    };
166
    $.ajax({
×
167
      type: "PUT",
168
      url: Routes.update_starter_file_course_assignment_path(
169
        this.props.course_id,
170
        this.props.assignment_id
171
      ),
172
      data: JSON.stringify(data),
173
      processData: false,
174
      contentType: "application/json",
175
    })
176
      .then(this.fetchData)
177
      .then(() => this.toggleFormChanged(false));
×
178
  };
179

180
  changeGroupRename = (groupUploadTarget, new_name) => {
10✔
181
    this.setState(
×
182
      prevState => {
183
        let new_files = prevState.files.map(files => {
×
184
          if (files.id === groupUploadTarget) {
×
185
            files.entry_rename = new_name;
×
186
          }
187
          return files;
×
188
        });
189
        return {files: new_files};
×
190
      },
191
      () => this.toggleFormChanged(true)
×
192
    );
193
  };
194

195
  changeGroupUseRename = (groupUploadTarget, checked) => {
10✔
196
    this.setState(
×
197
      prevState => {
198
        let new_files = prevState.files.map(files => {
×
199
          if (files.id === groupUploadTarget) {
×
200
            files.use_rename = checked;
×
201
          }
202
          return files;
×
203
        });
204
        return {files: new_files};
×
205
      },
206
      () => this.toggleFormChanged(true)
×
207
    );
208
  };
209

210
  renderFileManagers = () => {
10✔
211
    return (
20✔
212
      <React.Fragment>
213
        {Object.entries(this.state.files).map(data => {
214
          const {id, name, files} = data[1];
10✔
215
          return (
10✔
216
            <fieldset key={id}>
217
              <legend>
218
                <StarterFileGroupName
219
                  name={name}
220
                  groupUploadTarget={id}
221
                  changeGroupName={this.changeGroupName}
222
                />
223
              </legend>
224
              <StarterFileFileManager
225
                groupUploadTarget={id}
226
                files={files}
227
                noFilesMessage={I18n.t("submissions.no_files_available")}
228
                readOnly={this.props.read_only}
229
                onCreateFiles={this.handleCreateFiles}
230
                onDeleteFile={this.handleDeleteFile}
231
                onCreateFolder={this.handleCreateFolder}
232
                onRenameFolder={
233
                  typeof this.handleCreateFolder === "function" ? () => {} : undefined
10!
234
                }
235
                onDeleteFolder={this.handleDeleteFolder}
236
                onActionBarAddFileClick={this.openUploadModal}
237
                downloadAllURL={Routes.download_files_course_starter_file_group_path(
238
                  this.props.course_id,
239
                  id
240
                )}
241
                disableActions={{rename: true}}
242
                canFilter={false}
243
              />
244
              <button
245
                aria-label={I18n.t("assignments.starter_file.aria_labels.action_button")}
246
                key={"delete_starter_file_group_button"}
247
                className={"button"}
248
                onClick={() => this.deleteStarterFileGroup(id)}
×
249
                disabled={this.props.read_only}
250
              >
251
                <FontAwesomeIcon icon="fa-solid fa-trash" />
252
              </button>
253
            </fieldset>
254
          );
255
        })}
256
      </React.Fragment>
257
    );
258
  };
259

260
  updateSectionStarterFile = event => {
10✔
261
    let [section_id, group_id] = event.target.value.split("_").map(val => {
×
262
      return parseInt(val) || null;
×
263
    });
264
    this.setState(
×
265
      prevState => {
266
        let new_sections = prevState.sections.map(section => {
×
267
          if (section.section_id === section_id) {
×
268
            section.group_id = group_id;
×
269
          }
270
          return section;
×
271
        });
272
        return {sections: new_sections};
×
273
      },
274
      () => this.toggleFormChanged(true)
×
275
    );
276
  };
277

278
  renderStarterFileTypes = () => {
10✔
279
    return (
20✔
280
      <div>
281
        <p>
282
          <label>
283
            <input
284
              type={"radio"}
285
              name={"starter_file_type"}
286
              value={"simple"}
287
              checked={this.state.starterfileType === "simple"}
288
              disabled={!this.state.files.length || this.props.read_only}
30✔
289
              onChange={() => {
290
                this.setState({starterfileType: "simple"}, () => this.toggleFormChanged(true));
×
291
              }}
292
            />
293
            {I18n.t("assignments.starter_file.starter_file_rule_types.simple")}
294
          </label>
295
        </p>
296
        <p>
297
          <label>
298
            <input
299
              type={"radio"}
300
              name={"starter_file_type"}
301
              value={"sections"}
302
              checked={this.state.starterfileType === "sections"}
303
              disabled={!this.state.files.length || this.props.read_only}
30✔
304
              onChange={() => {
305
                this.setState({starterfileType: "sections"}, () => this.toggleFormChanged(true));
×
306
              }}
307
            />
308
            {I18n.t("assignments.starter_file.starter_file_rule_types.sections")}
309
          </label>
310
        </p>
311
        <p>
312
          <label>
313
            <input
314
              type={"radio"}
315
              name={"starter_file_type"}
316
              value={"group"}
317
              checked={this.state.starterfileType === "group"}
318
              disabled={!this.state.files.length || this.props.read_only}
30✔
319
              onChange={() => {
320
                this.setState({starterfileType: "group"}, () => this.toggleFormChanged(true));
×
321
              }}
322
            />
323
            {I18n.t("assignments.starter_file.starter_file_rule_types.group")}
324
          </label>
325
        </p>
326
        <p>
327
          <label>
328
            <input
329
              type={"radio"}
330
              name={"starter_file_type"}
331
              value={"shuffle"}
332
              checked={this.state.starterfileType === "shuffle"}
333
              disabled={!this.state.files.length || this.props.read_only}
30✔
334
              onChange={() => {
335
                this.setState({starterfileType: "shuffle"}, () => this.toggleFormChanged(true));
×
336
              }}
337
            />
338
            {I18n.t("assignments.starter_file.starter_file_rule_types.shuffle")}
339
          </label>
340
        </p>
341
      </div>
342
    );
343
  };
344

345
  renderStarterFileAssigner = () => {
10✔
346
    if (["simple", "sections"].includes(this.state.starterfileType)) {
20!
347
      let default_selector = (
348
        <label>
20✔
349
          {I18n.t("assignments.starter_file.default_starter_file_group")}
350
          <select
351
            onChange={e =>
352
              this.setState({defaultStarterFileGroup: parseInt(e.target.value)}, () =>
×
353
                this.toggleFormChanged(true)
×
354
              )
355
            }
356
            aria-label={I18n.t("assignments.starter_file.aria_labels.dropdown")}
357
            value={this.state.defaultStarterFileGroup}
358
            disabled={!this.state.files.length || this.props.read_only}
30✔
359
          >
360
            {Object.entries(this.state.files).map(data => {
361
              const {id, name} = data[1];
10✔
362
              return (
10✔
363
                <option value={id} key={id}>
364
                  {name}
365
                </option>
366
              );
367
            })}
368
          </select>
369
        </label>
370
      );
371

372
      let section_table = "";
20✔
373
      if (this.state.starterfileType === "sections") {
20✔
374
        section_table = (
10✔
375
          <ReactTable
376
            columns={[
377
              {
378
                Header: I18n.t("activerecord.models.section.one"),
379
                accessor: "section_name",
380
              },
381
              {
382
                Header: I18n.t("activerecord.models.starter_file_group.one"),
383
                Cell: row => {
384
                  let selected = `${row.original.section_id}_${row.original.group_id || ""}`;
20!
385
                  return (
20✔
386
                    <select
387
                      onChange={this.updateSectionStarterFile}
388
                      value={selected}
389
                      disabled={!this.state.files.length || this.props.read_only}
40✔
390
                      aria-label={I18n.t("assignments.starter_file.aria_labels.dropdown")}
391
                    >
392
                      <option value={`${row.original.section_id}_`} />
393
                      {Object.entries(this.state.files).map(data => {
394
                        const {id, name} = data[1];
20✔
395
                        const value = `${row.original.section_id}_${id}`;
20✔
396
                        return (
20✔
397
                          <option value={value} key={id}>
398
                            {name}
399
                          </option>
400
                        );
401
                      })}
402
                    </select>
403
                  );
404
                },
405
              },
406
            ]}
407
            data={this.state.sections}
408
          />
409
        );
410
      }
411
      return (
20✔
412
        <div>
413
          <p>{default_selector}</p>
414
          {section_table}
415
        </div>
416
      );
417
    }
418
    return "";
×
419
  };
420

421
  renderStarterFileRenamer = () => {
10✔
422
    if (this.state.starterfileType === "shuffle") {
20!
423
      return (
×
424
        <ReactTable
425
          columns={[
426
            {
427
              Header: I18n.t("activerecord.models.starter_file_group.one"),
428
              Cell: row => row.original.name,
×
429
            },
430
            {
431
              Header: I18n.t("assignments.starter_file.rename"),
432
              Cell: row => {
433
                return (
×
434
                  <StarterFileEntryRenameInput
435
                    entry_rename={row.original.entry_rename}
436
                    use_rename={row.original.use_rename}
437
                    groupUploadTarget={row.original.id}
438
                    changeGroupRename={this.changeGroupRename}
439
                    changeGroupUseRename={this.changeGroupUseRename}
440
                  />
441
                );
442
              },
443
            },
444
          ]}
445
          data={this.state.files}
446
        />
447
      );
448
    }
449
    return "";
20✔
450
  };
451

452
  renderVisibilityOptions = () => {
10✔
453
    return (
20✔
454
      <Fragment>
455
        <label>
456
          <input
457
            type={"checkbox"}
458
            checked={this.state.available_after_due}
459
            data-testid="available_after_due_checkbox"
460
            onChange={() => {
461
              this.setState(
×
462
                prev => ({available_after_due: !prev.available_after_due}),
×
463
                () => this.toggleFormChanged(true)
×
464
              );
465
            }}
466
            disabled={this.props.read_only}
467
          />
468
          {I18n.t("assignments.starter_file.available_after_due")}
469
        </label>
470
        <div className="inline-help">
471
          <p>{I18n.t("assignments.starter_file.available_after_due_help")}</p>
472
        </div>
473
      </Fragment>
474
    );
475
  };
476

477
  render() {
478
    return (
20✔
479
      <div>
480
        <fieldset>
481
          <legend>
482
            <span>{I18n.t("activerecord.models.starter_file_group.other")}</span>
483
          </legend>
484
          {this.renderFileManagers()}
485
          <button
486
            aria-label={I18n.t("assignments.starter_file.aria_labels.action_button")}
487
            key={"create_starter_file_group_button"}
488
            className={"button"}
489
            onClick={this.createStarterFileGroup}
490
            disabled={this.props.read_only}
491
          >
492
            <FontAwesomeIcon icon="fa-solid fa-add" />
493
          </button>
494
          <StarterFileFileUploadModal
495
            groupUploadTarget={this.state.groupUploadTarget}
496
            isOpen={this.state.showFileUploadModal}
497
            onRequestClose={() =>
498
              this.setState({
×
499
                showFileUploadModal: false,
500
                groupUploadTarget: undefined,
501
                dirUploadTarget: undefined,
502
              })
503
            }
504
            onSubmit={this.handleCreateFiles}
505
          />
506
        </fieldset>
507
        <fieldset className={"starter-file-rule-types"}>
508
          <legend>
509
            <span>{I18n.t("assignments.starter_file.starter_file_rule")}</span>
510
          </legend>
511
          <div className={"title_bar"}>
512
            <div className={"float-right"}>
513
              <a
514
                href={Routes.download_starter_file_mappings_course_assignment_path(
515
                  this.props.course_id,
516
                  this.props.assignment_id
517
                )}
518
              >
519
                {I18n.t("assignments.starter_file.download_mappings_csv")}
520
              </a>
521
              <span className={"menu_bar"} />
522
              <a
523
                href={Routes.download_sample_starter_files_course_assignment_path(
524
                  this.props.course_id,
525
                  this.props.assignment_id
526
                )}
527
              >
528
                {I18n.t("assignments.starter_file.download_sample_starter_files")}
529
              </a>
530
            </div>
531
          </div>
532
          {this.renderStarterFileTypes()}
533
          {this.renderStarterFileAssigner()}
534
          {this.renderStarterFileRenamer()}
535
          {this.renderVisibilityOptions()}
536
          <p>
537
            <input
538
              type={"submit"}
539
              value={I18n.t("save")}
540
              data-testid={"save_button"}
541
              onClick={this.saveStateChanges}
542
              disabled={!this.state.form_changed || this.props.read_only}
25✔
543
            ></input>
544
          </p>
545
        </fieldset>
546
      </div>
547
    );
548
  }
549
}
550

551
class StarterFileGroupName extends React.Component {
552
  constructor(props) {
553
    super(props);
10✔
554
    this.state = {
10✔
555
      editing: false,
556
    };
557
  }
558

559
  handleBlur = event => {
10✔
560
    this.setState(
×
561
      {editing: false},
562
      this.props.changeGroupName(this.props.groupUploadTarget, this.props.name, event)
563
    );
564
  };
565

566
  render() {
567
    if (this.state.editing) {
10!
568
      return (
×
569
        <input
570
          autoFocus
571
          type={"text"}
572
          placeholder={this.props.name}
573
          onKeyPress={blurOnEnter}
574
          onBlur={this.handleBlur}
575
        />
576
      );
577
    } else {
578
      return (
10✔
579
        <h3>
580
          <a
581
            href={"#"}
582
            onClick={() => {
583
              this.setState({editing: true});
×
584
            }}
585
          >
586
            {this.props.name}
587
          </a>
588
        </h3>
589
      );
590
    }
591
  }
592
}
593

594
class StarterFileEntryRenameInput extends React.Component {
595
  handleBlur = event => {
×
596
    this.props.changeGroupRename(this.props.groupUploadTarget, event.target.value);
×
597
  };
598

599
  handleClick = event => {
×
600
    this.props.changeGroupUseRename(this.props.groupUploadTarget, !event.target.checked);
×
601
  };
602

603
  render() {
604
    return (
×
605
      <span className={"starter-file-rename-cell-content"}>
606
        <input
607
          type={"text"}
608
          placeholder={this.props.entry_rename}
609
          onKeyPress={blurOnEnter}
610
          onBlur={this.handleBlur}
611
          disabled={!this.props.use_rename || this.props.disabled}
×
612
        />
613
        <label className={"float-right"}>
614
          <input
615
            type={"checkbox"}
616
            onChange={this.handleClick}
617
            checked={!this.props.use_rename || this.props.disabled}
×
618
          />
619
          {I18n.t("assignments.starter_file.use_original_filename")}
620
        </label>
621
      </span>
622
    );
623
  }
624
}
625

626
class StarterFileFileUploadModal extends React.Component {
627
  onSubmit = (...args) => {
10✔
628
    return this.props.onSubmit(this.props.groupUploadTarget, ...args);
×
629
  };
630

631
  render() {
632
    return <FileUploadModal {...this.props} onSubmit={this.onSubmit} />;
20✔
633
  }
634
}
635

636
class StarterFileFileManager extends React.Component {
637
  overridenProps = () => {
10✔
638
    return {
10✔
639
      onDeleteFile: (...args) => this.props.onDeleteFile(this.props.groupUploadTarget, ...args),
×
640
      onCreateFolder: (...args) => this.props.onCreateFolder(this.props.groupUploadTarget, ...args),
×
641
      onDeleteFolder: (...args) => this.props.onDeleteFolder(this.props.groupUploadTarget, ...args),
×
642
      onCreateFiles: (...args) => this.props.onCreateFiles(this.props.groupUploadTarget, ...args),
×
643
      onActionBarAddFileClick: (...args) =>
644
        this.props.onActionBarAddFileClick(this.props.groupUploadTarget, ...args),
×
645
    };
646
  };
647

648
  render() {
649
    return <FileManager {...{...this.props, ...this.overridenProps()}} />;
10✔
650
  }
651
}
652

653
export function makeStarterFileManager(elem, props) {
654
  const root = createRoot(elem);
×
UNCOV
655
  root.render(<StarterFileManager {...props} />);
×
656
}
657

658
export {StarterFileManager};
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