lib/listings/base.rb in listings-0.0.2 vs lib/listings/base.rb in listings-0.0.3

- old
+ new

@@ -1,94 +1,147 @@ +require 'listings/configuration_methods' +require 'listings/view_helper_methods' +require 'csv' + module Listings - module ConfigurationMethods - extend ActiveSupport::Concern + class Base + include Listings::ConfigurationMethods + include Listings::ViewHelperMethods + include ERB::Util # html_escape methods are private in the view_context + # so they need to be included in order to be available - included do - attr_accessor :page - attr_accessor :scope - attr_accessor :items + attr_accessor :view_context + attr_accessor :params - def page_size - self.class.page_size - end + attr_accessor :search_criteria + attr_accessor :search_filters - def scopes - self.class.scopes + def initialize + @page_size = self.class.page_size + end + + def name + self.class.name.underscore.sub(/_listing$/,'') + end + + def param_scope; :scope; end + def param_page; :page; end + def param_search; :s; end + def param_sort_by; :sort_by; end + def param_sort_direction; :sort_d; end + + def parse_search + if !filterable? + # if it is not filterable, all the search is used as search criteria + self.search_criteria = self.search + self.search_filters = {} + else + # otherwise parse the search stripping out allowed filterable fields + self.search_filters, self.search_criteria = parse_filter(self.search, self.filters) end + end - def model_class - self.class.model_class + def parse_filter(text, filter_keys) + filters = {} + filter_keys.each do |key| + text = collect_filter text, key, filters end - def columns - self.class.columns + return filters, text + end + + def collect_filter(text, filter, filters_hash) + ["#{filter}:\s*(\\w+)", + "#{filter}:\s*\"([^\"]+)\"", + "#{filter}:\s*\'([^\']+)\'"].each do |pattern| + m = Regexp.new(pattern, Regexp::IGNORECASE).match(text) + if m + filters_hash[filter] = m[1] + return "#{m.pre_match.strip} #{m.post_match.strip}".strip + end end + + return text end - module ClassMethods - attr_accessor :page_size - attr_accessor :model_class + def filter_items(params, items) + self.page = params[param_page] || 1 + self.scope = scope_by_name(params[param_scope]) + self.search = params[param_search] + parse_search - def paginates_per(val) - @page_size = val - end + items = paginatable(scope.apply(self, items)) unless scope.nil? - def scope(name, props = {}) - scopes << ScopeDescriptor.new(name, props) + if search_criteria.present? && self.searchable? + criteria = [] + values = [] + self.columns.select(&:searchable?).each do |col| + criteria << "#{model_class.table_name}.#{col.name} like ?" + values << "%#{search_criteria}%" + end + items = items.where(criteria.join(' or '), *values) end - def scopes - @scopes ||= [] + if filterable? + # pluck filters values before applying filters/pagination/sorting + self.filter_values = {} + filters.each do |v| + self.filter_values[v] = items.pluck("distinct #{v}").reject(&:nil?) + end + + self.search_filters.each do |key, filter_value| + items = items.where("#{model_class.table_name}.#{key} = ?", filter_value) + end end - def model(val) - @model_class = val + if params.include?(param_sort_by) + sort_col = column_with_name(params[param_sort_by]) + sort_col.sort = params[param_sort_direction] + items = items.reorder("#{sort_col.sort_by} #{params[param_sort_direction]}") end - def columns - @columns ||= [] + if paginated? + items = items.page(page).per(page_size) end - def column(name = '', &proc) - columns << ColumnDescriptor.new(self, name, proc) + if items.is_a?(Class) + items = items.all end + items end - end - module ViewHelperMethods - extend ActiveSupport::Concern + def query_items(params) + @params = params + items = self.model_class + @has_active_model_source = items.respond_to? :human_attribute_name - included do - def is_active_scope(scope) - self.scope.name == scope.name - end + self.items = filter_items(self.scoped_params, paginatable(items)) + end - def url_for_scope(scope) - params = view_context.params.merge(param_scope => scope.name) - params.delete param_page - view_context.url_for(params) + def paginatable(array_or_model) + if array_or_model.is_a?(Array) && paginated? && !array_or_model.respond_to?(:page) + Kaminari.paginate_array(array_or_model) + else + array_or_model end end - end - class Base - include Listings::ConfigurationMethods - include Listings::ViewHelperMethods - attr_accessor :view_context + def has_active_model_source? + @has_active_model_source + end - def param_scope; :scope; end - def param_page; :page; end + def selectable? + self.class.selectable? + end - def query_items(params) - self.page = params[param_page] || 1 - self.scope = scope_by_name(params[param_scope]) + def paginated? + self.page_size != :none + end - items = self.model_class - items = scope.apply(items) unless scope.nil? - - self.items = items.page(page).per(page_size) + def scoped_params + @params.except(:listing, :controller, :action) end def scope_by_name(name) default_scope = scopes.find { |s| s.is_default? } selected_scope = scopes.find { |s| s.name == name.try(:to_sym) } @@ -97,64 +150,71 @@ def value_for(column, item) column.value_for(self, item) end - def method_missing(m, *args, &block) - delegated_to = view_context.respond_to?(m) ? view_context : view_context.main_app - delegated_to.send(m, *args, block) + def column_with_name(name) + self.columns.find { |c| c.name.to_s == name.to_s } end - end - class ScopeDescriptor - attr_accessor :name + def searchable? + self.columns.any? &:searchable? + end - def initialize(name, props = {}) - props.reverse_merge! default: false + def filterable? + !self.filters.empty? + end - @name = name - @props = props + def selectable_id(model) + model.id end - def human_name - name.capitalize + def url + view_context.listings.listing_full_path(self.name, self.params) end - def is_default? - @props[:default] + def search_data + { criteria: self.search_criteria, filters: self.search_filters } end - def apply(items) - if name == :all - items - else - items.send(name) - end + def format + (params[:format] || :html).to_sym end - end - class ColumnDescriptor - attr_reader :name + def to_array + data = [] - def initialize(listing_class, name, proc) - @listing_class = listing_class - @name = name - @proc = proc + data << self.columns.map { |c| c.human_name } + + self.items.each do |item| + row = [] + self.columns.each do |col| + row << col.value_for(item) + end + data << row + end + + data end - def value_for(listing, model) - if @proc - listing.instance_exec model, &@proc - else - model.send(name) + def to_csv + CSV.generate do |csv| + self.to_array.each do |row| + csv << row + end end end - def human_name - if name.is_a? Symbol - @listing_class.model_class.human_attribute_name(name) + def method_missing(m, *args, &block) + view_context.send(m, *args, &block) + end + + def kaminari_theme + case Listings.configuration.theme + when 'twitter-bootstrap-2' + 'twitter-bootstrap' else - name + Listings.configuration.theme end end end -end \ No newline at end of file +end