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

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