Add basic project extraction

To allow project filtering

Prepare summary for accepting multiple groups

Modify deploys group summary class

Add filtering by project name in issues summary

Fix rubocop offences

Add changelog entry

Change name to id in project filtering

Fix rebase problem

Add project extraction
This commit is contained in:
Małgorzata Ksionek 2019-07-06 22:15:13 +02:00
parent f4101aeac7
commit 1b102f5d11
12 changed files with 167 additions and 89 deletions

View file

@ -13,7 +13,8 @@ module CycleAnalytics
def summary def summary
@summary ||= ::Gitlab::CycleAnalytics::GroupStageSummary.new(group, @summary ||= ::Gitlab::CycleAnalytics::GroupStageSummary.new(group,
from: options[:from], from: options[:from],
current_user: options[:current_user]).data current_user: options[:current_user],
options: options).data
end end
def permissions(*) def permissions(*)

View file

@ -0,0 +1,5 @@
---
title: Adjust group level analytics to accept multiple ids
merge_request: 30468
author:
type: added

View file

@ -63,7 +63,6 @@ ActiveRecord::Schema.define(version: 2019_07_15_114644) do
t.datetime "updated_at" t.datetime "updated_at"
t.string "home_page_url" t.string "home_page_url"
t.integer "default_branch_protection", default: 2 t.integer "default_branch_protection", default: 2
t.text "help_text"
t.text "restricted_visibility_levels" t.text "restricted_visibility_levels"
t.boolean "version_check_enabled", default: true t.boolean "version_check_enabled", default: true
t.integer "max_attachment_size", default: 10, null: false t.integer "max_attachment_size", default: 10, null: false
@ -105,8 +104,6 @@ ActiveRecord::Schema.define(version: 2019_07_15_114644) do
t.integer "container_registry_token_expire_delay", default: 5 t.integer "container_registry_token_expire_delay", default: 5
t.text "after_sign_up_text" t.text "after_sign_up_text"
t.boolean "user_default_external", default: false, null: false t.boolean "user_default_external", default: false, null: false
t.boolean "elasticsearch_indexing", default: false, null: false
t.boolean "elasticsearch_search", default: false, null: false
t.string "repository_storages", default: "default" t.string "repository_storages", default: "default"
t.string "enabled_git_access_protocol" t.string "enabled_git_access_protocol"
t.boolean "domain_blacklist_enabled", default: false t.boolean "domain_blacklist_enabled", default: false
@ -128,37 +125,18 @@ ActiveRecord::Schema.define(version: 2019_07_15_114644) do
t.boolean "html_emails_enabled", default: true t.boolean "html_emails_enabled", default: true
t.string "plantuml_url" t.string "plantuml_url"
t.boolean "plantuml_enabled" t.boolean "plantuml_enabled"
t.integer "shared_runners_minutes", default: 0, null: false
t.bigint "repository_size_limit", default: 0
t.integer "terminal_max_session_time", default: 0, null: false t.integer "terminal_max_session_time", default: 0, null: false
t.integer "unique_ips_limit_per_user" t.integer "unique_ips_limit_per_user"
t.integer "unique_ips_limit_time_window" t.integer "unique_ips_limit_time_window"
t.boolean "unique_ips_limit_enabled", default: false, null: false t.boolean "unique_ips_limit_enabled", default: false, null: false
t.string "default_artifacts_expire_in", default: "0", null: false t.string "default_artifacts_expire_in", default: "0", null: false
t.string "elasticsearch_url", default: "http://localhost:9200"
t.boolean "elasticsearch_aws", default: false, null: false
t.string "elasticsearch_aws_region", default: "us-east-1"
t.string "elasticsearch_aws_access_key"
t.string "elasticsearch_aws_secret_access_key"
t.integer "geo_status_timeout", default: 10
t.string "uuid" t.string "uuid"
t.decimal "polling_interval_multiplier", default: "1.0", null: false t.decimal "polling_interval_multiplier", default: "1.0", null: false
t.boolean "elasticsearch_experimental_indexer"
t.integer "cached_markdown_version" t.integer "cached_markdown_version"
t.boolean "check_namespace_plan", default: false, null: false
t.integer "mirror_max_delay", default: 300, null: false
t.integer "mirror_max_capacity", default: 100, null: false
t.integer "mirror_capacity_threshold", default: 50, null: false
t.boolean "prometheus_metrics_enabled", default: true, null: false t.boolean "prometheus_metrics_enabled", default: true, null: false
t.boolean "authorized_keys_enabled", default: true, null: false
t.boolean "help_page_hide_commercial_content", default: false t.boolean "help_page_hide_commercial_content", default: false
t.string "help_page_support_url" t.string "help_page_support_url"
t.boolean "slack_app_enabled", default: false
t.string "slack_app_id"
t.string "slack_app_secret"
t.string "slack_app_verification_token"
t.integer "performance_bar_allowed_group_id" t.integer "performance_bar_allowed_group_id"
t.boolean "allow_group_owners_to_manage_ldap", default: true, null: false
t.boolean "hashed_storage_enabled", default: true, null: false t.boolean "hashed_storage_enabled", default: true, null: false
t.boolean "project_export_enabled", default: true, null: false t.boolean "project_export_enabled", default: true, null: false
t.boolean "auto_devops_enabled", default: true, null: false t.boolean "auto_devops_enabled", default: true, null: false
@ -171,38 +149,22 @@ ActiveRecord::Schema.define(version: 2019_07_15_114644) do
t.boolean "throttle_authenticated_web_enabled", default: false, null: false t.boolean "throttle_authenticated_web_enabled", default: false, null: false
t.integer "throttle_authenticated_web_requests_per_period", default: 7200, null: false t.integer "throttle_authenticated_web_requests_per_period", default: 7200, null: false
t.integer "throttle_authenticated_web_period_in_seconds", default: 3600, null: false t.integer "throttle_authenticated_web_period_in_seconds", default: 3600, null: false
t.boolean "password_authentication_enabled_for_web"
t.boolean "password_authentication_enabled_for_git", default: true, null: false
t.integer "gitaly_timeout_default", default: 55, null: false t.integer "gitaly_timeout_default", default: 55, null: false
t.integer "gitaly_timeout_medium", default: 30, null: false t.integer "gitaly_timeout_medium", default: 30, null: false
t.integer "gitaly_timeout_fast", default: 10, null: false t.integer "gitaly_timeout_fast", default: 10, null: false
t.boolean "mirror_available", default: true, null: false t.boolean "authorized_keys_enabled", default: true, null: false
t.boolean "password_authentication_enabled_for_web"
t.boolean "password_authentication_enabled_for_git", default: true, null: false
t.string "auto_devops_domain" t.string "auto_devops_domain"
t.boolean "external_authorization_service_enabled", default: false, null: false
t.string "external_authorization_service_url"
t.string "external_authorization_service_default_label"
t.boolean "pages_domain_verification_enabled", default: true, null: false t.boolean "pages_domain_verification_enabled", default: true, null: false
t.string "user_default_internal_regex" t.string "user_default_internal_regex"
t.boolean "allow_local_requests_from_hooks_and_services", default: false, null: false t.boolean "allow_local_requests_from_hooks_and_services", default: false, null: false
t.float "external_authorization_service_timeout", default: 0.5
t.text "external_auth_client_cert"
t.text "encrypted_external_auth_client_key"
t.string "encrypted_external_auth_client_key_iv"
t.string "encrypted_external_auth_client_key_pass"
t.string "encrypted_external_auth_client_key_pass_iv"
t.string "email_additional_text"
t.boolean "enforce_terms", default: false t.boolean "enforce_terms", default: false
t.integer "file_template_project_id" t.boolean "mirror_available", default: true, null: false
t.boolean "pseudonymizer_enabled", default: false, null: false
t.boolean "hide_third_party_offers", default: false, null: false t.boolean "hide_third_party_offers", default: false, null: false
t.boolean "snowplow_enabled", default: false, null: false
t.string "snowplow_collector_uri"
t.string "snowplow_site_id"
t.string "snowplow_cookie_domain"
t.boolean "instance_statistics_visibility_private", default: false, null: false t.boolean "instance_statistics_visibility_private", default: false, null: false
t.boolean "web_ide_clientside_preview_enabled", default: false, null: false t.boolean "web_ide_clientside_preview_enabled", default: false, null: false
t.boolean "user_show_add_ssh_key_message", default: true, null: false t.boolean "user_show_add_ssh_key_message", default: true, null: false
t.integer "custom_project_templates_group_id"
t.integer "usage_stats_set_by_user_id" t.integer "usage_stats_set_by_user_id"
t.integer "receive_max_input_size" t.integer "receive_max_input_size"
t.integer "diff_max_patch_bytes", default: 102400, null: false t.integer "diff_max_patch_bytes", default: 102400, null: false
@ -212,21 +174,59 @@ ActiveRecord::Schema.define(version: 2019_07_15_114644) do
t.string "runners_registration_token_encrypted" t.string "runners_registration_token_encrypted"
t.integer "local_markdown_version", default: 0, null: false t.integer "local_markdown_version", default: 0, null: false
t.integer "first_day_of_week", default: 0, null: false t.integer "first_day_of_week", default: 0, null: false
t.boolean "elasticsearch_limit_indexing", default: false, null: false
t.integer "default_project_creation", default: 2, null: false t.integer "default_project_creation", default: 2, null: false
t.boolean "external_authorization_service_enabled", default: false, null: false
t.string "external_authorization_service_url"
t.string "external_authorization_service_default_label"
t.float "external_authorization_service_timeout", default: 0.5
t.text "external_auth_client_cert"
t.text "encrypted_external_auth_client_key"
t.string "encrypted_external_auth_client_key_iv"
t.string "encrypted_external_auth_client_key_pass"
t.string "encrypted_external_auth_client_key_pass_iv"
t.string "lets_encrypt_notification_email" t.string "lets_encrypt_notification_email"
t.boolean "lets_encrypt_terms_of_service_accepted", default: false, null: false t.boolean "lets_encrypt_terms_of_service_accepted", default: false, null: false
t.string "geo_node_allowed_ips", default: "0.0.0.0/0, ::/0"
t.integer "elasticsearch_shards", default: 5, null: false t.integer "elasticsearch_shards", default: 5, null: false
t.integer "elasticsearch_replicas", default: 1, null: false t.integer "elasticsearch_replicas", default: 1, null: false
t.text "encrypted_lets_encrypt_private_key" t.text "encrypted_lets_encrypt_private_key"
t.text "encrypted_lets_encrypt_private_key_iv" t.text "encrypted_lets_encrypt_private_key_iv"
t.string "required_instance_ci_template"
t.boolean "dns_rebinding_protection_enabled", default: true, null: false t.boolean "dns_rebinding_protection_enabled", default: true, null: false
t.boolean "default_project_deletion_protection", default: false, null: false t.boolean "default_project_deletion_protection", default: false, null: false
t.boolean "grafana_enabled", default: false, null: false t.text "help_text"
t.boolean "elasticsearch_indexing", default: false, null: false
t.boolean "elasticsearch_search", default: false, null: false
t.integer "shared_runners_minutes", default: 0, null: false
t.bigint "repository_size_limit", default: 0
t.string "elasticsearch_url", default: "http://localhost:9200"
t.boolean "elasticsearch_aws", default: false, null: false
t.string "elasticsearch_aws_region", default: "us-east-1"
t.string "elasticsearch_aws_access_key"
t.string "elasticsearch_aws_secret_access_key"
t.integer "geo_status_timeout", default: 10
t.boolean "elasticsearch_experimental_indexer"
t.boolean "check_namespace_plan", default: false, null: false
t.integer "mirror_max_delay", default: 300, null: false
t.integer "mirror_max_capacity", default: 100, null: false
t.integer "mirror_capacity_threshold", default: 50, null: false
t.boolean "slack_app_enabled", default: false
t.string "slack_app_id"
t.string "slack_app_secret"
t.string "slack_app_verification_token"
t.boolean "allow_group_owners_to_manage_ldap", default: true, null: false
t.string "email_additional_text"
t.integer "file_template_project_id"
t.boolean "pseudonymizer_enabled", default: false, null: false
t.boolean "snowplow_enabled", default: false, null: false
t.string "snowplow_collector_uri"
t.string "snowplow_site_id"
t.string "snowplow_cookie_domain"
t.integer "custom_project_templates_group_id"
t.boolean "elasticsearch_limit_indexing", default: false, null: false
t.string "geo_node_allowed_ips", default: "0.0.0.0/0, ::/0"
t.string "required_instance_ci_template"
t.boolean "lock_memberships_to_ldap", default: false, null: false t.boolean "lock_memberships_to_ldap", default: false, null: false
t.boolean "time_tracking_limit_to_hours", default: false, null: false t.boolean "time_tracking_limit_to_hours", default: false, null: false
t.boolean "grafana_enabled", default: false, null: false
t.string "grafana_url", default: "/-/grafana", null: false t.string "grafana_url", default: "/-/grafana", null: false
t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id", using: :btree t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id", using: :btree
t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id", using: :btree t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id", using: :btree
@ -407,10 +407,10 @@ ActiveRecord::Schema.define(version: 2019_07_15_114644) do
t.integer "project_id" t.integer "project_id"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.string "name", default: "Development", null: false
t.integer "milestone_id"
t.integer "group_id" t.integer "group_id"
t.integer "milestone_id"
t.integer "weight" t.integer "weight"
t.string "name", default: "Development", null: false
t.index ["group_id"], name: "index_boards_on_group_id", using: :btree t.index ["group_id"], name: "index_boards_on_group_id", using: :btree
t.index ["milestone_id"], name: "index_boards_on_milestone_id", using: :btree t.index ["milestone_id"], name: "index_boards_on_milestone_id", using: :btree
t.index ["project_id"], name: "index_boards_on_project_id", using: :btree t.index ["project_id"], name: "index_boards_on_project_id", using: :btree
@ -611,7 +611,7 @@ ActiveRecord::Schema.define(version: 2019_07_15_114644) do
t.index ["pipeline_id"], name: "index_ci_pipeline_chat_data_on_pipeline_id", unique: true, using: :btree t.index ["pipeline_id"], name: "index_ci_pipeline_chat_data_on_pipeline_id", unique: true, using: :btree
end end
create_table "ci_pipeline_schedule_variables", force: :cascade do |t| create_table "ci_pipeline_schedule_variables", id: :serial, force: :cascade do |t|
t.string "key", null: false t.string "key", null: false
t.text "value" t.text "value"
t.text "encrypted_value" t.text "encrypted_value"
@ -1865,7 +1865,7 @@ ActiveRecord::Schema.define(version: 2019_07_15_114644) do
t.index ["user_id"], name: "index_members_on_user_id", using: :btree t.index ["user_id"], name: "index_members_on_user_id", using: :btree
end end
create_table "merge_request_assignees", force: :cascade do |t| create_table "merge_request_assignees", id: :serial, force: :cascade do |t|
t.integer "user_id", null: false t.integer "user_id", null: false
t.integer "merge_request_id", null: false t.integer "merge_request_id", null: false
t.index ["merge_request_id", "user_id"], name: "index_merge_request_assignees_on_merge_request_id_and_user_id", unique: true, using: :btree t.index ["merge_request_id", "user_id"], name: "index_merge_request_assignees_on_merge_request_id_and_user_id", unique: true, using: :btree
@ -2101,8 +2101,8 @@ ActiveRecord::Schema.define(version: 2019_07_15_114644) do
t.integer "cached_markdown_version" t.integer "cached_markdown_version"
t.string "runners_token" t.string "runners_token"
t.string "runners_token_encrypted" t.string "runners_token_encrypted"
t.integer "project_creation_level"
t.boolean "auto_devops_enabled" t.boolean "auto_devops_enabled"
t.integer "project_creation_level"
t.datetime_with_timezone "last_ci_minutes_notification_at" t.datetime_with_timezone "last_ci_minutes_notification_at"
t.integer "custom_project_templates_group_id" t.integer "custom_project_templates_group_id"
t.integer "file_template_project_id" t.integer "file_template_project_id"
@ -2534,7 +2534,7 @@ ActiveRecord::Schema.define(version: 2019_07_15_114644) do
t.index ["project_id"], name: "index_project_import_data_on_project_id", using: :btree t.index ["project_id"], name: "index_project_import_data_on_project_id", using: :btree
end end
create_table "project_incident_management_settings", primary_key: "project_id", id: :serial, force: :cascade do |t| create_table "project_incident_management_settings", primary_key: "project_id", id: :integer, default: nil, force: :cascade do |t|
t.boolean "create_issue", default: true, null: false t.boolean "create_issue", default: true, null: false
t.boolean "send_email", default: false, null: false t.boolean "send_email", default: false, null: false
t.text "issue_template_key" t.text "issue_template_key"
@ -2885,6 +2885,7 @@ ActiveRecord::Schema.define(version: 2019_07_15_114644) do
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["path"], name: "index_redirect_routes_on_path", unique: true, using: :btree t.index ["path"], name: "index_redirect_routes_on_path", unique: true, using: :btree
t.index ["path"], name: "index_redirect_routes_on_path_text_pattern_ops", using: :btree, opclasses: {"path"=>"varchar_pattern_ops"}
t.index ["source_type", "source_id"], name: "index_redirect_routes_on_source_type_and_source_id", using: :btree t.index ["source_type", "source_id"], name: "index_redirect_routes_on_source_type_and_source_id", using: :btree
end end
@ -3304,6 +3305,7 @@ ActiveRecord::Schema.define(version: 2019_07_15_114644) do
t.string "timezone" t.string "timezone"
t.boolean "time_display_relative" t.boolean "time_display_relative"
t.boolean "time_format_in_24h" t.boolean "time_format_in_24h"
t.string "timezone_name"
t.integer "epic_notes_filter", limit: 2, default: 0, null: false t.integer "epic_notes_filter", limit: 2, default: 0, null: false
t.string "epics_sort" t.string "epics_sort"
t.integer "roadmap_epics_state" t.integer "roadmap_epics_state"

View file

@ -0,0 +1,27 @@
# frozen_string_literal: true
module Gitlab
module CycleAnalytics
module BaseDataExtraction
private
def projects
group ? extract_projects(options) : [project]
end
def group
@group ||= options.fetch(:group, nil)
end
def project
@project ||= options.fetch(:project, nil)
end
def extract_projects(options)
projects = Project.inside_path(group.full_path)
projects = projects.where(id: options[:projects]) if options[:projects]
projects
end
end
end
end

View file

@ -4,6 +4,7 @@ module Gitlab
module CycleAnalytics module CycleAnalytics
class BaseEventFetcher class BaseEventFetcher
include BaseQuery include BaseQuery
include BaseDataExtraction
attr_reader :projections, :query, :stage, :order, :options attr_reader :projections, :query, :stage, :order, :options
@ -73,18 +74,6 @@ module Gitlab
def serialization_context def serialization_context
{} {}
end end
def projects
group ? Project.inside_path(group.full_path) : [project]
end
def group
@group ||= options.fetch(:group, nil)
end
def project
@project ||= options.fetch(:project, nil)
end
end end
end end
end end

View file

@ -4,6 +4,7 @@ module Gitlab
module CycleAnalytics module CycleAnalytics
class BaseStage class BaseStage
include BaseQuery include BaseQuery
include BaseDataExtraction
attr_reader :options attr_reader :options
@ -77,18 +78,6 @@ module Gitlab
def event_options def event_options
options.merge(start_time_attrs: start_time_attrs, end_time_attrs: end_time_attrs) options.merge(start_time_attrs: start_time_attrs, end_time_attrs: end_time_attrs)
end end
def projects
group ? Project.inside_path(group.full_path) : [project]
end
def group
@group ||= options.fetch(:group, nil)
end
def project
@project ||= options.fetch(:project, nil)
end
end end
end end
end end

View file

@ -3,15 +3,16 @@
module Gitlab module Gitlab
module CycleAnalytics module CycleAnalytics
class GroupStageSummary class GroupStageSummary
def initialize(group, from:, current_user:) def initialize(group, from:, current_user:, options:)
@group = group @group = group
@from = from @from = from
@current_user = current_user @current_user = current_user
@options = options
end end
def data def data
[serialize(Summary::Group::Issue.new(group: @group, from: @from, current_user: @current_user)), [serialize(Summary::Group::Issue.new(group: @group, from: @from, current_user: @current_user, options: @options)),
serialize(Summary::Group::Deploy.new(group: @group, from: @from))] serialize(Summary::Group::Deploy.new(group: @group, from: @from, options: @options))]
end end
private private

View file

@ -5,9 +5,10 @@ module Gitlab
module Summary module Summary
module Group module Group
class Base class Base
def initialize(group:, from:) def initialize(group:, from:, options:)
@group = group @group = group
@from = from @from = from
@options = options
end end
def title def title

View file

@ -10,15 +10,19 @@ module Gitlab
end end
def value def value
@value ||= Deployment.joins(:project) @value ||= find_deployments
.where(projects: { id: projects })
.where("deployments.created_at > ?", @from)
.success
.count
end end
private private
def find_deployments
deployments = Deployment.joins(:project)
.where(projects: { id: projects })
.where("deployments.created_at > ?", @from)
deployments = deployments.where(projects: { id: @options[:projects] }) if @options[:projects]
deployments.success.count
end
def projects def projects
Project.inside_path(@group.full_path).ids Project.inside_path(@group.full_path).ids
end end

View file

@ -5,10 +5,11 @@ module Gitlab
module Summary module Summary
module Group module Group
class Issue < Group::Base class Issue < Group::Base
def initialize(group:, from:, current_user:) def initialize(group:, from:, current_user:, options:)
@group = group @group = group
@from = from @from = from
@current_user = current_user @current_user = current_user
@options = options
end end
def title def title
@ -16,7 +17,15 @@ module Gitlab
end end
def value def value
@value ||= IssuesFinder.new(@current_user, group_id: @group.id, include_subgroups: true, created_after: @from).execute.count @value ||= find_issues
end
private
def find_issues
issues = IssuesFinder.new(@current_user, group_id: @group.id, include_subgroups: true, created_after: @from).execute
issues = issues.where(projects: { id: @options[:projects] }) if @options[:projects]
issues.count
end end
end end
end end

View file

@ -8,7 +8,7 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do
let(:from) { 1.day.ago } let(:from) { 1.day.ago }
let(:user) { create(:user, :admin) } let(:user) { create(:user, :admin) }
subject { described_class.new(group, from: Time.now, current_user: user).data } subject { described_class.new(group, from: Time.now, current_user: user, options: {}).data }
describe "#new_issues" do describe "#new_issues" do
context 'with from date' do context 'with from date' do
@ -32,6 +32,18 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do
expect(subject.first[:value]).to eq(3) expect(subject.first[:value]).to eq(3)
end end
end end
context 'with projects specified in options' do
before do
Timecop.freeze(5.days.from_now) { create(:issue, project: create(:project, namespace: group)) }
end
subject { described_class.new(group, from: Time.now, current_user: user, options: { projects: [project.id, project_2.id] }).data }
it 'finds issues from those projects' do
expect(subject.first[:value]).to eq(2)
end
end
end end
context 'with other projects' do context 'with other projects' do
@ -71,6 +83,20 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do
expect(subject.second[:value]).to eq(3) expect(subject.second[:value]).to eq(3)
end end
end end
context 'with projects specified in options' do
before do
Timecop.freeze(5.days.from_now) do
create(:deployment, :success, project: create(:project, :repository, namespace: group, name: 'not_applicable'))
end
end
subject { described_class.new(group, from: Time.now, current_user: user, options: { projects: [project.id, project_2.id] }).data }
it 'shows deploys from those projects' do
expect(subject.second[:value]).to eq(2)
end
end
end end
context 'with other projects' do context 'with other projects' do
@ -84,5 +110,6 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do
expect(subject.second[:value]).to eq(0) expect(subject.second[:value]).to eq(0)
end end
end end
end end
end end

View file

@ -71,6 +71,29 @@ describe Gitlab::CycleAnalytics::IssueStage do
end end
end end
context 'when only part of projects is chosen' do
let(:stage) { described_class.new(options: { from: 2.days.ago, current_user: user, group: group, projects: [project_2.id] }) }
describe '#group_median' do
around do |example|
Timecop.freeze { example.run }
end
it 'counts median from issues with metrics' do
expect(stage.group_median).to eq(ISSUES_MEDIAN)
end
end
describe '#events' do
it 'exposes merge requests that close issues' do
result = stage.events
expect(result.count).to eq(1)
expect(result.map { |event| event[:title] }).to contain_exactly(issue_2_1.title)
end
end
end
context 'when subgroup is given' do context 'when subgroup is given' do
let(:subgroup) { create(:group, parent: group) } let(:subgroup) { create(:group, parent: group) }
let(:project_4) { create(:project, group: subgroup) } let(:project_4) { create(:project, group: subgroup) }