diff --git a/.gitignore b/.gitignore
index 2b6f7c3..5750ef4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,42 @@
+.coverage
*.egg-info
+*.log
*.mo
*.py?
-*.sw?
-*.wpr
-.*
+*.swp
+# dirs
+bin/
+buildout-cache/
+develop-eggs/
+eggs/
+extras/
+htmlcov/
+include/
+lib/
+local/
+node_modules/
+parts/
+src/*
+test.plone_addon/
+var/
+# files
+.installed.cfg
+.mr.developer.cfg
+Gruntfile.js
+lib64
+log.html
+output.xml
+package.json
+pip-selfcheck.json
+report.html
+.vscode/
+.tox/
+reports/
+# excludes
+!.coveragerc
+!.editorconfig
+!.gitattributes
!.gitignore
+!.gitkeep
!.travis.yml
-dist/
+!src/collective
diff --git a/.travis.yml b/.travis.yml
index d005453..324216c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,50 +1,55 @@
language: python
-addons:
- chrome: stable
+python:
+ - "2.7"
matrix:
include:
- python: "2.7"
- env: PLONE_VERSION=5.1.x
+ env: PLONE_VERSION=51
- python: "2.7"
- env: PLONE_VERSION=5.2.x
- - python: "3.6"
- env: PLONE_VERSION=5.2.x
+ env: PLONE_VERSION=52
- python: "3.7"
- env: PLONE_VERSION=5.2.x
+ env: PLONE_VERSION=52
- python: "3.8"
- env: PLONE_VERSION=5.2.x
+ env: PLONE_VERSION=52
+ dist: xenial
cache:
pip: true
directories:
- eggs
- downloads
+fast_finish: true
before_install:
-# install chrome webdriver
- - mkdir webdriver;
- wget https://chromedriver.storage.googleapis.com/2.40/chromedriver_linux64.zip;
- unzip chromedriver_linux64.zip -d webdriver;
- export PATH=$PATH:$(pwd)/webdriver;
+ - virtualenv -p `which python` .
- mkdir -p $HOME/buildout-cache/{eggs,downloads}
- mkdir $HOME/.buildout
- echo "[buildout]" > $HOME/.buildout/default.cfg
- echo "download-cache = $HOME/buildout-cache/downloads" >> $HOME/.buildout/default.cfg
- echo "eggs-directory = $HOME/buildout-cache/eggs" >> $HOME/.buildout/default.cfg
- - pip install zc.buildout
- - sed -ie "s#5.1.x.cfg#$PLONE_VERSION.cfg#" buildout.cfg
+ - bin/pip install -r requirements.txt -c constraints_plone$PLONE_VERSION.txt
+ # - bin/buildout -N out:download-cache=downloads code-analysis:return-status-codes=True annotate
+ # - bin/buildout -N buildout:download-cache=downloads code-analysis:return-status-codes=True
+ - cp test_plone$PLONE_VERSION.cfg buildout.cfg
+
install:
- - buildout -N annotate
- - buildout -N
+ - bin/buildout -N -t 3 code-analysis:return-status-codes=True annotate
+ - bin/buildout -N -t 3 code-analysis:return-status-codes=True
+
before_script:
- - 'export DISPLAY=:99.0'
+ - "export DISPLAY=:99.0"
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
- sleep 3
+ - firefox -v
+
script:
-# Run code-analysis, except on Python 3.6, which mysteriously fails to find zc.buildout.
+ # Run code-analysis, except on Python 3.6, which mysteriously fails to find zc.buildout.
- python --version 2> /dev/stdout | grep 3.6 || bin/code-analysis
- bin/test
+
+after_script:
+ - bin/createcoverage --output-dir=parts/test/coverage
+
after_success:
- - bin/createcoverage -t "--layer=!Robot"
- - pip install --upgrade coveralls
+ - bin/pip install coverage
+ - bin/python -m coverage.pickle2json
+ - bin/pip install -q coveralls
- coveralls
-notifications:
- irc: irc.freenode.org#plone-testing
diff --git a/CHANGES.rst b/CHANGES.rst
index 36a1d13..77b1d26 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -4,7 +4,11 @@ Changelog
4.2 (unreleased)
----------------
-- Nothing changed yet.
+- Fallback to default event field value in accessor, if location reference is not set.
+ [cekk]
+
+- Change field in behaviors: they are now relation fields to be correctly serialized in plone.restapi.
+ [cekk]
4.1 (2020-07-10)
@@ -41,8 +45,6 @@ New features:
- uninstall profiles
[petschki]
-- Customize event_summary view and portlet event renderer to properly show location info.
- [cekk]
Bug fixes:
diff --git a/base.cfg b/base.cfg
new file mode 100644
index 0000000..480f0dc
--- /dev/null
+++ b/base.cfg
@@ -0,0 +1,106 @@
+[buildout]
+show-picked-versions = true
+extensions =
+ mr.developer
+
+index = https://pypi.python.org/simple/
+
+parts =
+ instance
+ test
+ code-analysis
+ coverage
+ test-coverage
+ createcoverage
+ releaser
+ i18ndude
+ omelette
+ robot
+ plone-helper-scripts
+develop = .
+
+
+[instance]
+recipe = plone.recipe.zope2instance
+user = admin:admin
+http-address = 8080
+environment-vars =
+ zope_i18n_compile_mo_files true
+eggs =
+ Plone
+ Pillow
+ collective.venue [test]
+
+
+[code-analysis]
+recipe = plone.recipe.codeanalysis[recommended]
+directory = ${buildout:directory}/src/collective
+return-status-codes = False
+
+
+[omelette]
+recipe = collective.recipe.omelette
+eggs = ${instance:eggs}
+
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = ${instance:eggs}
+initialization =
+ os.environ['TZ'] = 'UTC'
+defaults = ['-s', 'collective.venue', '--auto-color', '--auto-progress']
+
+
+[coverage]
+recipe = zc.recipe.egg
+eggs = coverage
+
+
+[test-coverage]
+recipe = collective.recipe.template
+input = inline:
+ #!/bin/bash
+ export TZ=UTC
+ ${buildout:directory}/bin/coverage run bin/test $*
+ ${buildout:directory}/bin/coverage html
+ ${buildout:directory}/bin/coverage report -m --fail-under=90
+ # Fail (exit status 1) if coverage returns exit status 2 (this happens
+ # when test coverage is below 100%.
+output = ${buildout:directory}/bin/test-coverage
+mode = 755
+
+
+[createcoverage]
+recipe = zc.recipe.egg
+eggs = createcoverage
+
+
+[robot]
+recipe = zc.recipe.egg
+eggs =
+ ${test:eggs}
+ plone.app.robotframework[debug,reload]
+
+
+[releaser]
+recipe = zc.recipe.egg
+eggs = zest.releaser
+
+
+[i18ndude]
+recipe = zc.recipe.egg
+eggs = i18ndude
+
+[plone-helper-scripts]
+recipe = zc.recipe.egg
+eggs =
+ Products.CMFPlone
+ ${instance:eggs}
+interpreter = zopepy
+scripts =
+ zopepy
+ plone-compile-resources
+
+[versions]
+# Don't use a released version of collective.venue
+collective.venue =
diff --git a/bobtemplate.cfg b/bobtemplate.cfg
new file mode 100644
index 0000000..16211ef
--- /dev/null
+++ b/bobtemplate.cfg
@@ -0,0 +1,4 @@
+[main]
+version = 5.1
+template = plone_addon
+git_init = True
diff --git a/buildout.cfg b/buildout.cfg
index 2365ea7..ba7ee39 100644
--- a/buildout.cfg
+++ b/buildout.cfg
@@ -1,55 +1,7 @@
[buildout]
+# use this extend one of the buildout configuration:
extends =
- https://raw.githubusercontent.com/collective/buildout.plonetest/master/test-5.1.x.cfg
- https://raw.githubusercontent.com/collective/buildout.plonetest/master/qa.cfg
- https://raw.githubusercontent.com/plone/plone.app.robotframework/master/versions.cfg
-
-package-name = collective.venue
-package-extras =
-
-parts +=
- code-analysis
- i18ndude
- node
- omelette
-
-extensions = mr.developer
-eggs +=
- collective.geolocationbehavior
-sources = sources
-versions = versions
-sources-dir = extras
-#auto-checkout =
-# plone.formwidget.geolocation
-# collective.geolocationbehavior
-
-[code-analysis]
-flake8-ignore = E501
-
-[i18ndude]
-recipe = zc.recipe.egg
-eggs = i18ndude
-
-[omelette]
-recipe = collective.recipe.omelette
-eggs = ${test:eggs}
-
-[versions]
-# use latest version of coverage
-coverage =
-setuptools =
-zc.buildout =
-
-[sources]
-plone.formwidget.geolocation = git ${remotes:collective}/plone.formwidget.geolocation.git pushurl=${remotes:collective_push}/plone.formwidget.geolocation.git branch=map-settings
-collective.geolocationbehavior = git ${remotes:collective}/collective.geolocationbehavior.git pushurl=${remotes:collective_push}/collective.geolocationbehavior.git branch=petschki-indexer-adapter
-
-[versions]
-plone.formwidget.geolocation =
-collective.geolocationbehavior =
-collective.venue =
-
-[remotes]
-# Collective
-collective = https://github.com/collective
-collective_push = git@github.com:collective
+# test_plone43.cfg
+# test_plone50.cfg
+# test_plone51.cfg
+ test_plone52.cfg
diff --git a/collective/venue/behaviors.py b/collective/venue/behaviors.py
index c2454d2..89ff9bb 100644
--- a/collective/venue/behaviors.py
+++ b/collective/venue/behaviors.py
@@ -12,6 +12,7 @@
from zope import schema
from zope.interface import provider
from zope.schema.interfaces import IContextAwareDefaultFactory
+from z3c.relationfield.schema import RelationChoice
@provider(IContextAwareDefaultFactory)
@@ -25,25 +26,24 @@ def default_location(context):
@provider(IFormFieldProvider)
class ILocation(model.Schema, IVenueEnabled, IDXEvent):
- location_uid = schema.Choice(
+ location_ref = RelationChoice(
title=_(u'label_event_location', default=u'Location'),
description=_(
u'description_event_location', default=u'Select a location.'
),
required=False,
- missing_value='',
- defaultFactory=default_location,
+ # defaultFactory=default_location,
vocabulary='plone.app.vocabularies.Catalog',
)
form.widget(
- 'location_uid',
+ 'location_ref',
RelatedItemsFieldWidget,
+ vocabulary='plone.app.vocabularies.Catalog',
pattern_options={
'selectableTypes': ['Venue'],
- 'basePath': get_base_path,
+ # 'basePath': get_base_path,
},
)
-
location_notes = schema.Text(
title=_(u'label_event_location_notes', default=u'Location notes'),
description=_(
@@ -56,7 +56,7 @@ class ILocation(model.Schema, IVenueEnabled, IDXEvent):
directives.fieldset(
'venue',
label=_(u'fieldset_venue'),
- fields=['location_uid', 'location_notes'],
+ fields=['location_ref', 'location_notes'],
)
@@ -73,18 +73,17 @@ def default_organizer(context):
@provider(IFormFieldProvider)
class IOrganizer(model.Schema, IVenueEnabled):
- organizer_uid = schema.Choice(
+ organizer_ref = RelationChoice(
title=_(u'label_event_organizer', default=u'Organizer'),
description=_(
u'description_event_organizer', default=u'Select an organizer.'
),
required=False,
- missing_value='',
- defaultFactory=default_organizer,
+ # defaultFactory=default_organizer,
vocabulary='plone.app.vocabularies.Catalog',
)
form.widget(
- 'organizer_uid',
+ 'organizer_ref',
RelatedItemsFieldWidget,
pattern_options={
'selectableTypes': ['Venue'],
@@ -104,6 +103,6 @@ class IOrganizer(model.Schema, IVenueEnabled):
directives.fieldset(
'venue',
label=_(u'fieldset_venue'),
- fields=['organizer_uid', 'organizer_notes'],
+ fields=['organizer_ref', 'organizer_notes'],
order=10,
)
diff --git a/collective/venue/configure.zcml b/collective/venue/configure.zcml
index 3004dce..049da4f 100644
--- a/collective/venue/configure.zcml
+++ b/collective/venue/configure.zcml
@@ -142,7 +142,15 @@
handler="collective.venue.upgrades.upgrade_registry"
profile="collective.venue:base"
/>
-
+
+
{title}' # noqa
+ @property
+ def _default_location_template(self):
+ return u'{location}'
+
+ @property
+ def default_location(self):
+ location = getattr(self.context, 'location', '')
+ if not location:
+ return ''
+ return self._default_location_template.format(location=location)
+
@property
def location(self):
context = self.context
- location_ref = ILocation(context, None)
+ location_behavior = ILocation(context, None)
+ if not location_behavior:
+ return self.default_location
+ location_ref = location_behavior.location_ref
if not location_ref:
- return
- location_uid = location_ref.location_uid
- location_notes = location_ref.location_notes
- location = uuidToObject(location_uid)
+ return self.default_location
+ location_notes = location_behavior.location_notes
+ location = location_ref.to_object
+ if not location:
+ return self.default_location
meta_basic = IBasic(location, None)
add = IAddress(location, None)
@@ -51,7 +66,7 @@ def location(self):
if site_path not in location_path:
# location in different site - cannot directly open it
location_url = u'{0}/@@venue_view?uid={1}'.format(
- site.absolute_url(), location_uid
+ site.absolute_url(), location.UID()
)
country = get_pycountry_name(add.country)
diff --git a/collective/venue/geolocation.py b/collective/venue/geolocation.py
index db7b961..db2fced 100644
--- a/collective/venue/geolocation.py
+++ b/collective/venue/geolocation.py
@@ -9,10 +9,12 @@
def get_location_ref(obj):
- location_ref = ILocation(obj, None)
- if location_ref:
- location_uid = location_ref.location_uid
- return uuidToObject(location_uid)
+ location_behavior = ILocation(obj, None)
+ if location_behavior:
+ location_ref = location_behavior.location_ref
+ if location_ref:
+ return location_ref.to_object
+ return None
@indexer(ILocation)
@@ -30,7 +32,6 @@ def longitude(obj):
@adapter(ILocation)
@implementer(IGeoJSONProperties)
class GeoJSONProperties(object):
-
def __init__(self, context):
self.context = context
@@ -44,9 +45,7 @@ def popup(self):
{2}
""".format(
- self.context.absolute_url(),
- self.context.title,
- location.title,
+ self.context.absolute_url(), self.context.title, location.title
)
@property
diff --git a/collective/venue/icalexporter.py b/collective/venue/icalexporter.py
index 7462b9f..7f11ef7 100644
--- a/collective/venue/icalexporter.py
+++ b/collective/venue/icalexporter.py
@@ -24,22 +24,25 @@ def location(self):
if not ILocation.providedBy(self.context):
return super(VenueICalendarEventComponent, self).location
- ref = ILocation(self.context)
- item = uuidToObject(ref.location_uid)
+ location_behavior = ILocation(self.context)
+ location_ref = location_behavior.location_ref
+ if location_ref:
+ item = location_ref.to_object
+ else:
+ item = None
ret = None
if item:
basic = IBasic(item, None)
- locationstring = utils.join_nonempty([
- basic.title,
- utils.get_venue_address_string(item)
- ], sep=u', ')
+ locationstring = utils.join_nonempty(
+ [basic.title, utils.get_venue_address_string(item)], sep=u', '
+ )
ret = {
'value': locationstring,
- 'parameters': {'altrep': item.absolute_url()}
+ 'parameters': {'altrep': item.absolute_url()},
}
else:
- ret = {'value': ref.location_notes}
+ ret = {'value': location_behavior.location_notes}
return ret
@@ -48,19 +51,21 @@ def contact(self):
if not IOrganizer.providedBy(self.context):
return super(VenueICalendarEventComponent, self).contact
- ref = IOrganizer(self.context)
- item = uuidToObject(ref.organizer_uid)
+ organizer_behavior = IOrganizer(self.context)
+ organizer_ref = organizer_behavior.organizer_ref
+ if not organizer_ref:
+ return None
+ item = organizer_ref.to_object
ret = None
if item:
basic = IBasic(item, None)
- retstring = utils.join_nonempty([
- basic.title,
- utils.get_venue_contact_string(item)
- ], sep=u', ')
+ retstring = utils.join_nonempty(
+ [basic.title, utils.get_venue_contact_string(item)], sep=u', '
+ )
ret = {'value': retstring}
else:
- ret = {'value': ref.organizer_notes}
+ ret = {'value': organizer_behavior.organizer_notes}
return ret
@@ -69,15 +74,16 @@ def geo(self):
if not ILocation.providedBy(self.context):
return super(VenueICalendarEventComponent, self).geo
- ref = ILocation(self.context)
- item = uuidToObject(ref.location_uid)
+ location_behavior = ILocation(self.context)
+ location_ref = location_behavior.location_ref
+ if location_ref:
+ item = location_ref.to_object
+ else:
+ item = None
if not IGeolocatable.providedBy(item):
return super(VenueICalendarEventComponent, self).geo
geo = IGeolocatable(item, None)
- ret = (
- geo.geolocation.latitude,
- geo.geolocation.longitude
- )
+ ret = (geo.geolocation.latitude, geo.geolocation.longitude)
return {'value': ret}
diff --git a/collective/venue/indexer.py b/collective/venue/indexer.py
index 7df2b90..a1cbea0 100644
--- a/collective/venue/indexer.py
+++ b/collective/venue/indexer.py
@@ -16,12 +16,12 @@ def _concat_and_utf8(*args):
matter if input was unicode or str.
Taken from ``plone.app.contenttypes.indexers``
"""
- result = ''
+ result = ""
for value in args:
if six.PY2 and isinstance(value, six.text_type):
- value = value.encode('utf-8', 'replace')
+ value = value.encode("utf-8", "replace")
if value:
- result = ' '.join((result, value))
+ result = " ".join((result, value))
return result
@@ -29,19 +29,28 @@ def _concat_and_utf8(*args):
# IGeolocatable (which venue objects provide) are already indexed in
# collective.geolocationbehavior
+
@indexer(ILocation)
def latitude(obj):
- if not obj.location_uid:
- raise AttributeError('no location')
- venue = uuidToObject(obj.location_uid)
+ location_behavior = ILocation(obj)
+ location_ref = location_behavior.location_ref
+ if not location_ref:
+ raise AttributeError("no location")
+ venue = location_ref.to_object
+ if not venue:
+ raise AttributeError("no location")
return venue.geolocation.latitude
@indexer(ILocation)
def longitude(obj):
- if not obj.location_uid:
- raise AttributeError('no location')
- venue = uuidToObject(obj.location_uid)
+ location_behavior = ILocation(obj)
+ location_ref = location_behavior.location_ref
+ if not location_ref:
+ raise AttributeError("no location")
+ venue = location_ref.to_object
+ if not venue:
+ raise AttributeError("no location")
return venue.geolocation.longitude
@@ -51,20 +60,20 @@ def searchable_text_indexer(obj):
address = address_idx(obj)() # returns DelegatingIndexer callable
meta_basic = IBasic(obj)
venue = IVenue(obj)
- notes = venue.notes and venue.notes.output_relative_to(obj) or u''
+ notes = venue.notes and venue.notes.output_relative_to(obj) or u""
if notes:
- transforms = getToolByName(obj, 'portal_transforms')
- body_plain = transforms.convertTo(
- 'text/plain',
- notes,
- mimetype='text/html',
- ).getData().strip()
+ transforms = getToolByName(obj, "portal_transforms")
+ body_plain = (
+ transforms.convertTo("text/plain", notes, mimetype="text/html")
+ .getData()
+ .strip()
+ )
notes = body_plain
parts = [
safe_unicode(address),
safe_unicode(meta_basic.title),
safe_unicode(meta_basic.description),
- safe_unicode(notes)
+ safe_unicode(notes),
]
ret = _concat_and_utf8(*parts)
return ret
diff --git a/collective/venue/interfaces.py b/collective/venue/interfaces.py
index 2849f0f..117a97e 100644
--- a/collective/venue/interfaces.py
+++ b/collective/venue/interfaces.py
@@ -12,19 +12,17 @@
from zope import schema
from zope.interface import Interface
from zope.interface import provider
+from plone.app.vocabularies.catalog import CatalogSource
@provider(IFormFieldProvider)
class IVenue(model.Schema):
"""Marker schema interface for Venue types."""
+
notes = RichText(
- title=_(
- u'label_notes',
- default=u'Notes'
- ),
+ title=_(u'label_notes', default=u'Notes'),
description=_(
- u'help_notes',
- default=u'Additional notes for the address.'
+ u'help_notes', default=u'Additional notes for the address.'
),
required=False,
)
@@ -36,15 +34,12 @@ class IVenueSettings(Interface):
"""
search_base = schema.Choice(
- title=_(
- u'label_search_base',
- default=u'Location Search Base'
- ),
+ title=_(u'label_search_base', default=u'Location Search Base'),
description=_(
u'help_search_base',
u"Path, from which venue types should be searched. Useful for "
u"lineage multisites to seperate main from childsite venue "
- u"folders. Keep empty to search anywhere."
+ u"folders. Keep empty to search anywhere.",
),
required=False,
default='',
@@ -56,20 +51,18 @@ class IVenueSettings(Interface):
pattern_options={
'selectableTypes': ['Folder'], # better: is_folderish
'basePath': get_site,
- }
+ },
)
default_venue = schema.Choice(
- title=_(
- u'label_default_venue',
- default=u'Default Location'
- ),
+ title=_(u'label_default_venue', default=u'Default Location'),
description=_(
- u'help_default_venue',
- u"Default location to be used in events."),
+ u'help_default_venue', u"Default location to be used in events."
+ ),
required=False,
default='',
vocabulary='plone.app.vocabularies.Catalog',
+ # source=CatalogSource(portal_type=""),
)
form.widget(
'default_venue',
@@ -77,17 +70,15 @@ class IVenueSettings(Interface):
pattern_options={
'selectableTypes': ['Venue'],
'basePath': get_base_path,
- }
+ },
)
default_organizer = schema.Choice(
- title=_(
- u'label_default_organizer',
- default=u'Default Organizer'
- ),
+ title=_(u'label_default_organizer', default=u'Default Organizer'),
description=_(
u'help_default_organizer',
- u"Default organizer to be used in events."),
+ u"Default organizer to be used in events.",
+ ),
required=False,
default='',
vocabulary='plone.app.vocabularies.Catalog',
@@ -98,7 +89,7 @@ class IVenueSettings(Interface):
pattern_options={
'selectableTypes': ['Venue'],
'basePath': get_base_path,
- }
+ },
)
diff --git a/collective/venue/profiles/base/metadata.xml b/collective/venue/profiles/base/metadata.xml
index 5ff23bc..40cd45f 100644
--- a/collective/venue/profiles/base/metadata.xml
+++ b/collective/venue/profiles/base/metadata.xml
@@ -1,6 +1,6 @@
- 5
+ 6
profile-plone.app.dexterity:default
diff --git a/collective/venue/testing.py b/collective/venue/testing.py
new file mode 100644
index 0000000..a1cfc9c
--- /dev/null
+++ b/collective/venue/testing.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+from plone.app.contenttypes.testing import PLONE_APP_CONTENTTYPES_FIXTURE
+from plone.app.robotframework.testing import REMOTE_LIBRARY_BUNDLE_FIXTURE
+from plone.app.testing import applyProfile
+from plone.app.testing import FunctionalTesting
+from plone.app.testing import IntegrationTesting
+from plone.app.testing import PloneSandboxLayer
+from plone.testing import z2
+
+import collective.venue
+
+
+class CollectiveVenueLayer(PloneSandboxLayer):
+
+ defaultBases = (PLONE_APP_CONTENTTYPES_FIXTURE,)
+
+ def setUpZope(self, app, configurationContext):
+ # Load any other ZCML that is required for your tests.
+ # The z3c.autoinclude feature is disabled in the Plone fixture base
+ # layer.
+ self.loadZCML(package=collective.venue)
+
+ def setUpPloneSite(self, portal):
+ applyProfile(portal, 'collective.venue:default')
+
+
+COLLECTIVE_VENUE_FIXTURE = CollectiveVenueLayer()
+
+
+COLLECTIVE_VENUE_INTEGRATION_TESTING = IntegrationTesting(
+ bases=(COLLECTIVE_VENUE_FIXTURE,),
+ name='CollectiveVenueLayer:IntegrationTesting',
+)
+
+
+COLLECTIVE_VENUE_FUNCTIONAL_TESTING = FunctionalTesting(
+ bases=(COLLECTIVE_VENUE_FIXTURE,),
+ name='CollectiveVenueLayer:FunctionalTesting',
+)
+
+
+COLLECTIVE_VENUE_ACCEPTANCE_TESTING = FunctionalTesting(
+ bases=(
+ COLLECTIVE_VENUE_FIXTURE,
+ REMOTE_LIBRARY_BUNDLE_FIXTURE,
+ z2.ZSERVER_FIXTURE,
+ ),
+ name='CollectiveVenueLayer:AcceptanceTesting',
+)
diff --git a/collective/venue/tests/__init__.py b/collective/venue/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/collective/venue/tests/robot/test_example.robot b/collective/venue/tests/robot/test_example.robot
new file mode 100644
index 0000000..bcdfadc
--- /dev/null
+++ b/collective/venue/tests/robot/test_example.robot
@@ -0,0 +1,66 @@
+# ============================================================================
+# EXAMPLE ROBOT TESTS
+# ============================================================================
+#
+# Run this robot test stand-alone:
+#
+# $ bin/test -s collective.venue -t test_example.robot --all
+#
+# Run this robot test with robot server (which is faster):
+#
+# 1) Start robot server:
+#
+# $ bin/robot-server --reload-path src collective.venue.testing.COLLECTIVE_VENUE_ACCEPTANCE_TESTING
+#
+# 2) Run robot tests:
+#
+# $ bin/robot src/collective/venue/tests/robot/test_example.robot
+#
+# See the http://docs.plone.org for further details (search for robot
+# framework).
+#
+# ============================================================================
+
+*** Settings *****************************************************************
+
+Resource plone/app/robotframework/selenium.robot
+Resource plone/app/robotframework/keywords.robot
+
+Library Remote ${PLONE_URL}/RobotRemote
+
+Test Setup Open test browser
+Test Teardown Close all browsers
+
+
+*** Test Cases ***************************************************************
+
+Scenario: As a member I want to be able to log into the website
+ [Documentation] Example of a BDD-style (Behavior-driven development) test.
+ Given a login form
+ When I enter valid credentials
+ Then I am logged in
+
+
+*** Keywords *****************************************************************
+
+# --- Given ------------------------------------------------------------------
+
+a login form
+ Go To ${PLONE_URL}/login_form
+ Wait until page contains Login Name
+ Wait until page contains Password
+
+
+# --- WHEN -------------------------------------------------------------------
+
+I enter valid credentials
+ Input Text __ac_name admin
+ Input Text __ac_password secret
+ Click Button Log in
+
+
+# --- THEN -------------------------------------------------------------------
+
+I am logged in
+ Wait until page contains You are now logged in
+ Page should contain You are now logged in
diff --git a/collective/venue/tests/test_event_accessor.py b/collective/venue/tests/test_event_accessor.py
new file mode 100644
index 0000000..8d31152
--- /dev/null
+++ b/collective/venue/tests/test_event_accessor.py
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+"""Setup tests for this package."""
+from collective.venue.testing import (
+ COLLECTIVE_VENUE_INTEGRATION_TESTING,
+) # noqa: E501
+from plone import api
+from plone.app.testing import setRoles
+from plone.app.testing import TEST_USER_ID
+from plone.event.interfaces import IEventAccessor
+from z3c.relationfield import RelationValue
+from z3c.relationfield.event import _setRelation
+from zc.relation.interfaces import ICatalog
+from zope.component import getUtility
+from zope.intid.interfaces import IIntIds
+
+import unittest
+
+
+class TestEventAccessor(unittest.TestCase):
+ """Test that collective.venue is properly installed."""
+
+ layer = COLLECTIVE_VENUE_INTEGRATION_TESTING
+
+ def setUp(self):
+ """Custom shared utility setup for tests."""
+ self.portal = self.layer['portal']
+ setRoles(self.portal, TEST_USER_ID, ['Manager'])
+ self.portal.invokeFactory(
+ 'Event', 'test-event', title=u"Test event", location="Wonderland"
+ )
+ self.event = self.portal['test-event']
+
+ def test_get_standard_location_if_location_ref_not_set(self):
+ """Test if collective.venue is installed."""
+ accessor = IEventAccessor(self.event)
+ self.assertIn(self.event.location, accessor.location)
+
+ def test_get_venue_location_if_location_ref_is_set(self):
+ """Test if collective.venue is installed."""
+ venue_id = self.portal.invokeFactory(
+ 'Venue', 'test-venue', title=u"Test Venue"
+ )
+ venue = self.portal[venue_id]
+
+ intids_tool = getUtility(IIntIds)
+ to_id = intids_tool.getId(venue)
+ rel = RelationValue(to_id)
+ self.event.location_ref = rel
+ accessor = IEventAccessor(self.event)
+ self.assertNotIn(self.event.location, accessor.location)
+ self.assertIn(venue.title, accessor.location)
diff --git a/collective/venue/tests/test_robot.py b/collective/venue/tests/test_robot.py
new file mode 100644
index 0000000..a9faede
--- /dev/null
+++ b/collective/venue/tests/test_robot.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+from collective.venue.testing import COLLECTIVE_VENUE_ACCEPTANCE_TESTING # noqa: E501
+from plone.app.testing import ROBOT_TEST_LEVEL
+from plone.testing import layered
+
+import os
+import robotsuite
+import unittest
+
+
+def test_suite():
+ suite = unittest.TestSuite()
+ current_dir = os.path.abspath(os.path.dirname(__file__))
+ robot_dir = os.path.join(current_dir, 'robot')
+ robot_tests = [
+ os.path.join('robot', doc) for doc in os.listdir(robot_dir)
+ if doc.endswith('.robot') and doc.startswith('test_')
+ ]
+ for robot_test in robot_tests:
+ robottestsuite = robotsuite.RobotTestSuite(robot_test)
+ robottestsuite.level = ROBOT_TEST_LEVEL
+ suite.addTests([
+ layered(
+ robottestsuite,
+ layer=COLLECTIVE_VENUE_ACCEPTANCE_TESTING,
+ ),
+ ])
+ return suite
diff --git a/collective/venue/tests/test_setup.py b/collective/venue/tests/test_setup.py
new file mode 100644
index 0000000..c8e9479
--- /dev/null
+++ b/collective/venue/tests/test_setup.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+"""Setup tests for this package."""
+from collective.venue.testing import (
+ COLLECTIVE_VENUE_INTEGRATION_TESTING,
+) # noqa: E501
+from plone import api
+from plone.app.testing import setRoles
+from plone.app.testing import TEST_USER_ID
+
+import unittest
+
+try:
+ from Products.CMFPlone.utils import get_installer
+except ImportError:
+ get_installer = None
+
+
+class TestSetup(unittest.TestCase):
+ """Test that collective.venue is properly installed."""
+
+ layer = COLLECTIVE_VENUE_INTEGRATION_TESTING
+
+ def setUp(self):
+ """Custom shared utility setup for tests."""
+ self.portal = self.layer['portal']
+ if get_installer:
+ self.installer = get_installer(self.portal, self.layer['request'])
+ else:
+ self.installer = api.portal.get_tool('portal_quickinstaller')
+
+ def test_product_installed(self):
+ """Test if collective.venue is installed."""
+ self.assertTrue(self.installer.isProductInstalled('collective.venue'))
+
+ def test_browserlayer(self):
+ """Test that IVenueLayer is registered."""
+ from collective.venue.interfaces import IVenueLayer
+ from plone.browserlayer import utils
+
+ self.assertIn(IVenueLayer, utils.registered_layers())
+
+
+class TestUninstall(unittest.TestCase):
+
+ layer = COLLECTIVE_VENUE_INTEGRATION_TESTING
+
+ def setUp(self):
+ self.portal = self.layer['portal']
+ if get_installer:
+ self.installer = get_installer(self.portal, self.layer['request'])
+ else:
+ self.installer = api.portal.get_tool('portal_quickinstaller')
+ roles_before = api.user.get_roles(TEST_USER_ID)
+ setRoles(self.portal, TEST_USER_ID, ['Manager'])
+ self.installer.uninstallProducts(['collective.venue'])
+ setRoles(self.portal, TEST_USER_ID, roles_before)
+
+ def test_product_uninstalled(self):
+ """Test if collective.venue is cleanly uninstalled."""
+ self.assertFalse(self.installer.isProductInstalled('collective.venue'))
+
+ def test_browserlayer_removed(self):
+ """Test that IVenueLayer is removed."""
+ from collective.venue.interfaces import IVenueLayer
+ from plone.browserlayer import utils
+
+ self.assertNotIn(IVenueLayer, utils.registered_layers())
+
diff --git a/collective/venue/upgrades.py b/collective/venue/upgrades.py
index 57fda9f..9a575c9 100644
--- a/collective/venue/upgrades.py
+++ b/collective/venue/upgrades.py
@@ -1,5 +1,14 @@
# -*- coding: utf-8 -*-
+from collective.venue.behaviors import ILocation
+from collective.venue.behaviors import IOrganizer
+from plone import api
+from plone.app.uuid.utils import uuidToObject
from Products.CMFCore.utils import getToolByName
+from z3c.relationfield import RelationValue
+from z3c.relationfield.event import _setRelation
+from zope.component import getUtility
+from zope.intid.interfaces import IIntIds
+
import logging
@@ -26,13 +35,56 @@ def upgrade_3_to_4(context):
# Unregister JavaScript
unregister_resource(
getToolByName(context, 'portal_javascripts'),
- '++resource++collective.venue/scripts.js'
+ '++resource++collective.venue/scripts.js',
)
# Unregister CSS
unregister_resource(
getToolByName(context, 'portal_css'),
- '++resource++collective.venue/styles.css'
+ '++resource++collective.venue/styles.css',
)
upgrade_registry(context)
+
+
+def upgrade_5_to_6(context):
+ """
+ move fields:
+ - from location_uid to location_ref
+ - from organizer_uid to organizer_ref
+ """
+ location_brains = api.content.find(
+ object_provides=ILocation.__identifier__
+ )
+ organizer_brains = api.content.find(
+ object_provides=IOrganizer.__identifier__
+ )
+ for brain in location_brains:
+ fix_ref(
+ brain=brain, old_field='location_uid', new_field='location_ref'
+ )
+ for brain in organizer_brains:
+ fix_ref(
+ brain=brain, old_field='organizer_uid', new_field='organizer_ref'
+ )
+
+
+def fix_ref(brain, old_field, new_field):
+ item = brain.getObject()
+ uid = getattr(item, old_field, '')
+ if not uid:
+ return
+ ref_obj = uuidToObject(uid)
+ delattr(item, old_field)
+ if not ref_obj:
+ return
+ intids_tool = getUtility(IIntIds)
+ to_id = intids_tool.getId(ref_obj)
+ rel = RelationValue(to_id)
+ setattr(item, new_field, rel)
+ _setRelation(item, new_field, rel)
+ logger.info(
+ 'Fix "{field}" for {url}'.format(
+ field=new_field, url=item.absolute_url()
+ )
+ )
diff --git a/constraints.txt b/constraints.txt
new file mode 100644
index 0000000..24cbf87
--- /dev/null
+++ b/constraints.txt
@@ -0,0 +1 @@
+-c constraints_plone52.txt
diff --git a/constraints_plone50.txt b/constraints_plone50.txt
new file mode 100644
index 0000000..f3a36b2
--- /dev/null
+++ b/constraints_plone50.txt
@@ -0,0 +1,2 @@
+setuptools==27.3.0
+zc.buildout==2.5.3
diff --git a/constraints_plone51.txt b/constraints_plone51.txt
new file mode 100644
index 0000000..b57e577
--- /dev/null
+++ b/constraints_plone51.txt
@@ -0,0 +1,2 @@
+setuptools==40.2.0
+zc.buildout==2.12.2
diff --git a/constraints_plone52.txt b/constraints_plone52.txt
new file mode 100644
index 0000000..b57e577
--- /dev/null
+++ b/constraints_plone52.txt
@@ -0,0 +1,2 @@
+setuptools==40.2.0
+zc.buildout==2.12.2
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..fa2f614
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,3 @@
+-c constraints_plone52.txt
+setuptools
+zc.buildout
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..e920041
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,19 @@
+[check-manifest]
+ignore =
+ *.cfg
+ .coveragerc
+ .editorconfig
+ .gitattributes
+
+[isort]
+# for details see
+# http://docs.plone.org/develop/styleguide/python.html#grouping-and-sorting
+force_alphabetical_sort = True
+force_single_line = True
+lines_after_imports = 2
+line_length = 200
+not_skip = __init__.py
+
+[flake8]
+exclude = bootstrap.py,docs,*.egg.,omelette
+max-complexity = 15
diff --git a/setup.py b/setup.py
index 288d0fc..0ee36e5 100644
--- a/setup.py
+++ b/setup.py
@@ -1,24 +1,24 @@
# -*- coding: utf-8 -*-
-from setuptools import setup
+"""Installer for the collective.venue package."""
from setuptools import find_packages
+from setuptools import setup
-version = '4.2.dev0'
+version = "4.2.dev0"
-def read(fname):
- with open(fname) as f:
- return f.read()
+long_description = "\n\n".join([open("README.rst").read(), open("CHANGES.rst").read()])
setup(
- name='collective.venue',
+ name="collective.venue",
version=version,
- description="Dexterity venue type for use with events.",
- long_description=read("README.rst")
- + "\n"
- + read("CHANGES.rst"),
+ description="An add-on for Plone",
+ long_description=long_description,
+ # Get more from https://pypi.python.org/pypi?%3Aaction=list_classifiers
classifiers=[
+ "Environment :: Web Environment",
"Framework :: Plone",
+ "Framework :: Plone :: Addon",
"Framework :: Plone :: 5.1",
"Framework :: Plone :: 5.2",
"Framework :: Plone :: Addon",
@@ -26,43 +26,56 @@ def read(fname):
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
+ "Operating System :: OS Independent",
+ "License :: OSI Approved :: GNU General Public License v2 (GPLv2)",
"Topic :: Software Development :: Libraries :: Python Modules",
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: GNU General Public License v2 (GPLv2)",
],
- keywords='plone collective event geo location',
- author='Johannes Raggam',
- author_email='raggam-nl@adm.at',
- url='https://github.com/collective/collective.venue',
- license='GPL',
- packages=find_packages(exclude=['ez_setup']),
- namespace_packages=['collective'],
+ keywords="plone collective event geo location",
+ author="Johannes Raggam",
+ author_email="raggam-nl@adm.at",
+ url="https://github.com/collective/collective.venue",
+ license="GPL",
+ packages=find_packages(exclude=["ez_setup"]),
+ namespace_packages=["collective"],
include_package_data=True,
zip_safe=False,
install_requires=[
- 'setuptools',
- 'collective.address',
- 'plone.api',
- 'plone.app.content',
- 'plone.app.dexterity',
- 'plone.app.event',
- 'plone.browserlayer',
- 'plone.event',
- 'plone.registry >= 1.1.0.dev0', # implicit dependency, see PR #10
- 'plone.resource',
- 'Products.CMFPlone',
- 'Products.GenericSetup',
+ "setuptools",
+ "collective.address",
+ "plone.api",
+ "plone.app.content",
+ "plone.app.dexterity",
+ "plone.app.event",
+ "plone.browserlayer",
+ "plone.event",
+ "plone.registry >= 1.1.0.dev0", # implicit dependency, see PR #10
+ "plone.resource",
+ "Products.CMFPlone",
+ "Products.GenericSetup",
],
extras_require={
- 'geolocation': [
- 'collective.geolocationbehavior',
- 'plone.formwidget.geolocation',
- 'geopy',
- 'six',
- ]
+ "geolocation": [
+ "collective.geolocationbehavior",
+ "plone.formwidget.geolocation",
+ "geopy",
+ "six",
+ ],
+ "test": [
+ "plone.app.testing",
+ # Plone KGS does not use this version, because it would break
+ # Remove if your package shall be part of coredev.
+ # plone_coredev tests as of 2016-04-01.
+ "plone.testing>=5.0.0",
+ "plone.app.contenttypes",
+ "plone.app.robotframework[debug]",
+ ],
},
entry_points="""
- [z3c.autoinclude.plugin]
- target = plone
- """,
+ [z3c.autoinclude.plugin]
+ target = plone
+ [console_scripts]
+ update_locale = collective.venue.locales.update:update_locale
+ """,
)
diff --git a/test_plone50.cfg b/test_plone50.cfg
new file mode 100644
index 0000000..88b1320
--- /dev/null
+++ b/test_plone50.cfg
@@ -0,0 +1,11 @@
+[buildout]
+
+extends =
+ https://raw.github.com/collective/buildout.plonetest/master/test-5.0.x.cfg
+ https://raw.githubusercontent.com/collective/buildout.plonetest/master/qa.cfg
+ base.cfg
+
+update-versions-file = test_plone50.cfg
+
+[versions]
+plone.schemaeditor = >=2.0.18
diff --git a/test_plone51.cfg b/test_plone51.cfg
new file mode 100644
index 0000000..11d152b
--- /dev/null
+++ b/test_plone51.cfg
@@ -0,0 +1,11 @@
+[buildout]
+
+extends =
+ https://raw.github.com/collective/buildout.plonetest/master/test-5.1.x.cfg
+ https://raw.githubusercontent.com/collective/buildout.plonetest/master/qa.cfg
+ base.cfg
+
+update-versions-file = test_plone51.cfg
+
+[versions]
+plone.testing = 5.0.0
diff --git a/test_plone52.cfg b/test_plone52.cfg
new file mode 100644
index 0000000..07adcbf
--- /dev/null
+++ b/test_plone52.cfg
@@ -0,0 +1,56 @@
+[buildout]
+
+extends =
+ https://raw.github.com/collective/buildout.plonetest/master/test-5.2.x.cfg
+ https://raw.githubusercontent.com/collective/buildout.plonetest/master/qa.cfg
+ base.cfg
+
+update-versions-file = test_plone52.cfg
+
+[versions]
+plone.testing = 7.0.1
+
+# Added by buildout at 2019-06-23 08:11:18.454738
+PyYAML = 3.13
+argh = 0.26.2
+createcoverage = 1.5
+flake8-blind-except = 0.1.1
+flake8-coding = 1.3.1
+flake8-commas = 2.0.0
+flake8-debugger = 3.1.0
+flake8-deprecated = 1.3
+flake8-isort = 2.6.0
+flake8-pep3101 = 1.2.1
+flake8-plone-api = 1.4
+flake8-plone-hasattr = 0.2.post0
+flake8-print = 3.1.0
+flake8-quotes = 1.0.0
+flake8-string-format = 0.2.3
+flake8-todo = 0.7
+isort = 4.3.10
+mccabe = 0.6.1
+pathtools = 0.1.2
+plone.recipe.codeanalysis = 3.0.1
+prompt-toolkit = 1.0.15
+pyflakes = 1.5.0
+watchdog = 0.9.0
+
+# Required by:
+# plone.recipe.codeanalysis==3.0.1
+check-manifest = 0.37
+
+# Required by:
+# collective.venue==1.0a1
+collective.address = 1.6
+
+# Required by:
+# collective.address==1.5
+pycountry = 18.12.8
+
+# Required by:
+# flake8-isort==2.6.0
+testfixtures = 6.6.0
+
+# Required by:
+# prompt-toolkit==1.0.15
+wcwidth = 0.1.7
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..e8b3ad7
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,174 @@
+[tox]
+envlist =
+ {py27,py37}-lint,
+ py{27}-Plone{51},
+ py{27,37}-Plone{52},
+ build_instance,
+# docs,
+# coverage-report,
+
+skip_missing_interpreters = True
+
+[testenv]
+skip_install = true
+
+extras =
+ develop
+ test
+
+commands =
+ {envbindir}/buildout -c {toxinidir}/{env:version_file} buildout:directory={envdir} buildout:develop={toxinidir} bootstrap
+ {envbindir}/buildout -c {toxinidir}/{env:version_file} buildout:directory={envdir} buildout:develop={toxinidir} annotate
+ {envbindir}/buildout -c {toxinidir}/{env:version_file} buildout:directory={envdir} buildout:develop={toxinidir} install test robot code-analysis
+ coverage run {envbindir}/test -v1 --auto-color {posargs}
+ # coverage run {envbindir}/test -v --all -t robot {posargs}
+ {envbindir}/code-analysis
+
+setenv =
+ COVERAGE_FILE=.coverage.{envname}
+ version_file=test_plone51.cfg
+ Plone50: version_file=test_plone50.cfg
+ Plone51: version_file=test_plone51.cfg
+ Plone52: version_file=test_plone52.cfg
+
+deps =
+ -rrequirements.txt
+ Plone50: -cconstraints_plone50.txt
+ Plone51: -cconstraints_plone51.txt
+ Plone52: -cconstraints_plone52.txt
+ coverage
+
+[testenv:coverage-report]
+skip_install = true
+usedevelop = True
+basepython = python2.7
+
+deps =
+ coverage
+ -cconstraints_plone51.txt
+
+setenv =
+ COVERAGE_FILE=.coverage
+
+commands =
+ coverage erase
+ coverage combine
+ coverage html
+ coverage xml
+ coverage report
+
+
+[lint]
+skip_install = true
+
+deps =
+ isort
+ flake8
+ # helper to generate HTML reports:
+ flake8-html
+ # Useful flake8 plugins that are Python and Plone specific:
+ flake8-coding
+ flake8-debugger
+ flake8-deprecated
+ flake8-print
+ #flake8-pytest
+ flake8-todo
+ flake8-isort
+ mccabe
+ # Potential flake8 plugins that should be used: # TBD
+ #flake8-blind-except
+ #flake8-commas
+ #flake8-docstrings
+ #flake8-mypy
+ #flake8-pep3101
+ #flake8-plone-hasattr
+ #flake8-string-format
+ #flake8_strict
+ #flake8-quotes
+ #flake8-polyfill
+
+commands =
+ mkdir -p {toxinidir}/reports/flake8
+ - flake8 --format=html --htmldir={toxinidir}/reports/flake8 --doctests src setup.py
+ flake8 --doctests src tests setup.py
+ isort --check-only --recursive {toxinidir}/src
+
+whitelist_externals =
+ mkdir
+
+[testenv:isort-apply]
+skip_install = true
+
+deps =
+ isort
+
+commands =
+ isort --apply --recursive {toxinidir}/src
+
+[testenv:py27-lint]
+basepython = python2.7
+skip_install = true
+deps = {[lint]deps}
+commands = {[lint]commands}
+whitelist_externals = {[lint]whitelist_externals}
+
+[testenv:py35-lint]
+basepython = python3.5
+skip_install = true
+deps = {[lint]deps}
+commands = {[lint]commands}
+whitelist_externals = {[lint]whitelist_externals}
+
+[testenv:py36-lint]
+basepython = python3.6
+skip_install = true
+deps = {[lint]deps}
+commands = {[lint]commands}
+whitelist_externals = {[lint]whitelist_externals}
+
+[testenv:py37-lint]
+basepython = python3.7
+skip_install = true
+deps = {[lint]deps}
+commands = {[lint]commands}
+whitelist_externals = {[lint]whitelist_externals}
+
+[testenv:docs]
+skip_install = true
+
+deps =
+ Sphinx
+
+commands =
+ sphinx-build -b html -d _build/docs/doctrees docs _build/docs/html
+
+[testenv:update_translation]
+skip_install = true
+
+deps =
+ i18ndude
+
+commands =
+ i18ndude find-untranslated
+ i18ndude rebuild-pot
+ i18ndude merge
+ i18ndude sync
+ i18ndude list
+
+[testenv:release]
+skip_install = true
+
+deps =
+ zest.releaser[recommended]
+
+commands =
+ fullrelease --no-input -v
+
+[testenv:build_instance]
+basepython = python2.7
+skip_install = true
+
+commands =
+ {envbindir}/buildout -c {toxinidir}/{env:version_file} buildout:directory={toxinidir} bootstrap
+ {envbindir}/buildout -c {toxinidir}/{env:version_file} buildout:directory={toxinidir} annotate
+ {envbindir}/buildout -c {toxinidir}/{env:version_file} buildout:directory={toxinidir}