├── .gitignore ├── .travis.yml ├── CHANGELOG ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── docs ├── Makefile ├── changelog.rst ├── conf.py ├── filters.rst ├── index.rst ├── installation.rst ├── make.bat ├── managing_blocks.rst └── templates.rst ├── pytest.ini ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── conftest.py ├── filters │ ├── __init__.py │ ├── test_markdown.py │ └── test_uploaded_file.py ├── settings.py ├── test_alternate_text.py ├── test_auth.py ├── test_bad_syntax.py ├── test_caching.py ├── test_content_name.py ├── test_custom_filters.py ├── test_custom_verbose_name.py ├── test_html.py ├── test_models.py ├── test_simple.py ├── urls.py └── utils.py ├── tinycontent ├── __init__.py ├── admin.py ├── apps.py ├── conf.py ├── filters │ ├── __init__.py │ ├── builtin.py │ └── md.py ├── locale │ └── pl │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── migrations │ ├── 0001_initial.py │ ├── 0002_tinycontentfileupload.py │ ├── 0003_update_slug.py │ ├── 0004_py3_compatible.py │ └── __init__.py ├── models.py ├── templates │ └── tinycontent │ │ ├── tinycontent.html │ │ └── tinycontent_add.html ├── templatetags │ ├── __init__.py │ └── tinycontent_tags.py └── utils │ ├── __init__.py │ ├── file_uploads.py │ └── importer.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | django_tinycontent.egg-info/ 3 | docs/_build/ 4 | dist/ 5 | *.egg 6 | .eggs/ 7 | pep8.txt 8 | coverage.xml 9 | .tox/ 10 | build/ 11 | .coverage 12 | test.db 13 | tests/testmedia/ 14 | .cache/ 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3.5 3 | 4 | sudo: false 5 | 6 | cache: 7 | directories: 8 | - $HOME/.pip-cache/ 9 | 10 | matrix: 11 | include: 12 | - env: TOX_ENV=py36-master 13 | python: 3.6 14 | - env: TOX_ENV=py36-2.0.x 15 | python: 3.6 16 | - env: TOX_ENV=py36-2.1.x 17 | python: 3.6 18 | - env: TOX_ENV=py36-2.2.x 19 | python: 3.6 20 | - env: TOX_ENV=py36-3.0.x 21 | python: 3.6 22 | - env: TOX_ENV=py37-master 23 | python: 3.7 24 | - env: TOX_ENV=py37-2.0.x 25 | python: 3.7 26 | - env: TOX_ENV=py37-2.1.x 27 | python: 3.7 28 | - env: TOX_ENV=py37-2.2.x 29 | python: 3.7 30 | - env: TOX_ENV=py37-3.0.x 31 | python: 3.7 32 | - env: TOX_ENV=py38-master 33 | python: 3.8 34 | - env: TOX_ENV=py38-2.0.x 35 | python: 3.8 36 | - env: TOX_ENV=py38-2.1.x 37 | python: 3.8 38 | - env: TOX_ENV=py38-2.2.x 39 | python: 3.8 40 | - env: TOX_ENV=py38-3.0.x 41 | python: 3.8 42 | allow_failures: 43 | - env: TOX_ENV=py35-master 44 | - env: TOX_ENV=py36-master 45 | python: 3.6 46 | - env: TOX_ENV=py37-master 47 | python: 3.7 48 | - env: TOX_ENV=py38-master 49 | python: 3.8 50 | fast_finish: true 51 | 52 | install: 53 | - pip install tox coveralls --cache-dir $HOME/.pip-cache 54 | 55 | script: 56 | - tox -e $TOX_ENV 57 | 58 | after_success: 59 | coveralls 60 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | v0.7.1 2 | ------ 3 | 4 | * Added migration required for Python 3 (thanks @markus-hinsche). 5 | 6 | v0.7.0 7 | ------ 8 | 9 | * Compatibility changes for Django 1.11 - dropped support for 10 | versions of Django earlier than 1.8, and Python 3.4 (Python 2.7 and 11 | Python 3.5 are still supported). 12 | 13 | v0.6.1 14 | ------ 15 | 16 | * Modify cache name, to prevent warnings for non-ASCII characters or 17 | whitespace (thanks @ad-m). 18 | 19 | v0.6.0 20 | ------ 21 | 22 | * Compatibility changes for Django 1.10. 23 | 24 | v0.5.1 25 | ------ 26 | 27 | * Added a Polish translation and locale (thanks @ad-m). 28 | 29 | v0.5.0 30 | ------ 31 | 32 | * Add support for multiple arguments to both the ``tinycontent`` and 33 | the ``tinycontent_simple`` template tags. See the documentation 34 | about :ref:`multiple-arguments`. 35 | * Start caching database queries - fetching a TinyContent block by 36 | name (as the template tags do), will only hit the database the 37 | first time that content block is loaded (unless the content block 38 | is changed). 39 | 40 | v0.4.0 41 | ------ 42 | 43 | * Require at least django-autoslug 1.8.0, to fix a warning about 44 | unapplied migrations. 45 | 46 | v0.3.0 47 | ------ 48 | 49 | * Drop support for Django 1.4 (it's quite hard to support Django 1.4 50 | and 1.9 in a single release - since Django 1.4 requires ``{% load 51 | url from future %}``, and Django 1.9 doesn't support it). 52 | * Ensure the wheel we upload to PyPI is universal. 53 | * Forward compatibility for Django 1.9 - remove the ``{% load url 54 | from future %}`` from tinycontent templates. 55 | 56 | v0.2.1 57 | ------ 58 | 59 | * Forwards compatibility change for Django 1.9 - which will remove 60 | the version of ``importlib`` bundled with Django. All supported 61 | versions of Python (2.7, 3.3 and 3.4) have ``importlib``. 62 | 63 | v0.2.0 64 | ------ 65 | 66 | * Dropped support for Python 2.6. 67 | * Added a built-in markdown filter - you can use it by setting 68 | ``TINYCONTENT_FILTER`` to 69 | ``'tinycontent.filters.md.markdown_filter'``. 70 | * Added the ability to include links to files which you can upload 71 | via the admin. 72 | * Added support for setting ``TINYCONTENT_FILTER`` to a list of 73 | dotted paths, to allow chaining filters. 74 | 75 | v0.1.8 76 | ------ 77 | 78 | * Added the ``TINYCONTENT_FILTER`` setting for controlling the way 79 | content is output. 80 | * Improved testing with Travis (we now test all supported Python 81 | versions and Django versions). 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Dominic Rodger 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | * Neither the name of tinycontent nor the names of its contributors 14 | may be used to endorse or promote products derived from this software 15 | without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 21 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 22 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 23 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 24 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 25 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 26 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | recursive-include tinycontent/templates * 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean docs lint test testall coverage release sdist 2 | 3 | help: 4 | @echo "clean - remove build and Python artifacts" 5 | @echo "docs - build the documentation" 6 | @echo "lint - run flake8" 7 | @echo "test - run tests quickly with the default Python" 8 | @echo "testall - run tests on every Python version with tox" 9 | @echo "coverage - check code coverage quickly with the default Python" 10 | @echo "release - package and upload a release" 11 | @echo "sdist - package" 12 | 13 | clean: 14 | rm -rf build/ 15 | rm -rf dist/ 16 | rm -rf *.egg-info/ 17 | rm -rf *.egg/ 18 | find . -name '*.pyc' -exec rm -f {} + 19 | find . -name '*.pyo' -exec rm -f {} + 20 | find . -name '*~' -exec rm -f {} + 21 | rm -f pep8.txt 22 | rm -f coverage.xml 23 | rm -rf __pycache__ 24 | 25 | lint: 26 | flake8 tinycontent tests 27 | 28 | test: 29 | python setup.py test 30 | 31 | testall: 32 | tox 33 | 34 | coverage: 35 | coverage run --source tinycontent setup.py test 36 | coverage report -m 37 | 38 | release: clean lint testall 39 | twine upload -r pypi dist/* 40 | 41 | sdist: clean 42 | python setup.py sdist 43 | ls -l dist 44 | 45 | docs: 46 | tox -e docs 47 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ***** 2 | WARNING: This code is entirely unsupported, 0.9.0 will be the last 3 | release of django-tinycontent. 4 | ***** 5 | 6 | django-tinycontent is a simple Django application for re-usable 7 | content blocks, much like django-boxes. 8 | 9 | Installation is simple:: 10 | 11 | pip install django-tinycontent 12 | 13 | Add ``tinycontent`` to your ``INSTALLED_APPS``. 14 | 15 | Usage in templates is simple:: 16 | 17 | {% load tinycontent_tags %} 18 | 19 | {% tinycontent_simple 'content_name' %} 20 | 21 | Or, to specify a value if a content block by the given name cannot be 22 | found, use:: 23 | 24 | {% load tinycontent_tags %} 25 | 26 | {% tinycontent 'content_name' %} 27 | This will be shown if no matching object is found. 28 | {% endtinycontent %} 29 | 30 | The name of the content block can also be a context variable, using 31 | both the simple and the complex variants. 32 | 33 | Content blocks themselves can be added and edited using Django's admin 34 | interface. If a block with the name given in the template tag cannot 35 | be found, either nothing is rendered (if using 36 | ``tinycontent_simple``), or the text between ``tinycontent`` and 37 | ``endtinycontent`` is rendered (if using the more complex variant). 38 | 39 | To apply custom filters to your content, set ``TINYCONTENT_FILTER`` to 40 | a dotted path to a callable that takes the raw content and returns the 41 | transformed content. You can also set ``TINYCONTENT_FILTER`` to be a 42 | list of dotted paths to callables, to chain filters together. 43 | 44 | django-tinycontent supports all versions of Django from 2.0 to 45 | 3.0. Python 3.6, 3.7 and 3.8 are supported. 46 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-tinycontent.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-tinycontent.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-tinycontent" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-tinycontent" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | Release Notes 2 | ============= 3 | 4 | v0.9.0 5 | ------ 6 | 7 | * Update for recent Django versions (thanks to @ataylor32). 8 | 9 | v0.8.0 10 | ------ 11 | 12 | * Update for recent Django versions, remove support for older 13 | Django/Python versions. Supported Django versions are now 2.0 to 14 | 3.0, supported Python versions are Python 3.6, 3.7 and 3.8 (thanks 15 | @ad-m). 16 | 17 | v0.7.1 18 | ------ 19 | 20 | * Added migration required for Python 3 (thanks @markus-hinsche). 21 | 22 | v0.7.0 23 | ------ 24 | 25 | * Compatibility changes for Django 1.11 - dropped support for 26 | versions of Django earlier than 1.8, and Python 3.4 (Python 2.7 and 27 | Python 3.5 are still supported). 28 | 29 | v0.6.1 30 | ------ 31 | 32 | * Modify cache name, to prevent warnings for non-ASCII characters or 33 | whitespace (thanks @ad-m). 34 | 35 | v0.6.0 36 | ------ 37 | 38 | * Compatibility changes for Django 1.10. 39 | 40 | v0.5.1 41 | ------ 42 | 43 | * Added a Polish translation and locale (thanks @ad-m). 44 | 45 | v0.5.0 46 | ------ 47 | 48 | * Add support for multiple arguments to both the ``tinycontent`` and 49 | the ``tinycontent_simple`` template tags. See the documentation 50 | about :ref:`multiple-arguments`. 51 | * Start caching database queries - fetching a TinyContent block by 52 | name (as the template tags do), will only hit the database the 53 | first time that content block is loaded (unless the content block 54 | is changed). 55 | 56 | v0.4.0 57 | ------ 58 | 59 | * Require at least django-autoslug 1.8.0, to fix a warning about 60 | unapplied migrations. 61 | 62 | v0.3.0 63 | ------ 64 | 65 | * Drop support for Django 1.4 (it's quite hard to support Django 1.4 66 | and 1.9 in a single release - since Django 1.4 requires ``{% load 67 | url from future %}``, and Django 1.9 doesn't support it). 68 | * Ensure the wheel we upload to PyPI is universal. 69 | * Forward compatibility for Django 1.9 - remove the ``{% load url 70 | from future %}`` from tinycontent templates. 71 | 72 | v0.2.1 73 | ------ 74 | 75 | * Forwards compatibility change for Django 1.9 - which will remove 76 | the version of ``importlib`` bundled with Django. All supported 77 | versions of Python (2.7, 3.3 and 3.4) have ``importlib``. 78 | 79 | v0.2.0 80 | ------ 81 | 82 | * Dropped support for Python 2.6. 83 | * Added a built-in markdown filter - you can use it by setting 84 | ``TINYCONTENT_FILTER`` to 85 | ``'tinycontent.filters.md.markdown_filter'``. 86 | * Added the ability to include links to files which you can upload 87 | via the admin. 88 | * Added support for setting ``TINYCONTENT_FILTER`` to a list of 89 | dotted paths, to allow chaining filters. 90 | 91 | v0.1.8 92 | ------ 93 | 94 | * Added the ``TINYCONTENT_FILTER`` setting for controlling the way 95 | content is output. 96 | * Improved testing with Travis (we now test all supported Python 97 | versions and Django versions). 98 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # 3 | # django-tinycontent documentation build configuration file, created by 4 | # sphinx-quickstart on Fri Jan 4 17:20:42 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | from datetime import date 15 | import sys, os 16 | 17 | # If extensions (or modules to document with autodoc) are in another directory, 18 | # add these directories to sys.path here. If the directory is relative to the 19 | # documentation root, use os.path.abspath to make it absolute, like shown here. 20 | # sys.path.insert(0, os.path.abspath('.')) 21 | 22 | # -- General configuration ----------------------------------------------------- 23 | 24 | # If your documentation needs a minimal Sphinx version, state it here. 25 | # needs_sphinx = '1.0' 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be extensions 28 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 29 | extensions = [] 30 | 31 | # Add any paths that contain templates here, relative to this directory. 32 | templates_path = ["_templates"] 33 | 34 | # The suffix of source filenames. 35 | source_suffix = ".rst" 36 | 37 | # The encoding of source files. 38 | # source_encoding = 'utf-8-sig' 39 | 40 | # The master toctree document. 41 | master_doc = "index" 42 | 43 | # General information about the project. 44 | project = "django-tinycontent" 45 | copyright = "2013 - %d, Dominic Rodger" % date.today().year 46 | 47 | # The version info for the project you're documenting, acts as replacement for 48 | # |version| and |release|, also used in various other places throughout the 49 | # built documents. 50 | # 51 | # The short X.Y version. 52 | version = "0.9.0" 53 | # The full version, including alpha/beta/rc tags. 54 | release = "0.9.0" 55 | 56 | # The language for content autogenerated by Sphinx. Refer to documentation 57 | # for a list of supported languages. 58 | # language = None 59 | 60 | # There are two options for replacing |today|: either, you set today to some 61 | # non-false value, then it is used: 62 | # today = '' 63 | # Else, today_fmt is used as the format for a strftime call. 64 | # today_fmt = '%B %d, %Y' 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | exclude_patterns = ["_build"] 69 | 70 | # The reST default role (used for this markup: `text`) to use for all documents. 71 | # default_role = None 72 | 73 | # If true, '()' will be appended to :func: etc. cross-reference text. 74 | # add_function_parentheses = True 75 | 76 | # If true, the current module name will be prepended to all description 77 | # unit titles (such as .. function::). 78 | # add_module_names = True 79 | 80 | # If true, sectionauthor and moduleauthor directives will be shown in the 81 | # output. They are ignored by default. 82 | # show_authors = False 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | pygments_style = "sphinx" 86 | 87 | # A list of ignored prefixes for module index sorting. 88 | # modindex_common_prefix = [] 89 | 90 | 91 | # -- Options for HTML output --------------------------------------------------- 92 | 93 | on_rtd = os.environ.get("READTHEDOCS", None) == "True" 94 | 95 | # The theme to use for HTML and HTML Help pages. See the documentation for 96 | # a list of builtin themes. 97 | if not on_rtd: 98 | html_theme = "alabaster" 99 | 100 | # Theme options are theme-specific and customize the look and feel of a theme 101 | # further. For a list of options available for each theme, see the 102 | # documentation. 103 | # html_theme_options = {} 104 | 105 | # Add any paths that contain custom themes here, relative to this directory. 106 | # html_theme_path = [] 107 | 108 | # The name for this set of Sphinx documents. If None, it defaults to 109 | # " v documentation". 110 | # html_title = None 111 | 112 | # A shorter title for the navigation bar. Default is the same as html_title. 113 | # html_short_title = None 114 | 115 | # The name of an image file (relative to this directory) to place at the top 116 | # of the sidebar. 117 | # html_logo = None 118 | 119 | # The name of an image file (within the static path) to use as favicon of the 120 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 121 | # pixels large. 122 | # html_favicon = None 123 | 124 | # Add any paths that contain custom static files (such as style sheets) here, 125 | # relative to this directory. They are copied after the builtin static files, 126 | # so a file named "default.css" will overwrite the builtin "default.css". 127 | # html_static_path = ['_static'] 128 | 129 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 130 | # using the given strftime format. 131 | # html_last_updated_fmt = '%b %d, %Y' 132 | 133 | # If true, SmartyPants will be used to convert quotes and dashes to 134 | # typographically correct entities. 135 | # html_use_smartypants = True 136 | 137 | # Custom sidebar templates, maps document names to template names. 138 | # html_sidebars = {} 139 | 140 | # Additional templates that should be rendered to pages, maps page names to 141 | # template names. 142 | # html_additional_pages = {} 143 | 144 | # If false, no module index is generated. 145 | # html_domain_indices = True 146 | 147 | # If false, no index is generated. 148 | # html_use_index = True 149 | 150 | # If true, the index is split into individual pages for each letter. 151 | # html_split_index = False 152 | 153 | # If true, links to the reST sources are added to the pages. 154 | # html_show_sourcelink = True 155 | 156 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 157 | # html_show_sphinx = True 158 | 159 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 160 | # html_show_copyright = True 161 | 162 | # If true, an OpenSearch description file will be output, and all pages will 163 | # contain a tag referring to it. The value of this option must be the 164 | # base URL from which the finished HTML is served. 165 | # html_use_opensearch = '' 166 | 167 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 168 | # html_file_suffix = None 169 | 170 | # Output file base name for HTML help builder. 171 | htmlhelp_basename = "django-tinycontentdoc" 172 | 173 | 174 | # -- Options for LaTeX output -------------------------------------------------- 175 | 176 | latex_elements = { 177 | # The paper size ('letterpaper' or 'a4paper'). 178 | # 'papersize': 'letterpaper', 179 | # The font size ('10pt', '11pt' or '12pt'). 180 | # 'pointsize': '10pt', 181 | # Additional stuff for the LaTeX preamble. 182 | # 'preamble': '', 183 | } 184 | 185 | # Grouping the document tree into LaTeX files. List of tuples 186 | # (source start file, target name, title, author, documentclass [howto/manual]). 187 | latex_documents = [ 188 | ( 189 | "index", 190 | "django-tinycontent.tex", 191 | "django-tinycontent Documentation", 192 | "Dominic Rodger", 193 | "manual", 194 | ) 195 | ] 196 | 197 | # The name of an image file (relative to this directory) to place at the top of 198 | # the title page. 199 | # latex_logo = None 200 | 201 | # For "manual" documents, if this is true, then toplevel headings are parts, 202 | # not chapters. 203 | # latex_use_parts = False 204 | 205 | # If true, show page references after internal links. 206 | # latex_show_pagerefs = False 207 | 208 | # If true, show URL addresses after external links. 209 | # latex_show_urls = False 210 | 211 | # Documents to append as an appendix to all manuals. 212 | # latex_appendices = [] 213 | 214 | # If false, no module index is generated. 215 | # latex_domain_indices = True 216 | 217 | 218 | # -- Options for manual page output -------------------------------------------- 219 | 220 | # One entry per manual page. List of tuples 221 | # (source start file, name, description, authors, manual section). 222 | man_pages = [ 223 | ( 224 | "index", 225 | "django-tinycontent", 226 | "django-tinycontent Documentation", 227 | ["Dominic Rodger"], 228 | 1, 229 | ) 230 | ] 231 | 232 | # If true, show URL addresses after external links. 233 | # man_show_urls = False 234 | 235 | 236 | # -- Options for Texinfo output ------------------------------------------------ 237 | 238 | # Grouping the document tree into Texinfo files. List of tuples 239 | # (source start file, target name, title, author, 240 | # dir menu entry, description, category) 241 | texinfo_documents = [ 242 | ( 243 | "index", 244 | "django-tinycontent", 245 | "django-tinycontent Documentation", 246 | "Dominic Rodger", 247 | "django-tinycontent", 248 | "One line description of project.", 249 | "Miscellaneous", 250 | ) 251 | ] 252 | 253 | # Documents to append as an appendix to all manuals. 254 | # texinfo_appendices = [] 255 | 256 | # If false, no module index is generated. 257 | # texinfo_domain_indices = True 258 | 259 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 260 | # texinfo_show_urls = 'footnote' 261 | -------------------------------------------------------------------------------- /docs/filters.rst: -------------------------------------------------------------------------------- 1 | .. _filters: 2 | 3 | Filters 4 | ======= 5 | 6 | By default, no transformations are applied to the content blocks - 7 | they're just displayed as they were entered in the admin. Since you 8 | probably want to display HTML, you'll probably want to set up a 9 | filter to apply before displaying content blocks, such as Markdown. 10 | 11 | .. contents:: 12 | :local: 13 | 14 | Specifying Filters 15 | ------------------ 16 | 17 | You can configure what filter is applied using the setting 18 | ``TINYCONTENT_FILTER``, which should be set to a dotted path to a 19 | function to call to filter the content (for example, to convert 20 | Markdown to HTML). 21 | 22 | .. warning:: 23 | 24 | If the given path is invalid, any use of tinycontent tags will 25 | raise ``ImproperlyConfigured``. If this setting is not provided, 26 | the content will be returned exactly as stored. 27 | 28 | For example, if your project has a file called ``utils.py``, you might 29 | have a function in it called ``tinycontent_transform`` that would look 30 | something like this:: 31 | 32 | def tinycontent_transform(content): 33 | return do_something_to(content) 34 | 35 | To get the tinycontent templates to use that function, in your 36 | ``settings.py`` file, you'd write something like:: 37 | 38 | TINYCONTENT_FILTER = 'myproj.utils.tinycontent_transform' 39 | 40 | Chaining Filters 41 | ---------------- 42 | 43 | You can optionally set ``TINYCONTENT_FILTER`` to a list of dotted 44 | paths - filters will be applied in the order in which you provide 45 | them. 46 | 47 | For example, to use :ref:`markdown-filter` with tinycontent's 48 | :ref:`built-in file support `, you could set 49 | ``TINYCONTENT_FILTER`` like this:: 50 | 51 | TINYCONTENT_FILTER = [ 52 | 'tinycontent.filters.md.markdown_filter', 53 | 'tinycontent.filters.builtin.uploaded_file_filter', 54 | ] 55 | 56 | Built-in Filters 57 | ---------------- 58 | 59 | .. _markdown-filter: 60 | 61 | Markdown 62 | ^^^^^^^^ 63 | 64 | django-tinycontent ships with a filter for Markdown. You can enable 65 | this by setting ``TINYCONTENT_FILTER`` like this:: 66 | 67 | TINYCONTENT_FILTER = 'tinycontent.filters.md.markdown_filter' 68 | 69 | .. _file-filter: 70 | 71 | File-upload Handler 72 | ^^^^^^^^^^^^^^^^^^^ 73 | 74 | The file-upload filter replaces instances of ``@file:slug`` (where 75 | ``slug`` is the slug of a TinyContentFileUpload) with the URL to the 76 | file. 77 | 78 | You can enable this filter by setting ``TINYCONTENT_FILTER`` like 79 | this:: 80 | 81 | TINYCONTENT_FILTER = 'tinycontent.filters.builtin.uploaded_file_filter' 82 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | django-tinycontent 2 | ****************** 3 | 4 | django-tinycontent is a simple Django application for re-usable 5 | content blocks, much like `django-boxes`_. 6 | 7 | Blocks are configured via the Django admin, and are available to use 8 | in templates:: 9 | 10 | {% load tinycontent_tags %} 11 | 12 | {% tinycontent_simple 'welcome' %} 13 | 14 | That template fragment will look for a content block called 15 | ``welcome``, and display the content of it. 16 | 17 | Optionally, you can post-process the output with :ref:`filters`. 18 | 19 | 20 | Contents 21 | ======== 22 | 23 | .. toctree:: 24 | :maxdepth: 2 25 | 26 | installation 27 | templates 28 | managing_blocks 29 | filters 30 | changelog 31 | 32 | .. _django-boxes: https://github.com/eldarion/django-boxes 33 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ------------ 3 | 4 | Installation is simple:: 5 | 6 | pip install django-tinycontent 7 | 8 | Then, add ``tinycontent`` to your ``INSTALLED_APPS``. 9 | 10 | Version Support 11 | --------------- 12 | 13 | Python 2.7, 3.4 and 3.5 are supported. Django versions from 1.5 14 | upwards are supported with Python 2.7 and 3.4. Django versions from 15 | 1.8 upwards are supported with Python 3.5. 16 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-tinycontent.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-tinycontent.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /docs/managing_blocks.rst: -------------------------------------------------------------------------------- 1 | Adding and Editing Blocks 2 | ------------------------- 3 | 4 | Content blocks themselves can be added and edited using Django's 5 | admin interface. If a block with the name given in the template tag 6 | cannot be found, either nothing is rendered (if using 7 | ``tinycontent_simple``), or the text between ``tinycontent`` and 8 | ``endtinycontent`` is rendered (if using the more complex variant). 9 | 10 | If you're logged in as a user with permission to add or edit content 11 | blocks (which you can set via permissions in the Django admin), 12 | you'll see links to the admin page for adding blocks (if there's no 13 | block set up yet) or editing (if the content block already exists). 14 | -------------------------------------------------------------------------------- /docs/templates.rst: -------------------------------------------------------------------------------- 1 | Using Blocks in Templates 2 | ========================= 3 | 4 | Usage in templates is simple - to show the content of a block called 5 | ``content_name`` you can just use the template tag 6 | ``tinycontent_simple``:: 7 | 8 | {% load tinycontent_tags %} 9 | 10 | {% tinycontent_simple 'content_name' %} 11 | 12 | Or, to specify a value if a content block by the given name cannot be 13 | found, use the ``tinycontent`` tag:: 14 | 15 | {% load tinycontent_tags %} 16 | 17 | {% tinycontent 'content_name' %} 18 | This will be shown if no matching object is found. 19 | {% endtinycontent %} 20 | 21 | The name of the content block can also be a context variable, using 22 | both the simple and the complex variants. 23 | 24 | Optionally, you can post-process the output with :ref:`filters`. 25 | 26 | .. _multiple-arguments: 27 | 28 | Passing Multiple Arguments 29 | -------------------------- 30 | 31 | .. versionadded:: 0.5 32 | 33 | You can pass multiple arguments to the django-tinycontent template 34 | tags, like this:: 35 | 36 | {% load tinycontent_tags %} 37 | 38 | {% tinycontent_simple 'content_name' 'extra' %} 39 | 40 | Extra arguments are concatenated together before looking up the 41 | content block - the above example will look for a content block 42 | called ``content_name:extra``. 43 | 44 | The main use case for this is internationalisation - each argument 45 | can either be a string literal (as in our example above), or a 46 | context variable. For example - to include the language code as part 47 | of your block name, you could use:: 48 | 49 | {% load tinycontent_tags %} 50 | 51 | {% tinycontent_simple 'content_name' request.LANGUAGE_CODE %} 52 | 53 | For those of us running websites in Great Britain, that would result 54 | in fetching the content block ``content_name:en-gb``. 55 | 56 | This feature is available both for ``tinycontent_simple``, and 57 | ``tinycontent``. 58 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = tests.settings 3 | addopts = --tb=short 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | ignore = F999 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from setuptools import setup, find_packages 3 | from setuptools.command.test import test as TestCommand 4 | 5 | import tinycontent 6 | 7 | 8 | class PyTest(TestCommand): 9 | def finalize_options(self): 10 | TestCommand.finalize_options(self) 11 | self.test_args = [] 12 | self.test_suite = True 13 | 14 | def run_tests(self): 15 | import pytest 16 | 17 | errno = pytest.main(self.test_args) 18 | sys.exit(errno) 19 | 20 | 21 | setup( 22 | name="django-tinycontent", 23 | version=tinycontent.__version__, 24 | description="A Django app for managing re-usable blocks of tiny content.", 25 | long_description=open("README.rst").read(), 26 | long_description_content_type="text/x-rst", 27 | author="Dominic Rodger", 28 | author_email="internet@dominicrodger.com", 29 | url="http://github.com/dominicrodger/django-tinycontent", 30 | license="BSD", 31 | packages=find_packages(), 32 | include_package_data=True, 33 | install_requires=["Django>=2.0", "django-autoslug>=1.8.0", "markdown"], 34 | classifiers=[ 35 | "Development Status :: 5 - Production/Stable", 36 | "Framework :: Django", 37 | "License :: OSI Approved :: BSD License", 38 | "Environment :: Web Environment", 39 | "Intended Audience :: Developers", 40 | "Operating System :: OS Independent", 41 | "Programming Language :: Python :: 2.7", 42 | "Programming Language :: Python :: 3.5", 43 | ], 44 | tests_require=( 45 | "mock==2.0.0", 46 | "pytest==5.3.5", 47 | "pytest-cov==2.8.1", 48 | "pytest-django==3.8.0", 49 | ), 50 | cmdclass={"test": PyTest}, 51 | ) 52 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominicrodger/django-tinycontent/b202a106b0073cd736f84656467bd5f8fef4b78b/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.contrib.auth.models import User, Permission 3 | from django.core.files.uploadedfile import SimpleUploadedFile 4 | from tinycontent.models import TinyContent, TinyContentFileUpload 5 | 6 | 7 | @pytest.fixture() 8 | def simple_content(): 9 | content, _ = TinyContent.objects.get_or_create( 10 | name='foobar', 11 | content='This is a test.' 12 | ) 13 | return content 14 | 15 | 16 | @pytest.fixture() 17 | def simple_content_with_space(): 18 | content, _ = TinyContent.objects.get_or_create( 19 | name='foo bar', 20 | content='This is a test with a space.' 21 | ) 22 | return content 23 | 24 | 25 | @pytest.fixture() 26 | def split_content(): 27 | content, _ = TinyContent.objects.get_or_create( 28 | name='foo:bar', 29 | content='This is a second test.' 30 | ) 31 | return content 32 | 33 | 34 | @pytest.fixture() 35 | def html_content(): 36 | content, _ = TinyContent.objects.get_or_create( 37 | name='html', 38 | content='&' 39 | ) 40 | return content 41 | 42 | 43 | @pytest.fixture() 44 | def user(): 45 | user, _ = User.objects.get_or_create(username='dom') 46 | 47 | add_perm = Permission.objects.get( 48 | codename='add_tinycontent' 49 | ) 50 | change_perm = Permission.objects.get( 51 | codename='change_tinycontent' 52 | ) 53 | 54 | user.user_permissions.add(add_perm) 55 | user.user_permissions.add(change_perm) 56 | user.save() 57 | 58 | return user 59 | 60 | 61 | @pytest.fixture() 62 | def user_noauth(): 63 | user, _ = User.objects.get_or_create(username='barry') 64 | 65 | return user 66 | 67 | 68 | @pytest.fixture() 69 | def file_upload(): 70 | upload, _ = TinyContentFileUpload.objects.get_or_create( 71 | name='Foobar', 72 | file=SimpleUploadedFile( 73 | 'simple_file.txt', b'Hello, world!' 74 | ) 75 | ) 76 | 77 | return upload 78 | -------------------------------------------------------------------------------- /tests/filters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominicrodger/django-tinycontent/b202a106b0073cd736f84656467bd5f8fef4b78b/tests/filters/__init__.py -------------------------------------------------------------------------------- /tests/filters/test_markdown.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tinycontent.models import TinyContent 4 | from ..utils import render_template 5 | 6 | 7 | TEST_FILTER = 'tinycontent.filters.md.markdown_filter' 8 | 9 | 10 | @pytest.mark.django_db 11 | def test_with_markdown_filter_simple(simple_content, settings): 12 | settings.TINYCONTENT_FILTER = TEST_FILTER 13 | assert "

This is a test.

" == render_template( 14 | "{% tinycontent_simple 'foobar' %}" 15 | ) 16 | 17 | 18 | @pytest.mark.django_db 19 | def test_with_markdown_filter_newline(settings): 20 | settings.TINYCONTENT_FILTER = TEST_FILTER 21 | 22 | TinyContent.objects.get_or_create( 23 | name='newline', 24 | content='This is a test.\nHello' 25 | ) 26 | 27 | assert "

This is a test.
\nHello

" == render_template( 28 | "{% tinycontent_simple 'newline' %}" 29 | ) 30 | -------------------------------------------------------------------------------- /tests/filters/test_uploaded_file.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.core.files.uploadedfile import SimpleUploadedFile 3 | from tinycontent.models import TinyContent, TinyContentFileUpload 4 | from ..utils import render_template 5 | 6 | 7 | TEST_FILTER = 'tinycontent.filters.builtin.uploaded_file_filter' 8 | 9 | 10 | @pytest.mark.django_db 11 | def test_with_uploaded_file_filter(settings, file_upload): 12 | settings.TINYCONTENT_FILTER = TEST_FILTER 13 | 14 | TinyContent.objects.get_or_create( 15 | name='fileref', 16 | content='This is a @file:%s' % file_upload.slug 17 | ) 18 | 19 | result = render_template( 20 | "{% tinycontent_simple 'fileref' %}" 21 | ) 22 | 23 | assert result == 'This is a %s' % file_upload.file.url 24 | 25 | 26 | @pytest.mark.django_db 27 | def test_with_uploaded_file_multiple_files(settings): 28 | settings.TINYCONTENT_FILTER = TEST_FILTER 29 | 30 | file1 = TinyContentFileUpload.objects.create( 31 | name='File 1', 32 | file=SimpleUploadedFile( 33 | 'simple_file.txt', b'Hello, world!' 34 | ) 35 | ) 36 | 37 | file2 = TinyContentFileUpload.objects.create( 38 | name='File 2', 39 | file=SimpleUploadedFile( 40 | 'simple_file.txt', b'Hello, world!' 41 | ) 42 | ) 43 | 44 | TinyContent.objects.get_or_create( 45 | name='multifileref', 46 | content='This is a @file:file-1\nSo\'s this @file:file-2.' 47 | ) 48 | 49 | result = render_template( 50 | "{% tinycontent_simple 'multifileref' %}" 51 | ) 52 | lines = result.split('\n') 53 | assert len(lines) == 2 54 | assert lines[0] == "This is a %s" % file1.file.url 55 | assert lines[1] == "So's this %s." % file2.file.url 56 | 57 | 58 | @pytest.mark.django_db 59 | def test_with_uploaded_file_filter_badfile(settings, file_upload): 60 | settings.TINYCONTENT_FILTER = TEST_FILTER 61 | 62 | TinyContent.objects.get_or_create( 63 | name='fileref', 64 | content='This is a @file:hohohoho' 65 | ) 66 | 67 | result = render_template( 68 | "{% tinycontent_simple 'fileref' %}" 69 | ) 70 | 71 | assert result == 'This is a @file:hohohoho' 72 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | DATABASES = { 2 | 'default': { 3 | 'ENGINE': 'django.db.backends.sqlite3', 4 | 'NAME': 'test.db' 5 | } 6 | } 7 | 8 | INSTALLED_APPS = ( 9 | 'django.contrib.admin', 10 | 'django.contrib.auth', 11 | 'django.contrib.contenttypes', 12 | 'tinycontent.apps.TinyContentConfig', 13 | ) 14 | 15 | ROOT_URLCONF = 'tests.urls' 16 | SECRET_KEY = 'thisbagismadefromrecycledmaterial' 17 | 18 | MEDIA_ROOT = 'tests/testmedia' 19 | MEDIA_URL = 'http://media.example.com/' 20 | 21 | CACHES = { 22 | 'default': { 23 | 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', 24 | } 25 | } 26 | 27 | TEMPLATES = [ 28 | { 29 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 30 | 'APP_DIRS': True, 31 | }, 32 | ] 33 | 34 | TINYCONTENT_VERBOSE_NAME = 'Custom Tiny Content' 35 | -------------------------------------------------------------------------------- /tests/test_alternate_text.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .utils import ( 4 | render_template, 5 | render_template_with_context 6 | ) 7 | 8 | 9 | @pytest.mark.django_db 10 | def test_alternate_text_if_not_found(simple_content): 11 | t = ("{% tinycontent 'neverexists' %}" 12 | "I could not find it." 13 | "{% endtinycontent %}") 14 | 15 | assert "I could not find it." == render_template(t) 16 | 17 | 18 | @pytest.mark.django_db 19 | def test_alternate_text_if_found(simple_content): 20 | t = ("{% tinycontent 'foobar' %}" 21 | "I could not find it." 22 | "{% endtinycontent %}") 23 | 24 | assert "This is a test." == render_template(t) 25 | 26 | 27 | @pytest.mark.django_db 28 | def test_alternate_text_if_found_double_quotes(simple_content): 29 | t = ('{% tinycontent "foobar" %}' 30 | 'I could not find it.' 31 | '{% endtinycontent %}') 32 | 33 | assert "This is a test." == render_template(t) 34 | 35 | 36 | @pytest.mark.django_db 37 | def test_alternate_text_if_not_found_with_embedded_tags(simple_content): 38 | t = ("{% tinycontent 'neverexists' %}" 39 | "I could not find {{ meaning }}." 40 | "{% endtinycontent %}") 41 | 42 | ctx = {'meaning': 42} 43 | 44 | assert "I could not find 42." == render_template_with_context( 45 | t, ctx 46 | ) 47 | -------------------------------------------------------------------------------- /tests/test_auth.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from django.urls import reverse 4 | 5 | from .utils import render_for_test_user 6 | 7 | 8 | @pytest.mark.django_db 9 | def test_with_user(simple_content, user, user_noauth): 10 | t = ("{% tinycontent 'foobar' %}" 11 | "Text if empty." 12 | "{% endtinycontent %}") 13 | 14 | assert "This is a test." == render_for_test_user(t, user_noauth) 15 | 16 | root_edit_url = reverse('admin:tinycontent_tinycontent_change', 17 | args=[simple_content.pk, ]) 18 | 19 | rendered = render_for_test_user(t, user) 20 | assert root_edit_url in rendered 21 | assert 'Edit' in rendered 22 | assert "This is a test." in rendered 23 | 24 | t = "{% tinycontent_simple 'foobar' %}" 25 | rendered = render_for_test_user(t, user) 26 | assert root_edit_url in rendered 27 | assert 'Edit' in rendered 28 | assert "This is a test." in rendered 29 | 30 | 31 | @pytest.mark.django_db 32 | def test_with_user_for_nonexistent_tag(simple_content, user, user_noauth): 33 | t = ("{% tinycontent 'notthere' %}" 34 | "Text if empty." 35 | "{% endtinycontent %}") 36 | 37 | assert "Text if empty." == render_for_test_user(t, user_noauth) 38 | 39 | root_add_url = reverse('admin:tinycontent_tinycontent_add') 40 | 41 | rendered = render_for_test_user(t, user) 42 | assert '%s?name=notthere' % root_add_url in rendered 43 | assert 'Add' in rendered 44 | assert "Text if empty." in rendered 45 | 46 | t = "{% tinycontent_simple 'notthere' %}" 47 | rendered = render_for_test_user(t, user) 48 | assert '%s?name=notthere' % root_add_url in rendered 49 | assert 'Add' in rendered 50 | -------------------------------------------------------------------------------- /tests/test_bad_syntax.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from django.template.base import TemplateSyntaxError 4 | 5 | from .utils import render_template 6 | 7 | 8 | @pytest.mark.django_db 9 | def test_wrong_number_of_arguments(simple_content): 10 | t = ("{% tinycontent %}{% endtinycontent %}") 11 | 12 | with pytest.raises(TemplateSyntaxError): 13 | render_template(t) 14 | 15 | 16 | @pytest.mark.django_db 17 | def test_wrong_number_of_arguments_simple(simple_content): 18 | t = ("{% tinycontent_simple %}") 19 | 20 | with pytest.raises(TemplateSyntaxError): 21 | render_template(t) 22 | 23 | 24 | @pytest.mark.django_db 25 | def test_bad_arguments(simple_content): 26 | t = ("{% tinycontent 'foo %}{% endtinycontent %}") 27 | with pytest.raises(TemplateSyntaxError): 28 | render_template(t) 29 | 30 | t = ('{% tinycontent "foo %}{% endtinycontent %}') 31 | with pytest.raises(TemplateSyntaxError): 32 | render_template(t) 33 | -------------------------------------------------------------------------------- /tests/test_caching.py: -------------------------------------------------------------------------------- 1 | import django 2 | import pytest 3 | 4 | import mock 5 | from tinycontent.models import TinyContent 6 | 7 | # Django 1.5 doesn't have CaptureQueriesContext, and adding support 8 | # for checking query counts is irritating, so let's just skip those 9 | # tests. 10 | SKIP_CACHE_TESTS = django.VERSION < (1, 6) 11 | 12 | if not SKIP_CACHE_TESTS: 13 | from django.db import DEFAULT_DB_ALIAS, connections 14 | from django.test.utils import CaptureQueriesContext 15 | 16 | 17 | class FakeCache: 18 | def __init__(self): 19 | self.items = {} 20 | 21 | def get(self, key): 22 | return self.items.get(key, None) 23 | 24 | def set(self, key, value): 25 | self.items[key] = value 26 | 27 | def delete(self, key): 28 | del self.items[key] 29 | 30 | 31 | if not SKIP_CACHE_TESTS: 32 | class QueryCounter(CaptureQueriesContext): 33 | def __init__(self): 34 | conn = connections[DEFAULT_DB_ALIAS] 35 | super().__init__(conn) 36 | 37 | def num_queries(self): 38 | return len(self) 39 | 40 | 41 | @pytest.mark.django_db 42 | def test_cache_hit_on_second_time(simple_content): 43 | if SKIP_CACHE_TESTS: 44 | return 45 | 46 | with mock.patch('tinycontent.models.cache', FakeCache()): 47 | with QueryCounter() as q: 48 | obj = TinyContent.get_content_by_name(simple_content.name) 49 | assert obj == simple_content 50 | assert q.num_queries() == 1 51 | 52 | with QueryCounter() as q: 53 | obj = TinyContent.get_content_by_name(simple_content.name) 54 | assert obj == simple_content 55 | assert q.num_queries() == 0 56 | 57 | 58 | @pytest.mark.django_db 59 | def test_cache_invalidated_by_delete(simple_content): 60 | if SKIP_CACHE_TESTS: 61 | return 62 | 63 | with mock.patch('tinycontent.models.cache', FakeCache()): 64 | with QueryCounter() as q: 65 | obj = TinyContent.get_content_by_name(simple_content.name) 66 | assert obj == simple_content 67 | assert q.num_queries() == 1 68 | 69 | with QueryCounter() as q: 70 | simple_content.delete() 71 | assert q.num_queries() == 1 72 | 73 | with QueryCounter() as q: 74 | with pytest.raises(TinyContent.DoesNotExist): 75 | obj = TinyContent.get_content_by_name(simple_content.name) 76 | assert q.num_queries() == 1 77 | 78 | 79 | @pytest.mark.django_db 80 | def test_cache_invalidated_by_save(simple_content): 81 | if SKIP_CACHE_TESTS: 82 | return 83 | 84 | with mock.patch('tinycontent.models.cache', FakeCache()): 85 | with QueryCounter() as q: 86 | obj = TinyContent.get_content_by_name(simple_content.name) 87 | assert obj == simple_content 88 | assert q.num_queries() == 1 89 | 90 | with QueryCounter() as q: 91 | simple_content.content = 'hello' 92 | simple_content.save() 93 | q.num_queries() == 1 94 | 95 | with QueryCounter() as q: 96 | obj = TinyContent.get_content_by_name(simple_content.name) 97 | assert obj.name == simple_content.name 98 | assert obj.content == 'hello' 99 | assert q.num_queries() == 1 100 | 101 | with QueryCounter() as q: 102 | obj = TinyContent.get_content_by_name(simple_content.name) 103 | assert obj.name == simple_content.name 104 | assert obj.content == 'hello' 105 | assert q.num_queries() == 0 106 | -------------------------------------------------------------------------------- /tests/test_content_name.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .utils import ( 4 | render_template, 5 | render_template_with_context 6 | ) 7 | 8 | 9 | @pytest.mark.django_db 10 | def test_allows_context_variables_as_content_names_from_simple(simple_content): 11 | t = ("{% tinycontent_simple content_name %}") 12 | 13 | ctx = {'content_name': 'foobar'} 14 | 15 | assert "This is a test." == render_template_with_context(t, ctx) 16 | 17 | 18 | @pytest.mark.django_db 19 | def test_allows_context_variables_as_content_names_from_complex( 20 | simple_content 21 | ): 22 | t = ("{% tinycontent content_name %}" 23 | "Text if empty." 24 | "{% endtinycontent %}") 25 | 26 | ctx = {'content_name': 'foobar'} 27 | 28 | assert "This is a test." == render_template_with_context(t, ctx) 29 | 30 | 31 | @pytest.mark.django_db 32 | def test_allows_multiple_arguments_and_variables_from_simple(split_content): 33 | t = ("{% tinycontent_simple 'foo' var %}") 34 | 35 | ctx = {'var': 'bar'} 36 | 37 | assert "This is a second test." == render_template_with_context(t, ctx) 38 | 39 | 40 | @pytest.mark.django_db 41 | def test_allows_multiple_arguments_and_variables_from_complex( 42 | split_content 43 | ): 44 | t = ("{% tinycontent 'foo' key %}" 45 | "Text if empty." 46 | "{% endtinycontent %}") 47 | 48 | ctx = {'key': 'bar'} 49 | 50 | assert "This is a second test." == render_template_with_context(t, ctx) 51 | 52 | 53 | @pytest.mark.django_db 54 | def test_allows_with_tag_as_content_names_from_simple(simple_content): 55 | t = ("{% with content_name='foobar' %}" 56 | "{% tinycontent_simple content_name %}" 57 | "{% endwith %}") 58 | 59 | assert "This is a test." == render_template(t) 60 | 61 | 62 | @pytest.mark.django_db 63 | def test_allows_with_tag_as_content_names_from_complex(simple_content): 64 | t = ("{% with content_name='foobar' %}" 65 | "{% tinycontent content_name %}" 66 | "Text if empty." 67 | "{% endtinycontent %}" 68 | "{% endwith %}") 69 | 70 | assert "This is a test." == render_template(t) 71 | 72 | 73 | @pytest.mark.django_db 74 | def test_allows_unprovided_ctx_variables_as_content_name_complex( 75 | simple_content 76 | ): 77 | t = ("{% tinycontent content_name %}" 78 | "Text if empty." 79 | "{% endtinycontent %}") 80 | 81 | assert "Text if empty." == render_template(t) 82 | 83 | 84 | @pytest.mark.django_db 85 | def test_allows_unprovided_ctx_variables_as_content_name_simple( 86 | simple_content 87 | ): 88 | t = ("{% tinycontent_simple content_name %}") 89 | 90 | assert "" == render_template(t) 91 | 92 | 93 | @pytest.mark.django_db 94 | def test_ctx_variables_with_name_of_content_complex(simple_content): 95 | t = ("{% tinycontent foobar %}" 96 | "Text if empty." 97 | "{% endtinycontent %}") 98 | 99 | assert "Text if empty." == render_template(t) 100 | 101 | 102 | @pytest.mark.django_db 103 | def test_ctx_variables_with_name_of_content_simple(simple_content): 104 | t = ("{% tinycontent_simple foobar %}") 105 | 106 | assert "" == render_template(t) 107 | -------------------------------------------------------------------------------- /tests/test_custom_filters.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import pytest 5 | from django.core.exceptions import ImproperlyConfigured 6 | 7 | from .utils import render_template 8 | 9 | # Needed for the custom filter tests 10 | sys.path.append(os.path.dirname(__file__)) 11 | 12 | 13 | @pytest.mark.django_db 14 | def test_with_custom_filter_simple(simple_content, settings): 15 | settings.TINYCONTENT_FILTER = 'utils.toupper' 16 | assert "THIS IS A TEST." == render_template( 17 | "{% tinycontent_simple 'foobar' %}" 18 | ) 19 | 20 | 21 | @pytest.mark.django_db 22 | def test_with_custom_filter_complex(simple_content, settings): 23 | settings.TINYCONTENT_FILTER = 'utils.toupper' 24 | assert "THIS IS A TEST." == render_template( 25 | "{% tinycontent 'foobar' %}" 26 | "Not found." 27 | "{% endtinycontent %}" 28 | ) 29 | 30 | 31 | @pytest.mark.django_db 32 | def test_with_custom_filter_simple_with_html(html_content, settings): 33 | settings.TINYCONTENT_FILTER = 'utils.toupper' 34 | assert "&" == render_template( 35 | "{% tinycontent_simple 'html' %}" 36 | ) 37 | 38 | 39 | @pytest.mark.django_db 40 | def test_with_custom_filter_complex_with_html(html_content, settings): 41 | settings.TINYCONTENT_FILTER = 'utils.toupper' 42 | assert "&" == render_template( 43 | "{% tinycontent 'html' %}" 44 | "Not found." 45 | "{% endtinycontent %}" 46 | ) 47 | 48 | 49 | @pytest.mark.django_db 50 | def test_with_bad_custom_filter(simple_content, settings): 51 | settings.TINYCONTENT_FILTER = 'utils.ohnothisisfake' 52 | with pytest.raises(ImproperlyConfigured): 53 | render_template("{% tinycontent_simple 'foobar' %}") 54 | 55 | 56 | @pytest.mark.django_db 57 | def test_with_chained_custom_filters(simple_content, settings): 58 | settings.TINYCONTENT_FILTER = [ 59 | 'utils.toupper', 60 | 'utils.truncate_ten', 61 | 'utils.reverse', 62 | ] 63 | 64 | assert "A SI SIHT" == render_template( 65 | "{% tinycontent_simple 'foobar' %}" 66 | ) 67 | -------------------------------------------------------------------------------- /tests/test_custom_verbose_name.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from tinycontent.models import TinyContent 3 | 4 | 5 | @pytest.mark.django_db 6 | def test_with_custom_verbose_name(): 7 | assert TinyContent._meta.app_config.verbose_name == 'Custom Tiny Content' 8 | -------------------------------------------------------------------------------- /tests/test_html.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .utils import render_template 4 | 5 | 6 | @pytest.mark.django_db 7 | def test_with_html_simple(html_content): 8 | assert "&" == render_template( 9 | "{% tinycontent_simple 'html' %}" 10 | ) 11 | 12 | 13 | @pytest.mark.django_db 14 | def test_with_html_complex(html_content): 15 | assert "&" == render_template( 16 | "{% tinycontent 'html' %}" 17 | "Not found." 18 | "{% endtinycontent %}" 19 | ) 20 | -------------------------------------------------------------------------------- /tests/test_models.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.django_db 5 | def test_tinycontent_str(simple_content): 6 | assert "foobar" == str(simple_content) 7 | 8 | 9 | @pytest.mark.django_db 10 | def test_tinycontentfile_str(file_upload): 11 | assert "Foobar" == str(file_upload) 12 | 13 | 14 | @pytest.mark.django_db 15 | def test_tinycontentfile_slug(file_upload): 16 | assert "foobar" == file_upload.slug 17 | -------------------------------------------------------------------------------- /tests/test_simple.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .utils import render_template 4 | 5 | 6 | @pytest.mark.django_db 7 | def test_non_existent(simple_content): 8 | assert "" == render_template("{% tinycontent_simple 'foo' %}") 9 | 10 | 11 | @pytest.mark.django_db 12 | def test_simple_existent(simple_content): 13 | assert "This is a test." == render_template( 14 | "{% tinycontent_simple 'foobar' %}" 15 | ) 16 | 17 | 18 | @pytest.mark.django_db 19 | def test_simple_with_space(simple_content_with_space): 20 | assert "This is a test with a space." == render_template( 21 | "{% tinycontent_simple 'foo bar' %}" 22 | ) 23 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from django.contrib import admin 4 | 5 | urlpatterns = [ 6 | path("admin/", admin.site.urls), 7 | ] 8 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.context_processors import PermWrapper 2 | from django.template import Context, Template 3 | 4 | 5 | def render_template(input): 6 | t = Template("{% load tinycontent_tags %}" + input) 7 | c = Context() 8 | return t.render(c).strip() 9 | 10 | 11 | def render_template_with_context(input, context): 12 | t = Template("{% load tinycontent_tags %}" + input) 13 | c = Context(context) 14 | return t.render(c).strip() 15 | 16 | 17 | def render_for_test_user(t, user): 18 | ctx = {'user': user, 'perms': PermWrapper(user), } 19 | return render_template_with_context(t, ctx) 20 | 21 | 22 | def toupper(content): 23 | return content.upper() 24 | 25 | 26 | def truncate_ten(content): 27 | return content[:10] 28 | 29 | 30 | def reverse(content): 31 | return content[::-1] 32 | -------------------------------------------------------------------------------- /tinycontent/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.9.0" 2 | VERSION = tuple(map(int, __version__.split("."))) 3 | -------------------------------------------------------------------------------- /tinycontent/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from tinycontent.models import TinyContent, TinyContentFileUpload 3 | 4 | 5 | class TinyContentAdmin(admin.ModelAdmin): 6 | list_display = ('name', ) 7 | search_fields = ('name', 'content', ) 8 | 9 | 10 | admin.site.register(TinyContent, TinyContentAdmin) 11 | 12 | 13 | class TinyContentFileUploadAdmin(admin.ModelAdmin): 14 | list_display = ('name', 'slug', ) 15 | search_fields = ('name', ) 16 | 17 | 18 | admin.site.register(TinyContentFileUpload, TinyContentFileUploadAdmin) 19 | -------------------------------------------------------------------------------- /tinycontent/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | from .conf import get_app_verbose_name 4 | 5 | 6 | class TinyContentConfig(AppConfig): 7 | default_auto_field = 'django.db.models.BigAutoField' 8 | name = 'tinycontent' 9 | verbose_name = get_app_verbose_name() 10 | -------------------------------------------------------------------------------- /tinycontent/conf.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from tinycontent.utils.importer import import_from_dotted_path 3 | 4 | 5 | def get_app_verbose_name(): 6 | return getattr(settings, 'TINYCONTENT_VERBOSE_NAME', 'Tinycontent') 7 | 8 | 9 | def get_filter_list(): 10 | try: 11 | path_list = getattr(settings, 'TINYCONTENT_FILTER') 12 | except AttributeError: 13 | return [] 14 | 15 | if isinstance(path_list, str): 16 | path_list = [path_list, ] 17 | 18 | return [import_from_dotted_path(path) for path in path_list] 19 | -------------------------------------------------------------------------------- /tinycontent/filters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominicrodger/django-tinycontent/b202a106b0073cd736f84656467bd5f8fef4b78b/tinycontent/filters/__init__.py -------------------------------------------------------------------------------- /tinycontent/filters/builtin.py: -------------------------------------------------------------------------------- 1 | from tinycontent.utils.file_uploads import get_fileuploads 2 | 3 | 4 | def uploaded_file_filter(content): 5 | for match in get_fileuploads(content): 6 | content = content.replace(match.full, match.file.file.url) 7 | 8 | return content 9 | -------------------------------------------------------------------------------- /tinycontent/filters/md.py: -------------------------------------------------------------------------------- 1 | import markdown 2 | 3 | 4 | def markdown_filter(content): 5 | return markdown.markdown( 6 | content, 7 | extensions=['nl2br', ] 8 | ) 9 | -------------------------------------------------------------------------------- /tinycontent/locale/pl/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominicrodger/django-tinycontent/b202a106b0073cd736f84656467bd5f8fef4b78b/tinycontent/locale/pl/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /tinycontent/locale/pl/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: django-tinycontent 0.6.0\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2015-08-02 16:28+0200\n" 11 | "PO-Revision-Date: 2015-08-02 16:30+0100\n" 12 | "Last-Translator: Adam Dobrawy \n" 13 | "Language-Team: Adam Dobrawy \n" 14 | "Language: pl_PL\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " 19 | "|| n%100>=20) ? 1 : 2);\n" 20 | "X-Generator: Poedit 1.5.4\n" 21 | 22 | #: tinycontent/templates/tinycontent/tinycontent.html:5 23 | #, python-format 24 | msgid "(Edit)" 25 | msgstr "(Edytuj)" 26 | 27 | #: tinycontent/templates/tinycontent/tinycontent_add.html:4 28 | #, python-format 29 | msgid "" 30 | "(No content defined: Add some)" 32 | msgstr "" 33 | "(Treść nieokreślona: Dodaj)" 35 | -------------------------------------------------------------------------------- /tinycontent/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | from django.db import models, migrations 2 | 3 | 4 | class Migration(migrations.Migration): 5 | 6 | dependencies = [ 7 | ] 8 | 9 | operations = [ 10 | migrations.CreateModel( 11 | name='TinyContent', 12 | fields=[ 13 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 14 | ('name', models.CharField(unique=True, max_length=100)), 15 | ('content', models.TextField()), 16 | ], 17 | options={ 18 | 'verbose_name': 'Content block', 19 | }, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /tinycontent/migrations/0002_tinycontentfileupload.py: -------------------------------------------------------------------------------- 1 | from django.db import models, migrations 2 | import autoslug.fields 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [ 8 | ('tinycontent', '0001_initial'), 9 | ] 10 | 11 | operations = [ 12 | migrations.CreateModel( 13 | name='TinyContentFileUpload', 14 | fields=[ 15 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 16 | ('name', models.CharField(help_text=b'The name of the file.', max_length=60)), 17 | ('slug', autoslug.fields.AutoSlugField(unique=True, editable=False)), 18 | ('file', models.FileField(upload_to=b'tinycontent/uploads')), 19 | ('created', models.DateTimeField(auto_now_add=True)), 20 | ('modified', models.DateTimeField(auto_now=True)), 21 | ], 22 | options={ 23 | 'ordering': ('-created',), 24 | 'verbose_name': 'File upload', 25 | }, 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /tinycontent/migrations/0003_update_slug.py: -------------------------------------------------------------------------------- 1 | from django.db import models, migrations 2 | import autoslug.fields 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [ 8 | ('tinycontent', '0002_tinycontentfileupload'), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name='tinycontentfileupload', 14 | name='slug', 15 | field=autoslug.fields.AutoSlugField(populate_from=b'name', unique=True, editable=False), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /tinycontent/migrations/0004_py3_compatible.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.11.4 on 2017-08-16 06:09 2 | 3 | import autoslug.fields 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('tinycontent', '0003_update_slug'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='tinycontentfileupload', 16 | name='file', 17 | field=models.FileField(upload_to='tinycontent/uploads'), 18 | ), 19 | migrations.AlterField( 20 | model_name='tinycontentfileupload', 21 | name='name', 22 | field=models.CharField(help_text='The name of the file.', max_length=60), 23 | ), 24 | migrations.AlterField( 25 | model_name='tinycontentfileupload', 26 | name='slug', 27 | field=autoslug.fields.AutoSlugField(editable=False, populate_from='name', unique=True), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /tinycontent/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominicrodger/django-tinycontent/b202a106b0073cd736f84656467bd5f8fef4b78b/tinycontent/migrations/__init__.py -------------------------------------------------------------------------------- /tinycontent/models.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import autoslug 3 | from django.core.cache import cache 4 | from django.db import models 5 | from tinycontent.conf import get_filter_list 6 | 7 | 8 | class TinyContent(models.Model): 9 | name = models.CharField(max_length=100, unique=True) 10 | content = models.TextField() 11 | 12 | def __str__(self): 13 | return self.name 14 | 15 | def rendered_content(self): 16 | filters = get_filter_list() 17 | 18 | content = self.content 19 | 20 | for filter in filters: 21 | content = filter(content) 22 | 23 | return content 24 | 25 | @staticmethod 26 | def get_content_by_name(name): 27 | cache_key = TinyContent.get_cache_key(name) 28 | obj = cache.get(cache_key) 29 | 30 | if obj is None: 31 | obj = TinyContent.objects.get(name=name) 32 | cache.set(cache_key, obj) 33 | 34 | return obj 35 | 36 | @staticmethod 37 | def get_cache_key(name): 38 | return 'tinycontent_%s' % base64.b64encode(bytes(name, 'utf-8')) 39 | 40 | def delete(self, *args, **kwargs): 41 | cache.delete(TinyContent.get_cache_key(self.name)) 42 | return super().delete(*args, **kwargs) 43 | 44 | def save(self, *args, **kwargs): 45 | cache.delete(TinyContent.get_cache_key(self.name)) 46 | return super().save(*args, **kwargs) 47 | 48 | class Meta: 49 | verbose_name = 'Content block' 50 | 51 | 52 | class TinyContentFileUpload(models.Model): 53 | name = models.CharField( 54 | max_length=60, 55 | help_text='The name of the file.' 56 | ) 57 | slug = autoslug.AutoSlugField(populate_from='name', unique=True) 58 | file = models.FileField(upload_to='tinycontent/uploads') 59 | created = models.DateTimeField(auto_now_add=True) 60 | modified = models.DateTimeField(auto_now=True) 61 | 62 | def __str__(self): 63 | return self.name 64 | 65 | class Meta: 66 | verbose_name = 'File upload' 67 | ordering = ('-created', ) 68 | -------------------------------------------------------------------------------- /tinycontent/templates/tinycontent/tinycontent.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {{ obj.rendered_content|safe }} 3 | 4 | {% if perms.tinycontent.change_tinycontent %} 5 | {% url 'admin:tinycontent_tinycontent_change' obj.id as edit_url %} 6 | {% blocktrans %}(Edit){% endblocktrans %} 7 | {% endif %} 8 | -------------------------------------------------------------------------------- /tinycontent/templates/tinycontent/tinycontent_add.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% if perms.tinycontent.add_tinycontent %} 3 | {% url 'admin:tinycontent_tinycontent_add' as add_url %} {% with name_url=name|urlencode %} 4 | {% blocktrans %}(No content defined: Add some){% endblocktrans %}{%endwith %} 5 | {% endif %} 6 | -------------------------------------------------------------------------------- /tinycontent/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominicrodger/django-tinycontent/b202a106b0073cd736f84656467bd5f8fef4b78b/tinycontent/templatetags/__init__.py -------------------------------------------------------------------------------- /tinycontent/templatetags/tinycontent_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.template.base import TemplateSyntaxError 3 | from django.utils.encoding import force_str 4 | from django.template.loader import render_to_string 5 | from tinycontent.models import TinyContent 6 | 7 | 8 | register = template.Library() 9 | 10 | 11 | class TinyContentNode(template.Node): 12 | def __init__(self, args, nodelist): 13 | self.args = args 14 | self.nodelist = nodelist 15 | 16 | def get_name(self, context): 17 | return ':'.join(x.resolve(context) for x in self.args) 18 | 19 | def render(self, context): 20 | try: 21 | name = self.get_name(context) 22 | obj = TinyContent.get_content_by_name(name) 23 | context.update( 24 | { 25 | 'obj': obj 26 | } 27 | ) 28 | return render_to_string('tinycontent/tinycontent.html', 29 | context.flatten()) 30 | except TinyContent.DoesNotExist: 31 | rval = self.nodelist.render(context) 32 | context.update( 33 | { 34 | 'name': name 35 | } 36 | ) 37 | rval += render_to_string('tinycontent/tinycontent_add.html', 38 | context.flatten()) 39 | return rval 40 | 41 | 42 | @register.tag 43 | def tinycontent(parser, token): 44 | parts = token.split_contents()[1:] 45 | 46 | if not parts: 47 | raise TemplateSyntaxError("'tinycontent' tag takes arguments.") 48 | 49 | args = [parser.compile_filter(x) for x in parts] 50 | nodelist = parser.parse(('endtinycontent',)) 51 | parser.delete_first_token() 52 | return TinyContentNode(args, nodelist) 53 | 54 | 55 | @register.simple_tag(takes_context=True) 56 | def tinycontent_simple(context, *args): 57 | if not args: 58 | raise TemplateSyntaxError("'tinycontent' tag takes arguments.") 59 | 60 | content_name = ':'.join(map(force_str, args)) 61 | try: 62 | obj = TinyContent.get_content_by_name(content_name) 63 | context.update( 64 | { 65 | 'obj': obj 66 | } 67 | ) 68 | return render_to_string('tinycontent/tinycontent.html', 69 | context.flatten()) 70 | except TinyContent.DoesNotExist: 71 | context.update( 72 | { 73 | 'name': content_name 74 | } 75 | ) 76 | return render_to_string('tinycontent/tinycontent_add.html', 77 | context.flatten()) 78 | -------------------------------------------------------------------------------- /tinycontent/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominicrodger/django-tinycontent/b202a106b0073cd736f84656467bd5f8fef4b78b/tinycontent/utils/__init__.py -------------------------------------------------------------------------------- /tinycontent/utils/file_uploads.py: -------------------------------------------------------------------------------- 1 | import re 2 | from tinycontent.models import TinyContentFileUpload 3 | 4 | 5 | fileupload_expression = re.compile(r'(@file:([A-Za-z0-9\-_]+))') 6 | 7 | 8 | class FileUploadMatch: 9 | def __init__(self, match): 10 | self.full = match[0] 11 | self.file = TinyContentFileUpload.objects.get( 12 | slug=match[1] 13 | ) 14 | 15 | 16 | def get_fileuploads(text): 17 | fileuploads = [] 18 | 19 | for match in fileupload_expression.findall(text): 20 | try: 21 | fileuploads.append(FileUploadMatch(match)) 22 | except TinyContentFileUpload.DoesNotExist: 23 | # Just ignore bad slugs 24 | pass 25 | 26 | return fileuploads 27 | -------------------------------------------------------------------------------- /tinycontent/utils/importer.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ImproperlyConfigured 2 | from importlib import import_module 3 | 4 | 5 | def _imported_symbol(import_path): 6 | """Resolve a dotted path into a symbol, and return that. 7 | 8 | For example... 9 | 10 | >>> _imported_symbol('django.db.models.Model') 11 | 12 | 13 | Raise ImportError if there's no such module, AttributeError if no 14 | such symbol. 15 | 16 | """ 17 | module_name, symbol_name = import_path.rsplit('.', 1) 18 | module = import_module(module_name) 19 | return getattr(module, symbol_name) 20 | 21 | 22 | def import_from_dotted_path(path): 23 | """Return the resolution of an import path in the given string. 24 | 25 | :arg setting_name: The name of the setting holding the import path 26 | 27 | Raise ImproperlyConfigured if a path is given that can't be 28 | resolved. 29 | 30 | """ 31 | try: 32 | return _imported_symbol(path) 33 | except (ImportError, AttributeError): 34 | raise ImproperlyConfigured('No such module or attribute: %s' % path) 35 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = {py36,py37,py38}-{master,2.0.x,2.1.x,2.2.x,3.0.x},docs,flake8 3 | 4 | [flake8] 5 | exclude = tinycontent/migrations/* 6 | 7 | [testenv] 8 | commands= 9 | coverage run --source tinycontent --omit='*migrations*' setup.py test 10 | basepython = 11 | py36: python3.6 12 | py37: python3.7 13 | py38: python3.8 14 | deps = 15 | coverage==5.0.3 16 | master: https://github.com/django/django/archive/master.tar.gz 17 | 2.0.x: django==2.0.* 18 | 2.1.x: django==2.1.* 19 | 2.2.x: django==2.2.* 20 | 3.0.x: django==3.0.* 21 | 22 | [testenv:docs] 23 | basepython=python 24 | changedir=docs 25 | deps=sphinx 26 | commands= 27 | sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html 28 | 29 | [testenv:flake8] 30 | basepython=python 31 | deps=flake8 32 | commands= 33 | flake8 tinycontent tests setup.py 34 | --------------------------------------------------------------------------------