Skip to content
Snippets Groups Projects
Verified Commit 8277b648 authored by Daniel Tian's avatar Daniel Tian :one: Committed by GitLab
Browse files

Improve standard role type

EE: true
Changelog: changed
parent 1ef51613
No related branches found
No related tags found
1 merge request!170013Add ability to filter standard roles by access level
Showing
with 190 additions and 57 deletions
......@@ -71,7 +71,7 @@ export const BASE_ROLES = [
accessLevel: ACCESS_LEVEL_DEVELOPER_INTEGER,
occupiesSeat: true,
description: s__(
'MemberRole|The Developer role strikes a balance between giving users the necessary access to contribute code while restricting sensitive administrative actions.',
'MemberRole|The Developer role gives users access to contribute code while restricting sensitive administrative actions.',
),
},
{
......
......@@ -5,11 +5,15 @@ class MemberAccessLevelEnum < BaseEnum
graphql_name 'MemberAccessLevel'
description 'Access level of a group or project member'
value 'GUEST', value: Gitlab::Access::GUEST, description: 'Guest access.'
value 'REPORTER', value: Gitlab::Access::REPORTER, description: 'Reporter access.'
value 'DEVELOPER', value: Gitlab::Access::DEVELOPER, description: 'Developer access.'
value 'MAINTAINER', value: Gitlab::Access::MAINTAINER, description: 'Maintainer access.'
value 'OWNER', value: Gitlab::Access::OWNER, description: 'Owner access.'
def self.descriptions
Gitlab::Access.option_descriptions
end
value 'GUEST', value: Gitlab::Access::GUEST, description: descriptions[:guest]
value 'REPORTER', value: Gitlab::Access::REPORTER, description: descriptions[:reporter]
value 'DEVELOPER', value: Gitlab::Access::DEVELOPER, description: descriptions[:developer]
value 'MAINTAINER', value: Gitlab::Access::MAINTAINER, description: descriptions[:maintainer]
value 'OWNER', value: Gitlab::Access::OWNER, description: descriptions[:owner]
end
end
......
......@@ -741,7 +741,7 @@ four standard [pagination arguments](#pagination-arguments):
 
### `Query.memberRole`
 
Finds a single custom role.
Finds a single custom role for the instance. Available only for self-managed.
 
DETAILS:
**Introduced** in GitLab 16.6.
......@@ -773,7 +773,7 @@ four standard [pagination arguments](#pagination-arguments):
 
### `Query.memberRoles`
 
Member roles available for the instance.
Custom roles available for the instance. Available only for self-managed.
 
DETAILS:
**Introduced** in GitLab 16.7.
......@@ -1136,9 +1136,25 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="querysnippetstype"></a>`type` | [`TypeEnum`](#typeenum) | Type of snippet. |
| <a id="querysnippetsvisibility"></a>`visibility` | [`VisibilityScopesEnum`](#visibilityscopesenum) | Visibility of the snippet. |
 
### `Query.standardRole`
Finds a single default role for the instance. Available only for self-managed.
DETAILS:
**Introduced** in GitLab 17.6.
**Status**: Experiment.
Returns [`StandardRole`](#standardrole).
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="querystandardroleaccesslevel"></a>`accessLevel` | [`[MemberAccessLevel!]`](#memberaccesslevel) | Access level or levels to filter by. |
### `Query.standardRoles`
 
Standard roles available for the instance, available only for self-managed.
Default roles available for the instance. Available only for self-managed.
 
DETAILS:
**Introduced** in GitLab 17.3.
......@@ -1150,6 +1166,12 @@ This field returns a [connection](#connections). It accepts the
four standard [pagination arguments](#pagination-arguments):
`before: String`, `after: String`, `first: Int`, and `last: Int`.
 
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="querystandardrolesaccesslevel"></a>`accessLevel` | [`[MemberAccessLevel!]`](#memberaccesslevel) | Access level or levels to filter by. |
### `Query.subscriptionFutureEntries`
 
Fields related to entries in future subscriptions.
......@@ -23981,7 +24003,6 @@ GPG signature for a signed commit.
| <a id="groupsharewithgrouplock"></a>`shareWithGroupLock` | [`Boolean`](#boolean) | Indicates if sharing a project with another group within this group is prevented. |
| <a id="groupsharedrunnerssetting"></a>`sharedRunnersSetting` | [`SharedRunnersSetting`](#sharedrunnerssetting) | Shared runners availability for the namespace and its descendants. |
| <a id="groupsidebar"></a>`sidebar` **{warning-solid}** | [`NamespaceSidebar`](#namespacesidebar) | **Introduced** in GitLab 17.6. **Status**: Experiment. Data needed to render the sidebar for the namespace. |
| <a id="groupstandardroles"></a>`standardRoles` **{warning-solid}** | [`StandardRoleConnection`](#standardroleconnection) | **Introduced** in GitLab 17.4. **Status**: Experiment. Standard roles available for the instance, available only for self-managed. |
| <a id="groupstats"></a>`stats` | [`GroupStats`](#groupstats) | Group statistics. |
| <a id="groupstoragesizelimit"></a>`storageSizeLimit` | [`Float`](#float) | The storage limit (in bytes) included with the root namespace plan. This limit only applies to namespaces under namespace limit enforcement. |
| <a id="groupsubgroupcreationlevel"></a>`subgroupCreationLevel` | [`String`](#string) | Permission level required to create subgroups within the group. |
......@@ -24783,7 +24804,7 @@ four standard [pagination arguments](#pagination-arguments):
 
##### `Group.memberRoles`
 
Member roles available for the group.
Custom roles available for the group.
 
DETAILS:
**Introduced** in GitLab 16.5.
......@@ -25194,6 +25215,42 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="groupsecuritypolicyprojectsuggestionsonlylinked"></a>`onlyLinked` | [`Boolean`](#boolean) | Whether to suggest only projects already linked as security policy projects. |
| <a id="groupsecuritypolicyprojectsuggestionssearch"></a>`search` | [`String!`](#string) | Search query for projects' full paths. |
 
##### `Group.standardRole`
Finds a single default role for the group. Available only for SaaS.
DETAILS:
**Introduced** in GitLab 17.6.
**Status**: Experiment.
Returns [`StandardRole`](#standardrole).
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="groupstandardroleaccesslevel"></a>`accessLevel` | [`[MemberAccessLevel!]`](#memberaccesslevel) | Access level or levels to filter by. |
##### `Group.standardRoles`
Default roles available for the group. Available only for SaaS.
DETAILS:
**Introduced** in GitLab 17.4.
**Status**: Experiment.
Returns [`StandardRoleConnection`](#standardroleconnection).
This field returns a [connection](#connections). It accepts the
four standard [pagination arguments](#pagination-arguments):
`before: String`, `after: String`, `first: Int`, and `last: Int`.
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="groupstandardrolesaccesslevel"></a>`accessLevel` | [`[MemberAccessLevel!]`](#memberaccesslevel) | Access level or levels to filter by. |
##### `Group.timelogs`
 
Time logged on issues and merge requests in the group and its subgroups.
......@@ -26621,11 +26678,11 @@ Represents a member role.
| ---- | ---- | ----------- |
| <a id="memberrolebaseaccesslevel"></a>`baseAccessLevel` **{warning-solid}** | [`AccessLevel!`](#accesslevel) | **Introduced** in GitLab 16.5. **Status**: Experiment. Base access level for the custom role. |
| <a id="memberrolecreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of when the member role was created. |
| <a id="memberroledescription"></a>`description` | [`String`](#string) | Description of the member role. |
| <a id="memberroledescription"></a>`description` | [`String`](#string) | Role description. |
| <a id="memberroledetailspath"></a>`detailsPath` **{warning-solid}** | [`String`](#string) | **Introduced** in GitLab 17.4. **Status**: Experiment. URL path to the role details webpage. |
| <a id="memberroleeditpath"></a>`editPath` **{warning-solid}** | [`String!`](#string) | **Introduced** in GitLab 16.11. **Status**: Experiment. Web UI path to edit the custom role. |
| <a id="memberroleenabledpermissions"></a>`enabledPermissions` **{warning-solid}** | [`CustomizablePermissionConnection!`](#customizablepermissionconnection) | **Introduced** in GitLab 16.5. **Status**: Experiment. Array of all permissions enabled for the custom role. |
| <a id="memberroleid"></a>`id` | [`MemberRoleID!`](#memberroleid) | ID of the member role. |
| <a id="memberroleid"></a>`id` | [`ID!`](#id) | Role ID. |
| <a id="memberrolememberscount"></a>`membersCount` **{warning-solid}** | [`Int`](#int) | **Introduced** in GitLab 17.3. **Status**: Experiment. Number of times the role has been directly assigned to a group or project member. |
| <a id="memberrolename"></a>`name` | [`String`](#string) | Role name. |
| <a id="memberroleuserscount"></a>`usersCount` **{warning-solid}** | [`Int`](#int) | **Introduced** in GitLab 17.5. **Status**: Experiment. Number of users who have been directly assigned the role in at least one group or project. |
......@@ -34171,7 +34228,9 @@ Represents a standard role.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="standardroleaccesslevel"></a>`accessLevel` | [`Int!`](#int) | Access level as a number. |
| <a id="standardroledescription"></a>`description` | [`String`](#string) | Role description. |
| <a id="standardroledetailspath"></a>`detailsPath` **{warning-solid}** | [`String`](#string) | **Introduced** in GitLab 17.4. **Status**: Experiment. URL path to the role details webpage. |
| <a id="standardroleid"></a>`id` | [`ID!`](#id) | Role ID. |
| <a id="standardrolememberscount"></a>`membersCount` **{warning-solid}** | [`Int`](#int) | **Introduced** in GitLab 17.3. **Status**: Experiment. Number of times the role has been directly assigned to a group or project member. |
| <a id="standardrolename"></a>`name` | [`String`](#string) | Role name. |
| <a id="standardroleuserscount"></a>`usersCount` **{warning-solid}** | [`Int`](#int) | **Introduced** in GitLab 17.5. **Status**: Experiment. Number of users who have been directly assigned the role in at least one group or project. |
......@@ -38684,12 +38743,12 @@ Access level of a group or project member.
 
| Value | Description |
| ----- | ----------- |
| <a id="memberaccessleveldeveloper"></a>`DEVELOPER` | Developer access. |
| <a id="memberaccesslevelguest"></a>`GUEST` | Guest access. |
| <a id="memberaccesslevelmaintainer"></a>`MAINTAINER` | Maintainer access. |
| <a id="memberaccesslevelminimal_access"></a>`MINIMAL_ACCESS` | Minimal access. |
| <a id="memberaccesslevelowner"></a>`OWNER` | Owner access. |
| <a id="memberaccesslevelreporter"></a>`REPORTER` | Reporter access. |
| <a id="memberaccessleveldeveloper"></a>`DEVELOPER` | The Developer role gives users access to contribute code while restricting sensitive administrative actions. |
| <a id="memberaccesslevelguest"></a>`GUEST` | The Guest role is for users who need visibility into a project or group but should not have the ability to make changes, such as external stakeholders. |
| <a id="memberaccesslevelmaintainer"></a>`MAINTAINER` | The Maintainer role is primarily used for managing code reviews, approvals, and administrative settings for projects. This role can also manage project memberships. |
| <a id="memberaccesslevelminimal_access"></a>`MINIMAL_ACCESS` | The Minimal Access role is for users who need the least amount of access into groups and projects. You can assign this role as a default, before giving a user another role with more permissions. |
| <a id="memberaccesslevelowner"></a>`OWNER` | The Owner role is normally assigned to the individual or team responsible for managing and maintaining the group or creating the project. This role has the highest level of administrative control, and can manage all aspects of the group or project, including managing other Owners. |
| <a id="memberaccesslevelreporter"></a>`REPORTER` | The Reporter role is suitable for team members who need to stay informed about a project or group but do not actively contribute code. |
 
### `MemberAccessLevelName`
 
......@@ -42134,7 +42193,9 @@ Implementations:
 
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="roleinterfacedescription"></a>`description` | [`String`](#string) | Role description. |
| <a id="roleinterfacedetailspath"></a>`detailsPath` **{warning-solid}** | [`String`](#string) | **Introduced** in GitLab 17.4. **Status**: Experiment. URL path to the role details webpage. |
| <a id="roleinterfaceid"></a>`id` | [`ID!`](#id) | Role ID. |
| <a id="roleinterfacememberscount"></a>`membersCount` **{warning-solid}** | [`Int`](#int) | **Introduced** in GitLab 17.3. **Status**: Experiment. Number of times the role has been directly assigned to a group or project member. |
| <a id="roleinterfacename"></a>`name` | [`String`](#string) | Role name. |
| <a id="roleinterfaceuserscount"></a>`usersCount` **{warning-solid}** | [`Int`](#int) | **Introduced** in GitLab 17.5. **Status**: Experiment. Number of users who have been directly assigned the role in at least one group or project. |
......@@ -244,12 +244,17 @@ module GroupType
authorize: :admin_external_audit_events
field :member_roles, ::Types::MemberRoles::MemberRoleType.connection_type,
null: true, description: 'Member roles available for the group.',
null: true, description: 'Custom roles available for the group.',
resolver: ::Resolvers::MemberRoles::RolesResolver,
alpha: { milestone: '16.5' }
field :standard_role, ::Types::Members::StandardRoleType,
null: true, description: 'Finds a single default role for the group. Available only for SaaS.',
resolver: ::Resolvers::Members::StandardRolesResolver.single,
alpha: { milestone: '17.6' }
field :standard_roles, ::Types::Members::StandardRoleType.connection_type,
null: true, description: 'Standard roles available for the instance, available only for self-managed.',
null: true, description: 'Default roles available for the group. Available only for SaaS.',
resolver: ::Resolvers::Members::StandardRolesResolver,
alpha: { milestone: '17.4' }
......
......@@ -6,7 +6,8 @@ module MemberAccessLevelEnum
extend ActiveSupport::Concern
prepended do
value 'MINIMAL_ACCESS', value: ::Gitlab::Access::MINIMAL_ACCESS, description: 'Minimal access.'
value 'MINIMAL_ACCESS', value: ::Gitlab::Access::MINIMAL_ACCESS,
description: ::Gitlab::Access.option_descriptions[:minimal_access]
end
end
end
......
......@@ -151,11 +151,15 @@ module QueryType
description: 'List of all customizable permissions.',
alpha: { milestone: '16.4' }
field :member_role, ::Types::MemberRoles::MemberRoleType,
null: true, description: 'Finds a single custom role.',
null: true, description: 'Finds a single custom role for the instance. Available only for self-managed.',
resolver: ::Resolvers::MemberRoles::RolesResolver.single,
alpha: { milestone: '16.6' }
field :standard_role, ::Types::Members::StandardRoleType,
null: true, description: 'Finds a single default role for the instance. Available only for self-managed.',
resolver: ::Resolvers::Members::StandardRolesResolver.single,
alpha: { milestone: '17.6' }
field :standard_roles, ::Types::Members::StandardRoleType.connection_type,
null: true, description: 'Standard roles available for the instance, available only for self-managed.',
null: true, description: 'Default roles available for the instance. Available only for self-managed.',
resolver: ::Resolvers::Members::StandardRolesResolver,
alpha: { milestone: '17.3' }
field :self_managed_add_on_eligible_users,
......@@ -178,7 +182,7 @@ module QueryType
description: 'Instance-level Amazon S3 configurations for audit events.',
resolver: ::Resolvers::AuditEvents::Instance::AmazonS3ConfigurationsResolver
field :member_roles, ::Types::MemberRoles::MemberRoleType.connection_type,
null: true, description: 'Member roles available for the instance.',
null: true, description: 'Custom roles available for the instance. Available only for self-managed.',
resolver: ::Resolvers::MemberRoles::RolesResolver,
alpha: { milestone: '16.7' }
field :google_cloud_artifact_registry_repository_artifact,
......
......@@ -9,14 +9,21 @@ class StandardRolesResolver < BaseResolver
type Types::Members::StandardRoleType, null: true
def resolve_with_lookahead
result = Gitlab::Access.options_with_minimal_access.map do |name, access_level|
members_row = member_counts.find { |c| c.access_level == access_level } if selects_field?(:members_count)
users_row = user_counts.find { |c| c.access_level == access_level } if selects_field?(:users_count)
argument :access_level, [Types::MemberAccessLevelEnum],
required: false,
description: 'Access level or levels to filter by.'
def resolve_with_lookahead(access_level: nil)
options = Gitlab::Access.options_with_minimal_access
options = options.select { |_, level| access_level.include?(level) } if access_level.present?
result = options.map do |name, access_level_id|
members_row = member_counts.find { |c| c.access_level == access_level_id } if selects_field?(:members_count)
users_row = user_counts.find { |c| c.access_level == access_level_id } if selects_field?(:users_count)
{
name: name,
access_level: access_level,
access_level: access_level_id,
members_count: members_row&.members_count || 0,
users_count: users_row&.users_count || 0,
group: object
......
......@@ -14,16 +14,6 @@ class MemberRoleType < BaseObject
implements Types::Members::RoleInterface
field :id,
::Types::GlobalIDType[::MemberRole],
null: false,
description: 'ID of the member role.'
field :description,
GraphQL::Types::String,
null: true,
description: 'Description of the member role.'
field :base_access_level,
Types::AccessLevelType,
null: false,
......
......@@ -5,10 +5,20 @@ module Members
module RoleInterface
include BaseInterface
field :id,
GraphQL::Types::ID,
null: false,
description: 'Role ID.'
field :name,
GraphQL::Types::String,
description: 'Role name.'
field :description,
GraphQL::Types::String,
null: true,
description: 'Role description.'
field :members_count,
GraphQL::Types::Int,
alpha: { milestone: '17.3' },
......
......@@ -17,20 +17,31 @@ class StandardRoleType < BaseObject
null: false,
description: 'Access level as a number.'
def details_path
access_level = object[:access_level]
access_enum = access_enums[access_level].upcase
def id
"gid://gitlab/StandardRole/#{access_level_enum}"
end
def description
Types::MemberAccessLevelEnum.values[access_level_enum].description
end
def details_path
enum = access_level_enum
group = object[:group]
access_enum.define_singleton_method(:namespace) { group }
enum.define_singleton_method(:namespace) { group }
member_role_details_path(enum)
end
member_role_details_path(access_enum)
def access_level_enum
access_level = object[:access_level]
access_level_enums[access_level].upcase
end
def access_enums
def access_level_enums
Types::MemberAccessLevelEnum.enum.invert
end
strong_memoize_attr :access_enums
strong_memoize_attr :access_level_enums
end
# rubocop: enable Graphql/AuthorizeTypes
end
......
......@@ -32,6 +32,15 @@ def values_with_minimal_access
def human_access(access, member_role = nil)
member_role&.name || options_with_minimal_access.key(access)
end
override :option_descriptions
def option_descriptions
super.merge({
minimal_access: s_('MemberRole|The Minimal Access role is for users who need the least amount of access ' \
'into groups and projects. You can assign this role as a default, before giving a user another role ' \
'with more permissions.')
})
end
end
override :human_access
......
......@@ -47,7 +47,7 @@ export const standardRoles = [
usersCount: 3,
detailsPath: 'role/DEVELOPER',
description:
'The Developer role strikes a balance between giving users the necessary access to contribute code while restricting sensitive administrative actions.',
'The Developer role gives users access to contribute code while restricting sensitive administrative actions.',
},
{
accessLevel: 40,
......
......@@ -30,6 +30,7 @@
it { expect(described_class).to have_graphql_field(:project_compliance_standards_adherence) }
it { expect(described_class).to have_graphql_field(:amazon_s3_configurations) }
it { expect(described_class).to have_graphql_field(:member_roles) }
it { expect(described_class).to have_graphql_field(:standard_role) }
it { expect(described_class).to have_graphql_field(:standard_roles) }
it { expect(described_class).to have_graphql_field(:pending_members) }
it { expect(described_class).to have_graphql_field(:value_streams) }
......
......@@ -7,30 +7,49 @@
describe '#resolve' do
subject(:result) do
resolve(described_class, obj: group, lookahead: positive_lookahead, arg_style: :internal)
resolve(described_class, obj: group, args: args, lookahead: positive_lookahead, arg_style: :internal)
end
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
let_it_be(:user2) { create(:user) }
before do
group.add_member(user, ::Gitlab::Access::MAINTAINER)
group.add_member(user2, ::Gitlab::Access::DEVELOPER)
end
context 'when a user has maintainer access' do
before do
group.add_member(user, ::Gitlab::Access::MAINTAINER)
end
let_it_be(:args) { nil }
it 'returns the totals for each standard role' do
expect(result).to be_present
expect(result.count).to eq(6)
roles_with_members = [::Gitlab::Access::MAINTAINER, ::Gitlab::Access::DEVELOPER]
::Gitlab::Access.options_with_minimal_access.sort_by { |_, v| v }.each_with_index do |(name, value), index|
role = result[index]
expect(role[:access_level]).to eq(value)
expect(role[:name]).to eq(name)
expect(role[:members_count]).to eq(value == ::Gitlab::Access::MAINTAINER ? 1 : 0)
expect(role[:users_count]).to eq(value == ::Gitlab::Access::MAINTAINER ? 1 : 0)
expect(role[:members_count]).to eq(roles_with_members.include?(value) ? 1 : 0)
expect(role[:users_count]).to eq(roles_with_members.include?(value) ? 1 : 0)
expect(role[:group]).to eq(group)
end
end
end
context 'when filtering by a single access_level' do
let_it_be(:args) { { access_level: [::Gitlab::Access::MAINTAINER] } }
it 'returns only the specified role' do
expect(result.count).to eq(1)
role = result.first
expect(role[:access_level]).to eq(::Gitlab::Access::MAINTAINER)
expect(role[:members_count]).to eq(1)
expect(role[:users_count]).to eq(1)
end
end
end
end
......@@ -4,7 +4,7 @@
RSpec.describe Types::Members::RoleInterface, feature_category: :system_access do
it 'exposes the expected fields' do
expected_fields = %i[name membersCount usersCount detailsPath]
expected_fields = %i[id name description membersCount usersCount detailsPath]
expect(described_class).to have_graphql_fields(*expected_fields)
end
......
......@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['StandardRole'], feature_category: :system_access do
let(:fields) { %w[accessLevel name membersCount usersCount detailsPath] }
let(:fields) { %w[id accessLevel name description membersCount usersCount detailsPath] }
specify { expect(described_class.graphql_name).to eq('StandardRole') }
......
......@@ -41,6 +41,7 @@
:member_role,
:self_managed_add_on_eligible_users,
:member_roles,
:standard_role,
:standard_roles,
:google_cloud_artifact_registry_repository_artifact,
:audit_events_instance_streaming_destinations,
......
......@@ -63,6 +63,16 @@ def options_with_none
)
end
def option_descriptions
{
guest: s_('MemberRole|The Guest role is for users who need visibility into a project or group but should not have the ability to make changes, such as external stakeholders.'),
reporter: s_('MemberRole|The Reporter role is suitable for team members who need to stay informed about a project or group but do not actively contribute code.'),
developer: s_('MemberRole|The Developer role gives users access to contribute code while restricting sensitive administrative actions.'),
maintainer: s_('MemberRole|The Maintainer role is primarily used for managing code reviews, approvals, and administrative settings for projects. This role can also manage project memberships.'),
owner: s_('MemberRole|The Owner role is normally assigned to the individual or team responsible for managing and maintaining the group or creating the project. This role has the highest level of administrative control, and can manage all aspects of the group or project, including managing other Owners.')
}
end
def sym_options
{
guest: GUEST,
......
......@@ -33562,7 +33562,7 @@ msgstr ""
msgid "MemberRole|Select at least one permission."
msgstr ""
 
msgid "MemberRole|The Developer role strikes a balance between giving users the necessary access to contribute code while restricting sensitive administrative actions."
msgid "MemberRole|The Developer role gives users access to contribute code while restricting sensitive administrative actions."
msgstr ""
 
msgid "MemberRole|The Guest role is for users who need visibility into a project or group but should not have the ability to make changes, such as external stakeholders."
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment