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.

query.rb 47KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461
  1. # frozen_string_literal: true
  2. # Redmine - project management software
  3. # Copyright (C) 2006-2019 Jean-Philippe Lang
  4. #
  5. # This program is free software; you can redistribute it and/or
  6. # modify it under the terms of the GNU General Public License
  7. # as published by the Free Software Foundation; either version 2
  8. # of the License, or (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program; if not, write to the Free Software
  17. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. require 'redmine/sort_criteria'
  19. class QueryColumn
  20. attr_accessor :name, :groupable, :totalable, :default_order
  21. attr_writer :sortable
  22. include Redmine::I18n
  23. def initialize(name, options={})
  24. self.name = name
  25. self.sortable = options[:sortable]
  26. self.groupable = options[:groupable] || false
  27. if groupable == true
  28. self.groupable = name.to_s
  29. end
  30. self.totalable = options[:totalable] || false
  31. self.default_order = options[:default_order]
  32. @inline = options.key?(:inline) ? options[:inline] : true
  33. @caption_key = options[:caption] || "field_#{name}".to_sym
  34. @frozen = options[:frozen]
  35. end
  36. def caption
  37. case @caption_key
  38. when Symbol
  39. l(@caption_key)
  40. when Proc
  41. @caption_key.call
  42. else
  43. @caption_key
  44. end
  45. end
  46. # Returns true if the column is sortable, otherwise false
  47. def sortable?
  48. !@sortable.nil?
  49. end
  50. def sortable
  51. @sortable.is_a?(Proc) ? @sortable.call : @sortable
  52. end
  53. def inline?
  54. @inline
  55. end
  56. def frozen?
  57. @frozen
  58. end
  59. def value(object)
  60. object.send name
  61. end
  62. def value_object(object)
  63. object.send name
  64. end
  65. # Returns the group that object belongs to when grouping query results
  66. def group_value(object)
  67. value(object)
  68. end
  69. def css_classes
  70. name
  71. end
  72. end
  73. class TimestampQueryColumn < QueryColumn
  74. def groupable
  75. if @groupable
  76. Redmine::Database.timestamp_to_date(sortable, User.current.time_zone)
  77. end
  78. end
  79. def group_value(object)
  80. if time = value(object)
  81. User.current.time_to_date(time)
  82. end
  83. end
  84. end
  85. class QueryAssociationColumn < QueryColumn
  86. def initialize(association, attribute, options={})
  87. @association = association
  88. @attribute = attribute
  89. name_with_assoc = "#{association}.#{attribute}".to_sym
  90. super(name_with_assoc, options)
  91. end
  92. def value_object(object)
  93. if assoc = object.send(@association)
  94. assoc.send @attribute
  95. end
  96. end
  97. def css_classes
  98. @css_classes ||= "#{@association}-#{@attribute}"
  99. end
  100. end
  101. class QueryCustomFieldColumn < QueryColumn
  102. def initialize(custom_field, options={})
  103. self.name = "cf_#{custom_field.id}".to_sym
  104. self.sortable = custom_field.order_statement || false
  105. self.groupable = custom_field.group_statement || false
  106. self.totalable = options.key?(:totalable) ? !!options[:totalable] : custom_field.totalable?
  107. @inline = custom_field.full_width_layout? ? false : true
  108. @cf = custom_field
  109. end
  110. def caption
  111. @cf.name
  112. end
  113. def custom_field
  114. @cf
  115. end
  116. def value_object(object)
  117. if custom_field.visible_by?(object.project, User.current)
  118. cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
  119. cv.size > 1 ? cv.sort_by {|e| e.value.to_s} : cv.first
  120. else
  121. nil
  122. end
  123. end
  124. def value(object)
  125. raw = value_object(object)
  126. if raw.is_a?(Array)
  127. raw.map {|r| @cf.cast_value(r.value)}
  128. elsif raw
  129. @cf.cast_value(raw.value)
  130. else
  131. nil
  132. end
  133. end
  134. def css_classes
  135. @css_classes ||= "#{name} #{@cf.field_format}"
  136. end
  137. end
  138. class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
  139. def initialize(association, custom_field, options={})
  140. super(custom_field, options)
  141. self.name = "#{association}.cf_#{custom_field.id}".to_sym
  142. # TODO: support sorting/grouping by association custom field
  143. self.sortable = false
  144. self.groupable = false
  145. @association = association
  146. end
  147. def value_object(object)
  148. if assoc = object.send(@association)
  149. super(assoc)
  150. end
  151. end
  152. def css_classes
  153. @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
  154. end
  155. end
  156. class QueryFilter
  157. include Redmine::I18n
  158. def initialize(field, options)
  159. @field = field.to_s
  160. @options = options
  161. @options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
  162. # Consider filters with a Proc for values as remote by default
  163. @remote = options.key?(:remote) ? options[:remote] : options[:values].is_a?(Proc)
  164. end
  165. def [](arg)
  166. if arg == :values
  167. values
  168. else
  169. @options[arg]
  170. end
  171. end
  172. def values
  173. @values ||= begin
  174. values = @options[:values]
  175. if values.is_a?(Proc)
  176. values = values.call
  177. end
  178. values
  179. end
  180. end
  181. def remote
  182. @remote
  183. end
  184. end
  185. class Query < ActiveRecord::Base
  186. class StatementInvalid < ::ActiveRecord::StatementInvalid
  187. end
  188. include Redmine::SubclassFactory
  189. VISIBILITY_PRIVATE = 0
  190. VISIBILITY_ROLES = 1
  191. VISIBILITY_PUBLIC = 2
  192. belongs_to :project
  193. belongs_to :user
  194. has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
  195. serialize :filters
  196. serialize :column_names
  197. serialize :sort_criteria, Array
  198. serialize :options, Hash
  199. validates_presence_of :name
  200. validates_length_of :name, :maximum => 255
  201. validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
  202. validate :validate_query_filters
  203. validate do |query|
  204. errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
  205. end
  206. after_save do |query|
  207. if query.saved_change_to_visibility? && query.visibility != VISIBILITY_ROLES
  208. query.roles.clear
  209. end
  210. end
  211. class_attribute :operators
  212. self.operators = {
  213. "=" => :label_equals,
  214. "!" => :label_not_equals,
  215. "o" => :label_open_issues,
  216. "c" => :label_closed_issues,
  217. "!*" => :label_none,
  218. "*" => :label_any,
  219. ">=" => :label_greater_or_equal,
  220. "<=" => :label_less_or_equal,
  221. "><" => :label_between,
  222. "<t+" => :label_in_less_than,
  223. ">t+" => :label_in_more_than,
  224. "><t+"=> :label_in_the_next_days,
  225. "t+" => :label_in,
  226. "nd" => :label_tomorrow,
  227. "t" => :label_today,
  228. "ld" => :label_yesterday,
  229. "nw" => :label_next_week,
  230. "w" => :label_this_week,
  231. "lw" => :label_last_week,
  232. "l2w" => [:label_last_n_weeks, {:count => 2}],
  233. "nm" => :label_next_month,
  234. "m" => :label_this_month,
  235. "lm" => :label_last_month,
  236. "y" => :label_this_year,
  237. ">t-" => :label_less_than_ago,
  238. "<t-" => :label_more_than_ago,
  239. "><t-"=> :label_in_the_past_days,
  240. "t-" => :label_ago,
  241. "~" => :label_contains,
  242. "!~" => :label_not_contains,
  243. "^" => :label_starts_with,
  244. "$" => :label_ends_with,
  245. "=p" => :label_any_issues_in_project,
  246. "=!p" => :label_any_issues_not_in_project,
  247. "!p" => :label_no_issues_in_project,
  248. "*o" => :label_any_open_issues,
  249. "!o" => :label_no_open_issues,
  250. }
  251. class_attribute :operators_by_filter_type
  252. self.operators_by_filter_type = {
  253. :list => [ "=", "!" ],
  254. :list_status => [ "o", "=", "!", "c", "*" ],
  255. :list_optional => [ "=", "!", "!*", "*" ],
  256. :list_subprojects => [ "*", "!*", "=", "!" ],
  257. :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "nd", "t", "ld", "nw", "w", "lw", "l2w", "nm", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
  258. :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
  259. :string => [ "~", "=", "!~", "!", "^", "$", "!*", "*" ],
  260. :text => [ "~", "!~", "^", "$", "!*", "*" ],
  261. :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
  262. :float => [ "=", ">=", "<=", "><", "!*", "*" ],
  263. :relation => ["=", "=p", "=!p", "!p", "*o", "!o", "!*", "*"],
  264. :tree => ["=", "~", "!*", "*"]
  265. }
  266. class_attribute :available_columns
  267. self.available_columns = []
  268. class_attribute :queried_class
  269. # Permission required to view the queries, set on subclasses.
  270. class_attribute :view_permission
  271. # Scope of queries that are global or on the given project
  272. scope :global_or_on_project, lambda {|project|
  273. where(:project_id => (project.nil? ? nil : [nil, project.id]))
  274. }
  275. scope :sorted, lambda {order(:name, :id)}
  276. # Scope of visible queries, can be used from subclasses only.
  277. # Unlike other visible scopes, a class methods is used as it
  278. # let handle inheritance more nicely than scope DSL.
  279. def self.visible(*args)
  280. if self == ::Query
  281. # Visibility depends on permissions for each subclass,
  282. # raise an error if the scope is called from Query (eg. Query.visible)
  283. raise "Cannot call .visible scope from the base Query class, but from subclasses only."
  284. end
  285. user = args.shift || User.current
  286. base = Project.allowed_to_condition(user, view_permission, *args)
  287. scope = joins("LEFT OUTER JOIN #{Project.table_name} ON #{table_name}.project_id = #{Project.table_name}.id").
  288. where("#{table_name}.project_id IS NULL OR (#{base})")
  289. if user.admin?
  290. scope.where("#{table_name}.visibility <> ? OR #{table_name}.user_id = ?", VISIBILITY_PRIVATE, user.id)
  291. elsif user.memberships.any?
  292. scope.where("#{table_name}.visibility = ?" +
  293. " OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" +
  294. "SELECT DISTINCT q.id FROM #{table_name} q" +
  295. " INNER JOIN #{table_name_prefix}queries_roles#{table_name_suffix} qr on qr.query_id = q.id" +
  296. " INNER JOIN #{MemberRole.table_name} mr ON mr.role_id = qr.role_id" +
  297. " INNER JOIN #{Member.table_name} m ON m.id = mr.member_id AND m.user_id = ?" +
  298. " INNER JOIN #{Project.table_name} p ON p.id = m.project_id AND p.status <> ?" +
  299. " WHERE q.project_id IS NULL OR q.project_id = m.project_id))" +
  300. " OR #{table_name}.user_id = ?",
  301. VISIBILITY_PUBLIC, VISIBILITY_ROLES, user.id, Project::STATUS_ARCHIVED, user.id)
  302. elsif user.logged?
  303. scope.where("#{table_name}.visibility = ? OR #{table_name}.user_id = ?", VISIBILITY_PUBLIC, user.id)
  304. else
  305. scope.where("#{table_name}.visibility = ?", VISIBILITY_PUBLIC)
  306. end
  307. end
  308. # Returns true if the query is visible to +user+ or the current user.
  309. def visible?(user=User.current)
  310. return true if user.admin?
  311. return false unless project.nil? || user.allowed_to?(self.class.view_permission, project)
  312. case visibility
  313. when VISIBILITY_PUBLIC
  314. true
  315. when VISIBILITY_ROLES
  316. if project
  317. (user.roles_for_project(project) & roles).any?
  318. else
  319. user.memberships.joins(:member_roles).where(:member_roles => {:role_id => roles.map(&:id)}).any?
  320. end
  321. else
  322. user == self.user
  323. end
  324. end
  325. def is_private?
  326. visibility == VISIBILITY_PRIVATE
  327. end
  328. def is_public?
  329. !is_private?
  330. end
  331. # Returns true if the query is available for all projects
  332. def is_global?
  333. new_record? ? project_id.nil? : project_id_in_database.nil?
  334. end
  335. def queried_table_name
  336. @queried_table_name ||= self.class.queried_class.table_name
  337. end
  338. # Builds the query from the given params
  339. def build_from_params(params, defaults={})
  340. if params[:fields] || params[:f]
  341. self.filters = {}
  342. add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
  343. else
  344. available_filters.each_key do |field|
  345. add_short_filter(field, params[field]) if params[field]
  346. end
  347. end
  348. query_params = params[:query] || defaults || {}
  349. self.group_by = params[:group_by] || query_params[:group_by] || self.group_by
  350. self.column_names = params[:c] || query_params[:column_names] || self.column_names
  351. self.totalable_names = params[:t] || query_params[:totalable_names] || self.totalable_names
  352. self.sort_criteria = params[:sort] || query_params[:sort_criteria] || self.sort_criteria
  353. self.display_type = params[:display_type] || query_params[:display_type] || self.display_type
  354. self
  355. end
  356. # Builds a new query from the given params and attributes
  357. def self.build_from_params(params, attributes={})
  358. new(attributes).build_from_params(params)
  359. end
  360. def as_params
  361. if new_record?
  362. params = {}
  363. filters.each do |field, options|
  364. params[:f] ||= []
  365. params[:f] << field
  366. params[:op] ||= {}
  367. params[:op][field] = options[:operator]
  368. params[:v] ||= {}
  369. params[:v][field] = options[:values]
  370. end
  371. params[:c] = column_names
  372. params[:group_by] = group_by.to_s if group_by.present?
  373. params[:t] = totalable_names.map(&:to_s) if totalable_names.any?
  374. params[:sort] = sort_criteria.to_param
  375. params[:set_filter] = 1
  376. params
  377. else
  378. {:query_id => id}
  379. end
  380. end
  381. def validate_query_filters
  382. filters.each_key do |field|
  383. if values_for(field)
  384. case type_for(field)
  385. when :integer
  386. add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !/\A[+-]?\d+(,[+-]?\d+)*\z/.match?(v) }
  387. when :float
  388. add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !/\A[+-]?\d+(\.\d*)?\z/.match?(v) }
  389. when :date, :date_past
  390. case operator_for(field)
  391. when "=", ">=", "<=", "><"
  392. add_filter_error(field, :invalid) if values_for(field).detect {|v|
  393. v.present? && (!/\A\d{4}-\d{2}-\d{2}(T\d{2}((:)?\d{2}){0,2}(Z|\d{2}:?\d{2})?)?\z/.match?(v) || parse_date(v).nil?)
  394. }
  395. when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
  396. add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !/^\d+$/.match?(v) }
  397. end
  398. end
  399. end
  400. add_filter_error(field, :blank) unless
  401. # filter requires one or more values
  402. (values_for(field) and !values_for(field).first.blank?) or
  403. # filter doesn't require any value
  404. ["o", "c", "!*", "*", "nd", "t", "ld", "nw", "w", "lw", "l2w", "nm", "m", "lm", "y", "*o", "!o"].include? operator_for(field)
  405. end if filters
  406. end
  407. def add_filter_error(field, message)
  408. m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
  409. errors.add(:base, m)
  410. end
  411. def editable_by?(user)
  412. return false unless user
  413. # Admin can edit them all and regular users can edit their private queries
  414. return true if user.admin? || (is_private? && self.user_id == user.id)
  415. # Members can not edit public queries that are for all project (only admin is allowed to)
  416. is_public? && !is_global? && user.allowed_to?(:manage_public_queries, project)
  417. end
  418. def trackers
  419. @trackers ||= (project.nil? ? Tracker.all : project.rolled_up_trackers).visible.sorted
  420. end
  421. # Returns a hash of localized labels for all filter operators
  422. def self.operators_labels
  423. operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
  424. end
  425. # Returns a representation of the available filters for JSON serialization
  426. def available_filters_as_json
  427. json = {}
  428. available_filters.each do |field, filter|
  429. options = {:type => filter[:type], :name => filter[:name]}
  430. options[:remote] = true if filter.remote
  431. if has_filter?(field) || !filter.remote
  432. options[:values] = filter.values
  433. if options[:values] && values_for(field)
  434. missing = Array(values_for(field)).select(&:present?) - options[:values].map(&:last)
  435. if missing.any? && respond_to?(method = "find_#{field}_filter_values")
  436. options[:values] += send(method, missing)
  437. end
  438. end
  439. end
  440. json[field] = options.stringify_keys
  441. end
  442. json
  443. end
  444. def all_projects
  445. @all_projects ||= Project.visible.to_a
  446. end
  447. def all_projects_values
  448. return @all_projects_values if @all_projects_values
  449. values = []
  450. Project.project_tree(all_projects) do |p, level|
  451. prefix = (level > 0 ? ('--' * level + ' ') : '')
  452. values << ["#{prefix}#{p.name}", p.id.to_s]
  453. end
  454. @all_projects_values = values
  455. end
  456. def project_values
  457. project_values = []
  458. if User.current.logged? && User.current.memberships.any?
  459. project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
  460. end
  461. project_values += all_projects_values
  462. project_values
  463. end
  464. def subproject_values
  465. project.descendants.visible.collect{|s| [s.name, s.id.to_s] }
  466. end
  467. def principals
  468. @principal ||= begin
  469. principals = []
  470. if project
  471. principals += Principal.member_of(project).visible
  472. unless project.leaf?
  473. principals += Principal.member_of(project.descendants.visible).visible
  474. end
  475. else
  476. principals += Principal.member_of(all_projects).visible
  477. end
  478. principals.uniq!
  479. principals.sort!
  480. principals.reject! {|p| p.is_a?(GroupBuiltin)}
  481. principals
  482. end
  483. end
  484. def users
  485. principals.select {|p| p.is_a?(User)}
  486. end
  487. def author_values
  488. author_values = []
  489. author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
  490. author_values += users.sort_by(&:status).collect{|s| [s.name, s.id.to_s, l("status_#{User::LABEL_BY_STATUS[s.status]}")] }
  491. author_values
  492. end
  493. def assigned_to_values
  494. assigned_to_values = []
  495. assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
  496. assigned_to_values += (Setting.issue_group_assignment? ? principals : users).sort_by(&:status).collect{|s| [s.name, s.id.to_s, l("status_#{User::LABEL_BY_STATUS[s.status]}")] }
  497. assigned_to_values
  498. end
  499. def fixed_version_values
  500. versions = []
  501. if project
  502. versions = project.shared_versions.to_a
  503. else
  504. versions = Version.visible.to_a
  505. end
  506. Version.sort_by_status(versions).collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s, l("version_status_#{s.status}")] }
  507. end
  508. # Returns a scope of issue statuses that are available as columns for filters
  509. def issue_statuses_values
  510. if project
  511. statuses = project.rolled_up_statuses
  512. else
  513. statuses = IssueStatus.all.sorted
  514. end
  515. statuses.collect{|s| [s.name, s.id.to_s]}
  516. end
  517. def watcher_values
  518. watcher_values = [["<< #{l(:label_me)} >>", "me"]]
  519. watcher_values += users.sort_by(&:status).collect{|s| [s.name, s.id.to_s, l("status_#{User::LABEL_BY_STATUS[s.status]}")] } if User.current.allowed_to?(:view_issue_watchers, self.project)
  520. watcher_values
  521. end
  522. # Returns a scope of issue custom fields that are available as columns or filters
  523. def issue_custom_fields
  524. if project
  525. project.rolled_up_custom_fields
  526. else
  527. IssueCustomField.all
  528. end
  529. end
  530. # Returns a scope of project custom fields that are available as columns or filters
  531. def project_custom_fields
  532. ProjectCustomField.all
  533. end
  534. # Returns a scope of project statuses that are available as columns or filters
  535. def project_statuses_values
  536. [
  537. [l(:project_status_active), "#{Project::STATUS_ACTIVE}"],
  538. [l(:project_status_closed), "#{Project::STATUS_CLOSED}"]
  539. ]
  540. end
  541. # Adds available filters
  542. def initialize_available_filters
  543. # implemented by sub-classes
  544. end
  545. protected :initialize_available_filters
  546. # Adds an available filter
  547. def add_available_filter(field, options)
  548. @available_filters ||= ActiveSupport::OrderedHash.new
  549. @available_filters[field] = QueryFilter.new(field, options)
  550. @available_filters
  551. end
  552. # Removes an available filter
  553. def delete_available_filter(field)
  554. if @available_filters
  555. @available_filters.delete(field)
  556. end
  557. end
  558. # Return a hash of available filters
  559. def available_filters
  560. unless @available_filters
  561. initialize_available_filters
  562. @available_filters ||= {}
  563. end
  564. @available_filters
  565. end
  566. def add_filter(field, operator, values=nil)
  567. # values must be an array
  568. return unless values.nil? || values.is_a?(Array)
  569. # check if field is defined as an available filter
  570. if available_filters.has_key? field
  571. filters[field] = {:operator => operator, :values => (values || [''])}
  572. end
  573. end
  574. def add_short_filter(field, expression)
  575. return unless expression && available_filters.has_key?(field)
  576. field_type = available_filters[field][:type]
  577. operators_by_filter_type[field_type].sort.reverse.detect do |operator|
  578. next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
  579. values = $1
  580. add_filter field, operator, values.present? ? values.split('|') : ['']
  581. end || add_filter(field, '=', expression.to_s.split('|'))
  582. end
  583. # Add multiple filters using +add_filter+
  584. def add_filters(fields, operators, values)
  585. if fields.present? && operators.present?
  586. fields.each do |field|
  587. add_filter(field, operators[field], values && values[field])
  588. end
  589. end
  590. end
  591. def has_filter?(field)
  592. filters and filters[field]
  593. end
  594. def type_for(field)
  595. available_filters[field][:type] if available_filters.has_key?(field)
  596. end
  597. def operator_for(field)
  598. has_filter?(field) ? filters[field][:operator] : nil
  599. end
  600. def values_for(field)
  601. has_filter?(field) ? filters[field][:values] : nil
  602. end
  603. def value_for(field, index=0)
  604. (values_for(field) || [])[index]
  605. end
  606. def label_for(field)
  607. label = available_filters[field][:name] if available_filters.has_key?(field)
  608. label ||= queried_class.human_attribute_name(field, :default => field)
  609. end
  610. def self.add_available_column(column)
  611. self.available_columns << (column) if column.is_a?(QueryColumn)
  612. end
  613. # Returns an array of columns that can be used to group the results
  614. def groupable_columns
  615. available_columns.select {|c| c.groupable}
  616. end
  617. # Returns a Hash of columns and the key for sorting
  618. def sortable_columns
  619. available_columns.inject({}) {|h, column|
  620. h[column.name.to_s] = column.sortable
  621. h
  622. }
  623. end
  624. def columns
  625. return [] if available_columns.empty?
  626. # preserve the column_names order
  627. cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
  628. available_columns.find { |col| col.name == name }
  629. end.compact
  630. available_columns.select(&:frozen?) | cols
  631. end
  632. def inline_columns
  633. columns.select(&:inline?)
  634. end
  635. def block_columns
  636. columns.reject(&:inline?)
  637. end
  638. def available_inline_columns
  639. available_columns.select(&:inline?)
  640. end
  641. def available_block_columns
  642. available_columns.reject(&:inline?)
  643. end
  644. def available_totalable_columns
  645. available_columns.select(&:totalable)
  646. end
  647. def default_columns_names
  648. []
  649. end
  650. def default_totalable_names
  651. []
  652. end
  653. def column_names=(names)
  654. if names
  655. names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
  656. names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
  657. if names.delete(:all_inline)
  658. names = available_inline_columns.map(&:name) | names
  659. end
  660. # Set column_names to nil if default columns
  661. if names == default_columns_names
  662. names = nil
  663. end
  664. end
  665. write_attribute(:column_names, names)
  666. end
  667. def has_column?(column)
  668. name = column.is_a?(QueryColumn) ? column.name : column
  669. columns.detect {|c| c.name == name}
  670. end
  671. def has_custom_field_column?
  672. columns.any? {|column| column.is_a? QueryCustomFieldColumn}
  673. end
  674. def has_default_columns?
  675. column_names.nil? || column_names.empty?
  676. end
  677. def totalable_columns
  678. names = totalable_names
  679. available_totalable_columns.select {|column| names.include?(column.name)}
  680. end
  681. def totalable_names=(names)
  682. if names
  683. names = names.select(&:present?).map {|n| n.is_a?(Symbol) ? n : n.to_sym}
  684. end
  685. options[:totalable_names] = names
  686. end
  687. def totalable_names
  688. options[:totalable_names] || default_totalable_names || []
  689. end
  690. def default_sort_criteria
  691. []
  692. end
  693. def sort_criteria=(arg)
  694. c = Redmine::SortCriteria.new(arg)
  695. write_attribute(:sort_criteria, c.to_a)
  696. c
  697. end
  698. def sort_criteria
  699. c = read_attribute(:sort_criteria)
  700. if c.blank?
  701. c = default_sort_criteria
  702. end
  703. Redmine::SortCriteria.new(c)
  704. end
  705. def sort_criteria_key(index)
  706. sort_criteria[index].try(:first)
  707. end
  708. def sort_criteria_order(index)
  709. sort_criteria[index].try(:last)
  710. end
  711. def sort_clause
  712. if clause = sort_criteria.sort_clause(sortable_columns)
  713. clause.map {|c| Arel.sql c}
  714. end
  715. end
  716. # Returns the SQL sort order that should be prepended for grouping
  717. def group_by_sort_order
  718. if column = group_by_column
  719. order = (sort_criteria.order_for(column.name) || column.default_order || 'asc').try(:upcase)
  720. Array(column.sortable).map {|s| Arel.sql("#{s} #{order}")}
  721. end
  722. end
  723. # Returns true if the query is a grouped query
  724. def grouped?
  725. !group_by_column.nil?
  726. end
  727. def group_by_column
  728. groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
  729. end
  730. def group_by_statement
  731. group_by_column.try(:groupable)
  732. end
  733. def project_statement
  734. project_clauses = []
  735. active_subprojects_ids = []
  736. active_subprojects_ids = project.descendants.active.map(&:id) if project
  737. if active_subprojects_ids.any?
  738. if has_filter?("subproject_id")
  739. case operator_for("subproject_id")
  740. when '='
  741. # include the selected subprojects
  742. ids = [project.id] + values_for("subproject_id").map(&:to_i)
  743. project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
  744. when '!'
  745. # exclude the selected subprojects
  746. ids = [project.id] + active_subprojects_ids - values_for("subproject_id").map(&:to_i)
  747. project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
  748. when '!*'
  749. # main project only
  750. project_clauses << "#{Project.table_name}.id = %d" % project.id
  751. else
  752. # all subprojects
  753. project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
  754. end
  755. elsif Setting.display_subprojects_issues?
  756. project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
  757. else
  758. project_clauses << "#{Project.table_name}.id = %d" % project.id
  759. end
  760. elsif project
  761. project_clauses << "#{Project.table_name}.id = %d" % project.id
  762. end
  763. project_clauses.any? ? project_clauses.join(' AND ') : nil
  764. end
  765. def statement
  766. # filters clauses
  767. filters_clauses = []
  768. filters.each_key do |field|
  769. next if field == "subproject_id"
  770. v = values_for(field).clone
  771. next unless v and !v.empty?
  772. operator = operator_for(field)
  773. # "me" value substitution
  774. if %w(assigned_to_id author_id user_id watcher_id updated_by last_updated_by).include?(field)
  775. if v.delete("me")
  776. if User.current.logged?
  777. v.push(User.current.id.to_s)
  778. v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
  779. else
  780. v.push("0")
  781. end
  782. end
  783. end
  784. if field == 'project_id' || (self.type == 'ProjectQuery' && field == 'id')
  785. if v.delete('mine')
  786. v += User.current.memberships.map(&:project_id).map(&:to_s)
  787. end
  788. end
  789. if field =~ /^cf_(\d+)\.cf_(\d+)$/
  790. filters_clauses << sql_for_chained_custom_field(field, operator, v, $1, $2)
  791. elsif field =~ /cf_(\d+)$/
  792. # custom field
  793. filters_clauses << sql_for_custom_field(field, operator, v, $1)
  794. elsif field =~ /^cf_(\d+)\.(.+)$/
  795. filters_clauses << sql_for_custom_field_attribute(field, operator, v, $1, $2)
  796. elsif respond_to?(method = "sql_for_#{field.tr('.','_')}_field")
  797. # specific statement
  798. filters_clauses << send(method, field, operator, v)
  799. else
  800. # regular field
  801. filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
  802. end
  803. end if filters and valid?
  804. if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
  805. # Excludes results for which the grouped custom field is not visible
  806. filters_clauses << c.custom_field.visibility_by_project_condition
  807. end
  808. filters_clauses << project_statement
  809. filters_clauses.reject!(&:blank?)
  810. filters_clauses.any? ? filters_clauses.join(' AND ') : nil
  811. end
  812. # Returns the result count by group or nil if query is not grouped
  813. def result_count_by_group
  814. grouped_query do |scope|
  815. scope.count
  816. end
  817. end
  818. # Returns the sum of values for the given column
  819. def total_for(column)
  820. total_with_scope(column, base_scope)
  821. end
  822. # Returns a hash of the sum of the given column for each group,
  823. # or nil if the query is not grouped
  824. def total_by_group_for(column)
  825. grouped_query do |scope|
  826. total_with_scope(column, scope)
  827. end
  828. end
  829. def totals
  830. totals = totalable_columns.map {|column| [column, total_for(column)]}
  831. yield totals if block_given?
  832. totals
  833. end
  834. def totals_by_group
  835. totals = totalable_columns.map {|column| [column, total_by_group_for(column)]}
  836. yield totals if block_given?
  837. totals
  838. end
  839. def css_classes
  840. s = sort_criteria.first
  841. if s.present?
  842. key, asc = s
  843. "sort-by-#{key.to_s.dasherize} sort-#{asc}"
  844. end
  845. end
  846. def display_type
  847. options[:display_type] || self.available_display_types.first
  848. end
  849. def display_type=(type)
  850. unless type || self.available_display_types.include?(type)
  851. type = self.available_display_types.first
  852. end
  853. options[:display_type] = type
  854. end
  855. def available_display_types
  856. ['list']
  857. end
  858. private
  859. def grouped_query(&block)
  860. r = nil
  861. if grouped?
  862. r = yield base_group_scope
  863. c = group_by_column
  864. if c.is_a?(QueryCustomFieldColumn)
  865. r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
  866. end
  867. end
  868. r
  869. rescue ::ActiveRecord::StatementInvalid => e
  870. raise StatementInvalid.new(e.message)
  871. end
  872. def total_with_scope(column, scope)
  873. unless column.is_a?(QueryColumn)
  874. column = column.to_sym
  875. column = available_totalable_columns.detect {|c| c.name == column}
  876. end
  877. if column.is_a?(QueryCustomFieldColumn)
  878. custom_field = column.custom_field
  879. send "total_for_custom_field", custom_field, scope
  880. else
  881. send "total_for_#{column.name}", scope
  882. end
  883. rescue ::ActiveRecord::StatementInvalid => e
  884. raise StatementInvalid.new(e.message)
  885. end
  886. def base_scope
  887. raise "unimplemented"
  888. end
  889. def base_group_scope
  890. base_scope.
  891. joins(joins_for_order_statement(group_by_statement)).
  892. group(group_by_statement)
  893. end
  894. def total_for_custom_field(custom_field, scope, &block)
  895. total = custom_field.format.total_for_scope(custom_field, scope)
  896. total = map_total(total) {|t| custom_field.format.cast_total_value(custom_field, t)}
  897. total
  898. end
  899. def map_total(total, &block)
  900. if total.is_a?(Hash)
  901. total.each_key {|k| total[k] = yield total[k]}
  902. else
  903. total = yield total
  904. end
  905. total
  906. end
  907. def sql_for_custom_field(field, operator, value, custom_field_id)
  908. db_table = CustomValue.table_name
  909. db_field = 'value'
  910. filter = @available_filters[field]
  911. return nil unless filter
  912. if filter[:field].format.target_class && filter[:field].format.target_class <= User
  913. if value.delete('me')
  914. value.push User.current.id.to_s
  915. end
  916. end
  917. not_in = nil
  918. if operator == '!'
  919. # Makes ! operator work for custom fields with multiple values
  920. operator = '='
  921. not_in = 'NOT'
  922. end
  923. customized_key = "id"
  924. customized_class = queried_class
  925. if field =~ /^(.+)\.cf_/
  926. assoc = $1
  927. customized_key = "#{assoc}_id"
  928. customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
  929. raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
  930. end
  931. where = sql_for_field(field, operator, value, db_table, db_field, true)
  932. if /[<>]/.match?(operator)
  933. where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
  934. end
  935. "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
  936. "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
  937. " LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id}" +
  938. " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
  939. end
  940. def sql_for_chained_custom_field(field, operator, value, custom_field_id, chained_custom_field_id)
  941. not_in = nil
  942. if operator == '!'
  943. # Makes ! operator work for custom fields with multiple values
  944. operator = '='
  945. not_in = 'NOT'
  946. end
  947. filter = available_filters[field]
  948. target_class = filter[:through].format.target_class
  949. "#{queried_table_name}.id #{not_in} IN (" +
  950. "SELECT customized_id FROM #{CustomValue.table_name}" +
  951. " WHERE customized_type='#{queried_class}' AND custom_field_id=#{custom_field_id}" +
  952. " AND CAST(CASE value WHEN '' THEN '0' ELSE value END AS decimal(30,0)) IN (" +
  953. " SELECT customized_id FROM #{CustomValue.table_name}" +
  954. " WHERE customized_type='#{target_class}' AND custom_field_id=#{chained_custom_field_id}" +
  955. " AND #{sql_for_field(field, operator, value, CustomValue.table_name, 'value')}))"
  956. end
  957. def sql_for_custom_field_attribute(field, operator, value, custom_field_id, attribute)
  958. attribute = 'effective_date' if attribute == 'due_date'
  959. not_in = nil
  960. if operator == '!'
  961. # Makes ! operator work for custom fields with multiple values
  962. operator = '='
  963. not_in = 'NOT'
  964. end
  965. filter = available_filters[field]
  966. target_table_name = filter[:field].format.target_class.table_name
  967. "#{queried_table_name}.id #{not_in} IN (" +
  968. "SELECT customized_id FROM #{CustomValue.table_name}" +
  969. " WHERE customized_type='#{queried_class}' AND custom_field_id=#{custom_field_id}" +
  970. " AND CAST(CASE value WHEN '' THEN '0' ELSE value END AS decimal(30,0)) IN (" +
  971. " SELECT id FROM #{target_table_name} WHERE #{sql_for_field(field, operator, value, filter[:field].format.target_class.table_name, attribute)}))"
  972. end
  973. # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
  974. def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
  975. sql = ''
  976. case operator
  977. when "="
  978. if value.any?
  979. case type_for(field)
  980. when :date, :date_past
  981. sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter)
  982. when :integer
  983. int_values = value.first.to_s.scan(/[+-]?\d+/).map(&:to_i).join(",")
  984. if int_values.present?
  985. if is_custom_filter
  986. sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) IN (#{int_values}))"
  987. else
  988. sql = "#{db_table}.#{db_field} IN (#{int_values})"
  989. end
  990. else
  991. sql = "1=0"
  992. end
  993. when :float
  994. if is_custom_filter
  995. sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
  996. else
  997. sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
  998. end
  999. else
  1000. sql = queried_class.send(:sanitize_sql_for_conditions, ["#{db_table}.#{db_field} IN (?)", value])
  1001. end
  1002. else
  1003. # IN an empty set
  1004. sql = "1=0"
  1005. end
  1006. when "!"
  1007. if value.any?
  1008. sql = queried_class.send(:sanitize_sql_for_conditions, ["(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (?))", value])
  1009. else
  1010. # NOT IN an empty set
  1011. sql = "1=1"
  1012. end
  1013. when "!*"
  1014. sql = "#{db_table}.#{db_field} IS NULL"
  1015. sql += " OR #{db_table}.#{db_field} = ''" if (is_custom_filter || [:text, :string].include?(type_for(field)))
  1016. when "*"
  1017. sql = "#{db_table}.#{db_field} IS NOT NULL"
  1018. sql += " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
  1019. when ">="
  1020. if [:date, :date_past].include?(type_for(field))
  1021. sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter)
  1022. else
  1023. if is_custom_filter
  1024. sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) >= #{value.first.to_f})"
  1025. else
  1026. sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
  1027. end
  1028. end
  1029. when "<="
  1030. if [:date, :date_past].include?(type_for(field))
  1031. sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter)
  1032. else
  1033. if is_custom_filter
  1034. sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) <= #{value.first.to_f})"
  1035. else
  1036. sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
  1037. end
  1038. end
  1039. when "><"
  1040. if [:date, :date_past].include?(type_for(field))
  1041. sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter)
  1042. else
  1043. if is_custom_filter
  1044. sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
  1045. else
  1046. sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
  1047. end
  1048. end
  1049. when "o"
  1050. sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false})" if field == "status_id"
  1051. when "c"
  1052. sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_true})" if field == "status_id"
  1053. when "><t-"
  1054. # between today - n days and today
  1055. sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter)
  1056. when ">t-"
  1057. # >= today - n days
  1058. sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter)
  1059. when "<t-"
  1060. # <= today - n days
  1061. sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter)
  1062. when "t-"
  1063. # = n days in past
  1064. sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter)
  1065. when "><t+"
  1066. # between today and today + n days
  1067. sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter)
  1068. when ">t+"
  1069. # >= today + n days
  1070. sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter)
  1071. when "<t+"
  1072. # <= today + n days
  1073. sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter)
  1074. when "t+"
  1075. # = today + n days
  1076. sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter)
  1077. when "t"
  1078. # = today
  1079. sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter)
  1080. when "ld"
  1081. # = yesterday
  1082. sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter)
  1083. when "nd"
  1084. # = tomorrow
  1085. sql = relative_date_clause(db_table, db_field, 1, 1, is_custom_filter)
  1086. when "w"
  1087. # = this week
  1088. first_day_of_week = l(:general_first_day_of_week).to_i
  1089. day_of_week = User.current.today.cwday
  1090. days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
  1091. sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter)
  1092. when "lw"
  1093. # = last week
  1094. first_day_of_week = l(:general_first_day_of_week).to_i
  1095. day_of_week = User.current.today.cwday
  1096. days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
  1097. sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
  1098. when "l2w"
  1099. # = last 2 weeks
  1100. first_day_of_week = l(:general_first_day_of_week).to_i
  1101. day_of_week = User.current.today.cwday
  1102. days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
  1103. sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter)
  1104. when "nw"
  1105. # = next week
  1106. first_day_of_week = l(:general_first_day_of_week).to_i
  1107. day_of_week = User.current.today.cwday
  1108. from = -(day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week) + 7
  1109. sql = relative_date_clause(db_table, db_field, from, from + 6, is_custom_filter)
  1110. when "m"
  1111. # = this month
  1112. date = User.current.today
  1113. sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
  1114. when "lm"
  1115. # = last month
  1116. date = User.current.today.prev_month
  1117. sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
  1118. when "nm"
  1119. # = next month
  1120. date = User.current.today.next_month
  1121. sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
  1122. when "y"
  1123. # = this year
  1124. date = User.current.today
  1125. sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter)
  1126. when "~"
  1127. sql = sql_contains("#{db_table}.#{db_field}", value.first)
  1128. when "!~"
  1129. sql = sql_contains("#{db_table}.#{db_field}", value.first, :match => false)
  1130. when "^"
  1131. sql = sql_contains("#{db_table}.#{db_field}", value.first, :starts_with => true)
  1132. when "$"
  1133. sql = sql_contains("#{db_table}.#{db_field}", value.first, :ends_with => true)
  1134. else
  1135. raise "Unknown query operator #{operator}"
  1136. end
  1137. return sql
  1138. end
  1139. # Returns a SQL LIKE statement with wildcards
  1140. def sql_contains(db_field, value, options={})
  1141. options = {} unless options.is_a?(Hash)
  1142. options.symbolize_keys!
  1143. prefix = suffix = nil
  1144. prefix = '%' if options[:ends_with]
  1145. suffix = '%' if options[:starts_with]
  1146. prefix = suffix = '%' if prefix.nil? && suffix.nil?
  1147. queried_class.send :sanitize_sql_for_conditions,
  1148. [Redmine::Database.like(db_field, '?', :match => options[:match]), "#{prefix}#{value}#{suffix}"]
  1149. end
  1150. # Adds a filter for the given custom field
  1151. def add_custom_field_filter(field, assoc=nil)
  1152. options = field.query_filter_options(self)
  1153. filter_id = "cf_#{field.id}"
  1154. filter_name = field.name
  1155. if assoc.present?
  1156. filter_id = "#{assoc}.#{filter_id}"
  1157. filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
  1158. end
  1159. add_available_filter filter_id, options.merge({
  1160. :name => filter_name,
  1161. :field => field
  1162. })
  1163. end
  1164. # Adds filters for custom fields associated to the custom field target class
  1165. # Eg. having a version custom field "Milestone" for issues and a date custom field "Release date"
  1166. # for versions, it will add an issue filter on Milestone'e Release date.
  1167. def add_chained_custom_field_filters(field)
  1168. klass = field.format.target_class
  1169. if klass
  1170. CustomField.where(:is_filter => true, :type => "#{klass.name}CustomField").each do |chained|
  1171. options = chained.query_filter_options(self)
  1172. filter_id = "cf_#{field.id}.cf_#{chained.id}"
  1173. filter_name = chained.name
  1174. add_available_filter filter_id, options.merge({
  1175. :name => l(:label_attribute_of_object, :name => chained.name, :object_name => field.name),
  1176. :field => chained,
  1177. :through => field
  1178. })
  1179. end
  1180. end
  1181. end
  1182. # Adds filters for the given custom fields scope
  1183. def add_custom_fields_filters(scope, assoc=nil)
  1184. scope.visible.where(:is_filter => true).sorted.each do |field|
  1185. add_custom_field_filter(field, assoc)
  1186. if assoc.nil?
  1187. add_chained_custom_field_filters(field)
  1188. if field.format.target_class && field.format.target_class == Version
  1189. add_available_filter "cf_#{field.id}.due_date",
  1190. :type => :date,
  1191. :field => field,
  1192. :name => l(:label_attribute_of_object, :name => l(:field_effective_date), :object_name => field.name)
  1193. add_available_filter "cf_#{field.id}.status",
  1194. :type => :list,
  1195. :field => field,
  1196. :name => l(:label_attribute_of_object, :name => l(:field_status), :object_name => field.name),
  1197. :values => Version::VERSION_STATUSES.map{|s| [l("version_status_#{s}"), s] }
  1198. end
  1199. end
  1200. end
  1201. end
  1202. # Adds filters for the given associations custom fields
  1203. def add_associations_custom_fields_filters(*associations)
  1204. fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
  1205. associations.each do |assoc|
  1206. association_klass = queried_class.reflect_on_association(assoc).klass
  1207. fields_by_class.each do |field_class, fields|
  1208. if field_class.customized_class <= association_klass
  1209. fields.sort.each do |field|
  1210. add_custom_field_filter(field, assoc)
  1211. end
  1212. end
  1213. end
  1214. end
  1215. end
  1216. def quoted_time(time, is_custom_filter)
  1217. if is_custom_filter
  1218. # Custom field values are stored as strings in the DB
  1219. # using this format that does not depend on DB date representation
  1220. time.strftime("%Y-%m-%d %H:%M:%S")
  1221. else
  1222. self.class.connection.quoted_date(time)
  1223. end
  1224. end
  1225. def date_for_user_time_zone(y, m, d)
  1226. if tz = User.current.time_zone
  1227. tz.local y, m, d
  1228. else
  1229. Time.local y, m, d
  1230. end
  1231. end
  1232. # Returns a SQL clause for a date or datetime field.
  1233. def date_clause(table, field, from, to, is_custom_filter)
  1234. s = []
  1235. if from
  1236. if from.is_a?(Date)
  1237. from = date_for_user_time_zone(from.year, from.month, from.day).yesterday.end_of_day
  1238. else
  1239. from = from - 1 # second
  1240. end
  1241. if self.class.default_timezone == :utc
  1242. from = from.utc
  1243. end
  1244. s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)])
  1245. end
  1246. if to
  1247. if to.is_a?(Date)
  1248. to = date_for_user_time_zone(to.year, to.month, to.day).end_of_day
  1249. end
  1250. if self.class.default_timezone == :utc
  1251. to = to.utc
  1252. end
  1253. s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)])
  1254. end
  1255. s.join(' AND ')
  1256. end
  1257. # Returns a SQL clause for a date or datetime field using relative dates.
  1258. def relative_date_clause(table, field, days_from, days_to, is_custom_filter)
  1259. date_clause(table, field, (days_from ? User.current.today + days_from : nil), (days_to ? User.current.today + days_to : nil), is_custom_filter)
  1260. end
  1261. # Returns a Date or Time from the given filter value
  1262. def parse_date(arg)
  1263. if /\A\d{4}-\d{2}-\d{2}T/.match?(arg.to_s)
  1264. Time.parse(arg) rescue nil
  1265. else
  1266. Date.parse(arg) rescue nil
  1267. end
  1268. end
  1269. # Additional joins required for the given sort options
  1270. def joins_for_order_statement(order_options)
  1271. joins = []
  1272. if order_options
  1273. order_options.scan(/cf_\d+/).uniq.each do |name|
  1274. column = available_columns.detect {|c| c.name.to_s == name}
  1275. join = column && column.custom_field.join_for_order_statement
  1276. if join
  1277. joins << join
  1278. end
  1279. end
  1280. end
  1281. joins.any? ? joins.join(' ') : nil
  1282. end
  1283. end