From 6ee0b6bf81abda632719080d7e8e3d7149ce5bc3 Mon Sep 17 00:00:00 2001 From: Hauke Klement Date: Thu, 12 Mar 2015 11:05:11 +0100 Subject: [PATCH] implemented partial batch update for exercises --- app/assets/javascripts/exercises.js | 55 ++++++++++++++++++- app/controllers/exercises_controller.rb | 11 ++++ app/policies/exercise_policy.rb | 4 ++ app/views/exercises/index.html.slim | 10 +++- config/locales/de.yml | 1 + config/locales/en.yml | 1 + config/routes.rb | 4 ++ spec/controllers/exercises_controller_spec.rb | 14 +++++ spec/policies/exercise_policy_spec.rb | 9 +++ 9 files changed, 105 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/exercises.js b/app/assets/javascripts/exercises.js index 5b027258..b0cceb16 100644 --- a/app/assets/javascripts/exercises.js +++ b/app/assets/javascripts/exercises.js @@ -15,11 +15,45 @@ $(function() { $('body, html').scrollTo('#add-file'); }; + var ajaxError = function(response) { + $.flash.danger({ + text: $('#flash').data('message-failure') + }); + }; + + var buildCheckboxes = function() { + $('tbody tr').each(function(index, element) { + var td = $('td.public', element); + var checkbox = $('', { + checked: td.data('value'), + type: 'checkbox' + }); + td.on('click', function(event) { + event.preventDefault(); + checkbox.prop('checked', !checkbox.prop('checked')); + }); + td.html(checkbox); + }); + }; + var discardFile = function(event) { event.preventDefault(); $(this).parents('li').remove(); }; + var enableBatchUpdate = function() { + $('thead .batch a').on('click', function(event) { + event.preventDefault(); + if (!$(this).data('toggled')) { + $(this).data('toggled', true); + $(this).text($(this).data('text')); + buildCheckboxes(); + } else { + performBatchUpdate(); + } + }); + }; + var enableInlineFileCreation = function() { $('#add-file').on('click', addFileForm); $('#files').on('click', 'li .discard-file', discardFile); @@ -84,6 +118,23 @@ $(function() { }); }; + var performBatchUpdate = function() { + var jqxhr = $.ajax({ + data: { + exercises: _.map($('tbody tr'), function(element) { + return { + id: $(element).data('id'), + public: $('.public input', element).prop('checked') + }; + }) + }, + dataType: 'json', + method: 'PUT' + }); + jqxhr.done(window.CodeOcean.refresh); + jqxhr.fail(ajaxError); + }; + var toggleCodeHeight = function() { $('code').on('click', function() { $(this).css({ @@ -93,7 +144,9 @@ $(function() { }; if ($.isController('exercises')) { - if ($('.edit_exercise, .new_exercise').isPresent()) { + if ($('table').isPresent()) { + enableBatchUpdate(); + } else if ($('.edit_exercise, .new_exercise').isPresent()) { execution_environments = $('form').data('execution-environments'); file_types = $('form').data('file-types'); new MarkdownEditor('#exercise_instructions'); diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 56378ca1..df16c752 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -15,6 +15,17 @@ class ExercisesController < ApplicationController end private :authorize! + def batch_update + @exercises = Exercise.all + authorize! + @exercises = params[:exercises].values.map do |exercise_params| + exercise = Exercise.find(exercise_params.delete(:id)) + exercise.update(exercise_params) + exercise + end + render(json: {exercises: @exercises}) + end + def clone exercise = @exercise.duplicate(public: false, token: nil, user: current_user) exercise.send(:generate_token) diff --git a/app/policies/exercise_policy.rb b/app/policies/exercise_policy.rb index 1eb0ab12..b5aff89b 100644 --- a/app/policies/exercise_policy.rb +++ b/app/policies/exercise_policy.rb @@ -4,6 +4,10 @@ class ExercisePolicy < AdminOrAuthorPolicy end private :author? + def batch_update? + admin? + end + [:clone?, :destroy?, :edit?, :show?, :statistics?, :update?].each do |action| define_method(action) { admin? || author? || team_member? } end diff --git a/app/views/exercises/index.html.slim b/app/views/exercises/index.html.slim index 00d0729f..9cc44d32 100644 --- a/app/views/exercises/index.html.slim +++ b/app/views/exercises/index.html.slim @@ -17,17 +17,21 @@ h1 = Exercise.model_name.human(count: 2) th = sort_link(@search, :execution_environment_id, t('activerecord.attributes.exercise.execution_environment')) th = t('.test_files') th = t('activerecord.attributes.exercise.maximum_score') - th = t('activerecord.attributes.exercise.public') + th + = t('activerecord.attributes.exercise.public') + - if policy(Exercise).batch_update? + br + span.batch = link_to(t('shared.batch_update'), '#', 'data-text' => t('shared.update', model: t('activerecord.models.exercise.other'))) th colspan=6 = t('shared.actions') tbody - @exercises.each do |exercise| - tr + tr data-id=exercise.id td = exercise.title td = link_to(exercise.author, exercise.author) td = link_to(exercise.execution_environment, exercise.execution_environment) td = exercise.files.teacher_defined_tests.count td = exercise.maximum_score - td = symbol_for(exercise.public?) + td.public data-value=exercise.public? = symbol_for(exercise.public?) td = link_to(t('shared.show'), exercise) td = link_to(t('shared.edit'), edit_exercise_path(exercise)) td = link_to(t('shared.destroy'), exercise, data: {confirm: t('shared.confirm_destroy')}, method: :delete) diff --git a/config/locales/de.yml b/config/locales/de.yml index afba2af5..b253e3a0 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -283,6 +283,7 @@ de: administration: Administration already_signed_in: Sie sind bereits angemeldet. apply_filters: Filter anwenden + batch_update: Batch-Update confirm_destroy: Sind Sie sicher? create: '%{model} erstellen' created_at: Erstellt diff --git a/config/locales/en.yml b/config/locales/en.yml index b0db3ff5..0c919f79 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -283,6 +283,7 @@ en: administration: Administration already_signed_in: You are already signed in. apply_filters: Apply filters + batch_update: Batch Update confirm_destroy: Are you sure? create: 'Create %{model}' created_at: Created At diff --git a/config/routes.rb b/config/routes.rb index 9ba066ce..bb8381e2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -22,6 +22,10 @@ Rails.application.routes.draw do end resources :exercises do + collection do + match '', to: 'exercises#batch_update', via: [:patch, :put] + end + member do post :clone get :implement diff --git a/spec/controllers/exercises_controller_spec.rb b/spec/controllers/exercises_controller_spec.rb index 563c1bed..34d286dd 100644 --- a/spec/controllers/exercises_controller_spec.rb +++ b/spec/controllers/exercises_controller_spec.rb @@ -5,6 +5,20 @@ describe ExercisesController do let(:user) { FactoryGirl.create(:admin) } before(:each) { allow(controller).to receive(:current_user).and_return(user) } + describe 'PUT #batch_update' do + let(:attributes) { {public: true} } + let(:request) { proc { put :batch_update, exercises: {0 => attributes.merge(id: exercise.id)} } } + before(:each) { request.call } + + it 'updates the exercises' do + expect_any_instance_of(Exercise).to receive(:update).with(attributes) + request.call + end + + expect_json + expect_status(200) + end + describe 'POST #clone' do let(:request) { proc { post :clone, id: exercise.id } } diff --git a/spec/policies/exercise_policy_spec.rb b/spec/policies/exercise_policy_spec.rb index fc170c2f..c9762f9e 100644 --- a/spec/policies/exercise_policy_spec.rb +++ b/spec/policies/exercise_policy_spec.rb @@ -5,6 +5,15 @@ describe ExercisePolicy do let(:exercise) { FactoryGirl.build(:dummy, team: FactoryGirl.create(:team)) } + permissions :batch_update? do + it 'grants access to admins only' do + expect(subject).to permit(FactoryGirl.build(:admin), exercise) + [:external_user, :teacher].each do |factory_name| + expect(subject).not_to permit(FactoryGirl.build(factory_name), exercise) + end + end + end + [:create?, :index?, :new?].each do |action| permissions(action) do it 'grants access to admins' do