L'édition concurrent d'un événement est maintenant bloquée
This commit is contained in:
parent
83cb7e142f
commit
81ee54fff4
@ -63,8 +63,7 @@ h3.warning
|
||||
margin: 0
|
||||
padding: 1em
|
||||
text-align: center
|
||||
border-color: #de2b0f
|
||||
background-color: #f04124
|
||||
background-color: orange
|
||||
+box-shadow(0 0 0.3em gray)
|
||||
+inline-block()
|
||||
+border-radius(1em)
|
||||
|
@ -7,6 +7,7 @@ class EventsController < ApplicationController
|
||||
before_action :check_secret, only:
|
||||
[:edit, :preview, :update, :cancel, :destroy]
|
||||
before_action :set_mailer_host
|
||||
rescue_from ActiveRecord::StaleObjectError, with: :locked
|
||||
|
||||
def index
|
||||
respond_to do |format|
|
||||
@ -105,8 +106,9 @@ class EventsController < ApplicationController
|
||||
# through.
|
||||
def event_params
|
||||
params.require(:event)
|
||||
.permit :title, :start_time, :end_time, :description, :address, :city,
|
||||
:region, :locality, :url, :contact, :submitter, :tags
|
||||
.permit :lock_version, :title, :start_time, :end_time, :description,
|
||||
:address, :city, :region, :locality, :url, :contact, :submitter,
|
||||
:tags
|
||||
end
|
||||
|
||||
# Check that you can only edit an existing event if you know its secret
|
||||
@ -131,4 +133,9 @@ class EventsController < ApplicationController
|
||||
# Send an update mail to moderators
|
||||
ModerationMailer.update(@older_event, @event, nil).deliver
|
||||
end
|
||||
|
||||
def locked
|
||||
redirect_to edit_event_url(@event, secret: @event.secret),
|
||||
alert: t('staleObjectError')
|
||||
end
|
||||
end
|
||||
|
@ -3,6 +3,7 @@ class ModerationsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_moderation, :set_mailer_host, only:
|
||||
[:show, :edit, :preview, :update, :validate, :accept, :refuse, :destroy]
|
||||
rescue_from ActiveRecord::StaleObjectError, with: :locked
|
||||
|
||||
def index
|
||||
@events = Event.unmoderated
|
||||
@ -19,7 +20,7 @@ class ModerationsController < ApplicationController
|
||||
def update
|
||||
@older_mod = Event.new @event.attributes
|
||||
respond_to do |format|
|
||||
if @moderation.update(moderation_params) && send_moderation_mails
|
||||
if @moderation.update_attributes(moderation_params) && send_mails
|
||||
format.html { redirect_to moderations_path, notice: t('.ok') }
|
||||
format.json { head :no_content }
|
||||
else
|
||||
@ -71,8 +72,9 @@ class ModerationsController < ApplicationController
|
||||
# through.
|
||||
def moderation_params
|
||||
params.require(:event)
|
||||
.permit :title, :start_time, :end_time, :description, :address, :city,
|
||||
:region, :locality, :url, :contact, :submitter, :tags
|
||||
.permit :lock_version, :title, :start_time, :end_time, :description,
|
||||
:address, :city, :region, :locality, :url, :contact, :submitter,
|
||||
:tags
|
||||
end
|
||||
|
||||
# Useful to manage absolute url in mails
|
||||
@ -80,7 +82,7 @@ class ModerationsController < ApplicationController
|
||||
ActionMailer::Base.default_url_options[:host] = request.host_with_port
|
||||
end
|
||||
|
||||
def send_moderation_mails
|
||||
def send_mails
|
||||
# Send an update mail to moderators
|
||||
ModerationMailer.update(@older_mod, @moderation, current_user).deliver
|
||||
end
|
||||
@ -112,4 +114,8 @@ class ModerationsController < ApplicationController
|
||||
@reason = t "moderations.refuse.reason_#{params[:reason]}_long"
|
||||
end
|
||||
end
|
||||
|
||||
def locked
|
||||
redirect_to edit_moderation_url(@moderation), alert: t('staleObjectError')
|
||||
end
|
||||
end
|
||||
|
@ -1,13 +1,14 @@
|
||||
= form_for @event, url: (@moderation ? moderation_path(@moderation) : @event.persisted? ? event_path(@event) : nil) do |f|
|
||||
- if @event.persisted?
|
||||
= f.hidden_field :lock_version
|
||||
- unless @moderation
|
||||
= hidden_field_tag :secret, params[:secret]
|
||||
|
||||
- if @event.errors.any?
|
||||
#flash_messages
|
||||
- @event.errors.full_messages.each do |msg|
|
||||
%p.flash.alert= msg
|
||||
|
||||
- if @event.persisted?
|
||||
- unless @moderation
|
||||
= hidden_field_tag :secret, params[:secret]
|
||||
|
||||
.field.title
|
||||
= f.label :title
|
||||
= f.text_field :title, required: true, size: 70,
|
||||
|
@ -1,11 +1,11 @@
|
||||
=====================================================
|
||||
#{Event.human_attribute_name(:locality).concat(':').ljust 12 } #{t "attributes.locality_#{@event.locality}"}
|
||||
#{Event.human_attribute_name(:title).concat(':').ljust 12 } #{@event.title}
|
||||
#{Event.human_attribute_name(:start_time).concat(':').ljust 12 } #{l @event.start_time, format: :at}
|
||||
#{Event.human_attribute_name(:end_time).concat(':').ljust 12 } #{l @event.end_time, format: :at}
|
||||
#{Event.human_attribute_name(:address).concat(':').ljust 12 } #{@event.address}
|
||||
#{Event.human_attribute_name(:city).concat(':').ljust 12 } #{@event.city}
|
||||
#{Event.human_attribute_name(:region).concat(':').ljust 12 } #{@event.related_region}
|
||||
#{Event.human_attribute_name(:locality).concat(':').ljust 12 } #{t "attributes.locality_#{@event.locality}"}
|
||||
#{Event.human_attribute_name(:url).concat(':').ljust 12 } #{@event.url}
|
||||
#{Event.human_attribute_name(:contact).concat(':').ljust 12 } #{@event.contact}
|
||||
#{Event.human_attribute_name(:submitter).concat(':').ljust 12 } #{@event.submitter}
|
||||
|
@ -3,7 +3,9 @@
|
||||
=t '.title'
|
||||
|
||||
- if @moderation.moderated?
|
||||
%h3.warning=t '.warning'
|
||||
%h3.warning
|
||||
%em.fa.fa-warning
|
||||
=t '.warning'
|
||||
|
||||
%fieldset
|
||||
%legend
|
||||
|
@ -7,6 +7,8 @@ en:
|
||||
refuse: Refuse
|
||||
destroy: Destroy
|
||||
logout: Disconnect
|
||||
staleObjectError: Sorry, your modification was rejected because someone else
|
||||
already intervened
|
||||
|
||||
# Translatings screens
|
||||
layouts:
|
||||
@ -71,14 +73,14 @@ it more readable or agreable.
|
||||
It will appear online as soon as a moderator validates it.
|
||||
edit:
|
||||
title: Edit an event
|
||||
warning: Warning, this event is already moderated. Any modification will
|
||||
be immediately visible on the site.
|
||||
warning: 'Event already moderated: any modification will be immediately
|
||||
visible on the site'
|
||||
forbidden: You are not authorised to modify this event
|
||||
preview: Previsualisation
|
||||
edit: Edition
|
||||
preview:
|
||||
warning: Warning, this event is already moderated. Any modification will
|
||||
be immediately visible on the site.
|
||||
warning: 'Event already moderated: any modification will be immediately
|
||||
visible on the site'
|
||||
update:
|
||||
ok: Your event was updated
|
||||
form:
|
||||
@ -104,8 +106,8 @@ it more readable or agreable.
|
||||
visualise: Visualise
|
||||
cancel:
|
||||
title: Cancel event
|
||||
already_moderated: Warning, this event is already moderated. This
|
||||
cancellation will remove it from Agenda du Libre.
|
||||
already_moderated: 'Event already moderated: this cancellation will
|
||||
remove it from Agenda du Libre'
|
||||
confirm: Do you confirm this event cancellation?
|
||||
preview: Event visualisation
|
||||
ok: Yes
|
||||
@ -168,8 +170,8 @@ Example: `%{daylimit}`"
|
||||
edit:
|
||||
title: Edit event
|
||||
moderation: Moderation
|
||||
warning: Warning, this event is already moderated. Any modification will
|
||||
be immediately visible on the site.
|
||||
warning: 'Event already moderated: any modification will be immediately
|
||||
visible on the site'
|
||||
preview: Previsualisation
|
||||
edit: Edition
|
||||
update:
|
||||
|
@ -7,6 +7,8 @@ fr:
|
||||
refuse: Refuser
|
||||
destroy: Supprimer
|
||||
logout: Se déconnecter
|
||||
staleObjectError: Désolé, votre modification est rejetée car une autre
|
||||
personne est déjà intervenue
|
||||
|
||||
# Traductions des écrans
|
||||
layouts:
|
||||
@ -63,28 +65,44 @@ fr:
|
||||
l'aura validé.
|
||||
edit:
|
||||
title: Éditer un événement
|
||||
warning: Attention, cet événement est déjà modéré. Toute modification
|
||||
sera immédiatement visible sur le site.
|
||||
warning: 'Événement déjà modéré: toute modification sera immédiatement
|
||||
visible sur le site'
|
||||
forbidden: Vous n'êtes pas authorisé à modifier cet événement
|
||||
preview: Prévisualisation
|
||||
edit: Édition
|
||||
preview:
|
||||
warning: Attention, cet événement est déjà modéré. Toute modification
|
||||
sera immédiatement visible sur le site.
|
||||
warning: 'Événement déjà modéré: toute modification sera immédiatement
|
||||
visible sur le site'
|
||||
update:
|
||||
ok: Votre événement a été mis à jour
|
||||
form:
|
||||
title_helper: Décrivez en moins de 5 mots votre événement, sans y
|
||||
indiquer le lieu, la ville ou la date
|
||||
description_helper: Décrivez de la manière la plus complète possible
|
||||
votre événement
|
||||
address_helper: "*Associée à la ville et la région, elle générerera une
|
||||
carte [OpenStreetMap](http://www.openstreetmap.org), affichée aux côtés de
|
||||
l'événement*"
|
||||
carte [OpenStreetMap](http://www.openstreetmap.org), affichée aux côtés
|
||||
de l'événement*"
|
||||
url_helper: _Lien **direct** vers une page donnant plus d'informations
|
||||
sur l'événement (lieu précis, horaire précis, programme précis...)_
|
||||
contact_helper: _Adresse e-mail de contact, affichée de manière peu
|
||||
compréhensible par les spammeurs_
|
||||
submitter_helper: _Adresse e-mail du soumetteur de l'événement. Elle ne
|
||||
sera utilisée que par les modérateurs pour contacter la personne ayant
|
||||
proposé l'événement, pour l'informer de sa validation ou de son rejet.
|
||||
Si cette adresse n'est pas présente, l'adresse de contact sera
|
||||
utilisée_
|
||||
tags_helper: _Tags pour l'événement. Les tags sont séparés par des
|
||||
espaces. Un tag ne peut contenir que des lettres minuscules, des
|
||||
chiffres et des tirets. Dans les tags, indiquez le nom de la ou des
|
||||
associations organisatrices. N'indiquez pas le nom de la ville ou de la
|
||||
région._
|
||||
save: Valider
|
||||
visualise: Visualiser
|
||||
cancel:
|
||||
title: Annulation de l'événement
|
||||
already_moderated: Attention, cet événement est déjà modéré. Cette
|
||||
annulation le fera disparaître de l'Agenda du Libre.
|
||||
already_moderated: "Événement déjà modéré: cette annulation le fera
|
||||
disparaître de l'Agenda du Libre"
|
||||
confirm: Confirmez-vous l'annulation de cet événement?
|
||||
preview: Visualisation de l'événement
|
||||
ok: Oui
|
||||
@ -152,8 +170,8 @@ Exemple: `%{daylimit}`"
|
||||
edit:
|
||||
title: Éditer un événement
|
||||
moderation: Modération
|
||||
warning: Attention, cet événement est déjà modéré. Toute modification
|
||||
sera immédiatement visible sur le site.
|
||||
warning: 'Événement déjà modéré: toute modification sera
|
||||
immédiatement visible sur le site'
|
||||
preview: Prévisualisation
|
||||
edit: Édition
|
||||
update:
|
||||
|
7
db/migrate/20141011100700_add_lock_version_to_events.rb
Normal file
7
db/migrate/20141011100700_add_lock_version_to_events.rb
Normal file
@ -0,0 +1,7 @@
|
||||
# Add optimistic locking to events, so that moderators won't risk overwriting
|
||||
# each others' work
|
||||
class AddLockVersionToEvents < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :events, :lock_version, :integer, default: 0, null: false
|
||||
end
|
||||
end
|
25
db/schema.rb
25
db/schema.rb
@ -12,7 +12,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20140823111115) do
|
||||
ActiveRecord::Schema.define(version: 20141011100700) do
|
||||
|
||||
create_table "active_admin_comments", force: true do |t|
|
||||
t.string "namespace"
|
||||
@ -25,9 +25,9 @@ ActiveRecord::Schema.define(version: 20140823111115) do
|
||||
t.datetime "updated_at"
|
||||
end
|
||||
|
||||
add_index "active_admin_comments", ["author_type", "author_id"], name: "index_active_admin_comments_on_author_type_and_author_id"
|
||||
add_index "active_admin_comments", ["namespace"], name: "index_active_admin_comments_on_namespace"
|
||||
add_index "active_admin_comments", ["resource_type", "resource_id"], name: "index_active_admin_comments_on_resource_type_and_resource_id"
|
||||
add_index "active_admin_comments", ["author_type", "author_id"], name: "index_active_admin_comments_on_author_type_and_author_id", using: :btree
|
||||
add_index "active_admin_comments", ["namespace"], name: "index_active_admin_comments_on_namespace", using: :btree
|
||||
add_index "active_admin_comments", ["resource_type", "resource_id"], name: "index_active_admin_comments_on_resource_type_and_resource_id", using: :btree
|
||||
|
||||
create_table "admin_users", force: true do |t|
|
||||
t.string "email", default: "", null: false
|
||||
@ -44,8 +44,8 @@ ActiveRecord::Schema.define(version: 20140823111115) do
|
||||
t.datetime "updated_at"
|
||||
end
|
||||
|
||||
add_index "admin_users", ["email"], name: "index_admin_users_on_email", unique: true
|
||||
add_index "admin_users", ["reset_password_token"], name: "index_admin_users_on_reset_password_token", unique: true
|
||||
add_index "admin_users", ["email"], name: "index_admin_users_on_email", unique: true, using: :btree
|
||||
add_index "admin_users", ["reset_password_token"], name: "index_admin_users_on_reset_password_token", unique: true, using: :btree
|
||||
|
||||
create_table "cities", force: true do |t|
|
||||
t.string "name", default: "", null: false
|
||||
@ -57,7 +57,7 @@ ActiveRecord::Schema.define(version: 20140823111115) do
|
||||
t.float "longitude", limit: 24
|
||||
end
|
||||
|
||||
add_index "cities", ["name"], name: "cities_name"
|
||||
add_index "cities", ["name"], name: "cities_name", using: :btree
|
||||
|
||||
create_table "events", force: true do |t|
|
||||
t.string "title", default: "", null: false
|
||||
@ -73,16 +73,17 @@ ActiveRecord::Schema.define(version: 20140823111115) do
|
||||
t.integer "moderated", default: 0, null: false
|
||||
t.string "tags", default: "", null: false
|
||||
t.string "secret", default: "", null: false
|
||||
t.datetime "decision_time", null: false
|
||||
t.datetime "submission_time", null: false
|
||||
t.datetime "decision_time"
|
||||
t.datetime "submission_time"
|
||||
t.string "moderator_mail_id", limit: 32
|
||||
t.string "submitter_mail_id", limit: 32
|
||||
t.text "address"
|
||||
t.float "latitude"
|
||||
t.float "longitude"
|
||||
t.float "latitude", limit: 24
|
||||
t.float "longitude", limit: 24
|
||||
t.integer "lock_version", default: 0, null: false
|
||||
end
|
||||
|
||||
add_index "events", ["start_time", "end_time"], name: "events_date"
|
||||
add_index "events", ["start_time", "end_time"], name: "events_date", using: :btree
|
||||
|
||||
create_table "lugs", force: true do |t|
|
||||
t.integer "region", default: 0, null: false
|
||||
|
@ -11,7 +11,7 @@ class EventsControllerTest < ActionController::TestCase
|
||||
test 'should get index' do
|
||||
get :index
|
||||
assert_response :success
|
||||
assert_not_nil assigns(:events)
|
||||
assert_not_nil assigns :events
|
||||
end
|
||||
|
||||
test 'should get new' do
|
||||
@ -23,8 +23,7 @@ class EventsControllerTest < ActionController::TestCase
|
||||
assert_no_difference 'Event.count' do
|
||||
post :preview_create, event: {
|
||||
title: @event.title,
|
||||
start_time: @event.start_time,
|
||||
end_time: @event.end_time,
|
||||
start_time: @event.start_time, end_time: @event.end_time,
|
||||
description: @event.description,
|
||||
city: @event.city,
|
||||
region: @event.related_region,
|
||||
@ -42,8 +41,7 @@ class EventsControllerTest < ActionController::TestCase
|
||||
assert_difference 'Event.count' do
|
||||
post :create, event: {
|
||||
title: @event.title,
|
||||
start_time: @event.start_time,
|
||||
end_time: @event.end_time,
|
||||
start_time: @event.start_time, end_time: @event.end_time,
|
||||
description: @event.description,
|
||||
city: @event.city,
|
||||
region: @event.related_region,
|
||||
@ -59,11 +57,7 @@ class EventsControllerTest < ActionController::TestCase
|
||||
|
||||
test 'should not create event' do
|
||||
assert_no_difference 'Event.count' do
|
||||
post :create, event: {
|
||||
title: @event.title,
|
||||
city: @event.city,
|
||||
region: @event.related_region
|
||||
}
|
||||
post :create, event: { title: @event.title }
|
||||
|
||||
assert_not_empty assigns(:event).errors.messages
|
||||
end
|
||||
@ -75,7 +69,7 @@ class EventsControllerTest < ActionController::TestCase
|
||||
end
|
||||
|
||||
test 'should get edit' do
|
||||
get :edit, id: @event, secret: 'MyString'
|
||||
get :edit, id: @event, secret: @event.secret
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
@ -86,7 +80,7 @@ class EventsControllerTest < ActionController::TestCase
|
||||
|
||||
test 'should preview' do
|
||||
assert_no_difference 'Event.count' do
|
||||
patch :preview, id: @event, secret: 'MyString', event: {
|
||||
patch :preview, id: @event, secret: @event.secret, event: {
|
||||
title: @event.title
|
||||
}
|
||||
|
||||
@ -97,7 +91,7 @@ class EventsControllerTest < ActionController::TestCase
|
||||
end
|
||||
|
||||
test 'should update event' do
|
||||
patch :update, id: @event, secret: 'MyString', event: {
|
||||
patch :update, id: @event, secret: @event.secret, event: {
|
||||
title: @event.title
|
||||
}
|
||||
|
||||
@ -106,21 +100,29 @@ class EventsControllerTest < ActionController::TestCase
|
||||
end
|
||||
|
||||
test 'should not update event' do
|
||||
patch :update, id: @event, secret: 'MyString', event: {
|
||||
patch :update, id: @event, secret: @event.secret, event: {
|
||||
title: nil
|
||||
}
|
||||
|
||||
assert_not_empty assigns(:event).errors.messages
|
||||
end
|
||||
|
||||
test 'can not update event concurrently' do
|
||||
patch :update, id: @event, secret: @event.secret, event: {
|
||||
lock_version: @event.lock_version - 1
|
||||
}
|
||||
|
||||
assert_redirected_to edit_event_url(@event, secret: @event.secret)
|
||||
end
|
||||
|
||||
test 'should get cancel page' do
|
||||
get :cancel, id: @event, secret: 'MyString'
|
||||
get :cancel, id: @event, secret: @event.secret
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test 'should destroy event' do
|
||||
assert_difference('Event.count', -1) do
|
||||
delete :destroy, id: @event, secret: 'MyString'
|
||||
delete :destroy, id: @event, secret: @event.secret
|
||||
end
|
||||
|
||||
assert_redirected_to events_path
|
||||
|
@ -58,7 +58,7 @@ class ModerationsControllerTest < ActionController::TestCase
|
||||
end
|
||||
|
||||
test 'should update event' do
|
||||
patch :update, id: @moderation, secret: 'MyString', event: {
|
||||
patch :update, id: @moderation, event: {
|
||||
title: @moderation.title,
|
||||
start_time: @moderation.start_time,
|
||||
end_time: @moderation.end_time,
|
||||
@ -73,13 +73,21 @@ class ModerationsControllerTest < ActionController::TestCase
|
||||
end
|
||||
|
||||
test 'should not update event' do
|
||||
patch :update, id: @moderation, secret: 'MyString', event: {
|
||||
patch :update, id: @moderation, event: {
|
||||
title: nil
|
||||
}
|
||||
|
||||
assert_not_empty assigns(:moderation).errors
|
||||
end
|
||||
|
||||
test 'can not update event concurrently' do
|
||||
patch :update, id: @moderation, event: {
|
||||
lock_version: @moderation.lock_version - 1
|
||||
}
|
||||
|
||||
assert_redirected_to edit_moderation_path @moderation
|
||||
end
|
||||
|
||||
test 'should reject event' do
|
||||
assert_difference 'Event.count', -1 do
|
||||
delete :destroy, id: @moderation
|
||||
|
3
test/fixtures/events.yml
vendored
3
test/fixtures/events.yml
vendored
@ -18,6 +18,7 @@ one:
|
||||
submission_time: <%= 3.days.ago %>
|
||||
moderator_mail_id: MyString
|
||||
submitter_mail_id: MyString
|
||||
lock_version: 0
|
||||
|
||||
two:
|
||||
title: MyString
|
||||
@ -37,6 +38,7 @@ two:
|
||||
submission_time: 2013-12-28 16:04:56
|
||||
moderator_mail_id: MyString
|
||||
submitter_mail_id: MyString
|
||||
lock_version: 0
|
||||
|
||||
proposed:
|
||||
title: MyString
|
||||
@ -56,3 +58,4 @@ proposed:
|
||||
submission_time: 2013-12-28 16:04:56
|
||||
moderator_mail_id: MyString
|
||||
submitter_mail_id: MyString
|
||||
lock_version: 0
|
||||
|
Loading…
Reference in New Issue
Block a user