Skip to content

Commit

Permalink
Fix Canonical URL updater form
Browse files Browse the repository at this point in the history
A new upgrade step is also provided to update the objects_provides catalog index.
  • Loading branch information
hvelarde committed Jul 20, 2017
1 parent fa1c8c3 commit 3b3a838
Show file tree
Hide file tree
Showing 10 changed files with 183 additions and 53 deletions.
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
90 changes: 52 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(
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,33 @@ 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 site id
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>
4 changes: 3 additions & 1 deletion sc/social/like/subscribers.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ 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 site id
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
33 changes: 22 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,30 @@ 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)

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()

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 +57,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')
35 changes: 35 additions & 0 deletions sc/social/like/tests/test_upgrades.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,3 +285,38 @@ 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))
self.assertEqual(self.total_steps, 1)

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.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>
29 changes: 29 additions & 0 deletions sc/social/like/upgrades/v3046/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# -*- 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
if n % 1000 == 0 and not test:
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

0 comments on commit 3b3a838

Please sign in to comment.