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
Related
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.
I'm trying to find an elegant (standard) way to pass the parent of a polymorphic model on to the view. For example:
class Picture < ActiveRecord::Base
belongs_to :imageable, :polymorphic => true
end
class Employee < ActiveRecord::Base
has_many :pictures, :as => :imageable
end
class Product < ActiveRecord::Base
has_many :pictures, :as => :imageable
end
The following way (find_imageable) works, but it seems "hackish".
#PictureController (updated to include full listing)
class PictureController < ApplicationController
#/employees/:id/picture/new
#/products/:id/picture/new
def new
#picture = imageable.pictures.new
respond_with [imageable, #picture]
end
private
def imageable
#imageable ||= find_imageable
end
def find_imageable
params.each do |name, value|
if name =~ /(.+)_id$/
return $1.classify.constantize.find(value)
end
end
nil
end
end
Is there a better way?
EDIT
I'm doing a new action. The path takes the form of parent_model/:id/picture/new and params include the parent id (employee_id or product_id).
I'm not sure exactly what you're trying to do but if you're trying to find the object that 'owns' the picture you should be able to use the imageable_type field to get the class name. You don't even need a helper method for this, just
def show
#picture = Picture.find(params[:id])
#parent = #picture.imagable
#=> so on and so forth
end
Update
For an index action you could do
def index
#pictures = Picture.includes(:imagable).all
end
That will instantiate all 'imagables' for you.
Update II: The Wrath of Poly
For your new method you could just pass the id to your constructor, but if you want to instantiate the parent you could get it from the url like
def parent
#parent ||= %w(employee product).find {|p| request.path.split('/').include? p }
end
def parent_class
parent.classify.constantize
end
def imageable
#imageable ||= parent_class.find(params["#{parent}_id"])
end
You could of course define a constant in your controller that contained the possible parents and use that instead of listing them in the method explicitly. Using the request path object feels a little more 'Rails-y' to me.
I just ran into this same problem.
The way I 'sort of' solved it is defining a find_parent method in each model with polymorphic associations.
class Polymorphic1 < ActiveRecord::Base
belongs_to :parent1, :polymorphic => true
def find_parent
self.parent1
end
end
class Polymorphic2 < ActiveRecord::Base
belongs_to :parent2, :polymorphic => true
def find_parent
self.parent2
end
end
Unfortunately, I can not think of a better way. Hope this helps a bit for you.
This is the way I did it for multiple nested resources, where the last param is the polymorphic model we are dealing with: (only slightly different from your own)
def find_noteable
#possibilities = []
params.each do |name, value|
if name =~ /(.+)_id$/
#possibilities.push $1.classify.constantize.find(value)
end
end
return #possibilities.last
end
Then in the view, something like this:
<% # Don't think this was needed: #possibilities << picture %>
<%= link_to polymorphic_path(#possibilities.map {|p| p}) do %>
The reason for returning the last of that array is to allow finding the child/poly records in question i.e. #employee.pictures or #product.pictures
Hi I am new to rails and I would like to know what is the best way who save dependent objects in an HBTM relation.
Specifically, I have two classes Post and Tag
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags
end
class Tag < ActiveRecord::Base
has_and_belongs_to_many :posts
end
I have a migration to create the joining table
class AddPostsTagsJoinTable < ActiveRecord::Migration
def self.up
create_table :posts_tags, :id => false do |t|
t.integer :post_id
t.integer :tag_id
end
end
def self.down
drop_table :postss_tags
end
end
All is good up to here
So I have a PostsController from which I handle the creation, updates and deletes for the posts, and I want to encapsulate the Tags so that the creation is via the PostsController... like so:
class PostsController < ApplicationController
#... code removed for brevity
def create
#post = current_user.posts.build(params[:post])
if #post.save
tag_names = params[:post][:tags].strip.split(' ')
tag_names.each do |t|
#see if the tag already exists
tag = Tag.find_by_name(t);
if tag.nil?
#post.tags.create!(:name => t)
else
#post.tags << tag #just create the association
end
end
flash[:success] = "Post created."
redirect_to(user_posts_path(current_user.username))
else
#user = current_user
render 'new'
end
end
end
I am not sure how I should handle the creation of my Tag(s) because if I just call
#post.tags.create!(:name => t)
this will create duplicate records in the Tags table (even when :uniq => true is specified in the model).
So to avoid the duplication I see if a tag is already present and then add it like this
tag = Tag.find_by_name(t);
if tag.nil?
#post.tags.create!(:name => t)
else
#post.tags << tag #just create the association
end
Is this the way it's supposed to be done?
This seems expensive (especially 'cause it's in a loop) so I am wondering if there is another "cleaner" way to do this? (pls forget the DRY'ing up of the action and so on)
Is there a clean way to create my Tags without having to manually check for duplicates?
thank you in advance for your help!
You can save tags attribute of post if automatically by adding accepts_nested_attributes_for to Post model
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags
accepts_nested_attributes_for :tags
end
The next step is to output tags fields inside post form.
So I have a model (Photo), where when I call Photo.new #image => #image / Photo.create :image => #image, I want my model to find an existing photo with the same image hash OR create a new Photo from #image. Assume I can't use Photo.find_or_initialize_by_hash because I have a custom find function which finds close copies of images based on a soft image hash.
My first idea was to do
before_validation :check_duplicates, :on => :create
def check_duplicates
self = self.find_duplicate
end
Unfortunately, I realized you can't just redefine self in a model, so now I think the best approach is doing something along the lines of changing the return value from initialize to the duplicate.
Sort of like this, but it doesn't work (and I've heard horror stories about overriding initialize)
def initialize(*params)
super(*params)
return self.find_duplicate || self
end
From what I gather your model structure looks something like this?
class Photo < ActiveRecord::Base
has_one :image
end
class Image < ActiveRecord::Base
belongs_to :photo
end
If so, you can simply do this:
class Photo < ActiveRecord::Base
has_one :image, :uniq => true
end
Or if :image is just an attribute of Photo your first idea was on track:
class Photo < ActiveRecord::Base
before_create :check_duplicate
private
def check_duplicate
Photo.where(:image => self.image).count == 0 # will be false if Photo is found
end
end
which will cancel the Photo from being created if #check_duplicate returns false (http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html)
Or simply
class Photo < ActiveRecord::Base
validates_uniqueness_of :image
end
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