django-markupfield-1.2.0/0000775000175000017500000000000012173250105015473 5ustar jamesjames00000000000000django-markupfield-1.2.0/setup.py0000664000175000017500000000213412173250051017205 0ustar jamesjames00000000000000from distutils.core import setup long_description = open('README.rst').read() setup( name='django-markupfield', version="1.2.0", package_dir={'markupfield': 'markupfield'}, packages=['markupfield', 'markupfield.tests'], description='Custom Django field for easy use of markup in text fields', author='James Turk', author_email='james.p.turk@gmail.com', license='BSD License', url='http://github.com/jamesturk/django-markupfield/', long_description=long_description, platforms=["any"], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Environment :: Web Environment', ], ) django-markupfield-1.2.0/MANIFEST.in0000664000175000017500000000013512173241306017233 0ustar jamesjames00000000000000include README.rst include LICENSE include CHANGELOG include AUTHORS.txt include MANIFEST.in django-markupfield-1.2.0/CHANGELOG0000664000175000017500000000261412173246503016717 0ustar jamesjames000000000000001.2.0 - 22 July 2013 ==================== - drop support for markup_choices being a dict entirely - PEP8 compliance - bugfix for default 'plain' markup type that escapes HTML 1.1.1 - 16 March 2013 ===================== - experimental Python 3 support with Django 1.5 - drop support for Django 1.3/Python 2.5 - markup_choices can no longer be a dict (deprecated pre-1.0) 1.1.0 - bad release, ignore ============================= 1.0.2 - 25 March 2011 ===================== - fix Django 1.3 DeprecationWarning 1.0.1 - 28 Feb 2011 =================== - added a fix for MarkupField to work with South >= 0.7 1.0.0 - 1 Feb 2011 ================== - added markup_choices option to MarkupField - switch to tuple/list for setting markup_type, dict deprecated - split markup detection into markupfield.markup - escape_html option 0.3.1 - January 28 2010 ======================= - fix bug when adding a MarkupField to an abstract model (github issue #1) 0.3.0 - October 23 2009 ======================= - enable pygments support by default if it is installed 0.2.0 - August 3 2009 ===================== - fixed bug with using MarkupField widgets on postgres backend - correctly check markup_type when doing pre_save 0.1.2 - July 7 2009 =================== - fixed bug with using MarkupField on postgres backend 0.1.0 ===== - initial working release django-markupfield-1.2.0/README.rst0000664000175000017500000001267512173241306017200 0ustar jamesjames00000000000000================== django-markupfield ================== An implementation of a custom MarkupField for Django. A MarkupField is in essence a TextField with an associated markup type. The field also caches its rendered value on the assumption that disk space is cheaper than CPU cycles in a web application. Installation ============ The recommended way to install django-markupfield is with `pip `_ It is not necessary to add ``'markupfield'`` to your ``INSTALLED_APPS``, it merely needs to be on your ``PYTHONPATH``. Requirements ------------ django-markupfield depends on a relatively current version of Django (tested with 1.3-1.4, may work with 1.2 but not guaranteed) and libraries for whichever markup options you wish to include. Settings ======== To best make use of MarkupField you should define the ``MARKUP_FIELD_TYPES`` setting, a mapping of strings to callables that 'render' a markup type:: import markdown from docutils.core import publish_parts def render_rest(markup): parts = publish_parts(source=markup, writer_name="html4css1") return parts["fragment"] MARKUP_FIELD_TYPES = ( ('markdown', markdown.markdown), ('ReST', render_rest), ) If you do not define a ``MARKUP_FIELD_TYPES`` then one is provided with the following markup types available: html: allows HTML, potentially unsafe plain: plain text markup, calls urlize and replaces text with linebreaks markdown: default `markdown`_ renderer (only if `python-markdown`_ is installed) restructuredtext: default `ReST`_ renderer (only if `docutils`_ is installed) textile: default `textile`_ renderer (only if `textile`_ is installed) It is also possible to override ``MARKUP_FIELD_TYPES`` on a per-field basis by passing the ``markup_choices`` option to a ``MarkupField`` in your model declaration. .. _`markdown`: http://daringfireball.net/projects/markdown/ .. _`ReST`: http://docutils.sourceforge.net/rst.html .. _`textile`: http://hobix.com/textile/quick.html .. _`python-markdown`: http://www.freewisdom.org/projects/python-markdown/ .. _`docutils`: http://docutils.sourceforge.net/ .. _`python-textile`: http://pypi.python.org/pypi/textile Usage ===== Using MarkupField is relatively easy, it can be used in any model definition:: from django.db import models from markupfield.fields import MarkupField class Article(models.Model): title = models.CharField(max_length=100) slug = models.SlugField(max_length=100) body = MarkupField() ``Article`` objects can then be created with any markup type defined in ``MARKUP_FIELD_TYPES``:: Article.objects.create(title='some article', slug='some-article', body='*fancy*', body_markup_type='markdown') You will notice that a field named ``body_markup_type`` exists that you did not declare, MarkupField actually creates two extra fields here ``body_markup_type`` and ``_body_rendered``. These fields are always named according to the name of the declared ``MarkupField``. Arguments --------- ``MarkupField`` also takes three optional arguments. Either ``default_markup_type`` and ``markup_type`` arguments may be specified but not both. ``default_markup_type``: Set a markup_type that the field will default to if one is not specified. It is still possible to edit the markup type attribute and it will appear by default in ModelForms. ``markup_type``: Set markup type that the field will always use, ``editable=False`` is set on the hidden field so it is not shown in ModelForms. ``markup_choices``: A replacement list of markup choices to be used in lieu of ``MARKUP_FIELD_TYPES`` on a per-field basis. ``escape_html``: A flag (False by default) indicating that the input should be regarded as untrusted and as such will be run through Django's ``escape`` filter. Examples ~~~~~~~~ ``MarkupField`` that will default to using markdown but allow the user a choice:: MarkupField(default_markup_type='markdown') ``MarkupField`` that will use textile and not provide a choice on forms:: MarkupField(markup_type='textile') ``MarkupField`` that will use a custom set of renderers:: CUSTOM_RENDERERS = ( ('markdown', markdown.markdown), ('wiki', my_wiki_render_func) ) MarkupField(markup_choices=CUSTOM_RENDERERS) Accessing a MarkupField on a model ---------------------------------- When accessing an attribute of a model that was declared as a ``MarkupField`` a special ``Markup`` object is returned. The ``Markup`` object has three parameters: ``raw``: The unrendered markup. ``markup_type``: The markup type. ``rendered``: The rendered HTML version of ``raw``, this attribute is read-only. This object has a ``__unicode__`` method that calls ``django.utils.safestring.mark_safe`` on ``rendered`` allowing MarkupField objects to appear in templates as their rendered selfs without any 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.markup_type u'markdown' >>> a.body.rendered u'

fancy

' >>> print unicode(a.body)

fancy

Assignment to ``a.body`` is equivalent to assignment to ``a.body.raw`` and assignment to ``a.body_markup_type`` is equivalent to assignment to ``a.body.markup_type``. .. note:: a.body.rendered is only updated when a.save() is called django-markupfield-1.2.0/PKG-INFO0000664000175000017500000001723412173250105016577 0ustar jamesjames00000000000000Metadata-Version: 1.1 Name: django-markupfield Version: 1.2.0 Summary: Custom Django field for easy use of markup in text fields Home-page: http://github.com/jamesturk/django-markupfield/ Author: James Turk Author-email: james.p.turk@gmail.com License: BSD License Description: ================== django-markupfield ================== An implementation of a custom MarkupField for Django. A MarkupField is in essence a TextField with an associated markup type. The field also caches its rendered value on the assumption that disk space is cheaper than CPU cycles in a web application. Installation ============ The recommended way to install django-markupfield is with `pip `_ It is not necessary to add ``'markupfield'`` to your ``INSTALLED_APPS``, it merely needs to be on your ``PYTHONPATH``. Requirements ------------ django-markupfield depends on a relatively current version of Django (tested with 1.3-1.4, may work with 1.2 but not guaranteed) and libraries for whichever markup options you wish to include. Settings ======== To best make use of MarkupField you should define the ``MARKUP_FIELD_TYPES`` setting, a mapping of strings to callables that 'render' a markup type:: import markdown from docutils.core import publish_parts def render_rest(markup): parts = publish_parts(source=markup, writer_name="html4css1") return parts["fragment"] MARKUP_FIELD_TYPES = ( ('markdown', markdown.markdown), ('ReST', render_rest), ) If you do not define a ``MARKUP_FIELD_TYPES`` then one is provided with the following markup types available: html: allows HTML, potentially unsafe plain: plain text markup, calls urlize and replaces text with linebreaks markdown: default `markdown`_ renderer (only if `python-markdown`_ is installed) restructuredtext: default `ReST`_ renderer (only if `docutils`_ is installed) textile: default `textile`_ renderer (only if `textile`_ is installed) It is also possible to override ``MARKUP_FIELD_TYPES`` on a per-field basis by passing the ``markup_choices`` option to a ``MarkupField`` in your model declaration. .. _`markdown`: http://daringfireball.net/projects/markdown/ .. _`ReST`: http://docutils.sourceforge.net/rst.html .. _`textile`: http://hobix.com/textile/quick.html .. _`python-markdown`: http://www.freewisdom.org/projects/python-markdown/ .. _`docutils`: http://docutils.sourceforge.net/ .. _`python-textile`: http://pypi.python.org/pypi/textile Usage ===== Using MarkupField is relatively easy, it can be used in any model definition:: from django.db import models from markupfield.fields import MarkupField class Article(models.Model): title = models.CharField(max_length=100) slug = models.SlugField(max_length=100) body = MarkupField() ``Article`` objects can then be created with any markup type defined in ``MARKUP_FIELD_TYPES``:: Article.objects.create(title='some article', slug='some-article', body='*fancy*', body_markup_type='markdown') You will notice that a field named ``body_markup_type`` exists that you did not declare, MarkupField actually creates two extra fields here ``body_markup_type`` and ``_body_rendered``. These fields are always named according to the name of the declared ``MarkupField``. Arguments --------- ``MarkupField`` also takes three optional arguments. Either ``default_markup_type`` and ``markup_type`` arguments may be specified but not both. ``default_markup_type``: Set a markup_type that the field will default to if one is not specified. It is still possible to edit the markup type attribute and it will appear by default in ModelForms. ``markup_type``: Set markup type that the field will always use, ``editable=False`` is set on the hidden field so it is not shown in ModelForms. ``markup_choices``: A replacement list of markup choices to be used in lieu of ``MARKUP_FIELD_TYPES`` on a per-field basis. ``escape_html``: A flag (False by default) indicating that the input should be regarded as untrusted and as such will be run through Django's ``escape`` filter. Examples ~~~~~~~~ ``MarkupField`` that will default to using markdown but allow the user a choice:: MarkupField(default_markup_type='markdown') ``MarkupField`` that will use textile and not provide a choice on forms:: MarkupField(markup_type='textile') ``MarkupField`` that will use a custom set of renderers:: CUSTOM_RENDERERS = ( ('markdown', markdown.markdown), ('wiki', my_wiki_render_func) ) MarkupField(markup_choices=CUSTOM_RENDERERS) Accessing a MarkupField on a model ---------------------------------- When accessing an attribute of a model that was declared as a ``MarkupField`` a special ``Markup`` object is returned. The ``Markup`` object has three parameters: ``raw``: The unrendered markup. ``markup_type``: The markup type. ``rendered``: The rendered HTML version of ``raw``, this attribute is read-only. This object has a ``__unicode__`` method that calls ``django.utils.safestring.mark_safe`` on ``rendered`` allowing MarkupField objects to appear in templates as their rendered selfs without any 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.markup_type u'markdown' >>> a.body.rendered u'

fancy

' >>> print unicode(a.body)

fancy

Assignment to ``a.body`` is equivalent to assignment to ``a.body.raw`` and assignment to ``a.body_markup_type`` is equivalent to assignment to ``a.body.markup_type``. .. note:: a.body.rendered is only updated when a.save() is called Platform: any Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Environment :: Web Environment django-markupfield-1.2.0/AUTHORS.txt0000664000175000017500000000047512173250033017367 0ustar jamesjames00000000000000James Turk Jeremy Carbaugh - design help and minor fixes Carl J Meyer - simplified widgets.py Michael Fladischer - test patch & debian packaging Javed Khan - escape_html option Roman Vasiliev - fix for South Markus Holtermann - always escape plain markup preventing XSS django-markupfield-1.2.0/LICENSE0000664000175000017500000000302312173241306016501 0ustar jamesjames00000000000000django-markupfield ================== Copyright (c) 2010, James Turk All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of django-markupfield nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. django-markupfield-1.2.0/markupfield/0000775000175000017500000000000012173250105017776 5ustar jamesjames00000000000000django-markupfield-1.2.0/markupfield/widgets.py0000664000175000017500000000071712173241306022026 0ustar jamesjames00000000000000from django import forms from django.contrib.admin.widgets import AdminTextareaWidget from django.utils import six class MarkupTextarea(forms.widgets.Textarea): def render(self, name, value, attrs=None): if value is not None and not isinstance(value, six.text_type): value = value.raw return super(MarkupTextarea, self).render(name, value, attrs) class AdminMarkupTextareaWidget(MarkupTextarea, AdminTextareaWidget): pass django-markupfield-1.2.0/markupfield/markup.py0000664000175000017500000000537112173245776021677 0ustar jamesjames00000000000000from django.utils.html import escape, linebreaks, urlize from django.utils.functional import curry from django.conf import settings # build DEFAULT_MARKUP_TYPES DEFAULT_MARKUP_TYPES = [ ('html', lambda markup: markup), ('plain', lambda markup: urlize(linebreaks(escape(markup)))), ] try: import pygments # noqa PYGMENTS_INSTALLED = True def _register_pygments_rst_directive(): from docutils import nodes from docutils.parsers.rst import directives from pygments import highlight from pygments.lexers import get_lexer_by_name, TextLexer from pygments.formatters import HtmlFormatter DEFAULT = HtmlFormatter() VARIANTS = { 'linenos': HtmlFormatter(linenos=True), } def pygments_directive(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine): try: lexer = get_lexer_by_name(arguments[0]) except ValueError: # no lexer found - use the text one instead of an exception lexer = TextLexer() formatter = options and VARIANTS[options.keys()[0]] or DEFAULT parsed = highlight(u'\n'.join(content), lexer, formatter) return [nodes.raw('', parsed, format='html')] pygments_directive.arguments = (1, 0, 1) pygments_directive.content = 1 directives.register_directive('code', pygments_directive) except ImportError: PYGMENTS_INSTALLED = False try: import markdown md_filter = markdown.markdown # try and replace if pygments & codehilite are available if PYGMENTS_INSTALLED: try: from markdown.extensions.codehilite import makeExtension # noqa md_filter = curry(markdown.markdown, extensions=['codehilite(css_class=highlight)']) except ImportError: pass # whichever markdown_filter was available DEFAULT_MARKUP_TYPES.append(('markdown', md_filter)) except ImportError: pass try: from docutils.core import publish_parts if PYGMENTS_INSTALLED: _register_pygments_rst_directive() def render_rest(markup): overrides = getattr(settings, "RESTRUCTUREDTEXT_FILTER_SETTINGS", {}) parts = publish_parts(source=markup, writer_name="html4css1", settings_overrides=overrides) return parts["fragment"] DEFAULT_MARKUP_TYPES.append(('restructuredtext', render_rest)) except ImportError: pass try: import textile textile_filter = curry(textile.textile, encoding='utf-8', output='utf-8') DEFAULT_MARKUP_TYPES.append(('textile', textile_filter)) except ImportError: pass django-markupfield-1.2.0/markupfield/__init__.py0000664000175000017500000000002612173250062022107 0ustar jamesjames00000000000000__version__ = '1.2.0' django-markupfield-1.2.0/markupfield/tests/0000775000175000017500000000000012173250105021140 5ustar jamesjames00000000000000django-markupfield-1.2.0/markupfield/tests/settings.py0000664000175000017500000000110512173245776023371 0ustar jamesjames00000000000000DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': 'markuptest.db' } } import markdown from django.utils.html import escape, linebreaks, urlize from docutils.core import publish_parts def render_rest(markup): parts = publish_parts(source=markup, writer_name="html4css1") return parts["fragment"] MARKUP_FIELD_TYPES = [ ('markdown', markdown.markdown), ('ReST', render_rest), ('plain', lambda markup: urlize(linebreaks(escape(markup)))), ] INSTALLED_APPS = ( 'markupfield.tests', ) SECRET_KEY = 'sekrit' django-markupfield-1.2.0/markupfield/tests/tests.py0000664000175000017500000002274512173247754022706 0ustar jamesjames00000000000000from __future__ import unicode_literals import json from django.test import TestCase from django.core import serializers from django.utils.encoding import smart_text from markupfield.fields import MarkupField, Markup from markupfield.widgets import MarkupTextarea, AdminMarkupTextareaWidget from markupfield.tests.models import Post, Article, Concrete from django.forms.models import modelform_factory ArticleForm = modelform_factory(Article) class MarkupFieldTestCase(TestCase): def setUp(self): self.xss_str = "" self.mp = Post(title='example markdown post', body='**markdown**', body_markup_type='markdown') self.mp.save() self.rp = Post(title='example restructuredtext post', body='*ReST*', body_markup_type='ReST') self.rp.save() self.xss_post = Post(title='example xss post', body=self.xss_str, body_markup_type='markdown', comment=self.xss_str) self.xss_post.save() self.plain_str = ('plain post\n\n' 'http://example.com') self.pp = Post(title='example plain post', body=self.plain_str, body_markup_type='plain', comment=self.plain_str, comment_markup_type='plain') self.pp.save() def test_verbose_name(self): self.assertEqual(self.mp._meta.get_field('body').verbose_name, 'body of post') def test_markup_body(self): self.assertEqual(self.mp.body.raw, '**markdown**') self.assertEqual(self.mp.body.rendered, '

markdown

') self.assertEqual(self.mp.body.markup_type, 'markdown') def test_markup_unicode(self): u = smart_text(self.rp.body.rendered) self.assertEqual(u, '

ReST

\n') def test_from_database(self): """ Test that data loads back from the database correctly and 'post' has the right type.""" p1 = Post.objects.get(pk=self.mp.pk) self.assertTrue(isinstance(p1.body, Markup)) self.assertEqual(smart_text(p1.body), '

markdown

') ## Assignment ## def test_body_assignment(self): self.rp.body = '**ReST**' self.rp.save() self.assertEqual(smart_text(self.rp.body), '

ReST

\n') def test_raw_assignment(self): self.rp.body.raw = '*ReST*' self.rp.save() self.assertEqual(smart_text(self.rp.body), '

ReST

\n') def test_rendered_assignment(self): def f(): self.rp.body.rendered = 'this should fail' self.assertRaises(AttributeError, f) def test_body_type_assignment(self): self.rp.body.markup_type = 'markdown' self.rp.save() self.assertEqual(self.rp.body.markup_type, 'markdown') self.assertEqual(smart_text(self.rp.body), '

ReST

') ## Serialization ## def test_serialize_to_json(self): stream = serializers.serialize('json', Post.objects.all()) # Load the data back into Python so that a failed comparison gives a # better diff output. actual = json.loads(stream) expected = [ {"pk": 1, "model": "tests.post", "fields": {"body": "**markdown**", "comment": "", "_comment_rendered": "", "_body_rendered": "

markdown

", "title": "example markdown post", "comment_markup_type": "markdown", "body_markup_type": "markdown"}}, {"pk": 2, "model": "tests.post", "fields": {"body": "*ReST*", "comment": "", "_comment_rendered": "", "_body_rendered": "

ReST

\n", "title": "example restructuredtext post", "comment_markup_type": "markdown", "body_markup_type": "ReST"}}, {"pk": 3, "model": "tests.post", "fields": {"body": "", "comment": "", "_comment_rendered": ( "

<script>alert(" "'xss');</script>

"), "_body_rendered": "", "title": "example xss post", "comment_markup_type": "markdown", "body_markup_type": "markdown"}}, {"pk": 4, "model": "tests.post", "fields": {"body": ('plain ' 'post\n\nhttp://example.com'), "comment": ('plain ' 'post\n\nhttp://example.com'), "_comment_rendered": ( '

&lt;span style=&quot;color: red' '&quot;&gt;plain&lt;/span&gt; ' 'post

\n\n

http://example.com

'), "_body_rendered": ('

<span style="color: ' 'red">plain</span> ' 'post

\n\n

http://example.com' '

'), "title": "example plain post", "comment_markup_type": "plain", "body_markup_type": "plain"}}, ] self.assertEqual(expected, actual) def test_deserialize_json(self): stream = serializers.serialize('json', Post.objects.all()) obj = list(serializers.deserialize('json', stream))[0] self.assertEqual(obj.object, self.mp) ## Other ## def test_escape_html(self): # the rendered string has been escaped self.assertEqual(self.xss_post.comment.raw, self.xss_str) self.assertEqual( smart_text(self.xss_post.comment.rendered), '

<script>alert('xss');</script>

') def test_escape_html_false(self): # both strings here are the xss_str, no escaping was done self.assertEqual(self.xss_post.body.raw, self.xss_str) self.assertEqual(smart_text(self.xss_post.body.rendered), self.xss_str) def test_inheritance(self): # test that concrete correctly got the added fields concrete_fields = [f.name for f in Concrete._meta.fields] self.assertEqual(concrete_fields, ['id', 'content', 'content_markup_type', '_content_rendered']) def test_markup_type_validation(self): self.assertRaises(ValueError, MarkupField, 'verbose name', 'markup_field', 'bad_markup_type') def test_default_markup_types(self): from markupfield.markup import DEFAULT_MARKUP_TYPES for markup_type in DEFAULT_MARKUP_TYPES: rendered = markup_type[1]('test') self.assertTrue(hasattr(rendered, '__str__')) class MarkupWidgetTests(TestCase): def test_markuptextarea_used(self): self.assertTrue(isinstance(MarkupField().formfield().widget, MarkupTextarea)) self.assertTrue(isinstance(ArticleForm()['normal_field'].field.widget, MarkupTextarea)) def test_markuptextarea_render(self): a = Article(normal_field='**normal**', normal_field_markup_type='markdown', default_field='**default**', markdown_field='**markdown**', markup_choices_field_markup_type='nomarkup') a.save() af = ArticleForm(instance=a) self.assertHTMLEqual( smart_text(af['normal_field']), '' ) def test_no_markup_type_field_if_set(self): """ensure that a field with non-editable markup_type set does not have a _markup_type field""" self.assertTrue('markdown_field_markup_type' not in ArticleForm().fields.keys()) def test_markup_type_choices(self): self.assertEqual( ArticleForm().fields['normal_field_markup_type'].choices, [('', '--'), ('markdown', 'markdown'), ('ReST', 'ReST'), ('plain', 'plain')]) self.assertEqual( ArticleForm().fields['markup_choices_field_markup_type'].choices, [('', '--'), ('pandamarkup', 'pandamarkup'), ('nomarkup', 'nomarkup')]) def test_default_markup_type(self): self.assertTrue( ArticleForm().fields['normal_field_markup_type'].initial is None) self.assertEqual( ArticleForm().fields['default_field_markup_type'].initial, 'markdown') def test_model_admin_field(self): # borrows from regressiontests/admin_widgets/tests.py from django.contrib import admin ma = admin.ModelAdmin(Post, admin.site) self.assertTrue(isinstance(ma.formfield_for_dbfield( Post._meta.get_field('body')).widget, AdminMarkupTextareaWidget)) django-markupfield-1.2.0/markupfield/tests/__init__.py0000664000175000017500000000000012173241306023242 0ustar jamesjames00000000000000django-markupfield-1.2.0/markupfield/tests/models.py0000664000175000017500000000153412173244537023014 0ustar jamesjames00000000000000from django.db import models from django.utils.encoding import python_2_unicode_compatible from markupfield.fields import MarkupField @python_2_unicode_compatible class Post(models.Model): title = models.CharField(max_length=50) body = MarkupField('body of post') comment = MarkupField(escape_html=True, default_markup_type='markdown') def __str__(self): return self.title class Article(models.Model): normal_field = MarkupField() markup_choices_field = MarkupField(markup_choices=( ('pandamarkup', lambda x: 'panda'), ('nomarkup', lambda x: x))) default_field = MarkupField(default_markup_type='markdown') markdown_field = MarkupField(markup_type='markdown') class Abstract(models.Model): content = MarkupField() class Meta: abstract = True class Concrete(Abstract): pass django-markupfield-1.2.0/markupfield/fields.py0000664000175000017500000001555012173244442021633 0ustar jamesjames00000000000000import django from django.conf import settings from django.db import models from django.utils.safestring import mark_safe from django.utils.html import escape from markupfield import widgets from markupfield import markup _rendered_field_name = lambda name: '_%s_rendered' % name _markup_type_field_name = lambda name: '%s_markup_type' % name # for fields that don't set markup_types: detected types or from settings _MARKUP_TYPES = getattr(settings, 'MARKUP_FIELD_TYPES', markup.DEFAULT_MARKUP_TYPES) class Markup(object): def __init__(self, instance, field_name, rendered_field_name, markup_type_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 self.markup_type_field_name = markup_type_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) # markup_type is read/write def _get_markup_type(self): return self.instance.__dict__[self.markup_type_field_name] def _set_markup_type(self, val): return setattr(self.instance, self.markup_type_field_name, val) markup_type = property(_get_markup_type, _set_markup_type) # 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) self.markup_type_field_name = _markup_type_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, self.markup_type_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) setattr(obj, self.markup_type_field_name, value.markup_type) else: obj.__dict__[self.field.name] = value class MarkupField(models.TextField): def __init__(self, verbose_name=None, name=None, markup_type=None, default_markup_type=None, markup_choices=_MARKUP_TYPES, escape_html=False, **kwargs): if markup_type and default_markup_type: raise ValueError('Cannot specify both markup_type and ' 'default_markup_type') self.default_markup_type = markup_type or default_markup_type self.markup_type_editable = markup_type is None self.escape_html = escape_html self.markup_choices_list = [mc[0] for mc in markup_choices] self.markup_choices_dict = dict(markup_choices) if (self.default_markup_type and self.default_markup_type not in self.markup_choices_list): raise ValueError("Invalid default_markup_type for field '%s', " "allowed values: %s" % (name, ', '.join(self.markup_choices_list))) # for South FakeORM compatibility: the frozen version of a # MarkupField can't try to add a _rendered field, because the # _rendered field itself is frozen as well. See introspection # rules below. self.rendered_field = not kwargs.pop('rendered_field', False) super(MarkupField, self).__init__(verbose_name, name, **kwargs) def contribute_to_class(self, cls, name): if not cls._meta.abstract: choices = zip([''] + self.markup_choices_list, ['--'] + self.markup_choices_list) markup_type_field = models.CharField( max_length=30, choices=choices, default=self.default_markup_type, editable=self.markup_type_editable, blank=self.blank) rendered_field = models.TextField(editable=False) markup_type_field.creation_counter = self.creation_counter + 1 rendered_field.creation_counter = self.creation_counter + 2 cls.add_to_class(_markup_type_field_name(name), markup_type_field) 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) if value.markup_type not in self.markup_choices_list: raise ValueError('Invalid markup type (%s), allowed values: %s' % (value.markup_type, ', '.join(self.markup_choices_list))) if self.escape_html: raw = escape(value.raw) else: raw = value.raw rendered = self.markup_choices_dict[value.markup_type](raw) setattr(model_instance, _rendered_field_name(self.attname), rendered) return value.raw def get_prep_value(self, value): if isinstance(value, Markup): return value.raw else: return value # copy get_prep_value to get_db_prep_value if pre-1.2 if django.VERSION < (1, 2): get_db_prep_value = get_prep_value def value_to_string(self, obj): value = self._get_val_from_obj(obj) return value.raw 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.AdminMarkupTextareaWidget} # allow South to handle MarkupField smoothly try: from south.modelsinspector import add_introspection_rules # For a normal MarkupField, the add_rendered_field attribute is # always True, which means no_rendered_field arg will always be # True in a frozen MarkupField, which is what we want. add_introspection_rules(rules=[ ((MarkupField, ), [], {'rendered_field': ['rendered_field', {}], }) ], patterns=['markupfield\.fields\.MarkupField']) except ImportError: pass