Skip to content

Commit d107314

Browse files
jlgeevanyeyeye20wildmanjlykimcheedamianhxy
authored
Implement export course (#1887)
* Fix bug in GitHub api rate limit check (#1821) Fix buggy code * Update docker compose docs (#1823) * Update docker compose docs * Add make warning * Update Export / Import Assessment to support more fields, make importAsmtFromTar and importAssessment more robust (#1822) * - Lint ruby files within spec/ * Add more fields to yml serialization of assessment Add error checking to import assessments (still some errors) * add check to ensure asmt name is valid for import * remove redundant text * create assessment using factory bot, jank test for assessment export * - Add success flash to assessment import - Add a bunch of testcases for bad assessment imports - Modify create_course_with_many_students to handle custom assessment creation, do validation on assessment name * rubocop style * Jump to currently enrolled course (#1812) * Modifications for RuboCop style * Update Manage Submissions test specs to work regardless of jump to course logic --------- Co-authored-by: Damian Ho <[email protected]> * Bump rack from 2.2.6.2 to 2.2.6.3 (#1828) Bumps [rack](https://github.com/rack/rack) from 2.2.6.2 to 2.2.6.3. - [Release notes](https://github.com/rack/rack/releases) - [Changelog](https://github.com/rack/rack/blob/main/CHANGELOG.md) - [Commits](rack/rack@v2.2.6.2...v2.2.6.3) --- updated-dependencies: - dependency-name: rack dependency-type: indirect ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Fix bug where course gets created even if there are errors (#1820) * Add required fields to html for name and instructor email * Destroy course if instructor email is invalid * Change required syntax to favored form * Fix annotated PDF download when global annotation is present (#1833) Skip if coordinate is nil * Course start/end date nil checks (#1834) Add nil check for course start and end dates * Bump rack from 2.2.6.3 to 2.2.6.4 (#1835) Bumps [rack](https://github.com/rack/rack) from 2.2.6.3 to 2.2.6.4. - [Release notes](https://github.com/rack/rack/releases) - [Changelog](https://github.com/rack/rack/blob/main/CHANGELOG.md) - [Commits](rack/rack@v2.2.6.3...v2.2.6.4) --- updated-dependencies: - dependency-name: rack dependency-type: indirect ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Add LTI Configuration to "Manage Autolab" Dropdown, Update docs and gitignore (#1817) * begin updating lti integration documentation and add feature documentation * - update documentation for LTI configuration, linking, installation - add images for LTI linking for documentation - update gitignore, add ignore node_modules (for stylelint) * Revise LTI docs to make instructions more clear --------- Co-authored-by: Victor Huang <[email protected]> * Remove unused "Additional Submission Form" feature code (#1830) * Remove dead code * Update schema version * Fix thead alignment in manage submissions (#1838) * Add sticky to thead css * Remove js file that added a new thead element * Add export route * Add export option to manage course page * Add new stylesheet for export * Move export table to partial * Change id of checkboxes * Fix spacing in export page * Add table styling and checkbox spacing * Remove table header and make font bigger * Implement select all functionality * Fix select all styling when checked * Remove select all button * Fix style errors * Add new lines to eof * Fix style issues * Implement export course config * Add risk condition and watchlist configuration into yaml * Add attachments to export * Rubocop and add course.rb * Add error msg * Format * merge frontend and backend * rubcop * Add more error handling * Filter risk conditions to show only latest version * rubocop * Save actual late_penalty and version_penalty instead of id * Add render tests for export * Clean code 🧼🧼🧹🧹 * Make button repressable * Remove course id * Add more factories and helper functions * Add functionality tests for export_selected endpoint * Add dummy file for activatestorage attachment * Fix mistake in attachment * Rubocop * Comment out error handling for now * error handling tests * rubocop * Address comments * Add backwards compatibility for attachments * Address comment * Edit css to not affect breadcrumbs --------- Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: Evan Shi <[email protected]> Co-authored-by: Joey Wildman <[email protected]> Co-authored-by: lykimchee <[email protected]> Co-authored-by: Damian Ho <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Victor Huang <[email protected]>
1 parent a02478f commit d107314

File tree

13 files changed

+323
-11
lines changed

13 files changed

+323
-11
lines changed

app/assets/stylesheets/export.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ tr {
2929
font-weight: 400 !important;
3030
}
3131

32-
span {
32+
span:not(.left-nav):not(.item):not(.title) {
3333
padding-left: 10px !important;
34-
vertical-align: middle !important;
34+
vertical-align: middle;
3535
}

app/controllers/courses_controller.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,8 +599,24 @@ def run_moss
599599
`rm -rf #{tmp_dir}`
600600
end
601601

602+
action_auth_level :export, :instructor
602603
def export; end
603604

605+
action_auth_level :export_selected, :instructor
606+
def export_selected
607+
tar_stream = @course.generate_tar(params[:export_configs])
608+
609+
send_data tar_stream.string.force_encoding("binary"),
610+
filename: "#{@course.name}_#{Time.current.strftime('%Y%m%d')}.tar",
611+
type: "application/x-tar"
612+
rescue SystemCallError => e
613+
flash[:error] = "Unable to create the config YAML file: #{e.message}"
614+
redirect_to(action: :export)
615+
rescue StandardError => e
616+
flash[:error] = "Unable to generate tarball -- #{e.message}"
617+
redirect_to(action: :export)
618+
end
619+
604620
private
605621

606622
def new_course_params

app/models/attachment.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require "fileutils"
2+
require "utilities"
23

34
##
45
# Attachments are Course or Assessment specific, and allow instructors to
@@ -21,4 +22,9 @@ def file=(upload)
2122
def after_create
2223
COURSE_LOGGER.log("Created Attachment #{id}:#{filename} (#{mime_type}) as \"#{name}\")")
2324
end
25+
26+
SERIALIZABLE = Set.new %w[filename mime_type released name assessment_id]
27+
def serialize
28+
Utilities.serializable attributes, SERIALIZABLE
29+
end
2430
end

app/models/course.rb

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,87 @@ def watchlist_allow_ca
291291
watchlist_configuration.allow_ca
292292
end
293293

294+
def has_attachment?
295+
!attachments.nil? && attachments.count > 0
296+
end
297+
298+
def has_risk_conditions?
299+
!risk_conditions.nil? && risk_conditions.count > 0
300+
end
301+
302+
def has_watchlist_configuration?
303+
!watchlist_configuration.nil?
304+
end
305+
306+
def dump_yaml(include_metrics)
307+
YAML.dump(serialize(include_metrics))
308+
end
309+
310+
def generate_tar(export_configs)
311+
base_path = Rails.root.join("courses", name).to_s
312+
course_dir = name
313+
attachments_dir = File.join(course_dir, "attachments")
314+
rb_path = "course.rb"
315+
config_path = "#{name}.yml"
316+
mode = 0o755
317+
318+
begin
319+
tarStream = StringIO.new("")
320+
Gem::Package::TarWriter.new(tarStream) do |tar|
321+
tar.mkdir course_dir, File.stat(base_path).mode
322+
323+
# save course.rb
324+
source_file = File.open(File.join(base_path, rb_path), 'rb')
325+
tar.add_file File.join(course_dir, rb_path), File.stat(source_file).mode do |tar_file|
326+
tar_file.write(source_file.read)
327+
end
328+
329+
# save course and metrics config
330+
tar.add_file File.join(course_dir, config_path), mode do |tar_file|
331+
tar_file.write(dump_yaml(export_configs&.include?('metrics_config')))
332+
end
333+
334+
# save attachments
335+
tar.mkdir attachments_dir, File.stat(base_path).mode
336+
attachments.each do |attachment|
337+
next unless attachment.attachment_file.attached?
338+
339+
attachment_data = attachment.attachment_file.download
340+
filename = attachment.filename
341+
relative_path = File.join(attachments_dir, filename)
342+
343+
tar.add_file relative_path, mode do |file|
344+
file.write(attachment_data)
345+
end
346+
end
347+
348+
# save assessments
349+
if export_configs&.include?('assessments')
350+
assessments.each do |assessment|
351+
asmt_dir = assessment.name
352+
assessment.dump_yaml
353+
Dir[File.join(base_path, asmt_dir, "**")].each do |file|
354+
mode = File.stat(file).mode
355+
relative_path = File.join(course_dir, file.sub(%r{^#{Regexp.escape base_path}/?}, ""))
356+
357+
if File.directory?(file)
358+
tar.mkdir relative_path, mode
359+
elsif !relative_path.starts_with? File.join(asmt_dir,
360+
assessment.handin_directory)
361+
tar.add_file relative_path, mode do |tar_file|
362+
File.open(file, "rb") { |f| tar_file.write f.read }
363+
end
364+
end
365+
end
366+
end
367+
end
368+
end
369+
tarStream.rewind
370+
tarStream.close
371+
tarStream
372+
end
373+
end
374+
294375
private
295376

296377
def saved_change_to_grade_related_fields?
@@ -325,5 +406,36 @@ def config_module_name
325406
"Course#{sanitized_name.camelize}"
326407
end
327408

409+
def serialize(include_metrics)
410+
s = {}
411+
s["general"] = serialize_general
412+
s["general"]["late_penalty"] = late_penalty.serialize unless late_penalty.nil?
413+
s["general"]["version_penalty"] = version_penalty.serialize unless version_penalty.nil?
414+
s["attachments"] = attachments.map(&:serialize) if has_attachment?
415+
416+
if include_metrics
417+
if has_risk_conditions?
418+
s["risk_conditions"] = risk_conditions.map(&:serialize)
419+
latest_version = s["risk_conditions"].max_by{ |k| k["version"] }["version"]
420+
s["risk_conditions"] = s["risk_conditions"].select { |condition|
421+
condition["version"] == latest_version
422+
}
423+
end
424+
425+
if has_watchlist_configuration?
426+
s["watchlist_configuration"] =
427+
watchlist_configuration.serialize
428+
end
429+
end
430+
s
431+
end
432+
433+
GENERAL_SERIALIZABLE = Set.new %w[name semester late_slack grace_days display_name start_date
434+
end_date disabled exam_in_progress version_threshold
435+
gb_message website]
436+
def serialize_general
437+
Utilities.serializable attributes, GENERAL_SERIALIZABLE
438+
end
439+
328440
include CourseAssociationCache
329441
end

app/models/risk_condition.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require "utilities"
2+
13
class RiskCondition < ApplicationRecord
24
serialize :parameters, Hash
35
enum condition_type: { no_condition_selected: 0, grace_day_usage: 1, grade_drop: 2,
@@ -184,4 +186,9 @@ def self.get_max_version(course_id)
184186
max_version
185187
end
186188
end
189+
190+
SERIALIZABLE = Set.new %w[condition_type parameters version]
191+
def serialize
192+
Utilities.serializable attributes, SERIALIZABLE
193+
end
187194
end

app/models/watchlist_configuration.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,9 @@ def self.update_watchlist_configuration_for_course(course_name, blocklist_update
7070

7171
config
7272
end
73+
74+
SERIALIZABLE = Set.new %w[category_blocklist assessment_blocklist allow_ca]
75+
def serialize
76+
Utilities.serializable attributes, SERIALIZABLE
77+
end
7378
end
Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
<%= form_tag export_selected_course_path, method: 'post' do %>
12
<table class="prettyBorder" id="course_fields">
23
<colgroup>
34
<col span="1" style="width: 50px;">
@@ -7,7 +8,7 @@
78
<tr class="course-field checked">
89
<td>
910
<label>
10-
<input disabled=true class="cbox" type="checkbox" id="course_config_checkbox" checked>
11+
<%= check_box_tag 'export_configs[]', 'course_config', true, class: 'cbox', id: 'course_config_checkbox', disabled: true %>
1112
<span/>
1213
</label>
1314
</td>
@@ -16,26 +17,27 @@
1617
<tr class="course-field">
1718
<td>
1819
<label>
19-
<input class="cbox" type="checkbox" id="assessments_checkbox">
20+
<%= check_box_tag 'export_configs[]', 'metrics_config', false, class: 'cbox', id: 'metrics_config_checkbox' %>
2021
<span/>
2122
</label>
2223
</td>
23-
<td>Assessments</td>
24+
<td>Metrics configuration</td>
2425
</tr>
2526
<tr class="course-field">
2627
<td>
2728
<label>
28-
<input class="cbox" type="checkbox" id="metrics_config_checkbox">
29+
<%= check_box_tag 'export_configs[]', 'assessments', false, class: 'cbox', id: 'assessments_checkbox' %>
2930
<span/>
3031
</label>
3132
</td>
32-
<td>Metrics configuration</td>
33+
<td>Assessments</td>
3334
</tr>
3435
</tbody>
3536
</table>
3637
<br />
3738
<div class="row">
3839
<div>
39-
<a class="btn" id="export_btn" >Export</a>
40+
<%= submit_tag 'Export', class: 'btn', id: 'export_btn', data: { disable_with: false } %>
4041
</div>
4142
</div>
43+
<% end %>

config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@
230230
patch "update_lti_settings"
231231
match "email", via: [:get, :post]
232232
get "export"
233+
post "export_selected"
233234
get "manage"
234235
get "moss"
235236
post "reload"

spec/contexts_helper.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,14 @@ def create_course_with_users_as_hash(asmt_name: "testassessment2")
3232
instructor_user: @instructor_user, course_assistant_user: @course_assistant_user,
3333
students_cud: @students, assessment: @assessment }
3434
end
35+
36+
def create_course_with_attachment_as_hash
37+
create_users
38+
puts "Built users"
39+
create_course_with_attachment
40+
puts "Built course"
41+
{ course: @course, admin_user: @admin_user,
42+
instructor_user: @instructor_user, course_assistant_user: @course_assistant_user,
43+
students_cud: @students, assessment: @assessment, attachment: @attachment }
44+
end
3545
end

0 commit comments

Comments
 (0)