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

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