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

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"}, ActsAsTaggableOn::Tag)

Constant Summary collapse

DEFAULT_SKIPPED_ATTRIBUTES =
%w(id updated_at created_at creator_id updater_id)
DEFAULT_SKIPPED_ASSOCIATIONS =
%w(creator updater)

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.



93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/alchemy/resource.rb', line 93

def initialize(controller_path, module_definition=nil, custom_model=nil)
  @controller_path = controller_path
  @module_definition = module_definition
  @model = (custom_model or guess_model_from_controller_path)
  self.skipped_attributes = model.respond_to?(:skipped_alchemy_resource_attributes) ? model.skipped_alchemy_resource_attributes : DEFAULT_SKIPPED_ATTRIBUTES
  self.restricted_attributes = model.respond_to?(:restricted_alchemy_resource_attributes) ? model.restricted_alchemy_resource_attributes : []
  if model.respond_to?(:alchemy_resource_relations)
    if not 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.



88
89
90
# File 'lib/alchemy/resource.rb', line 88

def model
  @model
end

#model_associationsObject

Returns the value of attribute model_associations.



87
88
89
# File 'lib/alchemy/resource.rb', line 87

def model_associations
  @model_associations
end

#resource_relationsObject

Returns the value of attribute resource_relations.



87
88
89
# File 'lib/alchemy/resource.rb', line 87

def resource_relations
  @resource_relations
end

#restricted_attributesObject

Returns the value of attribute restricted_attributes.



87
88
89
# File 'lib/alchemy/resource.rb', line 87

def restricted_attributes
  @restricted_attributes
end

#skipped_attributesObject

Returns the value of attribute skipped_attributes.



87
88
89
# File 'lib/alchemy/resource.rb', line 87

def skipped_attributes
  @skipped_attributes
end

Instance Method Details

#attributesObject



142
143
144
145
146
147
148
149
150
151
152
# File 'lib/alchemy/resource.rb', line 142

def attributes
  @_attributes ||= self.model.columns.collect do |col|
    unless self.skipped_attributes.include?(col.name)
      {
        name: col.name,
        type: resource_column_type(col),
        relation: resource_relation(col.name)
      }.delete_if { |k, v| v.nil? }
    end
  end.compact
end

#editable_attributesObject



154
155
156
# File 'lib/alchemy/resource.rb', line 154

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

#engine_nameObject



170
171
172
# File 'lib/alchemy/resource.rb', line 170

def engine_name
  @module_definition and @module_definition['engine_name']
end

#help_text_for(attribute) ⇒ Object

Returns a help text for resource’s form

Example:

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


184
185
186
187
188
# File 'lib/alchemy/resource.rb', line 184

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

#in_engine?Boolean

Returns:

  • (Boolean)


166
167
168
# File 'lib/alchemy/resource.rb', line 166

def in_engine?
  not self.engine_name.nil?
end

#model_association_namesObject

Returns an array of underscored association names



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

def model_association_names
  return unless model_associations
  model_associations.map do |assoc|
    assoc.name.to_sym
  end
end

#namespace_for_scopeObject



127
128
129
130
131
# File 'lib/alchemy/resource.rb', line 127

def namespace_for_scope
  namespace_array = namespace_diff
  namespace_array.delete(engine_name) if in_engine?
  namespace_array
end

#namespaced_resource_nameObject



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

def namespaced_resource_name
  return @_namespaced_resource_name unless @_namespaced_resource_name.nil?
  resource_name_array = resource_array
  resource_name_array.delete(engine_name) if in_engine?
  @_namespaced_resource_name = resource_name_array.join('_').singularize
end

#resource_arrayObject



108
109
110
# File 'lib/alchemy/resource.rb', line 108

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

#resource_nameObject



116
117
118
# File 'lib/alchemy/resource.rb', line 116

def resource_name
  @_resource_name ||= resources_name.singularize
end

#resources_nameObject



112
113
114
# File 'lib/alchemy/resource.rb', line 112

def resources_name
  @_resources_name ||= resource_array.last
end

#searchable_attributesObject

Returns all columns that are searchable

For now it only uses string type columns



162
163
164
# File 'lib/alchemy/resource.rb', line 162

def searchable_attributes
  self.attributes.select { |a| a[:type].to_sym == :string }
end