Rails. Validations for many locales at the same time - ruby-on-rails-3

I have a bilingual web site with two locales: en and ru.
I want my site to have i18n. I use 'globalize3' and 'easy_globalize3_accessors' gems.
There are departments I can create and edit with standard forms.
Locales are given from URL: example.com/en/departments/ or example.com/ru/departments/
Now if I want to create a new department item, I would see such a thing:
A main form for current locale (I18n.locale).
A checkbox to add a translation on the same page.
If checkbox is active, show another form for another locale right next to the main form.
The most important thing — validations for each locale must be different. Say, for en it should pass ASCII symbols; for ru — Cyrillic ones.
My problem is number 4. I can't get my validations work with a checkbox.
The main problem is: checkbox active? If yes, show another form and run validations for it. If no, show nothing and don't run validations for that form, pass it empty.
For now, if I fill in two forms, everything works like a charm.
Ok. What I tried.
Model
class Department < ActiveRecord::Base
attr_accessible :name, :translations_attributes
translates :name, fallbacks_for_empty_translations: true
accepts_nested_attributes_for :translations
# The inline class Translation is a hack to solve
# "Can't mass-assign protected attributes: locale"
# See https://github.com/svenfuchs/globalize3/issues/128#issuecomment-11480650
class Translation
attr_accessible :locale, :name
validates :name, uniqueness: true
validates :name, format: {with: /\A[-а-яА-Я -]+\Z/}, if: ->(l) {l.locale.to_s == 'ru'}
validates :name, format: {with: /\A[-a-zA-Z -']+\Z/}, if: ->(l) {l.locale.to_s == 'en'}
end
end
Controller
def new
#department = Department.new
end
def create
#department = Department.new(params[:department])
#department.save ? (redirect_to action: :index) : (render :new)
end
View (new.haml.html) without checkbox
= form_for #department, url: {action: :create} do |f|
%h2
- f.globalize_fields_for_locale I18n.locale do |g|
= "Translation for"
= I18n.locale
= g.label t("department.form.new.label.name")
= g.text_field :name
%hr
%h2
- I18n.available_locales.each do |locale|
- next if locale == I18n.locale
%br
- f.globalize_fields_for_locale locale do |g|
= "Translation for"
= locale
= g.label t("department.form.new.label.name")
= g.text_field :name
= f.submit t("department.create.link"), class: "btn"
Help me understand what I have to do, please.

Related

Search for multiple possible param values

At the moment I have a form in which the user can input price per person and/or duration and/or team_size. What I would like to accomplish is to retrieve all records from a table that match the user input and for this I set scope in the model:
scope :filter_by_team_size, -> (team_size) { where("team_size = ?", team_size) }
scope :filter_by_duration, -> (duration) { where("duration = ?", duration) }
scope :filter_by_price, -> (price) { where("price = ?", price) }
And then in the controller use that again to retrieve the records by doing so:
#experiences = policy_scope(Experience).order(team_size: :desc).geocoded.filter_by_team_size(params[:team_size]) if params[:team_size].present?
#experiences = policy_scope(Experience).order(duration: :desc).geocoded.filter_by_duration(params[:duration]) if params[:duration].present?
#experiences = policy_scope(Experience).order(price: :desc).geocoded.filter_by_price(params[:price]) if params[:price].present?
However, this only gives me only the records for which the first input value matches but ignores all other values. Additionally, when you are viewing the search results and use the filter again it should apply the filter only for the records it found already.
Any suggestion on how to solve this would be much appreciated!
One way to handle this is to use a virtual model that handles binding parameters to and from the form:
class SearchQuery
include ActiveModel::Model
include ActiveModel::Attributes
attribute :team_size, :integer
attribute :duration
attribute :price
end
You can then setup the form:
<%= form_with(model: (#search_query || SearchQuery.new), url: '/experiences', method: :get) %>
<div>
<%= f.label :team_size %>
<%= f.number_field :team_size %>
</div>
# ..
<% end %>
And then you can just bind the params to the model with ActionController::Parameters#permit just like you would with a normal ActiveRecord model:
class ExperiencesController
before_action :set_search_query, only: :index, if: ->{ params[:search_query].present? }
# ...
def index
#experiences = if #search_query
#search_query.build_scope(policy_scope(Experience))
else
policy_scope(Experience)
end.geocoded
end
private
def set_search_query
#search_query = SearchQuery.new(search_query_params)
end
def search_query_params
params.fetch(:search_query).permit(:team_size, :duration, :price)
end
end
This loopback will make the form stateful just like your normal CRUD forms. We have not actually implemented #build_scope yes so lets do so:
class SearchQuery
include ActiveModel::Model
include ActiveModel::Attributes
attribute :team_size, :integer
attribute :duration
attribute :price
def build_scope(base_scope)
compacted_attributes = attributes.reject { value.nil? || value.empty? }
compacted_attributes.each_with_object(base_scope) do |(k,v), base|
if base.respond_to? "filter_by_#{k}"
# lets you customize the logic with a scope
base.send("filter_by_#{k}", v) # the scope is responsible for ordering
else
# convention over configuration!
base.where(Hash[k,v]).order(Hash[k,:desc])
end
end
end
end
Since this uses convention over configuration you can get rid of those pointless scopes in your model.

Why isn't my search method working in Ruby on Rails?

In my Ruby on Rails application, I have a cinema system and am trying to return the screen a showing is in when a user searches for the showing.
To display the search drop down I am using this code in my _application.html.erb:
<%= render( :partial => '/screen_lookup', :locals => {:showings => #showings = Showing.all, :my_path => '/screens/display_screens_by_showing' })%>
Which renders the search from the _screen_lookup.html.erb:
<%= form_tag my_path, :method=>'post', :multipart => true do %>
<%= select_tag ('showings_id'),
options_from_collection_for_select(#showings, :id, :showing_times, 0 ),
:prompt => "Showings" %>
<%= submit_tag 'Search' %>
<% end %>
And uses the display_screens_by_showing in the screens_controller:
def display_screens_by_showing
#screens = Screen.showing_search(params[:showing_id])
if #screens.empty?
# assign a warning message to the flash hash to be displayed in
# the div "feedback-top"
flash.now[:alert] = "There are no films of that genre."
# return all products, in alphabetical order
#screens = Screen.all
end
render :action => "index"
end
And this searches using the method in the screen.rb model:
def self.showing_search(showing_id)
screen = Showing.where("id = ?", showing_id).screen_id
self.where("id = ?", screen)
end
Now, the problem I am having is that because a showing belongs_to a screen, and a screen has_many showings, I need to be able to search for the showing, and store that showing's screen_id in a variable to search for the screen that showing is in with, which I have tried doing in the model:
screen = Showing.where("id = ?", showing_id).screen_id
self.where("id = ?", screen)
But the error I am getting is:
NoMethodError in ScreensController#display_screens_by_showing
undefined method `screen_id' for #<ActiveRecord::Relation []>
These are the model relationships:
showing.rb:
class Showing < ActiveRecord::Base
belongs_to :screen
end
screen.rb:
class Screen < ActiveRecord::Base
has_many :showings
end
What code will get my search working?
The problem is that where doesn't return a record, it returns a relation that can be enumerated or chained, instead you want to use find or find_by to return a single record, which is kind equivalent to where + first
screen = Showing.find(showing_id).screen_id
which is sort of like doing
screen = Showing.where(id: showing_id).first.screen_id
If you want to pass a hash you can use find_by which will be like this
screen = Showing.find_by(id: showing_id).screen_id
PS:
I'm not sure what you're doing exactly, but i think those two lines can be merged into a single query ( not sure what it should be returning, but I'm assuming a screen )
def self.showing_search(showing_id)
Showing.find(showing_id).screen
end

Rails form to edit JSON object as text

I'd like to make a form that lets a user edit one field of a mongoid object as rendered JSON text. There's a field in the model that my rails app should not understand, but I want to expose a generic editor. So for this field, I'd like to render it as pretty JSON, and expose it in a big <textarea> and then parse the JSON back in after any edits.
I can think of a dozen ways to do this, but I'm wonder what would be most consistent with Rails philosophy and least divergent from normal scaffolding. Should I render the object to JSON text in the controller? Then I'd have to repeat that code in the new and edit methods, and the parsing code in the update and create methods, which seems a bit kludgy. Is there a way to define a helper or custom form widget that goes in the _form.html.erb that is more reusable? Or maybe one already written?
You can make your own attribute writer/reader, in the model:
attr_accessible the_field_raw
def the_field_raw
self.the_field.to_s
end
def the_field_raw=(value)
self.the_field = JSON(value)
end
whitch should be compatible with form generators and no extra code in the controllers.
Hope it helps!
Serialize the values as JSON.
class Price < ActiveRecord::Base
serialize :values, JSON
validates :start, :end, :values, :presence => true
end
migration:
class CreateMyModels < ActiveRecord::Migration[7.0]
def change
create_table :my_models do |t|
t.jsonb :name, default: {}, null: false
t.jsonb :description, default: {}, null: false
t.integer :another_param
t.timestamps
end
end
end
model and concern:
class MyModel < ApplicationRecord
AVAILABLE_LOCALES = I18n.available_locales
include JsonLocalize
json_localize :name, :description
end
module JsonLocalize
extend ActiveSupport::Concern
included do
def self.json_localize(*attrs)
self::AVAILABLE_LOCALES.each do |locale|
attrs.each do |attr|
define_method("#{attr}_#{locale}") do
send(attr)[locale.to_s]
end
define_method("#{attr}_#{locale}=") do |value|
send(attr)[locale.to_s] = value
end
end
end
end
end
end
then you can have in your form:
.row
.col-md-6
- MyModel::AVAILABLE_LOCALES.each do |loc|
= f.input "name_#{loc}"
= f.input "description_#{loc}"
controller params:
def resource_params
params.require(:my_model).permit(
[
:another_param
] | [:name, :description].map {|attr| MyModel::AVAILABLE_LOCALES.map { |loc| "#{attr}_#{loc}".to_sym } }.flatten
)
end

Using nested attributes to easily select associations in a form

I am trying to create a nested attribute form to create a model which is primarily an association "connector" between two other models. In my case, the models represent books, awards, and the "connector" model book_awards. When I am editing a book, I want to be able to quickly select which awards it has won.
I've been using
http://railscasts.com/episodes/196-nested-model-form-part-1
to help me get started, but I'm afraid I'm pretty much stuck.
Another SO question which seems similar is
accepts_nested_attributes_for with find_or_create? Unfortunately, it's also not quite what I'm doing and I haven't been able to adapt it.
My models look like this. Each model has additional attributes and validations etc, but I've removed them for clarity.
class Book < ActiveRecord::Base
has_many :book_awards
accepts_nested_attributes_for :book_awards, :allow_destroy => true
end
class Award < ActiveRecord::Base
has_many :book_awards
end
class BookAward < ActiveRecord::Base
belongs_to :book, :award
end
In my book controller methods for edit and new, and the failure cases for create and update I have a line #awards = Award.all.
In my view, I would like to see a list of all awards with check boxes next to them. When I submit, I would like to either update, create, or destroy a book_award model. If the check box is selected, I would like to update an existing model or create a new one if it doesn't exist. If the check box isn't selected, then I would like to destroy an existing model or do nothing if the award never existed. I have a partial for book_awards. I'm not sure if the check box selector should be in this partial or not.
I think my check box will be my hook to :_destroy but with its polarity reversed. I think something like this will basically do it:
= f.check_box :_destroy, {}, 0, 1
Currently, I have this in my partial but I'm not sure where it really belongs.
Next comes my view which currently doesn't work, but maybe it will help demonstrate what I'm trying to do. I loop through the awards and use a fields_for to set nested attributes for anything that already exists. It's horribly ugly, but I think it somewhat works. However, I don't really know how to get started with the else case.
= f.label :awards
- #awards.each do |a|
- if f.object.awards && f.object.awards.include?(a)
= f.fields_for :book_awards, f.object.book_award.select{|bas| bas.award == a } do |ba|
= render 'book_awards', :f => ba, :a => a
- else
= fields_for :book_awards do |ba|
= render 'book_awards', :f => ba, :a => a
I would prefer the awards to be listed in the same order each time (my #awards assignment in the controller will probably specify the order) as opposed to listing the existing awards first or last.
I hate to answer my own question, but I finally figured out something which works. The first thing I needed to do was to update the "new" case based on the crazy object which was included in the railscast. Next, I needed to manually set the :child_index. Finally, I needed to manually set the :_destroy check box appropriately.
.field
= f.label :awards
- #awards.each_with_index do |a,i|
- if exists = (f.object.awards && f.object.awards.include?(a))
- new_ba = f.object.book_awards.select{|s| s.award == a}
- else
- new_ba = f.object.class.reflect_on_association(:book_awards).klass.new
= f.fields_for :book_awards, new_ba, :child_index => i do |ba|
= render 'book_awards', :f => ba, :a => a, :existing => exists
My partial looks like this:
.field
= f.check_box :_destroy, {:checked => existing}, 0, 1
= f.label a.name
= f.hidden_field :award_id, :value => a.id
= f.label :year
= f.number_field :year
It's not horribly pretty, but it seems to do exactly what I wanted.

How can I map between strings and attributes automatically?

I have a tiny logical error in my code somewhere and I can't figure out exactly what the problem is. Let's start from the beginning. I have the following extension that my order class uses.
class ActiveRecord::Base
def self.has_statuses(*status_names)
validates :status,
:presence => true,
:inclusion => { :in => status_names}
status_names.each do |status_name|
scope "all_#{status_name}", where(status: status_name)
end
status_names.each do |status_name|
define_method "#{status_name}?" do
status == status_name
end
end
end
end
This works great for the queries and initial setting of "statuses".
require "#{Rails.root}/lib/active_record_extensions"
class Order < ActiveRecord::Base
has_statuses :created, :in_progress, :approved, :rejected, :shipped
after_initialize :init
attr_accessible :store_id, :user_id, :order_reference, :sales_person
private
def init
if new_record?
self.status = :created
end
end
end
Now I set a status initially and that works great. No problems at all and I can save my new order as expected. Updating the order on the other hand is not working. I get a message saying:
"Status is not included in the list"
When I check it seems that order.status == 'created' and it's trying to match against :created. I tried setting the has_statuses 'created', 'in_progress' etc but couldn't get some of the other things to work.
Anyway to automatically map between string/attribute?
from your description, looks like you're comparing a string to a symbol. Probably need to add:
define_method "#{status_name}=" do
self.status = status_name.to_sym
end
or do a #to_s on the status_names