class Alchemy::Resource
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)
Constants
- DEFAULT_SKIPPED_ASSOCIATIONS
- DEFAULT_SKIPPED_ATTRIBUTES
- SEARCHABLE_COLUMN_TYPES
Attributes
Public Class Methods
# 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
Public Instance Methods
# 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
# File lib/alchemy/resource.rb, line 198 def editable_attributes attributes.reject { |h| restricted_attributes.map(&:to_s).include?(h[:name].to_s) } end
# File lib/alchemy/resource.rb, line 226 def engine_name @module_definition && @module_definition["engine_name"] end
# 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
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
# 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
# File lib/alchemy/resource.rb, line 222 def in_engine? !engine_name.nil? end
Returns an array of underscored association names
# 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
# 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
# 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
# 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
# File lib/alchemy/resource.rb, line 122 def resource_array @_resource_array ||= controller_path_array.reject { |el| el == "admin" } end
# File lib/alchemy/resource.rb, line 130 def resource_name @_resource_name ||= resources_name.singularize end
# File lib/alchemy/resource.rb, line 126 def resources_name @_resources_name ||= resource_array.last end
Return attributes that should be viewable but not editable.
# 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 input name
Joins all searchable attribute names into a Ransack compatible search query
# File lib/alchemy/resource.rb, line 218 def search_field_name searchable_attribute_names.join("_or_") + "_cont" end
Returns all attribute names that are searchable in the admin interface
# 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
Return attributes that should neither be viewable nor editable.
# 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
# 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
Private Instance Methods
Returns activerecord association that has the given name
# File lib/alchemy/resource.rb, line 339 def association_from_relation_name(name) model_associations.detect { |a| a.name == name.to_sym } end
# File lib/alchemy/resource.rb, line 292 def controller_path_array @controller_path.split("/") end
# File lib/alchemy/resource.rb, line 288 def guess_model_from_controller_path resource_array.join("/").classify.constantize end
Expands the resource_relations
hash with matching activerecord associations data.
# File lib/alchemy/resource.rb, line 318 def map_relations self.resource_relations = {} model.alchemy_resource_relations.each do |name, options| relation_name = name.to_s.gsub(/_id$/, "") # ensure that we don't have an id association = association_from_relation_name(relation_name) foreign_key = association.options[:foreign_key] || :"#{association.name}_id" collection = options[:collection] || resource_relation_class(association).all resource_relations[foreign_key] = options.merge( model_association: association, name: relation_name, collection: collection ) end end
# File lib/alchemy/resource.rb, line 296 def namespace_diff controller_path_array - resource_array end
# File lib/alchemy/resource.rb, line 309 def resource_column_type(col) resource_relation_type(col.name) || (col.try(:array) ? :array : col.type) end
# File lib/alchemy/resource.rb, line 313 def resource_relation(column_name) resource_relations[column_name.to_sym] if resource_relations end
# File lib/alchemy/resource.rb, line 304 def resource_relation_class(association) class_name = association.options[:class_name] || association.name.to_s.classify class_name.constantize end
# File lib/alchemy/resource.rb, line 300 def resource_relation_type(column_name) resource_relation(column_name).try(:[], :attr_type) end
# File lib/alchemy/resource.rb, line 268 def searchable_attribute?(attribute) SEARCHABLE_COLUMN_TYPES.include?(attribute[:type].to_sym) && !attribute.key?(:relation) end
# File lib/alchemy/resource.rb, line 272 def searchable_attribute_on_relation?(attribute) attribute.key?(:relation) && SEARCHABLE_COLUMN_TYPES.include?(attribute[:relation][:attr_type].to_sym) end
# File lib/alchemy/resource.rb, line 281 def searchable_relation_attribute(attribute) { name: "#{attribute[:relation][:model_association].name}_#{attribute[:relation][:attr_method]}", type: attribute[:relation][:attr_type] } end
# File lib/alchemy/resource.rb, line 277 def searchable_relation_attributes(attrs) attrs.select { |a| searchable_attribute_on_relation?(a) }.map { |a| searchable_relation_attribute(a) } end
Stores all activerecord associations in model_associations
attribute
# File lib/alchemy/resource.rb, line 334 def store_model_associations self.model_associations = model.reflect_on_all_associations.delete_if { |a| DEFAULT_SKIPPED_ASSOCIATIONS.include?(a.name.to_s) } end