summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJarka Kadlecova <jarka@gitlab.com>2017-11-01 18:35:14 +0100
committerJarka Kadlecova <jarka@gitlab.com>2017-11-02 07:14:35 +0100
commit064c8949bd2cbbd304909b84f4ddac3c80827b94 (patch)
tree2eda37a87768439b4485dd161b8aa0fe809ad923
parent101779e4e9fd83ef1890a7499ef9b2723fb75cbf (diff)
downloadgitlab-ce-jk-epic-changes-ce-port.tar.gz
CE port of code changed for epicsjk-epic-changes-ce-port
-rw-r--r--app/controllers/concerns/issuable_actions.rb72
-rw-r--r--app/controllers/projects/issues_controller.rb69
-rw-r--r--app/controllers/projects/merge_requests_controller.rb10
-rw-r--r--app/helpers/gitlab_routing_helper.rb6
-rw-r--r--app/helpers/issuables_helper.rb27
-rw-r--r--app/models/concerns/cache_markdown_field.rb3
-rw-r--r--app/models/concerns/issuable.rb3
-rw-r--r--app/models/epic.rb9
-rw-r--r--app/models/group.rb6
-rw-r--r--app/models/issue.rb3
-rw-r--r--app/models/merge_request.rb3
-rw-r--r--app/models/note.rb8
-rw-r--r--app/serializers/issuable_entity.rb8
-rw-r--r--app/serializers/issue_entity.rb4
-rw-r--r--app/serializers/merge_request_entity.rb4
-rw-r--r--app/serializers/time_trackable_entity.rb11
-rw-r--r--app/services/issuable/common_system_notes_service.rb81
-rw-r--r--app/services/issuable_base_service.rb93
-rw-r--r--app/services/issues/update_service.rb4
-rw-r--r--app/services/merge_requests/update_service.rb10
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml2
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb6
-rw-r--r--lib/banzai/filter/reference_filter.rb4
-rw-r--r--lib/banzai/filter/user_reference_filter.rb42
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb39
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb56
-rw-r--r--spec/helpers/gitlab_routing_helper_spec.rb26
-rw-r--r--spec/helpers/issuables_helper_spec.rb32
-rw-r--r--spec/lib/banzai/filter/issue_reference_filter_spec.rb62
-rw-r--r--spec/lib/banzai/filter/label_reference_filter_spec.rb12
-rw-r--r--spec/lib/banzai/filter/merge_request_reference_filter_spec.rb10
-rw-r--r--spec/lib/banzai/filter/milestone_reference_filter_spec.rb11
-rw-r--r--spec/lib/banzai/filter/snippet_reference_filter_spec.rb10
-rw-r--r--spec/lib/banzai/filter/user_reference_filter_spec.rb33
-rw-r--r--spec/serializers/issue_entity_spec.rb20
-rw-r--r--spec/serializers/merge_request_entity_spec.rb11
-rw-r--r--spec/services/issuable/common_system_notes_service_spec.rb49
37 files changed, 631 insertions, 228 deletions
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 4079072a930..b1ed973d178 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -7,6 +7,54 @@ module IssuableActions
before_action :authorize_admin_issuable!, only: :bulk_update
end
+ def show
+ respond_to do |format|
+ format.html do
+ render show_view
+ end
+ format.json do
+ render json: serializer.represent(issuable, serializer: params[:serializer])
+ end
+ end
+ end
+
+ def update
+ @issuable = update_service.execute(issuable)
+
+ respond_to do |format|
+ format.html do
+ recaptcha_check_with_fallback { render :edit }
+ end
+
+ format.json do
+ render_entity_json
+ end
+ end
+
+ rescue ActiveRecord::StaleObjectError
+ render_conflict_response
+ end
+
+ def realtime_changes
+ Gitlab::PollingInterval.set_header(response, interval: 3_000)
+
+ response = {
+ title: view_context.markdown_field(issuable, :title),
+ title_text: issuable.title,
+ description: view_context.markdown_field(issuable, :description),
+ description_text: issuable.description,
+ task_status: issuable.task_status
+ }
+
+ if issuable.edited?
+ response[:updated_at] = issuable.updated_at
+ response[:updated_by_name] = issuable.last_edited_by.name
+ response[:updated_by_path] = user_path(issuable.last_edited_by)
+ end
+
+ render json: response
+ end
+
def destroy
issuable.destroy
destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym
@@ -68,6 +116,10 @@ module IssuableActions
end
end
+ def authorize_update_issuable!
+ render_404 unless can?(current_user, :"update_#{resource_name}", issuable)
+ end
+
def bulk_update_params
permitted_keys = [
:issuable_ids,
@@ -92,4 +144,24 @@ module IssuableActions
def resource_name
@resource_name ||= controller_name.singularize
end
+
+ def render_entity_json
+ if @issuable.valid?
+ render json: serializer.represent(@issuable)
+ else
+ render json: { errors: @issuable.errors.full_messages }, status: :unprocessable_entity
+ end
+ end
+
+ def show_view
+ 'show'
+ end
+
+ def serializer
+ raise NotImplementedError
+ end
+
+ def update_service
+ raise NotImplementedError
+ end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 6a5e4538717..d4e763aa5b8 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -16,7 +16,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_create_issue!, only: [:new, :create]
# Allow modify issue
- before_action :authorize_update_issue!, only: [:edit, :update, :move]
+ before_action :authorize_update_issuable!, only: [:edit, :update, :move]
# Allow create a new branch and empty WIP merge request from current issue
before_action :authorize_create_merge_request!, only: [:create_merge_request]
@@ -67,18 +67,6 @@ class Projects::IssuesController < Projects::ApplicationController
respond_with(@issue)
end
- def show
- @noteable = @issue
- @note = @project.notes.new(noteable: @issue)
-
- respond_to do |format|
- format.html
- format.json do
- render json: serializer.represent(@issue, serializer: params[:serializer])
- end
- end
- end
-
def discussions
notes = @issue.notes
.inc_relations_for_view
@@ -120,25 +108,6 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
- def update
- update_params = issue_params.merge(spammable_params)
-
- @issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue)
-
- respond_to do |format|
- format.html do
- recaptcha_check_with_fallback { render :edit }
- end
-
- format.json do
- render_issue_json
- end
- end
-
- rescue ActiveRecord::StaleObjectError
- render_conflict_response
- end
-
def move
params.require(:move_to_project_id)
@@ -196,26 +165,6 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
- def realtime_changes
- Gitlab::PollingInterval.set_header(response, interval: 3_000)
-
- response = {
- title: view_context.markdown_field(@issue, :title),
- title_text: @issue.title,
- description: view_context.markdown_field(@issue, :description),
- description_text: @issue.description,
- task_status: @issue.task_status
- }
-
- if @issue.edited?
- response[:updated_at] = @issue.updated_at
- response[:updated_by_name] = @issue.last_edited_by.name
- response[:updated_by_path] = user_path(@issue.last_edited_by)
- end
-
- render json: response
- end
-
def create_merge_request
result = ::MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute
@@ -231,7 +180,8 @@ class Projects::IssuesController < Projects::ApplicationController
def issue
return @issue if defined?(@issue)
# The Sortable default scope causes performance issues when used with find_by
- @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take!
+ @issuable = @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take!
+ @note = @project.notes.new(noteable: @issuable)
return render_404 unless can?(current_user, :read_issue, @issue)
@@ -246,14 +196,6 @@ class Projects::IssuesController < Projects::ApplicationController
project_issue_path(@project, @issue)
end
- def authorize_update_issue!
- render_404 unless can?(current_user, :update_issue, @issue)
- end
-
- def authorize_admin_issues!
- render_404 unless can?(current_user, :admin_issue, @project)
- end
-
def authorize_create_merge_request!
render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
end
@@ -305,4 +247,9 @@ class Projects::IssuesController < Projects::ApplicationController
def serializer
IssueSerializer.new(current_user: current_user, project: issue.project)
end
+
+ def update_service
+ update_params = issue_params.merge(spammable_params)
+ Issues::UpdateService.new(project, current_user, update_params)
+ end
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 2b0294c8387..17cac69e588 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -9,7 +9,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
skip_before_action :merge_request, only: [:index, :bulk_update]
skip_before_action :ensure_ref_fetched, only: [:index, :bulk_update]
- before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort]
+ before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort]
before_action :authenticate_user!, only: [:assign_related_issues]
@@ -256,14 +256,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
alias_method :issuable, :merge_request
alias_method :awardable, :merge_request
- def authorize_update_merge_request!
- return render_404 unless can?(current_user, :update_merge_request, @merge_request)
- end
-
- def authorize_admin_merge_request!
- return render_404 unless can?(current_user, :admin_merge_request, @merge_request)
- end
-
def validates_merge_request
# Show git not found page
# if there is no saved commits between source & target branch
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index d4a91e533c1..a77aa0ad2cc 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -71,11 +71,13 @@ module GitlabRoutingHelper
project_commit_url(entity.project, entity.sha, *args)
end
- def preview_markdown_path(project, *args)
+ def preview_markdown_path(parent, *args)
+ return group_preview_markdown_path(parent) if parent.is_a?(Group)
+
if @snippet.is_a?(PersonalSnippet)
preview_markdown_snippets_path
else
- preview_markdown_project_path(project, *args)
+ preview_markdown_project_path(parent, *args)
end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index d0069cd48cf..85407e38532 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -211,15 +211,13 @@ module IssuablesHelper
def issuable_initial_data(issuable)
data = {
- endpoint: project_issue_path(@project, issuable),
- canUpdate: can?(current_user, :update_issue, issuable),
- canDestroy: can?(current_user, :destroy_issue, issuable),
+ endpoint: issuable_path(issuable),
+ canUpdate: can?(current_user, :"update_#{issuable.to_ability_name}", issuable),
+ canDestroy: can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable),
issuableRef: issuable.to_reference,
- markdownPreviewPath: preview_markdown_path(@project),
+ markdownPreviewPath: preview_markdown_path(parent),
markdownDocsPath: help_page_path('user/markdown'),
issuableTemplates: issuable_templates(issuable),
- projectPath: ref_project.path,
- projectNamespace: ref_project.namespace.full_path,
initialTitleHtml: markdown_field(issuable, :title),
initialTitleText: issuable.title,
initialDescriptionHtml: markdown_field(issuable, :description),
@@ -227,6 +225,12 @@ module IssuablesHelper
initialTaskStatus: issuable.task_status
}
+ if parent.is_a?(Group)
+ data[:groupPath] = parent.path
+ else
+ data.merge!(projectPath: ref_project.path, projectNamespace: ref_project.namespace.full_path)
+ end
+
data.merge!(updated_at_by(issuable))
data.to_json
@@ -263,12 +267,7 @@ module IssuablesHelper
end
def issuable_path(issuable, *options)
- case issuable
- when Issue
- issue_path(issuable, *options)
- when MergeRequest
- merge_request_path(issuable, *options)
- end
+ polymorphic_path(issuable, *options)
end
def issuable_url(issuable, *options)
@@ -369,4 +368,8 @@ module IssuablesHelper
fullPath: @project.full_path
}
end
+
+ def parent
+ @project || @group
+ end
end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 9417033d1f6..98776eab424 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -49,7 +49,8 @@ module CacheMarkdownField
# Always include a project key, or Banzai complains
project = self.project if self.respond_to?(:project)
- context = cached_markdown_fields[field].merge(project: project)
+ group = self.group if self.respond_to?(:group)
+ context = cached_markdown_fields[field].merge(project: project, group: group)
# Banzai is less strict about authors, so don't always have an author key
context[:author] = self.author if self.respond_to?(:author)
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 27f4dedffd3..a928b9d6367 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -14,7 +14,6 @@ module Issuable
include StripAttribute
include Awardable
include Taskable
- include TimeTrackable
include Importable
include Editable
include AfterCommitQueue
@@ -95,8 +94,6 @@ module Issuable
strip_attributes :title
- acts_as_paranoid
-
after_save :record_metrics, unless: :imported?
# We want to use optimistic lock for cases when only title or description are involved
diff --git a/app/models/epic.rb b/app/models/epic.rb
new file mode 100644
index 00000000000..0d5f21fb617
--- /dev/null
+++ b/app/models/epic.rb
@@ -0,0 +1,9 @@
+# Placeholder class for model that is implemented in EE
+# It will reserve (ee#3853) '&' as a reference prefix, but the table does not exists in CE
+class Epic < ActiveRecord::Base
+ prepend EE::Epic
+
+ # TODO: this will be implemented as part of #3853
+ def to_reference
+ end
+end
diff --git a/app/models/group.rb b/app/models/group.rb
index 07fb62bb249..4e8023cdb7f 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -180,6 +180,12 @@ class Group < Namespace
add_user(user, :owner, current_user: current_user)
end
+ def member?(user, min_access_level = Gitlab::Access::GUEST)
+ return false unless user
+
+ max_member_access_for_user(user) >= min_access_level
+ end
+
def has_owner?(user)
return false unless user
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 36e4108b9d6..fc590f9257e 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -10,6 +10,7 @@ class Issue < ActiveRecord::Base
include FasterCacheKeys
include RelativePositioning
include CreatedAtFilterable
+ include TimeTrackable
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
@@ -74,6 +75,8 @@ class Issue < ActiveRecord::Base
end
end
+ acts_as_paranoid
+
def self.reference_prefix
'#'
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index d45b9c805a4..3133dc9e7eb 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -6,6 +6,7 @@ class MergeRequest < ActiveRecord::Base
include Sortable
include IgnorableColumn
include CreatedAtFilterable
+ include TimeTrackable
ignore_column :locked_at
@@ -119,6 +120,8 @@ class MergeRequest < ActiveRecord::Base
after_save :keep_around_commit
+ acts_as_paranoid
+
def self.reference_prefix
'!'
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 8939e590ef1..f9676361072 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -69,7 +69,7 @@ class Note < ActiveRecord::Base
delegate :title, to: :noteable, allow_nil: true
validates :note, presence: true
- validates :project, presence: true, unless: :for_personal_snippet?
+ validates :project, presence: true, if: :for_project_noteable?
# Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size }
@@ -114,7 +114,7 @@ class Note < ActiveRecord::Base
after_initialize :ensure_discussion_id
before_validation :nullify_blank_type, :nullify_blank_line_code
before_validation :set_discussion_id, on: :create
- after_save :keep_around_commit, unless: :for_personal_snippet?
+ after_save :keep_around_commit, if: :for_project_noteable?
after_save :expire_etag_cache
after_destroy :expire_etag_cache
@@ -208,6 +208,10 @@ class Note < ActiveRecord::Base
noteable.is_a?(PersonalSnippet)
end
+ def for_project_noteable?
+ !for_personal_snippet?
+ end
+
def skip_project_check?
for_personal_snippet?
end
diff --git a/app/serializers/issuable_entity.rb b/app/serializers/issuable_entity.rb
index 61c7a428745..3b5a4fd4f79 100644
--- a/app/serializers/issuable_entity.rb
+++ b/app/serializers/issuable_entity.rb
@@ -1,20 +1,16 @@
class IssuableEntity < Grape::Entity
+ include RequestAwareEntity
+
expose :id
expose :iid
expose :author_id
expose :description
expose :lock_version
expose :milestone_id
- expose :state
expose :title
expose :updated_by_id
expose :created_at
expose :updated_at
- expose :deleted_at
- expose :time_estimate
- expose :total_time_spent
- expose :human_time_estimate
- expose :human_total_time_spent
expose :milestone, using: API::Entities::Milestone
expose :labels, using: LabelEntity
end
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index 10d3ad0214b..5f47592e4ad 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -1,6 +1,8 @@
class IssueEntity < IssuableEntity
- include RequestAwareEntity
+ include TimeTrackableEntity
+ expose :state
+ expose :deleted_at
expose :branch_name
expose :confidential
expose :discussion_locked
diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb
index 297a459e394..b53a49fe59e 100644
--- a/app/serializers/merge_request_entity.rb
+++ b/app/serializers/merge_request_entity.rb
@@ -1,6 +1,8 @@
class MergeRequestEntity < IssuableEntity
- include RequestAwareEntity
+ include TimeTrackableEntity
+ expose :state
+ expose :deleted_at
expose :in_progress_merge_commit_sha
expose :merge_commit_sha
expose :merge_error
diff --git a/app/serializers/time_trackable_entity.rb b/app/serializers/time_trackable_entity.rb
new file mode 100644
index 00000000000..e81cd7bec72
--- /dev/null
+++ b/app/serializers/time_trackable_entity.rb
@@ -0,0 +1,11 @@
+module TimeTrackableEntity
+ extend ActiveSupport::Concern
+ extend Grape
+
+ included do
+ expose :time_estimate
+ expose :total_time_spent
+ expose :human_time_estimate
+ expose :human_total_time_spent
+ end
+end
diff --git a/app/services/issuable/common_system_notes_service.rb b/app/services/issuable/common_system_notes_service.rb
new file mode 100644
index 00000000000..92eaa5d5115
--- /dev/null
+++ b/app/services/issuable/common_system_notes_service.rb
@@ -0,0 +1,81 @@
+module Issuable
+ class CommonSystemNotesService < ::BaseService
+ attr_reader :issuable
+
+ def execute(issuable, old_labels)
+ @issuable = issuable
+
+ if issuable.previous_changes.include?('title')
+ create_title_change_note(issuable.previous_changes['title'].first)
+ end
+
+ handle_description_change_note
+
+ handle_time_tracking_note if issuable.is_a?(TimeTrackable)
+ create_labels_note(old_labels) if issuable.labels != old_labels
+ create_discussion_lock_note if issuable.previous_changes.include?('discussion_locked')
+ create_milestone_note if issuable.previous_changes.include?('milestone_id')
+ end
+
+ private
+
+ def handle_time_tracking_note
+ if issuable.previous_changes.include?('time_estimate')
+ create_time_estimate_note
+ end
+
+ if issuable.time_spent?
+ create_time_spent_note
+ end
+ end
+
+ def handle_description_change_note
+ if issuable.previous_changes.include?('description')
+ if issuable.tasks? && issuable.updated_tasks.any?
+ create_task_status_note
+ else
+ # TODO: Show this note if non-task content was modified.
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/33577
+ create_description_change_note
+ end
+ end
+ end
+
+ def create_labels_note(old_labels)
+ added_labels = issuable.labels - old_labels
+ removed_labels = old_labels - issuable.labels
+
+ SystemNoteService.change_label(issuable, issuable.project, current_user, added_labels, removed_labels)
+ end
+
+ def create_title_change_note(old_title)
+ SystemNoteService.change_title(issuable, issuable.project, current_user, old_title)
+ end
+
+ def create_description_change_note
+ SystemNoteService.change_description(issuable, issuable.project, current_user)
+ end
+
+ def create_task_status_note
+ issuable.updated_tasks.each do |task|
+ SystemNoteService.change_task_status(issuable, issuable.project, current_user, task)
+ end
+ end
+
+ def create_time_estimate_note
+ SystemNoteService.change_time_estimate(issuable, issuable.project, current_user)
+ end
+
+ def create_time_spent_note
+ SystemNoteService.change_time_spent(issuable, issuable.project, issuable.time_spent_user)
+ end
+
+ def create_milestone_note
+ SystemNoteService.change_milestone(issuable, issuable.project, current_user, issuable.milestone)
+ end
+
+ def create_discussion_lock_note
+ SystemNoteService.discussion_lock(issuable, current_user)
+ end
+ end
+end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index d61a342ebad..68b49d880f7 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -1,56 +1,10 @@
class IssuableBaseService < BaseService
private
- def create_milestone_note(issuable)
- SystemNoteService.change_milestone(
- issuable, issuable.project, current_user, issuable.milestone)
- end
-
- def create_labels_note(issuable, old_labels)
- added_labels = issuable.labels - old_labels
- removed_labels = old_labels - issuable.labels
-
- SystemNoteService.change_label(
- issuable, issuable.project, current_user, added_labels, removed_labels)
- end
-
- def create_title_change_note(issuable, old_title)
- SystemNoteService.change_title(
- issuable, issuable.project, current_user, old_title)
- end
-
- def create_description_change_note(issuable)
- SystemNoteService.change_description(issuable, issuable.project, current_user)
- end
-
- def create_branch_change_note(issuable, branch_type, old_branch, new_branch)
- SystemNoteService.change_branch(
- issuable, issuable.project, current_user, branch_type,
- old_branch, new_branch)
- end
-
- def create_task_status_note(issuable)
- issuable.updated_tasks.each do |task|
- SystemNoteService.change_task_status(issuable, issuable.project, current_user, task)
- end
- end
-
- def create_time_estimate_note(issuable)
- SystemNoteService.change_time_estimate(issuable, issuable.project, current_user)
- end
-
- def create_time_spent_note(issuable)
- SystemNoteService.change_time_spent(issuable, issuable.project, current_user)
- end
-
- def create_discussion_lock_note(issuable)
- SystemNoteService.discussion_lock(issuable, current_user)
- end
-
def filter_params(issuable)
ability_name = :"admin_#{issuable.to_ability_name}"
- unless can?(current_user, ability_name, project)
+ unless can?(current_user, ability_name, issuable)
params.delete(:milestone_id)
params.delete(:labels)
params.delete(:add_label_ids)
@@ -233,15 +187,14 @@ class IssuableBaseService < BaseService
# We have to perform this check before saving the issuable as Rails resets
# the changed fields upon calling #save.
- update_project_counters = issuable.update_project_counter_caches?
+ update_project_counters = issuable.project && issuable.update_project_counter_caches?
if issuable.with_transaction_returning_status { issuable.save }
# We do not touch as it will affect a update on updated_at field
ActiveRecord::Base.no_touching do
- handle_common_system_notes(issuable, old_labels: old_labels)
+ Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, old_labels)
end
- change_discussion_lock(issuable)
handle_changes(
issuable,
old_labels: old_labels,
@@ -300,12 +253,6 @@ class IssuableBaseService < BaseService
end
end
- def change_discussion_lock(issuable)
- if issuable.previous_changes.include?('discussion_locked')
- create_discussion_lock_note(issuable)
- end
- end
-
def toggle_award(issuable)
award = params.delete(:emoji_award)
if award
@@ -328,35 +275,17 @@ class IssuableBaseService < BaseService
attrs_changed || labels_changed || assignees_changed
end
- def handle_common_system_notes(issuable, old_labels: [])
- if issuable.previous_changes.include?('title')
- create_title_change_note(issuable, issuable.previous_changes['title'].first)
- end
-
- if issuable.previous_changes.include?('description')
- if issuable.tasks? && issuable.updated_tasks.any?
- create_task_status_note(issuable)
- else
- # TODO: Show this note if non-task content was modified.
- # https://gitlab.com/gitlab-org/gitlab-ce/issues/33577
- create_description_change_note(issuable)
- end
- end
-
- if issuable.previous_changes.include?('time_estimate')
- create_time_estimate_note(issuable)
- end
-
- if issuable.time_spent?
- create_time_spent_note(issuable)
- end
-
- create_labels_note(issuable, old_labels) if issuable.labels != old_labels
- end
-
def invalidate_cache_counts(issuable, users: [])
users.each do |user|
user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts") # rubocop:disable GitlabSecurity/PublicSend
end
end
+
+ # override if needed
+ def handle_changes(issuable, options)
+ end
+
+ # override if needed
+ def execute_hooks(issuable, action = 'open', params = {})
+ end
end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index e0339ddf9bb..1b7b5927c5a 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -27,10 +27,6 @@ module Issues
todo_service.update_issue(issue, current_user, old_mentioned_users)
end
- if issue.previous_changes.include?('milestone_id')
- create_milestone_note(issue)
- end
-
if issue.assignees != old_assignees
create_assignee_note(issue, old_assignees)
notification_service.reassigned_issue(issue, current_user, old_assignees)
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 2832d893e95..1f394cacc64 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -40,10 +40,6 @@ module MergeRequests
merge_request.target_branch)
end
- if merge_request.previous_changes.include?('milestone_id')
- create_milestone_note(merge_request)
- end
-
if merge_request.previous_changes.include?('assignee_id')
create_assignee_note(merge_request)
notification_service.reassigned_merge_request(merge_request, current_user)
@@ -111,5 +107,11 @@ module MergeRequests
end
end
end
+
+ def create_branch_change_note(issuable, branch_type, old_branch, new_branch)
+ SystemNoteService.change_branch(
+ issuable, issuable.project, current_user, branch_type,
+ old_branch, new_branch)
+ end
end
end
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index cb723fe6a18..72d5c4961ec 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -34,7 +34,7 @@
%li{ class: [merge_request_button_visibility(@merge_request, true), 'js-close-item'] }
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request'
%li{ class: merge_request_button_visibility(@merge_request, false) }
- = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
+ = link_to 'Reopen', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
- if can_update_merge_request
= link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit"
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index ef4578aabd6..a0f7e4e5ad5 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -95,7 +95,7 @@ module Banzai
end
def call
- return doc if project.nil?
+ return doc unless project || group
ref_pattern = object_class.reference_pattern
link_pattern = object_class.link_reference_pattern
@@ -288,10 +288,14 @@ module Banzai
end
def current_project_path
+ return unless project
+
@current_project_path ||= project.full_path
end
def current_project_namespace_path
+ return unless project
+
@current_project_namespace_path ||= project.namespace.full_path
end
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index a6f8650ed3d..c6ae28adf87 100644
--- a/lib/banzai/filter/reference_filter.rb
+++ b/lib/banzai/filter/reference_filter.rb
@@ -55,6 +55,10 @@ module Banzai
context[:project]
end
+ def group
+ context[:group]
+ end
+
def skip_project_check?
context[:skip_project_check]
end
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index f3356d6c51e..afb6e25963c 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -24,7 +24,7 @@ module Banzai
end
def call
- return doc if project.nil? && !skip_project_check?
+ return doc if project.nil? && group.nil? && !skip_project_check?
ref_pattern = User.reference_pattern
ref_pattern_start = /\A#{ref_pattern}\z/
@@ -101,19 +101,12 @@ module Banzai
end
def link_to_all(link_content: nil)
- project = context[:project]
author = context[:author]
- if author && !project.team.member?(author)
+ if author && !team_member?(author)
link_content
else
- url = urls.project_url(project,
- only_path: context[:only_path])
-
- data = data_attribute(project: project.id, author: author.try(:id))
- content = link_content || User.reference_prefix + 'all'
-
- link_tag(url, data, content, 'All Project and Group Members')
+ parent_url(link_content, author)
end
end
@@ -144,6 +137,35 @@ module Banzai
def link_tag(url, data, link_content, title)
%(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{link_content}</a>)
end
+
+ def parent
+ context[:project] || context[:group]
+ end
+
+ def parent_group?
+ parent.is_a?(Group)
+ end
+
+ def team_member?(user)
+ if parent_group?
+ parent.member?(user)
+ else
+ parent.team.member?(user)
+ end
+ end
+
+ def parent_url(link_content, author)
+ if parent_group?
+ url = urls.group_url(parent, only_path: context[:only_path])
+ data = data_attribute(group: group.id, author: author.try(:id))
+ else
+ url = urls.project_url(parent, only_path: context[:only_path])
+ data = data_attribute(project: project.id, author: author.try(:id))
+ end
+
+ content = link_content || User.reference_prefix + 'all'
+ link_tag(url, data, content, 'All Project and Group Members')
+ end
end
end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index aecdfb50759..8016176110e 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -248,6 +248,45 @@ describe Projects::IssuesController do
end
end
+ describe 'PUT #update' do
+ subject do
+ put :update,
+ namespace_id: project.namespace,
+ project_id: project,
+ id: issue.to_param,
+ issue: { title: 'New title' }, format: :json
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when user has access to update issue' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'updates the issue' do
+ subject
+
+ expect(response).to have_http_status(:ok)
+ expect(issue.reload.title).to eq('New title')
+ end
+ end
+
+ context 'when user does not have access to update issue' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'responds with 404' do
+ subject
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+ end
+
describe 'Confidential Issues' do
let(:project) { create(:project_empty_repo, :public) }
let(:assignee) { create(:assignee) }
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 52ef8c6a589..14021b8ca50 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -186,17 +186,23 @@ describe Projects::MergeRequestsController do
end
describe 'PUT update' do
+ def update_merge_request(mr_params, additional_params = {})
+ params = {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: merge_request.iid,
+ merge_request: mr_params
+ }.merge(additional_params)
+
+ put :update, params
+ end
+
context 'changing the assignee' do
it 'limits the attributes exposed on the assignee' do
assignee = create(:user)
project.add_developer(assignee)
- put :update,
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: merge_request.iid,
- merge_request: { assignee_id: assignee.id },
- format: :json
+ update_merge_request({ assignee_id: assignee.id }, format: :json)
body = JSON.parse(response.body)
expect(body['assignee'].keys)
@@ -204,6 +210,20 @@ describe Projects::MergeRequestsController do
end
end
+ context 'when user does not have access to update issue' do
+ before do
+ reporter = create(:user)
+ project.add_reporter(reporter)
+ sign_in(reporter)
+ end
+
+ it 'responds with 404' do
+ update_merge_request(title: 'New title')
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
context 'there is no source project' do
let(:project) { create(:project, :repository) }
let(:forked_project) { fork_project_with_submodules(project) }
@@ -214,13 +234,7 @@ describe Projects::MergeRequestsController do
end
it 'closes MR without errors' do
- post :update,
- namespace_id: project.namespace,
- project_id: project,
- id: merge_request.iid,
- merge_request: {
- state_event: 'close'
- }
+ update_merge_request(state_event: 'close')
expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request])
expect(merge_request.reload.closed?).to be_truthy
@@ -229,13 +243,7 @@ describe Projects::MergeRequestsController do
it 'allows editing of a closed merge request' do
merge_request.close!
- put :update,
- namespace_id: project.namespace,
- project_id: project,
- id: merge_request.iid,
- merge_request: {
- title: 'New title'
- }
+ update_merge_request(title: 'New title')
expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request])
expect(merge_request.reload.title).to eq 'New title'
@@ -244,13 +252,7 @@ describe Projects::MergeRequestsController do
it 'does not allow to update target branch closed merge request' do
merge_request.close!
- put :update,
- namespace_id: project.namespace,
- project_id: project,
- id: merge_request.iid,
- merge_request: {
- target_branch: 'new_branch'
- }
+ update_merge_request(target_branch: 'new_branch')
expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch }
end
diff --git a/spec/helpers/gitlab_routing_helper_spec.rb b/spec/helpers/gitlab_routing_helper_spec.rb
index a44b200c5da..6c4f7050ee0 100644
--- a/spec/helpers/gitlab_routing_helper_spec.rb
+++ b/spec/helpers/gitlab_routing_helper_spec.rb
@@ -63,4 +63,30 @@ describe GitlabRoutingHelper do
it { expect(resend_invite_group_member_path(group_member)).to eq resend_invite_group_group_member_path(group_member.source, group_member) }
end
end
+
+ describe '#preview_markdown_path' do
+ let(:project) { create(:project) }
+
+ it 'returns group preview markdown path for a group parent' do
+ group = create(:group)
+
+ expect(preview_markdown_path(group)).to eq("/groups/#{group.path}/preview_markdown")
+ end
+
+ it 'returns project preview markdown path for a project parent' do
+ expect(preview_markdown_path(project)).to eq("/#{project.full_path}/preview_markdown")
+ end
+
+ it 'returns snippet preview markdown path for a personal snippet' do
+ @snippet = create(:personal_snippet)
+
+ expect(preview_markdown_path(nil)).to eq("/snippets/preview_markdown")
+ end
+
+ it 'returns project preview markdown path for a project snippet' do
+ @snippet = create(:project_snippet, project: project)
+
+ expect(preview_markdown_path(project)).to eq("/#{project.full_path}/preview_markdown")
+ end
+ end
end
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index ead3e28438e..cb851d828f2 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -159,4 +159,36 @@ describe IssuablesHelper do
end
end
end
+
+ describe '#issuable_initial_data' do
+ let(:user) { create(:user) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ allow(helper).to receive(:can?).and_return(true)
+ end
+
+ it 'returns the correct json for an issue' do
+ issue = create(:issue, author: user, description: 'issue text')
+ @project = issue.project
+
+ expected_data = {
+ 'endpoint' => "/#{@project.full_path}/issues/#{issue.iid}",
+ 'canUpdate' => true,
+ 'canDestroy' => true,
+ 'issuableRef' => "##{issue.iid}",
+ 'markdownPreviewPath' => "/#{@project.full_path}/preview_markdown",
+ 'markdownDocsPath' => '/help/user/markdown',
+ 'issuableTemplates' => [],
+ 'projectPath' => @project.path,
+ 'projectNamespace' => @project.namespace.path,
+ 'initialTitleHtml' => issue.title,
+ 'initialTitleText' => issue.title,
+ 'initialDescriptionHtml' => '<p dir="auto">issue text</p>',
+ 'initialDescriptionText' => 'issue text',
+ 'initialTaskStatus' => '0 of 0 tasks completed'
+ }
+ expect(JSON.parse(helper.issuable_initial_data(issue))).to eq(expected_data)
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
index 9c74c9b8c99..3c98b18f99b 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -317,6 +317,68 @@ describe Banzai::Filter::IssueReferenceFilter do
end
end
+ context 'group context' do
+ let(:group) { create(:group) }
+ let(:context) { { project: nil, group: group } }
+
+ it 'ignores shorthanded issue reference' do
+ reference = "##{issue.iid}"
+ text = "Fixed #{reference}"
+
+ expect(reference_filter(text, context).to_html).to eq(text)
+ end
+
+ it 'ignores valid references when cross-reference project uses external tracker' do
+ expect_any_instance_of(described_class).to receive(:find_object)
+ .with(project, issue.iid)
+ .and_return(nil)
+
+ reference = "#{project.full_path}##{issue.iid}"
+ text = "Issue #{reference}"
+
+ expect(reference_filter(text, context).to_html).to eq(text)
+ end
+
+ it 'links to a valid reference for complete cross-reference' do
+ reference = "#{project.full_path}##{issue.iid}"
+ doc = reference_filter("See #{reference}", context)
+
+ expect(doc.css('a').first.attr('href')).to eq helper.url_for_issue(issue.iid, project)
+ end
+
+ it 'ignores reference for shorthand cross-reference' do
+ reference = "#{project.path}##{issue.iid}"
+ text = "See #{reference}"
+
+ expect(reference_filter(text, context).to_html).to eq(text)
+ end
+
+ it 'links to a valid reference for url cross-reference' do
+ reference = helper.url_for_issue(issue.iid, project) + "#note_123"
+
+ doc = reference_filter("See #{reference}", context)
+
+ expect(doc.css('a').first.attr('href')).to eq(helper.url_for_issue(issue.iid, project) + "#note_123")
+ end
+
+ it 'links to a valid reference for cross-reference in link href' do
+ reference = "#{helper.url_for_issue(issue.iid, project) + "#note_123"}"
+ reference_link = %{<a href="#{reference}">Reference</a>}
+
+ doc = reference_filter("See #{reference_link}", context)
+
+ expect(doc.css('a').first.attr('href')).to eq helper.url_for_issue(issue.iid, project) + "#note_123"
+ end
+
+ it 'links to a valid reference for issue reference in the link href' do
+ reference = issue.to_reference(group)
+ reference_link = %{<a href="#{reference}">Reference</a>}
+ doc = reference_filter("See #{reference_link}", context)
+
+ expect(doc.css('a').first.attr('href')).to eq helper.url_for_issue(issue.iid, project)
+ end
+ end
+
describe '#issues_per_project' do
context 'using an internal issue tracker' do
it 'returns a Hash containing the issues per project' do
diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb
index 2cd30a5e302..862b1fe3fd3 100644
--- a/spec/lib/banzai/filter/label_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb
@@ -594,4 +594,16 @@ describe Banzai::Filter::LabelReferenceFilter do
expect(reference_filter(act).to_html).to eq exp
end
end
+
+ describe 'group context' do
+ it 'points to referenced project issues page' do
+ project = create(:project)
+ label = create(:label, project: project)
+ reference = "#{project.full_path}~#{label.name}"
+
+ result = reference_filter("See #{reference}", { project: nil, group: create(:group) } )
+
+ expect(result.css('a').first.attr('href')).to eq(urls.project_issues_url(project, label_name: label.name))
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
index ed2788f8a33..158844e25ae 100644
--- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
@@ -214,4 +214,14 @@ describe Banzai::Filter::MergeRequestReferenceFilter do
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)<\/a>\.\)/)
end
end
+
+ context 'group context' do
+ it 'links to a valid reference' do
+ reference = "#{project.full_path}!#{merge.iid}"
+
+ result = reference_filter("See #{reference}", { project: nil, group: create(:group) } )
+
+ expect(result.css('a').first.attr('href')).to eq(urls.project_merge_request_url(project, merge))
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
index fe7a8c84c9e..84578668133 100644
--- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
@@ -343,4 +343,15 @@ describe Banzai::Filter::MilestoneReferenceFilter do
expect(doc.css('a')).to be_empty
end
end
+
+ context 'group context' do
+ it 'links to a valid reference' do
+ milestone = create(:milestone, project: project)
+ reference = "#{project.full_path}%#{milestone.iid}"
+
+ result = reference_filter("See #{reference}", { project: nil, group: create(:group) } )
+
+ expect(result.css('a').first.attr('href')).to eq(urls.milestone_url(milestone))
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
index 90ac4c7b238..3a07a6dc179 100644
--- a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
@@ -201,4 +201,14 @@ describe Banzai::Filter::SnippetReferenceFilter do
expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/)
end
end
+
+ context 'group context' do
+ it 'links to a valid reference' do
+ reference = "#{project.full_path}$#{snippet.id}"
+
+ result = reference_filter("See #{reference}", { project: nil, group: create(:group) } )
+
+ expect(result.css('a').first.attr('href')).to eq(urls.project_snippet_url(project, snippet))
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb
index 34dac1db69a..fc03741976e 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -208,6 +208,39 @@ describe Banzai::Filter::UserReferenceFilter do
end
end
+ context 'in group context' do
+ let(:group) { create(:group) }
+ let(:group_member) { create(:user) }
+
+ before do
+ group.add_developer(group_member)
+ end
+
+ let(:context) { { author: group_member, project: nil, group: group } }
+
+ it 'supports a special @all mention' do
+ reference = User.reference_prefix + 'all'
+ doc = reference_filter("Hey #{reference}", context)
+
+ expect(doc.css('a').length).to eq(1)
+ expect(doc.css('a').first.attr('href')).to eq urls.group_url(group)
+ end
+
+ it 'supports mentioning a single user' do
+ reference = group_member.to_reference
+ doc = reference_filter("Hey #{reference}", context)
+
+ expect(doc.css('a').first.attr('href')).to eq urls.user_url(group_member)
+ end
+
+ it 'supports mentioning a group' do
+ reference = group.to_reference
+ doc = reference_filter("Hey #{reference}", context)
+
+ expect(doc.css('a').first.attr('href')).to eq urls.user_url(group)
+ end
+ end
+
describe '#namespaces' do
it 'returns a Hash containing all Namespaces' do
document = Nokogiri::HTML.fragment("<p>#{user.to_reference}</p>")
diff --git a/spec/serializers/issue_entity_spec.rb b/spec/serializers/issue_entity_spec.rb
new file mode 100644
index 00000000000..caa3e41402b
--- /dev/null
+++ b/spec/serializers/issue_entity_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe IssueEntity do
+ let(:project) { create(:project) }
+ let(:resource) { create(:issue, project: project) }
+ let(:user) { create(:user) }
+
+ let(:request) { double('request', current_user: user) }
+
+ subject { described_class.new(resource, request: request).as_json }
+
+ it 'has Issuable attributes' do
+ expect(subject).to include(:id, :iid, :author_id, :description, :lock_version, :milestone_id,
+ :title, :updated_by_id, :created_at, :updated_at, :milestone, :labels)
+ end
+
+ it 'has time estimation attributes' do
+ expect(subject).to include(:time_estimate, :total_time_spent, :human_time_estimate, :human_total_time_spent)
+ end
+end
diff --git a/spec/serializers/merge_request_entity_spec.rb b/spec/serializers/merge_request_entity_spec.rb
index 87832b3dca1..f9285049c0d 100644
--- a/spec/serializers/merge_request_entity_spec.rb
+++ b/spec/serializers/merge_request_entity_spec.rb
@@ -30,8 +30,17 @@ describe MergeRequestEntity do
:assign_to_closing)
end
+ it 'has Issuable attributes' do
+ expect(subject).to include(:id, :iid, :author_id, :description, :lock_version, :milestone_id,
+ :title, :updated_by_id, :created_at, :updated_at, :milestone, :labels)
+ end
+
+ it 'has time estimation attributes' do
+ expect(subject).to include(:time_estimate, :total_time_spent, :human_time_estimate, :human_total_time_spent)
+ end
+
it 'has important MergeRequest attributes' do
- expect(subject).to include(:diff_head_sha, :merge_commit_message,
+ expect(subject).to include(:state, :deleted_at, :diff_head_sha, :merge_commit_message,
:has_conflicts, :has_ci, :merge_path,
:conflict_resolution_path,
:cancel_merge_when_pipeline_succeeds_path,
diff --git a/spec/services/issuable/common_system_notes_service_spec.rb b/spec/services/issuable/common_system_notes_service_spec.rb
new file mode 100644
index 00000000000..9f92b662be1
--- /dev/null
+++ b/spec/services/issuable/common_system_notes_service_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe Issuable::CommonSystemNotesService do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:issuable) { create(:issue) }
+
+ shared_examples 'system note creation' do |update_params, note_text|
+ subject { described_class.new(project, user).execute(issuable, [])}
+
+ before do
+ issuable.assign_attributes(update_params)
+ issuable.save
+ end
+
+ it 'creates 1 system note with the correct content' do
+ expect { subject }.to change { Note.count }.from(0).to(1)
+
+ note = Note.last
+ expect(note.note).to match(note_text)
+ expect(note.noteable_type).to eq('Issue')
+ end
+ end
+
+ describe '#execute' do
+ it_behaves_like 'system note creation', { title: 'New title' }, 'changed title'
+ it_behaves_like 'system note creation', { description: 'New description' }, 'changed the description'
+ it_behaves_like 'system note creation', { discussion_locked: true }, 'locked this issue'
+ it_behaves_like 'system note creation', { time_estimate: 5 }, 'changed time estimate'
+
+ context 'when new label is added' do
+ before do
+ label = create(:label, project: project)
+ issuable.labels << label
+ end
+
+ it_behaves_like 'system note creation', {}, /added ~\w+ label/
+ end
+
+ context 'when new milestone is assigned' do
+ before do
+ milestone = create(:milestone, project: project)
+ issuable.milestone_id = milestone.id
+ end
+
+ it_behaves_like 'system note creation', {}, 'changed milestone'
+ end
+ end
+end