Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow reusing score items #5625

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions app/controllers/score_items_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,54 @@ def copy
end
end

def upload
return render json: { message: I18n.t('course_members.upload_labels_csv.no_file') }, status: :unprocessable_entity if params[:upload].nil? || params[:upload][:file] == 'undefined' || params[:upload][:file].nil?

file = params[:upload][:file]

@evaluation_exercise = EvaluationExercise.find(params[:evaluation_exercise_id])
authorize @evaluation_exercise, :update?

begin
headers = CSV.foreach(file.path).first
%w[name maximum].each do |column|
return render json: { message: I18n.t('course_members.upload_labels_csv.missing_column', column: column) }, status: :unprocessable_entity unless headers&.include?(column)
end

ScoreItem.transaction do
# Remove existing score items.
@evaluation_exercise.score_items.destroy_all

CSV.foreach(file.path, headers: true) do |row|
row = row.to_hash
score_item = ScoreItem.new(
name: row['name'],
maximum: row['maximum'],
visible: row.key?('visible') ? row['visible'] : true,
description: row.key?('description') ? row['description'] : nil,
evaluation_exercise: @evaluation_exercise
)

score_item.save!
end
end
rescue CSV::MalformedCSVError, ActiveRecord::RecordInvalid
return render json: { message: I18n.t('course_members.upload_labels_csv.malformed') }, status: :unprocessable_entity
end

respond_to do |format|
format.js { render 'score_items/index', locals: { new: nil, evaluation_exercise: @evaluation_exercise.reload } }
format.json { head :no_content }
end
end

def index
@evaluation_exercise = EvaluationExercise.find(params[:evaluation_exercise_id])
authorize @evaluation_exercise, :show?

@score_items = policy_scope(@evaluation_exercise.score_items)
end

def create
@score_item = ScoreItem.new(permitted_attributes(ScoreItem))
@score_item.last_updated_by = current_user
Expand Down
1 change: 1 addition & 0 deletions app/models/score_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class ScoreItem < ApplicationRecord
after_update :uncomplete_feedbacks_if_maximum_changed

validates :maximum, numericality: { greater_than: 0, less_than: 1000 }
validates :name, presence: true

default_scope { order(id: :asc) }

Expand Down
16 changes: 16 additions & 0 deletions app/policies/evaluation_exercise_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@
record.visible_score? && record.evaluation.released?
end

def show?
return true if course_admin?

return false unless evaluation_member?

Check warning on line 12 in app/policies/evaluation_exercise_policy.rb

View check run for this annotation

Codecov / codecov/patch

app/policies/evaluation_exercise_policy.rb#L12

Added line #L12 was not covered by tests

record&.visible_score? && record&.evaluation&.released?

Check warning on line 14 in app/policies/evaluation_exercise_policy.rb

View check run for this annotation

Codecov / codecov/patch

app/policies/evaluation_exercise_policy.rb#L14

Added line #L14 was not covered by tests
end

def update?
course_admin?
end

def permitted_attributes
%i[visible_score]
end
Expand All @@ -16,4 +28,8 @@
course = record&.evaluation&.series&.course
user&.course_admin?(course)
end

def evaluation_member?
record&.evaluation&.users&.include?(user)

Check warning on line 33 in app/policies/evaluation_exercise_policy.rb

View check run for this annotation

Codecov / codecov/patch

app/policies/evaluation_exercise_policy.rb#L33

Added line #L33 was not covered by tests
end
end
31 changes: 29 additions & 2 deletions app/views/score_items/_exercise.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,30 @@
<%= javascript_include_tag 'score_item' %>
<% end %>
<% maximum_score = evaluation_exercise.maximum_score %>
<div id="score-items-<%= evaluation_exercise.id %>">
<h4><%= evaluation_exercise.exercise.name %></h4>
<div id="score-items-<%= evaluation_exercise.id %>" class="mb-2">
<div class="d-flex flex-row align-items-center">
<h4><%= evaluation_exercise.exercise.name %></h4>
<div class="flex-spacer"></div>
<div class="dropdown">
<a class="btn btn-icon dropdown-toggle" data-bs-toggle="dropdown">
<i class="mdi mdi-dots-vertical"></i>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<%= link_to evaluation_evaluation_exercise_score_items_path(evaluation_exercise.evaluation_id, evaluation_exercise.id, format: :csv), class: "dropdown-item" do %>
<i class="mdi mdi-download mdi-18"></i>
<%= I18n.t "score_items.exercise.download" %>
<% end %>
</li>
<li>
<a href="#upload-form-<%=evaluation_exercise.id%>" data-bs-toggle="modal" class="dropdown-item">
<i class="mdi mdi-upload mdi-18"></i>
<%= I18n.t "score_items.exercise.upload" %>
</a>
</li>
</ul>
</div>
</div>

<table class="table table-resource score-items-table" id="table-for-<%= evaluation_exercise.id %>">
<thead>
Expand Down Expand Up @@ -104,6 +126,11 @@
<%= render 'score_items/copy', evaluation_exercise: evaluation_exercise, evaluation: @evaluation %>
</div>
</div>
<div class="modal fade modal-<%= evaluation_exercise.id %>" id="upload-form-<%= evaluation_exercise.id %>" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<%= render partial: 'score_items/upload', locals: {evaluation_exercise: evaluation_exercise} %>
</div>
</div>
</div>
<script>
window.dodona.initVisibilityCheckboxes(document.getElementById("score-items-<%= evaluation_exercise.id %>"));
Expand Down
25 changes: 25 additions & 0 deletions app/views/score_items/_upload.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title"><%= t "score_items.upload.title" %></h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<%= t("score_items.form.close") %>"></button>
</div>

<%= form_for(:upload, url: upload_evaluation_evaluation_exercise_score_items_path(evaluation_exercise.evaluation_id, evaluation_exercise.id), html: { class: 'form-horizontal' }, namespace: evaluation_exercise.id, remote: true) do |f| %>
<div class="modal-body">
<div class="field form-group row">
<div class="col-12">
<%= f.file_field :file,
class: "form-control",
accept: '.csv',
required: true
%>
</div>
</div>
</div>
<div class="modal-footer">
<%= f.submit t("score_items.upload.upload"),
class: "btn btn-filled",
data: { confirm: t('score_items.upload.confirm') } %>
</div>
<% end %>
</div>
9 changes: 9 additions & 0 deletions app/views/score_items/index.csv.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<%= CSV.generate_line %w[name maximum description visible], row_sep: nil %>
<% @score_items.each do |cm| %>
<%= CSV.generate_line([
cm.name,
cm.maximum,
cm.description,
cm.visible
], row_sep: nil).html_safe %>
<% end %>
6 changes: 6 additions & 0 deletions config/locales/views/score_items/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,9 @@ en:
Show or hide the calculated total score in Dodona for this exercise when the feedback is released.
Individual score items are still visible if they are marked as such.
visibility: Show or hide this score item.
upload: Upload score items
download: Download score items
upload:
title: Upload csv with score items
upload: Upload
confirm: Are you sure you want to upload these score items? Existing score items will be overwritten.
6 changes: 6 additions & 0 deletions config/locales/views/score_items/nl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,9 @@ nl:
Toon of verberg de berekende totaalscore in Dodona voor deze oefening als de feedback vrijgegeven wordt.
Individuele scoreonderdelen zijn nog wel zichtbaar als ze als zodanig gemarkeerd zijn.
visibility: Toon of verberg dit scoreonderdeel.
upload: Scoreonderdelen uploaden
download: Scoreonderdelen downloaden
upload:
title: CSV met scoreonderdelen uploaden
upload: Uploaden
confirm: Ben je zeker dat je deze scoreonderdelen wil uploaden? Bestaande scoreonderdelen zullen overschreven worden.
6 changes: 6 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,12 @@
post 'add_all', on: :collection
end
resources :scores, only: %i[show create update destroy]

resources :evaluation_exercise do
resources :score_items, only: %i[index] do
post 'upload', on: :collection
end
end
end
resources :feedbacks, only: %i[show edit update] do
delete 'scores', action: :destroy_scores, controller: :feedbacks, on: :member
Expand Down
160 changes: 160 additions & 0 deletions test/controllers/score_items_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,164 @@ def setup
exercise.update!(score_items: [])
end
end

test 'should be able to download score items as CSV' do
exercise = @evaluation.evaluation_exercises.first
create :score_item, evaluation_exercise: exercise, name: 'foo', maximum: 10.0, description: 'bar'
create :score_item, evaluation_exercise: exercise, name: 'baz', maximum: 20.0, description: 'qux', visible: false

get evaluation_evaluation_exercise_score_items_path(@evaluation, exercise, format: :csv)

assert_response :success

csv = CSV.parse(response.body, headers: true)

assert_equal 2, csv.length
assert_equal %w[name maximum description visible], csv.headers
assert_equal %w[foo 10.0 bar true], csv[0].fields
assert_equal %w[baz 20.0 qux false], csv[1].fields
end

test 'should be able to upload score items as CSV' do
exercise = @evaluation.evaluation_exercises.first

assert_equal 0, exercise.score_items.count

file = Tempfile.new
file.write("name,maximum,description,visible\n")
file.write("foo,10.0,bar,true\n")
file.write("baz,20.0,qux,false\n")
file.close

post upload_evaluation_evaluation_exercise_score_items_path(@evaluation, exercise, format: :json), params: {
upload: {
file: Rack::Test::UploadedFile.new(file.path, 'text/csv')
}
}

assert_response :no_content
exercise.reload

assert_equal 2, exercise.score_items.count
assert_equal 'foo', exercise.score_items[0].name
assert_in_delta(10.0, exercise.score_items[0].maximum)
assert_equal 'bar', exercise.score_items[0].description
assert exercise.score_items[0].visible
assert_equal 'baz', exercise.score_items[1].name
assert_in_delta(20.0, exercise.score_items[1].maximum)
assert_equal 'qux', exercise.score_items[1].description
assert_not exercise.score_items[1].visible
end

test 'should be able to upload with only name and maximum' do
exercise = @evaluation.evaluation_exercises.first

assert_equal 0, exercise.score_items.count

file = Tempfile.new
file.write("name,maximum\n")
file.write("foo,10.0\n")
file.write("baz,20.0\n")
file.close

post upload_evaluation_evaluation_exercise_score_items_path(@evaluation, exercise, format: :json), params: {
upload: {
file: Rack::Test::UploadedFile.new(file.path, 'text/csv')
}
}

assert_response :success

exercise.reload

assert_equal 2, exercise.score_items.count
assert_equal 'foo', exercise.score_items[0].name
assert_in_delta(10.0, exercise.score_items[0].maximum)
assert_nil exercise.score_items[0].description
assert exercise.score_items[0].visible
assert_equal 'baz', exercise.score_items[1].name
assert_in_delta(20.0, exercise.score_items[1].maximum)
assert_nil exercise.score_items[1].description
assert exercise.score_items[1].visible
end

test 'should not upload without name or maximum' do
exercise = @evaluation.evaluation_exercises.first

assert_equal 0, exercise.score_items.count

file = Tempfile.new
file.write("name,maximum\n")
file.write("foo,10.0\n")
file.write(",20.0\n")
file.close

post upload_evaluation_evaluation_exercise_score_items_path(@evaluation, exercise, format: :json), params: {
upload: {
file: Rack::Test::UploadedFile.new(file.path, 'text/csv')
}
}

assert_response :unprocessable_entity

exercise.reload

assert_equal 0, exercise.score_items.count
end

test 'should replace score items if uploading again' do
exercise = @evaluation.evaluation_exercises.first
create :score_item, evaluation_exercise: exercise, name: 'foo', maximum: 10.0, description: 'bar'

assert_equal 1, exercise.score_items.count

file = Tempfile.new
file.write("name,maximum,description,visible\n")
file.write("baz,20.0,qux,false\n")
file.close

post upload_evaluation_evaluation_exercise_score_items_path(@evaluation, exercise, format: :json), params: {
upload: {
file: Rack::Test::UploadedFile.new(file.path, 'text/csv')
}
}

assert_response :no_content

exercise.reload

assert_equal 1, exercise.score_items.count
assert_equal 'baz', exercise.score_items[0].name
assert_in_delta(20.0, exercise.score_items[0].maximum)
assert_equal 'qux', exercise.score_items[0].description
assert_not exercise.score_items[0].visible
end

test 'Should not replace score items if uploading invalid data' do
exercise = @evaluation.evaluation_exercises.first
create :score_item, evaluation_exercise: exercise, name: 'foo', maximum: 10.0, description: 'bar'

assert_equal 1, exercise.score_items.count

file = Tempfile.new
file.write("name,maximum,description,visible\n")
file.write("baz,-20.0,qux,false\n")
file.close

post upload_evaluation_evaluation_exercise_score_items_path(@evaluation, exercise, format: :json), params: {
upload: {
file: Rack::Test::UploadedFile.new(file.path, 'text/csv')
}
}

assert_response :unprocessable_entity

exercise.reload

assert_equal 1, exercise.score_items.count
assert_equal 'foo', exercise.score_items[0].name
assert_in_delta(10.0, exercise.score_items[0].maximum)
assert_equal 'bar', exercise.score_items[0].description
assert exercise.score_items[0].visible
end
end