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

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