Globalize rails: Save all translations by checking I18n.available_locales - ruby-on-rails

Is it possibile to save all translations looking at I18n.available_locales (or maybe some other Globalize config file) when the main record is created?
I'm using Globalize in combination with Active Admin and I created a custom page only for the translations but I would like the person who needs to translate to know which are the fields yet to be translated.
This is what I'm doing now (base model) even though I'm not proud of it. It seems to be twisted for no reason I did try way simpler solution which appeared at first to be valid but they turned out not to work.
after_save :add_empty_translations
def add_empty_translations
# if the class is translatable
if (self.class.translates?)
# get available locales
locales = I18n.available_locales.map do |l| l.to_s end
# get foreign key for translated table
foreign_key = "#{self.class.to_s.underscore}_id"
# get translated columns
translated_columns = self.class::Translation.column_names.select do |col|
!['id', 'created_at', 'updated_at', 'locale', "#{self.class.to_s.underscore}_id"].include? col
end
# save current locale
current_locale = I18n.locale
# foreach available locale check if column was difined by user
locales.each do |l|
I18n.locale = l
add_translation = true
translated_columns.each do |col|
add_translation = add_translation && self[col].nil?
end
if (add_translation)
payload = {}
payload[foreign_key] = self.id
payload['locale'] = l
self.class::Translation.create(payload)
end
end
#restore locale
I18n.locale = current_locale
end
end
Is there a way to do it with globalize?

Since the above solution wasn't working all the times I ended up patching the gem itself like it follows:
Globalize::ActiveRecord::Adapter.module_eval do
def save_translations!
# START PATCH
translated_columns = self.record.class::Translation.column_names.select do |col|
!['id', 'created_at', 'updated_at', 'locale', "#{self.record.class.to_s.underscore}_id"].include? col
end
payload = {}
translated_columns.each do |column|
payload[column] = ""
end
I18n.available_locales.each do |l|
add_translation = true
translated_columns.each { |column| add_translation &&= stash[l][column].nil? }
if (record.translations_by_locale[l].nil? && add_translation)
stash[l] = payload
end
end
# END PATCH
stash.each do |locale, attrs|
next if attrs.empty?
translation = record.translations_by_locale[locale] ||
record.translations.build(locale: locale.to_s)
attrs.each do |name, value|
value = value.val if value.is_a?(Arel::Nodes::Casted)
translation[name] = value
end
end
reset
end
end

Related

Neat way to get and set keys of json column in Rails

I have a model/table with a json column in it as follows
t.json :options, default: {}
The column can contain many keys within it, something like this
options = {"details" : {key1: "Value1", key2: "Value2"}}
I want to set and get these values easily. So i have made getters and setters for the same.
def key1
options['details']&.[]('key1')
end
def key1=(value)
options['details'] ||= {}
options['details']['key1'] ||=0
options['details']['key1'] += value
end
But this just adds lines to my code, and it does not scale when more details are added. Can you please suggest a clean and neat way of doing this?
Use dynamic method creation:
options['details'].default_proc = ->(_,_) {{}}
ALLOWED_KEYS = %i[key1 key2 key3]
ALLOWED_KEYS.each do |key|
define_method key do
options['details'][key] if options['details'].key?(key)
end
define_method "#{key}=" do |value|
(options['details'][key] ||= 0) += value
end
end
You can just pass the key as a parameter as well right?
def get_key key=:key1
options['details']&.[](key)
end
def set_key= value, key=:key1
options['details'] ||= {}
options['details'][key] ||=0
options['details'][key] += value
end
Simple & Short
Depending on re-usability you can choose different options. The short option is to simply define the methods using a loop in combination with #define_method.
class SomeModel < ApplicationRecord
option_accessors = ['key1', 'key2']
option_accessors.map(&:to_s).each do |accessor_name|
# ^ in case you provide symbols in option_accessors
# this can be left out if know this is not the case
define_method accessor_name do
options.dig('details', accessor_name)
end
define_method "#{accessor_name}=" do |value|
details = options['details'] ||= {}
details[accessor_name] ||= 0
details[accessor_name] += value
end
end
end
Writing a Module
Alternatively you could write a module that provide the above as helpers. A simple module could look something like this:
# app/model_helpers/option_details_attribute_accessors.rb
module OptionDetailsAttributeAccessors
def option_details_attr_reader(*accessors)
accessors.map(&:to_s).each do |accessor|
define_method accessor do
options.dig('details', accessor)
end
end
end
def option_details_attr_writer(*accessors)
accessors.map(&:to_s).each do |accessor|
define_method "#{accessor}=" do |value|
details = options['details'] ||= {}
details[accessor] ||= 0
details[accessor] += value
end
end
end
def option_details_attr_accessor(*accessors)
option_details_attr_reader(*accessors)
option_details_attr_writer(*accessors)
end
end
Now you can simply extend your class with these helpers and easily add readers/writers.
class SomeModel < ApplicationRecord
extend OptionDetailsAttributeAccessors
option_details_attr_accessor :key1, :key2
end
If anything is unclear simply ask away in the comments.

Ruby/Rails Iterate through array and save to db?

I want to put each string from #enc into each field of column_name as a value
#enc=["hUt7ocoih//kFpgEizBowBAdxqqbGV1jkKVipVJwJnPGoPtTN16ZAJvW9tsi\n3inn\n", "wGNyaoEZ09jSg+/IclWFGAXzwz5lXLxJTUKqCFIiOy3ZXRgdwFUsNf/75R2V\nZm83\n", "MPq3KSzDzLvTeYh+h00HD+5FAgKoNksykJhzROVZWbIJ36WNoBgkSoicJ5wx\nog0g\n"]
Model.all.each do |row|
encrypted = #enc.map { |i| i}
row.column_name = encrypted
row.save!
end
My code puts all strings from array #enc into a single field?
I do not want that.
Help
Rails by default won't allow mass assignment. You have to whitelist parameters you want permitted. Have you tried doing something like the following?
#enc.each do |s|
cparams = create_params
cparams[:column_name] = s
Model.create(cparams)
end
def create_params
params.permit(:column_name)
end
You will need to specify the column names you are saving to. By setting each column separately you can also avoid mass-assignment errors:
#enc=["hUt7ocoih//kFpgEizBowBAdxqqbGV1jkKVipVJwJnPGoPtTN16ZAJvW9tsi\n3inn\n", "wGNyaoEZ09jSg+/IclWFGAXzwz5lXLxJTUKqCFIiOy3ZXRgdwFUsNf/75R2V\nZm83\n", "MPq3KSzDzLvTeYh+h00HD+5FAgKoNksykJhzROVZWbIJ36WNoBgkSoicJ5wx\nog0g\n"]
model = Widget.new
column_names = [:column1, :column2, :column3]
#enc.each_with_index do |s, i|
model[column_names[i]] = s
end
model.save
I think you are looking for something like this:
#enc.each do |str|
m = Model.new
m.column_name = str
m.save
end

Rails: How to initialize an object with the attributes in strings?

Probably been working on this too long, sloppy design, or both. My issue is I have a model I wish to initialize. The object has like 52 attributes, but I'm only setting a certain ~25 depending on which object I've just scanned. When I scan an object I get the columns and match them up with a hash_map I've created.
Example Hash Map
This just matches the scanned text to their respective attribute name.
hash_map = {"Pizza."=>"pizza_pie","PastaBowl"=>"pasta_bowl","tacos"=>"hard_shell_taco","IceCream"=>"ice_cream","PopTarts"=>"pop_tart"}
What I want to do
menu = RestaurantMenu.new(pizza_pie => var1, pasta_bowl => var2, ...)
My only problem is in my code at the moment I have this...
t.rows.each do |r|
for i in 0..r.length-1
#hash_map[t.combined_columns[i]] => r.[i]
puts "#{hash_map["#{t.combined_columns[i]}"]} => #{r[i]}"
end
end
the puts line displays what I want, but unsure how to get that in my app properly.
Here is several ways to fix this:
hash_map = {"Pizza."=>"pizza_pie","PastaBowl"=>"pasta_bowl","tacos"=>"hard_shell_taco","IceCream"=>"ice_cream","PopTarts"=>"pop_tart"}
attributes.each do |attribute, element|
message.send((attribute + '=').to_sym, hash_map[element])
end
or like this:
class Example
attr_reader :Pizza, :PastaBowl #...
def initialize args
args.each do |k, v|
instance_variable_set("##{k}", v) unless v.nil?
end
end
end
for more details click here
I ended up doing the following method:
attributes = Hash[]
attributes["restaurant"] = tmp_basic_info.name
attributes["menu_item"] = tmp_basic_info.item_name
t.rows.each do |r|
for i in 0..r.length-1
attributes["other"] = t.other_information
attributes[hash_map[t.combined_columns[i]] = r[i]
end
row = ImportMenuItem.new(attributes)
row.save
end

Papertrail and Carrierwave

I have a model that use both: Carrierwave for store photos, and PaperTrail for versioning.
I also configured Carrierwave for store diferent files when updates (That's because I want to version the photos) with config.remove_previously_stored_files_after_update = false
The problem is that PaperTrail try to store the whole Ruby Object from the photo (CarrierWave Uploader) instead of simply a string (that would be its url)
(version table, column object)
---
first_name: Foo
last_name: Bar
photo: !ruby/object:PhotoUploader
model: !ruby/object:Bla
attributes:
id: 2
first_name: Foo1
segundo_nombre: 'Bar1'
........
How can I fix this to store a simple string in the photo version?
You can override item_before_change on your versioned model so you don't call the uploader accesor directly and use write_attribute instead. Alternatively, since you might want to do that for several models, you can monkey-patch the method directly, like this:
module PaperTrail
module Model
module InstanceMethods
private
def item_before_change
previous = self.dup
# `dup` clears timestamps so we add them back.
all_timestamp_attributes.each do |column|
previous[column] = send(column) if respond_to?(column) && !send(column).nil?
end
previous.tap do |prev|
prev.id = id
changed_attributes.each do |attr, before|
if defined?(CarrierWave::Uploader::Base) && before.is_a?(CarrierWave::Uploader::Base)
prev.send(:write_attribute, attr, before.url && File.basename(before.url))
else
prev[attr] = before
end
end
end
end
end
end
end
Not sure if it's the best solution, but it seems to work.
Adding #beardedd's comment as an answer because I think this is a better way to handle the problem.
Name your database columns something like picture_filename and then in your model mount the uploader using:
class User < ActiveRecord::Base
has_paper_trail
mount_uploader :picture, PictureUploader, mount_on: :picture_filename
end
You still use the user.picture.url attribute to access your model but PaperTrail will store revisions under picture_filename.
Here is a bit updated version of monkeypatch from #rabusmar, I use it for rails 4.2.0 and paper_trail 4.0.0.beta2, in /config/initializers/paper_trail.rb.
The second method override is required if you use optional object_changes column for versions. It works in a bit strange way for carrierwave + fog if you override filename in uploader, old value will be from cloud and new one from local filename, but in my case it's ok.
Also I have not checked if it works correctly when you restore old version.
module PaperTrail
module Model
module InstanceMethods
private
# override to keep only basename for carrierwave attributes in object hash
def item_before_change
previous = self.dup
# `dup` clears timestamps so we add them back.
all_timestamp_attributes.each do |column|
if self.class.column_names.include?(column.to_s) and not send("#{column}_was").nil?
previous[column] = send("#{column}_was")
end
end
enums = previous.respond_to?(:defined_enums) ? previous.defined_enums : {}
previous.tap do |prev|
prev.id = id # `dup` clears the `id` so we add that back
changed_attributes.select { |k,v| self.class.column_names.include?(k) }.each do |attr, before|
if defined?(CarrierWave::Uploader::Base) && before.is_a?(CarrierWave::Uploader::Base)
prev.send(:write_attribute, attr, before.url && File.basename(before.url))
else
before = enums[attr][before] if enums[attr]
prev[attr] = before
end
end
end
end
# override to keep only basename for carrierwave attributes in object_changes hash
def changes_for_paper_trail
_changes = changes.delete_if { |k,v| !notably_changed.include?(k) }
if PaperTrail.serialized_attributes?
self.class.serialize_attribute_changes(_changes)
end
if defined?(CarrierWave::Uploader::Base)
Hash[
_changes.to_hash.map do |k, values|
[k, values.map { |value| value.is_a?(CarrierWave::Uploader::Base) ? value.url && File.basename(value.url) : value }]
end
]
else
_changes.to_hash
end
end
end
end
end
This is what actually functions for me, put this on config/initializers/paper_trail/.rb
module PaperTrail
module Reifier
class << self
def reify_attributes(model, version, attrs)
enums = model.class.respond_to?(:defined_enums) ? model.class.defined_enums : {}
AttributeSerializers::ObjectAttribute.new(model.class).deserialize(attrs)
attrs.each do |k, v|
is_enum_without_type_caster = ::ActiveRecord::VERSION::MAJOR < 5 && enums.key?(k)
if model.send("#{k}").is_a?(CarrierWave::Uploader::Base)
if v.present?
model.send("remote_#{k}_url=", v["#{k}"][:url])
model.send("#{k}").recreate_versions!
else
model.send("remove_#{k}!")
end
else
if model.has_attribute?(k) && !is_enum_without_type_caster
model[k.to_sym] = v
elsif model.respond_to?("#{k}=")
model.send("#{k}=", v)
elsif version.logger
version.logger.warn(
"Attribute #{k} does not exist on #{version.item_type} (Version id: #{version.id})."
)
end
end
end
end
end
end
end
This overrides the reify method to work on S3 + heroku
For uploaders to keep old files from updated or deleted records do this in the uploader
configure do |config|
config.remove_previously_stored_files_after_update = false
end
def remove!
true
end
Then make up some routine to clear old files from time to time, good luck
I want to add to the previous answers the following:
It can happen that you upload different files with the same name, and this may overwrite your previous file, so you won't be able to restore the old one.
You may use a timestamp in file names or create random and unique filenames for all versioned files.
Update
This doesn't seem to work in all edge cases for me, when assigning more than a single file to the same object within a single request request.
I'm using this right now:
def filename
[#cache_id, original_filename].join('-') if original_filename.present?
end
This seems to work, as the #cache_id is generated for each and every upload again (which isn't the case as it seems for the ideas provided in the links above).
#Sjors Provoost
We also need to override pt_recordable_object method in PaperTrail::Model::InstanceMethods module
def pt_recordable_object
attr = attributes_before_change
object_attrs = object_attrs_for_paper_trail(attr)
hash = Hash[
object_attrs.to_hash.map do |k, value|
[k, value.is_a?(CarrierWave::Uploader::Base) ? value.url && File.basename(value.url) : value ]
end
]
if self.class.paper_trail_version_class.object_col_is_json?
hash
else
PaperTrail.serializer.dump(hash)
end
end

mongo_mapper custom data types for localization

i have created a LocalizedString custom data type for storing / displaying translations using mongo_mapper.
This works for one field but as soon as i introduce another field they get written over each and display only one value for both fields. The to_mongo and from_mongo seem to be not workings properly. Please can any one help with this ? her is the code :
class LocalizedString
attr_accessor :translations
def self.from_mongo(value)
puts self.inspect
#translations ||= if value.is_a?(Hash)
value
elsif value.nil?
{}
else
{ I18n.locale.to_s => value }
end
#translations[I18n.locale.to_s]
end
def self.to_mongo(value)
puts self.inspect
if value.is_a?(Hash)
#translations = value
else
#translations[I18n.locale.to_s] = value
end
#translations
end
end
Thank alot
Rick
The problem is that from within your [to|from]_mongo methods, #translations refers to a class variable, not the instance variable you expect. So what's happening is that each time from_mongo is called, it overwrites the value.
A fixed version would be something like this:
class LocalizedString
attr_accessor :translations
def initialize( translations = {} )
#translations = translations
end
def self.from_mongo(value)
if value.is_a?(Hash)
LocalizedString.new(value)
elsif value.nil?
LocalizedString.new()
else
LocalizedString.new( { I18n.locale.to_s => value })
end
end
def self.to_mongo(value)
value.translations if value.present?
end
end
I found that jared's response didn't work for me -- I would get that translations was not found when using LocalizedString in an EmbeddedDocument.
I would get a similar problem on rick's solution where translations was nil when using embedded documents. To get a working solution, I took Rick's solution, changed the translation variable to be an instance variable so it wouldn't be overwritten for each new field that used LocalizedString, and then added a check to make sure translations wasn't nil (and create a new Hash if it was).
Of all the LocalizedString solutions floating around, this is the first time I've been able to get it working on EmbeddedDocuments and without the overwritting problem -- there still may be other issues! :)
class LocalizedString
attr_accessor :translations
def self.from_mongo(value)
puts self.inspect
translations ||= if value.is_a?(Hash)
value
elsif value.nil?
{}
else
{ I18n.locale.to_s => value }
end
translations[I18n.locale.to_s]
end
def self.to_mongo(value)
puts self.inspect
if value.is_a?(Hash)
translations = value
else
if translations.nil?
translations = Hash.new()
end
translations[I18n.locale.to_s] = value
end
translations
end
end
I found this post: which was very helpful. He extended HashWithIndifferentAccess to work as a LocalizedString. The only thing I didn't like about it was having to explicly specify the locale when setting it each time -- I wanted it to work more like a string. of course, you can't overload the = operator (at least I don't think you can) so I used <<, and added a to_s method that would output the string of the current locale....
class LocalizedString < HashWithIndifferentAccess
def self.from_mongo(value)
LocalizedString.new(value || {})
end
def available_locales
symbolize_keys.keys
end
def to_s
self[I18n.locale]
end
def in_current_locale=(value)
self[I18n.locale] = value
end
def << (value)
self[I18n.locale] = value
end
end
and then I have a class like:
class SimpleModel
include MongoMapper::Document
key :test, LocalizedString
end
and can do things like
I18n.locale = :en
a = SimpleModel.new
a.test << "English"
I18n.locale = :de
a.test << "German"
puts a.test # access the translation for the current locale
I18n.locale = :en
puts a.test # access the translation for the current locale
puts a.test[:de] # access a translation explicitly
puts a.test[:en]
puts a.test.inspect
and get
German
English
German
English
{"en"=>"English", "de"=>"German"}
so there we go -- this one actually seems to work for me. Comments welcome, and hope this helps someone!

Resources