Commit 5d2f132a authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch '33142-count-users-in-nested-projects-on-gitlab-com' into 'master'

Count users in nested projects on Gitlab.com

Closes #33142

See merge request gitlab-org/gitlab!22967
parents d7984102 54f976e4
...@@ -75,6 +75,7 @@ class Member < ApplicationRecord ...@@ -75,6 +75,7 @@ class Member < ApplicationRecord
scope :reporters, -> { active.where(access_level: REPORTER) } scope :reporters, -> { active.where(access_level: REPORTER) }
scope :developers, -> { active.where(access_level: DEVELOPER) } scope :developers, -> { active.where(access_level: DEVELOPER) }
scope :maintainers, -> { active.where(access_level: MAINTAINER) } scope :maintainers, -> { active.where(access_level: MAINTAINER) }
scope :non_guests, -> { where('members.access_level > ?', GUEST) }
scope :masters, -> { maintainers } # @deprecated scope :masters, -> { maintainers } # @deprecated
scope :owners, -> { active.where(access_level: OWNER) } scope :owners, -> { active.where(access_level: OWNER) }
scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) } scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) }
......
...@@ -248,12 +248,22 @@ module EE ...@@ -248,12 +248,22 @@ module EE
# For now, we are not billing for members with a Guest role for subscriptions # For now, we are not billing for members with a Guest role for subscriptions
# with a Gold plan. The other plans will treat Guest members as a regular member # with a Gold plan. The other plans will treat Guest members as a regular member
# for billing purposes. # for billing purposes.
#
# We are plucking the user_ids from the "Members" table in an array and
# concatenating the array of user_ids with ruby "|" (pipe) method to generate
# one single array of unique user_ids.
override :billable_members_count override :billable_members_count
def billable_members_count(requested_hosted_plan = nil) def billable_members_count(requested_hosted_plan = nil)
if [actual_plan_name, requested_hosted_plan].include?(Plan::GOLD) if [actual_plan_name, requested_hosted_plan].include?(Plan::GOLD)
users_with_descendants.excluding_guests.count (billed_group_members.non_guests.distinct.pluck(:user_id) |
billed_project_members.non_guests.distinct.pluck(:user_id) |
billed_shared_group_members.non_guests.distinct.pluck(:user_id) |
billed_invited_group_members.non_guests.distinct.pluck(:user_id)).count
else else
users_with_descendants.count (billed_group_members.distinct.pluck(:user_id) |
billed_project_members.distinct.pluck(:user_id) |
billed_shared_group_members.distinct.pluck(:user_id) |
billed_invited_group_members.distinct.pluck(:user_id)).count
end end
end end
...@@ -296,5 +306,36 @@ module EE ...@@ -296,5 +306,36 @@ module EE
errors.add(:custom_project_templates_group_id, "has to be a subgroup of the group") errors.add(:custom_project_templates_group_id, "has to be a subgroup of the group")
end end
def billed_group_members
::GroupMember.active_without_invites_and_requests.where(
source_id: self_and_descendants
)
end
def billed_project_members
::ProjectMember.active_without_invites_and_requests.where(
source_id: ::Project.joins(:group).where(namespace: self_and_descendants)
)
end
def billed_invited_group_members
invited_or_shared_group_members(invited_groups_in_projects)
end
def billed_shared_group_members
return ::GroupMember.none unless ::Feature.enabled?(:share_group_with_group)
invited_or_shared_group_members(shared_groups)
end
def invited_or_shared_group_members(groups)
::GroupMember.active_without_invites_and_requests.where(source_id: ::Gitlab::ObjectHierarchy.new(groups).base_and_ancestors)
end
def invited_groups_in_projects
::Group.joins(:project_group_links)
.where(project_group_links: { project_id: all_projects })
end
end end
end end
...@@ -62,7 +62,7 @@ module EE ...@@ -62,7 +62,7 @@ module EE
scope scope
} }
scope :excluding_guests, -> { joins(:members).where('members.access_level > ?', ::Gitlab::Access::GUEST).distinct } scope :excluding_guests, -> { joins(:members).merge(::Member.non_guests).distinct }
scope :subscribed_for_admin_email, -> { where(admin_email_unsubscribed_at: nil) } scope :subscribed_for_admin_email, -> { where(admin_email_unsubscribed_at: nil) }
scope :ldap, -> { joins(:identities).where('identities.provider LIKE ?', 'ldap%') } scope :ldap, -> { joins(:identities).where('identities.provider LIKE ?', 'ldap%') }
......
---
title: Include users from all sub-projects and shared groups when counting billing seats currently in use
merge_request: 22967
author:
type: fixed
...@@ -843,28 +843,159 @@ describe Namespace do ...@@ -843,28 +843,159 @@ describe Namespace do
context 'with a group namespace' do context 'with a group namespace' do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:developer) { create(:user) } let(:developer) { create(:user) }
let(:guest) { create(:user) }
before do before do
group.add_developer(developer) group.add_developer(developer)
group.add_guest(guest) group.add_developer(create(:user, :blocked))
group.add_guest(create(:user))
end end
context 'with a gold plan' do context 'with a gold plan' do
it 'does not count guest users' do before do
create(:gitlab_subscription, namespace: group, hosted_plan: gold_plan) create(:gitlab_subscription, namespace: group, hosted_plan: gold_plan)
end
it 'does not count guest users and counts only active users' do
expect(group.billable_members_count).to eq(1) expect(group.billable_members_count).to eq(1)
end end
context 'when group has a project and users invited to it' do
let(:project) { create(:project, namespace: group) }
before do
project.add_developer(create(:user))
project.add_guest(create(:user))
project.add_developer(developer)
project.add_developer(create(:user, :blocked))
end
it 'includes invited active users except guests to the group' do
expect(group.billable_members_count).to eq(2)
end
context 'when group is invited to the project' do
let(:invited_group) { create(:group) }
before do
invited_group.add_developer(create(:user))
invited_group.add_guest(create(:user))
invited_group.add_developer(create(:user, :blocked))
invited_group.add_developer(developer)
create(:project_group_link, project: project, group: invited_group)
end
it 'counts the only active users except guests of the invited groups' do
expect(group.billable_members_count).to eq(3)
end
end
end
context 'when group has been shared with another group' do
let(:shared_group) { create(:group) }
before do
shared_group.add_developer(create(:user))
shared_group.add_guest(create(:user))
shared_group.add_developer(create(:user, :blocked))
create(:group_group_link, { shared_with_group: group,
shared_group: shared_group })
end
context 'when feature is not enabled' do
before do
stub_feature_flags(share_group_with_group: false)
end
it 'does not include users coming from the shared groups' do
expect(group.billable_members_count).to eq(1)
end
end
context 'when feature is enabled' do
before do
stub_feature_flags(share_group_with_group: true)
end
it 'includes active users from the shared group to the billed members count' do
expect(group.billable_members_count).to eq(2)
end
end
end
end end
context 'with other plans' do context 'with other plans' do
%i[bronze_plan silver_plan].each do |plan| %i[bronze_plan silver_plan].each do |plan|
it 'counts guest users' do it 'counts active guest users' do
create(:gitlab_subscription, namespace: group, hosted_plan: send(plan)) create(:gitlab_subscription, namespace: group, hosted_plan: send(plan))
expect(group.billable_members_count).to eq(2) expect(group.billable_members_count).to eq(2)
end end
context 'when group has a project and users invited to it' do
let(:project) { create(:project, namespace: group) }
before do
create(:gitlab_subscription, namespace: group, hosted_plan: send(plan))
project.add_developer(create(:user))
project.add_guest(create(:user))
project.add_developer(create(:user, :blocked))
project.add_developer(developer)
end
it 'includes invited active users to the group' do
expect(group.billable_members_count).to eq(4)
end
context 'when group is invited to the project' do
let(:invited_group) { create(:group) }
before do
invited_group.add_developer(create(:user))
invited_group.add_developer(developer)
invited_group.add_guest(create(:user))
invited_group.add_developer(create(:user, :blocked))
create(:project_group_link, project: project, group: invited_group)
end
it 'counts the unique active users including guests of the invited groups' do
expect(group.billable_members_count).to eq(6)
end
end
end
context 'when group has been shared with another group' do
let(:shared_group) { create(:group) }
before do
create(:gitlab_subscription, namespace: group, hosted_plan: send(plan))
shared_group.add_developer(create(:user))
shared_group.add_guest(create(:user))
shared_group.add_developer(create(:user, :blocked))
create(:group_group_link, { shared_with_group: group,
shared_group: shared_group })
end
context 'when feature is not enabled' do
before do
stub_feature_flags(share_group_with_group: false)
end
it 'does not include users coming from the shared groups' do
expect(group.billable_members_count).to eq(2)
end
end
context 'when feature is enabled' do
before do
stub_feature_flags(share_group_with_group: true)
end
it 'includes active users from the shared group including guests to the billed members count' do
expect(group.billable_members_count).to eq(4)
end
end
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