Commit 997def72 authored by Bryce Johnson's avatar Bryce Johnson

Merge branch 'time-tracking-integration' of gitlab.com:gitlab-org/gitlab-ee...

Merge branch 'time-tracking-integration' of gitlab.com:gitlab-org/gitlab-ee into time-tracking-integration
parents 1c98d7cc c4e0a7d1
...@@ -75,7 +75,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -75,7 +75,7 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
render json: @issue.to_json(include: [:milestone, :labels]) render json: @issue.to_json(include: [:milestone, :labels], methods: [:total_time_spent, :human_total_time_spent, :human_time_estimate])
end end
end end
end end
......
...@@ -146,21 +146,27 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -146,21 +146,27 @@ class Projects::NotesController < Projects::ApplicationController
end end
def note_json(note) def note_json(note)
attrs = {
award: false,
id: note.id,
commands_changes: note.commands_changes
}
if note.is_a?(AwardEmoji) if note.is_a?(AwardEmoji)
attrs = { attrs.merge!(
valid: note.valid?, valid: note.valid?,
award: true, award: true,
name: note.name name: note.name
} )
elsif note.persisted? elsif note.persisted?
Banzai::NoteRenderer.render([note], @project, current_user) Banzai::NoteRenderer.render([note], @project, current_user)
attrs = { attrs.merge!(
valid: true, valid: true,
discussion_id: note.discussion_id, discussion_id: note.discussion_id,
html: note_html(note), html: note_html(note),
note: note.note note: note.note
} )
if note.diff_note? if note.diff_note?
discussion = note.to_discussion discussion = note.to_discussion
...@@ -186,16 +192,12 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -186,16 +192,12 @@ class Projects::NotesController < Projects::ApplicationController
end end
end end
else else
attrs = { attrs.merge!(
valid: false, valid: false,
errors: note.errors errors: note.errors
} )
end end
attrs[:award] ||= false
attrs[:id] = note.id
attrs[:commands_changes] = note.commands_changes
attrs attrs
end end
......
...@@ -13,23 +13,51 @@ module TimeTrackable ...@@ -13,23 +13,51 @@ module TimeTrackable
alias_method :time_spent?, :time_spent alias_method :time_spent?, :time_spent
default_value_for :time_estimate, value: 0, allows_nil: false
has_many :timelogs, as: :trackable, dependent: :destroy has_many :timelogs, as: :trackable, dependent: :destroy
end end
def spend_time=(seconds:, user:) def spend_time(seconds, user)
# Exit if time to subtract exceeds the total time spent. return if seconds == 0
return if seconds < 0 && (seconds.abs > total_time_spent)
# When seconds = 0 we reset the total time spent by creating a new Timelog @time_spent = seconds
# record with a negative value that is equal to the current total time spent. @time_spent_user = user
new_time_spent = seconds.zero? ? (total_time_spent * -1) : seconds
timelogs.new(user: user, time_spent: new_time_spent) if seconds == :reset
reset_spent_time
else
add_or_susbtract_spent_time
end
end
@time_spent = seconds def spend_time!(seconds, user)
spend_time(seconds, user)
save!
end end
def total_time_spent def total_time_spent
timelogs.sum(:time_spent) timelogs.sum(:time_spent)
end end
def human_total_time_spent
ChronicDuration.output(total_time_spent, format: :short)
end
def human_time_estimate
ChronicDuration.output(time_estimate, format: :short)
end
private
def reset_spent_time
timelogs.new(time_spent: total_time_spent * -1, user: @time_spent_user)
end
def add_or_susbtract_spent_time
# Exit if time to subtract exceeds the total time spent.
return if time_spent < 0 && (time_spent.abs > total_time_spent)
timelogs.new(time_spent: time_spent, user: @time_spent_user)
end
end end
...@@ -145,6 +145,7 @@ class IssuableBaseService < BaseService ...@@ -145,6 +145,7 @@ class IssuableBaseService < BaseService
def create(issuable) def create(issuable)
merge_slash_commands_into_params!(issuable) merge_slash_commands_into_params!(issuable)
filter_params filter_params
change_time_spent(issuable)
params.delete(:state_event) params.delete(:state_event)
params[:author] ||= current_user params[:author] ||= current_user
...@@ -185,6 +186,7 @@ class IssuableBaseService < BaseService ...@@ -185,6 +186,7 @@ class IssuableBaseService < BaseService
change_state(issuable) change_state(issuable)
change_subscription(issuable) change_subscription(issuable)
change_todo(issuable) change_todo(issuable)
change_time_spent(issuable)
filter_params filter_params
old_labels = issuable.labels.to_a old_labels = issuable.labels.to_a
old_mentioned_users = issuable.mentioned_users.to_a old_mentioned_users = issuable.mentioned_users.to_a
...@@ -236,6 +238,12 @@ class IssuableBaseService < BaseService ...@@ -236,6 +238,12 @@ class IssuableBaseService < BaseService
end end
end end
def change_time_spent(issuable)
if params[:spend_time]
issuable.spend_time(params.delete(:spend_time), current_user)
end
end
def has_changes?(issuable, old_labels: []) def has_changes?(issuable, old_labels: [])
valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch] valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch]
......
...@@ -254,10 +254,10 @@ module SlashCommands ...@@ -254,10 +254,10 @@ module SlashCommands
current_user.can?(:"admin_#{issuable.to_ability_name}", project) current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end end
command :estimate do |raw_duration| command :estimate do |raw_duration|
time_spent = ChronicDuration.parse(raw_duration, default_unit: 'hours') rescue nil time_estimate = ChronicDuration.parse(raw_duration, default_unit: 'hours') rescue nil
if time_spent if time_estimate
@updates[:time_estimate] = time_spent @updates[:time_estimate] = time_estimate
end end
end end
...@@ -269,31 +269,29 @@ module SlashCommands ...@@ -269,31 +269,29 @@ module SlashCommands
command :spend do |raw_duration| command :spend do |raw_duration|
reduce_time = raw_duration.sub!(/\A-/, '') reduce_time = raw_duration.sub!(/\A-/, '')
time_spent = ChronicDuration.parse(raw_duration, default_unit: 'hours') rescue nil time_spent = ChronicDuration.parse(raw_duration, default_unit: 'hours') rescue nil
time_spent *= -1 if time_spent && reduce_time
if time_spent if time_spent
@updates[:spend_time] = { @updates[:spend_time] = time_spent
seconds: reduce_time ? (time_spent * -1) : time_spent,
user: current_user
}
end end
end end
desc 'Remove the estimated time' desc 'Remove time estimate'
condition do condition do
issuable.persisted? && issuable.persisted? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", project) current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end end
command :remove_estimation do command :remove_estimate do
@updates[:time_estimate] = 0 @updates[:time_estimate] = 0
end end
desc 'Remove the time spent' desc 'Remove spent time'
condition do condition do
issuable.persisted? && issuable.persisted? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", project) current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end end
command :remove_time_spent do command :remove_time_spent do
@updates[:spend_time] = { seconds: 0, user: current_user } @updates[:spend_time] = :reset
end end
def find_label_ids(labels_param) def find_label_ids(labels_param)
......
...@@ -151,7 +151,7 @@ module SystemNoteService ...@@ -151,7 +151,7 @@ module SystemNoteService
def change_time_spent(noteable, project, author) def change_time_spent(noteable, project, author)
time_spent = noteable.time_spent time_spent = noteable.time_spent
if time_spent.zero? if time_spent == :reset
body = "Removed time spent on this #{noteable.human_class_name}" body = "Removed time spent on this #{noteable.human_class_name}"
else else
parsed_time = ChronicDuration.output(time_spent.abs, format: :short) parsed_time = ChronicDuration.output(time_spent.abs, format: :short)
......
...@@ -24,8 +24,8 @@ class AddEstimateToIssuables < ActiveRecord::Migration ...@@ -24,8 +24,8 @@ class AddEstimateToIssuables < ActiveRecord::Migration
# disable_ddl_transaction! # disable_ddl_transaction!
def up def up
add_column_with_default :issues, :time_estimate, :integer, default: 0 add_column :issues, :time_estimate, :integer
add_column_with_default :merge_requests, :time_estimate, :integer, default: 0 add_column :merge_requests, :time_estimate, :integer
end end
def down def down
......
...@@ -563,7 +563,7 @@ ActiveRecord::Schema.define(version: 20161109150329) do ...@@ -563,7 +563,7 @@ ActiveRecord::Schema.define(version: 20161109150329) do
t.integer "lock_version" t.integer "lock_version"
t.text "title_html" t.text "title_html"
t.text "description_html" t.text "description_html"
t.integer "time_estimate", default: 0 t.integer "time_estimate"
end end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
...@@ -760,7 +760,7 @@ ActiveRecord::Schema.define(version: 20161109150329) do ...@@ -760,7 +760,7 @@ ActiveRecord::Schema.define(version: 20161109150329) do
t.integer "lock_version" t.integer "lock_version"
t.text "title_html" t.text "title_html"
t.text "description_html" t.text "description_html"
t.integer "time_estimate", default: 0 t.integer "time_estimate"
end end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
...@@ -825,9 +825,9 @@ ActiveRecord::Schema.define(version: 20161109150329) do ...@@ -825,9 +825,9 @@ ActiveRecord::Schema.define(version: 20161109150329) do
t.datetime "ldap_sync_last_successful_update_at" t.datetime "ldap_sync_last_successful_update_at"
t.datetime "ldap_sync_last_sync_at" t.datetime "ldap_sync_last_sync_at"
t.datetime "deleted_at" t.datetime "deleted_at"
t.text "description_html"
t.boolean "lfs_enabled" t.boolean "lfs_enabled"
t.integer "repository_size_limit" t.integer "repository_size_limit"
t.text "description_html"
end end
add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
...@@ -1010,6 +1010,7 @@ ActiveRecord::Schema.define(version: 20161109150329) do ...@@ -1010,6 +1010,7 @@ ActiveRecord::Schema.define(version: 20161109150329) do
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.integer "creator_id" t.integer "creator_id"
t.boolean "wall_enabled", default: true, null: false
t.integer "namespace_id" t.integer "namespace_id"
t.datetime "last_activity_at" t.datetime "last_activity_at"
t.string "import_url" t.string "import_url"
......
...@@ -30,6 +30,6 @@ do. ...@@ -30,6 +30,6 @@ do.
| `/remove_due_date` | Remove due date | | `/remove_due_date` | Remove due date |
| `/wip` | Toggle the Work In Progress status | | `/wip` | Toggle the Work In Progress status |
| <code>/estimate &lt;1w 3d 2h 14m&gt;</code> | Set time estimate | | <code>/estimate &lt;1w 3d 2h 14m&gt;</code> | Set time estimate |
| `/remove_estimation` | Remove estimated time | | `/remove_estimate` | Remove estimated time |
| <code>/spend &lt;1h 30m &#124; -1h 5m&gt;</code> | Add or substract spent time | | <code>/spend &lt;1h 30m &#124; -1h 5m&gt;</code> | Add or substract spent time |
| `/remove_time_spent` | Remove time spent | | `/remove_time_spent` | Remove time spent |
...@@ -272,6 +272,20 @@ describe Projects::IssuesController do ...@@ -272,6 +272,20 @@ describe Projects::IssuesController do
end end
describe 'POST #create' do describe 'POST #create' do
def post_new_issue(attrs = {})
sign_in(user)
project = create(:empty_project, :public)
project.team << [user, :developer]
post :create, {
namespace_id: project.namespace.to_param,
project_id: project.to_param,
issue: { title: 'Title', description: 'Description' }.merge(attrs)
}
project.issues.first
end
context 'Akismet is enabled' do context 'Akismet is enabled' do
before do before do
allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true) allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
...@@ -279,13 +293,7 @@ describe Projects::IssuesController do ...@@ -279,13 +293,7 @@ describe Projects::IssuesController do
end end
def post_spam_issue def post_spam_issue
sign_in(user) post_new_issue(title: 'Spam Title', description: 'Spam lives here')
spam_project = create(:empty_project, :public)
post :create, {
namespace_id: spam_project.namespace.to_param,
project_id: spam_project.to_param,
issue: { title: 'Spam Title', description: 'Spam lives here' }
}
end end
it 'rejects an issue recognized as spam' do it 'rejects an issue recognized as spam' do
...@@ -306,18 +314,26 @@ describe Projects::IssuesController do ...@@ -306,18 +314,26 @@ describe Projects::IssuesController do
request.env['action_dispatch.remote_ip'] = '127.0.0.1' request.env['action_dispatch.remote_ip'] = '127.0.0.1'
end end
def post_new_issue it 'creates a user agent detail' do
expect{ post_new_issue }.to change(UserAgentDetail, :count).by(1)
end
end
context 'when description has slash commands' do
before do
sign_in(user) sign_in(user)
project = create(:empty_project, :public)
post :create, {
namespace_id: project.namespace.to_param,
project_id: project.to_param,
issue: { title: 'Title', description: 'Description' }
}
end end
it 'creates a user agent detail' do it 'can add spent time' do
expect{ post_new_issue }.to change(UserAgentDetail, :count).by(1) issue = post_new_issue(description: '/spend 1h')
expect(issue.total_time_spent).to eq(3600)
end
it 'can set the time estimate' do
issue = post_new_issue(description: '/estimate 2h')
expect(issue.time_estimate).to eq(7200)
end end
end end
end end
......
...@@ -391,7 +391,7 @@ describe Issue, "Issuable" do ...@@ -391,7 +391,7 @@ describe Issue, "Issuable" do
context 'adding time' do context 'adding time' do
it 'should update the total time spent' do it 'should update the total time spent' do
issue.update_attributes!(spend_time: { seconds: 1800, user: user }) issue.spend_time!(1800, user)
expect(issue.total_time_spent).to eq(1800) expect(issue.total_time_spent).to eq(1800)
end end
...@@ -399,18 +399,18 @@ describe Issue, "Issuable" do ...@@ -399,18 +399,18 @@ describe Issue, "Issuable" do
context 'substracting time' do context 'substracting time' do
before do before do
issue.update_attributes!(spend_time: { seconds: 1800, user: user }) issue.spend_time!(1800, user)
end end
it 'should update the total time spent' do it 'should update the total time spent' do
issue.update_attributes!(spend_time: { seconds: -900, user: user }) issue.spend_time!(-900, user)
expect(issue.total_time_spent).to eq(900) expect(issue.total_time_spent).to eq(900)
end end
context 'when time to substract exceeds the total time spent' do context 'when time to substract exceeds the total time spent' do
it 'should not alter the total time spent' do it 'should not alter the total time spent' do
issue.update_attributes!(spend_time: { seconds: -3600, user: user }) issue.spend_time!(-3600, user)
expect(issue.total_time_spent).to eq(1800) expect(issue.total_time_spent).to eq(1800)
end end
......
...@@ -222,7 +222,7 @@ describe SlashCommands::InterpretService, services: true do ...@@ -222,7 +222,7 @@ describe SlashCommands::InterpretService, services: true do
it 'populates spend_time: { seconds: 3600, user: user } if content contains /spend 1h' do it 'populates spend_time: { seconds: 3600, user: user } if content contains /spend 1h' do
_, updates = service.execute(content, issuable) _, updates = service.execute(content, issuable)
expect(updates).to eq(spend_time: { seconds: 3600, user: developer }) expect(updates).to eq(spend_time: 3600)
end end
end end
...@@ -230,12 +230,12 @@ describe SlashCommands::InterpretService, services: true do ...@@ -230,12 +230,12 @@ describe SlashCommands::InterpretService, services: true do
it 'populates spend_time: { seconds: -1800, user: user } if content contains /spend -30m' do it 'populates spend_time: { seconds: -1800, user: user } if content contains /spend -30m' do
_, updates = service.execute(content, issuable) _, updates = service.execute(content, issuable)
expect(updates).to eq(spend_time: { seconds: -1800, user: developer }) expect(updates).to eq(spend_time: -1800)
end end
end end
shared_examples 'remove_estimation command' do shared_examples 'remove_estimate command' do
it 'populates time_estimate: "0" if content contains /remove_estimation' do it 'populates time_estimate: "0" if content contains /remove_estimate' do
_, updates = service.execute(content, issuable) _, updates = service.execute(content, issuable)
expect(updates).to eq(time_estimate: 0) expect(updates).to eq(time_estimate: 0)
...@@ -243,10 +243,10 @@ describe SlashCommands::InterpretService, services: true do ...@@ -243,10 +243,10 @@ describe SlashCommands::InterpretService, services: true do
end end
shared_examples 'remove_time_spent command' do shared_examples 'remove_time_spent command' do
it 'populates spend_time: "0" if content contains /remove_time_spent' do it 'populates spend_time: ":reset" if content contains /remove_time_spent' do
_, updates = service.execute(content, issuable) _, updates = service.execute(content, issuable)
expect(updates).to eq(spend_time: { seconds: 0, user: developer }) expect(updates).to eq(spend_time: :reset)
end end
end end
...@@ -526,8 +526,8 @@ describe SlashCommands::InterpretService, services: true do ...@@ -526,8 +526,8 @@ describe SlashCommands::InterpretService, services: true do
let(:issuable) { issue } let(:issuable) { issue }
end end
it_behaves_like 'remove_estimation command' do it_behaves_like 'remove_estimate command' do
let(:content) { '/remove_estimation' } let(:content) { '/remove_estimate' }
let(:issuable) { issue } let(:issuable) { issue }
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment