better_model 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +274 -208
- data/lib/better_model/archivable.rb +203 -92
- data/lib/better_model/errors/archivable/already_archived_error.rb +11 -0
- data/lib/better_model/errors/archivable/archivable_error.rb +13 -0
- data/lib/better_model/errors/archivable/configuration_error.rb +10 -0
- data/lib/better_model/errors/archivable/not_archived_error.rb +11 -0
- data/lib/better_model/errors/archivable/not_enabled_error.rb +11 -0
- data/lib/better_model/errors/better_model_error.rb +9 -0
- data/lib/better_model/errors/permissible/configuration_error.rb +9 -0
- data/lib/better_model/errors/permissible/permissible_error.rb +13 -0
- data/lib/better_model/errors/predicable/configuration_error.rb +9 -0
- data/lib/better_model/errors/predicable/predicable_error.rb +13 -0
- data/lib/better_model/errors/searchable/configuration_error.rb +9 -0
- data/lib/better_model/errors/searchable/invalid_order_error.rb +11 -0
- data/lib/better_model/errors/searchable/invalid_pagination_error.rb +11 -0
- data/lib/better_model/errors/searchable/invalid_predicate_error.rb +11 -0
- data/lib/better_model/errors/searchable/invalid_security_error.rb +11 -0
- data/lib/better_model/errors/searchable/searchable_error.rb +13 -0
- data/lib/better_model/errors/sortable/configuration_error.rb +10 -0
- data/lib/better_model/errors/sortable/sortable_error.rb +13 -0
- data/lib/better_model/errors/stateable/check_failed_error.rb +14 -0
- data/lib/better_model/errors/stateable/configuration_error.rb +10 -0
- data/lib/better_model/errors/stateable/invalid_state_error.rb +11 -0
- data/lib/better_model/errors/stateable/invalid_transition_error.rb +11 -0
- data/lib/better_model/errors/stateable/not_enabled_error.rb +11 -0
- data/lib/better_model/errors/stateable/stateable_error.rb +13 -0
- data/lib/better_model/errors/stateable/validation_failed_error.rb +11 -0
- data/lib/better_model/errors/statusable/configuration_error.rb +9 -0
- data/lib/better_model/errors/statusable/statusable_error.rb +13 -0
- data/lib/better_model/errors/taggable/configuration_error.rb +10 -0
- data/lib/better_model/errors/taggable/taggable_error.rb +13 -0
- data/lib/better_model/errors/traceable/configuration_error.rb +10 -0
- data/lib/better_model/errors/traceable/not_enabled_error.rb +11 -0
- data/lib/better_model/errors/traceable/traceable_error.rb +13 -0
- data/lib/better_model/errors/validatable/configuration_error.rb +10 -0
- data/lib/better_model/errors/validatable/not_enabled_error.rb +11 -0
- data/lib/better_model/errors/validatable/validatable_error.rb +13 -0
- data/lib/better_model/models/state_transition.rb +122 -0
- data/lib/better_model/models/version.rb +68 -0
- data/lib/better_model/permissible.rb +103 -52
- data/lib/better_model/predicable.rb +142 -131
- data/lib/better_model/repositable/base_repository.rb +232 -0
- data/lib/better_model/repositable.rb +32 -0
- data/lib/better_model/searchable.rb +123 -96
- data/lib/better_model/sortable.rb +137 -41
- data/lib/better_model/stateable/configurator.rb +103 -85
- data/lib/better_model/stateable/guard.rb +41 -21
- data/lib/better_model/stateable/transition.rb +64 -35
- data/lib/better_model/stateable.rb +43 -25
- data/lib/better_model/statusable.rb +84 -52
- data/lib/better_model/taggable.rb +120 -75
- data/lib/better_model/traceable.rb +56 -48
- data/lib/better_model/validatable/configurator.rb +54 -177
- data/lib/better_model/validatable.rb +88 -113
- data/lib/better_model/version.rb +1 -1
- data/lib/better_model.rb +42 -9
- data/lib/generators/better_model/repository/repository_generator.rb +141 -0
- data/lib/generators/better_model/repository/templates/application_repository.rb.tt +21 -0
- data/lib/generators/better_model/repository/templates/repository.rb.tt +71 -0
- data/lib/generators/better_model/stateable/templates/README +1 -1
- metadata +45 -14
- data/lib/better_model/schedulable/occurrence_calculator.rb +0 -1034
- data/lib/better_model/schedulable/schedule_builder.rb +0 -269
- data/lib/better_model/schedulable.rb +0 -356
- data/lib/better_model/state_transition.rb +0 -106
- data/lib/better_model/stateable/errors.rb +0 -45
- data/lib/better_model/validatable/business_rule_validator.rb +0 -47
- data/lib/better_model/validatable/order_validator.rb +0 -77
- data/lib/better_model/version_record.rb +0 -66
- data/lib/generators/better_model/taggable/taggable_generator.rb +0 -129
- data/lib/generators/better_model/taggable/templates/README.tt +0 -62
- data/lib/generators/better_model/taggable/templates/migration.rb.tt +0 -21
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module BetterModel
|
|
4
|
-
# StateTransition - Base ActiveRecord model for state transition history
|
|
5
|
-
#
|
|
6
|
-
# Questo è un modello abstract. Le classi concrete vengono generate dinamicamente
|
|
7
|
-
# per ogni tabella (state_transitions, order_transitions, etc.).
|
|
8
|
-
#
|
|
9
|
-
# Schema della tabella:
|
|
10
|
-
# t.string :transitionable_type, null: false
|
|
11
|
-
# t.integer :transitionable_id, null: false
|
|
12
|
-
# t.string :event, null: false
|
|
13
|
-
# t.string :from_state, null: false
|
|
14
|
-
# t.string :to_state, null: false
|
|
15
|
-
# t.json :metadata
|
|
16
|
-
# t.datetime :created_at, null: false
|
|
17
|
-
#
|
|
18
|
-
# Utilizzo:
|
|
19
|
-
# # Tutte le transizioni di un modello
|
|
20
|
-
# order.state_transitions
|
|
21
|
-
#
|
|
22
|
-
# # Query globali (tramite classi dinamiche)
|
|
23
|
-
# BetterModel::StateTransitions.for_model(Order)
|
|
24
|
-
# BetterModel::OrderTransitions.by_event(:confirm)
|
|
25
|
-
#
|
|
26
|
-
class StateTransition < ActiveRecord::Base
|
|
27
|
-
# Default table name (can be overridden by dynamic subclasses)
|
|
28
|
-
self.table_name = "state_transitions"
|
|
29
|
-
|
|
30
|
-
# Polymorphic association
|
|
31
|
-
belongs_to :transitionable, polymorphic: true
|
|
32
|
-
|
|
33
|
-
# Validations
|
|
34
|
-
validates :event, :from_state, :to_state, presence: true
|
|
35
|
-
|
|
36
|
-
# Scopes
|
|
37
|
-
|
|
38
|
-
# Scope per modello specifico
|
|
39
|
-
#
|
|
40
|
-
# @param model_class [Class] Classe del modello
|
|
41
|
-
# @return [ActiveRecord::Relation]
|
|
42
|
-
#
|
|
43
|
-
scope :for_model, ->(model_class) {
|
|
44
|
-
where(transitionable_type: model_class.name)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
# Scope per evento specifico
|
|
48
|
-
#
|
|
49
|
-
# @param event [Symbol, String] Nome dell'evento
|
|
50
|
-
# @return [ActiveRecord::Relation]
|
|
51
|
-
#
|
|
52
|
-
scope :by_event, ->(event) {
|
|
53
|
-
where(event: event.to_s)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
# Scope per stato di partenza
|
|
57
|
-
#
|
|
58
|
-
# @param state [Symbol, String] Stato di partenza
|
|
59
|
-
# @return [ActiveRecord::Relation]
|
|
60
|
-
#
|
|
61
|
-
scope :from_state, ->(state) {
|
|
62
|
-
where(from_state: state.to_s)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
# Scope per stato di arrivo
|
|
66
|
-
#
|
|
67
|
-
# @param state [Symbol, String] Stato di arrivo
|
|
68
|
-
# @return [ActiveRecord::Relation]
|
|
69
|
-
#
|
|
70
|
-
scope :to_state, ->(state) {
|
|
71
|
-
where(to_state: state.to_s)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
# Scope per transizioni recenti
|
|
75
|
-
#
|
|
76
|
-
# @param duration [ActiveSupport::Duration] Durata (es. 7.days)
|
|
77
|
-
# @return [ActiveRecord::Relation]
|
|
78
|
-
#
|
|
79
|
-
scope :recent, ->(duration = 7.days) {
|
|
80
|
-
where("created_at >= ?", duration.ago)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
# Scope per transizioni in un periodo
|
|
84
|
-
#
|
|
85
|
-
# @param start_time [Time, Date] Inizio periodo
|
|
86
|
-
# @param end_time [Time, Date] Fine periodo
|
|
87
|
-
# @return [ActiveRecord::Relation]
|
|
88
|
-
#
|
|
89
|
-
scope :between, ->(start_time, end_time) {
|
|
90
|
-
where(created_at: start_time..end_time)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
# Metodi di istanza
|
|
94
|
-
|
|
95
|
-
# Formatted description della transizione
|
|
96
|
-
#
|
|
97
|
-
# @return [String]
|
|
98
|
-
#
|
|
99
|
-
def description
|
|
100
|
-
"#{transitionable_type}##{transitionable_id}: #{from_state} -> #{to_state} (#{event})"
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
# Alias per retrocompatibilità
|
|
104
|
-
alias_method :to_s, :description
|
|
105
|
-
end
|
|
106
|
-
end
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module BetterModel
|
|
4
|
-
module Stateable
|
|
5
|
-
# Base error for all Stateable errors
|
|
6
|
-
class StateableError < StandardError; end
|
|
7
|
-
|
|
8
|
-
# Raised when Stateable is not enabled but methods are called
|
|
9
|
-
class NotEnabledError < StateableError
|
|
10
|
-
def initialize(msg = nil)
|
|
11
|
-
super(msg || "Stateable is not enabled. Add 'stateable do...end' to your model.")
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# Raised when an invalid state is referenced
|
|
16
|
-
class InvalidStateError < StateableError
|
|
17
|
-
def initialize(state)
|
|
18
|
-
super("Invalid state: #{state.inspect}")
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# Raised when trying to transition to an invalid state from current state
|
|
23
|
-
class InvalidTransitionError < StateableError
|
|
24
|
-
def initialize(event, from_state, to_state)
|
|
25
|
-
super("Cannot transition from #{from_state.inspect} to #{to_state.inspect} via #{event.inspect}")
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Raised when a guard condition fails
|
|
30
|
-
class GuardFailedError < StateableError
|
|
31
|
-
def initialize(event, guard_description = nil)
|
|
32
|
-
msg = "Guard failed for transition #{event.inspect}"
|
|
33
|
-
msg += ": #{guard_description}" if guard_description
|
|
34
|
-
super(msg)
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Raised when a transition validation fails
|
|
39
|
-
class ValidationFailedError < StateableError
|
|
40
|
-
def initialize(event, errors)
|
|
41
|
-
super("Validation failed for transition #{event.inspect}: #{errors.full_messages.join(', ')}")
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module BetterModel
|
|
4
|
-
module Validatable
|
|
5
|
-
# Validator per business rules custom
|
|
6
|
-
#
|
|
7
|
-
# Permette di eseguire metodi custom come validatori, delegando la logica
|
|
8
|
-
# di validazione complessa a metodi del modello.
|
|
9
|
-
#
|
|
10
|
-
# Il metodo della business rule deve aggiungere errori tramite `errors.add`
|
|
11
|
-
# se la validazione fallisce.
|
|
12
|
-
#
|
|
13
|
-
# Esempio:
|
|
14
|
-
# validates_with BusinessRuleValidator, rule_name: :valid_category
|
|
15
|
-
#
|
|
16
|
-
# # Nel modello:
|
|
17
|
-
# def valid_category
|
|
18
|
-
# unless Category.exists?(id: category_id)
|
|
19
|
-
# errors.add(:category_id, "must be a valid category")
|
|
20
|
-
# end
|
|
21
|
-
# end
|
|
22
|
-
#
|
|
23
|
-
class BusinessRuleValidator < ActiveModel::Validator
|
|
24
|
-
def initialize(options)
|
|
25
|
-
super
|
|
26
|
-
|
|
27
|
-
@rule_name = options[:rule_name]
|
|
28
|
-
|
|
29
|
-
unless @rule_name
|
|
30
|
-
raise ArgumentError, "BusinessRuleValidator requires :rule_name option"
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def validate(record)
|
|
35
|
-
# Verifica che il metodo esista
|
|
36
|
-
unless record.respond_to?(@rule_name, true)
|
|
37
|
-
raise NoMethodError, "Business rule method '#{@rule_name}' not found in #{record.class.name}. " \
|
|
38
|
-
"Define it in your model: def #{@rule_name}; ...; end"
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# Esegui il metodo della business rule
|
|
42
|
-
# Il metodo stesso è responsabile di aggiungere errori tramite errors.add
|
|
43
|
-
record.send(@rule_name)
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module BetterModel
|
|
4
|
-
module Validatable
|
|
5
|
-
# Validator per validazioni di ordine tra campi (cross-field)
|
|
6
|
-
#
|
|
7
|
-
# Verifica che un campo sia in una relazione d'ordine rispetto ad un altro campo.
|
|
8
|
-
# Supporta date/time (before/after) e numeri (lteq/gteq/lt/gt).
|
|
9
|
-
#
|
|
10
|
-
# Esempio:
|
|
11
|
-
# validates_with OrderValidator,
|
|
12
|
-
# attributes: [:starts_at],
|
|
13
|
-
# second_field: :ends_at,
|
|
14
|
-
# comparator: :before
|
|
15
|
-
#
|
|
16
|
-
class OrderValidator < ActiveModel::EachValidator
|
|
17
|
-
COMPARATORS = {
|
|
18
|
-
before: :<,
|
|
19
|
-
after: :>,
|
|
20
|
-
lteq: :<=,
|
|
21
|
-
gteq: :>=,
|
|
22
|
-
lt: :<,
|
|
23
|
-
gt: :>
|
|
24
|
-
}.freeze
|
|
25
|
-
|
|
26
|
-
def initialize(options)
|
|
27
|
-
super
|
|
28
|
-
|
|
29
|
-
@second_field = options[:second_field]
|
|
30
|
-
@comparator = options[:comparator]
|
|
31
|
-
|
|
32
|
-
unless @second_field
|
|
33
|
-
raise ArgumentError, "OrderValidator requires :second_field option"
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
unless COMPARATORS.key?(@comparator)
|
|
37
|
-
raise ArgumentError, "Invalid comparator: #{@comparator}. Valid: #{COMPARATORS.keys.join(', ')}"
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def validate_each(record, attribute, value)
|
|
42
|
-
second_value = record.send(@second_field)
|
|
43
|
-
|
|
44
|
-
# Skip validation if either field is nil (use presence validation for that)
|
|
45
|
-
return if value.nil? || second_value.nil?
|
|
46
|
-
|
|
47
|
-
# Get the comparison operator
|
|
48
|
-
operator = COMPARATORS[@comparator]
|
|
49
|
-
|
|
50
|
-
# Perform comparison
|
|
51
|
-
unless value.send(operator, second_value)
|
|
52
|
-
record.errors.add(attribute, error_message(attribute, @comparator, @second_field))
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
private
|
|
57
|
-
|
|
58
|
-
def error_message(first_field, comparator, second_field)
|
|
59
|
-
# Messaggi user-friendly basati sul comparatore
|
|
60
|
-
case comparator
|
|
61
|
-
when :before
|
|
62
|
-
"must be before #{second_field.to_s.humanize.downcase}"
|
|
63
|
-
when :after
|
|
64
|
-
"must be after #{second_field.to_s.humanize.downcase}"
|
|
65
|
-
when :lteq
|
|
66
|
-
"must be less than or equal to #{second_field.to_s.humanize.downcase}"
|
|
67
|
-
when :gteq
|
|
68
|
-
"must be greater than or equal to #{second_field.to_s.humanize.downcase}"
|
|
69
|
-
when :lt
|
|
70
|
-
"must be less than #{second_field.to_s.humanize.downcase}"
|
|
71
|
-
when :gt
|
|
72
|
-
"must be greater than #{second_field.to_s.humanize.downcase}"
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module BetterModel
|
|
4
|
-
# Version model for tracking changes
|
|
5
|
-
# This is the base AR model for version history
|
|
6
|
-
# Actual table_name is set dynamically in subclasses
|
|
7
|
-
class Version < ActiveRecord::Base
|
|
8
|
-
self.abstract_class = true
|
|
9
|
-
|
|
10
|
-
# Polymorphic association to the tracked model
|
|
11
|
-
belongs_to :item, polymorphic: true, optional: true
|
|
12
|
-
|
|
13
|
-
# Optional: belongs_to user who made the change
|
|
14
|
-
# belongs_to :updated_by, class_name: "User", optional: true
|
|
15
|
-
|
|
16
|
-
# Serialize object_changes as JSON
|
|
17
|
-
# Rails handles this automatically for json/jsonb columns
|
|
18
|
-
|
|
19
|
-
# Validations
|
|
20
|
-
validates :item_type, :event, presence: true
|
|
21
|
-
validates :event, inclusion: { in: %w[created updated destroyed] }
|
|
22
|
-
|
|
23
|
-
# Scopes
|
|
24
|
-
scope :for_item, ->(item) { where(item_type: item.class.name, item_id: item.id) }
|
|
25
|
-
scope :created_events, -> { where(event: "created") }
|
|
26
|
-
scope :updated_events, -> { where(event: "updated") }
|
|
27
|
-
scope :destroyed_events, -> { where(event: "destroyed") }
|
|
28
|
-
scope :by_user, ->(user_id) { where(updated_by_id: user_id) }
|
|
29
|
-
scope :between, ->(start_time, end_time) { where(created_at: start_time..end_time) }
|
|
30
|
-
scope :recent, ->(limit = 10) { order(created_at: :desc).limit(limit) }
|
|
31
|
-
|
|
32
|
-
# Get the change for a specific field
|
|
33
|
-
#
|
|
34
|
-
# @param field_name [Symbol, String] Field name
|
|
35
|
-
# @return [Hash, nil] Hash with :before and :after keys
|
|
36
|
-
def change_for(field_name)
|
|
37
|
-
return nil unless object_changes
|
|
38
|
-
|
|
39
|
-
field = field_name.to_s
|
|
40
|
-
return nil unless object_changes.key?(field)
|
|
41
|
-
|
|
42
|
-
{
|
|
43
|
-
before: object_changes[field][0],
|
|
44
|
-
after: object_changes[field][1]
|
|
45
|
-
}
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Check if a specific field changed in this version
|
|
49
|
-
# This method overrides ActiveRecord's changed? to accept a field_name parameter
|
|
50
|
-
#
|
|
51
|
-
# @param field_name [Symbol, String, nil] Field name (if nil, calls ActiveRecord's changed?)
|
|
52
|
-
# @return [Boolean]
|
|
53
|
-
def changed?(field_name = nil)
|
|
54
|
-
return super() if field_name.nil?
|
|
55
|
-
|
|
56
|
-
object_changes&.key?(field_name.to_s) || false
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Get list of changed fields
|
|
60
|
-
#
|
|
61
|
-
# @return [Array<String>]
|
|
62
|
-
def changed_fields
|
|
63
|
-
object_changes&.keys || []
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "rails/generators"
|
|
4
|
-
require "rails/generators/migration"
|
|
5
|
-
|
|
6
|
-
module BetterModel
|
|
7
|
-
module Generators
|
|
8
|
-
class TaggableGenerator < Rails::Generators::NamedBase
|
|
9
|
-
include Rails::Generators::Migration
|
|
10
|
-
|
|
11
|
-
source_root File.expand_path("templates", __dir__)
|
|
12
|
-
|
|
13
|
-
class_option :column_name, type: :string, default: "tags",
|
|
14
|
-
desc: "Name of the tags column (default: tags)"
|
|
15
|
-
class_option :skip_index, type: :boolean, default: false,
|
|
16
|
-
desc: "Skip adding GIN index (PostgreSQL only)"
|
|
17
|
-
|
|
18
|
-
def self.next_migration_number(dirname)
|
|
19
|
-
next_migration_number = current_migration_number(dirname) + 1
|
|
20
|
-
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def create_migration_file
|
|
24
|
-
migration_template "migration.rb.tt",
|
|
25
|
-
"db/migrate/add_#{column_name}_to_#{table_name}.rb"
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def show_readme
|
|
29
|
-
# Display README as template to interpolate variables
|
|
30
|
-
if behavior == :invoke
|
|
31
|
-
say "=" * 79, :green
|
|
32
|
-
say ""
|
|
33
|
-
say "Taggable column has been added to your model!"
|
|
34
|
-
say ""
|
|
35
|
-
say "Next steps:"
|
|
36
|
-
say ""
|
|
37
|
-
say "1. Run the migration:"
|
|
38
|
-
say " $ bin/rails db:migrate"
|
|
39
|
-
say ""
|
|
40
|
-
say "2. Add Taggable configuration to your model:"
|
|
41
|
-
say ""
|
|
42
|
-
say " class #{model_name} < ApplicationRecord"
|
|
43
|
-
say " include BetterModel"
|
|
44
|
-
say ""
|
|
45
|
-
if postgresql?
|
|
46
|
-
say " # PostgreSQL: No additional configuration needed"
|
|
47
|
-
else
|
|
48
|
-
say " # SQLite/MySQL: Add serialization for the tags column"
|
|
49
|
-
say " serialize :#{column_name}, coder: JSON, type: Array"
|
|
50
|
-
say ""
|
|
51
|
-
end
|
|
52
|
-
say " taggable do"
|
|
53
|
-
say " tag_field :#{column_name} # Column name (default: :tags)"
|
|
54
|
-
say " normalize true # Convert to lowercase"
|
|
55
|
-
say " strip true # Remove whitespace (default)"
|
|
56
|
-
say " min_length 2 # Minimum tag length"
|
|
57
|
-
say " max_length 30 # Maximum tag length"
|
|
58
|
-
say " validates_tags minimum: 1, maximum: 10"
|
|
59
|
-
say " end"
|
|
60
|
-
say " end"
|
|
61
|
-
say ""
|
|
62
|
-
say "3. Usage examples:"
|
|
63
|
-
say ""
|
|
64
|
-
say " # Add tags"
|
|
65
|
-
say " #{name.underscore}.tag_with(\"ruby\", \"rails\", \"web\")"
|
|
66
|
-
say ""
|
|
67
|
-
say " # Remove tags"
|
|
68
|
-
say " #{name.underscore}.untag(\"web\")"
|
|
69
|
-
say ""
|
|
70
|
-
say " # Replace all tags"
|
|
71
|
-
say " #{name.underscore}.retag(\"ruby\", \"api\")"
|
|
72
|
-
say ""
|
|
73
|
-
say " # Check for tag"
|
|
74
|
-
say " #{name.underscore}.tagged_with?(\"ruby\") # => true"
|
|
75
|
-
say ""
|
|
76
|
-
say " # CSV interface"
|
|
77
|
-
say " #{name.underscore}.tag_list = \"ruby, rails, tutorial\""
|
|
78
|
-
say " #{name.underscore}.tag_list # => \"ruby, rails, tutorial\""
|
|
79
|
-
say ""
|
|
80
|
-
say " # Search with tags (Predicable integration)"
|
|
81
|
-
say " #{model_name}.#{column_name}_contains(\"ruby\")"
|
|
82
|
-
say " #{model_name}.#{column_name}_overlaps([\"ruby\", \"python\"])"
|
|
83
|
-
say " #{model_name}.#{column_name}_contains_all([\"ruby\", \"rails\"])"
|
|
84
|
-
say ""
|
|
85
|
-
say " # Statistics"
|
|
86
|
-
say " #{model_name}.tag_counts # => {\"ruby\" => 45, \"rails\" => 38}"
|
|
87
|
-
say " #{model_name}.popular_tags(limit: 10) # => [[\"ruby\", 45], [\"rails\", 38]]"
|
|
88
|
-
say " #{model_name}.related_tags(\"ruby\", limit: 5) # => [\"rails\", \"gem\", \"tutorial\"]"
|
|
89
|
-
say ""
|
|
90
|
-
say "For more information, see: docs/taggable.md"
|
|
91
|
-
say ""
|
|
92
|
-
say "=" * 79, :green
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
private
|
|
97
|
-
|
|
98
|
-
def table_name
|
|
99
|
-
name.tableize
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def model_name
|
|
103
|
-
name.camelize
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def migration_class_name
|
|
107
|
-
"Add#{column_name.camelize}To#{name.camelize.pluralize}"
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def column_name
|
|
111
|
-
options[:column_name]
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def add_index?
|
|
115
|
-
!options[:skip_index]
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def postgresql?
|
|
119
|
-
# Check if PostgreSQL adapter is being used
|
|
120
|
-
# Note: This checks the current adapter, which may not reflect production
|
|
121
|
-
# The migration template includes runtime detection as well
|
|
122
|
-
return false unless defined?(ActiveRecord::Base)
|
|
123
|
-
|
|
124
|
-
adapter_name = ActiveRecord::Base.connection.adapter_name rescue nil
|
|
125
|
-
adapter_name == "PostgreSQL"
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
end
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
===============================================================================
|
|
2
|
-
|
|
3
|
-
Taggable column has been added to your model!
|
|
4
|
-
|
|
5
|
-
Next steps:
|
|
6
|
-
|
|
7
|
-
1. Run the migration:
|
|
8
|
-
$ bin/rails db:migrate
|
|
9
|
-
|
|
10
|
-
2. Add Taggable configuration to your model:
|
|
11
|
-
|
|
12
|
-
class <%= model_name %> < ApplicationRecord
|
|
13
|
-
include BetterModel
|
|
14
|
-
|
|
15
|
-
<% if postgresql? -%>
|
|
16
|
-
# PostgreSQL: No additional configuration needed
|
|
17
|
-
<% else -%>
|
|
18
|
-
# SQLite/MySQL: Add serialization for the tags column
|
|
19
|
-
serialize :<%= column_name %>, coder: JSON, type: Array
|
|
20
|
-
|
|
21
|
-
<% end -%>
|
|
22
|
-
taggable do
|
|
23
|
-
tag_field :<%= column_name %> # Column name (default: :tags)
|
|
24
|
-
normalize true # Convert to lowercase
|
|
25
|
-
strip true # Remove whitespace (default)
|
|
26
|
-
min_length 2 # Minimum tag length
|
|
27
|
-
max_length 30 # Maximum tag length
|
|
28
|
-
validates_tags minimum: 1, maximum: 10
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
3. Usage examples:
|
|
33
|
-
|
|
34
|
-
# Add tags
|
|
35
|
-
product.tag_with("ruby", "rails", "web")
|
|
36
|
-
|
|
37
|
-
# Remove tags
|
|
38
|
-
product.untag("web")
|
|
39
|
-
|
|
40
|
-
# Replace all tags
|
|
41
|
-
product.retag("ruby", "api")
|
|
42
|
-
|
|
43
|
-
# Check for tag
|
|
44
|
-
product.tagged_with?("ruby") # => true
|
|
45
|
-
|
|
46
|
-
# CSV interface
|
|
47
|
-
product.tag_list = "ruby, rails, tutorial"
|
|
48
|
-
product.tag_list # => "ruby, rails, tutorial"
|
|
49
|
-
|
|
50
|
-
# Search with tags (Predicable integration)
|
|
51
|
-
<%= model_name %>.<%= column_name %>_contains("ruby")
|
|
52
|
-
<%= model_name %>.<%= column_name %>_overlaps(["ruby", "python"])
|
|
53
|
-
<%= model_name %>.<%= column_name %>_contains_all(["ruby", "rails"])
|
|
54
|
-
|
|
55
|
-
# Statistics
|
|
56
|
-
<%= model_name %>.tag_counts # => {"ruby" => 45, "rails" => 38}
|
|
57
|
-
<%= model_name %>.popular_tags(limit: 10) # => [["ruby", 45], ["rails", 38]]
|
|
58
|
-
<%= model_name %>.related_tags("ruby", limit: 5) # => ["rails", "gem", "tutorial"]
|
|
59
|
-
|
|
60
|
-
For more information, see: docs/taggable.md
|
|
61
|
-
|
|
62
|
-
===============================================================================
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
2
|
-
def change
|
|
3
|
-
change_table :<%= table_name %> do |t|
|
|
4
|
-
# Tags column - database-specific implementation
|
|
5
|
-
if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
|
|
6
|
-
# PostgreSQL: Use native array with default empty array
|
|
7
|
-
t.string :<%= column_name %>, array: true, default: []
|
|
8
|
-
else
|
|
9
|
-
# SQLite/MySQL: Use text column (requires serialization in model)
|
|
10
|
-
t.text :<%= column_name %>
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
<% if add_index? -%>
|
|
14
|
-
|
|
15
|
-
# Add index for PostgreSQL array searches (GIN index for better performance)
|
|
16
|
-
if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
|
|
17
|
-
add_index :<%= table_name %>, :<%= column_name %>, using: 'gin'
|
|
18
|
-
end
|
|
19
|
-
<% end -%>
|
|
20
|
-
end
|
|
21
|
-
end
|