django-markupfield-2.0.1/0000755000076600000240000000000014135623342015503 5ustar jamesstaff00000000000000django-markupfield-2.0.1/PKG-INFO0000644000076600000240000002200314135623342016575 0ustar jamesstaff00000000000000Metadata-Version: 1.1 Name: django-markupfield Version: 2.0.1 Summary: Custom Django field for easy use of markup in text fields Home-page: https://github.com/jamesturk/django-markupfield/ Author: James Turk Author-email: dev@jamesturk.net License: BSD License Description: ================== django-markupfield ================== .. image:: https://github.com/jamesturk/django-markupfield/workflows/Test/badge.svg .. image:: https://img.shields.io/pypi/v/django-markupfield.svg :target: https://pypi.python.org/pypi/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``. However, to use titled markup you either add ``'markupfield'`` to your ``INSTALLED_APPS`` or add the corresponding translations to your project translation. Requirements ------------ Requires Django >= 2.2 and 3.6+ * 1.5.x is the last release to officially support Django < 2.2 or Python 2.7 * 1.4.x is the last release to officially support Django < 1.11 * 1.3.x is the last release to officially support Django 1.4 or Python 3.3 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 `markdown`_ is installed) restructuredtext: default `ReST`_ renderer (only if `docutils`_ 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. .. _`ReST`: http://docutils.sourceforge.net/rst.html .. _`markdown`: https://pypi.python.org/pypi/Markdown .. _`docutils`: http://docutils.sourceforge.net/ 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 ReST and not provide a choice on forms:: MarkupField(markup_type='restructuredtext') ``MarkupField`` that will use a custom set of renderers:: CUSTOM_RENDERERS = ( ('markdown', markdown.markdown), ('wiki', my_wiki_render_func) ) MarkupField(markup_choices=CUSTOM_RENDERERS) .. note:: When using ``markdown``, be sure to use ``markdown.markdown`` and not the ``markdown.Markdown`` class, the class requires an explicit ``reset`` to function properly in some cases. (See [issue #40](https://github.com/jamesturk/django-markupfield/issues/40) for details.) 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``. .. important:: Keeping in mind that ``body`` is MarkupField instance is particullary important with ``default`` or ``default_if_none`` filter for model that could be blank. If ``body``'s ``rendered`` is ``None`` or empty string (``""``) these filters will *not* evaluate ``body`` as falsy to display default text:: {{ a.body|default:"" }} That's because ``body`` is regular non-``None`` MarkupField instance. To let ``default`` or ``default_if_none`` filters to work evaluate ``rendered`` MarkupField attribute instead. To prevent escaping HTML for the case ``rendered`` is truethy, finish chain with ``safe`` filter:: {{ a.body.rendered|default:""|safe }} .. 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 :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Environment :: Web Environment django-markupfield-2.0.1/LICENSE0000644000076600000240000000250713567311512016515 0ustar jamesstaff00000000000000django-markupfield ================== Copyright (c) 2015, 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. 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-2.0.1/MANIFEST.in0000644000076600000240000000013513567311512017241 0ustar jamesstaff00000000000000include README.rst include LICENSE include CHANGELOG include AUTHORS.txt include MANIFEST.in django-markupfield-2.0.1/setup.py0000644000076600000240000000227414127105111017210 0ustar jamesstaff00000000000000from setuptools import setup long_description = open("README.rst").read() setup( name="django-markupfield", version="2.0.1", package_dir={"markupfield": "markupfield"}, packages=["markupfield", "markupfield.tests"], package_data={"markupfield": ["locale/*/*/*"]}, description="Custom Django field for easy use of markup in text fields", author="James Turk", author_email="dev@jamesturk.net", license="BSD License", url="https://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 :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Environment :: Web Environment", ], ) django-markupfield-2.0.1/setup.cfg0000644000076600000240000000010314135623342017316 0ustar jamesstaff00000000000000[bdist_wheel] universal = 1 [egg_info] tag_build = tag_date = 0 django-markupfield-2.0.1/README.rst0000644000076600000240000001513313764514330017200 0ustar jamesstaff00000000000000================== django-markupfield ================== .. image:: https://github.com/jamesturk/django-markupfield/workflows/Test/badge.svg .. image:: https://img.shields.io/pypi/v/django-markupfield.svg :target: https://pypi.python.org/pypi/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``. However, to use titled markup you either add ``'markupfield'`` to your ``INSTALLED_APPS`` or add the corresponding translations to your project translation. Requirements ------------ Requires Django >= 2.2 and 3.6+ * 1.5.x is the last release to officially support Django < 2.2 or Python 2.7 * 1.4.x is the last release to officially support Django < 1.11 * 1.3.x is the last release to officially support Django 1.4 or Python 3.3 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 `markdown`_ is installed) restructuredtext: default `ReST`_ renderer (only if `docutils`_ 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. .. _`ReST`: http://docutils.sourceforge.net/rst.html .. _`markdown`: https://pypi.python.org/pypi/Markdown .. _`docutils`: http://docutils.sourceforge.net/ 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 ReST and not provide a choice on forms:: MarkupField(markup_type='restructuredtext') ``MarkupField`` that will use a custom set of renderers:: CUSTOM_RENDERERS = ( ('markdown', markdown.markdown), ('wiki', my_wiki_render_func) ) MarkupField(markup_choices=CUSTOM_RENDERERS) .. note:: When using ``markdown``, be sure to use ``markdown.markdown`` and not the ``markdown.Markdown`` class, the class requires an explicit ``reset`` to function properly in some cases. (See [issue #40](https://github.com/jamesturk/django-markupfield/issues/40) for details.) 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``. .. important:: Keeping in mind that ``body`` is MarkupField instance is particullary important with ``default`` or ``default_if_none`` filter for model that could be blank. If ``body``'s ``rendered`` is ``None`` or empty string (``""``) these filters will *not* evaluate ``body`` as falsy to display default text:: {{ a.body|default:"" }} That's because ``body`` is regular non-``None`` MarkupField instance. To let ``default`` or ``default_if_none`` filters to work evaluate ``rendered`` MarkupField attribute instead. To prevent escaping HTML for the case ``rendered`` is truethy, finish chain with ``safe`` filter:: {{ a.body.rendered|default:""|safe }} .. note:: a.body.rendered is only updated when a.save() is called django-markupfield-2.0.1/AUTHORS.txt0000644000076600000240000000060013567313305017370 0ustar jamesstaff00000000000000James 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 translatable titles & German translation Frank Wiles - fix for Django 1.7 django-markupfield-2.0.1/markupfield/0000755000076600000240000000000014135623342020006 5ustar jamesstaff00000000000000django-markupfield-2.0.1/markupfield/locale/0000755000076600000240000000000014135623342021245 5ustar jamesstaff00000000000000django-markupfield-2.0.1/markupfield/locale/de/0000755000076600000240000000000014135623342021635 5ustar jamesstaff00000000000000django-markupfield-2.0.1/markupfield/locale/de/LC_MESSAGES/0000755000076600000240000000000014135623342023422 5ustar jamesstaff00000000000000django-markupfield-2.0.1/markupfield/locale/de/LC_MESSAGES/django.mo0000644000076600000240000000145513567311512025227 0ustar jamesstaff00000000000000Þ•L |¨©ÁÝ$ö€6 ·Ã1Ódjango-markupfieldHTMLdjango-markupfieldMarkdowndjango-markupfieldPlaindjango-markupfieldRestructured Textdjango-markupfieldTextileProject-Id-Version: Report-Msgid-Bugs-To: POT-Creation-Date: 2013-08-06 03:44+0200 PO-Revision-Date: 2013-08-06 03:45+0200 Last-Translator: Markus Holtermann Language-Team: German <> Language: de MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); X-Generator: Lokalize 1.5 HTML SyntaxMarkdown SyntaxNormaler Text (Zeilenumbrüche und URL-Erkennung)Restructured Text SyntaxTextile Syntaxdjango-markupfield-2.0.1/markupfield/locale/de/LC_MESSAGES/django.po0000644000076600000240000000172713567311512025234 0ustar jamesstaff00000000000000# Markus Holtermann , 2013. msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2013-08-06 03:44+0200\n" "PO-Revision-Date: 2013-08-06 03:45+0200\n" "Last-Translator: Markus Holtermann \n" "Language-Team: German <>\n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Lokalize 1.5\n" #: markup.py:8 msgctxt "django-markupfield" msgid "HTML" msgstr "HTML Syntax" #: markup.py:9 msgctxt "django-markupfield" msgid "Plain" msgstr "Normaler Text (Zeilenumbrüche und URL-Erkennung)" #: markup.py:61 msgctxt "django-markupfield" msgid "Markdown" msgstr "Markdown Syntax" #: markup.py:78 msgctxt "django-markupfield" msgid "Restructured Text" msgstr "Restructured Text Syntax" #: markup.py:85 msgctxt "django-markupfield" msgid "Textile" msgstr "Textile Syntax" django-markupfield-2.0.1/markupfield/fields.py0000644000076600000240000001705714135621366021644 0ustar jamesstaff00000000000000from django.conf import settings from django.db import models from django.utils.safestring import mark_safe from django.utils.html import escape from django.utils.encoding import force_str from markupfield import widgets from markupfield import markup from django.contrib.admin.options import FORMFIELD_FOR_DBFIELD_DEFAULTS _rendered_field_name = lambda name: "_%s_rendered" % name # noqa _markup_type_field_name = lambda name: "%s_markup_type" % name # noqa # 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 __str__(self): if self.rendered is None: return mark_safe("") return mark_safe(force_str(self.rendered)) def __bool__(self): return bool(self.raw) 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: return self 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((mc[0], mc[1]) for mc in markup_choices) self.markup_choices_title = [] for mc in markup_choices: if len(mc) == 3: self.markup_choices_title.append(mc[2]) else: # Fallback for 2-tuples (we now use 3-tuple) self.markup_choices_title.append(mc[0]) 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 migration compatibility, avoid adding rendered_field 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 self.rendered_field and not cls._meta.abstract: choices = zip( [""] + self.markup_choices_list, ["--"] + self.markup_choices_title ) markup_type_field = models.CharField( max_length=30, choices=choices, default=self.default_markup_type, editable=self.markup_type_editable, blank=False if self.default_markup_type else True, null=False if self.default_markup_type else True, ) rendered_field = models.TextField( editable=False, null=self.null, default=self.default ) 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 deconstruct(self): name, path, args, kwargs = super(MarkupField, self).deconstruct() # Don't migrate rendered fields kwargs["rendered_field"] = True return name, path, args, kwargs 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 value.raw is not None: if self.escape_html: raw = escape(value.raw) else: raw = value.raw rendered = self.markup_choices_dict[value.markup_type](raw) else: rendered = None 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 def get_searchable_content(self, value): # Wagtail checks for existence of this method to determine what # value to index in its search backend. Incoming value comes from # model_instance.field_name return self.get_prep_value(value) def value_to_string(self, obj): if obj is not None: value = self.value_from_object(obj) else: value = self.get_default() if hasattr(value, "raw"): return value.raw return value def formfield(self, **kwargs): defaults = {"widget": widgets.MarkupTextarea} defaults.update(kwargs) return super(MarkupField, self).formfield(**defaults) def to_python(self, value): if isinstance(value, Markup): return value else: return super(MarkupField, self).to_python(value) # register MarkupField to use the custom widget in the Admin FORMFIELD_FOR_DBFIELD_DEFAULTS[MarkupField] = { "widget": widgets.AdminMarkupTextareaWidget } django-markupfield-2.0.1/markupfield/tests/0000755000076600000240000000000014135623342021150 5ustar jamesstaff00000000000000django-markupfield-2.0.1/markupfield/tests/models.py0000644000076600000240000000243213605410525023004 0ustar jamesstaff00000000000000from django.db import models from markupfield.fields import MarkupField 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), ("fancy", lambda x: x[::-1], "Some fancy Markup"), # String reverse ) ) 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 class NullTestModel(models.Model): text = MarkupField( null=True, blank=True, default=None, default_markup_type="markdown" ) class DefaultTestModel(models.Model): text = MarkupField(null=True, default="**nice**", default_markup_type="markdown") class NullDefaultTestModel(models.Model): text = MarkupField( null=False, blank=True, default="*nice*", default_markup_type="markdown" ) django-markupfield-2.0.1/markupfield/tests/__init__.py0000644000076600000240000000000013567311512023250 0ustar jamesstaff00000000000000django-markupfield-2.0.1/markupfield/tests/settings.py0000644000076600000240000000163314034167502023364 0ustar jamesstaff00000000000000import os import markdown from django.utils.html import escape, linebreaks, urlize from docutils.core import publish_parts DEFAULT_AUTO_FIELD = "django.db.models.AutoField" if os.environ.get("DB") == "postgres": DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql_psycopg2", "NAME": "test", "USER": "postgres", "PASSWORD": "", } } else: DATABASES = { "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": "markuptest.db"} } 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" MIDDLEWARE_CLASSES = () ROOT_URLCONF = () django-markupfield-2.0.1/markupfield/tests/tests.py0000644000076600000240000004212514135622001022657 0ustar jamesstaff00000000000000import json import django from django.test import TestCase from django.core import serializers from django.utils.encoding import force_str from markupfield.markup import DEFAULT_MARKUP_TYPES from markupfield.fields import MarkupField, Markup, MarkupDescriptor from markupfield.widgets import MarkupTextarea, AdminMarkupTextareaWidget from markupfield.tests.models import ( Post, Article, Concrete, NullTestModel, DefaultTestModel, NullDefaultTestModel, ) from django.forms.models import modelform_factory ArticleForm = modelform_factory( Article, fields=[ "normal_field", "normal_field_markup_type", "markup_choices_field", "markup_choices_field_markup_type", "default_field", "default_field_markup_type", "markdown_field", ], ) 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 = force_str(self.rp.body.rendered) self.assertEqual(u, "

ReST

\n") def test_markup_bool(self): # ensure that __bool__ functions as expected p = Post( title="example empty", body="", body_markup_type="plain", comment=":)", comment_markup_type="markdown", ) assert bool(p.body) is False assert bool(p.comment) is True 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(force_str(p1.body), "

markdown

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

ReST

\n") def test_raw_assignment(self): self.rp.body.raw = "*ReST*" self.rp.save() self.assertEqual(force_str(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(force_str(self.rp.body), "

ReST

") # Serialization ########### def test_serialize_to_json(self): stream = serializers.serialize("json", Post.objects.all()[:3]) # 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(len(expected), len(actual)) if django.VERSION[0] < 3: expected[2]["fields"]["_comment_rendered"] = expected[2]["fields"][ "_comment_rendered" ].replace("x27", "39") for n, item in enumerate(expected): self.maxDiff = None self.assertEqual(item["fields"], actual[n]["fields"]) 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) def test_value_to_string(self): """ Ensure field converts to string during _meta access Other libraries (Django REST framework, etc) go directly to the field layer to serialize, which can cause a "unicode object has no property called 'raw'" error. This tests the bugfix. """ obj = self.rp field = self.rp._meta.get_field("body") self.assertNotEqual(field.value_to_string(obj), u"") # expected self.assertEqual(field.value_to_string(None), u"") # edge case # Other ################# def test_escape_html(self): # the rendered string has been escaped self.assertEqual(self.xss_post.comment.raw, self.xss_str) self.assertIn( force_str(self.xss_post.comment.rendered), ( "

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

", "

<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(force_str(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): for markup_type in DEFAULT_MARKUP_TYPES: rendered = markup_type[1]("test") self.assertTrue(hasattr(rendered, "__str__")) def test_plain_markup_urlize(self): for key, func, _ in DEFAULT_MARKUP_TYPES: if key != "plain": continue txt1 = "http://example.com some text" txt2 = "Some http://example.com text" txt3 = "Some text http://example.com" txt4 = "http://example.com. some text" txt5 = "Some http://example.com. text" txt6 = "Some text http://example.com." txt7 = ".http://example.com some text" txt8 = "Some .http://example.com text" txt9 = "Some text .http://example.com" self.assertEqual( func(txt1), '

http://example.com some text

', ) self.assertEqual( func(txt2), '

Some http://example.com text

', ) self.assertEqual( func(txt3), '

Some text http://example.com

', ) self.assertEqual( func(txt4), '

http://example.com. some text

', ) self.assertEqual( func(txt5), '

Some http://example.com. text

', ) self.assertEqual( func(txt6), '

Some text http://example.com.

', ) self.assertEqual(func(txt7), "

.http://example.com some text

") self.assertEqual(func(txt8), "

Some .http://example.com text

") self.assertEqual(func(txt9), "

Some text .http://example.com

") break 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): expected = ( '' ) 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(force_str(af["normal_field"]), expected) 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): # This function primarily tests the choices available to the widget. # By introducing titled markups (as third element in the markup_choices # tuples), this function also shows the backwards compatibility to the # old 2-tuple style and, by checking for the title of the 'fancy' # markup in the second test, also for the correkt title to the widget # choices. 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"), ("fancy", "Some fancy Markup"), ], ) 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"), request=None ).widget, AdminMarkupTextareaWidget, ) ) class MarkupFieldFormSaveTests(TestCase): def setUp(self): self.data = { "title": "example post", "body": "**markdown**", "body_markup_type": "markdown", } self.form_class = modelform_factory( Post, fields=["title", "body", "body_markup_type"] ) def test_form_create(self): form = self.form_class(self.data) form.save() actual = Post.objects.get(title=self.data["title"]) self.assertEquals(actual.body.raw, self.data["body"]) def test_form_update(self): existing = Post.objects.create( title=self.data["title"], body=self.data["body"], body_markup_type="markdown", ) update = { "title": "New title", "body": "**different markdown**", "body_markup_type": "markdown", } form = self.form_class(update, instance=existing) form.save() actual = Post.objects.get(title=update["title"]) self.assertEquals(actual.body.raw, update["body"]) class MarkupFieldLocalFileTestCase(TestCase): def test_no_raw(self): for markup_opt in DEFAULT_MARKUP_TYPES: if markup_opt[0] == "restructuredtext": render_rest = markup_opt[1] body = render_rest(".. raw:: html\n :file: AUTHORS.txt") self.assertNotIn("James Turk", body) class MarkupWidgetRenderTestCase(TestCase): def test_model_admin_render(self): from django.utils.translation import gettext_lazy as _ w = AdminMarkupTextareaWidget() assert w.render(_("body"), _("Body")) class NullTestCase(TestCase): def test_default_null_save(self): m = NullTestModel() m.save() self.assertEqual(force_str(m.text), "") self.assertIsNone(m.text.raw) self.assertIsNone(m.text.rendered) class DefaultTestCase(TestCase): def test_default_value_rendered(self): m = DefaultTestModel() m.save() self.assertEqual( m._meta.get_field("_text_rendered").default, m._meta.get_field("text").default, ) self.assertEqual(m._text_rendered, "

nice

") def test_default_text_save(self): m = DefaultTestModel() m.save() self.assertEqual(force_str(m.text), "

nice

") def test_assign_none(self): m = DefaultTestModel() m.save() self.assertEqual(force_str(m.text), "

nice

") m.text.raw = None m.save() self.assertEqual(force_str(m.text), "") self.assertIsNone(m.text.raw) self.assertIsNone(m.text.rendered) class NullDefaultTestCase(TestCase): def test_default_value_rendered(self): m = NullDefaultTestModel() m.save() self.assertEqual( m._meta.get_field("_text_rendered").default, m._meta.get_field("text").default, ) self.assertEqual(m._text_rendered, "

nice

") class MarkupDescriptorTestCase(TestCase): def test_class_access_returns_descriptor(self): """ Accessing a markup field through the class returns the descriptor instance for the field. (Regression test for issue #50.) """ self.assertIsInstance(Post.body, MarkupDescriptor) self.assertIs(Post._meta.get_field("body"), Post.body.field) django-markupfield-2.0.1/markupfield/__init__.py0000644000076600000240000000002613605410651022113 0ustar jamesstaff00000000000000__version__ = "2.0.0" django-markupfield-2.0.1/markupfield/markup.py0000644000076600000240000000567513605410547021676 0ustar jamesstaff00000000000000from functools import partial from django.utils.html import escape, linebreaks, urlize from django.utils.translation import pgettext_lazy as _ from django.conf import settings # build DEFAULT_MARKUP_TYPES DEFAULT_MARKUP_TYPES = [ ("html", lambda markup: markup, _("django-markupfield", "HTML")), ( "plain", lambda markup: linebreaks(urlize(escape(markup))), _("django-markupfield", "Plain"), ), ] 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 = partial( markdown.markdown, extensions=[makeExtension(css_class="highlight")] ) except ImportError: pass # whichever markdown_filter was available DEFAULT_MARKUP_TYPES.append( ("markdown", md_filter, _("django-markupfield", "Markdown")) ) 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", {}) overrides.update({"raw_enabled": False, "file_insertion_enabled": False}) parts = publish_parts( source=markup, writer_name="html4css1", settings_overrides=overrides ) return parts["fragment"] DEFAULT_MARKUP_TYPES.append( ("restructuredtext", render_rest, _("django-markupfield", "Restructured Text")) ) except ImportError: pass django-markupfield-2.0.1/markupfield/widgets.py0000644000076600000240000000065613605410525022033 0ustar jamesstaff00000000000000from django import forms from django.contrib.admin.widgets import AdminTextareaWidget class MarkupTextarea(forms.widgets.Textarea): def render(self, name, value, attrs=None, renderer=None): if hasattr(value, "raw"): value = value.raw return super(MarkupTextarea, self).render(name, value, attrs, renderer=renderer) class AdminMarkupTextareaWidget(MarkupTextarea, AdminTextareaWidget): pass django-markupfield-2.0.1/django_markupfield.egg-info/0000755000076600000240000000000014135623342023022 5ustar jamesstaff00000000000000django-markupfield-2.0.1/django_markupfield.egg-info/PKG-INFO0000644000076600000240000002200314135623342024114 0ustar jamesstaff00000000000000Metadata-Version: 1.1 Name: django-markupfield Version: 2.0.1 Summary: Custom Django field for easy use of markup in text fields Home-page: https://github.com/jamesturk/django-markupfield/ Author: James Turk Author-email: dev@jamesturk.net License: BSD License Description: ================== django-markupfield ================== .. image:: https://github.com/jamesturk/django-markupfield/workflows/Test/badge.svg .. image:: https://img.shields.io/pypi/v/django-markupfield.svg :target: https://pypi.python.org/pypi/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``. However, to use titled markup you either add ``'markupfield'`` to your ``INSTALLED_APPS`` or add the corresponding translations to your project translation. Requirements ------------ Requires Django >= 2.2 and 3.6+ * 1.5.x is the last release to officially support Django < 2.2 or Python 2.7 * 1.4.x is the last release to officially support Django < 1.11 * 1.3.x is the last release to officially support Django 1.4 or Python 3.3 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 `markdown`_ is installed) restructuredtext: default `ReST`_ renderer (only if `docutils`_ 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. .. _`ReST`: http://docutils.sourceforge.net/rst.html .. _`markdown`: https://pypi.python.org/pypi/Markdown .. _`docutils`: http://docutils.sourceforge.net/ 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 ReST and not provide a choice on forms:: MarkupField(markup_type='restructuredtext') ``MarkupField`` that will use a custom set of renderers:: CUSTOM_RENDERERS = ( ('markdown', markdown.markdown), ('wiki', my_wiki_render_func) ) MarkupField(markup_choices=CUSTOM_RENDERERS) .. note:: When using ``markdown``, be sure to use ``markdown.markdown`` and not the ``markdown.Markdown`` class, the class requires an explicit ``reset`` to function properly in some cases. (See [issue #40](https://github.com/jamesturk/django-markupfield/issues/40) for details.) 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``. .. important:: Keeping in mind that ``body`` is MarkupField instance is particullary important with ``default`` or ``default_if_none`` filter for model that could be blank. If ``body``'s ``rendered`` is ``None`` or empty string (``""``) these filters will *not* evaluate ``body`` as falsy to display default text:: {{ a.body|default:"" }} That's because ``body`` is regular non-``None`` MarkupField instance. To let ``default`` or ``default_if_none`` filters to work evaluate ``rendered`` MarkupField attribute instead. To prevent escaping HTML for the case ``rendered`` is truethy, finish chain with ``safe`` filter:: {{ a.body.rendered|default:""|safe }} .. 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 :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Environment :: Web Environment django-markupfield-2.0.1/django_markupfield.egg-info/SOURCES.txt0000644000076600000240000000101314135623342024701 0ustar jamesstaff00000000000000AUTHORS.txt LICENSE MANIFEST.in README.rst setup.cfg setup.py django_markupfield.egg-info/PKG-INFO django_markupfield.egg-info/SOURCES.txt django_markupfield.egg-info/dependency_links.txt django_markupfield.egg-info/top_level.txt markupfield/__init__.py markupfield/fields.py markupfield/markup.py markupfield/widgets.py markupfield/locale/de/LC_MESSAGES/django.mo markupfield/locale/de/LC_MESSAGES/django.po markupfield/tests/__init__.py markupfield/tests/models.py markupfield/tests/settings.py markupfield/tests/tests.pydjango-markupfield-2.0.1/django_markupfield.egg-info/top_level.txt0000644000076600000240000000001414135623342025547 0ustar jamesstaff00000000000000markupfield django-markupfield-2.0.1/django_markupfield.egg-info/dependency_links.txt0000644000076600000240000000000114135623342027070 0ustar jamesstaff00000000000000