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.

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