• 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

51.25
/app/javascript/Components/submission_file_manager.jsx
1
import React from "react";
2
import {createRoot} from "react-dom/client";
3
import FileManager from "./markus_file_manager";
4
import SubmissionFileUploadModal from "./Modals/submission_file_upload_modal";
5
import SubmitUrlUploadModal from "./Modals/submission_url_submit_modal";
6
import {FileViewer} from "./Result/file_viewer";
7
import mime from "mime/lite";
8
import {flashMessage} from "../common/flash";
9

10
class SubmissionFileManager extends React.Component {
11
  constructor(props) {
12
    super(props);
6✔
13
    this.state = {
6✔
14
      files: [],
15
      showUploadModal: false,
16
      showURLModal: false,
17
      uploadTarget: undefined,
18
      viewFile: null,
19
      viewFileType: null,
20
      viewFileURL: null,
21
      onlyRequiredFiles: false,
22
      requiredFiles: [],
23
      maxFileSize: 0,
24
      numberOfMissingFiles: 0,
25
      uploadModalProgressVisible: false,
26
      uploadModalProgressPercentage: 0.0,
27
    };
28
  }
29

30
  static defaultProps = {
2✔
31
    fetchOnMount: true,
32
    readOnly: false,
33
    revision_identifier: undefined,
34
    starterFileChanged: false,
35
  };
36

37
  componentDidMount() {
38
    if (this.props.fetchOnMount) {
6!
39
      this.fetchData();
6✔
40
    }
41
  }
42

43
  fetchData = () => {
6✔
44
    let data = {course_id: this.props.course_id, assignment_id: this.props.assignment_id};
6✔
45
    if (typeof this.props.grouping_id !== "undefined") {
6!
46
      data.grouping_id = this.props.grouping_id;
×
47
    }
48
    if (typeof this.props.revision_identifier !== "undefined") {
6!
49
      data.revision_identifier = this.props.revision_identifier;
×
50
    }
51

52
    fetch(Routes.populate_file_manager_course_assignment_submissions_path(data), {
6✔
53
      credentials: "same-origin",
54
      headers: {
55
        "content-type": "application/json",
56
      },
57
    })
58
      .then(data => data.json())
6✔
59
      .then(data =>
60
        this.setState({
6✔
61
          files: data.entries,
62
          viewFile: null,
63
          viewFileType: null,
64
          viewFileURL: null,
65
          onlyRequiredFiles: data.only_required_files,
66
          requiredFiles: data.required_files,
67
          maxFileSize: data.max_file_size,
68
          numberOfMissingFiles: data.number_of_missing_files,
69
        })
70
      );
71
  };
72

73
  // Update state when a new revision_identifier props is passed
74
  componentDidUpdate(oldProps) {
75
    if (oldProps.revision_identifier !== this.props.revision_identifier) {
18!
76
      this.fetchData();
×
77
    }
78
  }
79

80
  handleCreateUrl = (url, url_text) => {
6✔
81
    this.setState({showURLModal: false});
×
82
    const data_to_upload = {
×
83
      new_url: url,
84
      url_text: url_text,
85
      path: "/" + (this.state.uploadTarget || ""),
×
86
    };
87
    if (this.props.grouping_id) {
×
88
      data_to_upload.grouping_id = this.props.grouping_id;
×
89
    }
90
    $.post({
×
91
      url: Routes.update_files_course_assignment_submissions_path(
92
        this.props.course_id,
93
        this.props.assignment_id
94
      ),
95
      data: data_to_upload,
96
    })
97
      .then(typeof this.props.onChange === "function" ? this.props.onChange : this.fetchData)
×
98
      .then(this.endAction);
99
  };
100

101
  handleCreateFiles = (files, path, unzip, renameTo = "") => {
6!
102
    if (
4!
103
      !this.props.starterFileChanged ||
4!
104
      confirm(I18n.t("assignments.starter_file.upload_confirmation"))
105
    ) {
106
      this.setState({uploadModalProgressVisible: true});
4✔
107

108
      const prefix = path || this.state.uploadTarget || "";
4✔
109
      let data = new FormData();
4✔
110
      if (!!renameTo && files.length === 1) {
4!
111
        Array.from(files).forEach(f => data.append("new_files[]", f, renameTo));
×
112
      } else {
113
        Array.from(files).forEach(f => data.append("new_files[]", f, f.name));
4✔
114
      }
115
      data.append("path", "/" + prefix); // Server expects path with leading slash (TODO: fix that)
4✔
116
      if (this.props.grouping_id) {
4!
117
        data.append("grouping_id", this.props.grouping_id);
×
118
      }
119
      data.append("unzip", unzip);
4✔
120

121
      $.post({
4✔
122
        url: Routes.update_files_course_assignment_submissions_path(
123
          this.props.course_id,
124
          this.props.assignment_id
125
        ),
126
        data: data,
127
        processData: false, // tell jQuery not to process the data
128
        contentType: false, // tell jQuery not to set contentType
129
        xhr: () => {
130
          const xhr = new XMLHttpRequest();
3✔
131

132
          xhr.upload.addEventListener(
3✔
133
            "progress",
134
            event => {
135
              if (event.lengthComputable) {
3!
136
                this.setState({uploadModalProgressPercentage: (event.loaded / event.total) * 100});
3✔
137
              }
138
            },
139
            false
140
          );
141

142
          return xhr;
3✔
143
        },
144
      })
145
        .then(typeof this.props.onChange === "function" ? this.props.onChange : this.fetchData)
4!
146
        .then(this.endAction)
147
        .fail(jqXHR => {
148
          if (jqXHR.getResponseHeader("x-message-error") == null) {
×
149
            flashMessage(I18n.t("upload_errors.generic"), "error");
×
150
          }
151
        })
152
        .always(() => {
153
          this.setState({
1✔
154
            showUploadModal: false,
155
            uploadTarget: undefined,
156
            uploadModalProgressVisible: false,
157
            uploadModalProgressPercentage: 0.0,
158
          });
159
        });
160
    }
161
  };
162

163
  handleDeleteFile = fileKeys => {
6✔
164
    if (!this.state.files.some(f => fileKeys.includes(f.key))) {
×
165
      return;
×
166
    }
167

168
    $.post({
×
169
      url: Routes.update_files_course_assignment_submissions_path(
170
        this.props.course_id,
171
        this.props.assignment_id
172
      ),
173
      data: {
174
        delete_files: fileKeys,
175
        grouping_id: this.props.grouping_id,
176
      },
177
    })
178
      .then(
179
        typeof this.props.onChange === "function"
×
180
          ? () =>
181
              this.setState(
×
182
                {viewFile: null, viewFileType: null, viewFileURL: null},
183
                this.props.onChange
184
              )
185
          : this.fetchData
186
      )
187
      .then(this.endAction);
188
  };
189

190
  handleCreateFolder = folderKey => {
6✔
191
    if (
×
192
      !this.props.starterFileChanged ||
×
193
      confirm(I18n.t("assignments.starter_file.upload_confirmation"))
194
    ) {
195
      $.post({
×
196
        url: Routes.update_files_course_assignment_submissions_path(
197
          this.props.course_id,
198
          this.props.assignment_id
199
        ),
200
        data: {
201
          new_folders: [folderKey],
202
          grouping_id: this.props.grouping_id,
203
        },
204
      })
205
        .then(typeof this.props.onChange === "function" ? this.props.onChange : this.fetchData)
×
206
        .then(this.endAction);
207
    }
208
  };
209

210
  handleDeleteFolder = folderKeys => {
6✔
211
    $.post({
×
212
      url: Routes.update_files_course_assignment_submissions_path(
213
        this.props.course_id,
214
        this.props.assignment_id
215
      ),
216
      data: {
217
        delete_folders: folderKeys,
218
        grouping_id: this.props.grouping_id,
219
      },
220
    })
221
      .then(typeof this.props.onChange === "function" ? this.props.onChange : this.fetchData)
×
222
      .then(this.endAction);
223
  };
224

225
  handleActionBarDeleteClick = event => {
6✔
226
    event.preventDefault();
×
227
    if (this.state.selection) {
×
228
      this.handleDeleteFile(this.state.selection);
×
229
    }
230
  };
231

232
  getDownloadAllURL = () => {
6✔
233
    return Routes.downloads_course_assignment_submissions_path(
24✔
234
      this.props.course_id,
235
      this.props.assignment_id,
236
      this.props.grouping_id,
237
      {
238
        revision_identifier: this.props.revision_identifier,
239
        grouping_id: this.props.grouping_id,
240
      }
241
    );
242
  };
243

244
  openUploadModal = uploadTarget => {
6✔
245
    this.setState({showUploadModal: true, uploadTarget: uploadTarget});
4✔
246
  };
247

248
  openSubmitURLModal = uploadTarget => {
6✔
249
    this.setState({showURLModal: true, uploadTarget: uploadTarget});
×
250
  };
251

252
  updateViewFile = item => {
6✔
253
    this.setState({
4✔
254
      viewFile: item.relativeKey,
255
      viewFileType: item.type,
256
      viewFileURL: item.url,
257
    });
258
  };
259

260
  renderFileViewer = () => {
6✔
261
    let heading;
262
    let content = "";
24✔
263
    if (this.state.viewFile !== null) {
24✔
264
      let withinSize =
265
        document.getElementById("content").getBoundingClientRect().width - 150 + "px";
4✔
266
      heading = this.state.viewFile;
4✔
267
      content = (
4✔
268
        <div
269
          id="codeviewer"
270
          className="text-viewer-container"
271
          style={{maxWidth: withinSize}}
272
          data-testid="file-preview-root"
273
        >
274
          <FileViewer
275
            assignment_id={this.props.assignment_id}
276
            grouping_id={this.props.grouping_id}
277
            revision_id={this.props.revision_identifier}
278
            selectedFile={this.state.viewFile}
279
            selectedFileType={this.state.viewFileType}
280
            selectedFileURL={this.state.viewFileURL}
281
            mime_type={mime.getType(this.state.viewFile)}
282
          />
283
        </div>
284
      );
285
    } else {
286
      heading = I18n.t("submissions.student.select_file");
20✔
287
    }
288

289
    return (
24✔
290
      <fieldset style={{display: "flex", flexDirection: "column"}}>
291
        <legend>
292
          <span>{heading}</span>
293
        </legend>
294
        {content}
295
      </fieldset>
296
    );
297
  };
298

299
  renderRequiredFiles = () => {
6✔
300
    let requiredFilesBox;
301

302
    if (this.state.requiredFiles.length > 0) {
24!
303
      requiredFilesBox = (
×
304
        <div>
305
          <h2>{I18n.t("activerecord.attributes.assignment.assignment_files")}</h2>
306
          <p>
307
            {this.state.numberOfMissingFiles === 0
×
308
              ? I18n.t("student.submission.all_files_submitted")
309
              : I18n.t("student.submission.missing_files", {file: this.state.numberOfMissingFiles})}
310
          </p>
311
          {this.state.requiredFiles.map(filename => {
312
            return (
×
313
              <p key={filename} className={"required-files-container"}>
314
                <input
315
                  className={"required-files-checkbox"}
316
                  type={"checkbox"}
317
                  disabled={true}
318
                  checked={this.state.files.some(element => element.key === filename)}
×
319
                  name={`required-file-${filename}`}
320
                  id={`required-file-${filename}`}
321
                />
322
                <label htmlFor={`required-file-${filename}`}>
323
                  &nbsp; {filename}
324
                  {this.state.files.some(element => element.key === filename) ? (
×
325
                    ""
326
                  ) : (
327
                    <strong>&nbsp; {I18n.t("submissions.student.missing")}</strong>
328
                  )}
329
                </label>
330
              </p>
331
            );
332
          })}
333
        </div>
334
      );
335
    } else {
336
      requiredFilesBox = "";
24✔
337
    }
338
    return (
24✔
339
      <div className={"pane-wrapper small-bottom-margin"}>
340
        <div className={"pane"}>
341
          {requiredFilesBox}
342
          {this.state.onlyRequiredFiles ? (
24!
343
            <p>{I18n.t("submissions.student.only_required_files")}</p>
344
          ) : (
345
            ""
346
          )}
347
          <p>
348
            {I18n.t("submissions.student.maximum_file_size", {file_size: this.state.maxFileSize})}
349
          </p>
350
        </div>
351
      </div>
352
    );
353
  };
354

355
  render() {
356
    return (
24✔
357
      <div>
358
        {this.renderRequiredFiles()}
359
        <FileManager
360
          files={this.state.files}
361
          noFilesMessage={I18n.t("submissions.no_files_available")}
362
          readOnly={this.props.readOnly}
363
          onDeleteFile={this.props.readOnly ? undefined : this.handleDeleteFile}
24!
364
          onCreateFiles={this.props.readOnly ? undefined : this.handleCreateFiles}
24!
365
          onCreateFolder={this.props.readOnly ? undefined : this.handleCreateFolder}
24!
366
          onRenameFolder={
367
            !this.props.readOnly && typeof this.handleCreateFolder === "function"
72!
368
              ? () => {}
369
              : undefined
370
          }
371
          onDeleteFolder={this.props.readOnly ? undefined : this.handleDeleteFolder}
24!
372
          downloadAllURL={this.getDownloadAllURL()}
373
          onActionBarAddFileClick={this.props.readOnly ? undefined : this.openUploadModal}
24!
374
          disableActions={{
375
            rename: true,
376
            addFolder: !this.props.enableSubdirs,
377
            deleteFolder: !this.props.enableSubdirs,
378
          }}
379
          onSelectFile={this.updateViewFile}
380
          enableUrlSubmit={this.props.enableUrlSubmit}
381
          onActionBarSubmitURLClick={this.props.readOnly ? undefined : this.openSubmitURLModal}
24!
382
          isSubmittingItems={true}
383
        />
384
        <SubmissionFileUploadModal
385
          isOpen={this.state.showUploadModal}
386
          onRequestClose={() => this.setState({showUploadModal: false, uploadTarget: undefined})}
×
387
          onSubmit={this.handleCreateFiles}
388
          progressVisible={this.state.uploadModalProgressVisible}
389
          progressPercentage={this.state.uploadModalProgressPercentage}
390
          onlyRequiredFiles={this.state.onlyRequiredFiles}
391
          requiredFiles={this.state.requiredFiles}
392
          uploadTarget={this.state.uploadTarget}
393
        />
394
        <SubmitUrlUploadModal
395
          isOpen={this.state.showURLModal}
396
          onRequestClose={() => this.setState({showURLModal: false})}
×
397
          onSubmit={this.handleCreateUrl}
398
        />
399
        {this.renderFileViewer()}
400
      </div>
401
    );
402
  }
403
}
404

405
export function makeSubmissionFileManager(elem, props) {
406
  const root = createRoot(elem);
×
UNCOV
407
  root.render(<SubmissionFileManager {...props} />);
×
408
}
409

410
export {SubmissionFileManager};
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