Commit cc643ce9 authored by Jean-Philippe Lang's avatar Jean-Philippe Lang

Merged nbc branch @ r1812 (commit access permission and reposman improvements).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1814 e93f8b46-1217-0410-a6f0-8f06a7374b81
parent 6ef64596
...@@ -15,11 +15,19 @@ ...@@ -15,11 +15,19 @@
# along with this program; if not, write to the Free Software # along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class AWSProjectWithRepository < ActionWebService::Struct
member :id, :int
member :identifier, :string
member :name, :string
member :is_public, :bool
member :repository, Repository
end
class SysApi < ActionWebService::API::Base class SysApi < ActionWebService::API::Base
api_method :projects, api_method :projects_with_repository_enabled,
:expects => [], :expects => [],
:returns => [[Project]] :returns => [[AWSProjectWithRepository]]
api_method :repository_created, api_method :repository_created,
:expects => [:string, :string], :expects => [:string, :string, :string],
:returns => [:int] :returns => [:int]
end end
...@@ -23,18 +23,17 @@ class SysController < ActionController::Base ...@@ -23,18 +23,17 @@ class SysController < ActionController::Base
before_invocation :check_enabled before_invocation :check_enabled
# Returns the projects list, with their repositories # Returns the projects list, with their repositories
def projects def projects_with_repository_enabled
Project.find(:all, :include => :repository) Project.has_module(:repository).find(:all, :include => :repository, :order => 'identifier')
end end
# Registers a repository for the given project identifier # Registers a repository for the given project identifier
# (Subversion specific) def repository_created(identifier, vendor, url)
def repository_created(identifier, url)
project = Project.find_by_identifier(identifier) project = Project.find_by_identifier(identifier)
# Do not create the repository if the project has already one # Do not create the repository if the project has already one
return 0 unless project && project.repository.nil? return 0 unless project && project.repository.nil?
logger.debug "Repository for #{project.name} was created" logger.debug "Repository for #{project.name} was created"
repository = Repository.factory('Subversion', :project => project, :url => url) repository = Repository.factory(vendor, :project => project, :url => url)
repository.save repository.save
repository.id || 0 repository.id || 0
end end
......
...@@ -62,6 +62,8 @@ class Project < ActiveRecord::Base ...@@ -62,6 +62,8 @@ class Project < ActiveRecord::Base
validates_format_of :identifier, :with => /^[a-z0-9\-]*$/ validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
before_destroy :delete_all_members before_destroy :delete_all_members
named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
def identifier=(identifier) def identifier=(identifier)
super unless identifier_frozen? super unless identifier_frozen?
......
...@@ -19,6 +19,11 @@ class Role < ActiveRecord::Base ...@@ -19,6 +19,11 @@ class Role < ActiveRecord::Base
# Built-in roles # Built-in roles
BUILTIN_NON_MEMBER = 1 BUILTIN_NON_MEMBER = 1
BUILTIN_ANONYMOUS = 2 BUILTIN_ANONYMOUS = 2
named_scope :builtin, lambda { |*args|
compare = 'not' if args.first == true
{ :conditions => "#{compare} builtin = 0" }
}
before_destroy :check_deletable before_destroy :check_deletable
has_many :workflows, :dependent => :delete_all do has_many :workflows, :dependent => :delete_all do
...@@ -36,7 +41,7 @@ class Role < ActiveRecord::Base ...@@ -36,7 +41,7 @@ class Role < ActiveRecord::Base
has_many :members has_many :members
acts_as_list acts_as_list
serialize :permissions serialize :permissions, Array
attr_protected :builtin attr_protected :builtin
validates_presence_of :name validates_presence_of :name
...@@ -49,9 +54,27 @@ class Role < ActiveRecord::Base ...@@ -49,9 +54,27 @@ class Role < ActiveRecord::Base
end end
def permissions=(perms) def permissions=(perms)
perms = perms.collect {|p| p.to_sym unless p.blank? }.compact if perms perms = perms.collect {|p| p.to_sym unless p.blank? }.compact.uniq if perms
write_attribute(:permissions, perms) write_attribute(:permissions, perms)
end end
def add_permission!(*perms)
self.permissions = [] unless permissions.is_a?(Array)
permissions_will_change!
perms.each do |p|
p = p.to_sym
permissions << p unless permissions.include?(p)
end
save!
end
def remove_permission!(*perms)
return unless permissions.is_a?(Array)
permissions_will_change!
perms.each { |p| permissions.delete(p.to_sym) }
save!
end
def <=>(role) def <=>(role)
position <=> role.position position <=> role.position
......
class AddCommitAccessPermission < ActiveRecord::Migration
def self.up
Role.find(:all).select { |r| not r.builtin? }.each do |r|
r.add_permission!(:commit_access)
end
end
def self.down
Role.find(:all).select { |r| not r.builtin? }.each do |r|
r.remove_permission!(:commit_access)
end
end
end
...@@ -148,11 +148,12 @@ sub RedmineDSN { ...@@ -148,11 +148,12 @@ sub RedmineDSN {
my ($self, $parms, $arg) = @_; my ($self, $parms, $arg) = @_;
$self->{RedmineDSN} = $arg; $self->{RedmineDSN} = $arg;
my $query = "SELECT my $query = "SELECT
hashed_password, auth_source_id hashed_password, auth_source_id, permissions
FROM members, projects, users FROM members, projects, users, roles
WHERE WHERE
projects.id=members.project_id projects.id=members.project_id
AND users.id=members.user_id AND users.id=members.user_id
AND roles.id=members.role_id
AND users.status=1 AND users.status=1
AND login=? AND login=?
AND identifier=? "; AND identifier=? ";
...@@ -277,9 +278,11 @@ sub is_member { ...@@ -277,9 +278,11 @@ sub is_member {
$sth->execute($redmine_user, $project_id); $sth->execute($redmine_user, $project_id);
my $ret; my $ret;
while (my @row = $sth->fetchrow_array) { while (my ($hashed_password, $auth_source_id, $permissions) = $sth->fetchrow_array) {
unless ($row[1]) {
if ($row[0] eq $pass_digest) { unless ($auth_source_id) {
my $method = $r->method;
if ($hashed_password eq $pass_digest && (defined $read_only_methods{$method} || $permissions =~ /:commit_access/) ) {
$ret = 1; $ret = 1;
last; last;
} }
...@@ -287,7 +290,7 @@ sub is_member { ...@@ -287,7 +290,7 @@ sub is_member {
my $sthldap = $dbh->prepare( my $sthldap = $dbh->prepare(
"SELECT host,port,tls,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;" "SELECT host,port,tls,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;"
); );
$sthldap->execute($row[1]); $sthldap->execute($auth_source_id);
while (my @rowldap = $sthldap->fetchrow_array) { while (my @rowldap = $sthldap->fetchrow_array) {
my $ldap = Authen::Simple::LDAP->new( my $ldap = Authen::Simple::LDAP->new(
host => ($rowldap[2] == 1 || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]" : $rowldap[0], host => ($rowldap[2] == 1 || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]" : $rowldap[0],
......
#!/usr/bin/perl
#
# redMine is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
use strict;
use SOAP::Lite;
use Getopt::Long;
Getopt::Long::Configure ("bundling", "no_auto_abbrev", "no_ignore_case");
use Pod::Usage;
use vars qw/$VERSION/;
$VERSION = "1.0";
my $warning = "This program is now deprecated. Use the reposman.rb for new features";
print STDERR "*" x length($warning), "\n",
$warning, "\n",
"*" x length($warning), "\n\n";
my %opts = (verbose => 0);
GetOptions(\%opts, 'verbose|v+', 'version|V', 'help|h', 'man|m', 'quiet|q', 'svn-dir|s=s', 'redmine-host|r=s') or pod2usage(2);
die "$VERSION\n" if $opts{version};
pod2usage(1) if $opts{help};
pod2usage( -verbose => 2 ) if $opts{man};
my $repos_base = $opts{'svn-dir'};
my $redmine_host = $opts{'redmine-host'};
pod2usage(2) unless $repos_base and $redmine_host;
unless (-d $repos_base) {
Log(text => "$repos_base doesn't exist", exit => 1);
}
Log(level => 1, text => "querying redMine for projects...");
my $wdsl = "http://$redmine_host/sys/service.wsdl";
my $service = SOAP::Lite->service($wdsl);
my $projects = $service->Projects('');
my $project_count = @{$projects};
Log(level => 1, text => "retrieved $project_count projects");
foreach my $project (@{$projects}) {
Log(level => 1, text => "treating project $project->{name}");
my $repos_name = $project->{identifier};
if ($repos_name eq "") {
Log(text => "\tno identifier for project $project->{name}");
next;
}
unless ($repos_name =~ /^[a-z0-9\-]+$/) {
Log(text => "\tinvalid identifier for project $project->{name}");
next;
}
my $repos_path = "$repos_base/$repos_name";
if (-e $repos_path) {
# check unix right and change them if needed
my $other_read = (stat($repos_path))[2] & 00007;
my $right;
if ($project->{is_public} and not $other_read) {
$right = "0775";
} elsif (not $project->{is_public} and $other_read) {
$right = "0770";
} else {
next;
}
# change mode
system('chmod', '-R', $right, $repos_path) == 0 or
warn("\tunable to change mode on $repos_path : $?\n"), next;
Log(text => "\tmode change on $repos_path");
} else {
# change umask to suit the repository's privacy
$project->{is_public} ? umask 0002 : umask 0007;
# create the repository
system('svnadmin', 'create', $repos_path) == 0 or
warn("\tsystem svnadmin failed unable to create $repos_path\n"), next;
# set the group owner
system('chown', '-R', "root:$repos_name", $repos_path) == 0 or
warn("\tunable to create $repos_path : $?\n"), next;
Log(text => "\trepository $repos_path created");
}
}
sub Log {
my %args = (level => 0, text => '', @_);
my $level = delete $args{level};
my $text = delete $args{text};
return unless $level <= $opts{verbose};
return if $opts{quiet};
print "$text\n";
exit $args{exit}
if defined $args{exit};
}
__END__
=head1 NAME
reposman - manages your svn repositories with redMine
=head1 SYNOPSIS
reposman [options] arguments
example: reposman --svn-dir=/var/svn --redmine-host=redmine.mydomain.foo
reposman -s /var/svn -r redmine.mydomain.foo
=head1 ARGUMENTS
-s, --svn-dir=DIR use DIR as base directory for svn repositories
-r, --redmine-host=HOST assume redMine is hosted on HOST
=head1 OPTIONS
-v verbose
-V print version and exit
...@@ -6,52 +6,49 @@ ...@@ -6,52 +6,49 @@
# #
# == Usage # == Usage
# #
# reposman [ -h | --help ] [ -v | --verbose ] [ -V | --version ] [ -q | --quiet ] -s /var/svn -r redmine.host.org # reposman [OPTIONS...] -s [DIR] -r [HOST]
# example: reposman --svn-dir=/var/svn --redmine-host=redmine.mydomain.foo #
# reposman -s /var/svn -r redmine.mydomain.foo # Examples:
# reposman --svn-dir=/var/svn --redmine-host=redmine.example.net
# reposman -s /var/svn -r redmine.example.net -u http://svn.example.net
# #
# == Arguments (mandatory) # == Arguments (mandatory)
#
# -s, --svn-dir=DIR
# use DIR as base directory for svn repositories
# #
# -r, --redmine-host=HOST # -s, --svn-dir=DIR use DIR as base directory for svn repositories
# assume Redmine is hosted on HOST. # -r, --redmine-host=HOST assume Redmine is hosted on HOST. Examples:
# you can use : # -r redmine.example.net
# * -r redmine.mydomain.foo (will add http://) # -r http://redmine.example.net
# * -r http://redmine.mydomain.foo # -r https://example.net/redmine
# * -r https://mydomain.foo/redmine
# #
# == Options # == Options
# #
# -o, --owner=OWNER # -o, --owner=OWNER owner of the repository. using the rails login
# owner of the repository. using the rails login allow user to browse # allow user to browse the repository within
# the repository in Redmine even for private project # Redmine even for private project
# # -u, --url=URL the base url Redmine will use to access your
# -u, --url=URL # repositories. This option is used to automatically
# the base url Redmine will use to access your repositories. This # register the repositories in Redmine. The project
# will be used to register the repository in Redmine so that user # identifier will be appended to this url. Examples:
# doesn't need to do anything. reposman will add the identifier to this url : # -u https://example.net/svn
# # -u file:///var/svn/
# -u https://my.svn.server/my/reposity/root # if the repository can be access by http # if this option isn't set, reposman won't register
# -u file:///var/svn/ # if the repository is local # the repositories in Redmine
# if this option isn't set, reposman won't register the repository # -c, --command=COMMAND use this command instead of "svnadmin create" to
# # create a repository. This option can be used to
# -t, --test # create non-subversion repositories
# only show what should be done # --scm SCM vendor used to register the repository in
# # Redmine (default: Subversion). Can be one of the
# -h, --help: # other supported SCM: Bazaar, Darcs, Filesystem,
# show help and exit # Git, Mercurial (case sensitive).
# # This option should be used when both options --url
# -v, --verbose # and --command are used.
# verbose # -f, --force force repository creation even if the project
# # repository is already declared in Redmine
# -V, --version # -t, --test only show what should be done
# print version and exit # -h, --help show help and exit
# # -v, --verbose verbose
# -q, --quiet # -V, --version print version and exit
# no log # -q, --quiet no log
#
require 'getoptlong' require 'getoptlong'
require 'rdoc/usage' require 'rdoc/usage'
...@@ -59,14 +56,18 @@ require 'soap/wsdlDriver' ...@@ -59,14 +56,18 @@ require 'soap/wsdlDriver'
require 'find' require 'find'
require 'etc' require 'etc'
Version = "1.0" Version = "1.1"
SUPPORTED_SCM = %w( Subversion Darcs Mercurial Bazaar Git Filesystem )
opts = GetoptLong.new( opts = GetoptLong.new(
['--svn-dir', '-s', GetoptLong::REQUIRED_ARGUMENT], ['--svn-dir', '-s', GetoptLong::REQUIRED_ARGUMENT],
['--redmine-host', '-r', GetoptLong::REQUIRED_ARGUMENT], ['--redmine-host', '-r', GetoptLong::REQUIRED_ARGUMENT],
['--owner', '-o', GetoptLong::REQUIRED_ARGUMENT], ['--owner', '-o', GetoptLong::REQUIRED_ARGUMENT],
['--url', '-u', GetoptLong::REQUIRED_ARGUMENT], ['--url', '-u', GetoptLong::REQUIRED_ARGUMENT],
['--command' , '-c', GetoptLong::REQUIRED_ARGUMENT],
['--scm', GetoptLong::REQUIRED_ARGUMENT],
['--test', '-t', GetoptLong::NO_ARGUMENT], ['--test', '-t', GetoptLong::NO_ARGUMENT],
['--force', '-f', GetoptLong::NO_ARGUMENT],
['--verbose', '-v', GetoptLong::NO_ARGUMENT], ['--verbose', '-v', GetoptLong::NO_ARGUMENT],
['--version', '-V', GetoptLong::NO_ARGUMENT], ['--version', '-V', GetoptLong::NO_ARGUMENT],
['--help' , '-h', GetoptLong::NO_ARGUMENT], ['--help' , '-h', GetoptLong::NO_ARGUMENT],
...@@ -81,6 +82,9 @@ $svn_owner = 'root' ...@@ -81,6 +82,9 @@ $svn_owner = 'root'
$use_groupid = true $use_groupid = true
$svn_url = false $svn_url = false
$test = false $test = false
$command = "svnadmin create"
$force = false
$scm = 'Subversion'
def log(text,level=0, exit=false) def log(text,level=0, exit=false)
return if $quiet or level > $verbose return if $quiet or level > $verbose
...@@ -95,8 +99,11 @@ begin ...@@ -95,8 +99,11 @@ begin
when '--redmine-host'; $redmine_host = arg.dup when '--redmine-host'; $redmine_host = arg.dup
when '--owner'; $svn_owner = arg.dup; $use_groupid = false; when '--owner'; $svn_owner = arg.dup; $use_groupid = false;
when '--url'; $svn_url = arg.dup when '--url'; $svn_url = arg.dup
when '--scm'; $scm = arg.dup; log("Invalid SCM: #{$scm}", 0, true) unless SUPPORTED_SCM.include?($scm)
when '--command'; $command = arg.dup
when '--verbose'; $verbose += 1 when '--verbose'; $verbose += 1
when '--test'; $test = true when '--test'; $test = true
when '--force'; $force = true
when '--version'; puts Version; exit when '--version'; puts Version; exit
when '--help'; RDoc::usage when '--help'; RDoc::usage
when '--quiet'; $quiet = true when '--quiet'; $quiet = true
...@@ -110,6 +117,12 @@ if $test ...@@ -110,6 +117,12 @@ if $test
log("running in test mode") log("running in test mode")
end end
# Make sure command is overridden if SCM vendor is not Subversion
if $scm != 'Subversion' && $command == 'svnadmin create'
log("Please use --command option to specify how to create a #{$scm} repository.", 0, true)
end
$svn_url += "/" if $svn_url and not $svn_url.match(/\/$/) $svn_url += "/" if $svn_url and not $svn_url.match(/\/$/)
if ($redmine_host.empty? or $repos_base.empty?) if ($redmine_host.empty? or $repos_base.empty?)
...@@ -133,7 +146,7 @@ rescue => e ...@@ -133,7 +146,7 @@ rescue => e
log("Unable to connect to #{wsdl_url} : #{e}", 0, true) log("Unable to connect to #{wsdl_url} : #{e}", 0, true)
end end
projects = soap.Projects projects = soap.ProjectsWithRepositoryEnabled
if projects.nil? if projects.nil?
log('no project found, perhaps you forgot to "Enable WS for repository management"', 0, true) log('no project found, perhaps you forgot to "Enable WS for repository management"', 0, true)
...@@ -201,6 +214,13 @@ projects.each do |project| ...@@ -201,6 +214,13 @@ projects.each do |project|
log("\tmode change on #{repos_path}"); log("\tmode change on #{repos_path}");
else else
# if repository is already declared in redmine, we don't create
# unless user use -f with reposman
if $force == false and not project.repository.nil?
log("\trepository for project #{project.identifier} already exists in Redmine", 1)
next
end
project.is_public ? File.umask(0002) : File.umask(0007) project.is_public ? File.umask(0002) : File.umask(0007)
if $test if $test
...@@ -211,7 +231,8 @@ projects.each do |project| ...@@ -211,7 +231,8 @@ projects.each do |project|
begin begin
set_owner_and_rights(project, repos_path) do set_owner_and_rights(project, repos_path) do
raise "svnadmin create #{repos_path} failed" unless system("svnadmin", "create", repos_path) command = "#{$command} #{repos_path}"
raise "#{command} failed" unless system( command )
end end
rescue => e rescue => e
log("\tunable to create #{repos_path} : #{e}\n") log("\tunable to create #{repos_path} : #{e}\n")
...@@ -219,7 +240,7 @@ projects.each do |project| ...@@ -219,7 +240,7 @@ projects.each do |project|
end end
if $svn_url if $svn_url
ret = soap.RepositoryCreated project.identifier, "#{$svn_url}#{project.identifier}" ret = soap.RepositoryCreated project.identifier, $scm, "#{$svn_url}#{project.identifier}"
if ret > 0 if ret > 0
log("\trepository #{repos_path} registered in Redmine with url #{$svn_url}#{project.identifier}"); log("\trepository #{repos_path} registered in Redmine with url #{$svn_url}#{project.identifier}");
else else
......
...@@ -88,6 +88,7 @@ Redmine::AccessControl.map do |map| ...@@ -88,6 +88,7 @@ Redmine::AccessControl.map do |map|
map.permission :manage_repository, {:repositories => [:edit, :destroy]}, :require => :member map.permission :manage_repository, {:repositories => [:edit, :destroy]}, :require => :member
map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph] map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]
map.permission :view_changesets, :repositories => [:show, :revisions, :revision] map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
map.permission :commit_access, {}
end end
map.project_module :boards do |map| map.project_module :boards do |map|
......
...@@ -67,7 +67,8 @@ module Redmine ...@@ -67,7 +67,8 @@ module Redmine
:view_files, :view_files,
:manage_files, :manage_files,
:browse_repository, :browse_repository,
:view_changesets] :view_changesets,
:commit_access]
reporter = Role.create! :name => l(:default_role_reporter), reporter = Role.create! :name => l(:default_role_reporter),
:position => 3, :position => 3,
......
...@@ -5,7 +5,7 @@ require 'sys_controller' ...@@ -5,7 +5,7 @@ require 'sys_controller'
class SysController; def rescue_action(e) raise e end; end class SysController; def rescue_action(e) raise e end; end
class SysControllerTest < Test::Unit::TestCase class SysControllerTest < Test::Unit::TestCase
fixtures :projects, :repositories fixtures :projects, :enabled_modules, :repositories
def setup def setup
@controller = SysController.new @controller = SysController.new
...@@ -15,17 +15,36 @@ class SysControllerTest < Test::Unit::TestCase ...@@ -15,17 +15,36 @@ class SysControllerTest < Test::Unit::TestCase
Setting.sys_api_enabled = 1 Setting.sys_api_enabled = 1
end end
def test_projects def test_projects_with_repository_enabled
result = invoke :projects result = invoke :projects_with_repository_enabled
assert_equal Project.count, result.size assert_equal EnabledModule.count(:all, :conditions => {:name => 'repository'}), result.size
assert result.first.is_a?(Project)
project = result.first
assert project.is_a?(AWSProjectWithRepository)
assert project.respond_to?(:id)
assert_equal 1, project.id
assert project.respond_to?(:identifier)
assert_equal 'ecookbook', project.identifier
assert project.respond_to?(:name)
assert_equal 'eCookbook', project.name
assert project.respond_to?(:is_public)
assert project.is_public
assert project.respond_to?(:repository)
assert project.repository.is_a?(Repository)
end end
def test_repository_created def test_repository_created
project = Project.find(3) project = Project.find(3)
assert_nil project.repository assert_nil project.repository
assert invoke(:repository_created, project.identifier, 'http://localhost/svn') assert invoke(:repository_created, project.identifier, 'Subversion', 'http://localhost/svn')
project.reload project.reload
assert_not_nil project.repository assert_not_nil project.repository
assert project.repository.is_a?(Repository::Subversion)
assert_equal 'http://localhost/svn', project.repository.url
end end
end end
...@@ -30,4 +30,24 @@ class RoleTest < Test::Unit::TestCase ...@@ -30,4 +30,24 @@ class RoleTest < Test::Unit::TestCase
target.reload target.reload
assert_equal 90, target.workflows.size assert_equal 90, target.workflows.size
end end
def test_add_permission
role = Role.find(1)
size = role.permissions.size
role.add_permission!("apermission", "anotherpermission")
role.reload
assert role.permissions.include?(:anotherpermission)
assert_equal size + 2, role.permissions.size
end
def test_remove_permission
role = Role.find(1)
size = role.permissions.size
perm = role.permissions[0..1]
role.remove_permission!(*perm)
role.reload
assert ! role.permissions.include?(perm[0])
assert_equal size - 2, role.permissions.size
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