mobility 0.1.10 → 0.1.11

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,69 @@
1
+ # frozen-string-literal: true
2
+ require "rails/generators/active_record/migration/migration_generator"
3
+
4
+ module Mobility
5
+ module BackendGenerators
6
+ class Base < ::Rails::Generators::NamedBase
7
+ argument :attributes, type: :array, default: []
8
+ include ::ActiveRecord::Generators::Migration
9
+
10
+ def create_migration_file
11
+ if self.class.migration_exists?(migration_dir, migration_file)
12
+ ::Kernel.warn "Migration already exists: #{migration_file}"
13
+ else
14
+ migration_template "#{template}.rb", "db/migrate/#{migration_file}.rb"
15
+ end
16
+ end
17
+
18
+ def self.next_migration_number(dirname)
19
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
20
+ end
21
+
22
+ def backend
23
+ self.class.name.split('::').last.gsub(/Backend$/,'').underscore
24
+ end
25
+
26
+ protected
27
+
28
+ def attributes_with_index
29
+ attributes.select { |a| !a.reference? && a.has_index? }
30
+ end
31
+
32
+ private
33
+
34
+ def check_data_source!
35
+ unless data_source_exists?
36
+ raise NoTableDefined, "The table #{table_name} does not exist. Create it first before generating translated columns."
37
+ end
38
+ end
39
+
40
+ def data_source_exists?
41
+ connection.data_source_exists?(table_name)
42
+ end
43
+
44
+ delegate :connection, to: ::ActiveRecord::Base
45
+
46
+ def truncate_index_name(index_name)
47
+ if index_name.size < connection.index_name_length
48
+ index_name
49
+ else
50
+ "index_#{Digest::SHA1.hexdigest(index_name)}"[0, connection.index_name_length].freeze
51
+ end
52
+ end
53
+
54
+ def template
55
+ "#{backend}_translations".freeze
56
+ end
57
+
58
+ def migration_dir
59
+ File.expand_path("db/migrate".freeze)
60
+ end
61
+
62
+ def migration_file
63
+ "create_#{file_name}_#{attributes.map(&:name).join('_and_')}_translations_for_mobility_#{backend}_backend".freeze
64
+ end
65
+ end
66
+
67
+ class NoTableDefined < StandardError; end
68
+ end
69
+ end
@@ -0,0 +1,15 @@
1
+ # frozen-string-literal: true
2
+ require "rails/generators"
3
+
4
+ module Mobility
5
+ module BackendGenerators
6
+ class ColumnBackend < Mobility::BackendGenerators::Base
7
+ source_root File.expand_path("../../templates", __FILE__)
8
+
9
+ def initialize(*args)
10
+ super
11
+ check_data_source!
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,42 @@
1
+ # frozen-string-literal: true
2
+ require "rails/generators"
3
+
4
+ module Mobility
5
+ module BackendGenerators
6
+ class TableBackend < Mobility::BackendGenerators::Base
7
+ source_root File.expand_path("../../templates", __FILE__)
8
+
9
+ def create_migration_file
10
+ if data_source_exists? && !self.class.migration_exists?(migration_dir, migration_file)
11
+ migration_template "#{backend}_migration.rb", "db/migrate/#{migration_file}.rb"
12
+ else
13
+ super
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ alias_method :model_table_name, :table_name
20
+ def table_name
21
+ model_table_name = super
22
+ "#{model_table_name.singularize}_translations"
23
+ end
24
+
25
+ def foreign_key
26
+ "#{model_table_name.singularize}_id"
27
+ end
28
+
29
+ def translation_index_name
30
+ truncate_index_name("index_#{table_name}_on_#{foreign_key}")
31
+ end
32
+
33
+ def translation_locale_index_name
34
+ truncate_index_name("index_#{table_name}_on_locale")
35
+ end
36
+
37
+ def translation_unique_index_name
38
+ truncate_index_name("index_#{table_name}_on_#{foreign_key}_and_locale")
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,5 @@
1
+ require "rails/generators"
2
+
3
+ require_relative "./install_generator"
4
+ require_relative "./translations_generator"
5
+ require_relative "./backend_generators/base"
@@ -0,0 +1,17 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ <% attributes.each do |attribute| -%>
4
+ <% I18n.available_locales.each do |locale| -%>
5
+ <% column_name = Mobility.normalize_locale_accessor(attribute.name, locale) -%>
6
+ <% if connection.column_exists?(table_name, column_name) -%>
7
+ <% warn "#{column_name} already exists, skipping." %>
8
+ <% else -%>
9
+ add_column :<%= table_name %>, :<%= column_name %>, :<%= attribute.type %><%= attribute.inject_options %>
10
+ <%- if attribute.has_index? -%>
11
+ add_index :<%= table_name %>, :<%= column_name %><%= attribute.inject_index_options %>
12
+ <%- end -%>
13
+ <% end -%>
14
+ <% end -%>
15
+ <% end -%>
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ <% attributes.each do |attribute| -%>
4
+ <%- if attribute.reference? -%>
5
+ add_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %>
6
+ <%- elsif attribute.token? -%>
7
+ add_column :<%= table_name %>, :<%= attribute.name %>, :string<%= attribute.inject_options %>
8
+ add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true
9
+ <%- else -%>
10
+ add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
11
+ <%- if attribute.has_index? -%>
12
+ add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
13
+ <%- end -%>
14
+ <%- end -%>
15
+ <% end -%>
16
+ end
17
+ end
@@ -0,0 +1,28 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ create_table :<%= table_name %><%= primary_key_type %> do |t|
4
+
5
+ # Translated attribute(s)
6
+ <% attributes.each do |attribute| -%>
7
+ <% if attribute.token? -%>
8
+ t.string :<%= attribute.name %><%= attribute.inject_options %>
9
+ <% else -%>
10
+ t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
11
+ <% end -%>
12
+ <% end -%>
13
+
14
+ t.string :locale, null: false
15
+ t.integer :<%= foreign_key %>, null: false
16
+
17
+ t.timestamps null: false
18
+ end
19
+
20
+ add_index :<%= table_name %>, :<%= foreign_key %>, name: :<%= translation_index_name %>
21
+ add_index :<%= table_name %>, :locale, name: :<%= translation_locale_index_name %>
22
+ add_index :<%= table_name %>, [:<%= foreign_key %>, :locale], name: :<%= translation_unique_index_name %>, unique: true
23
+
24
+ <%- attributes_with_index.each do |attribute| -%>
25
+ add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
26
+ <%- end -%>
27
+ end
28
+ end
@@ -0,0 +1,75 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Mobility
4
+ =begin
5
+
6
+ Generator to create translation tables or add translation columns to a model
7
+ table, for either Table or Column backends.
8
+
9
+ ==Usage
10
+
11
+ To add translations for a string attribute +title+ to a model +Post+, call the
12
+ generator with:
13
+
14
+ rails generate mobility:translations post title:string
15
+
16
+ Here, the backend is implicit in the value of +Mobility.default_backend+, but
17
+ it can be explicitly set using the +backend+ option:
18
+
19
+ rails generate mobility:translations post title:string --backend=table
20
+
21
+ For the +table+ backend, the generator will either create a translation table
22
+ (in this case, +post_translations+) or add columns to the table if it already
23
+ exists.
24
+
25
+ For the +column+ backend, the generator will add columns for all locales in
26
+ +I18n.available_locales+. If some columns already exist, they will simply be
27
+ skipped.
28
+
29
+ Other backends are not supported, for obvious reasons:
30
+ * the +key_value+ backend does not need any model-specific migrations, simply
31
+ run the install generator.
32
+ * +jsonb+, +hstore+ and +serialized+ backends simply require a single column on
33
+ a model table, which can be added with the normal Rails migration generator.
34
+
35
+ =end
36
+ class TranslationsGenerator < ::Rails::Generators::NamedBase
37
+ SUPPORTED_BACKENDS = %w[column table]
38
+ BACKEND_OPTIONS = { type: :string, desc: "Backend to use for translations (defaults to Mobility.default_backend)".freeze }
39
+ argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]"
40
+
41
+ class_option(:backend, BACKEND_OPTIONS)
42
+ invoke_from_option :backend
43
+
44
+ def self.class_options(options = nil)
45
+ super
46
+ @class_options[:backend] = Thor::Option.new(:backend, BACKEND_OPTIONS.merge(default: Mobility.default_backend.to_s.freeze))
47
+ @class_options
48
+ end
49
+
50
+ def self.prepare_for_invocation(name, value)
51
+ if name == :backend
52
+ if SUPPORTED_BACKENDS.include?(value)
53
+ require_relative "./backend_generators/#{value}_backend".freeze
54
+ Mobility::BackendGenerators.const_get("#{value}_backend".camelcase.freeze)
55
+ elsif Mobility::Backend.const_get(value.to_s.camelize.gsub(/\s+/, ''.freeze))
56
+ raise Thor::Error, "The #{value} backend does not have a translations generator."
57
+ else
58
+ raise Thor::Error, "#{value} is not a Mobility backend."
59
+ end
60
+ else
61
+ super
62
+ end
63
+ end
64
+
65
+ protected
66
+
67
+ def say_status(status, message, *args)
68
+ if status == :invoke && SUPPORTED_BACKENDS.include?(message)
69
+ super(status, "#{message}_backend".freeze, *args)
70
+ else
71
+ super
72
+ end
73
+ end
74
+ end
75
+ end
data/lib/mobility.rb CHANGED
@@ -208,6 +208,15 @@ module Mobility
208
208
  "#{attribute}_#{normalize_locale(locale)}".freeze
209
209
  end
210
210
 
211
+ # Raises InvalidLocale exception if the locale passed in is present but not available.
212
+ # @param [String,Symbol] locale
213
+ # @raise [InvalidLocale] if locale is present but not available
214
+ def enforce_available_locales!(locale)
215
+ if I18n.enforce_available_locales
216
+ raise Mobility::InvalidLocale.new(locale) unless (I18n.locale_available?(locale) || locale.nil?)
217
+ end
218
+ end
219
+
211
220
  protected
212
221
 
213
222
  def read_locale
@@ -216,9 +225,7 @@ module Mobility
216
225
 
217
226
  def set_locale(locale)
218
227
  locale = locale.to_sym if locale
219
- if I18n.enforce_available_locales
220
- raise Mobility::InvalidLocale.new(locale) unless (I18n.available_locales.include?(locale) || locale.nil?)
221
- end
228
+ enforce_available_locales!(locale)
222
229
  storage[:mobility_locale] = locale
223
230
  end
224
231
  end
@@ -137,6 +137,7 @@ with other backends.
137
137
  if (options[:dirty] && options[:fallthrough_accessors] != false)
138
138
  options[:fallthrough_accessors] = true
139
139
  end
140
+ include FallthroughAccessors.new(attributes) if options[:fallthrough_accessors]
140
141
 
141
142
  @backend_class.configure!(options) if @backend_class.respond_to?(:configure!)
142
143
 
@@ -158,8 +159,8 @@ with other backends.
158
159
  end
159
160
  end
160
161
 
161
- define_method "#{attribute}=" do |value|
162
- mobility_set(attribute, value)
162
+ define_method "#{attribute}=" do |value, **options|
163
+ mobility_set(attribute, value, **options)
163
164
  end if %i[accessor writer].include?(method)
164
165
 
165
166
  define_locale_accessors(attribute, @accessor_locales) if @accessor_locales
@@ -187,7 +188,6 @@ with other backends.
187
188
  backend_class.include(Backend::Cache) unless options[:cache] == false
188
189
  backend_class.include(Backend::Dirty.for(options[:model_class])) if options[:dirty]
189
190
  backend_class.include(Backend::Fallbacks) unless options[:fallbacks] == false
190
- backend_class.include(FallthroughAccessors.new(attributes)) if options[:fallthrough_accessors]
191
191
  end
192
192
 
193
193
  def define_backend(attribute)
@@ -215,7 +215,7 @@ with other backends.
215
215
 
216
216
  def get_backend_class(backend: nil, model_class: nil)
217
217
  raise Mobility::BackendRequired, "Backend option required if Mobility.config.default_backend is not set." if backend.nil?
218
- klass = Module === backend ? backend : Mobility::Backend.const_get(backend.to_s.camelize.gsub(/\s+/, ''))
218
+ klass = Module === backend ? backend : Mobility::Backend.const_get(backend.to_s.camelize.gsub(/\s+/, ''.freeze).freeze)
219
219
  model_class.nil? ? klass : klass.for(model_class)
220
220
  end
221
221
  end
@@ -78,8 +78,6 @@ value of the translated attribute if passed to it.
78
78
  private :restore_attribute!
79
79
  end
80
80
  model_class.include restore_methods
81
-
82
- model_class.include(FallthroughAccessors.new(*attributes))
83
81
  end
84
82
  end
85
83
  end
@@ -4,6 +4,15 @@ module Mobility
4
4
 
5
5
  Implements the {Mobility::Backend::Column} backend for ActiveRecord models.
6
6
 
7
+ You can use the +mobility:translations+ generator to create a migration adding
8
+ translatable columns to the model table with:
9
+
10
+ rails generate mobility:translations post title:string
11
+
12
+ The generated migration will add columns +title_<locale>+ for every locale in
13
+ +I18n.available_locales+. (The generator can be run again to add new attributes
14
+ or locales.)
15
+
7
16
  @note This backend disables the +locale_accessors+ option, which would
8
17
  otherwise interfere with column methods.
9
18
 
@@ -27,11 +36,15 @@ Implements the {Mobility::Backend::Column} backend for ActiveRecord models.
27
36
 
28
37
  # @!group Backend Accessors
29
38
  # @!macro backend_reader
30
- # @!method read(locale, **options)
39
+ def read(locale, **_)
40
+ model.read_attribute(column(locale))
41
+ end
31
42
 
32
43
  # @!group Backend Accessors
33
44
  # @!macro backend_writer
34
- # @!method write(locale, value, **options)
45
+ def write(locale, value, **_)
46
+ model.write_attribute(column(locale), value)
47
+ end
35
48
 
36
49
  # @!group Backend Configuration
37
50
  def self.configure!(options)
@@ -6,6 +6,15 @@ module Mobility
6
6
 
7
7
  Implements the {Mobility::Backend::Table} backend for ActiveRecord models.
8
8
 
9
+ To generate a translation table for a model +Post+, you can use the included
10
+ +mobility:translations+ generator:
11
+
12
+ rails generate mobility:translations post title:string content:text
13
+
14
+ This will create a migration which can be run to create the translation table.
15
+ If the translation table already exists, it will create a migration adding
16
+ columns to that table.
17
+
9
18
  @example Model with table backend
10
19
  class Post < ActiveRecord::Base
11
20
  translates :title, backend: :table, association_name: :translations
@@ -2,10 +2,18 @@ module Mobility
2
2
  module Backend
3
3
  =begin
4
4
 
5
- Stores translated attribute as a column on the model table.
5
+ Stores translated attribute as a column on the model table. To use this
6
+ backend, ensure that the model table has columns named +<attribute>_<locale>+
7
+ for every locale in +I18n.available_locales+.
6
8
 
7
- To use this backend, ensure that the model table has columns named
8
- +<attribute>_<locale>+ for every locale in +I18n.available_locales+.
9
+ If you are using Rails, you can use the +mobility:translations+ generator to
10
+ create a migration adding these columns to the model table with:
11
+
12
+ rails generate mobility:translations post title:string
13
+
14
+ The generated migration will add columns +title_<locale>+ for every locale in
15
+ +I18n.available_locales+. (The generator can be run again to add new attributes
16
+ or locales.)
9
17
 
10
18
  ==Backend Options
11
19
 
@@ -19,19 +27,6 @@ be ignored if set, since it would cause a conflict with column accessors.
19
27
  module Column
20
28
  include OrmDelegator
21
29
 
22
- # @!group Backend Accessors
23
- #
24
- # @!macro backend_reader
25
- def read(locale, **_)
26
- model.send(column(locale))
27
- end
28
-
29
- # @!macro backend_writer
30
- def write(locale, value, **_)
31
- model.send("#{column(locale)}=", value)
32
- end
33
- # @!endgroup
34
-
35
30
  # Returns name of column where translated attribute is stored
36
31
  # @param [Symbol] locale
37
32
  # @return [String]