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

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