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.

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