Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
051c4be4
Commit
051c4be4
authored
May 26, 2020
by
Igor Drozdov
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch '208412-featurable' into 'master'
Extract featurable concern See merge request gitlab-org/gitlab!31700
parents
9eaf886f
969b7f01
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
296 additions
and
231 deletions
+296
-231
app/models/concerns/featurable.rb
app/models/concerns/featurable.rb
+99
-0
app/models/project_feature.rb
app/models/project_feature.rb
+7
-99
changelogs/unreleased/208412-featurable.yml
changelogs/unreleased/208412-featurable.yml
+5
-0
ee/spec/elastic_integration/global_search_spec.rb
ee/spec/elastic_integration/global_search_spec.rb
+1
-1
spec/models/concerns/featurable_spec.rb
spec/models/concerns/featurable_spec.rb
+184
-0
spec/models/project_feature_spec.rb
spec/models/project_feature_spec.rb
+0
-131
No files found.
app/models/concerns/featurable.rb
0 → 100644
View file @
051c4be4
# frozen_string_literal: true
# == Featurable concern
#
# This concern adds features (tools) functionality to Project and Group
# To enable features you need to call `set_available_features`
#
# Example:
#
# class ProjectFeature
# include Featurable
# set_available_features %i(wiki merge_request)
module
Featurable
extend
ActiveSupport
::
Concern
# Can be enabled only for members, everyone or disabled
# Access control is made only for non private containers.
#
# Permission levels:
#
# Disabled: not enabled for anyone
# Private: enabled only for team members
# Enabled: enabled for everyone able to access the project
# Public: enabled for everyone (only allowed for pages)
DISABLED
=
0
PRIVATE
=
10
ENABLED
=
20
PUBLIC
=
30
STRING_OPTIONS
=
HashWithIndifferentAccess
.
new
({
'disabled'
=>
DISABLED
,
'private'
=>
PRIVATE
,
'enabled'
=>
ENABLED
,
'public'
=>
PUBLIC
}).
freeze
class_methods
do
def
set_available_features
(
available_features
=
[])
@available_features
=
available_features
class_eval
do
available_features
.
each
do
|
feature
|
define_method
(
"
#{
feature
}
_enabled?"
)
do
public_send
(
"
#{
feature
}
_access_level"
)
>
DISABLED
# rubocop:disable GitlabSecurity/PublicSend
end
end
end
end
def
available_features
@available_features
end
def
access_level_attribute
(
feature
)
feature
=
ensure_feature!
(
feature
)
"
#{
feature
}
_access_level"
.
to_sym
end
def
quoted_access_level_column
(
feature
)
attribute
=
connection
.
quote_column_name
(
access_level_attribute
(
feature
))
table
=
connection
.
quote_table_name
(
table_name
)
"
#{
table
}
.
#{
attribute
}
"
end
def
access_level_from_str
(
level
)
STRING_OPTIONS
.
fetch
(
level
)
end
def
str_from_access_level
(
level
)
STRING_OPTIONS
.
key
(
level
)
end
def
ensure_feature!
(
feature
)
feature
=
feature
.
model_name
.
plural
if
feature
.
respond_to?
(
:model_name
)
feature
=
feature
.
to_sym
raise
ArgumentError
,
"invalid feature:
#{
feature
}
"
unless
available_features
.
include?
(
feature
)
feature
end
end
def
access_level
(
feature
)
public_send
(
self
.
class
.
access_level_attribute
(
feature
))
# rubocop:disable GitlabSecurity/PublicSend
end
def
feature_available?
(
feature
,
user
)
# This feature might not be behind a feature flag at all, so default to true
return
false
unless
::
Feature
.
enabled?
(
feature
,
user
,
default_enabled:
true
)
get_permission
(
user
,
feature
)
end
def
string_access_level
(
feature
)
self
.
class
.
str_from_access_level
(
access_level
(
feature
))
end
end
app/models/project_feature.rb
View file @
051c4be4
# frozen_string_literal: true
# frozen_string_literal: true
class
ProjectFeature
<
ApplicationRecord
class
ProjectFeature
<
ApplicationRecord
# == Project features permissions
include
Featurable
#
# Grants access level to project tools
#
# Tools can be enabled only for users, everyone or disabled
# Access control is made only for non private projects
#
# levels:
#
# Disabled: not enabled for anyone
# Private: enabled only for team members
# Enabled: enabled for everyone able to access the project
# Public: enabled for everyone (only allowed for pages)
#
# Permission levels
DISABLED
=
0
PRIVATE
=
10
ENABLED
=
20
PUBLIC
=
30
FEATURES
=
%i(issues forking merge_requests wiki snippets builds repository pages metrics_dashboard)
.
freeze
FEATURES
=
%i(issues forking merge_requests wiki snippets builds repository pages metrics_dashboard)
.
freeze
set_available_features
(
FEATURES
)
PRIVATE_FEATURES_MIN_ACCESS_LEVEL
=
{
merge_requests:
Gitlab
::
Access
::
REPORTER
,
metrics_dashboard:
Gitlab
::
Access
::
REPORTER
}.
freeze
PRIVATE_FEATURES_MIN_ACCESS_LEVEL
=
{
merge_requests:
Gitlab
::
Access
::
REPORTER
,
metrics_dashboard:
Gitlab
::
Access
::
REPORTER
}.
freeze
PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT
=
{
repository:
Gitlab
::
Access
::
REPORTER
}.
freeze
PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT
=
{
repository:
Gitlab
::
Access
::
REPORTER
}.
freeze
STRING_OPTIONS
=
HashWithIndifferentAccess
.
new
({
'disabled'
=>
DISABLED
,
'private'
=>
PRIVATE
,
'enabled'
=>
ENABLED
,
'public'
=>
PUBLIC
}).
freeze
class
<<
self
class
<<
self
def
access_level_attribute
(
feature
)
feature
=
ensure_feature!
(
feature
)
"
#{
feature
}
_access_level"
.
to_sym
end
def
quoted_access_level_column
(
feature
)
attribute
=
connection
.
quote_column_name
(
access_level_attribute
(
feature
))
table
=
connection
.
quote_table_name
(
table_name
)
"
#{
table
}
.
#{
attribute
}
"
end
def
required_minimum_access_level
(
feature
)
def
required_minimum_access_level
(
feature
)
feature
=
ensure_feature!
(
feature
)
feature
=
ensure_feature!
(
feature
)
...
@@ -60,24 +25,6 @@ class ProjectFeature < ApplicationRecord
...
@@ -60,24 +25,6 @@ class ProjectFeature < ApplicationRecord
required_minimum_access_level
(
feature
)
required_minimum_access_level
(
feature
)
end
end
end
end
def
access_level_from_str
(
level
)
STRING_OPTIONS
.
fetch
(
level
)
end
def
str_from_access_level
(
level
)
STRING_OPTIONS
.
key
(
level
)
end
private
def
ensure_feature!
(
feature
)
feature
=
feature
.
model_name
.
plural
if
feature
.
respond_to?
(
:model_name
)
feature
=
feature
.
to_sym
raise
ArgumentError
,
"invalid project feature:
#{
feature
}
"
unless
FEATURES
.
include?
(
feature
)
feature
end
end
end
# Default scopes force us to unscope here since a service may need to check
# Default scopes force us to unscope here since a service may need to check
...
@@ -107,45 +54,6 @@ class ProjectFeature < ApplicationRecord
...
@@ -107,45 +54,6 @@ class ProjectFeature < ApplicationRecord
end
end
end
end
def
feature_available?
(
feature
,
user
)
# This feature might not be behind a feature flag at all, so default to true
return
false
unless
::
Feature
.
enabled?
(
feature
,
user
,
default_enabled:
true
)
get_permission
(
user
,
feature
)
end
def
access_level
(
feature
)
public_send
(
ProjectFeature
.
access_level_attribute
(
feature
))
# rubocop:disable GitlabSecurity/PublicSend
end
def
string_access_level
(
feature
)
ProjectFeature
.
str_from_access_level
(
access_level
(
feature
))
end
def
builds_enabled?
builds_access_level
>
DISABLED
end
def
wiki_enabled?
wiki_access_level
>
DISABLED
end
def
merge_requests_enabled?
merge_requests_access_level
>
DISABLED
end
def
forking_enabled?
forking_access_level
>
DISABLED
end
def
issues_enabled?
issues_access_level
>
DISABLED
end
def
pages_enabled?
pages_access_level
>
DISABLED
end
def
public_pages?
def
public_pages?
return
true
unless
Gitlab
.
config
.
pages
.
access_control
return
true
unless
Gitlab
.
config
.
pages
.
access_control
...
@@ -164,7 +72,7 @@ class ProjectFeature < ApplicationRecord
...
@@ -164,7 +72,7 @@ class ProjectFeature < ApplicationRecord
# which cannot be higher than repository access level
# which cannot be higher than repository access level
def
repository_children_level
def
repository_children_level
validator
=
lambda
do
|
field
|
validator
=
lambda
do
|
field
|
level
=
public_send
(
field
)
||
ProjectFeature
::
ENABLED
# rubocop:disable GitlabSecurity/PublicSend
level
=
public_send
(
field
)
||
ENABLED
# rubocop:disable GitlabSecurity/PublicSend
not_allowed
=
level
>
repository_access_level
not_allowed
=
level
>
repository_access_level
self
.
errors
.
add
(
field
,
"cannot have higher visibility level than repository access level"
)
if
not_allowed
self
.
errors
.
add
(
field
,
"cannot have higher visibility level than repository access level"
)
if
not_allowed
end
end
...
@@ -175,8 +83,8 @@ class ProjectFeature < ApplicationRecord
...
@@ -175,8 +83,8 @@ class ProjectFeature < ApplicationRecord
# Validates access level for other than pages cannot be PUBLIC
# Validates access level for other than pages cannot be PUBLIC
def
allowed_access_levels
def
allowed_access_levels
validator
=
lambda
do
|
field
|
validator
=
lambda
do
|
field
|
level
=
public_send
(
field
)
||
ProjectFeature
::
ENABLED
# rubocop:disable GitlabSecurity/PublicSend
level
=
public_send
(
field
)
||
ENABLED
# rubocop:disable GitlabSecurity/PublicSend
not_allowed
=
level
>
ProjectFeature
::
ENABLED
not_allowed
=
level
>
ENABLED
self
.
errors
.
add
(
field
,
"cannot have public visibility level"
)
if
not_allowed
self
.
errors
.
add
(
field
,
"cannot have public visibility level"
)
if
not_allowed
end
end
...
...
changelogs/unreleased/208412-featurable.yml
0 → 100644
View file @
051c4be4
---
title
:
Extract featurable concern from ProjectFeature
merge_request
:
31700
author
:
Alexander Randa
type
:
other
ee/spec/elastic_integration/global_search_spec.rb
View file @
051c4be4
...
@@ -160,7 +160,7 @@ describe 'GlobalSearch', :elastic do
...
@@ -160,7 +160,7 @@ describe 'GlobalSearch', :elastic do
# access_level can be :disabled, :enabled or :private
# access_level can be :disabled, :enabled or :private
def
feature_settings
(
access_level
)
def
feature_settings
(
access_level
)
Hash
[
features
.
collect
{
|
k
|
[
"
#{
k
}
_access_level"
,
ProjectFeatur
e
.
const_get
(
access_level
.
to_s
.
upcase
,
false
)]
}]
Hash
[
features
.
collect
{
|
k
|
[
"
#{
k
}
_access_level"
,
Featurabl
e
.
const_get
(
access_level
.
to_s
.
upcase
,
false
)]
}]
end
end
def
expect_no_items_to_be_found
(
user
)
def
expect_no_items_to_be_found
(
user
)
...
...
spec/models/concerns/featurable_spec.rb
0 → 100644
View file @
051c4be4
# frozen_string_literal: true
require
'spec_helper'
describe
Featurable
do
let_it_be
(
:user
)
{
create
(
:user
)
}
let
(
:project
)
{
create
(
:project
)
}
let
(
:feature_class
)
{
subject
.
class
}
let
(
:features
)
{
feature_class
::
FEATURES
}
subject
{
project
.
project_feature
}
describe
'.quoted_access_level_column'
do
it
'returns the table name and quoted column name for a feature'
do
expected
=
'"project_features"."issues_access_level"'
expect
(
feature_class
.
quoted_access_level_column
(
:issues
)).
to
eq
(
expected
)
end
end
describe
'.access_level_attribute'
do
it
{
expect
(
feature_class
.
access_level_attribute
(
:wiki
)).
to
eq
:wiki_access_level
}
it
'raises error for unspecified feature'
do
expect
{
feature_class
.
access_level_attribute
(
:unknown
)
}
.
to
raise_error
(
ArgumentError
,
/invalid feature: unknown/
)
end
end
describe
'.set_available_features'
do
let!
(
:klass
)
do
Class
.
new
do
include
Featurable
set_available_features
%i(feature1 feature2)
def
feature1_access_level
Featurable
::
DISABLED
end
def
feature2_access_level
Featurable
::
ENABLED
end
end
end
let!
(
:instance
)
{
klass
.
new
}
it
{
expect
(
klass
.
available_features
).
to
eq
[
:feature1
,
:feature2
]
}
it
{
expect
(
instance
.
feature1_enabled?
).
to
be_falsey
}
it
{
expect
(
instance
.
feature2_enabled?
).
to
be_truthy
}
end
describe
'.available_features'
do
it
{
expect
(
feature_class
.
available_features
).
to
include
(
*
features
)
}
end
describe
'#access_level'
do
it
'returns access level'
do
expect
(
subject
.
access_level
(
:wiki
)).
to
eq
(
subject
.
wiki_access_level
)
end
end
describe
'#feature_available?'
do
let
(
:features
)
{
%w(issues wiki builds merge_requests snippets repository pages metrics_dashboard)
}
context
'when features are disabled'
do
it
"returns false"
do
update_all_project_features
(
project
,
features
,
ProjectFeature
::
DISABLED
)
features
.
each
do
|
feature
|
expect
(
project
.
feature_available?
(
feature
.
to_sym
,
user
)).
to
eq
(
false
),
"
#{
feature
}
failed"
end
end
end
context
'when features are enabled only for team members'
do
it
"returns false when user is not a team member"
do
update_all_project_features
(
project
,
features
,
ProjectFeature
::
PRIVATE
)
features
.
each
do
|
feature
|
expect
(
project
.
feature_available?
(
feature
.
to_sym
,
user
)).
to
eq
(
false
),
"
#{
feature
}
failed"
end
end
it
"returns true when user is a team member"
do
project
.
add_developer
(
user
)
update_all_project_features
(
project
,
features
,
ProjectFeature
::
PRIVATE
)
features
.
each
do
|
feature
|
expect
(
project
.
feature_available?
(
feature
.
to_sym
,
user
)).
to
eq
(
true
),
"
#{
feature
}
failed"
end
end
it
"returns true when user is a member of project group"
do
group
=
create
(
:group
)
project
=
create
(
:project
,
namespace:
group
)
group
.
add_developer
(
user
)
update_all_project_features
(
project
,
features
,
ProjectFeature
::
PRIVATE
)
features
.
each
do
|
feature
|
expect
(
project
.
feature_available?
(
feature
.
to_sym
,
user
)).
to
eq
(
true
),
"
#{
feature
}
failed"
end
end
context
'when admin mode is enabled'
,
:enable_admin_mode
do
it
"returns true if user is an admin"
do
user
.
update_attribute
(
:admin
,
true
)
update_all_project_features
(
project
,
features
,
ProjectFeature
::
PRIVATE
)
features
.
each
do
|
feature
|
expect
(
project
.
feature_available?
(
feature
.
to_sym
,
user
)).
to
eq
(
true
),
"
#{
feature
}
failed"
end
end
end
context
'when admin mode is disabled'
do
it
"returns false when user is an admin"
do
user
.
update_attribute
(
:admin
,
true
)
update_all_project_features
(
project
,
features
,
ProjectFeature
::
PRIVATE
)
features
.
each
do
|
feature
|
expect
(
project
.
feature_available?
(
feature
.
to_sym
,
user
)).
to
eq
(
false
),
"
#{
feature
}
failed"
end
end
end
end
context
'when feature is enabled for everyone'
do
it
"returns true"
do
expect
(
project
.
feature_available?
(
:issues
,
user
)).
to
eq
(
true
)
end
end
context
'when feature is disabled by a feature flag'
do
it
'returns false'
do
stub_feature_flags
(
issues:
false
)
expect
(
project
.
feature_available?
(
:issues
,
user
)).
to
eq
(
false
)
end
end
context
'when feature is enabled by a feature flag'
do
it
'returns true'
do
stub_feature_flags
(
issues:
true
)
expect
(
project
.
feature_available?
(
:issues
,
user
)).
to
eq
(
true
)
end
end
end
describe
'#*_enabled?'
do
let
(
:features
)
{
%w(wiki builds merge_requests)
}
it
"returns false when feature is disabled"
do
update_all_project_features
(
project
,
features
,
ProjectFeature
::
DISABLED
)
features
.
each
do
|
feature
|
expect
(
project
.
public_send
(
"
#{
feature
}
_enabled?"
)).
to
eq
(
false
),
"
#{
feature
}
failed"
end
end
it
"returns true when feature is enabled only for team members"
do
update_all_project_features
(
project
,
features
,
ProjectFeature
::
PRIVATE
)
features
.
each
do
|
feature
|
expect
(
project
.
public_send
(
"
#{
feature
}
_enabled?"
)).
to
eq
(
true
),
"
#{
feature
}
failed"
end
end
it
"returns true when feature is enabled for everyone"
do
features
.
each
do
|
feature
|
expect
(
project
.
public_send
(
"
#{
feature
}
_enabled?"
)).
to
eq
(
true
),
"
#{
feature
}
failed"
end
end
end
def
update_all_project_features
(
project
,
features
,
value
)
project_feature_attributes
=
features
.
map
{
|
f
|
[
"
#{
f
}
_access_level"
,
value
]
}.
to_h
project
.
project_feature
.
update
(
project_feature_attributes
)
end
end
spec/models/project_feature_spec.rb
View file @
051c4be4
...
@@ -18,106 +18,6 @@ describe ProjectFeature do
...
@@ -18,106 +18,6 @@ describe ProjectFeature do
end
end
end
end
describe
'.quoted_access_level_column'
do
it
'returns the table name and quoted column name for a feature'
do
expected
=
'"project_features"."issues_access_level"'
expect
(
described_class
.
quoted_access_level_column
(
:issues
)).
to
eq
(
expected
)
end
end
describe
'#feature_available?'
do
let
(
:features
)
{
%w(issues wiki builds merge_requests snippets repository pages metrics_dashboard)
}
context
'when features are disabled'
do
it
"returns false"
do
update_all_project_features
(
project
,
features
,
ProjectFeature
::
DISABLED
)
features
.
each
do
|
feature
|
expect
(
project
.
feature_available?
(
feature
.
to_sym
,
user
)).
to
eq
(
false
),
"
#{
feature
}
failed"
end
end
end
context
'when features are enabled only for team members'
do
it
"returns false when user is not a team member"
do
update_all_project_features
(
project
,
features
,
ProjectFeature
::
PRIVATE
)
features
.
each
do
|
feature
|
expect
(
project
.
feature_available?
(
feature
.
to_sym
,
user
)).
to
eq
(
false
),
"
#{
feature
}
failed"
end
end
it
"returns true when user is a team member"
do
project
.
add_developer
(
user
)
update_all_project_features
(
project
,
features
,
ProjectFeature
::
PRIVATE
)
features
.
each
do
|
feature
|
expect
(
project
.
feature_available?
(
feature
.
to_sym
,
user
)).
to
eq
(
true
),
"
#{
feature
}
failed"
end
end
it
"returns true when user is a member of project group"
do
group
=
create
(
:group
)
project
=
create
(
:project
,
namespace:
group
)
group
.
add_developer
(
user
)
update_all_project_features
(
project
,
features
,
ProjectFeature
::
PRIVATE
)
features
.
each
do
|
feature
|
expect
(
project
.
feature_available?
(
feature
.
to_sym
,
user
)).
to
eq
(
true
),
"
#{
feature
}
failed"
end
end
context
'when admin mode is enabled'
,
:enable_admin_mode
do
it
"returns true if user is an admin"
do
user
.
update_attribute
(
:admin
,
true
)
update_all_project_features
(
project
,
features
,
ProjectFeature
::
PRIVATE
)
features
.
each
do
|
feature
|
expect
(
project
.
feature_available?
(
feature
.
to_sym
,
user
)).
to
eq
(
true
),
"
#{
feature
}
failed"
end
end
end
context
'when admin mode is disabled'
do
it
"returns false when user is an admin"
do
user
.
update_attribute
(
:admin
,
true
)
update_all_project_features
(
project
,
features
,
ProjectFeature
::
PRIVATE
)
features
.
each
do
|
feature
|
expect
(
project
.
feature_available?
(
feature
.
to_sym
,
user
)).
to
eq
(
false
),
"
#{
feature
}
failed"
end
end
end
end
context
'when feature is enabled for everyone'
do
it
"returns true"
do
expect
(
project
.
feature_available?
(
:issues
,
user
)).
to
eq
(
true
)
end
end
context
'when feature is disabled by a feature flag'
do
it
'returns false'
do
stub_feature_flags
(
issues:
false
)
expect
(
project
.
feature_available?
(
:issues
,
user
)).
to
eq
(
false
)
end
end
context
'when feature is enabled by a feature flag'
do
it
'returns true'
do
stub_feature_flags
(
issues:
true
)
expect
(
project
.
feature_available?
(
:issues
,
user
)).
to
eq
(
true
)
end
end
end
context
'repository related features'
do
context
'repository related features'
do
before
do
before
do
project
.
project_feature
.
update
(
project
.
project_feature
.
update
(
...
@@ -153,32 +53,6 @@ describe ProjectFeature do
...
@@ -153,32 +53,6 @@ describe ProjectFeature do
end
end
end
end
describe
'#*_enabled?'
do
let
(
:features
)
{
%w(wiki builds merge_requests)
}
it
"returns false when feature is disabled"
do
update_all_project_features
(
project
,
features
,
ProjectFeature
::
DISABLED
)
features
.
each
do
|
feature
|
expect
(
project
.
public_send
(
"
#{
feature
}
_enabled?"
)).
to
eq
(
false
),
"
#{
feature
}
failed"
end
end
it
"returns true when feature is enabled only for team members"
do
update_all_project_features
(
project
,
features
,
ProjectFeature
::
PRIVATE
)
features
.
each
do
|
feature
|
expect
(
project
.
public_send
(
"
#{
feature
}
_enabled?"
)).
to
eq
(
true
),
"
#{
feature
}
failed"
end
end
it
"returns true when feature is enabled for everyone"
do
features
.
each
do
|
feature
|
expect
(
project
.
public_send
(
"
#{
feature
}
_enabled?"
)).
to
eq
(
true
),
"
#{
feature
}
failed"
end
end
end
describe
'default pages access level'
do
describe
'default pages access level'
do
subject
{
project_feature
.
pages_access_level
}
subject
{
project_feature
.
pages_access_level
}
...
@@ -313,9 +187,4 @@ describe ProjectFeature do
...
@@ -313,9 +187,4 @@ describe ProjectFeature do
expect
(
described_class
.
required_minimum_access_level_for_private_project
(
:issues
)).
to
eq
(
Gitlab
::
Access
::
GUEST
)
expect
(
described_class
.
required_minimum_access_level_for_private_project
(
:issues
)).
to
eq
(
Gitlab
::
Access
::
GUEST
)
end
end
end
end
def
update_all_project_features
(
project
,
features
,
value
)
project_feature_attributes
=
features
.
map
{
|
f
|
[
"
#{
f
}
_access_level"
,
value
]
}.
to_h
project
.
project_feature
.
update
(
project_feature_attributes
)
end
end
end
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment