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):
{1}

{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}