Class: Alchemy::Resource
- Inherits:
-
Object
- Object
- Alchemy::Resource
- Includes:
- Admin::ResourceName
- Defined in:
- app/models/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
-
#controller_path ⇒ Object
readonly
Returns the value of attribute controller_path.
-
#model ⇒ Object
readonly
Returns the value of attribute model.
-
#model_associations ⇒ Object
Returns the value of attribute model_associations.
-
#resource_relations ⇒ Object
Returns the value of attribute resource_relations.
Instance Method Summary collapse
- #attributes ⇒ Object
- #editable_attributes ⇒ Object
- #engine_name ⇒ Object
- #enum_values_collection_for_select(column_name) ⇒ Object
-
#help_text_for(attribute) ⇒ Object
Returns a help text for resource’s form or nil if no help text is available.
- #in_engine? ⇒ Boolean
-
#initialize(controller_path, module_definition = nil, custom_model = nil) ⇒ Resource
constructor
A new instance of Resource.
-
#model_association_names ⇒ Object
Returns an array of underscored association names.
- #namespace_for_scope ⇒ Object
- #namespaced_resource_name ⇒ Object
- #namespaced_resources_name ⇒ Object
-
#restricted_attributes ⇒ Object
Return attributes that should be viewable but not editable.
-
#search_field_name ⇒ Object
Search field input name.
-
#searchable_attribute_names ⇒ Object
Returns all attribute names that are searchable in the admin interface.
-
#skipped_attributes ⇒ Object
Return attributes that should neither be viewable nor editable.
-
#sorted_attributes ⇒ Object
Returns a sorted array of attributes.
Methods included from Admin::ResourceName
#controller_path_array, #resource_array, #resource_model_name, #resource_name, #resources_name
Constructor Details
#initialize(controller_path, module_definition = nil, custom_model = nil) ⇒ Resource
Returns a new instance of Resource.
110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'app/models/alchemy/resource.rb', line 110 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
#controller_path ⇒ Object (readonly)
Returns the value of attribute controller_path.
104 105 106 |
# File 'app/models/alchemy/resource.rb', line 104 def controller_path @controller_path end |
#model ⇒ Object (readonly)
Returns the value of attribute model.
104 105 106 |
# File 'app/models/alchemy/resource.rb', line 104 def model @model end |
#model_associations ⇒ Object
Returns the value of attribute model_associations.
103 104 105 |
# File 'app/models/alchemy/resource.rb', line 103 def model_associations @model_associations end |
#resource_relations ⇒ Object
Returns the value of attribute resource_relations.
103 104 105 |
# File 'app/models/alchemy/resource.rb', line 103 def resource_relations @resource_relations end |
Instance Method Details
#attributes ⇒ Object
154 155 156 157 158 159 160 161 162 163 164 165 |
# File 'app/models/alchemy/resource.rb', line 154 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_attributes ⇒ Object
198 199 200 |
# File 'app/models/alchemy/resource.rb', line 198 def editable_attributes attributes.reject { |h| restricted_attributes.map(&:to_s).include?(h[:name].to_s) } end |
#engine_name ⇒ Object
226 227 228 |
# File 'app/models/alchemy/resource.rb', line 226 def engine_name @module_definition && @module_definition["engine_name"] end |
#enum_values_collection_for_select(column_name) ⇒ Object
167 168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'app/models/alchemy/resource.rb', line 167 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 'app/models/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
222 223 224 |
# File 'app/models/alchemy/resource.rb', line 222 def in_engine? !engine_name.nil? end |
#model_association_names ⇒ Object
Returns an array of underscored association names
146 147 148 149 150 151 152 |
# File 'app/models/alchemy/resource.rb', line 146 def model_association_names return unless model_associations model_associations.map do |assoc| assoc.name.to_sym end end |
#namespace_for_scope ⇒ Object
138 139 140 141 142 |
# File 'app/models/alchemy/resource.rb', line 138 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_name ⇒ Object
124 125 126 127 128 |
# File 'app/models/alchemy/resource.rb', line 124 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_name ⇒ Object
130 131 132 133 134 135 136 |
# File 'app/models/alchemy/resource.rb', line 130 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 |
#restricted_attributes ⇒ Object
Return attributes that should be viewable but not editable.
248 249 250 251 252 253 254 |
# File 'app/models/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_name ⇒ Object
Search field input name
Joins all searchable attribute names into a Ransack compatible search query
218 219 220 |
# File 'app/models/alchemy/resource.rb', line 218 def search_field_name searchable_attribute_names.join("_or_") + "_cont" end |
#searchable_attribute_names ⇒ Object
Returns all attribute names that are searchable in the admin interface
204 205 206 207 208 209 210 211 212 |
# File 'app/models/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_attributes ⇒ Object
Return attributes that should neither be viewable nor editable.
258 259 260 261 262 263 264 |
# File 'app/models/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_attributes ⇒ Object
Returns a sorted array of attributes.
Attribute called “name” comes first. Attribute called “updated_at” comes last. Boolean type attributes come after non-boolean attributes but before “updated_at”.
187 188 189 190 191 192 193 194 195 196 |
# File 'app/models/alchemy/resource.rb', line 187 def sorted_attributes @_sorted_attributes ||= attributes.sort_by! do |attr| [ (attr[:name] == "name") ? 0 : 1, (attr[:name] == "updated_at") ? 3 : 2, (attr[:type] == :boolean) ? 2 : 1, attr[:name] ] end end |