You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

query.rb 47KB

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