Commit 1644117a1ac45bd7d250e7bced929a00a3befe5e

Authored by Andrew8xx8
1 parent 0b512af8

Issue uses StateMachine now

app/helpers/issues_helper.rb
... ... @@ -6,7 +6,7 @@ module IssuesHelper
6 6  
7 7 def issue_css_classes issue
8 8 classes = "issue"
9   - classes << " closed" if issue.closed
  9 + classes << " closed" if issue.closed?
10 10 classes << " today" if issue.today?
11 11 classes
12 12 end
... ...
app/models/issue.rb
... ... @@ -9,7 +9,7 @@
9 9 # project_id :integer
10 10 # created_at :datetime not null
11 11 # updated_at :datetime not null
12   -# closed :boolean default(FALSE), not null
  12 +# state :string default(FALSE), not null
13 13 # position :integer default(0)
14 14 # branch_name :string(255)
15 15 # description :text
... ... @@ -19,11 +19,29 @@
19 19 class Issue < ActiveRecord::Base
20 20 include Issuable
21 21  
22   - attr_accessible :title, :assignee_id, :closed, :position, :description,
23   - :milestone_id, :label_list, :author_id_of_changes
  22 + attr_accessible :title, :assignee_id, :position, :description,
  23 + :milestone_id, :label_list, :author_id_of_changes,
  24 + :state_event
24 25  
25 26 acts_as_taggable_on :labels
26 27  
  28 + state_machine :state, :initial => :opened do
  29 + event :close do
  30 + transition [:reopened, :opened] => :closed
  31 + end
  32 +
  33 + event :reopen do
  34 + transition :closed => :reopened
  35 + end
  36 +
  37 + state :opened
  38 +
  39 + state :reopened
  40 +
  41 + state :closed
  42 + end
  43 +
  44 +
27 45 def self.open_for(user)
28 46 opened.assigned(user)
29 47 end
... ...
app/models/project.rb
... ... @@ -43,7 +43,7 @@ class Project &lt; ActiveRecord::Base
43 43  
44 44 has_many :events, dependent: :destroy
45 45 has_many :merge_requests, dependent: :destroy
46   - has_many :issues, dependent: :destroy, order: "closed, created_at DESC"
  46 + has_many :issues, dependent: :destroy, order: "state, created_at DESC"
47 47 has_many :milestones, dependent: :destroy
48 48 has_many :users_projects, dependent: :destroy
49 49 has_many :notes, dependent: :destroy
... ...
app/observers/issue_observer.rb
... ... @@ -7,22 +7,31 @@ class IssueObserver &lt; ActiveRecord::Observer
7 7 end
8 8 end
9 9  
10   - def after_update(issue)
  10 + def after_close(issue, transition)
11 11 send_reassigned_email(issue) if issue.is_being_reassigned?
12 12  
13   - status = nil
14   - status = 'closed' if issue.is_being_closed?
15   - status = 'reopened' if issue.is_being_reopened?
16   - if status
17   - Note.create_status_change_note(issue, current_user, status)
18   - [issue.author, issue.assignee].compact.each do |recipient|
19   - Notify.delay.issue_status_changed_email(recipient.id, issue.id, status, current_user.id)
20   - end
21   - end
  13 + create_note(issue)
  14 + end
  15 +
  16 + def after_reopen(issue, transition)
  17 + send_reassigned_email(issue) if issue.is_being_reassigned?
  18 +
  19 + create_note(issue)
  20 + end
  21 +
  22 + def after_update(issue)
  23 + send_reassigned_email(issue) if issue.is_being_reassigned?
22 24 end
23 25  
24 26 protected
25 27  
  28 + def create_note(issue)
  29 + Note.create_status_change_note(issue, current_user, issue.state)
  30 + [issue.author, issue.assignee].compact.each do |recipient|
  31 + Notify.delay.issue_status_changed_email(recipient.id, issue.id, issue.state, current_user.id)
  32 + end
  33 + end
  34 +
26 35 def send_reassigned_email(issue)
27 36 recipient_ids = [issue.assignee_id, issue.assignee_id_was].keep_if {|id| id && id != current_user.id }
28 37  
... ...
app/views/issues/_show.html.haml
... ... @@ -8,10 +8,10 @@
8 8 %i.icon-comment
9 9 = issue.notes.count
10 10 - if can? current_user, :modify_issue, issue
11   - - if issue.closed
12   - = link_to 'Reopen', project_issue_path(issue.project, issue, issue: {closed: false }, status_only: true), method: :put, class: "btn btn-small grouped reopen_issue", remote: true
  11 + - if issue.closed?
  12 + = link_to 'Reopen', project_issue_path(issue.project, issue, issue: {state: :reopened }, status_only: true), method: :put, class: "btn btn-small grouped reopen_issue", remote: true
13 13 - else
14   - = link_to 'Close', project_issue_path(issue.project, issue, issue: {closed: true }, status_only: true), method: :put, class: "btn btn-small grouped close_issue", remote: true
  14 + = link_to 'Close', project_issue_path(issue.project, issue, issue: {state: :closed }, status_only: true), method: :put, class: "btn btn-small grouped close_issue", remote: true
15 15 = link_to edit_project_issue_path(issue.project, issue), class: "btn btn-small edit-issue-link grouped" do
16 16 %i.icon-edit
17 17 Edit
... ...
app/views/issues/show.html.haml
... ... @@ -7,10 +7,10 @@
7 7  
8 8 %span.pull-right
9 9 - if can?(current_user, :admin_project, @project) || @issue.author == current_user
10   - - if @issue.closed
11   - = link_to 'Reopen', project_issue_path(@project, @issue, issue: {closed: false }, status_only: true), method: :put, class: "btn grouped reopen_issue"
  10 + - if @issue.closed?
  11 + = link_to 'Reopen', project_issue_path(@project, @issue, issue: {state: :reopened }, status_only: true), method: :put, class: "btn grouped reopen_issue"
12 12 - else
13   - = link_to 'Close', project_issue_path(@project, @issue, issue: {closed: true }, status_only: true), method: :put, class: "btn grouped close_issue", title: "Close Issue"
  13 + = link_to 'Close', project_issue_path(@project, @issue, issue: {state: :closed }, status_only: true), method: :put, class: "btn grouped close_issue", title: "Close Issue"
14 14 - if can?(current_user, :admin_project, @project) || @issue.author == current_user
15 15 = link_to edit_project_issue_path(@project, @issue), class: "btn grouped" do
16 16 %i.icon-edit
... ... @@ -27,7 +27,7 @@
27 27 .ui-box.ui-box-show
28 28 .ui-box-head
29 29 %h4.box-title
30   - - if @issue.closed
  30 + - if @issue.closed?
31 31 .error.status_info Closed
32 32 = gfm escape_once(@issue.title)
33 33  
... ...
lib/api/entities.rb
... ... @@ -35,12 +35,11 @@ module Gitlab
35 35 class Group < Grape::Entity
36 36 expose :id, :name, :path, :owner_id
37 37 end
38   -
  38 +
39 39 class GroupDetail < Group
40 40 expose :projects, using: Entities::Project
41 41 end
42 42  
43   -
44 43 class RepoObject < Grape::Entity
45 44 expose :name, :commit
46 45 expose :protected do |repo, options|
... ... @@ -63,7 +62,7 @@ module Gitlab
63 62 class Milestone < Grape::Entity
64 63 expose :id
65 64 expose (:project_id) {|milestone| milestone.project.id}
66   - expose :title, :description, :due_date, :closed, :updated_at, :created_at
  65 + expose :title, :description, :due_date, :state, :updated_at, :created_at
67 66 end
68 67  
69 68 class Issue < Grape::Entity
... ... @@ -73,7 +72,7 @@ module Gitlab
73 72 expose :label_list, as: :labels
74 73 expose :milestone, using: Entities::Milestone
75 74 expose :assignee, :author, using: Entities::UserBasic
76   - expose :closed, :updated_at, :created_at
  75 + expose :state, :updated_at, :created_at
77 76 end
78 77  
79 78 class SSHKey < Grape::Entity
... ...
lib/api/issues.rb
... ... @@ -69,14 +69,14 @@ module Gitlab
69 69 # assignee_id (optional) - The ID of a user to assign issue
70 70 # milestone_id (optional) - The ID of a milestone to assign issue
71 71 # labels (optional) - The labels of an issue
72   - # closed (optional) - The state of an issue (0 = false, 1 = true)
  72 + # state (optional) - The state of an issue (close|reopen)
73 73 # Example Request:
74 74 # PUT /projects/:id/issues/:issue_id
75 75 put ":id/issues/:issue_id" do
76 76 @issue = user_project.issues.find(params[:issue_id])
77 77 authorize! :modify_issue, @issue
78 78  
79   - attrs = attributes_for_keys [:title, :description, :assignee_id, :milestone_id, :closed]
  79 + attrs = attributes_for_keys [:title, :description, :assignee_id, :milestone_id, :state_event]
80 80 attrs[:label_list] = params[:labels] if params[:labels].present?
81 81 IssueObserver.current_user = current_user
82 82 if @issue.update_attributes attrs
... ...
spec/factories.rb
... ... @@ -54,10 +54,15 @@ FactoryGirl.define do
54 54 project
55 55  
56 56 trait :closed do
57   - closed true
  57 + state :closed
  58 + end
  59 +
  60 + trait :reopened do
  61 + state :reopened
58 62 end
59 63  
60 64 factory :closed_issue, traits: [:closed]
  65 + factory :reopened_issue, traits: [:reopened]
61 66 end
62 67  
63 68 factory :merge_request do
... ...
spec/models/issue_spec.rb
... ... @@ -9,7 +9,7 @@
9 9 # project_id :integer
10 10 # created_at :datetime not null
11 11 # updated_at :datetime not null
12   -# closed :boolean default(FALSE), not null
  12 +# state :string default(FALSE), not null
13 13 # position :integer default(0)
14 14 # branch_name :string(255)
15 15 # description :text
... ...
spec/observers/issue_observer_spec.rb
1 1 require 'spec_helper'
2 2  
3 3 describe IssueObserver do
4   - let(:some_user) { double(:user, id: 1) }
5   - let(:assignee) { double(:user, id: 2) }
6   - let(:author) { double(:user, id: 3) }
7   - let(:issue) { double(:issue, id: 42, assignee: assignee, author: author) }
  4 + let(:some_user) { create :user }
  5 + let(:assignee) { create :user }
  6 + let(:author) { create :user }
  7 + let(:mock_issue) { double(:issue, id: 42, assignee: assignee, author: author) }
  8 + let(:assigned_issue) { create(:issue, assignee: assignee, author: author) }
  9 + let(:unassigned_issue) { create(:issue, author: author) }
  10 + let(:closed_assigned_issue) { create(:closed_issue, assignee: assignee, author: author) }
  11 + let(:closed_unassigned_issue) { create(:closed_issue, author: author) }
  12 +
8 13  
9 14 before(:each) { subject.stub(:current_user).and_return(some_user) }
10 15  
... ... @@ -21,137 +26,91 @@ describe IssueObserver do
21 26 end
22 27  
23 28 it 'sends an email to the assignee' do
24   - Notify.should_receive(:new_issue_email).with(issue.id)
  29 + Notify.should_receive(:new_issue_email).with(mock_issue.id)
25 30  
26   - subject.after_create(issue)
  31 + subject.after_create(mock_issue)
27 32 end
28 33  
29 34 it 'does not send an email to the assignee if assignee created the issue' do
30 35 subject.stub(:current_user).and_return(assignee)
31 36 Notify.should_not_receive(:new_issue_email)
32 37  
33   - subject.after_create(issue)
  38 + subject.after_create(mock_issue)
34 39 end
35 40 end
36 41  
37   - context '#after_update' do
38   - before(:each) do
39   - issue.stub(:is_being_reassigned?).and_return(false)
40   - issue.stub(:is_being_closed?).and_return(false)
41   - issue.stub(:is_being_reopened?).and_return(false)
42   - end
43   -
44   - it 'is called when an issue is changed' do
45   - changed = create(:issue, project: create(:project))
46   - subject.should_receive(:after_update)
47   -
48   - Issue.observers.enable :issue_observer do
49   - changed.description = 'I changed'
50   - changed.save
51   - end
52   - end
53   -
54   - context 'a reassigned email' do
55   - it 'is sent if the issue is being reassigned' do
56   - issue.should_receive(:is_being_reassigned?).and_return(true)
57   - subject.should_receive(:send_reassigned_email).with(issue)
58   -
59   - subject.after_update(issue)
60   - end
61   -
62   - it 'is not sent if the issue is not being reassigned' do
63   - issue.should_receive(:is_being_reassigned?).and_return(false)
64   - subject.should_not_receive(:send_reassigned_email)
65   -
66   - subject.after_update(issue)
67   - end
68   - end
69   -
  42 + context '#after_close' do
70 43 context 'a status "closed"' do
71 44 it 'note is created if the issue is being closed' do
72   - issue.should_receive(:is_being_closed?).and_return(true)
73   - Notify.should_receive(:issue_status_changed_email).twice
74   - Note.should_receive(:create_status_change_note).with(issue, some_user, 'closed')
75   -
76   - subject.after_update(issue)
77   - end
78   -
79   - it 'note is not created if the issue is not being closed' do
80   - issue.should_receive(:is_being_closed?).and_return(false)
81   - Note.should_not_receive(:create_status_change_note).with(issue, some_user, 'closed')
  45 + Note.should_receive(:create_status_change_note).with(assigned_issue, some_user, 'closed')
82 46  
83   - subject.after_update(issue)
  47 + assigned_issue.close
84 48 end
85 49  
86 50 it 'notification is delivered if the issue being closed' do
87   - issue.stub(:is_being_closed?).and_return(true)
88 51 Notify.should_receive(:issue_status_changed_email).twice
89   - Note.should_receive(:create_status_change_note).with(issue, some_user, 'closed')
90 52  
91   - subject.after_update(issue)
92   - end
93   -
94   - it 'notification is not delivered if the issue not being closed' do
95   - issue.stub(:is_being_closed?).and_return(false)
96   - Notify.should_not_receive(:issue_status_changed_email)
97   - Note.should_not_receive(:create_status_change_note).with(issue, some_user, 'closed')
98   -
99   - subject.after_update(issue)
  53 + assigned_issue.close
100 54 end
101 55  
102 56 it 'notification is delivered only to author if the issue being closed' do
103   - issue_without_assignee = double(:issue, id: 42, author: author, assignee: nil)
104   - issue_without_assignee.stub(:is_being_reassigned?).and_return(false)
105   - issue_without_assignee.stub(:is_being_closed?).and_return(true)
106   - issue_without_assignee.stub(:is_being_reopened?).and_return(false)
107 57 Notify.should_receive(:issue_status_changed_email).once
108   - Note.should_receive(:create_status_change_note).with(issue_without_assignee, some_user, 'closed')
  58 + Note.should_receive(:create_status_change_note).with(unassigned_issue, some_user, 'closed')
109 59  
110   - subject.after_update(issue_without_assignee)
  60 + unassigned_issue.close
111 61 end
112 62 end
113 63  
114 64 context 'a status "reopened"' do
115 65 it 'note is created if the issue is being reopened' do
  66 + Note.should_receive(:create_status_change_note).with(closed_assigned_issue, some_user, 'reopened')
  67 +
  68 + closed_assigned_issue.reopen
  69 + end
  70 +
  71 + it 'notification is delivered if the issue being reopened' do
116 72 Notify.should_receive(:issue_status_changed_email).twice
117   - issue.should_receive(:is_being_reopened?).and_return(true)
118   - Note.should_receive(:create_status_change_note).with(issue, some_user, 'reopened')
119 73  
120   - subject.after_update(issue)
  74 + closed_assigned_issue.reopen
121 75 end
122 76  
123   - it 'note is not created if the issue is not being reopened' do
124   - issue.should_receive(:is_being_reopened?).and_return(false)
125   - Note.should_not_receive(:create_status_change_note).with(issue, some_user, 'reopened')
  77 + it 'notification is delivered only to author if the issue being reopened' do
  78 + Notify.should_receive(:issue_status_changed_email).once
  79 + Note.should_receive(:create_status_change_note).with(closed_unassigned_issue, some_user, 'reopened')
126 80  
127   - subject.after_update(issue)
  81 + closed_unassigned_issue.reopen
128 82 end
  83 + end
  84 + end
129 85  
130   - it 'notification is delivered if the issue being reopened' do
131   - issue.stub(:is_being_reopened?).and_return(true)
132   - Notify.should_receive(:issue_status_changed_email).twice
133   - Note.should_receive(:create_status_change_note).with(issue, some_user, 'reopened')
  86 + context '#after_update' do
  87 + before(:each) do
  88 + mock_issue.stub(:is_being_reassigned?).and_return(false)
  89 + end
  90 +
  91 + it 'is called when an issue is changed' do
  92 + changed = create(:issue, project: create(:project))
  93 + subject.should_receive(:after_update)
134 94  
135   - subject.after_update(issue)
  95 + Issue.observers.enable :issue_observer do
  96 + changed.description = 'I changed'
  97 + changed.save
136 98 end
  99 + end
137 100  
138   - it 'notification is not delivered if the issue not being reopened' do
139   - issue.stub(:is_being_reopened?).and_return(false)
140   - Notify.should_not_receive(:issue_status_changed_email)
141   - Note.should_not_receive(:create_status_change_note).with(issue, some_user, 'reopened')
  101 + context 'a reassigned email' do
  102 + it 'is sent if the issue is being reassigned' do
  103 + mock_issue.should_receive(:is_being_reassigned?).and_return(true)
  104 + subject.should_receive(:send_reassigned_email).with(mock_issue)
142 105  
143   - subject.after_update(issue)
  106 + subject.after_update(mock_issue)
144 107 end
145 108  
146   - it 'notification is delivered only to author if the issue being reopened' do
147   - issue_without_assignee = double(:issue, id: 42, author: author, assignee: nil)
148   - issue_without_assignee.stub(:is_being_reassigned?).and_return(false)
149   - issue_without_assignee.stub(:is_being_closed?).and_return(false)
150   - issue_without_assignee.stub(:is_being_reopened?).and_return(true)
151   - Notify.should_receive(:issue_status_changed_email).once
152   - Note.should_receive(:create_status_change_note).with(issue_without_assignee, some_user, 'reopened')
  109 + it 'is not sent if the issue is not being reassigned' do
  110 + mock_issue.should_receive(:is_being_reassigned?).and_return(false)
  111 + subject.should_not_receive(:send_reassigned_email)
153 112  
154   - subject.after_update(issue_without_assignee)
  113 + subject.after_update(mock_issue)
155 114 end
156 115 end
157 116 end
... ... @@ -160,23 +119,23 @@ describe IssueObserver do
160 119 let(:previous_assignee) { double(:user, id: 3) }
161 120  
162 121 before(:each) do
163   - issue.stub(:assignee_id).and_return(assignee.id)
164   - issue.stub(:assignee_id_was).and_return(previous_assignee.id)
  122 + mock_issue.stub(:assignee_id).and_return(assignee.id)
  123 + mock_issue.stub(:assignee_id_was).and_return(previous_assignee.id)
165 124 end
166 125  
167 126 def it_sends_a_reassigned_email_to(recipient)
168   - Notify.should_receive(:reassigned_issue_email).with(recipient, issue.id, previous_assignee.id)
  127 + Notify.should_receive(:reassigned_issue_email).with(recipient, mock_issue.id, previous_assignee.id)
169 128 end
170 129  
171 130 def it_does_not_send_a_reassigned_email_to(recipient)
172   - Notify.should_not_receive(:reassigned_issue_email).with(recipient, issue.id, previous_assignee.id)
  131 + Notify.should_not_receive(:reassigned_issue_email).with(recipient, mock_issue.id, previous_assignee.id)
173 132 end
174 133  
175 134 it 'sends a reassigned email to the previous and current assignees' do
176 135 it_sends_a_reassigned_email_to assignee.id
177 136 it_sends_a_reassigned_email_to previous_assignee.id
178 137  
179   - subject.send(:send_reassigned_email, issue)
  138 + subject.send(:send_reassigned_email, mock_issue)
180 139 end
181 140  
182 141 context 'does not send an email to the user who made the reassignment' do
... ... @@ -185,14 +144,14 @@ describe IssueObserver do
185 144 it_sends_a_reassigned_email_to previous_assignee.id
186 145 it_does_not_send_a_reassigned_email_to assignee.id
187 146  
188   - subject.send(:send_reassigned_email, issue)
  147 + subject.send(:send_reassigned_email, mock_issue)
189 148 end
190 149 it 'if the user is the previous assignee' do
191 150 subject.stub(:current_user).and_return(previous_assignee)
192 151 it_sends_a_reassigned_email_to assignee.id
193 152 it_does_not_send_a_reassigned_email_to previous_assignee.id
194 153  
195   - subject.send(:send_reassigned_email, issue)
  154 + subject.send(:send_reassigned_email, mock_issue)
196 155 end
197 156 end
198 157 end
... ...
spec/requests/api/issues_spec.rb
... ... @@ -54,14 +54,24 @@ describe Gitlab::API do
54 54 end
55 55 end
56 56  
57   - describe "PUT /projects/:id/issues/:issue_id" do
  57 + describe "PUT /projects/:id/issues/:issue_id to update only title" do
58 58 it "should update a project issue" do
59 59 put api("/projects/#{project.id}/issues/#{issue.id}", user),
60   - title: 'updated title', labels: 'label2', closed: 1
  60 + title: 'updated title'
61 61 response.status.should == 200
  62 +
62 63 json_response['title'].should == 'updated title'
  64 + end
  65 + end
  66 +
  67 + describe "PUT /projects/:id/issues/:issue_id to update state and label" do
  68 + it "should update a project issue" do
  69 + put api("/projects/#{project.id}/issues/#{issue.id}", user),
  70 + labels: 'label2', state_event: "close"
  71 + response.status.should == 200
  72 +
63 73 json_response['labels'].should == ['label2']
64   - json_response['closed'].should be_true
  74 + json_response['state'].should eq "closed"
65 75 end
66 76 end
67 77  
... ...
spec/requests/issues_spec.rb
... ... @@ -58,8 +58,7 @@ describe &quot;Issues&quot; do
58 58  
59 59 it "should be able to search on different statuses" do
60 60 issue = Issue.first # with title 'foobar'
61   - issue.closed = true
62   - issue.save
  61 + issue.close
63 62  
64 63 visit project_issues_path(project)
65 64 click_link 'Closed'
... ...