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

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