Commits

Carl Meyer  committed fbf20c4 Merge

integrated MarkupField (thanks Mike Korobov)

  • Participants
  • Parent commits d62355a, 25f1eb1

Comments (0)

Files changed (12)

 ^dist/
 ^django_markitup.egg-info/
+^build/
+.pyc$
+
+^.project$
+^.pydevproject$
 Carl Meyer <carl@dirtcircle.com>
+
+Original MarkupField implementation by James Turk
+<james.p.turk@gmail.com> with design help and minor fixes from Jeremy
+Carbaugh. Merged into django-markitup by Mike Korobov
+<kmike84@gmail.com>.

File CHANGELOG.txt

 django-markitup changelog
 =========================
 
+2009-11-06 Carl Meyer <carl@dirtcircle.com> and Mike Korobov <kmike84@gmail.com>
+
+* Added ``MarkupField`` from http://github.com/carljm/django-markupfield
+
 2009-11-05 Carl Meyer <carl@dirtcircle.com>
 
 * Added test suite.
 Easy integration of the MarkItUp_ markup editor widget (by Jay Salvat) in
 Django projects. Includes server-side support for MarkItUp!'s AJAX preview.
 
+Also includes ``MarkupField``, a ``TextField`` that automatically
+renders and stores both its raw and rendered values in the database,
+on the assumption that disk space is cheaper than CPU cycles in a web
+application.
 
 .. _MarkItUp: http://markitup.jaysalvat.com/
 
        webserver configuration.
 
     3. If you want to use AJAX-based preview:
-       
+
         - Add ``url(r'^markitup/', include('markitup.urls')`` in your
           root URLconf.
-        - Set the MARKITUP_PREVIEW_FILTER setting (see `Using AJAX preview`_ 
+        - Set the MARKUP_FILTER setting (see `Using AJAX preview`_
           below).
 
 Using the MarkItUp! widget
 media somewhere on the page using ``{{ form.media }}``, or the
 MarkItUpWidget will have no effect.
 
-To use MarkItUpWidget in the Django admin::
+To use widget in the Django admin::
 
-    from markitup.widgets import MarkItUpWidget
-    
+    from markitup.widgets import AdminMarkItUpWidget
+
     class MyModelAdmin(admin.ModelAdmin):
     ...
     def formfield_for_dbfield(self, db_field, **kwargs):
         if db_field.name == 'content':
-            kwargs['widget'] = MarkItUpWidget(attrs={'class': 'vLargeTextField'})
+            kwargs['widget'] = AdminMarkItUpWidget()
         return super(MyModelAdmin, self).formfield_for_dbfield(db_field, **kwargs)
 
 You can also use the formfield_overrides attribute of the ModelAdmin, which
 possible to use the MarkItUpWidget on one TextField in a model and not
 another)::
 
-    from markitup.widgets import MarkItUpWidget
+    from markitup.widgets import AdminMarkItUpWidget
     
     class MyModelAdmin(admin.ModelAdmin):
-        formfield_overrides = {models.TextField: {'widget': MarkItUpWidget}}
+        formfield_overrides = {models.TextField: {'widget': AdminMarkItUpWidget}}
 
-Using MarkItUp! via templates
-=============================
+**Note:** If you use ``MarkupField`` in your model (see below), it is
+  rendered in the admin with an ``AdminMarkItUpWidget`` by default.
+
+Using MarkItUp! via templatetags
+================================
 
 In some cases it may be inconvenient to use ``MarkItUpWidget`` (for
 instance, if the form in question is defined in third-party code). For
 ``form.fieldname.auto_id``::
 
     {{ form.fieldname }}
-    
+
     {% markitup_editor form.fieldname.auto_id %}
 
 You can use ``markitup_editor`` on as many different textareas as you
 override these templates in your project and customize them however
 you wish.
 
+MarkupField
+===========
+
+You can apply the MarkItUp! editor control to any textarea using the
+above techniques, and handle the markup on the server side however you
+prefer. 
+
+For a seamless markup-handling solution, django-markitup also provides
+a ``MarkupField`` model field that automatically renders and stores
+both its raw and rendered values in the database, using the value of
+the ``MARKUP_FILTER`` setting to parse the markup into HTML.
+
+A ``MarkupField`` is easy to add to any model definition::
+
+    from django.db import models
+    from markitup.fields import MarkupField
+
+    class Article(models.Model):
+        title = models.CharField(max_length=100)
+        body = MarkupField()
+
+``MarkupField`` automatically creates an extra non-editable field
+``_body_rendered`` to store the rendered markup. This field doesn't
+need to be accessed directly; see below.
+
+Accessing a MarkupField on a model
+----------------------------------
+
+When accessing an attribute of a model that was declared as a
+``MarkupField``, a ``Markup`` object is returned.  The ``Markup``
+object has two attributes:
+
+``raw``:
+    The unrendered markup.
+``rendered``:
+    The rendered HTML version of ``raw`` (read-only).
+
+This object also has a ``__unicode__`` method that calls
+``django.utils.safestring.mark_safe`` on ``rendered``, allowing
+``MarkupField`` attributes to appear in templates as rendered HTML
+without any special template tag or having to access ``rendered``
+directly.
+
+Assuming the ``Article`` model above::
+
+    >>> a = Article.objects.all()[0]
+    >>> a.body.raw
+    u'*fancy*'
+    >>> a.body.rendered
+    u'<p><em>fancy</em></p>'
+    >>> print unicode(a.body)
+    <p><em>fancy</em></p>
+
+Assignment to ``a.body`` is equivalent to assignment to
+``a.body.raw``.
+
+.. note::
+    a.body.rendered is only updated when a.save() is called
+
+Editing a MarkupField in a form
+-------------------------------
+
+When editing a ``MarkupField`` model attribute in a ``ModelForm``
+(i.e. in the Django admin), you'll generally want to edit the original
+markup and not the rendered HTML.  Because the ``Markup`` object
+returns rendered HTML from its __unicode__ method, it's necessary to
+use the ``MarkupTextarea`` widget from the ``markupfield.widgets``
+module, which knows to return the raw markup instead.
+
+By default, a ``MarkupField`` uses the MarkItUp! editor control in the
+admin (via the provided ``AdminMarkItUpWidget``), but a plain
+``MarkupTextarea`` in other forms. If you wish to use the MarkItUp!
+editor with this ``MarkupField`` in your own form, you'll need to use
+the provided ``MarkItUpWidget`` rather than ``MarkupTextarea``.
+
+If you apply your own custom widget to the form field representing a
+``MarkupField``, your widget must either inherit from
+``MarkupTextarea`` or its ``render`` method must convert its ``value``
+argument to ``value.raw``.
+
+
 Choosing a MarkItUp! button set and skin
 ========================================
 
 
 If you've included ``markitup.urls`` in your root URLconf (as
 demonstrated above under `Installation`_), all you need to enable
-server-side AJAX preview is the ``MARKITUP_PREVIEW_FILTER`` setting.
+server-side AJAX preview is the ``MARKUP_FILTER`` setting.
 
-``MARKITUP_PREVIEW_FILTER`` must be a two-tuple.  
+``MARKUP_FILTER`` must be a two-tuple.
 
 The first element must be a string, the Python dotted path to a markup
 filter function.  This function should accept markup as its first
 For example, if you have python-markdown installed, you could use it
 like this::
 
-    MARKITUP_PREVIEW_FILTER = ('markdown.markdown', {'safe_mode': True})
+    MARKUP_FILTER = ('markdown.markdown', {'safe_mode': True})
 
 Alternatively, you could use the "textile" filter provided by Django
 like this::
 
-    MARKITUP_PREVIEW_FILTER = ('django.contrib.markup.templatetags.markup.textile', {})
+    MARKUP_FILTER = ('django.contrib.markup.templatetags.markup.textile', {})
 
 (The textile filter function doesn't accept keyword arguments, so the
 kwargs dictionary must be empty in this case.)
 
+``django-markitup`` provides one sample rendering function,
+``render_rest`` in the ``markitup.renderers`` module.
+
 The rendered HTML content is displayed in the Ajax preview wrapped by
 an HTML page generated by the ``markitup/preview.html`` template; you
 can override this template in your project and customize the preview
 sure the contents of the ``markitup/media/markitup`` directory are
 available at ``MARKITUP_MEDIA_URL/markitup/``.
 
+MARKITUP_PREVIEW_FILTER
+-----------------------
+
+This optional setting can be used to override the markup filter used
+for the Ajax preview view. It has the same format as
+``MARKUP_FILTER``; by default it is set equal to ``MARKUP_FILTER``.
+
 JQUERY_URL
 ----------
 

File markitup/fields.py

+from django.conf import settings
+from django.db import models
+from django.utils.safestring import mark_safe
+from django.utils.functional import curry
+from django.core.exceptions import ImproperlyConfigured
+from markitup import widgets
+
+_rendered_field_name = lambda name: '_%s_rendered' % name
+
+def _get_render_func(dotted_path, **kwargs):
+    (module, func) = dotted_path.rsplit('.', 1)
+    func = getattr(__import__(module, {}, {}, [func]), func)
+    return curry(func, **kwargs)
+
+try:
+    render_func = _get_render_func(settings.MARKUP_FILTER[0],
+                                   **settings.MARKUP_FILTER[1])
+except ImportError, e:
+    raise ImproperlyConfigured("Could not import MARKUP_FILTER %s: %s" %
+                               (settings.MARKUP_FILTER, e))
+except AttributeError, e:
+    raise ImproperlyConfigured("MARKUP_FILTER setting is required")
+
+class Markup(object):
+    def __init__(self, instance, field_name, rendered_field_name):
+        # instead of storing actual values store a reference to the instance
+        # along with field names, this makes assignment possible
+        self.instance = instance
+        self.field_name = field_name
+        self.rendered_field_name = rendered_field_name
+
+    # raw is read/write
+    def _get_raw(self):
+        return self.instance.__dict__[self.field_name]
+    def _set_raw(self, val):
+        setattr(self.instance, self.field_name, val)
+    raw = property(_get_raw, _set_raw)
+
+    # rendered is a read only property
+    def _get_rendered(self):
+        return getattr(self.instance, self.rendered_field_name)
+    rendered = property(_get_rendered)
+
+    # allows display via templates to work without safe filter
+    def __unicode__(self):
+        return mark_safe(self.rendered)
+
+class MarkupDescriptor(object):
+    def __init__(self, field):
+        self.field = field
+        self.rendered_field_name = _rendered_field_name(self.field.name)
+
+    def __get__(self, instance, owner):
+        if instance is None:
+            raise AttributeError('Can only be accessed via an instance.')
+        markup = instance.__dict__[self.field.name]
+        if markup is None:
+            return None
+        return Markup(instance, self.field.name, self.rendered_field_name)
+
+    def __set__(self, obj, value):
+        if isinstance(value, Markup):
+            obj.__dict__[self.field.name] = value.raw
+            setattr(obj, self.rendered_field_name, value.rendered)
+        else:
+            obj.__dict__[self.field.name] = value
+
+class MarkupField(models.TextField):
+    def contribute_to_class(self, cls, name):
+        rendered_field = models.TextField(editable=False)
+        rendered_field.creation_counter = self.creation_counter+1
+        cls.add_to_class(_rendered_field_name(name), rendered_field)
+        super(MarkupField, self).contribute_to_class(cls, name)
+        setattr(cls, self.name, MarkupDescriptor(self))
+
+    def pre_save(self, model_instance, add):
+        value = super(MarkupField, self).pre_save(model_instance, add)
+        rendered = render_func(value.raw)
+        setattr(model_instance, _rendered_field_name(self.attname), rendered)
+        return value.raw
+
+    def value_to_string(self, obj):
+        value = self._get_val_from_obj(obj)
+        return value.raw
+
+    def get_db_prep_value(self, value):
+        try:
+            return value.raw
+        except AttributeError:
+            return value
+
+    def formfield(self, **kwargs):
+        defaults = {'widget': widgets.MarkupTextarea}
+        defaults.update(kwargs)
+        return super(MarkupField, self).formfield(**defaults)
+
+# register MarkupField to use the custom widget in the Admin
+from django.contrib.admin.options import FORMFIELD_FOR_DBFIELD_DEFAULTS
+FORMFIELD_FOR_DBFIELD_DEFAULTS[MarkupField] = {'widget': widgets.AdminMarkItUpWidget}

File markitup/renderers.py

+try:
+    from docutils.core import publish_parts
+    def render_rest(markup, **docutils_settings):
+        parts = publish_parts(source=markup, writer_name="html4css1", settings_overrides=docutils_settings)
+        return parts["fragment"]
+except ImportError:
+    pass
+

File markitup/settings.py

 import posixpath
 
 
-MARKITUP_PREVIEW_FILTER = getattr(settings, 'MARKITUP_PREVIEW_FILTER', None)
+MARKITUP_PREVIEW_FILTER = getattr(settings, 'MARKITUP_PREVIEW_FILTER',
+                                  getattr(settings, 'MARKUP_FILTER', None))
 MARKITUP_MEDIA_URL = getattr(settings, 'MARKITUP_MEDIA_URL', settings.MEDIA_URL)
-
-MARKITUP_SET = getattr(settings, 'MARKITUP_SET',
-                       'markitup/sets/default')
-MARKITUP_SKIN = getattr(settings, 'MARKITUP_SKIN',
-                        'markitup/skins/simple')
+MARKITUP_SET = getattr(settings, 'MARKITUP_SET', 'markitup/sets/default')
+MARKITUP_SKIN = getattr(settings, 'MARKITUP_SKIN', 'markitup/skins/simple')
 JQUERY_URL = getattr(
     settings, 'JQUERY_URL',
     'http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js')

File markitup/widgets.py

 """
 widgets for django-markitup
 
-Time-stamp: <2009-11-06 00:23:41 carljm widgets.py>
+Time-stamp: <2009-11-06 14:28:29 carljm widgets.py>
 
 """
 from django import forms
 from django.utils.safestring import mark_safe
+from django.contrib.admin.widgets import AdminTextareaWidget
 
 from markitup import settings
 from markitup.util import absolute_url
 import posixpath
 
-class MarkItUpWidget(forms.Textarea):
+class MarkupTextarea(forms.Textarea):
+    def render(self, name, value, attrs=None):
+        if value is not None:
+            # Special handling for MarkupField value.
+            # This won't touch simple TextFields because they don't have
+            # 'raw' attribute.
+            try:
+                value = value.raw
+            except AttributeError:
+                pass
+        return super(MarkupTextarea, self).render(name, value, attrs)
+
+
+class MarkItUpWidget(MarkupTextarea):
     """
     Widget for a MarkItUp editor textarea.
 
     ``markitup_skin``
         URL path (absolute or relative to MEDIA_URL) to MarkItUp skin
         directory.  Default: value of MARKITUP_SKIN setting.
-        
+
     """
     def __init__(self, attrs=None,
                  markitup_set=None,
                 absolute_url('markitup/jquery.markitup.js'),
                 posixpath.join(self.miu_set, 'set.js')))
     media = property(_media)
-        
+
     def render(self, name, value, attrs=None):
         html = super(MarkItUpWidget, self).render(name, value, attrs)
         html += ('<script type="text/javascript">'
                  '});'
                  '</script>' % attrs['id'])
         return mark_safe(html)
-        
+
+
+class AdminMarkItUpWidget(MarkItUpWidget, AdminTextareaWidget):
+    """
+    Add vLargeTextarea class to MarkItUpWidget so it looks more
+    similar to other admin textareas.
+    
+    """
+    pass
  
 setup(
     name='django-markitup',
-    version='0.3.1dev',
+    version='0.4.0dev',
     description='Django integration with the MarkItUp universal markup editor',
     long_description=open('README.txt').read() + open('CHANGELOG.txt').read(),
     author='Carl Meyer',

File tests/models.py

+from django.db import models
+
+from markitup.fields import MarkupField
+
+class Post(models.Model):
+    title = models.CharField(max_length=50)
+    body = MarkupField('body of post')
+
+    def __unicode__(self):
+        return self.title

File tests/test_settings.py

 
 ROOT_URLCONF = 'tests.urls'
 
-MARKITUP_PREVIEW_FILTER = ('tests.filter.testfilter', {'arg': 'replacement'})
+MARKUP_FILTER = ('tests.filter.testfilter', {'arg': 'replacement'})

File tests/tests.py

 from django.test import TestCase, Client
 from django.conf import settings as django_settings
 from markitup import settings
-from markitup.widgets import MarkItUpWidget
+from markitup.widgets import MarkItUpWidget, MarkupTextarea, AdminMarkItUpWidget
 from django.templatetags.markitup_tags import _get_markitup_context
+from django.core import serializers
+from django.forms.models import modelform_factory
+from django.contrib import admin
+from markitup.fields import MarkupField
+
+from models import Post
+
+def test_filter(text, **kwargs):
+    return unicode(text) + unicode(kwargs)
+
+class MarkupFieldTests(TestCase):
+    def setUp(self):
+        self.post = Post.objects.create(title='example post',
+                                        body='replace this text')
+
+    def testUnicodeRender(self):
+        self.assertEquals(unicode(self.post.body),
+                          u'replacement text')
+
+    def testRaw(self):
+        self.assertEquals(self.post.body.raw, 'replace this text')
+
+    def testRendered(self):
+        self.assertEquals(self.post.body.rendered,
+                          u'replacement text')
+
+    def testLoadBack(self):
+        post = Post.objects.get(pk=self.post.pk)
+        self.assertEquals(post.body.raw, self.post.body.raw)
+        self.assertEquals(post.body.rendered, self.post.body.rendered)
+
+    def testAssignToBody(self):
+        self.post.body = 'replace this other text'
+        self.post.save()
+        self.assertEquals(unicode(self.post.body),
+                          u'replacement other text')
+
+    def testAssignToRaw(self):
+        self.post.body.raw = 'new text, replace this'
+        self.post.save()
+        self.assertEquals(unicode(self.post.body),
+                          u'new text, replacement')
+
+    def testAssignToRendered(self):
+        def _invalid_assignment():
+            self.post.body.rendered = 'this should fail'
+        self.assertRaises(AttributeError, _invalid_assignment)
+
+# TODO
+#    def testOverrideFilter(self):
+#        self.post.body.save_markup('tests.tests.test_filter',
+#                                   some_arg='some_val')
+#        self.assertEquals(unicode(self.post.body),
+#                          u"**markdown**{'some_arg': 'some_val'}")
+
+
+class MarkupFieldSerializationTests(TestCase):
+    def setUp(self):
+        self.post = Post.objects.create(title='example post',
+                                        body='replace this thing')
+        self.stream = serializers.serialize('json', Post.objects.all())
+
+    def testSerializeJSON(self):
+        self.assertEquals(self.stream,
+                          '[{"pk": 1, "model": "tests.post", '
+                          '"fields": {"body": "replace this thing", '
+                          '"_body_rendered": "replacement thing", '
+                          '"title": "example post"}}]')
+
+    def testDeserialize(self):
+        self.assertEquals(list(serializers.deserialize("json",
+                                                       self.stream))[0].object,
+                          self.post)
+
+
+class MarkupFieldFormTests(TestCase):
+    def setUp(self):
+        self.post = Post(title='example post', body='**markdown**')
+        self.form_class = modelform_factory(Post)
+
+    def testWidget(self):
+        self.assertEquals(self.form_class().fields['body'].widget.__class__,
+                          MarkupTextarea)
+
+    def testFormFieldContents(self):
+        form = self.form_class(instance=self.post)
+        self.assertEquals(unicode(form['body']),
+                          u'<textarea id="id_body" rows="10" cols="40" name="body">**markdown**</textarea>')
+
+    def testAdminFormField(self):
+        ma = admin.ModelAdmin(Post, admin.site)
+        self.assertEquals(ma.formfield_for_dbfield(
+                Post._meta.get_field('body')).widget.__class__,
+                          AdminMarkItUpWidget)
 
 class PreviewTests(TestCase):
     def test_preview_filter(self):