Advertisement
Guest User

Untitled

a guest
Jan 23rd, 2018
68
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 31.11 KB | None | 0 0
  1. # redMine - project management software
  2. # Copyright (C) 2006-2007 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.  
  18. class Issue < ActiveRecord::Base
  19. belongs_to :project
  20. belongs_to :tracker
  21. belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
  22. belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
  23. belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
  24. belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
  25. belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
  26. belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
  27.  
  28.  
  29. belongs_to :contact
  30.  
  31. has_many :journals, :as => :journalized, :dependent => :destroy
  32. has_many :time_entries, :dependent => :delete_all
  33. has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
  34.  
  35. has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
  36. has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
  37.  
  38. acts_as_nested_set :scope => 'root_id'
  39. acts_as_attachable :after_remove => :attachment_removed
  40. acts_as_customizable
  41. acts_as_watchable
  42. acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
  43. :include => [:project, :journals],
  44. # sort by id so that limited eager loading doesn't break with postgresql
  45. :order_column => "#{table_name}.id"
  46. acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
  47. :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
  48. :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
  49.  
  50. acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
  51. :author_key => :author_id
  52.  
  53. DONE_RATIO_OPTIONS = %w(issue_field issue_status)
  54.  
  55. FORMAT_FOR_ME = "%Y-%m-%d"
  56.  
  57. attr_reader :current_journal
  58.  
  59. validates_presence_of :subject, :priority, :project, :tracker, :author, :status
  60.  
  61. validates_length_of :subject, :maximum => 255
  62. validates_inclusion_of :done_ratio, :in => 0..100
  63. validates_numericality_of :estimated_hours, :allow_nil => true
  64.  
  65. named_scope :visible, lambda {|*args| { :include => :project,
  66. :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
  67.  
  68. named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
  69.  
  70. named_scope :recently_updated, :order => "#{self.table_name}.updated_on DESC"
  71. named_scope :with_limit, lambda { |limit| { :limit => limit} }
  72. named_scope :on_active_project, :include => [:status, :project, :tracker],
  73. :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
  74.  
  75. # all issues overdue!
  76. named_scope :overdue, :conditions => ["#{self.table_name}.due_date < ?", Time.now.to_date]
  77. # all issues due a week from now
  78. named_scope :due_in_a_week, :conditions => ["#{self.table_name}.due_date = ?", 1.week.from_now.to_date]
  79. # all issues due within this week
  80. named_scope :upcoming, :conditions => ["#{self.table_name}.due_date between ? and ?", Time.now.to_date, 7.days.from_now.to_date]
  81.  
  82. before_create :default_assign
  83. before_save :close_duplicates, :update_done_ratio_from_issue_status
  84. after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
  85. after_destroy :destroy_children
  86. after_destroy :update_parent_attributes
  87.  
  88.  
  89. def latest_comment
  90. cmts = []
  91. jrnls = self.journals { |a,b| b.created_on <=> a.created_on }
  92. jrnls.each { |j| cmts << j.notes unless j.notes.empty? }
  93. unless cmts.empty? then
  94. return cmts.first if cmts.first
  95. else
  96. return ""
  97. end
  98. end
  99.  
  100. # returns journal records that contain a message, aka comment
  101. def comments
  102. cmts = []
  103. jrnls = self.journals { |a,b| b.created_on <=> a.created_on }
  104. jrnls.each { |j| cmts << j unless j.notes.empty? }
  105. cmts
  106. end
  107.  
  108. # Returns true if usr or current user is allowed to view the issue
  109. def visible?(usr=nil)
  110. (usr || User.current).allowed_to?(:view_issues, self.project)
  111. end
  112.  
  113. def after_initialize
  114. if new_record?
  115. # set default values for new records only
  116. self.status ||= IssueStatus.default
  117. self.priority ||= IssuePriority.default
  118. end
  119. end
  120.  
  121. # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
  122. def available_custom_fields
  123. (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
  124. end
  125.  
  126. def copy_from(arg)
  127. issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
  128. self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
  129. self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
  130. self.status = issue.status
  131. self
  132. end
  133.  
  134. # Moves/copies an issue to a new project and tracker
  135. # Returns the moved/copied issue on success, false on failure
  136. def move_to_project(*args)
  137. ret = Issue.transaction do
  138. move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
  139. end || false
  140. end
  141.  
  142. def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
  143. options ||= {}
  144. issue = options[:copy] ? self.class.new.copy_from(self) : self
  145.  
  146. if new_project && issue.project_id != new_project.id
  147. # delete issue relations
  148. unless Setting.cross_project_issue_relations?
  149. issue.relations_from.clear
  150. issue.relations_to.clear
  151. end
  152. # issue is moved to another project
  153. # reassign to the category with same name if any
  154. new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
  155. issue.category = new_category
  156. # Keep the fixed_version if it's still valid in the new_project
  157. unless new_project.shared_versions.include?(issue.fixed_version)
  158. issue.fixed_version = nil
  159. end
  160. issue.project = new_project
  161. if issue.parent && issue.parent.project_id != issue.project_id
  162. issue.parent_issue_id = nil
  163. end
  164. end
  165. if new_tracker
  166. issue.tracker = new_tracker
  167. issue.reset_custom_values!
  168. end
  169. if options[:copy]
  170. issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
  171. issue.status = if options[:attributes] && options[:attributes][:status_id]
  172. IssueStatus.find_by_id(options[:attributes][:status_id])
  173. else
  174. self.status
  175. end
  176. end
  177. # Allow bulk setting of attributes on the issue
  178. if options[:attributes]
  179. issue.attributes = options[:attributes]
  180. end
  181. if issue.save
  182. unless options[:copy]
  183. # Manually update project_id on related time entries
  184. TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
  185.  
  186. issue.children.each do |child|
  187. unless child.move_to_project_without_transaction(new_project)
  188. # Move failed and transaction was rollback'd
  189. return false
  190. end
  191. end
  192. end
  193. else
  194. return false
  195. end
  196. issue
  197. end
  198.  
  199. def status_id=(sid)
  200. self.status = nil
  201. write_attribute(:status_id, sid)
  202. end
  203.  
  204. def priority_id=(pid)
  205. self.priority = nil
  206. write_attribute(:priority_id, pid)
  207. end
  208.  
  209. def tracker_id=(tid)
  210. self.tracker = nil
  211. result = write_attribute(:tracker_id, tid)
  212. @custom_field_values = nil
  213. result
  214. end
  215.  
  216. # Overrides attributes= so that tracker_id gets assigned first
  217. def attributes_with_tracker_first=(new_attributes, *args)
  218. return if new_attributes.nil?
  219. new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
  220. if new_tracker_id
  221. self.tracker_id = new_tracker_id
  222. end
  223. send :attributes_without_tracker_first=, new_attributes, *args
  224. end
  225. # Do not redefine alias chain on reload (see #4838)
  226. alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
  227.  
  228. def estimated_hours=(h)
  229. write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
  230. end
  231.  
  232. SAFE_ATTRIBUTES = %w(
  233. tracker_id
  234. status_id
  235. parent_issue_id
  236. category_id
  237. assigned_to_id
  238. contact_id
  239. priority_id
  240. fixed_version_id
  241. subject
  242. description
  243. start_date
  244. due_date
  245. done_ratio
  246. estimated_hours
  247. custom_field_values
  248. lock_version
  249. ) unless const_defined?(:SAFE_ATTRIBUTES)
  250.  
  251. # Safely sets attributes
  252. # Should be called from controllers instead of #attributes=
  253. # attr_accessible is too rough because we still want things like
  254. # Issue.new(:project => foo) to work
  255. # TODO: move workflow/permission checks from controllers to here
  256. def safe_attributes=(attrs, user=User.current)
  257. return if attrs.nil?
  258. attrs = attrs.reject {|k,v| !SAFE_ATTRIBUTES.include?(k)}
  259. if attrs['status_id']
  260. unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
  261. attrs.delete('status_id')
  262. end
  263. end
  264.  
  265. unless leaf?
  266. attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
  267. end
  268.  
  269. if attrs.has_key?('parent_issue_id')
  270. if !user.allowed_to?(:manage_subtasks, project)
  271. attrs.delete('parent_issue_id')
  272. elsif !attrs['parent_issue_id'].blank?
  273. attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'])
  274. end
  275. end
  276.  
  277. self.attributes = attrs
  278. end
  279.  
  280. def done_ratio
  281. if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
  282. status.default_done_ratio
  283. else
  284. read_attribute(:done_ratio)
  285. end
  286. end
  287.  
  288. def self.use_status_for_done_ratio?
  289. Setting.issue_done_ratio == 'issue_status'
  290. end
  291.  
  292. def self.use_field_for_done_ratio?
  293. Setting.issue_done_ratio == 'issue_field'
  294. end
  295.  
  296. def validate
  297. if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
  298. errors.add :due_date, :not_a_date
  299. end
  300.  
  301. if self.due_date and self.start_date and self.due_date < self.start_date
  302. errors.add :due_date, :greater_than_start_date
  303. end
  304.  
  305. if start_date && soonest_start && start_date < soonest_start
  306. errors.add :start_date, :invalid
  307. end
  308.  
  309. if fixed_version
  310. if !assignable_versions.include?(fixed_version)
  311. errors.add :fixed_version_id, :inclusion
  312. elsif reopened? && fixed_version.closed?
  313. errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
  314. end
  315. end
  316.  
  317. # Checks that the issue can not be added/moved to a disabled tracker
  318. if project && (tracker_id_changed? || project_id_changed?)
  319. unless project.trackers.include?(tracker)
  320. errors.add :tracker_id, :inclusion
  321. end
  322. end
  323.  
  324. # Checks parent issue assignment
  325. if @parent_issue
  326. if @parent_issue.project_id != project_id
  327. errors.add :parent_issue_id, :not_same_project
  328. elsif !new_record?
  329. # moving an existing issue
  330. if @parent_issue.root_id != root_id
  331. # we can always move to another tree
  332. elsif move_possible?(@parent_issue)
  333. # move accepted inside tree
  334. else
  335. errors.add :parent_issue_id, :not_a_valid_parent
  336. end
  337. end
  338. end
  339. end
  340.  
  341. # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
  342. # even if the user turns off the setting later
  343. def update_done_ratio_from_issue_status
  344. if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
  345. self.done_ratio = status.default_done_ratio
  346. end
  347. end
  348.  
  349. def init_journal(user, notes = "")
  350. @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
  351. @issue_before_change = self.clone
  352. @issue_before_change.status = self.status
  353. @custom_values_before_change = {}
  354. self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
  355. # Make sure updated_on is updated when adding a note.
  356. updated_on_will_change!
  357. @current_journal
  358. end
  359.  
  360. # Return true if the issue is closed, otherwise false
  361. def closed?
  362. self.status.is_closed?
  363. end
  364.  
  365. # Return true if the issue is being reopened
  366. def reopened?
  367. if !new_record? && status_id_changed?
  368. status_was = IssueStatus.find_by_id(status_id_was)
  369. status_new = IssueStatus.find_by_id(status_id)
  370. if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
  371. return true
  372. end
  373. end
  374. false
  375. end
  376.  
  377. # Return true if the issue is being closed
  378. def closing?
  379. if !new_record? && status_id_changed?
  380. status_was = IssueStatus.find_by_id(status_id_was)
  381. status_new = IssueStatus.find_by_id(status_id)
  382. if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
  383. return true
  384. end
  385. end
  386. false
  387. end
  388.  
  389. # Returns true if the issue is overdue
  390. def overdue?
  391. !due_date.nil? && (due_date < Date.today) && !status.is_closed?
  392. end
  393.  
  394. # Does this issue have children?
  395. def children?
  396. !leaf?
  397. end
  398.  
  399. # Users the issue can be assigned to
  400. def assignable_users
  401. users = project.assignable_users
  402. users << author if author
  403. users.uniq.sort
  404. end
  405.  
  406. # Versions that the issue can be assigned to
  407. def assignable_versions
  408. @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
  409. end
  410.  
  411. # Returns true if this issue is blocked by another issue that is still open
  412. def blocked?
  413. !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
  414. end
  415.  
  416. # Returns an array of status that user is able to apply
  417. def new_statuses_allowed_to(user, include_default=false)
  418. statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
  419. statuses << status unless statuses.empty?
  420. statuses << IssueStatus.default if include_default
  421. statuses = statuses.uniq.sort
  422. blocked? ? statuses.reject {|s| s.is_closed?} : statuses
  423. end
  424.  
  425. # Returns the mail adresses of users that should be notified
  426. def recipients
  427. notified = project.notified_users
  428. # Author and assignee are always notified unless they have been locked
  429. notified << author if author && author.active?
  430. notified << assigned_to if assigned_to && assigned_to.active?
  431. notified.uniq!
  432. # Remove users that can not view the issue
  433. notified.reject! {|user| !visible?(user)}
  434. notified.collect(&:mail)
  435. end
  436.  
  437. # Returns the total number of hours spent on this issue and its descendants
  438. #
  439. # Example:
  440. # spent_hours => 0.0
  441. # spent_hours => 50.2
  442. def spent_hours
  443. @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
  444. end
  445.  
  446. def relations
  447. (relations_from + relations_to).sort
  448. end
  449.  
  450. def all_dependent_issues
  451. dependencies = []
  452. relations_from.each do |relation|
  453. dependencies << relation.issue_to
  454. dependencies += relation.issue_to.all_dependent_issues
  455. end
  456. dependencies
  457. end
  458.  
  459. # Returns an array of issues that duplicate this one
  460. def duplicates
  461. relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
  462. end
  463.  
  464. # Returns the due date or the target due date if any
  465. # Used on gantt chart
  466. def due_before
  467. due_date || (fixed_version ? fixed_version.effective_date : nil)
  468. end
  469.  
  470. # Returns the time scheduled for this issue.
  471. #
  472. # Example:
  473. # Start Date: 2/26/09, End Date: 3/04/09
  474. # duration => 6
  475. def duration
  476. (start_date && due_date) ? due_date - start_date : 0
  477. end
  478.  
  479. def soonest_start
  480. @soonest_start ||= (
  481. relations_to.collect{|relation| relation.successor_soonest_start} +
  482. ancestors.collect(&:soonest_start)
  483. ).compact.max
  484. end
  485.  
  486. def reschedule_after(date)
  487. return if date.nil?
  488. if leaf?
  489. if start_date.nil? || start_date < date
  490. self.start_date, self.due_date = date, date + duration
  491. save
  492. end
  493. else
  494. leaves.each do |leaf|
  495. leaf.reschedule_after(date)
  496. end
  497. end
  498. end
  499.  
  500. def <=>(issue)
  501. if issue.nil?
  502. -1
  503. elsif root_id != issue.root_id
  504. (root_id || 0) <=> (issue.root_id || 0)
  505. else
  506. (lft || 0) <=> (issue.lft || 0)
  507. end
  508. end
  509.  
  510. def to_s
  511. "#{tracker} ##{id}: #{subject}"
  512. end
  513.  
  514. # Returns a string of css classes that apply to the issue
  515. def css_classes
  516. s = "issue status-#{status.position} priority-#{priority.position}"
  517. s << ' closed' if closed?
  518. s << ' overdue' if overdue?
  519. s << ' created-by-me' if User.current.logged? && author_id == User.current.id
  520. s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
  521. s
  522. end
  523.  
  524. # Saves an issue, time_entry, attachments, and a journal from the parameters
  525. # Returns false if save fails
  526. def save_issue_with_child_records(params, existing_time_entry=nil)
  527. Issue.transaction do
  528. if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project)
  529. @time_entry = existing_time_entry || TimeEntry.new
  530. @time_entry.project = project
  531. @time_entry.issue = self
  532. @time_entry.user = User.current
  533. @time_entry.spent_on = Date.today
  534. @time_entry.attributes = params[:time_entry]
  535. self.time_entries << @time_entry
  536. end
  537.  
  538. if valid?
  539. attachments = Attachment.attach_files(self, params[:attachments])
  540.  
  541. attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
  542. # TODO: Rename hook
  543. Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
  544. begin
  545. if save
  546. # TODO: Rename hook
  547. Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
  548. else
  549. raise ActiveRecord::Rollback
  550. end
  551. rescue ActiveRecord::StaleObjectError
  552. attachments[:files].each(&:destroy)
  553. errors.add_to_base l(:notice_locking_conflict)
  554. raise ActiveRecord::Rollback
  555. end
  556. end
  557. end
  558. end
  559.  
  560. # Unassigns issues from +version+ if it's no longer shared with issue's project
  561. def self.update_versions_from_sharing_change(version)
  562. # Update issues assigned to the version
  563. update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
  564. end
  565.  
  566. # Unassigns issues from versions that are no longer shared
  567. # after +project+ was moved
  568. def self.update_versions_from_hierarchy_change(project)
  569. moved_project_ids = project.self_and_descendants.reload.collect(&:id)
  570. # Update issues of the moved projects and issues assigned to a version of a moved project
  571. Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
  572. end
  573.  
  574. def parent_issue_id=(arg)
  575. parent_issue_id = arg.blank? ? nil : arg.to_i
  576. if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
  577. @parent_issue.id
  578. else
  579. @parent_issue = nil
  580. nil
  581. end
  582. end
  583.  
  584. def parent_issue_id
  585. if instance_variable_defined? :@parent_issue
  586. @parent_issue.nil? ? nil : @parent_issue.id
  587. else
  588. parent_id
  589. end
  590. end
  591.  
  592. # Extracted from the ReportsController.
  593. def self.by_tracker(project)
  594. count_and_group_by(:project => project,
  595. :field => 'tracker_id',
  596. :joins => Tracker.table_name)
  597. end
  598.  
  599. def self.by_version(project)
  600. count_and_group_by(:project => project,
  601. :field => 'fixed_version_id',
  602. :joins => Version.table_name)
  603. end
  604.  
  605. def self.by_priority(project)
  606. count_and_group_by(:project => project,
  607. :field => 'priority_id',
  608. :joins => IssuePriority.table_name)
  609. end
  610.  
  611. def self.by_category(project)
  612. count_and_group_by(:project => project,
  613. :field => 'category_id',
  614. :joins => IssueCategory.table_name)
  615. end
  616.  
  617. def self.by_assigned_to(project)
  618. count_and_group_by(:project => project,
  619. :field => 'assigned_to_id',
  620. :joins => User.table_name)
  621. end
  622.  
  623. def self.by_author(project)
  624. count_and_group_by(:project => project,
  625. :field => 'author_id',
  626. :joins => User.table_name)
  627. end
  628.  
  629. def self.by_subproject(project)
  630. ActiveRecord::Base.connection.select_all("select s.id as status_id,
  631. s.is_closed as closed,
  632. i.project_id as project_id,
  633. count(i.id) as total
  634. from
  635. #{Issue.table_name} i, #{IssueStatus.table_name} s
  636. where
  637. i.status_id=s.id
  638. and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
  639. group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
  640. end
  641. # End ReportsController extraction
  642.  
  643. # Returns an array of projects that current user can move issues to
  644. def self.allowed_target_projects_on_move
  645. projects = []
  646. if User.current.admin?
  647. # admin is allowed to move issues to any active (visible) project
  648. projects = Project.visible.all
  649. elsif User.current.logged?
  650. if Role.non_member.allowed_to?(:move_issues)
  651. projects = Project.visible.all
  652. else
  653. User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
  654. end
  655. end
  656. projects
  657. end
  658.  
  659. private
  660.  
  661. def update_nested_set_attributes
  662. if root_id.nil?
  663. # issue was just created
  664. self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
  665. set_default_left_and_right
  666. Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
  667. if @parent_issue
  668. move_to_child_of(@parent_issue)
  669. end
  670. reload
  671. elsif parent_issue_id != parent_id
  672. former_parent_id = parent_id
  673. # moving an existing issue
  674. if @parent_issue && @parent_issue.root_id == root_id
  675. # inside the same tree
  676. move_to_child_of(@parent_issue)
  677. else
  678. # to another tree
  679. unless root?
  680. move_to_right_of(root)
  681. reload
  682. end
  683. old_root_id = root_id
  684. self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
  685. target_maxright = nested_set_scope.maximum(right_column_name) || 0
  686. offset = target_maxright + 1 - lft
  687. Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
  688. ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
  689. self[left_column_name] = lft + offset
  690. self[right_column_name] = rgt + offset
  691. if @parent_issue
  692. move_to_child_of(@parent_issue)
  693. end
  694. end
  695. reload
  696. # delete invalid relations of all descendants
  697. self_and_descendants.each do |issue|
  698. issue.relations.each do |relation|
  699. relation.destroy unless relation.valid?
  700. end
  701. end
  702. # update former parent
  703. recalculate_attributes_for(former_parent_id) if former_parent_id
  704. end
  705. remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
  706. end
  707.  
  708. def update_parent_attributes
  709. recalculate_attributes_for(parent_id) if parent_id
  710. end
  711.  
  712. def recalculate_attributes_for(issue_id)
  713. if issue_id && p = Issue.find_by_id(issue_id)
  714. # priority = highest priority of children
  715. if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
  716. p.priority = IssuePriority.find_by_position(priority_position)
  717. end
  718.  
  719. # start/due dates = lowest/highest dates of children
  720. p.start_date = p.children.minimum(:start_date)
  721. p.due_date = p.children.maximum(:due_date)
  722. if p.start_date && p.due_date && p.due_date < p.start_date
  723. p.start_date, p.due_date = p.due_date, p.start_date
  724. end
  725.  
  726. # done ratio = weighted average ratio of leaves
  727. unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
  728. leaves_count = p.leaves.count
  729. if leaves_count > 0
  730. average = p.leaves.average(:estimated_hours).to_f
  731. if average == 0
  732. average = 1
  733. end
  734. done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :include => :status).to_f
  735. progress = done / (average * leaves_count)
  736. p.done_ratio = progress.round
  737. end
  738. end
  739.  
  740. # estimate = sum of leaves estimates
  741. p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
  742. p.estimated_hours = nil if p.estimated_hours == 0.0
  743.  
  744. # ancestors will be recursively updated
  745. p.save(false)
  746. end
  747. end
  748.  
  749. def destroy_children
  750. unless leaf?
  751. children.each do |child|
  752. child.destroy
  753. end
  754. end
  755. end
  756.  
  757. # Update issues so their versions are not pointing to a
  758. # fixed_version that is not shared with the issue's project
  759. def self.update_versions(conditions=nil)
  760. # Only need to update issues with a fixed_version from
  761. # a different project and that is not systemwide shared
  762. Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
  763. " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
  764. " AND #{Version.table_name}.sharing <> 'system'",
  765. conditions),
  766. :include => [:project, :fixed_version]
  767. ).each do |issue|
  768. next if issue.project.nil? || issue.fixed_version.nil?
  769. unless issue.project.shared_versions.include?(issue.fixed_version)
  770. issue.init_journal(User.current)
  771. issue.fixed_version = nil
  772. issue.save
  773. end
  774. end
  775. end
  776.  
  777. # Callback on attachment deletion
  778. def attachment_removed(obj)
  779. journal = init_journal(User.current)
  780. journal.details << JournalDetail.new(:property => 'attachment',
  781. :prop_key => obj.id,
  782. :old_value => obj.filename)
  783. journal.save
  784. end
  785.  
  786. # Default assignment based on category
  787. def default_assign
  788. if assigned_to.nil? && category && category.assigned_to
  789. self.assigned_to = category.assigned_to
  790. end
  791. end
  792.  
  793. # Updates start/due dates of following issues
  794. def reschedule_following_issues
  795. if start_date_changed? || due_date_changed?
  796. relations_from.each do |relation|
  797. relation.set_issue_to_dates
  798. end
  799. end
  800. end
  801.  
  802. # Closes duplicates if the issue is being closed
  803. def close_duplicates
  804. if closing?
  805. duplicates.each do |duplicate|
  806. # Reload is need in case the duplicate was updated by a previous duplicate
  807. duplicate.reload
  808. # Don't re-close it if it's already closed
  809. next if duplicate.closed?
  810. # Same user and notes
  811. if @current_journal
  812. duplicate.init_journal(@current_journal.user, @current_journal.notes)
  813. end
  814. duplicate.update_attribute :status, self.status
  815. end
  816. end
  817. end
  818.  
  819. # Saves the changes in a Journal
  820. # Called after_save
  821. def create_journal
  822. if @current_journal
  823. # attributes changes
  824. (Issue.column_names - %w(id description root_id lft rgt lock_version created_on updated_on)).each {|c|
  825. @current_journal.details << JournalDetail.new(:property => 'attr',
  826. :prop_key => c,
  827. :old_value => @issue_before_change.send(c),
  828. :value => send(c)) unless send(c)==@issue_before_change.send(c)
  829. }
  830. # custom fields changes
  831. custom_values.each {|c|
  832. next if (@custom_values_before_change[c.custom_field_id]==c.value ||
  833. (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
  834. @current_journal.details << JournalDetail.new(:property => 'cf',
  835. :prop_key => c.custom_field_id,
  836. :old_value => @custom_values_before_change[c.custom_field_id],
  837. :value => c.value)
  838. }
  839. @current_journal.save
  840. # reset current journal
  841. init_journal @current_journal.user, @current_journal.notes
  842. end
  843. end
  844.  
  845. # Query generator for selecting groups of issue counts for a project
  846. # based on specific criteria
  847. #
  848. # Options
  849. # * project - Project to search in.
  850. # * field - String. Issue field to key off of in the grouping.
  851. # * joins - String. The table name to join against.
  852. def self.count_and_group_by(options)
  853. project = options.delete(:project)
  854. select_field = options.delete(:field)
  855. joins = options.delete(:joins)
  856.  
  857. where = "i.#{select_field}=j.id"
  858.  
  859. ActiveRecord::Base.connection.select_all("select s.id as status_id,
  860. s.is_closed as closed,
  861. j.id as #{select_field},
  862. count(i.id) as total
  863. from
  864. #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j
  865. where
  866. i.status_id=s.id
  867. and #{where}
  868. and i.project_id=#{project.id}
  869. group by s.id, s.is_closed, j.id")
  870. end
  871.  
  872.  
  873. end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement