lib/avo/base_resource.rb in avo-3.0.0.beta1 vs lib/avo/base_resource.rb in avo-3.0.0.pre1

- old
+ new

@@ -6,106 +6,125 @@ include Avo::Concerns::HasItems include Avo::Concerns::CanReplaceItems include Avo::Concerns::HasControls include Avo::Concerns::HasStimulusControllers include Avo::Concerns::ModelClassConstantized - include Avo::Concerns::HasDescription - # Avo::Current methods delegate :context, to: Avo::Current - def curent_user - Avo::Current.user - end + delegate :current_user, to: Avo::Current 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 - # 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 - class_attribute :search, default: {} + class_attribute :title, default: :id + class_attribute :description, default: :id + class_attribute :search_query, default: nil + class_attribute :search_query_help, 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 :view_types - class_attribute :grid_view + class_attribute :grid_loader class_attribute :visible_on_sidebar, default: true - class_attribute :index_query, default: -> { - query + class_attribute :unscoped_queries_on_index, default: false + class_attribute :resolve_index_query + 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 :find_record_method, default: -> { - query.find id - } + class_attribute :hide_from_global_search, default: false 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::Current + def grid(&block) + grid_collector = GridCollector.new + grid_collector.instance_eval(&block) + + self.grid_loader = grid_collector + end + def action(action_class, arguments: {}) - deprecated_dsl_api __method__, "actions" + self.actions_loader ||= Avo::Loaders::Loader.new + + action = {class: action_class, arguments: arguments} + self.actions_loader.use action end def filter(filter_class, arguments: {}) - deprecated_dsl_api __method__, "filters" + self.filters_loader ||= Avo::Loaders::Loader.new + + filter = { class: filter_class , arguments: arguments } + self.filters_loader.use filter end - def scope(scope_class) - deprecated_dsl_api __method__, "scopes" + # This is the search_query scope + # This should be removed and passed to the search block + def scope + query_scope end + def scopes(*args) + self.scopes_loader ||= Avo::Loaders::Loader.new + + args.each do |scope_class| + self.scopes_loader.use scope_class + end + end + # This resolves the scope when doing "where" queries (not find queries) - # - # It's used to apply the authorization feature. def query_scope - authorization.apply_policy Avo::ExecutionContext.new( - target: index_query, - query: model_class - ).handle + final_scope = if resolve_index_query.present? + Avo::ExecutionContext.new(target: resolve_index_query, model_class: model_class).handle + else + model_class + end + + authorization.apply_policy final_scope end # This resolves the scope when finding records (not "where" queries) - # - # It's used to apply the authorization feature. def find_scope - authorization.apply_policy model_class + final_scope = if resolve_find_scope.present? + Avo::ExecutionContext.new(target: resolve_find_scope, model_class: model_class).handle + else + model_class + end + + authorization.apply_policy final_scope end def authorization - Avo::Services::AuthorizationService.new Avo::Current.user, model_class, policy_class: authorization_policy + Avo::Services::AuthorizationService.new Avo::Current.current_user, model_class, policy_class: authorization_policy end def get_record_associations(record) record._reflections end @@ -130,187 +149,70 @@ def get_model_by_name(model_name) get_available_models.find do |m| m.to_s == model_name.to_s end end + 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? + delegate :context, to: ::Avo::Current - # 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 - - # 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 route_key - class_name.underscore.pluralize - end - - def singular_route_key - route_key.singularize - end - - def translation_key - @translation_key || "avo.resource_translations.#{class_name.underscore}" - end - - def name - default = class_name.underscore.humanize - - if translation_key - t(translation_key, count: 1, default: default).humanize - else - default + 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 end - alias_method :singular_name, :name - - def plural_name - default = name.pluralize - - if translation_key - t(translation_key, count: 2, default: default).humanize - else - default - end - end - - def underscore_name - return @name if @name.present? - - name.demodulize.underscore - end - - def navigation_label - plural_name.humanize - end - - 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 - - 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 - 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 - - def initialize(record: nil, view: nil, user: nil, params: nil) + 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? if record.present? @record = record hydrate_model_with_default_values if @view == :new end - 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 + self end - def detect_fields - self.items_holder = Avo::Resources::Items::Holder.new + def get_grid_fields + return if self.class.grid_loader.blank? - # Used in testing to replace items - if temporary_items.present? - instance_eval(&temporary_items) - else - fields - end - - self + self.class.grid_loader.hydrate(record: @record, view: @view, resource: self) end - def fields - # blank fields method + def get_filters + return [] if self.class.filters_loader.blank? + + self.class.filters_loader.bag end - [:action, :filter, :scope].each do |entity| - plural_entity = entity.to_s.pluralize + def get_filter_arguments(filter_class) + filter = get_filters.find { |filter| filter[:class] == filter_class.constantize } - # def actions / def filters / def scopes - define_method plural_entity do - # blank entity method - end + filter[:arguments] + 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 get_actions + return [] if self.class.actions_loader.blank? - # 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? + self.class.actions_loader.bag + end - instance_variable_set("@#{plural_entity}_loader", Avo::Loaders::Loader.new) - send plural_entity + def get_action_arguments(action_class) + action = get_actions.find { |action| action[:class].to_s == action_class.to_s } - entity_loader(entity).bag - end - - # 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 + action[:arguments] end - 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? + def get_scopes + return [] if self.class.scopes_loader.blank? - if record.present? - @record = record - - hydrate_model_with_default_values if @view == :new - end - - self + self.class.scopes_loader.bag end def default_panel_name return @params[:related_name].capitalize if @params.present? && @params[:related_name].present? @@ -318,77 +220,115 @@ when :show record_title when :edit record_title when :new - t("avo.create_new_item", item: name.humanize(capitalize: false)).upcase_first + t("avo.create_new_item", item: name.downcase).upcase_first end end - # 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 class_name + self.class.name.demodulize + end + def model_class - record_class = @record&.class + # get the model class off of the static property + return self.class.model_class if self.class.model_class.present? - self.class.model_class record_class: record_class + # get the model class off of the record + return @record.base_class if @record.present? + + # generate a model class + class_name.safe_constantize end 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? + the_title = @record.send title + return the_title if the_title.present? - # 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 + @record.id + rescue + name + end + + def resource_description + return instance_exec(&self.class.description) if self.class.description.respond_to? :call + + # 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 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 - ) + def translation_key + self.class.translation_key || "avo.resource_translations.#{class_name.underscore}" + end + + def name + default = class_name.underscore.humanize + + return @name if @name.present? + + if translation_key + t(translation_key, count: 1, default: default).capitalize + else + default end + end + def singular_name + name + end + + def plural_name + default = name.pluralize + + if translation_key + t(translation_key, count: 2, default: default).capitalize + else + default + end + end + + def underscore_name + return @name if @name.present? + + self.class.name.demodulize.underscore + end + + def navigation_label + plural_name.humanize + end + + def available_view_types view_types = [:table] - view_types << :grid if self.class.grid_view.present? - view_types << :map if map_view.present? + view_types << :grid if get_grid_fields.present? view_types end - def attachment_fields + def attached_file_fields 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 - def fields_by_database_id - get_field_definitions + def fill_record(record, params, extra_params: []) + # Map the received params to their actual fields + fields_by_database_id = get_field_definitions .reject do |field| field.computed end .map do |field| [field.database_id.to_s, field] end .to_h - end - 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? @@ -404,11 +344,11 @@ record end def authorization(user: nil) - current_user = user || Avo::Current.user + current_user = user || Avo::Current.current_user Avo::Services::AuthorizationService.new(current_user, record || model_class, policy_class: authorization_policy) end def file_hash content_to_be_hashed = "" @@ -441,41 +381,60 @@ default_values = get_fields .select do |field| !field.computed end .map do |field| + id = field.id value = field.value if field.type == "belongs_to" + id = field.foreign_key.to_sym 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.resource_manager.get_resource_by_model_class(@params[:via_relation_class]) - value = via_resource.find_record(@params[:via_record_id]) + value = @params[:via_relation_class].safe_constantize.find(@params[:via_record_id]) elsif reflection.present? && reflection.foreign_key.present? && field.id.to_s == @params[:via_relation].to_s - resource = Avo.resource_manager.get_resource_by_model_class params[:via_relation_class] + resource = Avo::App.resources.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 = record.send(id_param) end end - [field, value] + [id, value] end .to_h - .select do |_, value| + .select do |id, value| value.present? end - default_values.each do |field, value| - field.assign_value record: @record, value: value + default_values.each do |id, value| + if @record.send(id).nil? + @record.send("#{id}=", value) + end end end + def route_key + class_name.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 @@ -488,10 +447,22 @@ 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 || record_title + end + def avatar_field get_field_definitions.find do |field| field.as_avatar.present? end rescue @@ -512,36 +483,40 @@ avatar_field.as_avatar rescue nil end + def description_field + get_field_definitions.find do |field| + field.as_description.present? + end + rescue + nil + end + + def description + description_field&.value + end + def form_scope model_class.base_class.to_s.underscore.downcase end def has_record_id? record.present? && record_id.present? end + def find_record(id, query: nil, params: nil) + query ||= self.class.find_scope + + self.class.find_record_method.call(model_class: query, id: id, params: params) + end + def id_attribute :id end def record_id record.send(id_attribute) - end - - def description_attributes - { - view: view, - resource: self, - record: record - } - end - - private - - def entity_loader(entity) - instance_variable_get("@#{entity.to_s.pluralize}_loader") end end end