kanbanworkflowstimelinescrumrubyroadmapproject-planningproject-managementopenprojectangularissue-trackerifcgantt-chartganttbug-trackerboardsbcf
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
295 lines
8.5 KiB
295 lines
8.5 KiB
#-- copyright
|
|
# OpenProject is an open source project management software.
|
|
# Copyright (C) 2012-2023 the OpenProject GmbH
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License version 3.
|
|
#
|
|
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
|
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
# Copyright (C) 2010-2013 the ChiliProject Team
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License
|
|
# as published by the Free Software Foundation; either version 2
|
|
# of the License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
#
|
|
# See COPYRIGHT and LICENSE files for more details.
|
|
#++
|
|
|
|
# Extending this module provides some convenience methods for easier setup of pagination inside a controller.
|
|
# It assumes there is just one pagination method to set up per model.
|
|
#
|
|
# To set up basic functions:
|
|
# paginate_model Project
|
|
#
|
|
# This sets up the action #paginate_projects inside the controller.
|
|
#
|
|
# To change this action:
|
|
#
|
|
# action_for Project, :my_own_action
|
|
#
|
|
# or use a block:
|
|
#
|
|
# action_for Project do
|
|
# do_something
|
|
# end
|
|
#
|
|
# To set up multiple models at once:
|
|
# paginate_models Project, User
|
|
#
|
|
# To change the call the model uses for pagination (signature as in Pagination::Model#paginate_scope!):
|
|
# pagination_for Project, :my_own_pagination_method
|
|
# pagination_for Project do |scope, opts|
|
|
# do_something
|
|
# end
|
|
#
|
|
# To change the scope the model uses to search (signature as in Pagination::Model#search_scope):
|
|
# search_for Project, :my_own_pagination_method
|
|
# search_for Project do |query|
|
|
# do_something
|
|
# end
|
|
# Note that this needs to return an actual scope or its corresponding hash.
|
|
#
|
|
# To change the response the action will give:
|
|
# response_for Project, :my_custom_response
|
|
# response_for Project, Proc.new {
|
|
# respond_to do |format|
|
|
# DO SOMETHING
|
|
# end
|
|
# }
|
|
# This needs to return something that can be #instance_eval'ed AND #call'ed, i.e. a Proc.
|
|
# A String containing code will NOT work but a lambda will, if the execution context
|
|
# can be changed accordingly (simply providing an additional parameter will work in most
|
|
# cases).
|
|
#
|
|
# There are several possibilities to add options to the call to #search_method:
|
|
# Procs allow to change behaviour dynamically, as with ActiveRecords scopes.
|
|
# Lambdas will work just as procs, but an additional parameter needs to get passed to
|
|
# change their context.
|
|
# Everything else just gets passed as is.
|
|
# search_options_for Project, proc { @bar.nil? ? @bar : { a: b, c: d } }
|
|
# search_options_for Project, lambda { |self| (@bar.nil? ? @bar : { a: b, c: d }) }
|
|
# search_options_for Project, { a: b, c: d }
|
|
# search_options_for Project, "yeah!"
|
|
#
|
|
#
|
|
module Pagination::Controller
|
|
class Paginator
|
|
attr_accessor :model, :action, :pagination, :search, :controller, :last_action, :block, :response, :search_options
|
|
|
|
def initialize(controller, model)
|
|
self.controller = controller
|
|
self.model = model
|
|
end
|
|
|
|
def self.resolve_model(model)
|
|
(model.respond_to?(:constantize) ? model.constantize : model)
|
|
end
|
|
|
|
def default_action
|
|
model_name = self.class.resolve_model(model).name
|
|
model_name_without_modules = model_name.split('::').last || ''
|
|
:"paginate_#{model_name_without_modules.underscore.downcase.pluralize}"
|
|
end
|
|
|
|
def default_pagination
|
|
:paginate_scope!
|
|
end
|
|
|
|
def default_search
|
|
:search_scope
|
|
end
|
|
|
|
def default_search_options
|
|
{}
|
|
end
|
|
|
|
def last_action
|
|
@last_action ||= action
|
|
end
|
|
|
|
def action
|
|
@action ||= default_action
|
|
end
|
|
|
|
def action=(action)
|
|
@action = action
|
|
refresh_action!
|
|
@action
|
|
end
|
|
|
|
def search
|
|
@search ||= default_search
|
|
end
|
|
|
|
def pagination
|
|
@pagination ||= default_pagination
|
|
end
|
|
|
|
def block
|
|
@block ||= default_block
|
|
end
|
|
|
|
def response
|
|
@response ||= default_response_block
|
|
end
|
|
|
|
def search_options
|
|
@search_options ||= default_search_options
|
|
end
|
|
|
|
def changed?
|
|
last_action != action
|
|
end
|
|
|
|
def refresh_action!
|
|
undef_action!
|
|
define_action!
|
|
end
|
|
|
|
def undef_action!
|
|
controller.pagination.delete(last_action)
|
|
|
|
# remove old
|
|
controller.send(:remove_method, last_action) if controller.respond_to? last_action
|
|
end
|
|
|
|
def define_action!(block = default_block)
|
|
controller.pagination[action] = self
|
|
raise NameError, "method '#{action}' already defined in #{controller}" if controller.respond_to? action
|
|
|
|
controller.send(:define_method, action, block)
|
|
end
|
|
|
|
def default_block
|
|
Proc.new do
|
|
# TODO: less evilness
|
|
paginator = self.class.pagination[__method__]
|
|
size = params[:page_limit].to_i || 10
|
|
page = params[:page]
|
|
|
|
if page
|
|
page = page.to_i
|
|
|
|
methods = {}
|
|
%i[pagination search].each do |meth|
|
|
methods[meth] = if paginator.send(meth).respond_to?(:call)
|
|
paginator.send(meth)
|
|
else
|
|
paginator.model.method(paginator.send(meth))
|
|
end
|
|
end
|
|
|
|
if (options = paginator.search_options).respond_to?(:call)
|
|
options = instance_eval(&options.to_proc)
|
|
end
|
|
|
|
search_call = (options.presence ? methods[:search].call(params[:q], options) : methods[:search].call(params[:q]))
|
|
@paginated_items = methods[:pagination].call(
|
|
search_call,
|
|
page:, page_limit: size
|
|
)
|
|
|
|
@more = @paginated_items.total_pages > page
|
|
@total = @paginated_items.total_entries
|
|
|
|
instance_eval(&paginator.response.to_proc)
|
|
end
|
|
end
|
|
end
|
|
|
|
def default_response_block
|
|
Proc.new do
|
|
respond_to do |format|
|
|
format.json do
|
|
render json: { results:
|
|
{ items: @paginated_items.map { |item| { id: item.id, name: item.name } },
|
|
total: @total || @paginated_items.size,
|
|
more: @more || 0 } }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.included(base)
|
|
base.extend self
|
|
end
|
|
|
|
def self.extended(base)
|
|
base.instance_eval do
|
|
def paginate_models(*args)
|
|
args.each do |arg|
|
|
paginate_model(arg)
|
|
end
|
|
end
|
|
|
|
def paginate_model(model)
|
|
pagination_class.resolve_model(model)
|
|
pagination[model] = pagination_class.new(self, model)
|
|
pagination[model].refresh_action!
|
|
end
|
|
|
|
def pagination_class
|
|
@pagination_class ||= Pagination::Controller::Paginator
|
|
end
|
|
|
|
def pagination_class=(struct)
|
|
@pagination_class = struct
|
|
end
|
|
|
|
def pagination=(calls)
|
|
@pagination = calls
|
|
end
|
|
|
|
def pagination
|
|
@pagination ||= {}
|
|
end
|
|
|
|
# Has to return a method that takes a query as an argument
|
|
# See pagination::Model#paginate_scope!
|
|
def pagination_for(model, call)
|
|
resolve_paginator_for(model).pagination = (call.respond_to?(:call) ? call : call.to_s.to_sym)
|
|
end
|
|
|
|
def search_for(model, call)
|
|
resolve_paginator_for(model).search = (call.respond_to?(:call) ? call : call.to_s.to_sym)
|
|
end
|
|
|
|
def action_for(model, call)
|
|
resolve_paginator_for(model).action = (call.respond_to?(:call) ? call : call.to_s.to_sym)
|
|
end
|
|
|
|
def response_for(model, call)
|
|
resolve_paginator_for(model).response = (call.respond_to?(:call) ? call : call.to_s.to_sym)
|
|
end
|
|
|
|
def search_options_for(model, options)
|
|
resolve_paginator_for(model).search_options = options
|
|
end
|
|
|
|
private
|
|
|
|
def resolve_paginator_for(model)
|
|
model = pagination_class.resolve_model(model)
|
|
inst = pagination.find { |_, pag| pag.model == model }[1]
|
|
|
|
if inst.nil?
|
|
raise ArgumentError, "Model #{model} is not being paginated. Call #paginate_model(s) first."
|
|
else
|
|
inst
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|