Commit 36bed425 authored by Imre Farkas's avatar Imre Farkas

Merge branch 'ajk-design-activity-c' into 'master'

Add design activity views

See merge request gitlab-org/gitlab!33534
parents be0ee935 acbf9f56
...@@ -329,13 +329,6 @@ class ApplicationController < ActionController::Base ...@@ -329,13 +329,6 @@ class ApplicationController < ActionController::Base
end end
end end
def event_filter
@event_filter ||=
EventFilter.new(params[:event_filter].presence || cookies[:event_filter]).tap do |new_event_filter|
cookies[:event_filter] = new_event_filter.filter
end
end
# JSON for infinite scroll via Pager object # JSON for infinite scroll via Pager object
def pager_json(partial, count, locals = {}) def pager_json(partial, count, locals = {})
html = render_to_string( html = render_to_string(
......
# frozen_string_literal: true
module FiltersEvents
def event_filter
@event_filter ||= new_event_filter.tap { |ef| cookies[:event_filter] = ef.filter }
end
private
def new_event_filter
active_filter = params[:event_filter].presence || cookies[:event_filter]
EventFilter.new(active_filter)
end
end
...@@ -6,6 +6,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -6,6 +6,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
include OnboardingExperimentHelper include OnboardingExperimentHelper
include SortingHelper include SortingHelper
include SortingPreference include SortingPreference
include FiltersEvents
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) } prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
before_action :set_non_archived_param before_action :set_non_archived_param
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
class DashboardController < Dashboard::ApplicationController class DashboardController < Dashboard::ApplicationController
include IssuableCollectionsAction include IssuableCollectionsAction
include FiltersEvents
prepend_before_action(only: [:issues]) { authenticate_sessionless_user!(:rss) } prepend_before_action(only: [:issues]) { authenticate_sessionless_user!(:rss) }
prepend_before_action(only: [:issues_calendar]) { authenticate_sessionless_user!(:ics) } prepend_before_action(only: [:issues_calendar]) { authenticate_sessionless_user!(:ics) }
......
...@@ -7,6 +7,7 @@ class GroupsController < Groups::ApplicationController ...@@ -7,6 +7,7 @@ class GroupsController < Groups::ApplicationController
include PreviewMarkdown include PreviewMarkdown
include RecordUserLastActivity include RecordUserLastActivity
include SendFileUpload include SendFileUpload
include FiltersEvents
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
respond_to :html respond_to :html
......
...@@ -8,6 +8,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -8,6 +8,7 @@ class ProjectsController < Projects::ApplicationController
include SendFileUpload include SendFileUpload
include RecordUserLastActivity include RecordUserLastActivity
include ImportUrlParams include ImportUrlParams
include FiltersEvents
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) } prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
......
...@@ -46,7 +46,7 @@ class UserRecentEventsFinder ...@@ -46,7 +46,7 @@ class UserRecentEventsFinder
SQL SQL
# Workaround for https://github.com/rails/rails/issues/24193 # Workaround for https://github.com/rails/rails/issues/24193
Event.from([Arel.sql(sql)]) ensure_design_visibility(Event.from([Arel.sql(sql)]))
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
...@@ -59,4 +59,11 @@ class UserRecentEventsFinder ...@@ -59,4 +59,11 @@ class UserRecentEventsFinder
def projects def projects
target_user.project_interactions.to_sql target_user.project_interactions.to_sql
end end
# TODO: remove when the :design_activity_events feature flag is removed.
def ensure_design_visibility(events)
return events if Feature.enabled?(:design_activity_events)
events.not_design
end
end end
...@@ -29,7 +29,11 @@ module EventsHelper ...@@ -29,7 +29,11 @@ module EventsHelper
def event_action_name(event) def event_action_name(event)
target = if event.target_type target = if event.target_type
if event.note? if event.design? || event.design_note?
'design'
elsif event.wiki_page?
'wiki page'
elsif event.note?
event.note_target_type event.note_target_type
else else
event.target_type.titleize.downcase event.target_type.titleize.downcase
...@@ -58,11 +62,30 @@ module EventsHelper ...@@ -58,11 +62,30 @@ module EventsHelper
end end
def event_filter_visible(feature_key) def event_filter_visible(feature_key)
return designs_visible? if feature_key == :designs
return true unless @project return true unless @project
@project.feature_available?(feature_key, current_user) @project.feature_available?(feature_key, current_user)
end end
def designs_visible?
return false unless Feature.enabled?(:design_activity_events)
if @project
design_activity_enabled?(@project)
elsif @group
design_activity_enabled?(@group)
elsif @projects
@projects.with_namespace.include_project_feature.any? { |p| design_activity_enabled?(p) }
else
true
end
end
def design_activity_enabled?(project)
Ability.allowed?(current_user, :read_design_activity, project)
end
def comments_visible? def comments_visible?
event_filter_visible(:repository) || event_filter_visible(:repository) ||
event_filter_visible(:merge_requests) || event_filter_visible(:merge_requests) ||
...@@ -94,6 +117,12 @@ module EventsHelper ...@@ -94,6 +117,12 @@ module EventsHelper
elsif event.milestone? elsif event.milestone?
words << "##{event.target_iid}" if event.target_iid words << "##{event.target_iid}" if event.target_iid
words << "in" words << "in"
elsif event.design?
words << event.design.to_reference
words << "in"
elsif event.wiki_page?
words << event.target_title
words << "in"
elsif event.target elsif event.target
prefix = prefix =
if event.merge_request? if event.merge_request?
...@@ -187,6 +216,15 @@ module EventsHelper ...@@ -187,6 +216,15 @@ module EventsHelper
end end
end end
def event_design_title_html(event)
capture do
concat content_tag(:span, _('design'), class: "event-target-type append-right-4")
concat link_to(event.design.reference_link_text, design_url(event.design),
title: event.target_title,
class: 'has-tooltip event-design event-target-link append-right-4')
end
end
def event_wiki_page_target_url(event) def event_wiki_page_target_url(event)
project_wiki_url(event.project, event.target&.canonical_slug || Wiki::HOMEPAGE) project_wiki_url(event.project, event.target&.canonical_slug || Wiki::HOMEPAGE)
end end
...@@ -214,6 +252,18 @@ module EventsHelper ...@@ -214,6 +252,18 @@ module EventsHelper
sprite_icon(icon_name, size: size) if icon_name sprite_icon(icon_name, size: size) if icon_name
end end
DESIGN_ICONS = {
'created' => 'upload',
'updated' => 'pencil',
'destroyed' => ICON_NAMES_BY_EVENT_TYPE['destroyed'],
'archived' => 'archive'
}.freeze
def design_event_icon(action, size: 24)
icon_name = DESIGN_ICONS[action]
sprite_icon(icon_name, size: size) if icon_name
end
def icon_for_profile_event(event) def icon_for_profile_event(event)
if current_path?('users#show') if current_path?('users#show')
content_tag :div, class: "system-note-image #{event.action_name.parameterize}-icon" do content_tag :div, class: "system-note-image #{event.action_name.parameterize}-icon" do
...@@ -229,6 +279,8 @@ module EventsHelper ...@@ -229,6 +279,8 @@ module EventsHelper
def inline_event_icon(event) def inline_event_icon(event)
unless current_path?('users#show') unless current_path?('users#show')
content_tag :span, class: "system-note-image-inline d-none d-sm-flex append-right-4 #{event.action_name.parameterize}-icon align-self-center" do content_tag :span, class: "system-note-image-inline d-none d-sm-flex append-right-4 #{event.action_name.parameterize}-icon align-self-center" do
next design_event_icon(event.action, size: 14) if event.design?
icon_for_event(event.action_name, size: 14) icon_for_event(event.action_name, size: 14)
end end
end end
...@@ -244,7 +296,7 @@ module EventsHelper ...@@ -244,7 +296,7 @@ module EventsHelper
private private
def design_url(design, opts) def design_url(design, opts = {})
designs_project_issue_url( designs_project_issue_url(
design.project, design.project,
design.issue, design.issue,
......
...@@ -45,9 +45,10 @@ class EventCollection ...@@ -45,9 +45,10 @@ class EventCollection
private private
def apply_feature_flags(events) def apply_feature_flags(events)
return events if ::Feature.enabled?(:wiki_events) events = events.not_wiki_page unless ::Feature.enabled?(:wiki_events)
events = events.not_design unless ::Feature.enabled?(:design_activity_events)
events.not_wiki_page events
end end
def project_events def project_events
......
...@@ -455,6 +455,7 @@ class Project < ApplicationRecord ...@@ -455,6 +455,7 @@ class Project < ApplicationRecord
scope :with_statistics, -> { includes(:statistics) } scope :with_statistics, -> { includes(:statistics) }
scope :with_namespace, -> { includes(:namespace) } scope :with_namespace, -> { includes(:namespace) }
scope :with_import_state, -> { includes(:import_state) } scope :with_import_state, -> { includes(:import_state) }
scope :include_project_feature, -> { includes(:project_feature) }
scope :with_service, ->(service) { joins(service).eager_load(service) } scope :with_service, ->(service) { joins(service).eager_load(service) }
scope :with_shared_runners, -> { where(shared_runners_enabled: true) } scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
scope :with_container_registry, -> { where(container_registry_enabled: true) } scope :with_container_registry, -> { where(container_registry_enabled: true) }
......
...@@ -3,11 +3,11 @@ ...@@ -3,11 +3,11 @@
module FindGroupProjects module FindGroupProjects
extend ActiveSupport::Concern extend ActiveSupport::Concern
def group_projects_for(user:, group:) def group_projects_for(user:, group:, only_owned: true)
GroupProjectsFinder.new( GroupProjectsFinder.new(
group: group, group: group,
current_user: user, current_user: user,
options: { include_subgroups: true, only_owned: true } options: { include_subgroups: true, only_owned: only_owned }
).execute ).execute
end end
end end
...@@ -42,6 +42,14 @@ class GroupPolicy < BasePolicy ...@@ -42,6 +42,14 @@ class GroupPolicy < BasePolicy
@subject.subgroup_creation_level == ::Gitlab::Access::MAINTAINER_SUBGROUP_ACCESS @subject.subgroup_creation_level == ::Gitlab::Access::MAINTAINER_SUBGROUP_ACCESS
end end
condition(:design_management_enabled) do
group_projects_for(user: @user, group: @subject, only_owned: false).any? { |p| p.design_management_enabled? }
end
rule { design_management_enabled }.policy do
enable :read_design_activity
end
rule { public_group }.policy do rule { public_group }.policy do
enable :read_group enable :read_group
enable :read_package enable :read_package
...@@ -70,6 +78,10 @@ class GroupPolicy < BasePolicy ...@@ -70,6 +78,10 @@ class GroupPolicy < BasePolicy
enable :read_board enable :read_board
end end
rule { ~can?(:read_group) }.policy do
prevent :read_design_activity
end
rule { has_access }.enable :read_namespace rule { has_access }.enable :read_namespace
rule { developer }.policy do rule { developer }.policy do
......
...@@ -545,11 +545,13 @@ class ProjectPolicy < BasePolicy ...@@ -545,11 +545,13 @@ class ProjectPolicy < BasePolicy
rule { can?(:read_issue) }.policy do rule { can?(:read_issue) }.policy do
enable :read_design enable :read_design
enable :read_design_activity
end end
# Design abilities could also be prevented in the issue policy. # Design abilities could also be prevented in the issue policy.
rule { design_management_disabled }.policy do rule { design_management_disabled }.policy do
prevent :read_design prevent :read_design
prevent :read_design_activity
prevent :create_design prevent :create_design
prevent :destroy_design prevent :destroy_design
end end
......
...@@ -97,23 +97,16 @@ class EventCreateService ...@@ -97,23 +97,16 @@ class EventCreateService
end end
def save_designs(current_user, create: [], update: []) def save_designs(current_user, create: [], update: [])
created = create.group_by(&:project).flat_map do |project, designs| return [] unless Feature.enabled?(:design_activity_events)
Feature.enabled?(:design_activity_events, project) ? designs : []
end.to_set
updated = update.group_by(&:project).flat_map do |project, designs|
Feature.enabled?(:design_activity_events, project) ? designs : []
end.to_set
return [] if created.empty? && updated.empty?
records = created.zip([:created].cycle) + updated.zip([:updated].cycle) records = create.zip([:created].cycle) + update.zip([:updated].cycle)
return [] if records.empty?
create_record_events(records, current_user) create_record_events(records, current_user)
end end
def destroy_designs(designs, current_user) def destroy_designs(designs, current_user)
designs = designs.select do |design| return [] unless Feature.enabled?(:design_activity_events)
Feature.enabled?(:design_activity_events, design.project)
end
return [] unless designs.present? return [] unless designs.present?
create_record_events(designs.zip([:destroyed].cycle), current_user) create_record_events(designs.zip([:destroyed].cycle), current_user)
......
...@@ -7,6 +7,8 @@ ...@@ -7,6 +7,8 @@
- if event.wiki_page? - if event.wiki_page?
= render "events/event/wiki", event: event = render "events/event/wiki", event: event
- elsif event.design?
= render 'events/event/design', event: event
- elsif event.created_project_action? - elsif event.created_project_action?
= render "events/event/created_project", event: event = render "events/event/created_project", event: event
- elsif event.push_action? - elsif event.push_action?
......
= icon_for_profile_event(event)
= event_user_info(event)
.event-title.d-flex.flex-wrap
= inline_event_icon(event)
%span.event-type.d-inline-block.append-right-4{ class: event.action_name }
= event.action_name
= event_design_title_html(event)
= render "events/event_scope", event: event
...@@ -17,4 +17,6 @@ ...@@ -17,4 +17,6 @@
= event_filter_link EventFilter::COMMENTS, _('Comments'), s_('EventFilterBy|Filter by comments') = event_filter_link EventFilter::COMMENTS, _('Comments'), s_('EventFilterBy|Filter by comments')
- if Feature.enabled?(:wiki_events) && (@project.nil? || @project.has_wiki?) - if Feature.enabled?(:wiki_events) && (@project.nil? || @project.has_wiki?)
= event_filter_link EventFilter::WIKI, _('Wiki'), s_('EventFilterBy|Filter by wiki') = event_filter_link EventFilter::WIKI, _('Wiki'), s_('EventFilterBy|Filter by wiki')
- if event_filter_visible(:designs)
= event_filter_link EventFilter::DESIGNS, _('Designs'), s_('EventFilterBy|Filter by designs')
= event_filter_link EventFilter::TEAM, _('Team'), s_('EventFilterBy|Filter by team') = event_filter_link EventFilter::TEAM, _('Team'), s_('EventFilterBy|Filter by team')
---
title: Add design activity in event streams
merge_request: 33534
author:
type: added
...@@ -242,3 +242,35 @@ To disable it: ...@@ -242,3 +242,35 @@ To disable it:
```ruby ```ruby
Feature.disable(:design_management_reference_filter_gfm_pipeline) Feature.disable(:design_management_reference_filter_gfm_pipeline)
``` ```
## Design activity records
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33051) in GitLab 13.1
> - It's deployed behind a feature flag, disabled by default.
> - It's enabled on GitLab.com.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-design-events-core-only). **(CORE ONLY)**
User activity events on designs (creation, deletion, and updates) are tracked by GitLab and
displayed on the [user profile](../../profile/index.md#user-profile),
[group](../../group/index.md#view-group-activity),
and [project](../index.md#project-activity) activity pages.
### Enable or disable Design Events **(CORE ONLY)**
User activity for designs is under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/troubleshooting/navigating_gitlab_via_rails_console.md#starting-a-rails-console-session)
can enable it for your instance. You're welcome to test it, but use it at your
own risk.
To enable it:
```ruby
Feature.enable(:design_activity_events)
```
To disable it:
```ruby
Feature.disable(:design_activity_events)
```
# frozen_string_literal: true # frozen_string_literal: true
class EventFilter class EventFilter
include Gitlab::Utils::StrongMemoize
attr_accessor :filter attr_accessor :filter
ALL = 'all' ALL = 'all'
...@@ -10,6 +12,7 @@ class EventFilter ...@@ -10,6 +12,7 @@ class EventFilter
COMMENTS = 'comments' COMMENTS = 'comments'
TEAM = 'team' TEAM = 'team'
WIKI = 'wiki' WIKI = 'wiki'
DESIGNS = 'designs'
def initialize(filter) def initialize(filter)
# Split using comma to maintain backward compatibility Ex/ "filter1,filter2" # Split using comma to maintain backward compatibility Ex/ "filter1,filter2"
...@@ -38,6 +41,8 @@ class EventFilter ...@@ -38,6 +41,8 @@ class EventFilter
events.where(action: [:created, :updated, :closed, :reopened], target_type: 'Issue') events.where(action: [:created, :updated, :closed, :reopened], target_type: 'Issue')
when WIKI when WIKI
wiki_events(events) wiki_events(events)
when DESIGNS
design_events(events)
else else
events events
end end
...@@ -47,7 +52,8 @@ class EventFilter ...@@ -47,7 +52,8 @@ class EventFilter
private private
def apply_feature_flags(events) def apply_feature_flags(events)
return events.not_wiki_page unless Feature.enabled?(:wiki_events) events = events.not_wiki_page unless Feature.enabled?(:wiki_events)
events = events.not_design unless can_view_design_activity?
events events
end end
...@@ -58,8 +64,18 @@ class EventFilter ...@@ -58,8 +64,18 @@ class EventFilter
events.for_wiki_page events.for_wiki_page
end end
def design_events(events)
return events.for_design if can_view_design_activity?
events
end
def filters def filters
[ALL, PUSH, MERGED, ISSUE, COMMENTS, TEAM, WIKI] [ALL, PUSH, MERGED, ISSUE, COMMENTS, TEAM, WIKI, DESIGNS]
end
def can_view_design_activity?
Feature.enabled?(:design_activity_events)
end end
end end
......
...@@ -9104,6 +9104,9 @@ msgstr "" ...@@ -9104,6 +9104,9 @@ msgstr ""
msgid "EventFilterBy|Filter by comments" msgid "EventFilterBy|Filter by comments"
msgstr "" msgstr ""
msgid "EventFilterBy|Filter by designs"
msgstr ""
msgid "EventFilterBy|Filter by epic events" msgid "EventFilterBy|Filter by epic events"
msgstr "" msgstr ""
......
...@@ -5,13 +5,14 @@ require 'spec_helper' ...@@ -5,13 +5,14 @@ require 'spec_helper'
RSpec.describe Dashboard::ProjectsController do RSpec.describe Dashboard::ProjectsController do
include ExternalAuthorizationServiceHelpers include ExternalAuthorizationServiceHelpers
let_it_be(:user) { create(:user) }
describe '#index' do describe '#index' do
context 'user not logged in' do context 'user not logged in' do
it_behaves_like 'authenticates sessionless user', :index, :atom it_behaves_like 'authenticates sessionless user', :index, :atom
end end
context 'user logged in' do context 'user logged in' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:project2) { create(:project) } let_it_be(:project2) { create(:project) }
...@@ -71,8 +72,6 @@ RSpec.describe Dashboard::ProjectsController do ...@@ -71,8 +72,6 @@ RSpec.describe Dashboard::ProjectsController do
context 'json requests' do context 'json requests' do
render_views render_views
let(:user) { create(:user) }
before do before do
sign_in(user) sign_in(user)
end end
...@@ -114,16 +113,14 @@ RSpec.describe Dashboard::ProjectsController do ...@@ -114,16 +113,14 @@ RSpec.describe Dashboard::ProjectsController do
end end
context 'atom requests' do context 'atom requests' do
let(:user) { create(:user) }
before do before do
sign_in(user) sign_in(user)
end end
describe '#index' do describe '#index' do
context 'project pagination' do let_it_be(:projects) { create_list(:project, 2, creator: user) }
let(:projects) { create_list(:project, 2, creator: user) }
context 'project pagination' do
before do before do
allow(Kaminari.config).to receive(:default_per_page).and_return(1) allow(Kaminari.config).to receive(:default_per_page).and_return(1)
...@@ -138,6 +135,37 @@ RSpec.describe Dashboard::ProjectsController do ...@@ -138,6 +135,37 @@ RSpec.describe Dashboard::ProjectsController do
expect(assigns(:events).count).to eq(2) expect(assigns(:events).count).to eq(2)
end end
end end
describe 'rendering' do
include DesignManagementTestHelpers
render_views
let(:project) { projects.first }
let!(:design_event) { create(:design_event, project: project) }
let!(:wiki_page_event) { create(:wiki_page_event, project: project) }
let!(:issue_event) { create(:closed_issue_event, project: project) }
let(:design) { design_event.design }
let(:wiki_page) { wiki_page_event.wiki_page }
let(:issue) { issue_event.issue }
before do
enable_design_management
project.add_developer(user)
end
it 'renders all kinds of event without error', :aggregate_failures do
get :index, format: :atom
expect(assigns(:events)).to include(design_event, wiki_page_event, issue_event)
expect(response).to render_template('dashboard/projects/index')
expect(response.body).to include(
"uploaded design #{design.to_reference}",
"created wiki page #{wiki_page.title}",
"joined project #{project.full_name}",
"closed issue #{issue.to_reference}"
)
end
end
end end
end end
end end
...@@ -24,15 +24,20 @@ RSpec.describe DashboardController do ...@@ -24,15 +24,20 @@ RSpec.describe DashboardController do
end end
describe "GET activity as JSON" do describe "GET activity as JSON" do
include DesignManagementTestHelpers
render_views render_views
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, :public, issues_access_level: ProjectFeature::PRIVATE) } let(:project) { create(:project, :public, issues_access_level: ProjectFeature::PRIVATE) }
let(:other_project) { create(:project, :public) }
before do before do
enable_design_management
create(:event, :created, project: project, target: create(:issue)) create(:event, :created, project: project, target: create(:issue))
create(:wiki_page_event, :created, project: project) create(:wiki_page_event, :created, project: project)
create(:wiki_page_event, :updated, project: project) create(:wiki_page_event, :updated, project: project)
create(:design_event, project: project)
create(:design_event, author: user, project: other_project)
sign_in(user) sign_in(user)
...@@ -42,12 +47,27 @@ RSpec.describe DashboardController do ...@@ -42,12 +47,27 @@ RSpec.describe DashboardController do
context 'when user has permission to see the event' do context 'when user has permission to see the event' do
before do before do
project.add_developer(user) project.add_developer(user)
other_project.add_developer(user)
end end
it 'returns count' do it 'returns count' do
get :activity, params: { format: :json } get :activity, params: { format: :json }
expect(json_response['count']).to eq(3) expect(json_response['count']).to eq(6)
end
describe 'design_activity_events feature flag' do
context 'it is off' do
before do
stub_feature_flags(design_activity_events: false)
end
it 'excludes design activity' do
get :activity, params: { format: :json }
expect(json_response['count']).to eq(4)
end
end
end end
end end
......
...@@ -1138,6 +1138,48 @@ RSpec.describe GroupsController do ...@@ -1138,6 +1138,48 @@ RSpec.describe GroupsController do
it_behaves_like 'disabled when using an external authorization service' it_behaves_like 'disabled when using an external authorization service'
end end
describe "GET #activity as JSON" do
include DesignManagementTestHelpers
render_views
let(:project) { create(:project, :public, group: group) }
let(:other_project) { create(:project, :public, group: group) }
def get_activity
get :activity, params: { format: :json, id: group.to_param }
end
before do
enable_design_management
issue = create(:issue, project: project)
create(:event, :created, project: project, target: issue)
create(:design_event, project: project)
create(:design_event, project: other_project)
sign_in(user)
request.cookies[:event_filter] = 'all'
end
it 'returns count' do
get_activity
expect(json_response['count']).to eq(3)
end
context 'the design_activity_events feature flag is disabled' do
before do
stub_feature_flags(design_activity_events: false)
end
it 'does not include the design activity' do
get_activity
expect(json_response['count']).to eq(1)
end
end
end
describe 'GET #issues' do describe 'GET #issues' do
subject { get :issues, params: { id: group.to_param } } subject { get :issues, params: { id: group.to_param } }
......
...@@ -86,11 +86,13 @@ RSpec.describe ProjectsController do ...@@ -86,11 +86,13 @@ RSpec.describe ProjectsController do
end end
describe "GET #activity as JSON" do describe "GET #activity as JSON" do
include DesignManagementTestHelpers
render_views render_views
let(:project) { create(:project, :public, issues_access_level: ProjectFeature::PRIVATE) } let(:project) { create(:project, :public, issues_access_level: ProjectFeature::PRIVATE) }
before do before do
enable_design_management
create(:event, :created, project: project, target: create(:issue)) create(:event, :created, project: project, target: create(:issue))
sign_in(user) sign_in(user)
...@@ -103,11 +105,44 @@ RSpec.describe ProjectsController do ...@@ -103,11 +105,44 @@ RSpec.describe ProjectsController do
project.add_developer(user) project.add_developer(user)
end end
it 'returns count' do def get_activity(project)
get :activity, params: { namespace_id: project.namespace, id: project, format: :json } get :activity, params: { namespace_id: project.namespace, id: project, format: :json }
end
it 'returns count' do
get_activity(project)
expect(json_response['count']).to eq(1) expect(json_response['count']).to eq(1)
end end
context 'design events are visible' do
include DesignManagementTestHelpers
let(:other_project) { create(:project, namespace: user.namespace) }
before do
enable_design_management
create(:design_event, project: project)
request.cookies[:event_filter] = EventFilter::DESIGNS
end
it 'returns correct count' do
get_activity(project)
expect(json_response['count']).to eq(1)
end
context 'the feature flag is disabled' do
before do
stub_feature_flags(design_activity_events: false)
end
it 'returns correct count' do
get_activity(project)
expect(json_response['count']).to eq(0)
end
end
end
end end
context 'when user has no permission to see the event' do context 'when user has no permission to see the event' do
......
...@@ -21,7 +21,7 @@ FactoryBot.define do ...@@ -21,7 +21,7 @@ FactoryBot.define do
factory :closed_issue_event do factory :closed_issue_event do
action { :closed } action { :closed }
target factory: :closed_issue target { association(:closed_issue, project: project) }
end end
factory :wiki_page_event do factory :wiki_page_event do
......
# frozen_string_literal: true
require 'spec_helper'
describe 'Projects > Activity > User sees design Activity', :js do
include DesignManagementTestHelpers
let_it_be(:uploader) { create(:user) }
let_it_be(:editor) { create(:user) }
let_it_be(:deleter) { create(:user) }
let_it_be(:archiver) { create(:user) }
def design_activity(user, action)
[user.name, user.to_reference, action, 'design'].join(' ')
end
shared_examples 'being able to see design activity' do
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:design) { create(:design, issue: issue) }
before_all do
project.add_developer(user) # implicitly adds a project join event.
common_attrs = { project: project, design: design }
create(:design_event, :created, author: uploader, **common_attrs)
create(:design_event, :updated, author: editor, **common_attrs)
create(:design_event, :destroyed, author: deleter, **common_attrs)
create(:design_event, :archived, author: archiver, **common_attrs)
end
before do
enable_design_management
sign_in(user)
end
it 'shows the design comment action in the activity page' do
visit activity_project_path(project)
expect(page).to have_content('joined project')
expect(page).to have_content(design_activity(uploader, 'uploaded'))
expect(page).to have_content(design_activity(editor, 'revised'))
expect(page).to have_content(design_activity(deleter, 'deleted'))
expect(page).to have_content(design_activity(archiver, 'archived'))
end
it 'allows filtering out the design events', :aggregate_failures do
visit activity_project_path(project, event_filter: EventFilter::ISSUE)
expect(page).not_to have_content(design_activity(uploader, 'uploaded'))
expect(page).not_to have_content(design_activity(editor, 'revised'))
expect(page).not_to have_content(design_activity(deleter, 'deleted'))
expect(page).not_to have_content(design_activity(archiver, 'archived'))
end
it 'allows filtering in the design events', :aggregate_failures do
visit activity_project_path(project, event_filter: EventFilter::DESIGNS)
expect(page).not_to have_content('joined project')
expect(page).to have_content(design_activity(uploader, 'uploaded'))
expect(page).to have_content(design_activity(editor, 'revised'))
expect(page).to have_content(design_activity(deleter, 'deleted'))
expect(page).to have_content(design_activity(archiver, 'archived'))
end
end
context 'the project is public' do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:user) { create(:user) }
it_behaves_like 'being able to see design activity'
end
context 'the project is private' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user, developer_projects: [project]) }
it_behaves_like 'being able to see design activity'
end
end
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe UserRecentEventsFinder do RSpec.describe UserRecentEventsFinder do
let(:current_user) { create(:user) } let_it_be(:project_owner, reload: true) { create(:user) }
let(:project_owner) { create(:user) } let_it_be(:current_user, reload: true) { create(:user) }
let(:private_project) { create(:project, :private, creator: project_owner) } let(:private_project) { create(:project, :private, creator: project_owner) }
let(:internal_project) { create(:project, :internal, creator: project_owner) } let(:internal_project) { create(:project, :internal, creator: project_owner) }
let(:public_project) { create(:project, :public, creator: project_owner) } let(:public_project) { create(:project, :public, creator: project_owner) }
...@@ -36,5 +36,30 @@ RSpec.describe UserRecentEventsFinder do ...@@ -36,5 +36,30 @@ RSpec.describe UserRecentEventsFinder do
expect(finder.execute).to be_empty expect(finder.execute).to be_empty
end end
describe 'design_activity_events feature flag' do
let_it_be(:event_a) { create(:design_event, author: project_owner) }
let_it_be(:event_b) { create(:design_event, author: project_owner) }
context 'the design_activity_events feature-flag is enabled' do
it 'only includes design events in enabled projects', :aggregate_failures do
events = finder.execute
expect(events).to include(event_a)
expect(events).to include(event_b)
end
end
context 'the design_activity_events feature-flag is disabled' do
it 'excludes design events', :aggregate_failures do
stub_feature_flags(design_activity_events: false)
events = finder.execute
expect(events).not_to include(event_a)
expect(events).not_to include(event_b)
end
end
end
end end
end end
...@@ -227,4 +227,133 @@ describe EventsHelper do ...@@ -227,4 +227,133 @@ describe EventsHelper do
end end
end end
end end
describe '#event_filter_visible' do
include DesignManagementTestHelpers
let_it_be(:project) { create(:project) }
let_it_be(:current_user) { create(:user) }
subject { helper.event_filter_visible(key) }
before do
enable_design_management
project.add_reporter(current_user)
allow(helper).to receive(:current_user).and_return(current_user)
end
def disable_read_design_activity(object)
allow(Ability).to receive(:allowed?)
.with(current_user, :read_design_activity, eq(object))
.and_return(false)
end
context 'for :designs' do
let(:key) { :designs }
context 'there is no relevant instance variable' do
it { is_expected.to be(true) }
end
context 'the feature flag is off' do
before do
stub_feature_flags(design_activity_events: false)
end
it { is_expected.to be(false) }
end
context 'a project has been assigned' do
before do
assign(:project, project)
end
it { is_expected.to be(true) }
context 'the current user cannot read design activity' do
before do
disable_read_design_activity(project)
end
it { is_expected.to be(false) }
end
context 'the feature flag is off' do
before do
stub_feature_flags(design_activity_events: false)
end
it { is_expected.to be(false) }
end
end
context 'projects have been assigned' do
before do
assign(:projects, Project.where(id: project.id))
end
it { is_expected.to be(true) }
context 'the collection is empty' do
before do
assign(:projects, Project.none)
end
it { is_expected.to be(false) }
end
context 'the current user cannot read design activity' do
before do
disable_read_design_activity(project)
end
it { is_expected.to be(false) }
end
context 'the feature flag is off' do
before do
stub_feature_flags(design_activity_events: false)
end
it { is_expected.to be(false) }
end
end
context 'a group has been assigned' do
let_it_be(:group) { create(:group) }
before do
assign(:group, group)
end
context 'there are no projects in the group' do
it { is_expected.to be(false) }
end
context 'the group has at least one project' do
before do
create(:project_group_link, project: project, group: group)
end
it { is_expected.to be(true) }
context 'the current user cannot read design activity' do
before do
disable_read_design_activity(group)
end
it { is_expected.to be(false) }
end
context 'the feature flag is off' do
before do
stub_feature_flags(design_activity_events: false)
end
it { is_expected.to be(false) }
end
end
end
end
end
end end
...@@ -30,6 +30,7 @@ describe EventFilter do ...@@ -30,6 +30,7 @@ describe EventFilter do
let_it_be(:left_event) { create(:event, :left, project: public_project, target: public_project) } let_it_be(:left_event) { create(:event, :left, project: public_project, target: public_project) }
let_it_be(:wiki_page_event) { create(:wiki_page_event) } let_it_be(:wiki_page_event) { create(:wiki_page_event) }
let_it_be(:wiki_page_update_event) { create(:wiki_page_event, :updated) } let_it_be(:wiki_page_update_event) { create(:wiki_page_event, :updated) }
let_it_be(:design_event) { create(:design_event) }
let(:filtered_events) { described_class.new(filter).apply_filter(Event.all) } let(:filtered_events) { described_class.new(filter).apply_filter(Event.all) }
...@@ -91,6 +92,24 @@ describe EventFilter do ...@@ -91,6 +92,24 @@ describe EventFilter do
end end
end end
context 'with the "design" filter' do
let(:filter) { described_class::DESIGNS }
it 'returns only design events' do
expect(filtered_events).to contain_exactly(design_event)
end
context 'the :design_activity_events feature is disabled' do
before do
stub_feature_flags(design_activity_events: false)
end
it 'does not return design events' do
expect(filtered_events).to match_array(Event.not_design)
end
end
end
context 'with the "wiki" filter' do context 'with the "wiki" filter' do
let(:filter) { described_class::WIKI } let(:filter) { described_class::WIKI }
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
require 'spec_helper' require 'spec_helper'
describe EventCollection do describe EventCollection do
include DesignManagementTestHelpers
describe '#to_a' do describe '#to_a' do
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project_empty_repo, group: group) } let_it_be(:project) { create(:project_empty_repo, group: group) }
...@@ -10,6 +12,10 @@ describe EventCollection do ...@@ -10,6 +12,10 @@ describe EventCollection do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:merge_request) { create(:merge_request) } let_it_be(:merge_request) { create(:merge_request) }
before do
enable_design_management
end
context 'with project events' do context 'with project events' do
let_it_be(:push_event_payloads) do let_it_be(:push_event_payloads) do
Array.new(9) do Array.new(9) do
...@@ -21,11 +27,13 @@ describe EventCollection do ...@@ -21,11 +27,13 @@ describe EventCollection do
let_it_be(:merge_request_events) { create_list(:event, 10, :commented, project: project, target: merge_request) } let_it_be(:merge_request_events) { create_list(:event, 10, :commented, project: project, target: merge_request) }
let_it_be(:closed_issue_event) { create(:closed_issue_event, project: project, author: user) } let_it_be(:closed_issue_event) { create(:closed_issue_event, project: project, author: user) }
let_it_be(:wiki_page_event) { create(:wiki_page_event, project: project) } let_it_be(:wiki_page_event) { create(:wiki_page_event, project: project) }
let_it_be(:design_event) { create(:design_event, project: project) }
let(:push_events) { push_event_payloads.map(&:event) } let(:push_events) { push_event_payloads.map(&:event) }
it 'returns an Array of events', :aggregate_failures do it 'returns an Array of events', :aggregate_failures do
most_recent_20_events = [ most_recent_20_events = [
wiki_page_event, wiki_page_event,
design_event,
closed_issue_event, closed_issue_event,
*push_events, *push_events,
*merge_request_events *merge_request_events
...@@ -54,6 +62,31 @@ describe EventCollection do ...@@ -54,6 +62,31 @@ describe EventCollection do
end end
end end
context 'the design_activity_events feature flag is disabled' do
before do
stub_feature_flags(design_activity_events: false)
end
it 'omits the design events when using to_a' do
events = described_class.new(projects).to_a
expect(events).not_to include(design_event)
end
it 'omits the wiki page events when using all_project_events' do
events = described_class.new(projects).all_project_events
expect(events).not_to include(design_event)
end
end
it 'includes the design events' do
collection = described_class.new(projects)
expect(collection.to_a).to include(design_event)
expect(collection.all_project_events).to include(design_event)
end
context 'the wiki_events feature flag is enabled' do context 'the wiki_events feature flag is enabled' do
before do before do
stub_feature_flags(wiki_events: true) stub_feature_flags(wiki_events: true)
...@@ -81,7 +114,7 @@ describe EventCollection do ...@@ -81,7 +114,7 @@ describe EventCollection do
it 'can paginate through events' do it 'can paginate through events' do
events = described_class.new(projects, offset: 20).to_a events = described_class.new(projects, offset: 20).to_a
expect(events.length).to eq(1) expect(events.length).to eq(2)
end end
it 'returns an empty Array when crossing the maximum page number' do it 'returns an empty Array when crossing the maximum page number' do
......
...@@ -661,4 +661,61 @@ describe GroupPolicy do ...@@ -661,4 +661,61 @@ describe GroupPolicy do
end end
end end
end end
describe 'design activity' do
let_it_be(:group) { create(:group, :public) }
let(:current_user) { nil }
subject { described_class.new(current_user, group) }
context 'when design management is not available' do
it { is_expected.not_to be_allowed(:read_design_activity) }
context 'even when there are projects in the group' do
before do
create_list(:project_group_link, 2, group: group)
end
it { is_expected.not_to be_allowed(:read_design_activity) }
end
end
context 'when design management is available globally' do
include DesignManagementTestHelpers
before do
enable_design_management
end
context 'the group has no projects' do
it { is_expected.not_to be_allowed(:read_design_activity) }
end
context 'the group has a project' do
let(:project) { create(:project, :public) }
before do
create(:project_group_link, project: project, group: group)
end
it { is_expected.to be_allowed(:read_design_activity) }
context 'which does not have design management enabled' do
before do
project.update(lfs_enabled: false)
end
it { is_expected.not_to be_allowed(:read_design_activity) }
context 'but another project does' do
before do
create(:project_group_link, project: create(:project, :public), group: group)
end
it { is_expected.to be_allowed(:read_design_activity) }
end
end
end
end
end
end end
...@@ -855,6 +855,28 @@ describe ProjectPolicy do ...@@ -855,6 +855,28 @@ describe ProjectPolicy do
end end
end end
describe 'design permissions' do
subject { described_class.new(guest, project) }
let(:design_permissions) do
%i[read_design_activity read_design]
end
context 'when design management is not available' do
it { is_expected.not_to be_allowed(*design_permissions) }
end
context 'when design management is available' do
include DesignManagementTestHelpers
before do
enable_design_management
end
it { is_expected.to be_allowed(*design_permissions) }
end
end
describe 'read_build_report_results' do describe 'read_build_report_results' do
subject { described_class.new(guest, project) } subject { described_class.new(guest, project) }
......
...@@ -275,15 +275,6 @@ describe EventCreateService do ...@@ -275,15 +275,6 @@ describe EventCreateService do
specify { expect { result }.not_to change { Event.count } } specify { expect { result }.not_to change { Event.count } }
specify { expect { result }.not_to exceed_query_limit(0) } specify { expect { result }.not_to exceed_query_limit(0) }
end end
context 'the feature flag is enabled for a single project' do
before do
stub_feature_flags(design_activity_events: project)
end
specify { expect(result).not_to be_empty }
specify { expect { result }.to change { Event.count }.by(1) }
end
end end
describe '#save_designs' do describe '#save_designs' do
...@@ -310,9 +301,7 @@ describe EventCreateService do ...@@ -310,9 +301,7 @@ describe EventCreateService do
expect(events.map(&:design)).to match_array(updated) expect(events.map(&:design)).to match_array(updated)
end end
it_behaves_like 'feature flag gated multiple event creation' do it_behaves_like 'feature flag gated multiple event creation'
let(:project) { created.first.project }
end
end end
describe '#destroy_designs' do describe '#destroy_designs' do
...@@ -332,9 +321,7 @@ describe EventCreateService do ...@@ -332,9 +321,7 @@ describe EventCreateService do
expect(events.map(&:design)).to match_array(designs) expect(events.map(&:design)).to match_array(designs)
end end
it_behaves_like 'feature flag gated multiple event creation' do it_behaves_like 'feature flag gated multiple event creation'
let(:project) { designs.first.project }
end
end end
end 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