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

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