Skip to content
Browse files

Make the RichTextValue immutable. This greatly simplifies the code and

  avoids the need to keep track of the parent object.

svn path=/plone.app.textfield/trunk/; revision=29825
  • Loading branch information...
1 parent 39714e2 commit 0c29ac138afc2639b13c1be2c54cd52846aca827 @optilude optilude committed
View
11 docs/HISTORY.txt
@@ -1,8 +1,15 @@
Changelog
=========
-1.0b1 2009-09-17
-----------------
+1.0b2 -
+------------------
+
+* Make the RichTextValue immutable. This greatly simplifies the code and
+ avoids the need to keep track of the parent object.
+ [optilude]
+
+1.0b1 - 2009-09-17
+------------------
* Initial release
View
3 plone/app/textfield/TODO.txt
@@ -1,7 +1,4 @@
- [ ] Optimise widget.extract() and/or field.set() so that we re-use the same
- blob instead of creating a new one each time?
-
[ ] Test:
- placing a value in an annotation (needs to remain persistent)
View
18 plone/app/textfield/__init__.py
@@ -38,19 +38,13 @@ def __init__(self,
super(RichText, self).__init__(schema=schema, **kw)
def fromUnicode(self, str):
- return RichTextValue(parent=self.context, outputMimeType=self.output_mime_type,
- raw=str, mimeType=self.default_mime_type, encoding=getSiteEncoding())
+ return RichTextValue(
+ raw=str,
+ mimeType=self.default_mime_type,
+ outputMimeType=self.output_mime_type,
+ encoding=getSiteEncoding(),
+ )
def _validate(self, value):
if self.allowed_mime_types and value.mimeType not in self.allowed_mime_types:
raise WrongType(value, self.allowed_mime_types)
-
- def set(self, object, value):
- if not self.readonly:
- if value.readonly:
- value = value.copy(object)
- else:
- value.__parent__ = object
- if value.outputMimeType is None:
- value.outputMimeType = self.output_mime_type
- super(RichText, self).set(object, value)
View
176 plone/app/textfield/field.txt
@@ -48,20 +48,27 @@ Using the value object
----------------------
The value that is stored on a rich text field is a RichTextValue object.
-We can create an empty value to start with.
+This object is immutable and non-persistent.
>>> from plone.app.textfield.value import RichTextValue
- >>> value = RichTextValue()
The text value stores a 'raw' value in a ZODB blob, as well as an 'output'
value that is transformed to a target MIME type. It is possible to access
the raw value directly to transform to a different output type if needed,
of course.
-Before we can set the raw value and test the field, we need to provide a
-transformation engine. Here, we will make a rather simple one. This package
-comes with a default transformer that uses Products.PortalTransforms, which
-comes with Plone.
+If no transformation is available, the output will be None.
+
+ >>> value = RichTextValue(raw=u"Some plain text",
+ ... mimeType='text/plain',
+ ... outputMimeType=field.output_mime_type,
+ ... encoding='utf-8')
+ >>> value.output is None
+ True
+
+To test transformations, we need to provide a transformation engine. Here, we
+will make a rather simple one. This package comes with a default transformer
+that uses Products.PortalTransforms, which comes with Plone.
>>> from plone.app.textfield.interfaces import ITransformer, TransformError
>>> from zope.component import adapts, provideAdapter
@@ -85,57 +92,48 @@ comes with Plone.
...
>>> provideAdapter(TestTransformer)
-At this point, let's set some text onto the value. This would commonly be
-done in a form widget when the object is created.
-
-Note: It is an error to set the 'raw' value before the 'mimeType' has been
-set!
+Let's now access the output again:
- >>> value.mimeType = 'text/plain'
- >>> value.outputMimeType = field.output_mime_type
- >>> value.raw = u"Some plain text"
+ >>> value.output
-> Transforming from text/plain to text/x-uppercase
+ u'SOME PLAIN TEXT'
+
+Notice how the transform was invoked on demand. The value is now cached and
+will not be transformed again:
+
+ >>> value.output
+ u'SOME PLAIN TEXT'
-As you can see, the transform was invoked as soon as the 'raw' value was set.
-We can now obtain the cached value from the output:
+If the engine is available when the field is constructed, the transform will
+take place immediately:
+ >>> value = RichTextValue(raw=u"Some plain text",
+ ... mimeType='text/plain',
+ ... outputMimeType=field.output_mime_type,
+ ... encoding='utf-8')
+ -> Transforming from text/plain to text/x-uppercase
>>> value.output
u'SOME PLAIN TEXT'
+
+It is also possible to provide the transformed value directly. One reason to
+do this may be to create a copy of another value.
+
+ >>> copy = RichTextValue(value.raw, value.mimeType,value.outputMimeType, value.encoding, value.output)
-It is also possible to read the raw value:
+Notice how now transformation took place this time.
+
+It is of course possible to get the raw value:
>>> value.raw
u'Some plain text'
Or to get the value encoded:
- >>> value._encoding
+ >>> value.encoding
'utf-8'
>>> value.raw_encoded
'Some plain text'
-If the value is changed, the output transform is re-applied. We'll also show
-that it supports non-ASCII characters:
-
- >>> value.raw = u'Hello w\xf8rld'
- -> Transforming from text/plain to text/x-uppercase
- >>> value.raw_encoded
- 'Hello w\xc3\xb8rld'
-
- >>> value.raw
- u'Hello w\xf8rld'
-
- >>> value.output
- u'HELLO W\xd8RLD'
-
-Note that if the value is changed in a way that stops the transform from
-working, the output will be None.
-
- >>> value.outputMimeType = 'text/html'
- -> Transforming from text/plain to text/html
- >>> value.output is None
- True
-
Converting a value from unicode
-------------------------------
@@ -204,56 +202,6 @@ MIME types.
>>> default_field.default.mimeType
'text/plain'
-Copying and read-only
----------------------
-
-A value may be set to be readonly. In this case, it is not possible to set a
-new raw string. One example of a readonly value is the default value of a
-field.
-
- >>> default_field.default.readonly
- True
-
- >>> default_field.default.raw = u"New value"
- Traceback (most recent call last):
- ...
- TypeError: Value is readonly. Use copy() first.
-
- >>> default_field.default.mimeType = 'text/foo'
- Traceback (most recent call last):
- ...
- TypeError: Value is readonly. Use copy() first.
-
- >>> default_field.default.outputMimeType = 'text/foo'
- Traceback (most recent call last):
- ...
- TypeError: Value is readonly. Use copy() first.
-
-A field may be copied, in which case the new value will not be readonly.
-
- >>> clone = default_field.default.copy()
- >>> clone._blob is not default_field.default._blob
- True
- >>> clone.raw
- u'Default value'
-
-The copy() method may be passed a new parent object.
-
- >>> content = Content()
- >>> clone = default_field.default.copy(content)
- >>> clone.__parent__ is content
- True
-
-Alternatively, when using set() on the field, the object will be cloned if
-necessary.
-
- >>> content = Content()
- >>> default_field.set(content, default_field.default)
- >>> content.default_field.__parent__ is content
- True
- >>> content.default_field._blob is not default_field.default._blob
- True
-
Persistence
-----------
@@ -268,51 +216,3 @@ and so loading an object with a RichTextValue would mean loading two objects
from the ZODB. For the common use case of storing the body text of a content
object (or indeed, any situation where the RichTextValue is usually loaded
when the object is accessed), this is unnecessary overhead.
-
-Instead, the RichTextValue object keeps track of its parent and sets the
-_p_changed variable on it each time it is modified.
-
-So far, we haven't had a parent object.
-
- >>> value.__parent__ is None
- True
-
-Let's create a new value that does have a parent. One way to do that is to
-use the set() method on the field.
-
- >>> content = Content()
-
-We set up a dummy ZODB data manager so that we can test _p_changed.
-
- >>> class DM:
- ... def __init__(self):
- ... self.called = 0
- ... def register(self, ob):
- ... self.called += 1
- ... def setstate(self, ob):
- ... ob.__setstate__({})
- >>> content._p_jar = DM()
-
- >>> field.set(content, value)
- >>> value.__parent__ is content
- True
-
-Let's now show when _p_changed is modified
-
- >>> content._p_changed = False
- >>> value.raw = u"A raw value"
- -> Transforming from text/plain to text/x-uppercase
- >>> content._p_changed
- True
-
- >>> content._p_changed = False
- >>> value.mimeType = 'text/plain'
- -> Transforming from text/plain to text/x-uppercase
- >>> content._p_changed
- True
-
- >>> content._p_changed = False
- >>> value.outputMimeType = 'text/x-lowercase'
- -> Transforming from text/plain to text/x-lowercase
- >>> content._p_changed
- True
View
53 plone/app/textfield/interfaces.py
@@ -32,45 +32,44 @@ class IRichTextValue(Interface):
- A ZODB blob with the original value
- A cache of the value transformed to the default output type
+
+ The object is immutable.
"""
-
- __parent__ = schema.Object(
- title=u"Content object",
- schema=Interface
+
+ raw = schema.Text(
+ title=u"Raw value in the original MIME type",
+ readonly=True,
)
mimeType = schema.ASCIILine(
- title=u"MIME type"
+ title=u"MIME type",
+ readonly=True,
)
outputMimeType = schema.ASCIILine(
- title=u"Default output MIME type"
+ title=u"Default output MIME type",
+ readonly=True,
)
-
- output = schema.Text(
- title=u"Transformed value in the target MIME type",
- readonly=True
+
+ encoding = schema.ASCIILine(
+ title=u"Default encoding for the value",
+ description=u"Mainly used internally",
+ readonly=True,
)
-
- raw = schema.Text(
- title=u"Raw value in the original MIME type"
+
+ raw_encoded = schema.ASCII(
+ title=u"Get the raw value as an encoded string",
+ description=u"Mainly used internally",
+ readonly=True,
)
- readonly = schema.Bool(
- title=u"Is the value readonly? If so, setting the raw data will raise a TypeError."
+ output = schema.Text(
+ title=u"Transformed value in the target MIME type",
+ description=u"May be None if the transform cannot be completed",
+ readonly=True,
+ required=False,
+ missing_value=None,
)
-
- def modified():
- """Notify the parent (if set) that this object has been modified
- """
-
- def update():
- """Updated the cached output value
- """
-
- def copy(parent=None):
- """Return a copy of this value, with the given parent
- """
class TransformError(Exception):
"""Exception raised if a value could not be transformed. This is normally
View
4 plone/app/textfield/transform.py
@@ -33,9 +33,9 @@ def __call__(self, value, mimeType):
value.raw_encoded,
mimetype=value.mimeType,
object=None, # stop portal_transforms from caching - we have our own cache in the 'output' variable
- encoding=value._encoding)
+ encoding=value.encoding)
output = data.getData()
- return output.decode(value._encoding)
+ return output.decode(value.encoding)
except ConflictError:
raise
except Exception, e:
View
143 plone/app/textfield/value.py
@@ -1,7 +1,5 @@
import logging
-from rwproperty import getproperty, setproperty
-
from zope.interface import implements
from zope.app.component.hooks import getSite
@@ -15,61 +13,54 @@ class RichTextValue(object):
"""The actual value.
Note that this is not a persistent object, to avoid a separate ZODB object
- being loaded. Instead, we store the value and set _p_changed on the
- parent (with the _p_jar), to which we keep a reference.
+ being loaded.
"""
implements(IRichTextValue)
- def __init__(self, parent=None, outputMimeType=None, raw=None,
- mimeType=None, encoding='utf-8', readonly=False):
- self.__parent__ = parent
- self._blob = Blob()
+ def __init__(self, raw=None, mimeType=None, outputMimeType=None, encoding='utf-8', output=None):
+ self._blob = Blob()
+ self._mimeType = mimeType
self._outputMimeType = outputMimeType
- self._set = False
- self._mimeType = mimeType
- self._output = None
- self._encoding = encoding
- self._readonly = readonly
+ self._encoding = encoding
+ self._output = output
- if raw:
- self.raw = raw
+ raw_encoded = raw.encode(self._encoding)
- self.modified()
+ fp = self._blob.open('w')
+ try:
+ fp.write(raw_encoded)
+ self._set = True
+ finally:
+ fp.close()
+
+ if output is None:
+ self._update(raw_encoded)
# output: the cached transformed value. Not stored in a BLOB since it
# is probably used on the main view of the object and should thus be
# loaded with the object
- @getproperty
+ @property
def output(self):
+ if self._output is None:
+ self._update(self.raw_encoded)
return self._output
# the raw value - stored in a BLOB
- @getproperty
+ @property
def raw(self):
value = self.raw_encoded
return value.decode(self._encoding)
- @setproperty
- def raw(self, value):
- assert isinstance(value, unicode)
-
- if self.readonly:
- raise TypeError("Value is readonly. Use copy() first.")
-
- fp = self._blob.open('w')
- try:
- fp.write(value.encode(self._encoding))
- self._set = True
- finally:
- fp.close()
- self.update()
- self.modified()
-
# Encoded raw value
- @getproperty
+
+ @property
+ def encoding(self):
+ return self._encoding
+
+ @property
def raw_encoded(self):
fp = self._blob.open('r')
try:
@@ -79,99 +70,31 @@ def raw_encoded(self):
# the current mime type
- @getproperty
+ @property
def mimeType(self):
return self._mimeType
-
- @setproperty
- def mimeType(self, value):
-
- if self.readonly:
- raise TypeError("Value is readonly. Use copy() first.")
-
- self._mimeType = value
- self.update()
- self.modified()
-
+
# the default mime type
- @getproperty
+ @property
def outputMimeType(self):
return self._outputMimeType
- @setproperty
- def outputMimeType(self, value):
-
- if self.readonly:
- raise TypeError("Value is readonly. Use copy() first.")
-
- self._outputMimeType = value
- self.update()
- self.modified()
-
- # is the object readonly?
- @getproperty
- def readonly(self):
- return self._readonly
-
- @setproperty
- def readonly(self, value):
- self._readonly = value
- self.modified()
-
# cached output
- def update(self):
+ def _update(self, raw_encoded):
"""Update the cache
"""
- if not self._set:
- return
-
- if self.outputMimeType is None:
- return
-
- context = self.__parent__
- if context is None:
- context = getSite()
-
- transformer = ITransformer(context, None)
+ site = getSite()
+ transformer = ITransformer(site, None)
if transformer is None:
return
-
+
try:
self._output = transformer(self, self.outputMimeType)
except TransformError, e:
self._output = None
- self.modified()
-
- def modified(self):
- """Notify the parent that the value has been modified
- """
- if self.__parent__ is not None:
- self.__parent__._p_changed = 1
-
- def copy(self, parent=None, readonly=False):
- """Return a copy of this value, without the given parent.
- """
- newvalue = RichTextValue(parent)
- newvalue._blob = Blob()
- newvalue._outputMimeType = self.outputMimeType
- newvalue._mimeType = self.mimeType
- newvalue._output = self._output
- newvalue._encoding = self._encoding
- newvalue._readonly = readonly
-
- fp = newvalue._blob.open('w')
- try:
- fp.write(self.raw_encoded)
- finally:
- fp.close()
-
- newvalue._set = True
- newvalue.modified()
-
- return newvalue
def __repr__(self):
return u"RichTextValue object. (Did you mean <attribute>.raw or <attribute>.output?)"
View
7 plone/app/textfield/widget.py
@@ -1,4 +1,4 @@
-from Acquisition import ImplicitAcquisitionWrapper, aq_inner
+from Acquisition import ImplicitAcquisitionWrapper
from zope.interface import implementsOnly, implementer
from zope.component import adapts, adapter
@@ -48,10 +48,9 @@ def extract(self, default=NOVALUE):
return default
mimeType = self.request.get("%s.mimeType" % self.name, self.field.default_mime_type)
- return RichTextValue(aq_inner(self.context),
- outputMimeType=self.field.output_mime_type,
- raw=raw,
+ return RichTextValue(raw=raw,
mimeType=mimeType,
+ outputMimeType=self.field.output_mime_type,
encoding=getSiteEncoding())
def allowedMimeTypes(self):
View
1 setup.py
@@ -29,7 +29,6 @@
'zope.interface',
'zope.component',
'ZODB3 >= 3.8.1',
- 'rwproperty',
],
tests_require=['collective.testcaselayer'],
extras_require={

0 comments on commit 0c29ac1

Please sign in to comment.
Something went wrong with that request. Please try again.