Skip to content

Commit edf584a

Browse files
jlgeevanyeyeye20wildmanjlykimcheedamianhxy
committed
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 c26f134 commit edf584a

File tree

13 files changed

+331
-8
lines changed

13 files changed

+331
-8
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
@@ -581,8 +581,24 @@ def run_moss
581581
`rm -rf #{tmp_dir}`
582582
end
583583

584+
action_auth_level :export, :instructor
584585
def export; end
585586

587+
action_auth_level :export_selected, :instructor
588+
def export_selected
589+
tar_stream = @course.generate_tar(params[:export_configs])
590+
591+
send_data tar_stream.string.force_encoding("binary"),
592+
filename: "#{@course.name}_#{Time.current.strftime('%Y%m%d')}.tar",
593+
type: "application/x-tar"
594+
rescue SystemCallError => e
595+
flash[:error] = "Unable to create the config YAML file: #{e.message}"
596+
redirect_to(action: :export)
597+
rescue StandardError => e
598+
flash[:error] = "Unable to generate tarball -- #{e.message}"
599+
redirect_to(action: :export)
600+
end
601+
586602
private
587603

588604
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
@@ -33,4 +34,9 @@ def file=(upload)
3334
def after_create
3435
COURSE_LOGGER.log("Created Attachment #{id}:#{filename} (#{mime_type}) as \"#{name}\")")
3536
end
37+
38+
SERIALIZABLE = Set.new %w[filename mime_type released name assessment_id]
39+
def serialize
40+
Utilities.serializable attributes, SERIALIZABLE
41+
end
3642
end

app/models/course.rb

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,87 @@ def watchlist_allow_ca
302302
watchlist_configuration.allow_ca
303303
end
304304

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

307388
def saved_change_to_grade_related_fields?
@@ -336,5 +417,36 @@ def config_module_name
336417
"Course#{sanitized_name.camelize}"
337418
end
338419

420+
def serialize(include_metrics)
421+
s = {}
422+
s["general"] = serialize_general
423+
s["general"]["late_penalty"] = late_penalty.serialize unless late_penalty.nil?
424+
s["general"]["version_penalty"] = version_penalty.serialize unless version_penalty.nil?
425+
s["attachments"] = attachments.map(&:serialize) if has_attachment?
426+
427+
if include_metrics
428+
if has_risk_conditions?
429+
s["risk_conditions"] = risk_conditions.map(&:serialize)
430+
latest_version = s["risk_conditions"].max_by{ |k| k["version"] }["version"]
431+
s["risk_conditions"] = s["risk_conditions"].select { |condition|
432+
condition["version"] == latest_version
433+
}
434+
end
435+
436+
if has_watchlist_configuration?
437+
s["watchlist_configuration"] =
438+
watchlist_configuration.serialize
439+
end
440+
end
441+
s
442+
end
443+
444+
GENERAL_SERIALIZABLE = Set.new %w[name semester late_slack grace_days display_name start_date
445+
end_date disabled exam_in_progress version_threshold
446+
gb_message website]
447+
def serialize_general
448+
Utilities.serializable attributes, GENERAL_SERIALIZABLE
449+
end
450+
339451
include CourseAssociationCache
340452
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,
@@ -186,4 +188,9 @@ def self.get_max_version(course_id)
186188
max_version
187189
end
188190
end
191+
192+
SERIALIZABLE = Set.new %w[condition_type parameters version]
193+
def serialize
194+
Utilities.serializable attributes, SERIALIZABLE
195+
end
189196
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
@@ -225,6 +225,7 @@
225225
patch "update_lti_settings"
226226
match "email", via: [:get, :post]
227227
get "export"
228+
post "export_selected"
228229
get "manage"
229230
get "moss"
230231
post "reload"

spec/contexts_helper.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,14 @@ def create_course_with_users_as_hash(asmt_name: "testassessment2")
4949
instructor_user: @instructor_user, course_assistant_user: @course_assistant_user,
5050
students_cud: @students, assessment: @assessment }
5151
end
52+
53+
def create_course_with_attachment_as_hash
54+
create_users
55+
puts "Built users"
56+
create_course_with_attachment
57+
puts "Built course"
58+
{ course: @course, admin_user: @admin_user,
59+
instructor_user: @instructor_user, course_assistant_user: @course_assistant_user,
60+
students_cud: @students, assessment: @assessment, attachment: @attachment }
61+
end
5262
end

0 commit comments

Comments
 (0)