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 51KB

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