plone/plone.dexterity

Content type schema has no fields based on FTI ._p_mtime

Opened this issue · 0 comments

So, this was a fun bug to debug (NOT)

The model used to define fields for a content type is a generated schema, which uses the FTI ._p_mtime to generate its unique name. In addition to this, there is a string replacement that replaces . with _2_ and then joins parts using _0_.
The _p_mtime is a timestamp, and when you don't have milliseconds in your datetime object, it will always end in .0

So, this works fine

>>> from plone.dexterity.schema import SchemaNameEncoder
>>> from datetime import datetime
>>> enc = SchemaNameEncoder()
>>> now = datetime.now()
>>> now.timestamp()
1650381570.771721
>>> enc.join("Plone", "1650381570.771721", "Image")
'Plone_0_1650381570_2_771721_0_Image'
>>> enc.split('Plone_0_1650381570_2_771721_0_Image')
['Plone', '1650381570.771721', 'Image']

This doesn't

>>> from plone.dexterity.schema import SchemaNameEncoder
>>> from datetime import datetime
>>> enc = SchemaNameEncoder()
>>> now = datetime(2022,4,19,12,21,30)
>>> now.timestamp()
1650388890.0
>>> enc.join("Plone", "1650388890.0", "Image")
'Plone_0_1650388890_2_0_0_Image'
>>> enc.split('Plone_0_1650388890_2_0_0_Image')
['Plone', '1650388890_2', '0_Image']

I cannot figure out why, or under which circumstances, my Image FTI ends up with a timestamp from a datetime with no milliseconds

>>> fti = queryUtility(IDexterityFTI, name="Image")
>>> fti._p_mtime
1650326400.0

And the way this manifests, is that suddenly your Images have no image field

>>> api.content.create(container=site, type="Image", id='my-image')
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/opt/plone/eggs/decorator-5.1.1-py3.8.egg/decorator.py", line 232, in fun
    return caller(func, *(extras + args), **kw)
  File "/vagrant/src/plone.api/src/plone/api/validation.py", line 75, in wrapped
    return function(*args, **kwargs)
  File "/opt/plone/eggs/decorator-5.1.1-py3.8.egg/decorator.py", line 232, in fun
    return caller(func, *(extras + args), **kw)
  File "/vagrant/src/plone.api/src/plone/api/validation.py", line 147, in wrapped
    return function(*args, **kwargs)
  File "/vagrant/src/plone.api/src/plone/api/content.py", line 71, in create
    container.invokeFactory(type, content_id, **kwargs)
  File "/opt/plone/eggs/plone.dexterity-3.0.0a2-py3.8.egg/plone/dexterity/content.py", line 830, in invokeFactory
    return super(Container, self).invokeFactory(
  File "/opt/plone/eggs/Products.CMFCore-2.5.4-py3.8.egg/Products/CMFCore/PortalFolder.py", line 299, in invokeFactory
    return ttool.constructContent(type_name, self, id, RESPONSE,
  File "/opt/plone/eggs/Products.CMFCore-2.5.4-py3.8.egg/Products/CMFCore/TypesTool.py", line 809, in constructContent
    ob = info.constructInstance(container, id, *args, **kw)
  File "/opt/plone/eggs/Products.CMFCore-2.5.4-py3.8.egg/Products/CMFCore/TypesTool.py", line 308, in constructInstance
    return self._constructInstance(container, id, *args, **kw)
  File "/opt/plone/eggs/Products.CMFCore-2.5.4-py3.8.egg/Products/CMFCore/TypesTool.py", line 569, in _constructInstance
    notify(ObjectCreatedEvent(obj))
  File "/opt/plone/eggs/zope.event-4.5.0-py3.8.egg/zope/event/__init__.py", line 32, in notify
    subscriber(event)
  File "/opt/plone/eggs/zope.component-5.0.1-py3.8.egg/zope/component/event.py", line 27, in dispatch
    component_subscribers(event, None)
  File "/opt/plone/eggs/zope.component-5.0.1-py3.8.egg/zope/component/_api.py", line 134, in subscribers
    return sitemanager.subscribers(objects, interface)
  File "/opt/plone/eggs/zope.interface-5.4.0-py3.8-linux-x86_64.egg/zope/interface/registry.py", line 448, in subscribers
    return self.adapters.subscribers(objects, provided)
  File "/opt/plone/eggs/zope.interface-5.4.0-py3.8-linux-x86_64.egg/zope/interface/adapter.py", line 899, in subscribers
    subscription(*objects)
  File "/opt/plone/eggs/zope.component-5.0.1-py3.8.egg/zope/component/event.py", line 36, in objectEventNotify
    component_subscribers((event.object, event), None)
  File "/opt/plone/eggs/zope.component-5.0.1-py3.8.egg/zope/component/_api.py", line 134, in subscribers
    return sitemanager.subscribers(objects, interface)
  File "/opt/plone/eggs/zope.interface-5.4.0-py3.8-linux-x86_64.egg/zope/interface/registry.py", line 448, in subscribers
    return self.adapters.subscribers(objects, provided)
  File "/opt/plone/eggs/zope.interface-5.4.0-py3.8-linux-x86_64.egg/zope/interface/adapter.py", line 899, in subscribers
    subscription(*objects)
  File "/vagrant/src/plone.app.contenttypes/plone/app/contenttypes/subscribers.py", line 14, in set_title_description
    datafield = obj.image
  File "/opt/plone/eggs/plone.dexterity-3.0.0a2-py3.8.egg/plone/dexterity/content.py", line 407, in __getattr__
    raise AttributeError(name)
AttributeError: image

When I modify the FTI, the problem is solved

>>> setattr(fti, 'dummy', 1)
>>> transaction.commit()
>>> fti._p_mtime
1650382489.4636827
>>> api.content.create(container=site, type="Image", id='my-image')
<Image at /Plone/my-image>

In order to fix this, I believe it should be enough to simply return the int() part of the timestamp in

def get_suffix(fti):
mtime = getattr(fti, "_p_mtime", None)
# Python 2 rounds floats when we use the str function on them.
# Python 2:
# >>> str(1637689348.9999528)
# '1637689349.0'
# Python 3:
# >>> str(1637689348.9999528)
# '1637689348.9999528'
# This was causing the schema names in Python 2 to take an unexpected format,
# causing errors.
# So, we need to use the repr function, which doesn't round floats.
if mtime:
return repr(mtime)
return ""