Commit 0d5d80b7 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'comment-updated-by' into 'master'

Show who last edited a comment if it wasn't the original author

Fixes #1796.

The `updated_by` user is also tracked for issues and merge requests, but it isn't currently shown, because "edited by Person" would give the idea that Person edited the description, while most updates to those are assignee/milestone/labels, in which case who made that change is already visible in the comments.

See merge request !1075
parents 9b6c513b ad55f0d6
...@@ -29,6 +29,7 @@ v 7.14.0 (unreleased) ...@@ -29,6 +29,7 @@ v 7.14.0 (unreleased)
- Add project star and fork count, group avatar URL and user/group web URL attributes to API - Add project star and fork count, group avatar URL and user/group web URL attributes to API
- Fix bug causing Bitbucket importer to crash when OAuth application had been removed. - Fix bug causing Bitbucket importer to crash when OAuth application had been removed.
- Add fetch command to the MR page. - Add fetch command to the MR page.
- Show who last edited a comment if it wasn't the original author
- Add ability to manage user email addresses via the API. - Add ability to manage user email addresses via the API.
- Show buttons to add license, changelog and contribution guide if they're missing. - Show buttons to add license, changelog and contribution guide if they're missing.
- Tweak project page buttons. - Tweak project page buttons.
......
...@@ -30,13 +30,10 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -30,13 +30,10 @@ class Projects::NotesController < Projects::ApplicationController
end end
def update def update
if note.editable? @note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
note.update_attributes(note_params)
note.reset_events_cache
end
respond_to do |format| respond_to do |format|
format.json { render_note_json(note) } format.json { render_note_json(@note) }
format.html { redirect_to :back } format.html { redirect_to :back }
end end
end end
......
...@@ -43,21 +43,6 @@ module IssuesHelper ...@@ -43,21 +43,6 @@ module IssuesHelper
end end
end end
def issue_timestamp(issue)
# Shows the created at time and the updated at time if different
ts = time_ago_with_tooltip(issue.created_at, placement: 'bottom', html_class: 'note_created_ago')
if issue.updated_at != issue.created_at
ts << capture_haml do
haml_tag :span do
haml_concat '&middot;'
haml_concat icon('edit', title: 'edited')
haml_concat time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_edited_ago')
end
end
end
ts.html_safe
end
def bulk_update_milestone_options def bulk_update_milestone_options
options_for_select([['None (backlog)', -1]]) + options_for_select([['None (backlog)', -1]]) +
options_from_collection_for_select(project_active_milestones, 'id', options_from_collection_for_select(project_active_milestones, 'id',
......
...@@ -23,21 +23,6 @@ module NotesHelper ...@@ -23,21 +23,6 @@ module NotesHelper
end end
end end
def note_timestamp(note)
# Shows the created at time and the updated at time if different
ts = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note_created_ago')
if note.updated_at != note.created_at
ts << capture_haml do
haml_tag :span do
haml_concat '&middot;'
haml_concat icon('edit', title: 'edited')
haml_concat time_ago_with_tooltip(note.updated_at, placement: 'bottom', html_class: 'note_edited_ago')
end
end
end
ts.html_safe
end
def noteable_json(noteable) def noteable_json(noteable)
{ {
id: noteable.id, id: noteable.id,
......
...@@ -21,7 +21,7 @@ module ProjectsHelper ...@@ -21,7 +21,7 @@ module ProjectsHelper
end end
def link_to_member(project, author, opts = {}) def link_to_member(project, author, opts = {})
default_opts = { avatar: true, name: true, size: 16 } default_opts = { avatar: true, name: true, size: 16, author_class: 'author' }
opts = default_opts.merge(opts) opts = default_opts.merge(opts)
return "(deleted)" unless author return "(deleted)" unless author
...@@ -32,7 +32,7 @@ module ProjectsHelper ...@@ -32,7 +32,7 @@ module ProjectsHelper
author_html << image_tag(avatar_icon(author.try(:email), opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt:'') if opts[:avatar] author_html << image_tag(avatar_icon(author.try(:email), opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt:'') if opts[:avatar]
# Build name span tag # Build name span tag
author_html << content_tag(:span, sanitize(author.name), class: 'author') if opts[:name] author_html << content_tag(:span, sanitize(author.name), class: opts[:author_class]) if opts[:name]
author_html = author_html.html_safe author_html = author_html.html_safe
......
...@@ -12,6 +12,7 @@ module Issuable ...@@ -12,6 +12,7 @@ module Issuable
included do included do
belongs_to :author, class_name: "User" belongs_to :author, class_name: "User"
belongs_to :assignee, class_name: "User" belongs_to :assignee, class_name: "User"
belongs_to :updated_by, class_name: "User"
belongs_to :milestone belongs_to :milestone
has_many :notes, as: :noteable, dependent: :destroy has_many :notes, as: :noteable, dependent: :destroy
has_many :label_links, as: :target, dependent: :destroy has_many :label_links, as: :target, dependent: :destroy
......
...@@ -33,6 +33,7 @@ class Note < ActiveRecord::Base ...@@ -33,6 +33,7 @@ class Note < ActiveRecord::Base
belongs_to :project belongs_to :project
belongs_to :noteable, polymorphic: true belongs_to :noteable, polymorphic: true
belongs_to :author, class_name: "User" belongs_to :author, class_name: "User"
belongs_to :updated_by, class_name: "User"
delegate :name, to: :project, prefix: true delegate :name, to: :project, prefix: true
delegate :name, :email, to: :author, prefix: true delegate :name, :email, to: :author, prefix: true
......
...@@ -14,7 +14,7 @@ module Issues ...@@ -14,7 +14,7 @@ module Issues
filter_params filter_params
old_labels = issue.labels.to_a old_labels = issue.labels.to_a
if params.present? && issue.update_attributes(params) if params.present? && issue.update_attributes(params.merge(updated_by: current_user))
issue.reset_events_cache issue.reset_events_cache
if issue.labels != old_labels if issue.labels != old_labels
......
...@@ -24,7 +24,7 @@ module MergeRequests ...@@ -24,7 +24,7 @@ module MergeRequests
filter_params filter_params
old_labels = merge_request.labels.to_a old_labels = merge_request.labels.to_a
if params.present? && merge_request.update_attributes(params) if params.present? && merge_request.update_attributes(params.merge(updated_by: current_user))
merge_request.reset_events_cache merge_request.reset_events_cache
if merge_request.labels != old_labels if merge_request.labels != old_labels
......
module Notes module Notes
class UpdateService < BaseService class UpdateService < BaseService
def execute def execute(note)
note = project.notes.find(params[:note_id]) return note unless note.editable?
note.note = params[:note]
if note.save
notification_service.new_note(note)
# Skip system notes, like status changes and cross-references. note.update_attributes(params.merge(updated_by: current_user))
unless note.system
event_service.leave_note(note, note.author)
# Create a cross-reference note if this Note contains GFM that note.reset_events_cache
# names an issue, merge request, or commit.
note.references.each do |mentioned|
SystemNoteService.cross_reference(mentioned, note.noteable, note.author)
end
end
end
note note
end end
......
...@@ -9,7 +9,13 @@ ...@@ -9,7 +9,13 @@
Open Open
Issue ##{@issue.iid} Issue ##{@issue.iid}
%small.creator %small.creator
&middot; created by #{link_to_member(@project, @issue.author)} #{issue_timestamp(@issue)} &middot; created by #{link_to_member(@project, @issue.author)}
= time_ago_with_tooltip(@issue.created_at, placement: 'bottom', html_class: 'issue_created_ago')
- if @issue.updated_at != @issue.created_at
%span
&middot;
= icon('edit', title: 'edited')
= time_ago_with_tooltip(@issue.updated_at, placement: 'bottom', html_class: 'issue_edited_ago')
.pull-right .pull-right
- if can?(current_user, :create_issue, @project) - if can?(current_user, :create_issue, @project)
......
%h4.page-title %h4.page-title
.issue-box{ class: issue_box_class(@merge_request) } .issue-box{ class: issue_box_class(@merge_request) }
= @merge_request.state_human_name = @merge_request.state_human_name
= "Merge Request ##{@merge_request.iid}" Merge Request ##{@merge_request.iid}
%small.creator %small.creator
&middot; &middot;
created by #{link_to_member(@project, @merge_request.author)} #{time_ago_with_tooltip(@merge_request.created_at)} created by #{link_to_member(@project, @merge_request.author)}
= time_ago_with_tooltip(@merge_request.created_at)
- if @merge_request.updated_at != @merge_request.created_at
%span
&middot;
= icon('edit', title: 'edited')
= time_ago_with_tooltip(@merge_request.updated_at, placement: 'bottom')
.issue-btn-group.pull-right .issue-btn-group.pull-right
- if can?(current_user, :update_merge_request, @merge_request) - if can?(current_user, :update_merge_request, @merge_request)
......
...@@ -33,7 +33,14 @@ ...@@ -33,7 +33,14 @@
%span.note-last-update %span.note-last-update
= link_to "##{dom_id(note)}", name: dom_id(note), title: "Link here" do = link_to "##{dom_id(note)}", name: dom_id(note), title: "Link here" do
= note_timestamp(note) = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note_created_ago')
- if note.updated_at != note.created_at
%span
&middot;
= icon('edit', title: 'edited')
= time_ago_with_tooltip(note.updated_at, placement: 'bottom', html_class: 'note_edited_ago')
- if note.updated_by && note.updated_by != note.author
by #{link_to_member(note.project, note.updated_by, avatar: false, author_class: nil)}
- if note.superceded?(@notes) - if note.superceded?(@notes)
- if note.upvote? - if note.upvote?
......
class AddUpdatedByToIssuablesAndNotes < ActiveRecord::Migration
def change
add_column :notes, :updated_by_id, :integer
add_column :issues, :updated_by_id, :integer
add_column :merge_requests, :updated_by_id, :integer
end
end
...@@ -136,12 +136,13 @@ ActiveRecord::Schema.define(version: 20150806104937) do ...@@ -136,12 +136,13 @@ ActiveRecord::Schema.define(version: 20150806104937) do
t.integer "project_id" t.integer "project_id"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.integer "position", default: 0 t.integer "position", default: 0
t.string "branch_name" t.string "branch_name"
t.text "description" t.text "description"
t.integer "milestone_id" t.integer "milestone_id"
t.string "state" t.string "state"
t.integer "iid" t.integer "iid"
t.integer "updated_by_id"
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
...@@ -238,6 +239,7 @@ ActiveRecord::Schema.define(version: 20150806104937) do ...@@ -238,6 +239,7 @@ ActiveRecord::Schema.define(version: 20150806104937) do
t.text "description" t.text "description"
t.integer "position", default: 0 t.integer "position", default: 0
t.datetime "locked_at" t.datetime "locked_at"
t.integer "updated_by_id"
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
...@@ -297,6 +299,7 @@ ActiveRecord::Schema.define(version: 20150806104937) do ...@@ -297,6 +299,7 @@ ActiveRecord::Schema.define(version: 20150806104937) do
t.integer "noteable_id" t.integer "noteable_id"
t.boolean "system", default: false, null: false t.boolean "system", default: false, null: false
t.text "st_diff" t.text "st_diff"
t.integer "updated_by_id"
end end
add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree
......
...@@ -78,17 +78,15 @@ module API ...@@ -78,17 +78,15 @@ module API
put ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do put ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do
required_attributes! [:body] required_attributes! [:body]
authorize! :admin_note, user_project.notes.find(params[:note_id]) note = user_project.notes.find(params[:note_id])
authorize! :admin_note, note
opts = { opts = {
note: params[:body], note: params[:body]
note_id: params[:note_id],
noteable_type: noteables_str.classify,
noteable_id: params[noteable_id_str]
} }
@note = ::Notes::UpdateService.new(user_project, current_user, @note = ::Notes::UpdateService.new(user_project, current_user, opts).execute(note)
opts).execute
if @note.valid? if @note.valid?
present @note, with: Entities::Note present @note, with: Entities::Note
......
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