I have an application that creates pages, called campaign pages, from a selection of widgets that can be used to specify the layout of the pages. I'm working on the functionality to load and edit the contents of a campaign page, alter the contents, and update the relevant entries for that page. The order of a particular widget on a page is stored in a column called page_display_order. Page display order is unique across the campaign page, since two widgets cannot be in the same place on the page. This is the column that sometimes throws me an error when updating a page.
My model for widgets that belong to campaign pages looks like this:
class CampaignPagesWidget < ActiveRecord::Base
belongs_to :campaign_page, inverse_of: :campaign_pages_widgets
belongs_to :widget_type
has_one :actionkit_page
validates_presence_of :content, :page_display_order, :campaign_page_id, :widget_type_id
# validates that there are not two widgets with exactly same content on the same campaign page
# and that the page display order integer is unique across the widgets with that campaign page id
validates_uniqueness_of :page_display_order, :content, :scope => :campaign_page_id
end
This is how the widget data is read from the page upon requesting an update in editing a page (in campaign_pages_controller.rb). It first finds out which type of widget it is (stored in the table and model for widgets), then populates an object with the contents for that widget specified on the page edits, and pushes it into an array that specifies the nested attributes to be updated when updating the campaign page:
@campaign_page = CampaignPage.find params[:id]
@widgets = @campaign_page.campaign_pages_widgets
permitted_params = CampaignPageParameters.new(params).permit
widget_attributes = []
params[:widgets].each do |widget_type_name, widget_data|
# widget type id is contained in a field called widget_type:
widget_type_id = widget_data.delete :widget_type
widget = @widgets.find_by(widget_type_id: widget_type_id)
widget_attributes.push({
id: widget.id,
widget_type_id: widget_type_id,
content: widget_data,
page_display_order: i})
i += 1
end
permitted_params[:campaign_pages_widgets_attributes] = widget_attributes
@campaign_page.update! permitted_params.to_hash
redirect_to @campaign_page
On some occasions this updates and redirects as intended, at other times it throws the following error:
ActiveRecord::RecordInvalid in CampaignPagesController#update Validation failed: Campaign pages widgets page display order has already been taken, Campaign pages widgets is invalid
I'm new to both Ruby and Rails and would greatly appreciate advice on what I might be doing wrong as well as how I should be doing this better. I don't have a good hang on how to do things 'the Rails way' and I know I'll have to refactor the code in the controller.
UPDATE: The error is thrown only if there are more than one widgets. Things work fine if I comment out the scoped uniqueness constraint. It does genuinely update existing entries instead of creating new ones, so that can't be the problem.
This is happening because the widget updates do not happen all at once - they run one at a time. Rails checks uniqueness by querying the database for an existing row with the same data, and stops if it finds one with a different ID than the object you're trying to save. For example, assume you have
| id | campaign_page_id | page_display_order |
| 10 | 100 | 1 |
| 20 | 100 | 2 |
When you try to switch the widget order, to [10, 2] [20, 1]
, Rails starts to save widget 10 and goes to check that its new order (2
) is unique. But, it finds widget 20's row with that order already and raises the error you're seeing.
Check out acts as list, which is a gem that will manage this for you. Otherwise, I think you'll need to skirt Rails and perform the position updates manually.