django-markupfield-1.4.0/0000755000076500000240000000000012634651305015506 5ustar jamesstaff00000000000000django-markupfield-1.4.0/AUTHORS.txt0000644000076500000240000000060512600715255017373 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-1.4.0/CHANGELOG0000644000076500000240000000510612634651242016722 0ustar jamesstaff000000000000001.4.0 - 17 December 2015 ========================= - bugfixes for Django 1.9 - drop support for deprecated Django versions 1.3.5 - 21 May 2015 =================== - properly handle null=True 1.3.4 - 29 April 2015 ===================== - Fix for an issue where __proxy__ objects interfere w/ widget rendering. 1.3.3 - 21 April 2015 ===================== - Add test for issue fixed in last release: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-0846 Further documented here: https://www.djangoproject.com/weblog/2015/apr/21/docutils-security-advisory/ 1.3.2 - 16 April 2015 ===================== - Fix for an issue with ReST, thanks to Markus Holtermann 1.3.1 - 15 April 2015 ===================== - Fix translation support to be lazy, thanks to Michael Kutý 1.3.0 - 22 February 2015 ======================== - Django 1.7 migration support, thanks to Frank Wiles - dedicated option for titles in markup choice field thanks to Markus Holtermann - fix for latest version of markdown library 1.2.1 - 10 July 2014 ==================== - value_to_string check, fixing #16 - urllize & linebreak fix, fixing #18 - fix for __unicode__, fixing #9 1.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.4.0/django_markupfield.egg-info/0000755000076500000240000000000012634651305023025 5ustar jamesstaff00000000000000django-markupfield-1.4.0/django_markupfield.egg-info/dependency_links.txt0000644000076500000240000000000112634651304027072 0ustar jamesstaff00000000000000 django-markupfield-1.4.0/django_markupfield.egg-info/pbr.json0000644000076500000240000000005712634651304024504 0ustar jamesstaff00000000000000{"is_release": false, "git_version": "13efc7b"}django-markupfield-1.4.0/django_markupfield.egg-info/PKG-INFO0000644000076500000240000001752612634651304024134 0ustar jamesstaff00000000000000Metadata-Version: 1.1 Name: django-markupfield Version: 1.4.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 ================== .. image:: https://travis-ci.org/jamesturk/django-markupfield.svg?branch=master :target: https://travis-ci.org/jamesturk/django-markupfield .. 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 >= 1.7 and Python 2.7 or 3.4+ (1.3 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 `python-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. .. _`markdown`: http://daringfireball.net/projects/markdown/ .. _`ReST`: http://docutils.sourceforge.net/rst.html .. _`python-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) 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.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Environment :: Web Environment django-markupfield-1.4.0/django_markupfield.egg-info/SOURCES.txt0000644000076500000240000000107212634651305024711 0ustar jamesstaff00000000000000AUTHORS.txt CHANGELOG 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/pbr.json 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-1.4.0/django_markupfield.egg-info/top_level.txt0000644000076500000240000000001412634651304025551 0ustar jamesstaff00000000000000markupfield django-markupfield-1.4.0/LICENSE0000644000076500000240000000250712600715255016515 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-1.4.0/MANIFEST.in0000644000076500000240000000013512600715255017241 0ustar jamesstaff00000000000000include README.rst include LICENSE include CHANGELOG include AUTHORS.txt include MANIFEST.in django-markupfield-1.4.0/markupfield/0000755000076500000240000000000012634651305020011 5ustar jamesstaff00000000000000django-markupfield-1.4.0/markupfield/__init__.py0000644000076500000240000000002612600725541022115 0ustar jamesstaff00000000000000__version__ = '1.4.0' django-markupfield-1.4.0/markupfield/fields.py0000644000076500000240000001611112634650764021641 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 smart_text 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): if self.rendered is None: return mark_safe('') return mark_safe(smart_text(self.rendered)) __str__ = __unicode__ 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.') 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) 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 value_to_string(self, obj): value = self._get_val_from_obj(obj) 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 from django.contrib.admin.options import FORMFIELD_FOR_DBFIELD_DEFAULTS FORMFIELD_FOR_DBFIELD_DEFAULTS[MarkupField] = { 'widget': widgets.AdminMarkupTextareaWidget} django-markupfield-1.4.0/markupfield/locale/0000755000076500000240000000000012634651305021250 5ustar jamesstaff00000000000000django-markupfield-1.4.0/markupfield/locale/de/0000755000076500000240000000000012634651305021640 5ustar jamesstaff00000000000000django-markupfield-1.4.0/markupfield/locale/de/LC_MESSAGES/0000755000076500000240000000000012634651305023425 5ustar jamesstaff00000000000000django-markupfield-1.4.0/markupfield/locale/de/LC_MESSAGES/django.mo0000644000076500000240000000145512600715255025227 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-1.4.0/markupfield/locale/de/LC_MESSAGES/django.po0000644000076500000240000000172712600715255025234 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-1.4.0/markupfield/markup.py0000644000076500000240000000571212600715255021665 0ustar jamesstaff00000000000000from django.utils.html import escape, linebreaks, urlize from django.utils.functional import curry 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 = curry(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-1.4.0/markupfield/tests/0000755000076500000240000000000012634651305021153 5ustar jamesstaff00000000000000django-markupfield-1.4.0/markupfield/tests/__init__.py0000644000076500000240000000000012600715255023250 0ustar jamesstaff00000000000000django-markupfield-1.4.0/markupfield/tests/models.py0000644000076500000240000000225612600723517023013 0ustar jamesstaff00000000000000from 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), ('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") django-markupfield-1.4.0/markupfield/tests/settings.py0000644000076500000240000000161712600720737023371 0ustar jamesstaff00000000000000import os import markdown from django.utils.html import escape, linebreaks, urlize from docutils.core import publish_parts 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-1.4.0/markupfield/tests/tests.py0000644000076500000240000003615212634650764022706 0ustar jamesstaff00000000000000from __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.markup import DEFAULT_MARKUP_TYPES from markupfield.fields import MarkupField, Markup from markupfield.widgets import MarkupTextarea, AdminMarkupTextareaWidget from markupfield.tests.models import Post, Article, Concrete, NullTestModel, DefaultTestModel 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 = 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()[: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)) 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.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): 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): 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): # 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')).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 ugettext_lazy as _ w = AdminMarkupTextareaWidget() assert w.render(_('body'), _('Body')) class NullTestCase(TestCase): def test_default_null_save(self): m = NullTestModel() m.save() self.assertEqual(smart_text(m.text), '') self.assertIsNone(m.text.raw) self.assertIsNone(m.text.rendered) class DefaultTestCase(TestCase): def test_default_text_save(self): m = DefaultTestModel() m.save() self.assertEqual(smart_text(m.text), "

nice

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

nice

") m.text.raw = None m.save() self.assertEqual(smart_text(m.text), '') self.assertIsNone(m.text.raw) self.assertIsNone(m.text.rendered) django-markupfield-1.4.0/markupfield/widgets.py0000644000076500000240000000061512600715255022031 0ustar jamesstaff00000000000000from django import forms from django.contrib.admin.widgets import AdminTextareaWidget class MarkupTextarea(forms.widgets.Textarea): def render(self, name, value, attrs=None): if hasattr(value, 'raw'): value = value.raw return super(MarkupTextarea, self).render(name, value, attrs) class AdminMarkupTextareaWidget(MarkupTextarea, AdminTextareaWidget): pass django-markupfield-1.4.0/PKG-INFO0000644000076500000240000001752612634651305016616 0ustar jamesstaff00000000000000Metadata-Version: 1.1 Name: django-markupfield Version: 1.4.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 ================== .. image:: https://travis-ci.org/jamesturk/django-markupfield.svg?branch=master :target: https://travis-ci.org/jamesturk/django-markupfield .. 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 >= 1.7 and Python 2.7 or 3.4+ (1.3 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 `python-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. .. _`markdown`: http://daringfireball.net/projects/markdown/ .. _`ReST`: http://docutils.sourceforge.net/rst.html .. _`python-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) 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.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Environment :: Web Environment django-markupfield-1.4.0/README.rst0000644000076500000240000001313712600725657017207 0ustar jamesstaff00000000000000================== django-markupfield ================== .. image:: https://travis-ci.org/jamesturk/django-markupfield.svg?branch=master :target: https://travis-ci.org/jamesturk/django-markupfield .. 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 >= 1.7 and Python 2.7 or 3.4+ (1.3 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 `python-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. .. _`markdown`: http://daringfireball.net/projects/markdown/ .. _`ReST`: http://docutils.sourceforge.net/rst.html .. _`python-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) 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.4.0/setup.cfg0000644000076500000240000000013012634651305017321 0ustar jamesstaff00000000000000[bdist_wheel] universal = 1 [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 django-markupfield-1.4.0/setup.py0000644000076500000240000000221412634651125017217 0ustar jamesstaff00000000000000from setuptools import setup long_description = open('README.rst').read() setup( name='django-markupfield', version="1.4.0", 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='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.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Environment :: Web Environment', ], )