require 'carrierwave/orm/activerecord' class Issue < ActiveRecord::Base include InternalId include Issuable include Referable include Sortable include Taskable DueDateStruct = Struct.new(:title, :name).freeze NoDueDate = DueDateStruct.new('No Due Date', '0').freeze AnyDueDate = DueDateStruct.new('Any Due Date', '').freeze Overdue = DueDateStruct.new('Overdue', 'overdue').freeze DueThisWeek = DueDateStruct.new('Due This Week', 'week').freeze DueThisMonth = DueDateStruct.new('Due This Month', 'month').freeze ActsAsTaggableOn.strict_case_match = true belongs_to :project belongs_to :moved_to, class_name: 'Issue' has_many :events, as: :target, dependent: :destroy validates :project, presence: true scope :cared, ->(user) { where(assignee_id: user) } scope :open_for, ->(user) { opened.assigned_to(user) } scope :in_projects, ->(project_ids) { where(project_id: project_ids) } scope :without_due_date, -> { where(due_date: nil) } scope :due_before, ->(date) { where('issues.due_date < ?', date) } scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) } scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') } scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') } state_machine :state, initial: :opened do event :close do transition [:reopened, :opened] => :closed end event :reopen do transition closed: :reopened end state :opened state :reopened state :closed end def hook_attrs attributes end class << self private # Returns the project that the current scope belongs to if any, nil otherwise. # # Examples: # - my_project.issues.without_due_date.owner_project => my_project # - Issue.all.owner_project => nil def owner_project # No owner if we're not being called from an association return unless all.respond_to?(:proxy_association) owner = all.proxy_association.owner # Check if the association is or belongs to a project if owner.is_a?(Project) owner else begin owner.association(:project).target rescue ActiveRecord::AssociationNotFoundError nil end end end end def self.visible_to_user(user) return where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank? return all if user.admin? # Check if we are scoped to a specific project's issues if owner_project if owner_project.authorized_for_user?(user, Gitlab::Access::REPORTER) # If the project is authorized for the user, they can see all issues in the project return all else # else only non confidential and authored/assigned to them return where('issues.confidential IS NULL OR issues.confidential IS FALSE OR issues.author_id = :user_id OR issues.assignee_id = :user_id', user_id: user.id) end end where(' issues.confidential IS NULL OR issues.confidential IS FALSE OR (issues.confidential = TRUE AND (issues.author_id = :user_id OR issues.assignee_id = :user_id OR issues.project_id IN(:project_ids)))', user_id: user.id, project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id)) end def self.reference_prefix '#' end # Pattern used to extract `#123` issue references from text # # This pattern supports cross-project references. def self.reference_pattern @reference_pattern ||= %r{ (#{Project.reference_pattern})? #{Regexp.escape(reference_prefix)}(?\d+) }x end def self.link_reference_pattern @link_reference_pattern ||= super("issues", /(?\d+)/) end def self.reference_valid?(reference) reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE end def self.sort(method, excluded_labels: []) case method.to_s when 'due_date_asc' then order_due_date_asc when 'due_date_desc' then order_due_date_desc else super end end def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{iid}" if cross_project_reference?(from_project) reference = project.to_reference + reference end reference end def referenced_merge_requests(current_user = nil) ext = all_references(current_user) notes_with_associations.each do |object| object.all_references(current_user, extractor: ext) end ext.merge_requests.sort_by(&:iid) end # All branches containing the current issue's ID, except for # those with a merge request open referencing the current issue. def related_branches(current_user) branches_with_iid = project.repository.branch_names.select do |branch| branch =~ /\A#{iid}-(?!\d+-stable)/i end branches_with_merge_request = self.referenced_merge_requests(current_user).map(&:source_branch) branches_with_iid - branches_with_merge_request end # Reset issue events cache # # Since we do cache @event we need to reset cache in special cases: # * when an issue is updated # Events cache stored like events/23-20130109142513. # The cache key includes updated_at timestamp. # Thus it will automatically generate a new fragment # when the event is updated because the key changes. def reset_events_cache Event.reset_event_cache_for(self) end # To allow polymorphism with MergeRequest. def source_project project end # From all notes on this issue, we'll select the system notes about linked # merge requests. Of those, the MRs closing `self` are returned. def closed_by_merge_requests(current_user = nil) return [] unless open? ext = all_references(current_user) notes.system.each do |note| note.all_references(current_user, extractor: ext) end ext.merge_requests.select { |mr| mr.open? && mr.closes_issue?(self) } end def moved? !moved_to.nil? end def can_move?(user, to_project = nil) if to_project return false unless user.can?(:admin_issue, to_project) end !moved? && persisted? && user.can?(:admin_issue, self.project) end def to_branch_name if self.confidential? "#{iid}-confidential-issue" else "#{iid}-#{title.parameterize}" end end def can_be_worked_on?(current_user) !self.closed? && !self.project.forked? && self.related_branches(current_user).empty? && self.closed_by_merge_requests(current_user).empty? end def overdue? due_date.try(:past?) || false end end