lib/avo/base_resource.rb in avo-2.53.0 vs lib/avo/base_resource.rb in avo-3.0.0.beta1
- old
+ new
@@ -1,122 +1,115 @@
module Avo
class BaseResource
extend ActiveSupport::DescendantsTracker
include ActionView::Helpers::UrlHelper
- include Avo::Concerns::HasFields
- include Avo::Concerns::CanReplaceFields
- include Avo::Concerns::HasEditableControls
- include Avo::Concerns::HasResourceStimulusControllers
+ include Avo::Concerns::HasItems
+ include Avo::Concerns::CanReplaceItems
+ include Avo::Concerns::HasControls
+ include Avo::Concerns::HasStimulusControllers
include Avo::Concerns::ModelClassConstantized
- include Avo::Concerns::Pagination
+ include Avo::Concerns::HasDescription
- delegate :view_context, to: ::Avo::App
- delegate :current_user, to: ::Avo::App
- delegate :params, to: ::Avo::App
+ # Avo::Current methods
+ delegate :context, to: Avo::Current
+ def curent_user
+ Avo::Current.user
+ end
+ delegate :params, to: Avo::Current
+ delegate :request, to: Avo::Current
+ delegate :view_context, to: Avo::Current
+
+ # view_context methods
delegate :simple_format, :content_tag, to: :view_context
delegate :main_app, to: :view_context
delegate :avo, to: :view_context
delegate :resource_path, to: :view_context
delegate :resources_path, to: :view_context
+
+ # I18n methods
delegate :t, to: ::I18n
- delegate :context, to: ::Avo::App
+ # class methods
+ delegate :class_name, to: :class
+ delegate :route_key, to: :class
+ delegate :singular_route_key, to: :class
+
attr_accessor :view
attr_accessor :reflection
attr_accessor :user
- attr_accessor :params
+ attr_accessor :record
class_attribute :id, default: :id
- class_attribute :title, default: :id
- class_attribute :description, default: :id
- class_attribute :search_query, default: nil
- class_attribute :search_query_help, default: ""
- class_attribute :search_result_path
+ class_attribute :title
+ class_attribute :search, default: {}
class_attribute :includes, default: []
class_attribute :authorization_policy
class_attribute :translation_key
class_attribute :default_view_type, default: :table
class_attribute :devise_password_optional, default: false
- class_attribute :actions_loader
+ class_attribute :scopes_loader
class_attribute :filters_loader
- class_attribute :grid_loader
+ class_attribute :view_types
+ class_attribute :grid_view
class_attribute :visible_on_sidebar, default: true
- class_attribute :unscoped_queries_on_index, default: false
- class_attribute :resolve_query_scope
- class_attribute :resolve_find_scope
- # TODO: refactor this into a Host without args
- class_attribute :find_record_method, default: ->(model_class:, id:, params:) {
- model_class.find id
+ class_attribute :index_query, default: -> {
+ query
}
- class_attribute :ordering
- class_attribute :hide_from_global_search, default: false
+ class_attribute :find_record_method, default: -> {
+ query.find id
+ }
class_attribute :after_create_path, default: :show
class_attribute :after_update_path, default: :show
class_attribute :record_selector, default: true
class_attribute :keep_filters_panel_open, default: false
class_attribute :extra_params
class_attribute :link_to_child_resource, default: false
class_attribute :map_view
+ # EXTRACT:
+ class_attribute :ordering
+
class << self
delegate :t, to: ::I18n
- delegate :context, to: ::Avo::App
+ delegate :context, to: ::Avo::Current
- def grid(&block)
- grid_collector = GridCollector.new
- grid_collector.instance_eval(&block)
-
- self.grid_loader = grid_collector
- end
-
def action(action_class, arguments: {})
- self.actions_loader ||= Avo::Loaders::Loader.new
-
- action = { class: action_class, arguments: arguments }
- self.actions_loader.use action
+ deprecated_dsl_api __method__, "actions"
end
def filter(filter_class, arguments: {})
- self.filters_loader ||= Avo::Loaders::Loader.new
-
- filter = { class: filter_class , arguments: arguments }
- self.filters_loader.use filter
+ deprecated_dsl_api __method__, "filters"
end
- # This is the search_query scope
- # This should be removed and passed to the search block
- def scope
- query_scope
+ def scope(scope_class)
+ deprecated_dsl_api __method__, "scopes"
end
# This resolves the scope when doing "where" queries (not find queries)
+ #
+ # It's used to apply the authorization feature.
def query_scope
- final_scope = resolve_query_scope.present? ? resolve_query_scope.call(model_class: model_class) : model_class
-
- authorization.apply_policy final_scope
+ authorization.apply_policy Avo::ExecutionContext.new(
+ target: index_query,
+ query: model_class
+ ).handle
end
# This resolves the scope when finding records (not "where" queries)
+ #
+ # It's used to apply the authorization feature.
def find_scope
- final_scope = resolve_find_scope.present? ? resolve_find_scope.call(model_class: model_class) : model_class
-
- authorization.apply_policy final_scope
+ authorization.apply_policy model_class
end
def authorization
- Avo::Services::AuthorizationService.new Avo::App.current_user, model_class, policy_class: authorization_policy
+ Avo::Services::AuthorizationService.new Avo::Current.user, model_class, policy_class: authorization_policy
end
- def order_actions
- return {} if ordering.blank?
-
- ordering.dig(:actions) || {}
- end
-
def get_record_associations(record)
- record.class.reflections
+ record._reflections
end
def valid_association_name(record, association_name)
get_record_associations(record).keys.find do |name|
name == association_name
@@ -132,174 +125,246 @@
def get_available_models
ApplicationRecord.descendants
end
- def valid_model_class(model_class)
+ def get_model_by_name(model_name)
get_available_models.find do |m|
- m.to_s == model_class.to_s
+ m.to_s == model_name.to_s
end
end
- end
- def initialize
- unless self.class.model_class.present?
- if model_class.present? && model_class.respond_to?(:base_class)
- self.class.model_class = model_class.base_class
- end
+ # Returns the model class being used for this resource.
+ #
+ # The Resource instance has a model_class method too so it can support the STI use cases
+ # where we figure out the model class from the record
+ def model_class(record_class: nil)
+ # get the model class off of the static property
+ return @model_class if @model_class.present?
+
+ # get the model class off of the record for STI models
+ return record_class if record_class.present?
+
+ # generate a model class
+ class_name.safe_constantize
end
- end
- def record
- @model
- end
- alias_method :model, :record
+ # This is used as the model class ID
+ # We use this instead of the route_key to maintain compatibility with uncountable models
+ # With uncountable models route key appends an _index suffix (Fish->fish_index)
+ # Example: User->users, MediaItem->media_items, Fish->fish
+ def model_key
+ model_class.model_name.plural
+ end
+ def class_name
+ to_s.demodulize
+ end
- def hydrate(model: nil, view: nil, user: nil, params: nil)
- @view = view if view.present?
- @user = user if user.present?
- @params = params if params.present?
+ def route_key
+ class_name.underscore.pluralize
+ end
- if model.present?
- @model = model
+ def singular_route_key
+ route_key.singularize
+ end
- hydrate_model_with_default_values if @view == :new
+ def translation_key
+ @translation_key || "avo.resource_translations.#{class_name.underscore}"
end
- self
- end
+ def name
+ default = class_name.underscore.humanize
- def get_grid_fields
- return if self.class.grid_loader.blank?
+ if translation_key
+ t(translation_key, count: 1, default: default).humanize
+ else
+ default
+ end
+ end
+ alias_method :singular_name, :name
- self.class.grid_loader.hydrate(model: @model, view: @view, resource: self)
- end
+ def plural_name
+ default = name.pluralize
- def get_filters
- return [] if self.class.filters_loader.blank?
+ if translation_key
+ t(translation_key, count: 2, default: default).humanize
+ else
+ default
+ end
+ end
- self.class.filters_loader.bag
- end
+ def underscore_name
+ return @name if @name.present?
- def get_filter_arguments(filter_class)
- filter = get_filters.find { |filter| filter[:class] == filter_class.constantize }
+ name.demodulize.underscore
+ end
- filter[:arguments]
- end
+ def navigation_label
+ plural_name.humanize
+ end
- def get_actions
- return [] if self.class.actions_loader.blank?
+ def find_record(id, query: nil, params: nil)
+ Avo::ExecutionContext.new(
+ target: find_record_method,
+ query: query || find_scope, # If no record is given we'll use the default
+ id: id,
+ params: params
+ ).handle
+ end
- self.class.actions_loader.bag
+ def search_query
+ search.dig(:query)
+ end
+
+ def fetch_search(key, record: nil)
+ # self.class.fetch_search
+ Avo::ExecutionContext.new(target: search[key], resource: self, record: record).handle
+ end
end
- def get_action_arguments(action_class)
- action = get_actions.find { |action| action[:class].to_s == action_class.to_s }
+ delegate :context, to: ::Avo::Current
+ delegate :name, to: :class
+ delegate :singular_name, to: :class
+ delegate :plural_name, to: :class
+ delegate :underscore_name, to: :class
+ delegate :underscore_name, to: :class
+ delegate :find_record, to: :class
+ delegate :model_key, to: :class
- action[:arguments]
- end
+ def initialize(record: nil, view: nil, user: nil, params: nil)
+ @view = view if view.present?
+ @user = user if user.present?
+ @params = params if params.present?
- def default_panel_name
- return @params[:related_name].capitalize if @params.present? && @params[:related_name].present?
+ if record.present?
+ @record = record
- case @view
- when :show
- model_title
- when :edit
- model_title
- when :new
- t("avo.create_new_item", item: name.downcase).upcase_first
+ hydrate_model_with_default_values if @view == :new
end
- end
- def class_name_without_resource
- self.class.name.demodulize.delete_suffix("Resource")
+ detect_fields
+
+ unless self.class.model_class.present?
+ if model_class.present? && model_class.respond_to?(:base_class)
+ self.class.model_class = model_class.base_class
+ end
+ end
end
- def model_class
- # get the model class off of the static property
- return self.class.model_class if self.class.model_class.present?
+ def detect_fields
+ self.items_holder = Avo::Resources::Items::Holder.new
- # get the model class off of the model for STI models
- return @model.base_class if @model.present?
+ # Used in testing to replace items
+ if temporary_items.present?
+ instance_eval(&temporary_items)
+ else
+ fields
+ end
- # generate a model class
- class_name_without_resource.safe_constantize
+ self
end
- def model_id
- @model.send id
+ def fields
+ # blank fields method
end
- def model_title
- return name if @model.nil?
+ [:action, :filter, :scope].each do |entity|
+ plural_entity = entity.to_s.pluralize
- the_title = @model.send title
- return the_title if the_title.present?
+ # def actions / def filters / def scopes
+ define_method plural_entity do
+ # blank entity method
+ end
- model_id
- rescue
- name
- end
+ # def action / def filter / def scope
+ define_method entity do |entity_class, arguments: {}|
+ entity_loader(entity).use({class: entity_class, arguments: arguments})
+ end
- def resource_description
- return instance_exec(&self.class.description) if self.class.description.respond_to? :call
+ # def get_actions / def get_filters / def get_scopes
+ define_method "get_#{plural_entity}" do
+ return entity_loader(entity).bag if entity_loader(entity).present?
- # Show the description only on the resource index view.
- # If the user wants to conditionally it on all pages, they should use a block.
- if view == :index
- return self.class.description if self.class.description.is_a? String
+ instance_variable_set("@#{plural_entity}_loader", Avo::Loaders::Loader.new)
+ send plural_entity
+
+ entity_loader(entity).bag
end
- end
- def translation_key
- return "avo.resource_translations.#{class_name_without_resource.underscore}" if ::Avo::App.translation_enabled
-
- self.class.translation_key
+ # def get_action_arguments / def get_filter_arguments / def get_scope_arguments
+ define_method "get_#{entity}_arguments" do |entity_class|
+ send("get_#{plural_entity}").find { |entity| entity[:class].to_s == entity_class.to_s }[:arguments]
+ end
end
- def name
- default = class_name_without_resource.to_s.gsub('::', ' ').underscore.humanize
+ def hydrate(record: nil, view: nil, user: nil, params: nil)
+ @view = view if view.present?
+ @user = user if user.present?
+ @params = params if params.present?
- return @name if @name.present?
+ if record.present?
+ @record = record
- if translation_key && ::Avo::App.translation_enabled
- t(translation_key, count: 1, default: default).humanize
- else
- default
+ hydrate_model_with_default_values if @view == :new
end
- end
- def singular_name
- name
+ self
end
- def plural_name
- default = name.pluralize
+ def default_panel_name
+ return @params[:related_name].capitalize if @params.present? && @params[:related_name].present?
- if translation_key && ::Avo::App.translation_enabled
- t(translation_key, count: 2, default: default).humanize
- else
- default
+ case @view
+ when :show
+ record_title
+ when :edit
+ record_title
+ when :new
+ t("avo.create_new_item", item: name.humanize(capitalize: false)).upcase_first
end
end
- def underscore_name
- return @name if @name.present?
+ # Returns the model class being used for this resource.
+ #
+ # We use the class method as a fallback but we pass it the record too so it can support the STI use cases
+ # where we figure out the model class from that record.
+ def model_class
+ record_class = @record&.class
- self.class.name.demodulize.underscore
+ self.class.model_class record_class: record_class
end
- def navigation_label
- plural_name.humanize
+ def record_title
+ return name if @record.nil?
+
+ # Get the title from the record if title is not set, try to get the name, title or label, or fallback to the id
+ return @record.try(:name) || @record.try(:title) || @record.try(:label) || @record.id if title.nil?
+
+ # If the title is a symbol, get the value from the record else execute the block/string
+ case title
+ when Symbol
+ @record.send title
+ when Proc
+ Avo::ExecutionContext.new(target: title, resource: self, record: @record).handle
+ end
end
def available_view_types
+ if self.class.view_types.present?
+ return Array(
+ Avo::ExecutionContext.new(
+ target: self.class.view_types,
+ resource: self,
+ record: record
+ ).handle
+ )
+ end
+
view_types = [:table]
- view_types << :grid if get_grid_fields.present?
+ view_types << :grid if self.class.grid_view.present?
view_types << :map if map_view.present?
view_types
end
@@ -307,11 +372,11 @@
get_field_definitions.select do |field|
[Avo::Fields::FileField, Avo::Fields::FilesField].include? field.class
end
end
- # Map the received params to their actual fields.
+ # Map the received params to their actual fields
def fields_by_database_id
get_field_definitions
.reject do |field|
field.computed
end
@@ -319,32 +384,32 @@
[field.database_id.to_s, field]
end
.to_h
end
- def fill_model(model, params, extra_params: [])
+ def fill_record(record, params, extra_params: [])
# Write the field values
params.each do |key, value|
field = fields_by_database_id[key]
next unless field.present?
- model = field.fill_field model, key, value, params
+ record = field.fill_field record, key, value, params
end
- # Write the user configured extra params to the model
+ # Write the user configured extra params to the record
if extra_params.present?
# Let Rails fill in the rest of the params
- model.assign_attributes params.permit(extra_params)
+ record.assign_attributes params.permit(extra_params)
end
- model
+ record
end
def authorization(user: nil)
- current_user = user || Avo::App.current_user
- Avo::Services::AuthorizationService.new(current_user, model || model_class, policy_class: authorization_policy)
+ current_user = user || Avo::Current.user
+ Avo::Services::AuthorizationService.new(current_user, record || model_class, policy_class: authorization_policy)
end
def file_hash
content_to_be_hashed = ""
@@ -361,41 +426,41 @@
end
Digest::MD5.hexdigest(content_to_be_hashed)
end
- def cache_hash(parent_model)
- if parent_model.present?
- [model, file_hash, parent_model]
+ def cache_hash(parent_record)
+ if parent_record.present?
+ [record, file_hash, parent_record]
else
- [model, file_hash]
+ [record, file_hash]
end
end
- # We will not overwrite any attributes that come pre-filled in the model.
+ # We will not overwrite any attributes that come pre-filled in the record.
def hydrate_model_with_default_values
default_values = get_fields
.select do |field|
!field.computed
end
.map do |field|
value = field.value
if field.type == "belongs_to"
- reflection = @model.class.reflections[@params[:via_relation]]
+ reflection = @record._reflections[@params[:via_relation]]
if field.polymorphic_as.present? && field.types.map(&:to_s).include?(@params[:via_relation_class])
# set the value to the actual record
- via_resource = ::Avo::App.get_resource_by_model_name(@params[:via_relation_class])
- value = via_resource.find_record(@params[:via_resource_id])
+ via_resource = Avo.resource_manager.get_resource_by_model_class(@params[:via_relation_class])
+ value = via_resource.find_record(@params[:via_record_id])
elsif reflection.present? && reflection.foreign_key.present? && field.id.to_s == @params[:via_relation].to_s
- resource = Avo::App.get_resource_by_model_name params[:via_relation_class]
- model = resource.find_record @params[:via_resource_id], params: params
+ resource = Avo.resource_manager.get_resource_by_model_class params[:via_relation_class]
+ record = resource.find_record @params[:via_record_id], params: params
id_param = reflection.options[:primary_key] || :id
- value = model.send(id_param)
+ value = record.send(id_param)
end
end
[field, value]
end
@@ -403,58 +468,30 @@
.select do |_, value|
value.present?
end
default_values.each do |field, value|
- field.assign_value record: @model, value: value
+ field.assign_value record: @record, value: value
end
end
- def route_key
- class_name_without_resource.underscore.pluralize
- end
-
- def singular_route_key
- route_key.singularize
- end
-
- # This is used as the model class ID
- # We use this instead of the route_key to maintain compatibility with uncountable models
- # With uncountable models route key appends an _index suffix (Fish->fish_index)
- # Example: User->users, MediaItem->media_items, Fish->fish
- def model_key
- model_class.model_name.plural
- end
-
def model_name
model_class.model_name
end
def singular_model_key
model_class.model_name.singular
end
def record_path
- resource_path(model: model, resource: self)
+ resource_path(record: record, resource: self)
end
def records_path
resources_path(resource: self)
end
- def label_field
- get_field_definitions.find do |field|
- field.as_label.present?
- end
- rescue
- nil
- end
-
- def label
- label_field&.value || model_title
- end
-
def avatar_field
get_field_definitions.find do |field|
field.as_avatar.present?
end
rescue
@@ -475,36 +512,36 @@
avatar_field.as_avatar
rescue
nil
end
- def description_field
- get_field_definitions.find do |field|
- field.as_description.present?
- end
- rescue
- nil
+ def form_scope
+ model_class.base_class.to_s.underscore.downcase
end
- def description
- description_field&.value
+ def has_record_id?
+ record.present? && record_id.present?
end
- def form_scope
- model_class.base_class.to_s.underscore.downcase
+ def id_attribute
+ :id
end
- def ordering_host(**args)
- Avo::Hosts::Ordering.new resource: self, options: self.class.ordering, **args
+ def record_id
+ record.send(id_attribute)
end
- def has_model_id?
- model.present? && model.id.present?
+ def description_attributes
+ {
+ view: view,
+ resource: self,
+ record: record
+ }
end
- def find_record(id, query: nil, params: nil)
- query ||= self.class.find_scope
+ private
- self.class.find_record_method.call(model_class: query, id: id, params: params)
+ def entity_loader(entity)
+ instance_variable_get("@#{entity.to_s.pluralize}_loader")
end
end
end