Commit 004e9e0e authored by Clement Ho's avatar Clement Ho Committed by Jacob Schatz

Show epic view

parent 34528932
...@@ -7,6 +7,54 @@ module IssuableActions ...@@ -7,6 +7,54 @@ module IssuableActions
before_action :authorize_admin_issuable!, only: :bulk_update before_action :authorize_admin_issuable!, only: :bulk_update
end 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 def destroy
issuable.destroy issuable.destroy
destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym
...@@ -68,6 +116,10 @@ module IssuableActions ...@@ -68,6 +116,10 @@ module IssuableActions
end end
end end
def authorize_update_issuable!
render_404 unless can?(current_user, :"update_#{resource_name}", issuable)
end
def bulk_update_params def bulk_update_params
permitted_keys = [ permitted_keys = [
:issuable_ids, :issuable_ids,
...@@ -92,4 +144,24 @@ module IssuableActions ...@@ -92,4 +144,24 @@ module IssuableActions
def resource_name def resource_name
@resource_name ||= controller_name.singularize @resource_name ||= controller_name.singularize
end 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 end
...@@ -16,7 +16,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -16,7 +16,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_create_issue!, only: [:new, :create] before_action :authorize_create_issue!, only: [:new, :create]
# Allow modify issue # 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 # Allow create a new branch and empty WIP merge request from current issue
before_action :authorize_create_merge_request!, only: [:create_merge_request] before_action :authorize_create_merge_request!, only: [:create_merge_request]
...@@ -69,18 +69,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -69,18 +69,6 @@ class Projects::IssuesController < Projects::ApplicationController
respond_with(@issue) respond_with(@issue)
end 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 def discussions
notes = @issue.notes notes = @issue.notes
.inc_relations_for_view .inc_relations_for_view
...@@ -122,25 +110,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -122,25 +110,6 @@ class Projects::IssuesController < Projects::ApplicationController
end end
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 def move
params.require(:move_to_project_id) params.require(:move_to_project_id)
...@@ -198,26 +167,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -198,26 +167,6 @@ class Projects::IssuesController < Projects::ApplicationController
end end
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 def create_merge_request
result = ::MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute result = ::MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute
...@@ -233,7 +182,8 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -233,7 +182,8 @@ class Projects::IssuesController < Projects::ApplicationController
def issue def issue
return @issue if defined?(@issue) return @issue if defined?(@issue)
# The Sortable default scope causes performance issues when used with find_by # 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) return render_404 unless can?(current_user, :read_issue, @issue)
...@@ -248,14 +198,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -248,14 +198,6 @@ class Projects::IssuesController < Projects::ApplicationController
project_issue_path(@project, @issue) project_issue_path(@project, @issue)
end 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! def authorize_create_merge_request!
render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user) render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
end end
...@@ -307,4 +249,9 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -307,4 +249,9 @@ class Projects::IssuesController < Projects::ApplicationController
def serializer def serializer
IssueSerializer.new(current_user: current_user, project: issue.project) IssueSerializer.new(current_user: current_user, project: issue.project)
end end
def update_service
update_params = issue_params.merge(spammable_params)
Issues::UpdateService.new(project, current_user, update_params)
end
end end
...@@ -11,7 +11,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -11,7 +11,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
skip_before_action :merge_request, only: [:index, :bulk_update] skip_before_action :merge_request, only: [:index, :bulk_update]
skip_before_action :ensure_ref_fetched, 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] before_action :authenticate_user!, only: [:assign_related_issues]
...@@ -259,14 +259,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -259,14 +259,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
alias_method :issuable, :merge_request alias_method :issuable, :merge_request
alias_method :awardable, :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 def validates_merge_request
# Show git not found page # Show git not found page
# if there is no saved commits between source & target branch # if there is no saved commits between source & target branch
......
...@@ -73,11 +73,13 @@ module GitlabRoutingHelper ...@@ -73,11 +73,13 @@ module GitlabRoutingHelper
project_commit_url(entity.project, entity.sha, *args) project_commit_url(entity.project, entity.sha, *args)
end 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) if @snippet.is_a?(PersonalSnippet)
preview_markdown_snippets_path preview_markdown_snippets_path
else else
preview_markdown_project_path(project, *args) preview_markdown_project_path(parent, *args)
end end
end end
......
...@@ -212,15 +212,13 @@ module IssuablesHelper ...@@ -212,15 +212,13 @@ module IssuablesHelper
def issuable_initial_data(issuable) def issuable_initial_data(issuable)
data = { data = {
endpoint: project_issue_path(@project, issuable), endpoint: issuable_path(issuable),
canUpdate: can?(current_user, :update_issue, issuable), canUpdate: can?(current_user, :"update_#{issuable.to_ability_name}", issuable),
canDestroy: can?(current_user, :destroy_issue, issuable), canDestroy: can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable),
issuableRef: issuable.to_reference, issuableRef: issuable.to_reference,
markdownPreviewPath: preview_markdown_path(@project), markdownPreviewPath: preview_markdown_path(parent),
markdownDocsPath: help_page_path('user/markdown'), markdownDocsPath: help_page_path('user/markdown'),
issuableTemplates: issuable_templates(issuable), issuableTemplates: issuable_templates(issuable),
projectPath: ref_project.path,
projectNamespace: ref_project.namespace.full_path,
initialTitleHtml: markdown_field(issuable, :title), initialTitleHtml: markdown_field(issuable, :title),
initialTitleText: issuable.title, initialTitleText: issuable.title,
initialDescriptionHtml: markdown_field(issuable, :description), initialDescriptionHtml: markdown_field(issuable, :description),
...@@ -228,6 +226,12 @@ module IssuablesHelper ...@@ -228,6 +226,12 @@ module IssuablesHelper
initialTaskStatus: issuable.task_status 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.merge!(updated_at_by(issuable))
data.to_json data.to_json
...@@ -264,12 +268,7 @@ module IssuablesHelper ...@@ -264,12 +268,7 @@ module IssuablesHelper
end end
def issuable_path(issuable, *options) def issuable_path(issuable, *options)
case issuable polymorphic_path(issuable, *options)
when Issue
issue_path(issuable, *options)
when MergeRequest
merge_request_path(issuable, *options)
end
end end
def issuable_url(issuable, *options) def issuable_url(issuable, *options)
...@@ -370,4 +369,8 @@ module IssuablesHelper ...@@ -370,4 +369,8 @@ module IssuablesHelper
fullPath: @project.full_path fullPath: @project.full_path
} }
end end
def parent
@project || @group
end
end end
...@@ -49,7 +49,8 @@ module CacheMarkdownField ...@@ -49,7 +49,8 @@ module CacheMarkdownField
# Always include a project key, or Banzai complains # Always include a project key, or Banzai complains
project = self.project if self.respond_to?(:project) 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 # Banzai is less strict about authors, so don't always have an author key
context[:author] = self.author if self.respond_to?(:author) context[:author] = self.author if self.respond_to?(:author)
......
...@@ -14,7 +14,6 @@ module Issuable ...@@ -14,7 +14,6 @@ module Issuable
include StripAttribute include StripAttribute
include Awardable include Awardable
include Taskable include Taskable
include TimeTrackable
include Importable include Importable
include Editable include Editable
include AfterCommitQueue include AfterCommitQueue
...@@ -98,8 +97,6 @@ module Issuable ...@@ -98,8 +97,6 @@ module Issuable
strip_attributes :title strip_attributes :title
acts_as_paranoid
after_save :record_metrics, unless: :imported? after_save :record_metrics, unless: :imported?
# We want to use optimistic lock for cases when only title or description are involved # We want to use optimistic lock for cases when only title or description are involved
......
# 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
...@@ -199,6 +199,12 @@ class Group < Namespace ...@@ -199,6 +199,12 @@ class Group < Namespace
add_user(user, :owner, current_user: current_user) add_user(user, :owner, current_user: current_user)
end 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) def has_owner?(user)
return false unless user return false unless user
......
...@@ -14,6 +14,7 @@ class Issue < ActiveRecord::Base ...@@ -14,6 +14,7 @@ class Issue < ActiveRecord::Base
include FasterCacheKeys include FasterCacheKeys
include RelativePositioning include RelativePositioning
include CreatedAtFilterable include CreatedAtFilterable
include TimeTrackable
WEIGHT_RANGE = 1..9 WEIGHT_RANGE = 1..9
WEIGHT_ALL = 'Everything'.freeze WEIGHT_ALL = 'Everything'.freeze
...@@ -86,6 +87,8 @@ class Issue < ActiveRecord::Base ...@@ -86,6 +87,8 @@ class Issue < ActiveRecord::Base
end end
end end
acts_as_paranoid
def self.reference_prefix def self.reference_prefix
'#' '#'
end end
......
...@@ -7,6 +7,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -7,6 +7,7 @@ class MergeRequest < ActiveRecord::Base
include Elastic::MergeRequestsSearch include Elastic::MergeRequestsSearch
include IgnorableColumn include IgnorableColumn
include CreatedAtFilterable include CreatedAtFilterable
include TimeTrackable
ignore_column :locked_at ignore_column :locked_at
...@@ -123,6 +124,8 @@ class MergeRequest < ActiveRecord::Base ...@@ -123,6 +124,8 @@ class MergeRequest < ActiveRecord::Base
after_save :keep_around_commit after_save :keep_around_commit
acts_as_paranoid
def self.reference_prefix def self.reference_prefix
'!' '!'
end end
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
# A note of this type is never resolvable. # A note of this type is never resolvable.
class Note < ActiveRecord::Base class Note < ActiveRecord::Base
extend ActiveModel::Naming extend ActiveModel::Naming
prepend EE::Note
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
include Participable include Participable
include Mentionable include Mentionable
...@@ -70,7 +72,7 @@ class Note < ActiveRecord::Base ...@@ -70,7 +72,7 @@ class Note < ActiveRecord::Base
delegate :title, to: :noteable, allow_nil: true delegate :title, to: :noteable, allow_nil: true
validates :note, presence: 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 # Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size } validates :attachment, file_size: { maximum: :max_attachment_size }
...@@ -116,7 +118,7 @@ class Note < ActiveRecord::Base ...@@ -116,7 +118,7 @@ class Note < ActiveRecord::Base
after_initialize :ensure_discussion_id after_initialize :ensure_discussion_id
before_validation :nullify_blank_type, :nullify_blank_line_code before_validation :nullify_blank_type, :nullify_blank_line_code
before_validation :set_discussion_id, on: :create 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_save :expire_etag_cache
after_destroy :expire_etag_cache after_destroy :expire_etag_cache
...@@ -214,6 +216,10 @@ class Note < ActiveRecord::Base ...@@ -214,6 +216,10 @@ class Note < ActiveRecord::Base
noteable.is_a?(PersonalSnippet) noteable.is_a?(PersonalSnippet)
end end
def for_project_noteable?
!for_personal_snippet?
end
def skip_project_check? def skip_project_check?
for_personal_snippet? for_personal_snippet?
end end
......
class IssuableEntity < Grape::Entity class IssuableEntity < Grape::Entity
include RequestAwareEntity
expose :id expose :id
expose :iid expose :iid
expose :author_id expose :author_id
expose :description expose :description
expose :lock_version expose :lock_version
expose :milestone_id expose :milestone_id
expose :state
expose :title expose :title
expose :updated_by_id expose :updated_by_id
expose :created_at expose :created_at
expose :updated_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 :milestone, using: API::Entities::Milestone
expose :labels, using: LabelEntity expose :labels, using: LabelEntity
end end
class IssueEntity < IssuableEntity class IssueEntity < IssuableEntity
include RequestAwareEntity include TimeTrackableEntity
expose :state
expose :deleted_at
expose :branch_name expose :branch_name
expose :confidential expose :confidential
expose :discussion_locked expose :discussion_locked
......
class MergeRequestEntity < IssuableEntity class MergeRequestEntity < IssuableEntity
include RequestAwareEntity include TimeTrackableEntity
expose :state
expose :deleted_at
expose :in_progress_merge_commit_sha expose :in_progress_merge_commit_sha
expose :merge_commit_sha expose :merge_commit_sha
expose :merge_error expose :merge_error
......
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
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
...@@ -3,56 +3,10 @@ class IssuableBaseService < BaseService ...@@ -3,56 +3,10 @@ class IssuableBaseService < BaseService
private 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, issuable.time_spent_user)
end
def create_discussion_lock_note(issuable)
SystemNoteService.discussion_lock(issuable, current_user)
end
def filter_params(issuable) def filter_params(issuable)
ability_name = :"admin_#{issuable.to_ability_name}" 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(:milestone_id)
params.delete(:labels) params.delete(:labels)
params.delete(:add_label_ids) params.delete(:add_label_ids)
...@@ -235,15 +189,14 @@ class IssuableBaseService < BaseService ...@@ -235,15 +189,14 @@ class IssuableBaseService < BaseService
# We have to perform this check before saving the issuable as Rails resets # We have to perform this check before saving the issuable as Rails resets
# the changed fields upon calling #save. # 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 } if issuable.with_transaction_returning_status { issuable.save }
# We do not touch as it will affect a update on updated_at field # We do not touch as it will affect a update on updated_at field
ActiveRecord::Base.no_touching do 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 end
change_discussion_lock(issuable)
handle_changes( handle_changes(
issuable, issuable,
old_labels: old_labels, old_labels: old_labels,
...@@ -302,12 +255,6 @@ class IssuableBaseService < BaseService ...@@ -302,12 +255,6 @@ class IssuableBaseService < BaseService
end end
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) def toggle_award(issuable)
award = params.delete(:emoji_award) award = params.delete(:emoji_award)
if award if award
...@@ -330,35 +277,17 @@ class IssuableBaseService < BaseService ...@@ -330,35 +277,17 @@ class IssuableBaseService < BaseService
attrs_changed || labels_changed || assignees_changed attrs_changed || labels_changed || assignees_changed
end 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: []) def invalidate_cache_counts(issuable, users: [])
users.each do |user| users.each do |user|
user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts") # rubocop:disable GitlabSecurity/PublicSend user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts") # rubocop:disable GitlabSecurity/PublicSend
end end
end end
# override if needed
def handle_changes(issuable, options)
end
# override if needed
def execute_hooks(issuable, action = 'open', params = {})
end
end end
...@@ -27,10 +27,6 @@ module Issues ...@@ -27,10 +27,6 @@ module Issues
todo_service.update_issue(issue, current_user, old_mentioned_users) todo_service.update_issue(issue, current_user, old_mentioned_users)
end end
if issue.previous_changes.include?('milestone_id')
create_milestone_note(issue)
end
if issue.assignees != old_assignees if issue.assignees != old_assignees
create_assignee_note(issue, old_assignees) create_assignee_note(issue, old_assignees)
notification_service.reassigned_issue(issue, current_user, old_assignees) notification_service.reassigned_issue(issue, current_user, old_assignees)
......
...@@ -52,10 +52,6 @@ module MergeRequests ...@@ -52,10 +52,6 @@ module MergeRequests
reset_approvals(merge_request) reset_approvals(merge_request)
end end
if merge_request.previous_changes.include?('milestone_id')
create_milestone_note(merge_request)
end
if merge_request.previous_changes.include?('assignee_id') if merge_request.previous_changes.include?('assignee_id')
create_assignee_note(merge_request) create_assignee_note(merge_request)
notification_service.reassigned_merge_request(merge_request, current_user) notification_service.reassigned_merge_request(merge_request, current_user)
...@@ -131,5 +127,11 @@ module MergeRequests ...@@ -131,5 +127,11 @@ module MergeRequests
end end
end 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
end end
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
%li{ class: [merge_request_button_visibility(@merge_request, true), 'js-close-item'] } %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' = 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) } %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 - 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" = link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit"
......
---
title: View/edit epic at group level
merge_request: 3126
author:
type: added
...@@ -55,6 +55,10 @@ module Gitlab ...@@ -55,6 +55,10 @@ module Gitlab
#{config.root}/ee/app/views #{config.root}/ee/app/views
]) ])
config.helpers_paths.push(*%W[
#{config.root}/ee/app/helpers
])
# Only load the plugins named here, in the order given (default is alphabetical). # Only load the plugins named here, in the order given (default is alphabetical).
# :all can be used as a placeholder for all plugins not explicitly named. # :all can be used as a placeholder for all plugins not explicitly named.
# config.plugins = [ :exception_notification, :ssl_requirement, :all ] # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
......
...@@ -81,6 +81,11 @@ constraints(GroupUrlConstrainer.new) do ...@@ -81,6 +81,11 @@ constraints(GroupUrlConstrainer.new) do
## EE-specific ## EE-specific
resources :billings, only: [:index] resources :billings, only: [:index]
resources :boards, only: [:index, :show, :create, :update, :destroy] resources :boards, only: [:index, :show, :create, :update, :destroy]
resources :epics do
member do
get :realtime_changes
end
end
end end
## EE-specific ## EE-specific
......
...@@ -40,6 +40,7 @@ var config = { ...@@ -40,6 +40,7 @@ var config = {
diff_notes: './diff_notes/diff_notes_bundle.js', diff_notes: './diff_notes/diff_notes_bundle.js',
environments: './environments/environments_bundle.js', environments: './environments/environments_bundle.js',
environments_folder: './environments/folder/environments_folder_bundle.js', environments_folder: './environments/folder/environments_folder_bundle.js',
epic_show: 'ee/epics/epic_show/epic_show_bundle.js',
filtered_search: './filtered_search/filtered_search_bundle.js', filtered_search: './filtered_search/filtered_search_bundle.js',
graphs: './graphs/graphs_bundle.js', graphs: './graphs/graphs_bundle.js',
graphs_charts: './graphs/graphs_charts.js', graphs_charts: './graphs/graphs_charts.js',
......
Gitlab::Seeder.quiet do
Group.all.each do |group|
5.times do
epic_params = {
title: FFaker::Lorem.sentence(6),
description: FFaker::Lorem.paragraphs(3).join("\n\n"),
author: group.users.sample,
group: group
}
Epic.create!(epic_params)
print '.'
end
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class CreateEpics < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
create_table :epics do |t|
t.references :milestone, index: { name: 'index_milestone' }, foreign_key: { on_delete: :nullify }
t.integer :group_id, null: false, index: true
t.integer :author_id, null: false, index: true
t.integer :assignee_id, index: true
t.integer :iid, null: false, index: true
t.integer :cached_markdown_version, limit: 4
t.integer :updated_by_id
t.integer :last_edited_by_id
t.integer :lock_version
t.date :start_date
t.date :end_date
t.datetime_with_timezone :last_edited_at
t.timestamps_with_timezone
t.string :title, null: false
t.string :title_html, null: false
t.text :description
t.text :description_html
end
add_concurrent_foreign_key :epics, :namespaces, column: :group_id
add_concurrent_foreign_key :epics, :users, column: :author_id
add_concurrent_foreign_key :epics, :users, column: :assignee_id, on_delete: :nullify
create_table :epic_metrics do |t|
t.references :epic, index: { name: "index_epic_metrics" }, foreign_key: { on_delete: :cascade }, null: false
t.timestamps_with_timezone
end
end
def down
remove_foreign_key :epics, column: :group_id
remove_foreign_key :epics, column: :author_id
remove_foreign_key :epics, column: :assignee_id
remove_foreign_key :epics, column: :milestone_id
remove_foreign_key :epic_metrics, column: :epic_id
drop_table :epics
drop_table :epic_metrics
end
end
...@@ -640,6 +640,41 @@ ActiveRecord::Schema.define(version: 20171017145932) do ...@@ -640,6 +640,41 @@ ActiveRecord::Schema.define(version: 20171017145932) do
add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", unique: true, using: :btree add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", unique: true, using: :btree
add_index "environments", ["project_id", "slug"], name: "index_environments_on_project_id_and_slug", unique: true, using: :btree add_index "environments", ["project_id", "slug"], name: "index_environments_on_project_id_and_slug", unique: true, using: :btree
create_table "epic_metrics", force: :cascade do |t|
t.integer "epic_id", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
end
add_index "epic_metrics", ["epic_id"], name: "index_epic_metrics", using: :btree
create_table "epics", force: :cascade do |t|
t.integer "milestone_id"
t.integer "group_id", null: false
t.integer "author_id", null: false
t.integer "assignee_id"
t.integer "iid", null: false
t.integer "cached_markdown_version"
t.integer "updated_by_id"
t.integer "last_edited_by_id"
t.integer "lock_version"
t.date "start_date"
t.date "end_date"
t.datetime_with_timezone "last_edited_at"
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.string "title", null: false
t.string "title_html", null: false
t.text "description"
t.text "description_html"
end
add_index "epics", ["assignee_id"], name: "index_epics_on_assignee_id", using: :btree
add_index "epics", ["author_id"], name: "index_epics_on_author_id", using: :btree
add_index "epics", ["group_id"], name: "index_epics_on_group_id", using: :btree
add_index "epics", ["iid"], name: "index_epics_on_iid", using: :btree
add_index "epics", ["milestone_id"], name: "index_milestone", using: :btree
create_table "events", force: :cascade do |t| create_table "events", force: :cascade do |t|
t.integer "project_id" t.integer "project_id"
t.integer "author_id", null: false t.integer "author_id", null: false
...@@ -2193,6 +2228,11 @@ ActiveRecord::Schema.define(version: 20171017145932) do ...@@ -2193,6 +2228,11 @@ ActiveRecord::Schema.define(version: 20171017145932) do
add_foreign_key "deploy_keys_projects", "projects", name: "fk_58a901ca7e", on_delete: :cascade add_foreign_key "deploy_keys_projects", "projects", name: "fk_58a901ca7e", on_delete: :cascade
add_foreign_key "deployments", "projects", name: "fk_b9a3851b82", on_delete: :cascade add_foreign_key "deployments", "projects", name: "fk_b9a3851b82", on_delete: :cascade
add_foreign_key "environments", "projects", name: "fk_d1c8c1da6a", on_delete: :cascade add_foreign_key "environments", "projects", name: "fk_d1c8c1da6a", on_delete: :cascade
add_foreign_key "epic_metrics", "epics", on_delete: :cascade
add_foreign_key "epics", "milestones", on_delete: :nullify
add_foreign_key "epics", "namespaces", column: "group_id", name: "fk_f081aa4489", on_delete: :cascade
add_foreign_key "epics", "users", column: "assignee_id", name: "fk_dccd3f98fc", on_delete: :nullify
add_foreign_key "epics", "users", column: "author_id", name: "fk_3654b61b03", on_delete: :cascade
add_foreign_key "events", "projects", on_delete: :cascade add_foreign_key "events", "projects", on_delete: :cascade
add_foreign_key "events", "users", column: "author_id", name: "fk_edfd187b6f", on_delete: :cascade add_foreign_key "events", "users", column: "author_id", name: "fk_edfd187b6f", on_delete: :cascade
add_foreign_key "fork_network_members", "fork_networks", on_delete: :cascade add_foreign_key "fork_network_members", "fork_networks", on_delete: :cascade
......
<script>
import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import timeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
name: 'epicHeader',
props: {
author: {
type: Object,
required: true,
validator: value => value.url && value.username && value.name,
},
created: {
type: String,
required: true,
},
},
directives: {
tooltip,
},
components: {
userAvatarLink,
timeagoTooltip,
},
};
</script>
<template>
<div class="detail-page-header">
Opened
<timeagoTooltip
:time="created"
/>
by
<strong>
<user-avatar-link
:link-href="author.url"
:img-src="author.src"
:img-size="24"
:tooltipText="author.username"
:username="author.name"
imgCssClasses="avatar-inline"
/>
</strong>
</div>
</template>
<script>
import issuableApp from '~/issue_show/components/app.vue';
import epicHeader from './epic_header.vue';
export default {
name: 'epicShowApp',
props: {
endpoint: {
type: String,
required: true,
},
canUpdate: {
required: true,
type: Boolean,
},
canDestroy: {
required: true,
type: Boolean,
},
markdownPreviewPath: {
type: String,
required: true,
},
markdownDocsPath: {
type: String,
required: true,
},
groupPath: {
type: String,
required: true,
},
initialTitleHtml: {
type: String,
required: true,
},
initialTitleText: {
type: String,
required: true,
},
initialDescriptionHtml: {
type: String,
required: false,
default: '',
},
initialDescriptionText: {
type: String,
required: false,
default: '',
},
created: {
type: String,
required: true,
},
author: {
type: Object,
required: true,
},
},
components: {
epicHeader,
issuableApp,
},
created() {
// Epics specific configuration
this.issuableRef = '';
this.projectPath = this.groupPath;
this.projectNamespace = '';
},
};
</script>
<template>
<div>
<epic-header
:author="author"
:created="created"
/>
<div class="issuable-details detail-page-description content-block">
<issuable-app
:can-update="canUpdate"
:can-destroy="canDestroy"
:endpoint="endpoint"
:issuable-ref="issuableRef"
:initial-title-html="initialTitleHtml"
:initial-title-text="initialTitleText"
:initial-description-html="initialDescriptionHtml"
:initial-description-text="initialDescriptionText"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:project-path="projectPath"
:project-namespace="projectNamespace"
:show-inline-edit-button="true"
/>
</div>
</div>
</template>
import Vue from 'vue';
import EpicShowApp from './components/epic_show_app.vue';
document.addEventListener('DOMContentLoaded', () => {
const el = document.querySelector('#epic-show-app');
const metaData = JSON.parse(el.dataset.meta);
const initialData = JSON.parse(el.dataset.initial);
const props = Object.assign({}, initialData, metaData, {
// Current iteration does not enable users
// to delete epics
canDestroy: false,
});
return new Vue({
el,
components: {
'epic-show-app': EpicShowApp,
},
render: createElement => createElement('epic-show-app', {
props,
}),
});
});
class Groups::EpicsController < Groups::ApplicationController
include IssuableActions
before_action :epic
before_action :authorize_update_issuable!, only: :update
skip_before_action :labels
private
def epic
@issuable = @epic ||= @group.epics.find_by(iid: params[:id])
return render_404 unless can?(current_user, :read_epic, @epic)
@epic
end
alias_method :issuable, :epic
def epic_params
params.require(:epic).permit(*epic_params_attributes)
end
def epic_params_attributes
%i[
title
description
start_date
end_date
]
end
def serializer
EpicSerializer.new(current_user: current_user)
end
def update_service
Epics::UpdateService.new(nil, current_user, epic_params)
end
def show_view
'groups/ee/epics/show'
end
end
...@@ -23,5 +23,9 @@ module EE ...@@ -23,5 +23,9 @@ module EE
geo_primary_http_url_to_repo(project) geo_primary_http_url_to_repo(project)
end end
end end
def epic_path(entity, *args)
group_epic_path(entity.group, entity, *args)
end
end end
end end
module EpicsHelper
def epic_meta_data
author = @epic.author
data = {
created: @epic.created_at,
author: {
name: author.name,
url: user_path(author),
username: "@#{author.username}",
src: avatar_icon(@epic.author)
}
}
data.to_json
end
end
module EE
module Epic
extend ActiveSupport::Concern
prepended do
include InternalId
include Issuable
include Noteable
belongs_to :assignee, class_name: "User"
belongs_to :group
validates :group, presence: true
end
def assignees
Array(assignee)
end
def project
nil
end
def supports_weight?
false
end
end
end
...@@ -8,6 +8,7 @@ module EE ...@@ -8,6 +8,7 @@ module EE
included do included do
has_many :boards has_many :boards
has_many :epics
state_machine :ldap_sync_status, namespace: :ldap_sync, initial: :ready do state_machine :ldap_sync_status, namespace: :ldap_sync, initial: :ready do
state :ready state :ready
......
module EE
module Note
extend ActiveSupport::Concern
def for_epic?
noteable.is_a?(Epic)
end
def for_project_noteable?
!for_epic? && super
end
end
end
...@@ -21,6 +21,8 @@ module EE ...@@ -21,6 +21,8 @@ module EE
delegate :shared_runners_minutes_limit, :shared_runners_minutes_limit=, delegate :shared_runners_minutes_limit, :shared_runners_minutes_limit=,
to: :namespace to: :namespace
has_many :epics, foreign_key: :author_id
has_many :assigned_epics, foreign_key: :assignee_id, class_name: "Epic"
has_many :path_locks, dependent: :destroy # rubocop: disable Cop/ActiveRecordDependent has_many :path_locks, dependent: :destroy # rubocop: disable Cop/ActiveRecordDependent
has_many :approvals, dependent: :destroy # rubocop: disable Cop/ActiveRecordDependent has_many :approvals, dependent: :destroy # rubocop: disable Cop/ActiveRecordDependent
......
class Epic::Metrics < ActiveRecord::Base
belongs_to :epic
def record!
self.save
end
end
...@@ -17,6 +17,19 @@ module EE ...@@ -17,6 +17,19 @@ module EE
.allow_group_owners_to_manage_ldap .allow_group_owners_to_manage_ldap
end end
rule { public_group }.enable :read_epic
rule { logged_in_viewable }.enable :read_epic
rule { guest }.enable :read_epic
rule { reporter }.policy do
enable :create_epic
enable :admin_epic
enable :update_epic
enable :destroy_epic
end
rule { auditor }.enable :read_group rule { auditor }.enable :read_group
rule { admin | (can_owners_manage_ldap & owner) }.enable :admin_ldap_group_links rule { admin | (can_owners_manage_ldap & owner) }.enable :admin_ldap_group_links
......
class EpicPolicy < BasePolicy
delegate { @subject.group }
end
class EpicEntity < IssuableEntity
expose :group_id
expose :start_date
expose :end_date
expose :web_url do |epic|
group_epic_path(epic.group, epic)
end
end
class EpicSerializer < BaseSerializer
entity EpicEntity
end
module Epics
class UpdateService < ::IssuableBaseService
def execute(epic)
update(epic)
end
end
end
- @no_breadcrumb_container = false
- @no_container = false
- @content_class = "limit-container-width" unless fluid_layout
-# TODO: Move this to @epic.to_reference when implementing gitlab-ee#3853
- epic_reference = "&#{@epic.iid}"
- add_to_breadcrumbs _("Epics"), group_epics_path(@group)
- breadcrumb_title epic_reference
- page_title "#{@epic.title} (#{epic_reference})", _("Epics")
- page_description @epic.description
- page_card_attributes @epic.card_attributes
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'epic_show'
#epic-show-app{ data: { initial: issuable_initial_data(@epic), meta: epic_meta_data } }
...@@ -95,7 +95,7 @@ module Banzai ...@@ -95,7 +95,7 @@ module Banzai
end end
def call def call
return doc if project.nil? return doc unless project || group
ref_pattern = object_class.reference_pattern ref_pattern = object_class.reference_pattern
link_pattern = object_class.link_reference_pattern link_pattern = object_class.link_reference_pattern
...@@ -288,10 +288,14 @@ module Banzai ...@@ -288,10 +288,14 @@ module Banzai
end end
def current_project_path def current_project_path
return unless project
@current_project_path ||= project.full_path @current_project_path ||= project.full_path
end end
def current_project_namespace_path def current_project_namespace_path
return unless project
@current_project_namespace_path ||= project.namespace.full_path @current_project_namespace_path ||= project.namespace.full_path
end end
......
...@@ -55,6 +55,10 @@ module Banzai ...@@ -55,6 +55,10 @@ module Banzai
context[:project] context[:project]
end end
def group
context[:group]
end
def skip_project_check? def skip_project_check?
context[:skip_project_check] context[:skip_project_check]
end end
......
...@@ -24,7 +24,7 @@ module Banzai ...@@ -24,7 +24,7 @@ module Banzai
end end
def call 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 = User.reference_pattern
ref_pattern_start = /\A#{ref_pattern}\z/ ref_pattern_start = /\A#{ref_pattern}\z/
...@@ -101,19 +101,12 @@ module Banzai ...@@ -101,19 +101,12 @@ module Banzai
end end
def link_to_all(link_content: nil) def link_to_all(link_content: nil)
project = context[:project]
author = context[:author] author = context[:author]
if author && !project.team.member?(author) if author && !team_member?(author)
link_content link_content
else else
url = urls.project_url(project, parent_url(link_content, author)
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')
end end
end end
...@@ -144,6 +137,35 @@ module Banzai ...@@ -144,6 +137,35 @@ module Banzai
def link_tag(url, data, link_content, title) def link_tag(url, data, link_content, title)
%(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{link_content}</a>) %(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{link_content}</a>)
end 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 end
end end
...@@ -248,6 +248,45 @@ describe Projects::IssuesController do ...@@ -248,6 +248,45 @@ describe Projects::IssuesController do
end end
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 describe 'Confidential Issues' do
let(:project) { create(:project_empty_repo, :public) } let(:project) { create(:project_empty_repo, :public) }
let(:assignee) { create(:assignee) } let(:assignee) { create(:assignee) }
......
...@@ -186,17 +186,23 @@ describe Projects::MergeRequestsController do ...@@ -186,17 +186,23 @@ describe Projects::MergeRequestsController do
end end
describe 'PUT update' do 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 context 'changing the assignee' do
it 'limits the attributes exposed on the assignee' do it 'limits the attributes exposed on the assignee' do
assignee = create(:user) assignee = create(:user)
project.add_developer(assignee) project.add_developer(assignee)
put :update, update_merge_request({ assignee_id: assignee.id }, format: :json)
namespace_id: project.namespace.to_param,
project_id: project,
id: merge_request.iid,
merge_request: { assignee_id: assignee.id },
format: :json
body = JSON.parse(response.body) body = JSON.parse(response.body)
expect(body['assignee'].keys) expect(body['assignee'].keys)
...@@ -204,6 +210,20 @@ describe Projects::MergeRequestsController do ...@@ -204,6 +210,20 @@ describe Projects::MergeRequestsController do
end end
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 context 'there is no source project' do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:forked_project) { fork_project_with_submodules(project) } let(:forked_project) { fork_project_with_submodules(project) }
...@@ -214,13 +234,7 @@ describe Projects::MergeRequestsController do ...@@ -214,13 +234,7 @@ describe Projects::MergeRequestsController do
end end
it 'closes MR without errors' do it 'closes MR without errors' do
post :update, update_merge_request(state_event: 'close')
namespace_id: project.namespace,
project_id: project,
id: merge_request.iid,
merge_request: {
state_event: 'close'
}
expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request]) 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 expect(merge_request.reload.closed?).to be_truthy
...@@ -229,13 +243,7 @@ describe Projects::MergeRequestsController do ...@@ -229,13 +243,7 @@ describe Projects::MergeRequestsController do
it 'allows editing of a closed merge request' do it 'allows editing of a closed merge request' do
merge_request.close! merge_request.close!
put :update, update_merge_request(title: 'New title')
namespace_id: project.namespace,
project_id: project,
id: merge_request.iid,
merge_request: {
title: 'New title'
}
expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request]) 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' expect(merge_request.reload.title).to eq 'New title'
...@@ -244,13 +252,7 @@ describe Projects::MergeRequestsController do ...@@ -244,13 +252,7 @@ describe Projects::MergeRequestsController do
it 'does not allow to update target branch closed merge request' do it 'does not allow to update target branch closed merge request' do
merge_request.close! merge_request.close!
put :update, update_merge_request(target_branch: 'new_branch')
namespace_id: project.namespace,
project_id: project,
id: merge_request.iid,
merge_request: {
target_branch: 'new_branch'
}
expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch } expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch }
end end
......
require 'spec_helper'
describe Groups::EpicsController do
let(:group) { create(:group, :private) }
let(:epic) { create(:epic, group: group) }
let(:user) { create(:user) }
before do
sign_in(user)
end
describe 'GET #show' do
def show_epic(format = :html)
get :show, group_id: group, id: epic.to_param, format: format
end
context 'when format is HTML' do
it 'renders template' do
group.add_developer(user)
show_epic
expect(response.content_type).to eq 'text/html'
expect(response).to render_template 'groups/ee/epics/show'
end
context 'with unauthorized user' do
it 'returns a not found 404 response' do
show_epic
expect(response).to have_http_status(404)
expect(response.content_type).to eq 'text/html'
end
end
end
context 'when format is JSON' do
it 'returns epic' do
group.add_developer(user)
show_epic(:json)
expect(response).to have_http_status(200)
expect(response).to match_response_schema('entities/epic')
end
context 'with unauthorized user' do
it 'returns a not found 404 response' do
show_epic(:json)
expect(response).to have_http_status(404)
expect(response.content_type).to eq 'application/json'
end
end
end
end
describe 'PUT #update' do
before do
group.add_user(user, :developer)
put :update, group_id: group, id: epic.to_param, epic: { title: 'New title' }, format: :json
end
it 'returns status 200' do
expect(response.status).to eq(200)
end
it 'updates the epic correctly' do
expect(epic.reload.title).to eq('New title')
end
end
describe 'GET #realtime_changes' do
subject { get :realtime_changes, group_id: group, id: epic.to_param }
it 'returns epic' do
group.add_user(user, :developer)
subject
expect(response.content_type).to eq 'application/json'
expect(JSON.parse(response.body)).to include('title_text', 'title', 'description', 'description_text')
end
context 'with unauthorized user' do
it 'returns a not found 404 response' do
subject
expect(response).to have_http_status(404)
end
end
end
end
require 'spec_helper'
feature 'Update Epic', :js do
let(:user) { create(:user) }
let(:group) { create(:group, :public) }
let(:epic) { create(:epic, group: group) }
before do
sign_in(user)
end
context 'when user who is not a group member displays the epic' do
it 'does not show the Edit button' do
visit group_epic_path(group, epic)
expect(page).not_to have_selector('.btn-edit')
end
end
context 'when user with developer access displays the epic' do
before do
group.add_developer(user)
visit group_epic_path(group, epic)
wait_for_requests
end
it 'updates the issue' do
find('.btn-edit').trigger('click')
fill_in 'issuable-title', with: 'New epic title'
fill_in 'issue-description', with: 'New epic description'
click_button 'Save changes'
expect(find('.issuable-details h2.title')).to have_content('New epic title')
expect(find('.issuable-details .description')).to have_content('New epic description')
end
end
end
require 'spec_helper'
describe EpicsHelper do
include ApplicationHelper
describe '#epic_meta_data' do
it 'returns the correct json' do
user = create(:user)
@epic = create(:epic, author: user)
expect(JSON.parse(epic_meta_data).keys).to match_array(%w[created author])
expect(JSON.parse(epic_meta_data)['author']).to eq({
'name' => user.name,
'url' => "/#{user.username}",
'username' => "@#{user.username}",
'src' => "#{avatar_icon(user)}"
})
end
end
end
require 'spec_helper'
describe Epic do
describe 'associations' do
subject { build(:epic) }
it { is_expected.to belong_to(:author).class_name('User') }
it { is_expected.to belong_to(:group) }
end
describe 'validations' do
subject { create(:epic) }
it { is_expected.to validate_presence_of(:group) }
it { is_expected.to validate_presence_of(:author) }
it { is_expected.to validate_presence_of(:title) }
end
describe 'modules' do
subject { described_class }
it { is_expected.to include_module(InternalId) }
end
end
require 'spec_helper'
describe EpicPolicy do
let(:user) { create(:user) }
def permissions(user, group)
epic = create(:epic, group: group)
described_class.new(user, epic)
end
context 'when an epic is in a private group' do
let(:group) { create(:group, :private) }
it 'anonymous user can not read epics' do
expect(permissions(nil, group))
.to be_disallowed(:read_epic, :update_epic, :destroy_epic, :admin_epic, :create_epic)
end
it 'user who is not a group member can not read epics' do
expect(permissions(user, group))
.to be_disallowed(:read_epic, :update_epic, :destroy_epic, :admin_epic, :create_epic)
end
it 'guest group member can only read epics' do
group.add_guest(user)
expect(permissions(user, group)).to be_allowed(:read_epic)
expect(permissions(user, group)).to be_disallowed(:update_epic, :destroy_epic, :admin_epic, :create_epic)
end
it 'reporter group member can manage epics' do
group.add_reporter(user)
expect(permissions(user, group))
.to be_allowed(:read_epic, :update_epic, :destroy_epic, :admin_epic, :create_epic)
end
end
context 'when an epic is in an internal group' do
let(:group) { create(:group, :internal) }
it 'anonymous user can not read epics' do
expect(permissions(nil, group))
.to be_disallowed(:read_epic, :update_epic, :destroy_epic, :admin_epic, :create_epic)
end
it 'user who is not a group member can only read epics' do
expect(permissions(user, group)).to be_allowed(:read_epic)
expect(permissions(user, group)).to be_disallowed(:update_epic, :destroy_epic, :admin_epic, :create_epic)
end
it 'guest group member can only read epics' do
group.add_guest(user)
expect(permissions(user, group)).to be_allowed(:read_epic)
expect(permissions(user, group)).to be_disallowed(:update_epic, :destroy_epic, :admin_epic, :create_epic)
end
it 'reporter group member can manage epics' do
group.add_reporter(user)
expect(permissions(user, group))
.to be_allowed(:read_epic, :update_epic, :destroy_epic, :admin_epic, :create_epic)
end
end
context 'when an epic is in a public group' do
let(:group) { create(:group, :public) }
it 'anonymous user can only read epics' do
expect(permissions(nil, group)).to be_allowed(:read_epic)
expect(permissions(nil, group)).to be_disallowed(:update_epic, :destroy_epic, :admin_epic, :create_epic)
end
it 'user who is not a group member can only read epics' do
expect(permissions(user, group)).to be_allowed(:read_epic)
expect(permissions(user, group)).to be_disallowed(:update_epic, :destroy_epic, :admin_epic, :create_epic)
end
it 'guest group member can only read epics' do
group.add_guest(user)
expect(permissions(user, group)).to be_allowed(:read_epic)
expect(permissions(user, group)).to be_disallowed(:update_epic, :destroy_epic, :admin_epic, :create_epic)
end
it 'reporter group member can manage epics' do
group.add_reporter(user)
expect(permissions(user, group))
.to be_allowed(:read_epic, :update_epic, :destroy_epic, :admin_epic, :create_epic)
end
end
end
require 'spec_helper'
describe EpicEntity do
let(:group) { create(:group) }
let(:resource) { create(:epic, group: group) }
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 epic specific attributes' do
expect(subject).to include(:start_date, :end_date, :group_id, :web_url)
end
end
require 'spec_helper'
describe Epics::UpdateService do
let(:group) { create(:group, :internal)}
let(:user) { create(:user) }
let(:epic) { create(:epic, group: group) }
describe '#execute' do
def update_epic(opts)
described_class.new(nil, user, opts).execute(epic)
end
context 'multiple values update' do
let(:opts) do
{
title: 'New title',
description: 'New description',
start_date: '2017-01-09',
end_date: '2017-10-21'
}
end
it 'updates the epic correctly' do
update_epic(opts)
expect(epic).to be_valid
expect(epic.title).to eq(opts[:title])
expect(epic.description).to eq(opts[:description])
expect(epic.start_date).to eq(Date.strptime(opts[:start_date]))
expect(epic.end_date).to eq(Date.strptime(opts[:end_date]))
end
it 'updates the last_edited_at value' do
expect { update_epic(opts) }.to change { epic.last_edited_at }
end
end
context 'when title has changed' do
it 'creates system note about title change' do
expect { update_epic(title: 'New title') }.to change { Note.count }.from(0).to(1)
note = Note.last
expect(note.note).to start_with('changed title')
expect(note.noteable).to eq(epic)
end
end
context 'when description has changed' do
it 'creates system note about description change' do
expect { update_epic(description: 'New description') }.to change { Note.count }.from(0).to(1)
note = Note.last
expect(note.note).to start_with('changed the description')
expect(note.noteable).to eq(epic)
end
end
end
end
FactoryGirl.define do
factory :epic do
title { generate(:title) }
group
author
end
end
{
"type": "object",
"properties" : {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"author_id": { "type": "integer" },
"group_id": { "type": ["integer", "null"] },
"title": { "type": "string" },
"lock_version": { "type": ["string", "null"] },
"description": { "type": ["string" , "null"] },
"confidential": { "type": "boolean" },
"milestone_id": { "type": ["string", "null"] },
"state": { "type": "string" },
"start_date": { "type": ["date", "null"] },
"end_date": { "type": ["date", "null"] },
"updated_by_id": { "type": ["string", "null"] },
"created_at": { "type": "string" },
"updated_at": { "type": "string" },
"deleted_at": { "type": ["string", "null"] },
"web_url": { "type": "string" },
"milestone": { "type": ["object", "null"] },
"labels": { "type": ["array", "null"] },
"group": {
"id": { "type": "integer" },
"path": { "type": "string" }
},
"assignee": {
"id": { "type": "integer" },
"name": { "type": "string" },
"username": { "type": "string" },
"avatar_url": { "type": "uri" }
},
"assignees": {
"type": "array",
"items": {
"type": ["object", "null"],
"required": [
"id",
"name",
"username",
"avatar_url"
],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"username": { "type": "string" },
"avatar_url": { "type": "uri" }
}
}
}
},
"additionalProperties": false
}
...@@ -63,4 +63,30 @@ describe GitlabRoutingHelper do ...@@ -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) } it { expect(resend_invite_group_member_path(group_member)).to eq resend_invite_group_group_member_path(group_member.source, group_member) }
end end
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 end
...@@ -159,4 +159,58 @@ describe IssuablesHelper do ...@@ -159,4 +159,58 @@ describe IssuablesHelper do
end end
end 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
it 'returns the correct json for an epic' do
epic = create(:epic, author: user, description: 'epic text')
@group = epic.group
expected_data = {
'endpoint' => "/groups/#{@group.full_path}/-/epics/#{epic.iid}",
'canUpdate' => true,
'canDestroy' => true,
'issuableRef' => nil,
'markdownPreviewPath' => "/groups/#{@group.full_path}/preview_markdown",
'markdownDocsPath' => '/help/user/markdown',
'issuableTemplates' => nil,
'groupPath' => @group.path,
'initialTitleHtml' => epic.title,
'initialTitleText' => epic.title,
'initialDescriptionHtml' => '<p dir="auto">epic text</p>',
'initialDescriptionText' => 'epic text',
'initialTaskStatus' => '0 of 0 tasks completed'
}
expect(JSON.parse(helper.issuable_initial_data(epic))).to eq(expected_data)
end
end
end end
import Vue from 'vue';
import epicHeader from 'ee/epics/epic_show/components/epic_header.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import { headerProps } from '../mock_data';
describe('epicHeader', () => {
let vm;
const { author } = headerProps;
beforeEach(() => {
const EpicHeader = Vue.extend(epicHeader);
vm = mountComponent(EpicHeader, headerProps);
});
it('should render timeago tooltip', () => {
expect(vm.$el.querySelector('time')).toBeDefined();
});
it('should link to author url', () => {
expect(vm.$el.querySelector('a').href).toEqual(author.url);
});
it('should render author avatar', () => {
expect(vm.$el.querySelector('img').src).toEqual(author.src);
});
it('should render author name', () => {
expect(vm.$el.querySelector('.user-avatar-link').innerText.trim()).toEqual(author.name);
});
it('should render username tooltip', () => {
expect(vm.$el.querySelector('.user-avatar-link span').dataset.originalTitle).toEqual(author.username);
});
});
import Vue from 'vue';
import epicShowApp from 'ee/epics/epic_show/components/epic_show_app.vue';
import epicHeader from 'ee/epics/epic_show/components/epic_header.vue';
import issuableApp from '~/issue_show/components/app.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import { props } from '../mock_data';
import issueShowData from '../../../issue_show/mock_data';
describe('EpicShowApp', () => {
let vm;
let headerVm;
let issuableAppVm;
const interceptor = (request, next) => {
next(request.respondWith(JSON.stringify(issueShowData.initialRequest), {
status: 200,
}));
};
beforeEach(() => {
Vue.http.interceptors.push(interceptor);
const {
canUpdate,
canDestroy,
endpoint,
initialTitleHtml,
initialTitleText,
markdownPreviewPath,
markdownDocsPath,
author,
created,
} = props;
const EpicShowApp = Vue.extend(epicShowApp);
vm = mountComponent(EpicShowApp, props);
const EpicHeader = Vue.extend(epicHeader);
headerVm = mountComponent(EpicHeader, {
author,
created,
});
const IssuableApp = Vue.extend(issuableApp);
issuableAppVm = mountComponent(IssuableApp, {
canUpdate,
canDestroy,
endpoint,
issuableRef: '',
initialTitleHtml,
initialTitleText,
initialDescriptionHtml: '',
initialDescriptionText: '',
markdownPreviewPath,
markdownDocsPath,
projectPath: props.groupPath,
projectNamespace: '',
showInlineEditButton: true,
});
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
});
it('should render epic-header', () => {
expect(vm.$el.innerHTML.indexOf(headerVm.$el.innerHTML) !== -1).toEqual(true);
});
it('should render issuable-app', () => {
expect(vm.$el.innerHTML.indexOf(issuableAppVm.$el.innerHTML) !== -1).toEqual(true);
});
});
export const contentProps = {
endpoint: '',
canUpdate: true,
canDestroy: false,
markdownPreviewPath: '',
markdownDocsPath: '',
groupPath: '',
initialTitleHtml: '',
initialTitleText: '',
};
export const headerProps = {
author: {
url: `${gl.TEST_HOST}/url`,
src: `${gl.TEST_HOST}/image`,
username: '@root',
name: 'Administrator',
},
created: (new Date()).toISOString(),
};
export const props = Object.assign({}, contentProps, headerProps);
...@@ -74,7 +74,7 @@ describe('Title component', () => { ...@@ -74,7 +74,7 @@ describe('Title component', () => {
}); });
}); });
describe('inline edit button', () => { describe('show inline edit button', () => {
beforeEach(() => { beforeEach(() => {
spyOn(eventHub, '$emit'); spyOn(eventHub, '$emit');
}); });
......
...@@ -317,6 +317,68 @@ describe Banzai::Filter::IssueReferenceFilter do ...@@ -317,6 +317,68 @@ describe Banzai::Filter::IssueReferenceFilter do
end end
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 describe '#issues_per_project' do
context 'using an internal issue tracker' do context 'using an internal issue tracker' do
it 'returns a Hash containing the issues per project' do it 'returns a Hash containing the issues per project' do
......
...@@ -594,4 +594,16 @@ describe Banzai::Filter::LabelReferenceFilter do ...@@ -594,4 +594,16 @@ describe Banzai::Filter::LabelReferenceFilter do
expect(reference_filter(act).to_html).to eq exp expect(reference_filter(act).to_html).to eq exp
end end
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 end
...@@ -214,4 +214,14 @@ describe Banzai::Filter::MergeRequestReferenceFilter do ...@@ -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>\.\)/) expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)<\/a>\.\)/)
end end
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 end
...@@ -343,4 +343,15 @@ describe Banzai::Filter::MilestoneReferenceFilter do ...@@ -343,4 +343,15 @@ describe Banzai::Filter::MilestoneReferenceFilter do
expect(doc.css('a')).to be_empty expect(doc.css('a')).to be_empty
end end
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 end
...@@ -201,4 +201,14 @@ describe Banzai::Filter::SnippetReferenceFilter do ...@@ -201,4 +201,14 @@ describe Banzai::Filter::SnippetReferenceFilter do
expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/) expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/)
end end
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 end
...@@ -208,6 +208,39 @@ describe Banzai::Filter::UserReferenceFilter do ...@@ -208,6 +208,39 @@ describe Banzai::Filter::UserReferenceFilter do
end end
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 describe '#namespaces' do
it 'returns a Hash containing all Namespaces' do it 'returns a Hash containing all Namespaces' do
document = Nokogiri::HTML.fragment("<p>#{user.to_reference}</p>") document = Nokogiri::HTML.fragment("<p>#{user.to_reference}</p>")
......
...@@ -154,18 +154,6 @@ describe Gitlab::PathRegex do ...@@ -154,18 +154,6 @@ describe Gitlab::PathRegex do
end.uniq end.uniq
end end
let(:ee_paths_after_group_id) do
%w[
audit_events
analytics
ldap
ldap_group_links
notification_setting
pipeline_quota hooks
subgroups
]
end
describe 'TOP_LEVEL_ROUTES' do describe 'TOP_LEVEL_ROUTES' do
it 'includes all the top level namespaces' do it 'includes all the top level namespaces' do
failure_block = lambda do failure_block = lambda do
......
...@@ -10,7 +10,7 @@ describe GroupPolicy do ...@@ -10,7 +10,7 @@ describe GroupPolicy do
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
let(:group) { create(:group) } let(:group) { create(:group) }
let(:reporter_permissions) { [:admin_label] } let(:reporter_permissions) { [:admin_label, :create_epic, :admin_epic] }
let(:developer_permissions) { [:admin_milestones] } let(:developer_permissions) { [:admin_milestones] }
......
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
...@@ -30,8 +30,17 @@ describe MergeRequestEntity do ...@@ -30,8 +30,17 @@ describe MergeRequestEntity do
:assign_to_closing) :assign_to_closing)
end 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 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, :has_conflicts, :has_ci, :merge_path,
:conflict_resolution_path, :conflict_resolution_path,
:cancel_merge_when_pipeline_succeeds_path, :cancel_merge_when_pipeline_succeeds_path,
......
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
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