Commit 2e40d241 authored by Jérome Perrin's avatar Jérome Perrin

Fix sessions using newTempBase or collections

Fixes after nexedi/erp5!1451 

With the RAM implementation it was possible to use in session:
 - list or dict of temp documents
 - temp documents created with `newTempBase` ( not just `newContent(temp_object=True, ...)` )

This repairs these two use cases.

See merge request nexedi/erp5!1533
parents 30d115c8 86951172
Pipeline #19350 failed with stage
in 0 seconds
...@@ -82,6 +82,14 @@ class SessionToolTestCase(ERP5TypeTestCase): ...@@ -82,6 +82,14 @@ class SessionToolTestCase(ERP5TypeTestCase):
session = self.portal.portal_sessions[self.session_id] session = self.portal.portal_sessions[self.session_id]
self.assertEqual(primitives_kw, session) self.assertEqual(primitives_kw, session)
def test_store_temp_base(self):
portal_sessions = self.portal.portal_sessions
from Products.ERP5Type.Document import newTempBase
session = portal_sessions.newContent(
self.session_id,
temp_base=newTempBase(self.portal, 'temp_base', title='Temp Base'))
self.assertEqual(session['temp_base'].getTitle(), 'Temp Base')
def test_store_temp_object(self): def test_store_temp_object(self):
portal_sessions = self.portal.portal_sessions portal_sessions = self.portal.portal_sessions
session = portal_sessions.newContent( session = portal_sessions.newContent(
...@@ -96,7 +104,7 @@ class SessionToolTestCase(ERP5TypeTestCase): ...@@ -96,7 +104,7 @@ class SessionToolTestCase(ERP5TypeTestCase):
self.assertEqual(str(i), attr.getId()) self.assertEqual(str(i), attr.getId())
self.assertEqual(0, len(attr.objectIds())) self.assertEqual(0, len(attr.objectIds()))
def test_store_recursive_temp_object(self): def test_store_temp_object_with_sub_object(self):
doc = self.portal.newContent( doc = self.portal.newContent(
temp_object=True, portal_type='Document', id='doc', title='Doc') temp_object=True, portal_type='Document', id='doc', title='Doc')
doc.newContent( doc.newContent(
...@@ -108,6 +116,35 @@ class SessionToolTestCase(ERP5TypeTestCase): ...@@ -108,6 +116,35 @@ class SessionToolTestCase(ERP5TypeTestCase):
self.assertEqual(doc.sub_doc.getTitle(), 'Sub doc') self.assertEqual(doc.sub_doc.getTitle(), 'Sub doc')
self.assertEqual(len(doc.contentValues()), 1) self.assertEqual(len(doc.contentValues()), 1)
def test_store_temp_object_in_list(self):
doc = self.portal.newContent(
temp_object=True, portal_type='Document', id='doc', title='Doc')
self.portal.portal_sessions.newContent(self.session_id, doc_list=[doc])
self.commit()
doc, = self.portal.portal_sessions[self.session_id]['doc_list']
self.assertEqual(doc.getTitle(), 'Doc')
def test_store_temp_object_in_dict(self):
doc = self.portal.newContent(
temp_object=True, portal_type='Document', id='doc', title='Doc')
self.portal.portal_sessions.newContent(self.session_id, doc_dict={'doc': doc})
self.commit()
doc = self.portal.portal_sessions[self.session_id]['doc_dict']['doc']
self.assertEqual(doc.getTitle(), 'Doc')
def test_store_temp_object_in_nested_container(self):
doc = self.portal.newContent(
temp_object=True, portal_type='Document', id='doc', title='Doc')
self.portal.portal_sessions.newContent(
self.session_id, data={'doc_set_list': [set([doc])]})
self.commit()
doc_set_list = self.portal.portal_sessions[self.session_id]['data']['doc_set_list']
self.assertIsInstance(doc_set_list, list)
doc_set, = doc_set_list
self.assertIsInstance(doc_set, set)
doc, = list(doc_set)
self.assertEqual(doc.getTitle(), 'Doc')
def test_modify_session(self): def test_modify_session(self):
""" Modify session and check that modifications are updated in storage backend.""" """ Modify session and check that modifications are updated in storage backend."""
portal_sessions = self.portal.portal_sessions portal_sessions = self.portal.portal_sessions
......
...@@ -32,6 +32,7 @@ from Products.ERP5Type.Tool.BaseTool import BaseTool ...@@ -32,6 +32,7 @@ from Products.ERP5Type.Tool.BaseTool import BaseTool
from Products.ERP5Type import Permissions from Products.ERP5Type import Permissions
from Acquisition import aq_base from Acquisition import aq_base
from UserDict import UserDict from UserDict import UserDict
import collections
# the ERP5 cache factory used as a storage # the ERP5 cache factory used as a storage
SESSION_CACHE_FACTORY = 'erp5_session_cache' SESSION_CACHE_FACTORY = 'erp5_session_cache'
...@@ -45,6 +46,34 @@ _marker=[] ...@@ -45,6 +46,34 @@ _marker=[]
# global storage plugin # global storage plugin
storage_plugin = None storage_plugin = None
def remove_acquisition_wrapper(obj):
if isinstance(obj, basestring):
return obj
obj = aq_base(obj)
if isinstance(obj, collections.Mapping):
return obj.__class__({
remove_acquisition_wrapper(k): remove_acquisition_wrapper(v)
for k, v in obj.items()})
if isinstance(obj, (collections.Sequence, collections.Set)):
return obj.__class__([remove_acquisition_wrapper(o) for o in obj])
return obj
def restore_acquisition_wrapper(obj, context):
if isinstance(obj, basestring):
return obj
if hasattr(obj, '__of__'):
obj = obj.__of__(context)
if isinstance(obj, collections.Mapping):
return obj.__class__({
restore_acquisition_wrapper(k, context): restore_acquisition_wrapper(v, context)
for k, v in obj.items()})
if isinstance(obj, (collections.Sequence, collections.Set)):
return obj.__class__([restore_acquisition_wrapper(o, context) for o in obj])
return obj
class Session(UserDict): class Session(UserDict):
""" Session acts as a plain python dictionary stored in respecitve Cache Factory/Cache Plugin. """ Session acts as a plain python dictionary stored in respecitve Cache Factory/Cache Plugin.
Depending on cache plugin used as a storage some restrictions may apply. Depending on cache plugin used as a storage some restrictions may apply.
...@@ -80,11 +109,8 @@ class Session(UserDict): ...@@ -80,11 +109,8 @@ class Session(UserDict):
def __getitem__(self, key): def __getitem__(self, key):
if key in self.data: if key in self.data:
value = self.data[key]
if hasattr(value, '__of__'):
# returned it wrapped in aquisition context # returned it wrapped in aquisition context
value = value.__of__(self._aq_context) return restore_acquisition_wrapper(self.data[key], self._aq_context)
return value
raise KeyError(key) raise KeyError(key)
def _updateSessionDuration(self, session_duration): def _updateSessionDuration(self, session_duration):
...@@ -103,7 +129,7 @@ class Session(UserDict): ...@@ -103,7 +129,7 @@ class Session(UserDict):
def __setitem__(self, key, item): def __setitem__(self, key, item):
# save value without its acquisition context # save value without its acquisition context
UserDict.__setitem__(self, key, aq_base(item)) UserDict.__setitem__(self, key, remove_acquisition_wrapper(item))
def update(self, dict=None, **kwargs): # pylint: disable=redefined-builtin def update(self, dict=None, **kwargs): # pylint: disable=redefined-builtin
for k, v in (dict or kwargs).iteritems(): for k, v in (dict or kwargs).iteritems():
......
...@@ -70,6 +70,7 @@ from Products.ERP5Type.Accessor.Constant import PropertyGetter as ConstantGetter ...@@ -70,6 +70,7 @@ from Products.ERP5Type.Accessor.Constant import PropertyGetter as ConstantGetter
from Products.ERP5Type.Accessor.TypeDefinition import list_types from Products.ERP5Type.Accessor.TypeDefinition import list_types
from Products.ERP5Type.Accessor import Base as BaseAccessor from Products.ERP5Type.Accessor import Base as BaseAccessor
from Products.ERP5Type.mixin.property_translatable import PropertyTranslatableBuiltInDictMixIn from Products.ERP5Type.mixin.property_translatable import PropertyTranslatableBuiltInDictMixIn
from Products.ERP5Type.mixin.temporary import TemporaryDocumentMixin
from Products.ERP5Type.XMLExportImport import Base_asXML from Products.ERP5Type.XMLExportImport import Base_asXML
from Products.ERP5Type.Cache import CachingMethod, clearCache, getReadOnlyTransactionCache from Products.ERP5Type.Cache import CachingMethod, clearCache, getReadOnlyTransactionCache
from .Accessor import WorkflowState from .Accessor import WorkflowState
...@@ -3626,51 +3627,16 @@ def removeIContentishInterface(cls): ...@@ -3626,51 +3627,16 @@ def removeIContentishInterface(cls):
removeIContentishInterface(Base) removeIContentishInterface(Base)
class TempBase(Base): class TempBase(TemporaryDocumentMixin, Base):
"""A version of Base that does not persist in ZODB. """A version of Base that does not persist in ZODB.
This class only has the Base methods, so most of the times it is This class only has the Base methods, so most of the times it is
preferable to use a temporary portal type instead. preferable to use a temporary portal type instead.
""" """
isIndexable = ConstantGetter('isIndexable', value=False)
isTempDocument = ConstantGetter('isTempDocument', value=True)
# Declarative security # Declarative security
security = ClassSecurityInfo() security = ClassSecurityInfo()
def reindexObject(self, *args, **kw):
pass
def recursiveReindexObject(self, *args, **kw):
pass
def activate(self, *args, **kw):
return self
def setUid(self, value):
self.uid = value # Required for Listbox so that no casting happens when we use TempBase to create new objects
def setTitle(self, value):
"""
Required so that getProperty('title') will work on tempBase objects
The dynamic acquisition work very well for a lot of properties, but
not for title. For example, if we do setProperty('organisation_url'), then
even if organisation_url is not in a propertySheet, the method getOrganisationUrl
will be generated. But this does not work for title, because I(seb)'m almost sure
there is somewhere a method '_setTitle' or 'setTitle' with no method getTitle on Base.
That why setProperty('title') and getProperty('title') does not work.
"""
self.title = value
def getTitle(self):
"""
Returns the title of this document
"""
return getattr(aq_base(self), 'title', None)
security.declarePublic('setProperty') security.declarePublic('setProperty')
security.declarePublic('getProperty') security.declarePublic('getProperty')
security.declarePublic('edit') security.declarePublic('edit')
# Persistence.Persistent is one of the superclasses of TempBase, and on Zope2.8 # Persistence.Persistent is one of the superclasses of TempBase, and on Zope2.8
......
...@@ -41,7 +41,7 @@ from OFS.Folder import Folder as OFS_Folder ...@@ -41,7 +41,7 @@ from OFS.Folder import Folder as OFS_Folder
from persistent import Persistent, wref from persistent import Persistent, wref
from ZODB.serialize import ObjectWriter, ObjectReader from ZODB.serialize import ObjectWriter, ObjectReader
from Products.ERP5Type import Permissions from Products.ERP5Type import Permissions
from Products.ERP5Type.Base import Base, WorkflowMethod from Products.ERP5Type.Base import Base, TempBase, WorkflowMethod
log = logging.getLogger('ERP5Type') log = logging.getLogger('ERP5Type')
log.trace = lambda *args, **kw: log.log(5, *args, **kw) log.trace = lambda *args, **kw: log.log(5, *args, **kw)
...@@ -165,6 +165,8 @@ if 1: ...@@ -165,6 +165,8 @@ if 1:
if klass.__module__ in ('erp5.portal_type', 'erp5.temp_portal_type'): if klass.__module__ in ('erp5.portal_type', 'erp5.temp_portal_type'):
return Base__setstate__(self, value) return Base__setstate__(self, value)
if klass is TempBase:
return Base__setstate__(self, value)
try: try:
portal_type = value.get('portal_type') or klass.portal_type portal_type = value.get('portal_type') or klass.portal_type
except AttributeError: except AttributeError:
......
...@@ -40,7 +40,7 @@ class TemporaryDocumentMixin(object): ...@@ -40,7 +40,7 @@ class TemporaryDocumentMixin(object):
""" """
Setters and attributes that are attached to temporary documents. Setters and attributes that are attached to temporary documents.
""" """
isIndexable = 0 isIndexable = PropertyConstantGetter('isIndexable', value=False)
isTempDocument = PropertyConstantGetter('isTempDocument', value=True) isTempDocument = PropertyConstantGetter('isTempDocument', value=True)
__roles__ = None __roles__ = None
...@@ -85,7 +85,9 @@ class TemporaryDocumentMixin(object): ...@@ -85,7 +85,9 @@ class TemporaryDocumentMixin(object):
self.title = value self.title = value
def getTitle(self): def getTitle(self):
return getattr(self,'title',None) """Returns the title of this document
"""
return getattr(aq_base(self), 'title', None)
def edit(self, *args, **kw): def edit(self, *args, **kw):
if getattr(self, "_original", None) is None: if getattr(self, "_original", None) is None:
......
...@@ -236,7 +236,7 @@ class TestERP5Type(ERP5TypeTestCase, LogInterceptor): ...@@ -236,7 +236,7 @@ class TestERP5Type(ERP5TypeTestCase, LogInterceptor):
'newTemp*(self, ID) will be removed, use self.newContent(temp_object=True, id=ID, portal_type=...)', 'newTemp*(self, ID) will be removed, use self.newContent(temp_object=True, id=ID, portal_type=...)',
DeprecationWarning, 2) DeprecationWarning, 2)
def test_03_NewTempObject(self): def test_newTempBase(self):
# Products.ERP5Type.Document.newTempBase is another (not recommended) way # Products.ERP5Type.Document.newTempBase is another (not recommended) way
# of creating temp objects # of creating temp objects
import Products.ERP5Type.Document import Products.ERP5Type.Document
...@@ -246,19 +246,33 @@ class TestERP5Type(ERP5TypeTestCase, LogInterceptor): ...@@ -246,19 +246,33 @@ class TestERP5Type(ERP5TypeTestCase, LogInterceptor):
self.assertTrue(o.isTempObject()) self.assertTrue(o.isTempObject())
self.assertTrue(guarded_import("Products.ERP5Type.Document", fromlist=["newTempBase"])) self.assertTrue(guarded_import("Products.ERP5Type.Document", fromlist=["newTempBase"]))
def test_TempObjectPersistent(self): def _test_temp_object_persistent(self, temp_object):
# Temp objects can not be stored in ZODB # Temp objects can not be stored in ZODB
temp_object = self.portal.person_module.newContent(portal_type='Person', temp_object=True)
self.assertTrue(temp_object.isTempObject()) self.assertTrue(temp_object.isTempObject())
# they can be pickled # they can be pickled
self.assertTrue(pickle.dumps(aq_base(temp_object))) self.assertTrue(pickle.dumps(aq_base(temp_object)))
# they can be unpickled
import ZODB.broken
self.assertNotIsInstance(
pickle.loads(pickle.dumps(aq_base(temp_object))),
ZODB.broken.Broken,
)
# but they can not be saved in ZODB accidentally # but they can not be saved in ZODB accidentally
self.portal.person_module.oops = temp_object self.portal.person_module.oops = temp_object
self.assertRaisesRegexp(Exception, "Temporary objects can't be pickled", self.commit) self.assertRaisesRegexp(Exception, "Temporary objects can't be pickled", self.commit)
self.abort() self.abort()
def test_temp_object_persistent(self):
temp_object = self.portal.person_module.newContent(portal_type='Person', temp_object=True)
self._test_temp_object_persistent(temp_object)
def test_newTempBase_persistent(self):
import Products.ERP5Type.Document
temp_object = Products.ERP5Type.Document.newTempBase(self.portal, 'id')
self._test_temp_object_persistent(temp_object)
def test_warnings_redirected_to_event_log(self): def test_warnings_redirected_to_event_log(self):
self._catch_log_errors() self._catch_log_errors()
self.addCleanup(self._ignore_log_errors) self.addCleanup(self._ignore_log_errors)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment