Commit 3bc9d21c authored by jim's avatar jim

Added an upload entry point for extensions.

Also fixed some spurious failures in the bootstrap test.


git-svn-id: http://svn.zope.org/repos/main/zc.buildout/trunk@101154 62d5b8a3-27da-0310-9561-8e5933582275
parent ea56fd86
Change History Change History
************** **************
1.2.2 (unreleased) 1.2.2 (2009-06-19)
================== ==================
- Better Windows compatibility in test infrastructure. - Better Windows compatibility in test infrastructure.
...@@ -16,6 +16,8 @@ Change History ...@@ -16,6 +16,8 @@ Change History
- fixed usage of 'relative_paths' keyword parameter on Windows - fixed usage of 'relative_paths' keyword parameter on Windows
- Added an unload entry point for extensions.
1.2.1 (2009-03-18) 1.2.1 (2009-03-18)
================== ==================
......
...@@ -59,12 +59,7 @@ Let's try with an unknown version:: ...@@ -59,12 +59,7 @@ Let's try with an unknown version::
X X
No local packages or download links found for zc.buildout==UNKNOWN No local packages or download links found for zc.buildout==UNKNOWN
error: Could not find suitable distribution for Requirement.parse('zc.buildout==UNKNOWN') error: Could not find suitable distribution for Requirement.parse('zc.buildout==UNKNOWN')
Traceback (most recent call last): ...
File "bootstrap.py", line 78, in <module>
) == 0
AssertionError
<BLANKLINE>
X
Now let's try with `1.1.2`, which happens to exist:: Now let's try with `1.1.2`, which happens to exist::
......
...@@ -83,7 +83,7 @@ class Buildout(UserDict.DictMixin): ...@@ -83,7 +83,7 @@ class Buildout(UserDict.DictMixin):
user_defaults=True, windows_restart=False, command=None): user_defaults=True, windows_restart=False, command=None):
__doing__ = 'Initializing.' __doing__ = 'Initializing.'
self.__windows_restart = windows_restart self.__windows_restart = windows_restart
# default options # default options
...@@ -138,7 +138,7 @@ class Buildout(UserDict.DictMixin): ...@@ -138,7 +138,7 @@ class Buildout(UserDict.DictMixin):
# because while parsing options those attributes might be # because while parsing options those attributes might be
# used already (Gottfried Ganssauge) # used already (Gottfried Ganssauge)
buildout_section = data.get('buildout') buildout_section = data.get('buildout')
# Try to make sure we have absolute paths for standard # Try to make sure we have absolute paths for standard
# directories. We do this before doing substitutions, in case # directories. We do this before doing substitutions, in case
# a one of these gets read by another section. If any # a one of these gets read by another section. If any
...@@ -155,13 +155,13 @@ class Buildout(UserDict.DictMixin): ...@@ -155,13 +155,13 @@ class Buildout(UserDict.DictMixin):
allow_hosts = buildout_section and buildout_section.get( allow_hosts = buildout_section and buildout_section.get(
'allow-hosts', '*').split('\n') 'allow-hosts', '*').split('\n')
self._allow_hosts = tuple([host.strip() for host in allow_hosts self._allow_hosts = tuple([host.strip() for host in allow_hosts
if host.strip() != '']) if host.strip() != ''])
self._logger = logging.getLogger('zc.buildout') self._logger = logging.getLogger('zc.buildout')
self.offline = False self.offline = False
self.newest = True self.newest = True
################################################################## ##################################################################
## WARNING!!! ## WARNING!!!
## ALL ATTRIBUTES MUST HAVE REASONABLE DEFAULTS AT THIS POINT ## ALL ATTRIBUTES MUST HAVE REASONABLE DEFAULTS AT THIS POINT
...@@ -173,9 +173,9 @@ class Buildout(UserDict.DictMixin): ...@@ -173,9 +173,9 @@ class Buildout(UserDict.DictMixin):
# now reinitialize # now reinitialize
links = options.get('find-links', '') links = options.get('find-links', '')
self._links = links and links.split() or () self._links = links and links.split() or ()
allow_hosts = options.get('allow-hosts', '*').split('\n') allow_hosts = options.get('allow-hosts', '*').split('\n')
self._allow_hosts = tuple([host.strip() for host in allow_hosts self._allow_hosts = tuple([host.strip() for host in allow_hosts
if host.strip() != '']) if host.strip() != ''])
self._buildout_dir = options['directory'] self._buildout_dir = options['directory']
...@@ -216,7 +216,7 @@ class Buildout(UserDict.DictMixin): ...@@ -216,7 +216,7 @@ class Buildout(UserDict.DictMixin):
self._error('Invalid value for prefer-final option: %s', self._error('Invalid value for prefer-final option: %s',
prefer_final) prefer_final)
zc.buildout.easy_install.prefer_final(prefer_final=='true') zc.buildout.easy_install.prefer_final(prefer_final=='true')
use_dependency_links = options.get('use-dependency-links', 'true') use_dependency_links = options.get('use-dependency-links', 'true')
if use_dependency_links not in ('true', 'false'): if use_dependency_links not in ('true', 'false'):
self._error('Invalid value for use-dependency-links option: %s', self._error('Invalid value for use-dependency-links option: %s',
...@@ -243,7 +243,7 @@ class Buildout(UserDict.DictMixin): ...@@ -243,7 +243,7 @@ class Buildout(UserDict.DictMixin):
download_cache = os.path.join(download_cache, 'dist') download_cache = os.path.join(download_cache, 'dist')
if not os.path.isdir(download_cache): if not os.path.isdir(download_cache):
os.mkdir(download_cache) os.mkdir(download_cache)
zc.buildout.easy_install.download_cache(download_cache) zc.buildout.easy_install.download_cache(download_cache)
install_from_cache = options.get('install-from-cache') install_from_cache = options.get('install-from-cache')
...@@ -335,7 +335,7 @@ class Buildout(UserDict.DictMixin): ...@@ -335,7 +335,7 @@ class Buildout(UserDict.DictMixin):
installed_develop_eggs = self._develop() installed_develop_eggs = self._develop()
installed_part_options['buildout']['installed_develop_eggs' installed_part_options['buildout']['installed_develop_eggs'
] = installed_develop_eggs ] = installed_develop_eggs
if installed_exists: if installed_exists:
self._update_installed( self._update_installed(
installed_develop_eggs=installed_develop_eggs) installed_develop_eggs=installed_develop_eggs)
...@@ -345,7 +345,7 @@ class Buildout(UserDict.DictMixin): ...@@ -345,7 +345,7 @@ class Buildout(UserDict.DictMixin):
conf_parts = conf_parts and conf_parts.split() or [] conf_parts = conf_parts and conf_parts.split() or []
installed_parts = installed_part_options['buildout']['parts'] installed_parts = installed_part_options['buildout']['parts']
installed_parts = installed_parts and installed_parts.split() or [] installed_parts = installed_parts and installed_parts.split() or []
if install_args: if install_args:
install_parts = install_args install_parts = install_args
uninstall_missing = False uninstall_missing = False
...@@ -361,11 +361,11 @@ class Buildout(UserDict.DictMixin): ...@@ -361,11 +361,11 @@ class Buildout(UserDict.DictMixin):
if self._log_level < logging.DEBUG: if self._log_level < logging.DEBUG:
sections = list(self) sections = list(self)
sections.sort() sections.sort()
print print
print 'Configuration data:' print 'Configuration data:'
for section in self._data: for section in self._data:
_save_options(section, self[section], sys.stdout) _save_options(section, self[section], sys.stdout)
print print
# compute new part recipe signatures # compute new part recipe signatures
...@@ -493,7 +493,7 @@ class Buildout(UserDict.DictMixin): ...@@ -493,7 +493,7 @@ class Buildout(UserDict.DictMixin):
if need_to_save_installed: if need_to_save_installed:
installed_part_options['buildout']['parts'] = ( installed_part_options['buildout']['parts'] = (
' '.join(installed_parts)) ' '.join(installed_parts))
self._save_installed_options(installed_part_options) self._save_installed_options(installed_part_options)
installed_exists = True installed_exists = True
else: else:
...@@ -506,6 +506,8 @@ class Buildout(UserDict.DictMixin): ...@@ -506,6 +506,8 @@ class Buildout(UserDict.DictMixin):
elif (not installed_parts) and installed_exists: elif (not installed_parts) and installed_exists:
os.remove(self['buildout']['installed']) os.remove(self['buildout']['installed'])
self._unload_extensions()
def _update_installed(self, **buildout_options): def _update_installed(self, **buildout_options):
installed = self['buildout']['installed'] installed = self['buildout']['installed']
f = open(installed, 'a') f = open(installed, 'a')
...@@ -533,7 +535,6 @@ class Buildout(UserDict.DictMixin): ...@@ -533,7 +535,6 @@ class Buildout(UserDict.DictMixin):
self._uninstall( self._uninstall(
installed_part_options[part]['__buildout_installed__']) installed_part_options[part]['__buildout_installed__'])
def _setup_directories(self): def _setup_directories(self):
__doing__ = 'Setting up buildout directories' __doing__ = 'Setting up buildout directories'
...@@ -564,7 +565,8 @@ class Buildout(UserDict.DictMixin): ...@@ -564,7 +565,8 @@ class Buildout(UserDict.DictMixin):
setup = self._buildout_path(setup) setup = self._buildout_path(setup)
files = glob.glob(setup) files = glob.glob(setup)
if not files: if not files:
self._logger.warn("Couldn't develop %r (not found)", setup) self._logger.warn("Couldn't develop %r (not found)",
setup)
else: else:
files.sort() files.sort()
for setup in files: for setup in files:
...@@ -581,7 +583,7 @@ class Buildout(UserDict.DictMixin): ...@@ -581,7 +583,7 @@ class Buildout(UserDict.DictMixin):
if f not in old_files if f not in old_files
])) ]))
raise raise
else: else:
self._sanity_check_develop_eggs_files(dest, old_files) self._sanity_check_develop_eggs_files(dest, old_files)
return '\n'.join([os.path.join(dest, f) return '\n'.join([os.path.join(dest, f)
...@@ -628,7 +630,7 @@ class Buildout(UserDict.DictMixin): ...@@ -628,7 +630,7 @@ class Buildout(UserDict.DictMixin):
value = value.replace(k, v) value = value.replace(k, v)
options[option] = value options[option] = value
result[section] = Options(self, section, options) result[section] = Options(self, section, options)
return result, True return result, True
else: else:
return ({'buildout': Options(self, 'buildout', {'parts': ''})}, return ({'buildout': Options(self, 'buildout', {'parts': ''})},
...@@ -656,8 +658,8 @@ class Buildout(UserDict.DictMixin): ...@@ -656,8 +658,8 @@ class Buildout(UserDict.DictMixin):
# Sigh. This is the exectable used to run the buildout # Sigh. This is the exectable used to run the buildout
# and, of course, it's in use. Leave it. # and, of course, it's in use. Leave it.
): ):
raise raise
def _install(self, part): def _install(self, part):
options = self[part] options = self[part]
recipe, entry = _recipe(options) recipe, entry = _recipe(options)
...@@ -701,7 +703,7 @@ class Buildout(UserDict.DictMixin): ...@@ -701,7 +703,7 @@ class Buildout(UserDict.DictMixin):
buildout_handler.setFormatter(logging.Formatter('%(message)s')) buildout_handler.setFormatter(logging.Formatter('%(message)s'))
self._logger.propagate = False self._logger.propagate = False
self._logger.addHandler(buildout_handler) self._logger.addHandler(buildout_handler)
handler.setFormatter(logging.Formatter(log_format)) handler.setFormatter(logging.Formatter(log_format))
root_logger.addHandler(handler) root_logger.addHandler(handler)
...@@ -730,7 +732,7 @@ class Buildout(UserDict.DictMixin): ...@@ -730,7 +732,7 @@ class Buildout(UserDict.DictMixin):
if not self.newest: if not self.newest:
return return
ws = zc.buildout.easy_install.install( ws = zc.buildout.easy_install.install(
[ [
(spec + ' ' + self['buildout'].get(spec+'-version', '')).strip() (spec + ' ' + self['buildout'].get(spec+'-version', '')).strip()
...@@ -774,8 +776,8 @@ class Buildout(UserDict.DictMixin): ...@@ -774,8 +776,8 @@ class Buildout(UserDict.DictMixin):
if not __debug__: if not __debug__:
args.insert(0, '-O') args.insert(0, '-O')
args.insert(0, zc.buildout.easy_install._safe_arg (sys.executable)) args.insert(0, zc.buildout.easy_install._safe_arg (sys.executable))
os.execv(sys.executable, args) os.execv(sys.executable, args)
self._logger.info("Upgraded:\n %s;\nrestarting.", self._logger.info("Upgraded:\n %s;\nrestarting.",
",\n ".join([("%s version %s" ",\n ".join([("%s version %s"
% (dist.project_name, dist.version) % (dist.project_name, dist.version)
...@@ -784,7 +786,7 @@ class Buildout(UserDict.DictMixin): ...@@ -784,7 +786,7 @@ class Buildout(UserDict.DictMixin):
] ]
), ),
) )
# the new dist is different, so we've upgraded. # the new dist is different, so we've upgraded.
# Update the scripts and return True # Update the scripts and return True
zc.buildout.easy_install.scripts( zc.buildout.easy_install.scripts(
...@@ -831,6 +833,14 @@ class Buildout(UserDict.DictMixin): ...@@ -831,6 +833,14 @@ class Buildout(UserDict.DictMixin):
for ep in pkg_resources.iter_entry_points('zc.buildout.extension'): for ep in pkg_resources.iter_entry_points('zc.buildout.extension'):
ep.load()(self) ep.load()(self)
def _unload_extensions(self):
__doing__ = 'Unloading extensions.'
specs = self['buildout'].get('extensions', '').split()
if specs:
for ep in pkg_resources.iter_entry_points(
'zc.buildout.unloadextension'):
ep.load()(self)
def setup(self, args): def setup(self, args):
if not args: if not args:
raise zc.buildout.UserError( raise zc.buildout.UserError(
...@@ -854,13 +864,13 @@ class Buildout(UserDict.DictMixin): ...@@ -854,13 +864,13 @@ class Buildout(UserDict.DictMixin):
)) ))
if is_jython: if is_jython:
arg_list = list() arg_list = list()
for a in args: for a in args:
add_args.append(zc.buildout.easy_install._safe_arg(a)) add_args.append(zc.buildout.easy_install._safe_arg(a))
subprocess.Popen([zc.buildout.easy_install._safe_arg(sys.executable)] + list(tsetup) + subprocess.Popen([zc.buildout.easy_install._safe_arg(sys.executable)] + list(tsetup) +
arg_list).wait() arg_list).wait()
else: else:
os.spawnl(os.P_WAIT, sys.executable, zc.buildout.easy_install._safe_arg (sys.executable), tsetup, os.spawnl(os.P_WAIT, sys.executable, zc.buildout.easy_install._safe_arg (sys.executable), tsetup,
*[zc.buildout.easy_install._safe_arg(a) *[zc.buildout.easy_install._safe_arg(a)
...@@ -886,7 +896,7 @@ class Buildout(UserDict.DictMixin): ...@@ -886,7 +896,7 @@ class Buildout(UserDict.DictMixin):
options = Options(self, section, data) options = Options(self, section, data)
self._data[section] = options self._data[section] = options
options._initialize() options._initialize()
return options return options
def __setitem__(self, key, value): def __setitem__(self, key, value):
raise NotImplementedError('__setitem__') raise NotImplementedError('__setitem__')
...@@ -951,7 +961,7 @@ class Options(UserDict.DictMixin): ...@@ -951,7 +961,7 @@ class Options(UserDict.DictMixin):
def _initialize(self): def _initialize(self):
name = self.name name = self.name
__doing__ = 'Initializing section %s.', name __doing__ = 'Initializing section %s.', name
# force substitutions # force substitutions
for k, v in self._raw.items(): for k, v in self._raw.items():
if '${' in v: if '${' in v:
...@@ -959,11 +969,11 @@ class Options(UserDict.DictMixin): ...@@ -959,11 +969,11 @@ class Options(UserDict.DictMixin):
if self.name == 'buildout': if self.name == 'buildout':
return # buildout section can never be a part return # buildout section can never be a part
recipe = self.get('recipe') recipe = self.get('recipe')
if not recipe: if not recipe:
return return
reqs, entry = _recipe(self._data) reqs, entry = _recipe(self._data)
buildout = self.buildout buildout = self.buildout
recipe_class = _install_and_load(reqs, 'zc.buildout', entry, buildout) recipe_class = _install_and_load(reqs, 'zc.buildout', entry, buildout)
...@@ -1035,7 +1045,7 @@ class Options(UserDict.DictMixin): ...@@ -1035,7 +1045,7 @@ class Options(UserDict.DictMixin):
"The option name in substitution, %s,\n" "The option name in substitution, %s,\n"
"has invalid characters." "has invalid characters."
% ref) % ref)
v = self.buildout[s[0]].get(s[1], None, seen) v = self.buildout[s[0]].get(s[1], None, seen)
if v is None: if v is None:
raise MissingOption("Referenced option does not exist:", *s) raise MissingOption("Referenced option does not exist:", *s)
...@@ -1043,7 +1053,7 @@ class Options(UserDict.DictMixin): ...@@ -1043,7 +1053,7 @@ class Options(UserDict.DictMixin):
subs.append('') subs.append('')
return ''.join([''.join(v) for v in zip(value[::2], subs)]) return ''.join([''.join(v) for v in zip(value[::2], subs)])
def __getitem__(self, key): def __getitem__(self, key):
try: try:
return self._data[key] return self._data[key]
...@@ -1147,7 +1157,7 @@ def _save_option(option, value, f): ...@@ -1147,7 +1157,7 @@ def _save_option(option, value, f):
if value.endswith('\n\t'): if value.endswith('\n\t'):
value = value[:-2] + '%(__buildout_space_n__)s' value = value[:-2] + '%(__buildout_space_n__)s'
print >>f, option, '=', value print >>f, option, '=', value
def _save_options(section, options, f): def _save_options(section, options, f):
print >>f, '[%s]' % section print >>f, '[%s]' % section
items = options.items() items = options.items()
...@@ -1210,7 +1220,7 @@ def _open(base, filename, seen): ...@@ -1210,7 +1220,7 @@ def _open(base, filename, seen):
seen.pop() seen.pop()
return result return result
ignore_directories = '.svn', 'CVS' ignore_directories = '.svn', 'CVS'
def _dir_hash(dir): def _dir_hash(dir):
...@@ -1226,7 +1236,7 @@ def _dir_hash(dir): ...@@ -1226,7 +1236,7 @@ def _dir_hash(dir):
for name in filenames: for name in filenames:
hash.update(open(os.path.join(dirpath, name)).read()) hash.update(open(os.path.join(dirpath, name)).read())
return hash.digest().encode('base64').strip() return hash.digest().encode('base64').strip()
def _dists_sig(dists): def _dists_sig(dists):
result = [] result = []
for dist in dists: for dist in dists:
...@@ -1248,7 +1258,7 @@ def _update_section(s1, s2): ...@@ -1248,7 +1258,7 @@ def _update_section(s1, s2):
s2[key] = "\n".join([v for v in s1.get(key, "").split('\n') s2[key] = "\n".join([v for v in s1.get(key, "").split('\n')
if v not in s2[k].split('\n')]) if v not in s2[k].split('\n')])
del s2[k] del s2[k]
s1.update(s2) s1.update(s2)
return s1 return s1
...@@ -1278,7 +1288,7 @@ def _doing(): ...@@ -1278,7 +1288,7 @@ def _doing():
if d: if d:
doing.append(d) doing.append(d)
tb = tb.tb_next tb = tb.tb_next
if doing: if doing:
sys.stderr.write('While:\n') sys.stderr.write('While:\n')
for d in doing: for d in doing:
...@@ -1335,13 +1345,13 @@ Options: ...@@ -1335,13 +1345,13 @@ Options:
Don't read user defaults. Don't read user defaults.
-o -o
Run in off-line mode. This is equivalent to the assignment Run in off-line mode. This is equivalent to the assignment
buildout:offline=true. buildout:offline=true.
-O -O
Run in non-off-line mode. This is equivalent to the assignment Run in non-off-line mode. This is equivalent to the assignment
buildout:offline=false. This is the default buildout mode. The buildout:offline=false. This is the default buildout mode. The
-O option would normally be used to override a true offline -O option would normally be used to override a true offline
setting in a configuration file. setting in a configuration file.
...@@ -1355,10 +1365,10 @@ Options: ...@@ -1355,10 +1365,10 @@ Options:
-N -N
Run in non-newest mode. This is equivalent to the assignment Run in non-newest mode. This is equivalent to the assignment
buildout:newest=false. With this setting, buildout will not seek buildout:newest=false. With this setting, buildout will not seek
new distributions if installed distributions satisfy it's new distributions if installed distributions satisfy it's
requirements. requirements.
-D -D
...@@ -1408,7 +1418,7 @@ Commands: ...@@ -1408,7 +1418,7 @@ Commands:
The script can be given either as a script path or a path to a The script can be given either as a script path or a path to a
directory containing a setup.py script. directory containing a setup.py script.
""" """
def _help(): def _help():
print _usage print _usage
...@@ -1450,7 +1460,7 @@ def main(args=None): ...@@ -1450,7 +1460,7 @@ def main(args=None):
else: else:
_help() _help()
op = op[1:] op = op[1:]
if op[:1] in ('c', 't'): if op[:1] in ('c', 't'):
op_ = op[:1] op_ = op[:1]
op = op[1:] op = op[1:]
...@@ -1527,8 +1537,8 @@ def main(args=None): ...@@ -1527,8 +1537,8 @@ def main(args=None):
sys.stderr.write(_internal_error_template) sys.stderr.write(_internal_error_template)
traceback.print_exception(*exc_info) traceback.print_exception(*exc_info)
sys.exit(1) sys.exit(1)
finally: finally:
logging.shutdown() logging.shutdown()
......
...@@ -2368,15 +2368,17 @@ parts: ...@@ -2368,15 +2368,17 @@ parts:
Extensions Extensions
---------- ----------
An **experimental** feature allows code to be loaded and run after A feature allows code to be loaded and run after
configuration files have been read but before the buildout has begun configuration files have been read but before the buildout has begun
any processing. The intent is to allow special plugins such as any processing. The intent is to allow special plugins such as
urllib2 request handlers to be loaded. urllib2 request handlers to be loaded.
To load an extension, we use the extensions option and list one or To load an extension, we use the extensions option and list one or
more distribution requirements, on separate lines. The distributions more distribution requirements, on separate lines. The distributions
named will be loaded and any zc.buildout.extensions entry points found named will be loaded and any ``zc.buildout.extension`` entry points found
will be called with the buildout as an argument. will be called with the buildout as an argument. When buildout
finishes processing, any ``zc.buildout.unloadextension`` entry points
found will be called with the buildout as an argument.
Let's create a sample extension in our sample buildout created in the Let's create a sample extension in our sample buildout created in the
previous section: previous section:
...@@ -2387,6 +2389,8 @@ previous section: ...@@ -2387,6 +2389,8 @@ previous section:
... """ ... """
... def ext(buildout): ... def ext(buildout):
... print 'ext', list(buildout) ... print 'ext', list(buildout)
... def unload(buildout):
... print 'unload', list(buildout)
... """) ... """)
>>> write(sample_bootstrapped, 'demo', 'setup.py', >>> write(sample_bootstrapped, 'demo', 'setup.py',
...@@ -2395,7 +2399,10 @@ previous section: ...@@ -2395,7 +2399,10 @@ previous section:
... ...
... setup( ... setup(
... name = "demo", ... name = "demo",
... entry_points = {'zc.buildout.extension': ['ext = demo:ext']}, ... entry_points = {
... 'zc.buildout.extension': ['ext = demo:ext'],
... 'zc.buildout.unloadextension': ['ext = demo:unload'],
... },
... ) ... )
... """) ... """)
...@@ -2436,6 +2443,7 @@ We see that our extension is loaded and executed: ...@@ -2436,6 +2443,7 @@ We see that our extension is loaded and executed:
>>> print system(os.path.join(sample_bootstrapped, 'bin', 'buildout')), >>> print system(os.path.join(sample_bootstrapped, 'bin', 'buildout')),
ext ['buildout'] ext ['buildout']
Develop: '/sample-bootstrapped/demo' Develop: '/sample-bootstrapped/demo'
unload ['buildout']
Allow hosts Allow hosts
----------- -----------
......
...@@ -2895,6 +2895,7 @@ def test_suite(): ...@@ -2895,6 +2895,7 @@ def test_suite():
zc.buildout.testing.normalize_endings, zc.buildout.testing.normalize_endings,
zc.buildout.testing.normalize_script, zc.buildout.testing.normalize_script,
normalize_bang, normalize_bang,
(re.compile('Downloading.*setuptools.*egg\n'), ''),
]), ]),
)) ))
......
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