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

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