Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Canonical URL updater form #118

Merged
merged 7 commits into from
Jul 24, 2017
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ Changelog

There's a frood who really knows where his towel is.

2.11 (unreleased)
^^^^^^^^^^^^^^^^^
2.10.1 (unreleased)
^^^^^^^^^^^^^^^^^^^

- Fix Canonical URL updater form;
a new upgrade step is provided to update the ``objects_provides`` catalog index (fixes `#115 <https://github.com/collective/sc.social.like/issues/115>`_).
[hvelarde]

- Add ``canonical_domain`` field record to the registry when upgrading;
this fixes an issue in the upgrade step to profile version 3045 (fixes `#114 <https://github.com/collective/sc.social.like/issues/114>`_).
Expand Down
91 changes: 53 additions & 38 deletions sc/social/like/browser/canonicalurl.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,24 @@
# -*- coding:utf-8 -*-
from DateTime import DateTime
from plone import api
from plone.supermodel import model
from sc.social.like import LikeMessageFactory as _
from sc.social.like.behaviors import ISocialMedia
from sc.social.like.interfaces import ISocialLikeSettings
from sc.social.like.logger import logger
from sc.social.like.utils import get_valid_objects
from sc.social.like.utils import validate_canonical_domain
from z3c.form import button
from z3c.form import field
from z3c.form import form
from zope import schema


def get_valid_objects(brains):
"""Generate a list of objects associated with valid brains."""
for b in brains:
try:
obj = b.getObject()
except KeyError:
obj = None

if obj is None: # warn on broken entries in the catalog
logger.warn(
u'Invalid reference in the catalog: {0}'.format(b.getPath()))
continue
yield obj


class ICanonicalURLUpdater(model.Schema):
"""A form to update the canonical url of portal objects based on a date."""

canonical_domain = schema.URI(
title=_(u'Canonical domain'),
old_canonical_domain = schema.URI(
title=_(u'Old canonical domain'),
description=_(
u'help_canonical_domain',
default=u'The canonical domain will be used to construct the canonical URL (<code>og:url</code> property) of portal objects. '
Expand All @@ -42,11 +30,12 @@ class ICanonicalURLUpdater(model.Schema):
constraint=validate_canonical_domain,
)

created_before = schema.Date(
published_before = schema.Date(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I miss the 'date' name in this attribute, how about to change it to 'published_before_date'?

title=_(u'Date'),
description=_(
u'help_date',
u'All objects in the catalog created before this date will be updated.'
u'help_published_before',
default=u'Objects published before this date will be updated using the canonical domain defined in this form; '
u'objects published on or after this date will be updated using the canonical domain defined in the control panel configlet.'
),
required=True,
)
Expand All @@ -56,17 +45,35 @@ class CanonicalURLUpdater(form.Form):
"""A form to update the canonical url of portal objects based on a date."""

fields = field.Fields(ICanonicalURLUpdater)
label = _(u'This form is used to update the canonical URL of objects providing the Social Media behavior')
label = _(u'Canonical URL updater form')
description = _(
u'This form will update the canonical URL of all Dexterity-based '
u'objects in the catalog providing the Social Media behavior.'
)
ignoreContext = True

@property
def canonical_domain(self):
return api.portal.get_registry_record(name='canonical_domain', interface=ISocialLikeSettings)

def update(self):
"""Disable the green bar and the portlet columns."""
super(CanonicalURLUpdater, self).update()
# show error message if no canonical domain has been defined in the configlet
if not self.canonical_domain:
msg = _(u'Canonical domain has not been defined in the control panel configlet.')
api.portal.show_message(message=msg, request=self.request, type='error')

# disable the green bar and the portlet columns
self.request.set('disable_border', 1)
self.request.set('disable_plone.rightcolumn', 1)
self.request.set('disable_plone.leftcolumn', 1)

@button.buttonAndHandler(_('Update'), name='update')
@property
def update_button_enabled(self):
"""Condition to be used to display the "Update" button."""
return self.canonical_domain is not None

@button.buttonAndHandler(_('Update'), name='update', condition=lambda form: form.update_button_enabled)
def handle_update(self, action):
data, errors = self.extractData()
if errors:
Expand All @@ -80,26 +87,34 @@ def handle_cancel(self, action):
self.request.response.redirect(self.context.absolute_url())

def update_canonical_url(self, data):
"""Update all objects providing the ISocialMedia behavior
that were created before the specified date.
"""
canonical_domain = data['canonical_domain']
created_before = data['created_before'].isoformat()
logger.info(
u'Updating canonical URL of items created before {0}; '
u'using canonical domain "{1}"'.format(created_before, canonical_domain)
)
"""Update the canonical URL of all objects in the catalog
providing the ISocialMedia behavior.

catalog = api.portal.get_tool('portal_catalog')
results = catalog(
Objects published before the specified date will be updated
using the canonical domain defined in this form; objects
published on or after that date will be updated using the
canonical domain defined in the control panel configlet.
"""
old_canonical_domain = data['old_canonical_domain']
new_canonical_domain = self.canonical_domain
published_before = data['published_before'].isoformat()
results = api.content.find(
object_provides=ISocialMedia.__identifier__,
created=dict(query=created_before, range='max'),
review_state='published',
)

total = len(results)
logger.info(u'{0} objects will be processed'.format(total))
logger.info(u'{0} objects will have their canonical URL updated'.format(total))

for obj in get_valid_objects(results):
obj.canonical_url = '{0}/{1}'.format(canonical_domain, obj.virtual_url_path())
# FIXME: we're currently ignoring the Plone site id
# https://github.com/collective/sc.social.like/issues/119
path = '/'.join(obj.getPhysicalPath()[2:])
if obj.effective_date < DateTime(published_before):
# use the canonical domain defined in this form
obj.canonical_url = '{0}/{1}'.format(old_canonical_domain, path)
elif not obj.canonical_url:
# use the canonical domain defined in the configlet
obj.canonical_url = '{0}/{1}'.format(new_canonical_domain, path)

logger.info(u'Done.')
self.status = u'Update complete; {0} items processed.'.format(total)
2 changes: 1 addition & 1 deletion sc/social/like/profiles/default/metadata.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0"?>
<metadata>
<version>3045</version>
<version>3046</version>
</metadata>
5 changes: 4 additions & 1 deletion sc/social/like/subscribers.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,10 @@ def assign_canonical_url(obj, event):

# we can't assign a canonical URL without a canonical domain
if canonical_domain:
obj.canonical_url = '{0}/{1}'.format(canonical_domain, obj.virtual_url_path())
# FIXME: we're currently ignoring the Plone site id
# https://github.com/collective/sc.social.like/issues/119
path = '/'.join(obj.getPhysicalPath()[2:])
obj.canonical_url = '{0}/{1}'.format(canonical_domain, path)
logger.info('canonical_url set for {0}'.format(obj.canonical_url))
else:
logger.warn(
Expand Down
1 change: 1 addition & 0 deletions sc/social/like/tests/test_behaviors.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def setUp(self):
def test_socialmedia_behavior(self):
self.assertTrue(ISocialMedia.providedBy(self.obj))

@unittest.expectedFailure # FIXME: https://github.com/collective/sc.social.like/issues/119
def test_canonical_url(self):
# canonical URL is empty after creation
self.assertIsNone(self.obj.canonical_url)
Expand Down
34 changes: 23 additions & 11 deletions sc/social/like/tests/test_canonicalurl_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"""Test for the canonical URL updater form."""
from DateTime import DateTime
from plone import api
from sc.social.like.interfaces import ISocialLikeSettings
from sc.social.like.testing import HAS_DEXTERITY
from sc.social.like.testing import INTEGRATION_TESTING
from sc.social.like.tests.utils import enable_social_media_behavior
Expand All @@ -23,21 +24,31 @@ def setUp(self):
self.portal = self.layer['portal']
self.request = self.layer['request']
enable_social_media_behavior()
self.setup_content()
api.portal.set_registry_record(
name='canonical_domain', value='https://example.org', interface=ISocialLikeSettings)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought canonical_domain becomes old_canonical_domain

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only in the form.


def setup_content(self):
with api.env.adopt_roles(['Manager']):
api.content.create(self.portal, type='News Item', id='foo')
api.content.create(self.portal, type='News Item', id='bar')
api.content.create(self.portal, type='News Item', id='baz')
obj = api.content.create(self.portal, type='News Item', id='foo')
api.content.transition(obj, 'publish')
obj = api.content.create(self.portal, type='News Item', id='bar')
api.content.transition(obj, 'publish')
obj = api.content.create(self.portal, type='News Item', id='baz')
api.content.transition(obj, 'publish')

# simulate objects were create way long in the past
self.portal['foo'].creation_date = DateTime('2015/01/01')
self.portal['bar'].creation_date = DateTime('2016/01/01')
self.portal['foo'].effective_date = DateTime('2015/01/01')
self.portal['foo'].reindexObject()
self.portal['bar'].effective_date = DateTime('2016/01/01')
self.portal['bar'].reindexObject()
# XXX: publishing an object does not sets its effective date
# https://github.com/plone/plone.api/issues/343
self.portal['baz'].effective_date = DateTime()
self.portal['baz'].reindexObject()

@unittest.expectedFailure # FIXME: https://github.com/collective/sc.social.like/issues/119
def test_update_canonical_url(self):
self.setup_content()
# canonical URL is None as we did not set up a canonical domain
self.assertIsNone(self.portal['foo'].canonical_url)
self.assertIsNone(self.portal['bar'].canonical_url)
Expand All @@ -47,16 +58,17 @@ def test_update_canonical_url(self):
view = api.content.get_view(name, self.portal, self.request)
# simulate data comming from form
data = dict(
canonical_domain='https://example.org',
created_before=DateTime('2017/01/01').asdatetime(),
old_canonical_domain='http://example.org',
published_before=DateTime('2017/01/01').asdatetime(),
)
view.update_canonical_url(data)
# objects created before the specified date will have their
# canonical URL updated
self.assertEqual(
self.portal['foo'].canonical_url, 'https://example.org/plone/foo')
self.portal['foo'].canonical_url, 'http://example.org/plone/foo')
self.assertEqual(
self.portal['bar'].canonical_url, 'https://example.org/plone/bar')
self.portal['bar'].canonical_url, 'http://example.org/plone/bar')
# objects created after the specified date will have their
# canonical URL unchaged
self.assertIsNone(self.portal['baz'].canonical_url)
self.assertEqual(
self.portal['baz'].canonical_url, 'https://example.org/plone/baz')
39 changes: 39 additions & 0 deletions sc/social/like/tests/test_upgrades.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from plone import api
from sc.social.like.config import IS_PLONE_5
from sc.social.like.interfaces import ISocialLikeSettings
from sc.social.like.testing import HAS_DEXTERITY
from sc.social.like.testing import INTEGRATION_TESTING
from zope.component import getUtility

Expand All @@ -14,6 +15,7 @@ class UpgradeTestCaseBase(unittest.TestCase):

def setUp(self, from_version, to_version):
self.portal = self.layer['portal']
self.request = self.layer['request']
self.setup = self.portal['portal_setup']
self.profile_id = u'sc.social.like:default'
self.from_version = from_version
Expand Down Expand Up @@ -285,3 +287,40 @@ def test_enable_social_media_behavior(self):
continue # not a Dexterity-based content type
behaviors = list(fti.behaviors)
self.assertIn(ISocialMedia.__identifier__, behaviors)


class To3046TestCase(UpgradeTestCaseBase):

def setUp(self):
UpgradeTestCaseBase.setUp(self, u'3045', u'3046')

def test_upgrade_to_3046_registrations(self):
version = self.setup.getLastVersionForProfile(self.profile_id)[0]
self.assertGreaterEqual(int(version), int(self.to_version))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tcurvelo I have no idea why this is failing, I think is something from another test; do you mind to dig on it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I already found the issue and fixed it: 78bc1e1

self.assertEqual(self.total_steps, 1)

@unittest.skipUnless(HAS_DEXTERITY, 'plone.app.contenttypes must be installed')
def test_reindex_catalog(self):
# check if the upgrade step is registered
title = u'Reindex catalog'
step = self.get_upgrade_step(title)
self.assertIsNotNone(step)

from sc.social.like.behaviors import ISocialMedia
from sc.social.like.tests.utils import enable_social_media_behavior
with api.env.adopt_roles(['Manager']):
for i in xrange(0, 10):
api.content.create(self.portal, 'News Item', str(i))

# break the catalog by deleting an object without notifying
self.portal._delObject('0', suppress_events=True)
self.assertNotIn('0', self.portal)
enable_social_media_behavior()
results = api.content.find(object_provides=ISocialMedia.__identifier__)
self.assertEqual(len(results), 0)

# run the upgrade step to validate it
self.request.set('test', True) # avoid transaction commits on tests
self.execute_upgrade_step(step)
results = api.content.find(object_provides=ISocialMedia.__identifier__)
self.assertEqual(len(results), 9) # no failure and catalog updated
1 change: 1 addition & 0 deletions sc/social/like/upgrades/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@
<include package=".v3043" />
<include package=".v3044" />
<include package=".v3045" />
<include package=".v3046" />

</configure>
30 changes: 30 additions & 0 deletions sc/social/like/upgrades/v3046/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# -*- coding:utf-8 -*-
from plone import api
from sc.social.like.logger import logger
from sc.social.like.utils import get_valid_objects

import transaction


def reindex_catalog(setup_tool):
"""Reindex objects to fix interfaces on the catalog."""
test = 'test' in setup_tool.REQUEST # used to ignore transactions on tests
logger.info(
u'Reindexing the catalog. '
u'This process could take a long time on large sites. Be patient.'
)
catalog = api.portal.get_tool('portal_catalog')
results = catalog()
logger.info(u'Found {0} objects'.format(len(results)))
n = 0
for obj in get_valid_objects(results):
catalog.catalog_object(obj, idxs=['object_provides'], update_metadata=False)
n += 1
# XXX: https://github.com/gforcada/flake8-pep3101/issues/16
if n % 1000 == 0 and not test: # noqa: S001
transaction.commit()
logger.info('{0} items processed.'.format(n))

if not test:
transaction.commit()
logger.info('Done.')
18 changes: 18 additions & 0 deletions sc/social/like/upgrades/v3046/configure.zcml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:genericsetup="http://namespaces.zope.org/genericsetup">

<genericsetup:upgradeSteps
source="3045"
destination="3046"
profile="sc.social.like:default">

<genericsetup:upgradeStep
title="Reindex catalog"
description="Reindex objects to fix interfaces on the catalog."
handler=".reindex_catalog"
/>

</genericsetup:upgradeSteps>

</configure>
16 changes: 16 additions & 0 deletions sc/social/like/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from Acquisition import aq_base
from Products.Archetypes.interfaces import IBaseContent
from Products.CMFPlone.utils import safe_hasattr
from sc.social.like.logger import logger
from urlparse import urlparse
from zope.annotation.interfaces import IAnnotations
from zope.globalrequest import getRequest
Expand Down Expand Up @@ -112,3 +113,18 @@ def validate_canonical_domain(value):
raise Invalid(
u'Canonical domain should only include scheme and netloc (e.g. <strong>http://www.example.org</strong>)')
return True


def get_valid_objects(brains):
"""Generate a list of objects associated with valid brains."""
for b in brains:
try:
obj = b.getObject()
except KeyError:
obj = None

if obj is None: # warn on broken entries in the catalog
msg = u'Skipping invalid reference in the catalog: {0}'
logger.warn(msg.format(b.getPath()))
continue
yield obj