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

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