Class: Alchemy::Resource

Inherits:
Object
  • Object
show all
Defined in:
lib/alchemy/resource.rb

Overview

Alchemy::Resource

Used to DRY up resource like structures in Alchemy's admin backend. So far Language, User and Tag already uses this.

It provides convenience methods to create an admin interface without further knowledge about the model and the controller (it's instantiated with controller_path at least and guesses the model accordingly)

For examples how to use in controllers see Alchemy::ResourcesController or inherit from it directly.

Naming Conventions

As Rails' form helpers, path helpers, etc. and declarative authorization rely on controller_path even if the model class is named differently (or sits in another namespace) model and controller are handled separatly here. Therefore “resource” always refers to the controller_path whereas “model” refers to the model class.

Skip attributes

Usually you don't want your users to see and edit all attributes provided by a model. Hence some default attributes, namely id, updated_at, created_at, creator_id and updater_id are not returned by Resource#attributes.

If you want to skip a different set of attributes just define a skipped_alchemy_resource_attributes class method in your model class that returns an array of strings.

Example

def self.skipped_alchemy_resource_attributes
  %w(id updated_at secret_token remote_ip)
end

Restrict attributes

Beside skipping certain attributes you can also restrict them. Restricted attributes can not be edited by the user but still be seen in the index view. No attributes are restricted by default.

Example

def self.restricted_alchemy_resource_attributes
  %w(synced_at remote_record_id)
end

Searchable attributes

By default all :text and :string based attributes are searchable in the admin interface. You can overwrite this behaviour by providing a set of attribute names that should be searchable instead.

Example

def self.searchable_alchemy_resource_attributes
  %w(remote_record_id firstname lastname age)
end

Resource relations

Alchemy::Resource can take care of ActiveRecord relations.

BelongsTo Relations

For belongs_to associations you will have to define a alchemy_resource_relations class method in your model class:

def self.alchemy_resource_relations
  {
    location: {attr_method: 'name', attr_type: 'string'},
    organizer: {attr_method: 'name', attr_type: 'string'}
  }
end

With this knowledge Resource#attributes will return location#name and organizer#name instead of location_id and organizer_id. Refer to Alchemy::ResourcesController for further details on usage.

Creation

Resource needs a controller_path at least. Without other arguments it will guess the model name from it and assume that the model doesn't live in an engine. Moreover model and controller has to follow Rails' naming convention:

Event -> EventsController

It will also strip “admin” automatically, so this is also valid:

Event -> Admin::EventsController

If your Resource and it's controllers are part of an engine you need to provide Alchemy's module_definition, so resource can provide the correct url_proxy. If you don't declare it in Alchemy, you need at least provide the following hash (i.e. if your engine is named EventEngine):

resource = Resource.new(controller_path, {"engine_name" => "event_engine"})

If you don't want to stick with these conventions you can separate model and controller by providing a model class (for example used by Alchemy's Tags admin interface):

resource = Resource.new('/admin/tags', {"engine_name"=>"alchemy"}, Gutentag::Tag)

Constant Summary collapse

DEFAULT_SKIPPED_ATTRIBUTES =
%w(id created_at creator_id)
DEFAULT_SKIPPED_ASSOCIATIONS =
%w(creator)
SEARCHABLE_COLUMN_TYPES =
[:string, :text]

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(controller_path, module_definition = nil, custom_model = nil) ⇒ Resource

Returns a new instance of Resource.


108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/alchemy/resource.rb', line 108

def initialize(controller_path, module_definition = nil, custom_model = nil)
  @controller_path = controller_path
  @module_definition = module_definition
  @model = (custom_model || guess_model_from_controller_path)
  if model.respond_to?(:alchemy_resource_relations)
    if !model.respond_to?(:reflect_on_all_associations)
      raise MissingActiveRecordAssociation
    end

    store_model_associations
    map_relations
  end
end

Instance Attribute Details

#modelObject (readonly)

Returns the value of attribute model.


102
103
104
# File 'lib/alchemy/resource.rb', line 102

def model
  @model
end

#model_associationsObject

Returns the value of attribute model_associations.


101
102
103
# File 'lib/alchemy/resource.rb', line 101

def model_associations
  @model_associations
end

#resource_relationsObject

Returns the value of attribute resource_relations.


101
102
103
# File 'lib/alchemy/resource.rb', line 101

def resource_relations
  @resource_relations
end

Instance Method Details

#attributesObject


164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/alchemy/resource.rb', line 164

def attributes
  @_attributes ||= model.columns.collect do |col|
    next if skipped_attributes.include?(col.name)

    {
      name: col.name,
      type: resource_column_type(col),
      relation: resource_relation(col.name),
      enum: enum_values_collection_for_select(col.name),
    }.delete_if { |_k, v| v.blank? }
  end.compact
end

#editable_attributesObject


198
199
200
# File 'lib/alchemy/resource.rb', line 198

def editable_attributes
  attributes.reject { |h| restricted_attributes.map(&:to_s).include?(h[:name].to_s) }
end

#engine_nameObject


226
227
228
# File 'lib/alchemy/resource.rb', line 226

def engine_name
  @module_definition && @module_definition["engine_name"]
end

#enum_values_collection_for_select(column_name) ⇒ Object


177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/alchemy/resource.rb', line 177

def enum_values_collection_for_select(column_name)
  enum = model.defined_enums[column_name]
  return if enum.blank?

  enum.keys.map do |key|
    [
      ::I18n.t(key, scope: [
        :activerecord, :attributes, model.model_name.i18n_key, "#{column_name}_values"
      ], default: key.humanize),
      key,
    ]
  end
end

#help_text_for(attribute) ⇒ Object

Returns a help text for resource's form or nil if no help text is available

Example:

de:
  alchemy:
    resource_help_texts:
      my_resource_name:
        attribute_name: This is the fancy help text

240
241
242
243
244
# File 'lib/alchemy/resource.rb', line 240

def help_text_for(attribute)
  ::I18n.translate!(attribute[:name], scope: [:alchemy, :resource_help_texts, resource_name])
rescue ::I18n::MissingTranslationData
  nil
end

#in_engine?Boolean

Returns:

  • (Boolean)

222
223
224
# File 'lib/alchemy/resource.rb', line 222

def in_engine?
  !engine_name.nil?
end

#model_association_namesObject

Returns an array of underscored association names


156
157
158
159
160
161
162
# File 'lib/alchemy/resource.rb', line 156

def model_association_names
  return unless model_associations

  model_associations.map do |assoc|
    assoc.name.to_sym
  end
end

#namespace_for_scopeObject


148
149
150
151
152
# File 'lib/alchemy/resource.rb', line 148

def namespace_for_scope
  namespace_array = namespace_diff
  namespace_array.delete(engine_name) if in_engine?
  namespace_array.map(&:to_sym) # Rails >= 6.0.3.7 needs symbols in polymorphic routes
end

#namespaced_resource_nameObject


134
135
136
137
138
# File 'lib/alchemy/resource.rb', line 134

def namespaced_resource_name
  @_namespaced_resource_name ||= begin
    namespaced_resources_name.to_s.singularize
  end.to_sym # Rails >= 6.0.3.7 needs symbols in polymorphic routes
end

#namespaced_resources_nameObject


140
141
142
143
144
145
146
# File 'lib/alchemy/resource.rb', line 140

def namespaced_resources_name
  @_namespaced_resources_name ||= begin
    resource_name_array = resource_array.dup
    resource_name_array.delete(engine_name) if in_engine?
    resource_name_array.join("_")
  end.to_sym # Rails >= 6.0.3.7 needs symbols in polymorphic routes
end

#resource_arrayObject


122
123
124
# File 'lib/alchemy/resource.rb', line 122

def resource_array
  @_resource_array ||= controller_path_array.reject { |el| el == "admin" }
end

#resource_nameObject


130
131
132
# File 'lib/alchemy/resource.rb', line 130

def resource_name
  @_resource_name ||= resources_name.singularize
end

#resources_nameObject


126
127
128
# File 'lib/alchemy/resource.rb', line 126

def resources_name
  @_resources_name ||= resource_array.last
end

#restricted_attributesObject

Return attributes that should be viewable but not editable.


248
249
250
251
252
253
254
# File 'lib/alchemy/resource.rb', line 248

def restricted_attributes
  if model.respond_to?(:restricted_alchemy_resource_attributes)
    model.restricted_alchemy_resource_attributes
  else
    []
  end
end

#search_field_nameObject

Search field input name

Joins all searchable attribute names into a Ransack compatible search query


218
219
220
# File 'lib/alchemy/resource.rb', line 218

def search_field_name
  searchable_attribute_names.join("_or_") + "_cont"
end

#searchable_attribute_namesObject

Returns all attribute names that are searchable in the admin interface


204
205
206
207
208
209
210
211
212
# File 'lib/alchemy/resource.rb', line 204

def searchable_attribute_names
  if model.respond_to?(:searchable_alchemy_resource_attributes)
    model.searchable_alchemy_resource_attributes
  else
    attributes.select { |a| searchable_attribute?(a) }
      .concat(searchable_relation_attributes(attributes))
      .collect { |h| h[:name] }
  end
end

#skipped_attributesObject

Return attributes that should neither be viewable nor editable.


258
259
260
261
262
263
264
# File 'lib/alchemy/resource.rb', line 258

def skipped_attributes
  if model.respond_to?(:skipped_alchemy_resource_attributes)
    model.skipped_alchemy_resource_attributes
  else
    DEFAULT_SKIPPED_ATTRIBUTES
  end
end

#sorted_attributesObject


191
192
193
194
195
196
# File 'lib/alchemy/resource.rb', line 191

def sorted_attributes
  @_sorted_attributes ||= attributes.
    sort_by  { |attr| attr[:name] == "name" ? 0 : 1 }.
    sort_by! { |attr| attr[:type] == :boolean ? 1 : 0 }.
    sort_by! { |attr| attr[:name] == "updated_at" ? 1 : 0 }
end