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

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