├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── test.yml ├── .gitignore ├── .readthedocs.yaml ├── CHANGES ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── _static │ └── front-edit-usage.gif ├── changes.rst ├── conf.py ├── index.rst ├── installation.rst ├── introduction.rst ├── requirements.txt ├── settings.rst ├── setup.rst └── usage.rst ├── front ├── __init__.py ├── conf │ ├── __init__.py │ └── settings.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20150123_0219.py │ ├── 0003_auto_20170923_1427.py │ └── __init__.py ├── models.py ├── static │ ├── epiceditor │ │ ├── images │ │ │ ├── edit.png │ │ │ ├── fullscreen.png │ │ │ ├── modules │ │ │ │ └── pulls │ │ │ │ │ └── dirty-shade.png │ │ │ └── preview.png │ │ ├── js │ │ │ ├── epiceditor.js │ │ │ └── epiceditor.min.js │ │ └── themes │ │ │ ├── base │ │ │ └── epiceditor.css │ │ │ ├── editor │ │ │ ├── epic-dark.css │ │ │ └── epic-light.css │ │ │ └── preview │ │ │ ├── bartik.css │ │ │ ├── github.css │ │ │ └── preview-dark.css │ ├── front │ │ ├── css │ │ │ └── front-edit.css │ │ └── js │ │ │ ├── front-edit.ace-local.js │ │ │ ├── front-edit.ace.js │ │ │ ├── front-edit.ckeditor.js │ │ │ ├── front-edit.default.js │ │ │ ├── front-edit.epiceditor.js │ │ │ ├── front-edit.froala.js │ │ │ ├── front-edit.js │ │ │ ├── front-edit.medium.js │ │ │ ├── front-edit.redactor.js │ │ │ ├── front-edit.summernote.js │ │ │ └── front-edit.wymeditor.js │ ├── to-markdown │ │ └── to-markdown.js │ └── wym │ │ ├── django │ │ ├── icons.png │ │ ├── skin.css │ │ └── skin.js │ │ └── wymeditor_icon.png ├── templatetags │ ├── __init__.py │ └── front_tags.py ├── tests │ ├── __init__.py │ ├── tests.py │ ├── urls.py │ ├── urls_no_save.py │ └── views.py ├── urls.py └── views.py ├── setup.py ├── test_project ├── .coveragerc ├── manage.py ├── static │ └── .gitignore ├── templates │ └── base.html └── test_project │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ ├── views.py │ └── wsgi.py └── tox.ini /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - Which version of Django are you using?: 2 | - Which version of django-front are you using?: 3 | - Have you looked trough [recent issues](https://github.com/mbi/django-front/issues?utf8=%E2%9C%93&q=is%3Aissue) and checked this isn't a duplicate? 4 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### All Submissions: 2 | 3 | - [ ] Are tests passing? (From the root-level of the repository please run `pip install tox && tox`) 4 | - [ ] I have added or updated a test to cover the changes proposed in this Pull Request 5 | - [ ] I have updated the documentation to cover the changes proposed in this Pull Request 6 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test Django Front 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | max-parallel: 4 11 | matrix: 12 | python-version: ["3.9", "3.10", "3.11", "3.12"] 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: niden/actions-memcached@v7 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Install dependencies 22 | run: | 23 | sudo apt-get install gettext 24 | python -m pip install --upgrade pip 25 | pip install tox tox-gh-actions 26 | - name: Test with tox 27 | run: tox 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv* 2 | *.pyc 3 | test_project_* 4 | test_project/.coverage 5 | test_project/coverage.xml 6 | test_project/htmlcov/ 7 | test_project/static/redactor* 8 | dist 9 | django_front.egg-info 10 | docs/_build/ 11 | .tox 12 | .eggs -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | build: 3 | os: ubuntu-22.04 4 | tools: 5 | python: "3.10" 6 | # You can also specify other tool versions: 7 | # nodejs: "16" 8 | 9 | # Build documentation in the docs/ directory with Sphinx 10 | sphinx: 11 | configuration: docs/conf.py 12 | 13 | # Dependencies required to build your docs 14 | python: 15 | install: 16 | - requirements: docs/requirements.txt 17 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | docs/changes.rst -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2023 Marco Bonetti and contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include LICENSE 3 | include README.rst 4 | include CHANGES 5 | include tox.ini 6 | include .pep8 7 | recursive-include docs * 8 | recursive-include test_project * 9 | recursive-include front * 10 | exclude *.sh 11 | exclude .DS_Store 12 | exclude test_project/.coverage 13 | prune test_project_2 14 | prune .tox 15 | prune docs/_build 16 | prune test_project/test_project/__pycache__ 17 | prune test_project/static/redactor* 18 | prune test_project/htmlcov 19 | global-exclude *pyc 20 | global-exclude coverage.xml 21 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django-Front 2 | ********************* 3 | 4 | 5 | .. image:: https://github.com/mbi/django-front/actions/workflows/test.yml/badge.svg 6 | :target: https://github.com/mbi/django-front/actions/workflows/test.yml 7 | 8 | .. image:: https://img.shields.io/pypi/v/django-front 9 | :target: https://pypi.org/project/django-front/ 10 | 11 | .. image:: https://img.shields.io/pypi/l/django-front 12 | :target: https://github.com/mbi/django-front/blob/master/LICENSE 13 | 14 | 15 | 16 | Django-front is a front-end editing application: placeholders can be defined in Django templates, which can then be edited on the front-end. 17 | 18 | Currently supported editors: 19 | 20 | * `Ace `_ 21 | * `CKEditor `_ 22 | * `EpicEditor `_ 23 | * `Froala `_ 24 | * `Medium Editor `_ 25 | * `Redactor `_ 26 | * `WYMeditor `_ 27 | 28 | Please see the `online documentation `_ to install and get started. 29 | 30 | ---- 31 | 32 | .. image:: http://django-front.readthedocs.org/en/latest/_images/front-edit-usage.gif 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /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 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Django-Front.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Django-Front.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Django-Front" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Django-Front" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/_static/front-edit-usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbi/django-front/0ed620dcd7f0e4ce11f62b58149b878f3dfb3eb6/docs/_static/front-edit-usage.gif -------------------------------------------------------------------------------- /docs/changes.rst: -------------------------------------------------------------------------------- 1 | Version history 2 | ############### 3 | 4 | Version 0.6.1 (unreleased) 5 | ========================== 6 | * Django 5.2 support 7 | 8 | 9 | Version 0.6.0 10 | ============== 11 | * Django 4.2 and 5.0 support 12 | 13 | 14 | Version 0.5.15 15 | ============== 16 | * Django 3.2 support 17 | 18 | Version 0.5.14 19 | ============== 20 | * Django 3.1 support 21 | * Dropped support for Django < 2.2 and Python2 22 | 23 | Version 0.5.13 24 | ============== 25 | * Django 3.0 support 26 | 27 | Version 0.5.12 28 | ============== 29 | * Dropped support for Django < 1.11 30 | * Added new `DJANGO_FRONT_EXTRA_CONTAINER_CLASSES` setting to inject extra classes on the div wrapping the editable code blocks 31 | 32 | Version 0.5.11 33 | ============== 34 | * Test against Django 2.2a 35 | 36 | Version 0.5.10 37 | ============== 38 | * Test against Django 2.1a 39 | 40 | Version 0.5.9 41 | ============= 42 | * Add support for the Summernote editor 43 | 44 | Version 0.5.8 45 | ============= 46 | * Test against Django 2.0 final 47 | 48 | Version 0.5.7 49 | ============= 50 | * Test against Django 2.0b1 51 | * Add missing migration 0003 52 | 53 | Version 0.5.6 54 | ============= 55 | * Missing static folder (Issue #14, thanks @sekiroh) 56 | 57 | Version 0.5.5 58 | ============= 59 | * Added support for Medium Editor 60 | 61 | Version 0.5.4 62 | ============= 63 | * Test against Django 1.11 64 | 65 | Version 0.5.3 66 | ============= 67 | * Test against Django 1.10b1 68 | 69 | Version 0.5.2 70 | ============= 71 | * Fixes a possible unicode decode error on funky input 72 | 73 | Version 0.5.1 74 | ============= 75 | * Support for running tests via setuptools 76 | 77 | Version 0.5.0 78 | ============= 79 | * Supported Django versions are now 1.7, 1.8 and 1.9 80 | 81 | Version 0.4.9 82 | ============= 83 | * Wrap all JavaScript plugins in their distinct scope receiving a local jQuery 84 | 85 | Version 0.4.8 86 | ============= 87 | * Support for the Froala editor 88 | 89 | Version 0.4.7 90 | ============= 91 | * Upgraded the CDN and bundled versions of ACE, EPIC and CKEditor 92 | 93 | 94 | Version 0.4.6 95 | ============= 96 | * Test against Django 1.8 97 | 98 | Version 0.4.5 99 | ============= 100 | * Fixed editor history on RedactorJS > 10.0 101 | * Fixed documentation 102 | * Generate documentation during tox tests 103 | 104 | Version 0.4.4 105 | ============= 106 | * Added a missing migration 107 | * Test against Django 1.8a 108 | * Switched to tox 109 | 110 | Version 0.4.3 111 | ============= 112 | * Added an API allowing copying content from one Placeholder instance to another (e.g. same name, different arguments) 113 | 114 | Version 0.4.2 115 | ============= 116 | * Support for RedactorJS v10 API 117 | 118 | Version 0.4.1 119 | Version 0.4.0 120 | ============= 121 | * Destroy editor before removing its container. Issues #6 and #7, thanks @syabro 122 | 123 | Version 0.3.9 124 | ============= 125 | * Test against Django 1.7 final 126 | * Use event delegation instead of direct binding on .editable blocks 127 | 128 | Version 0.3.8 129 | ============= 130 | * Support both South and Django 1.7 native migrations, inspired by https://github.com/SmileyChris/easy-thumbnails 131 | 132 | Version 0.3.7 133 | ============= 134 | * Test against Django 1.7RC1 135 | 136 | Version 0.3.6 137 | ============= 138 | * Refactored JavaScript files to use "jQuery" instead of the shortcut ("$") 139 | 140 | Version 0.3.5 141 | ============= 142 | * Missing image in the EpicEditor static. (Issue #5, thanks @twined) 143 | 144 | Version 0.3.3 145 | ============= 146 | * Support for CKEditor 147 | 148 | Version 0.3.2 149 | ============= 150 | * Shipping with documentation 151 | 152 | Version 0.3.0 153 | ============= 154 | * History of content, possibility to move back to a previous version of the placeholder 155 | * Massive rework of front-end side, modularization of editor plugins 156 | 157 | Version 0.2.6 158 | ============= 159 | * Added an "ace-local" plugin, for when Ace is served locally 160 | 161 | Version 0.2.4 162 | ============= 163 | * Add an extra class to the container, when the placeholder will be rendered empty 164 | * Add a min-height on empty placeholders 165 | 166 | Version 0.2.3 167 | ============= 168 | * Make sure the urlconf entry was added properly 169 | * Set a min-height on Redactor 170 | * New DJANGO_FRONT_EDITOR_OPTIONS settings allows for options to be passed on to the editors (works with WYMeditor, Redactor, EpicEditor) 171 | 172 | Version 0.2.2 173 | ============= 174 | * Added support for the EpicEditor (thanks @daikeren - Issue #2) 175 | 176 | Version 0.2.1 177 | ============= 178 | * Clarified the installation section of the README (mentioned that django.core.context_processors.request needs to be enabled in TEMPLATE_CONTEXT_PROCESSORS) 179 | * Added the test project to the settings, so that it's easier to run tests 180 | 181 | Version 0.2.0 182 | ============= 183 | * Test against Django 1.6b1 184 | 185 | Version 0.1.9 186 | ============= 187 | * Python 3.3 support on Django 1.5+ 188 | 189 | Version 0.1.8 190 | ============= 191 | * Namespaced the layer and dialog CSS classes 192 | 193 | Version 0.1.7 194 | ============= 195 | * Editing mode (lightbox or inline) 196 | 197 | Version 0.1.6 198 | ============= 199 | * Support for Redactor 9 beta 200 | 201 | Version 0.1.5 202 | ============= 203 | * Support for the Redactor editor 204 | 205 | Version 0.1.4 206 | ============= 207 | * Include the Django Wymeditor theme, because django-wymeditor doesn't by default 208 | * Push the STATIC_URL to the JavaScript context so that we don't have to assume it's /static/ 209 | 210 | Version 0.1.3 211 | ============= 212 | * Basic test cases 213 | 214 | Version 0.1.2 215 | ============= 216 | * Support for WYMeditor (see note in README about installing django-wymeditor) 217 | 218 | Version 0.1.1 219 | ============= 220 | * Settings (permissions) 221 | * Cleanups 222 | 223 | Version 0.1.0 224 | ============= 225 | * First release 226 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Django-Front documentation build configuration file, created by 5 | # sphinx-quickstart on Sat Dec 28 10:24:16 2013. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import os 17 | import sys 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | # needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = ["sphinx.ext.autodoc"] 33 | 34 | # Add any paths that contain templates here, relative to this directory. 35 | templates_path = ["_templates"] 36 | 37 | # The suffix of source filenames. 38 | source_suffix = ".rst" 39 | 40 | # The encoding of source files. 41 | # source_encoding = 'utf-8-sig' 42 | 43 | # The master toctree document. 44 | master_doc = "index" 45 | 46 | # General information about the project. 47 | project = "Django-Front" 48 | copyright = "2013-2023, Marco Bonetti and contributors" 49 | 50 | 51 | def get_version(): 52 | sys.path.insert(0, os.path.abspath("..")) 53 | from front import get_version as get_version_ 54 | 55 | return get_version_() 56 | 57 | 58 | # The version info for the project you're documenting, acts as replacement for 59 | # |version| and |release|, also used in various other places throughout the 60 | # built documents. 61 | # 62 | # The short X.Y version. 63 | version = get_version() 64 | # The full version, including alpha/beta/rc tags. 65 | release = version 66 | 67 | # The language for content autogenerated by Sphinx. Refer to documentation 68 | # for a list of supported languages. 69 | # language = None 70 | 71 | # There are two options for replacing |today|: either, you set today to some 72 | # non-false value, then it is used: 73 | # today = '' 74 | # Else, today_fmt is used as the format for a strftime call. 75 | # today_fmt = '%B %d, %Y' 76 | 77 | # List of patterns, relative to source directory, that match files and 78 | # directories to ignore when looking for source files. 79 | exclude_patterns = ["_build"] 80 | 81 | # The reST default role (used for this markup: `text`) to use for all 82 | # documents. 83 | # default_role = None 84 | 85 | # If true, '()' will be appended to :func: etc. cross-reference text. 86 | # add_function_parentheses = True 87 | 88 | # If true, the current module name will be prepended to all description 89 | # unit titles (such as .. function::). 90 | # add_module_names = True 91 | 92 | # If true, sectionauthor and moduleauthor directives will be shown in the 93 | # output. They are ignored by default. 94 | # show_authors = False 95 | 96 | # The name of the Pygments (syntax highlighting) style to use. 97 | pygments_style = "sphinx" 98 | 99 | # A list of ignored prefixes for module index sorting. 100 | # modindex_common_prefix = [] 101 | 102 | # If true, keep warnings as "system message" paragraphs in the built documents. 103 | # keep_warnings = False 104 | 105 | 106 | # -- Options for HTML output ---------------------------------------------- 107 | 108 | # The theme to use for HTML and HTML Help pages. See the documentation for 109 | # a list of builtin themes. 110 | # html_theme = 'sphinx_rtd_theme' 111 | html_theme = "sphinx_book_theme" 112 | 113 | # Theme options are theme-specific and customize the look and feel of a theme 114 | # further. For a list of options available for each theme, see the 115 | # documentation. 116 | # html_theme_options = {} 117 | 118 | # Add any paths that contain custom themes here, relative to this directory. 119 | # html_theme_path = [] 120 | 121 | # The name for this set of Sphinx documents. If None, it defaults to 122 | # " v documentation". 123 | # html_title = None 124 | 125 | # A shorter title for the navigation bar. Default is the same as html_title. 126 | # html_short_title = None 127 | 128 | # The name of an image file (relative to this directory) to place at the top 129 | # of the sidebar. 130 | # html_logo = None 131 | 132 | # The name of an image file (within the static path) to use as favicon of the 133 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 134 | # pixels large. 135 | # html_favicon = None 136 | 137 | # Add any paths that contain custom static files (such as style sheets) here, 138 | # relative to this directory. They are copied after the builtin static files, 139 | # so a file named "default.css" will overwrite the builtin "default.css". 140 | html_static_path = ["_static"] 141 | 142 | # Add any extra paths that contain custom files (such as robots.txt or 143 | # .htaccess) here, relative to this directory. These files are copied 144 | # directly to the root of the documentation. 145 | # html_extra_path = [] 146 | 147 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 148 | # using the given strftime format. 149 | # html_last_updated_fmt = '%b %d, %Y' 150 | 151 | # If true, SmartyPants will be used to convert quotes and dashes to 152 | # typographically correct entities. 153 | # html_use_smartypants = True 154 | 155 | # Custom sidebar templates, maps document names to template names. 156 | # html_sidebars = {} 157 | 158 | # Additional templates that should be rendered to pages, maps page names to 159 | # template names. 160 | # html_additional_pages = {} 161 | 162 | # If false, no module index is generated. 163 | # html_domain_indices = True 164 | 165 | # If false, no index is generated. 166 | # html_use_index = True 167 | 168 | # If true, the index is split into individual pages for each letter. 169 | # html_split_index = False 170 | 171 | # If true, links to the reST sources are added to the pages. 172 | # html_show_sourcelink = True 173 | 174 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 175 | # html_show_sphinx = True 176 | 177 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 178 | # html_show_copyright = True 179 | 180 | # If true, an OpenSearch description file will be output, and all pages will 181 | # contain a tag referring to it. The value of this option must be the 182 | # base URL from which the finished HTML is served. 183 | # html_use_opensearch = '' 184 | 185 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 186 | # html_file_suffix = None 187 | 188 | # Output file base name for HTML help builder. 189 | htmlhelp_basename = "Django-Frontdoc" 190 | 191 | 192 | # -- Options for LaTeX output --------------------------------------------- 193 | 194 | latex_elements = { 195 | # The paper size ('letterpaper' or 'a4paper'). 196 | #'papersize': 'letterpaper', 197 | # The font size ('10pt', '11pt' or '12pt'). 198 | #'pointsize': '10pt', 199 | # Additional stuff for the LaTeX preamble. 200 | #'preamble': '', 201 | } 202 | 203 | # Grouping the document tree into LaTeX files. List of tuples 204 | # (source start file, target name, title, 205 | # author, documentclass [howto, manual, or own class]). 206 | latex_documents = [ 207 | ( 208 | "index", 209 | "Django-Front.tex", 210 | "Django-Front Documentation", 211 | "Marco Bonetti", 212 | "manual", 213 | ) 214 | ] 215 | 216 | # The name of an image file (relative to this directory) to place at the top of 217 | # the title page. 218 | # latex_logo = None 219 | 220 | # For "manual" documents, if this is true, then toplevel headings are parts, 221 | # not chapters. 222 | # latex_use_parts = False 223 | 224 | # If true, show page references after internal links. 225 | # latex_show_pagerefs = False 226 | 227 | # If true, show URL addresses after external links. 228 | # latex_show_urls = False 229 | 230 | # Documents to append as an appendix to all manuals. 231 | # latex_appendices = [] 232 | 233 | # If false, no module index is generated. 234 | # latex_domain_indices = True 235 | 236 | 237 | # -- Options for manual page output --------------------------------------- 238 | 239 | # One entry per manual page. List of tuples 240 | # (source start file, name, description, authors, manual section). 241 | man_pages = [ 242 | ("index", "django-front", "Django-Front Documentation", ["Marco Bonetti"], 1) 243 | ] 244 | 245 | # If true, show URL addresses after external links. 246 | # man_show_urls = False 247 | 248 | 249 | # -- Options for Texinfo output ------------------------------------------- 250 | 251 | # Grouping the document tree into Texinfo files. List of tuples 252 | # (source start file, target name, title, author, 253 | # dir menu entry, description, category) 254 | texinfo_documents = [ 255 | ( 256 | "index", 257 | "Django-Front", 258 | "Django-Front Documentation", 259 | "Marco Bonetti", 260 | "Django-Front", 261 | "One line description of project.", 262 | "Miscellaneous", 263 | ) 264 | ] 265 | 266 | # Documents to append as an appendix to all manuals. 267 | # texinfo_appendices = [] 268 | 269 | # If false, no module index is generated. 270 | # texinfo_domain_indices = True 271 | 272 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 273 | # texinfo_show_urls = 'footnote' 274 | 275 | # If true, do not generate a @detailmenu in the "Top" node's menu. 276 | # texinfo_no_detailmenu = False 277 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Django-Front documentation master file, created by 2 | sphinx-quickstart on Sat Dec 28 10:24:16 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Django-Front 7 | ============ 8 | 9 | Django-front is a front-end editing application for Django 10 | 11 | .. figure:: _static/front-edit-usage.gif 12 | :alt: using django-front 13 | 14 | Using django-front to edit a placholder with three of the supported editors. 15 | 16 | Contents: 17 | ========= 18 | .. toctree:: 19 | :maxdepth: 2 20 | 21 | introduction.rst 22 | installation.rst 23 | setup.rst 24 | usage.rst 25 | settings.rst 26 | changes.rst 27 | 28 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | =============== 3 | 4 | Requirements 5 | ------------ 6 | 7 | * Django-front supports Django 4.2 and 5.0 8 | * django-classy-tags 9 | * Python 3.9+ 10 | * jQuery is required in your template 11 | 12 | 13 | Installing django-front 14 | ----------------------- 15 | 16 | * ``pip install django-front`` 17 | * Add ``front`` to your ``INSTALLED_APPS`` 18 | * Add a line to your ``urls.py``:: 19 | 20 | urlpatterns += [ 21 | url(r'^front-edit/', include('front.urls')), 22 | ] 23 | 24 | * ``python manage.py migrate`` (or syncdb if that's your dope) 25 | 26 | * Note: the ``django.core.context_processors.request`` context processor must be enabled in your ``TEMPLATE_CONTEXT_PROCESSORS`` setting. 27 | 28 | 29 | Testing 30 | ------- 31 | 32 | * ``pip install --upgrade tox && tox`` 33 | -------------------------------------------------------------------------------- /docs/introduction.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | =============== 3 | 4 | Django-front is a front-end editing application: placeholders can be defined in Django templates, which can then be edited on the front-end (in place) by authorized users. 5 | 6 | Features 7 | -------- 8 | 9 | * Edit html content directly on the front-end of your site. 10 | * Version history: jump back to a previous version of a content block at any time. 11 | * Content blocks are persisted in the database, and in Django's cache (read: zero database queries) 12 | * Content scope and inheritance: content blocks (placeholders) can be defined globally (i.e. edited once, displayed on every page), or on a single url, for a single language, … 13 | * Built-in support for several content editor plugins (WYSIWYG, html, Markdown, …), adding a new editor is very simple. 14 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx-book-theme 2 | -------------------------------------------------------------------------------- /docs/settings.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _settings-section: 3 | 4 | ######## 5 | Settings 6 | ######## 7 | 8 | These settings are defined, and can be overridden in your project settings 9 | 10 | * ``DJANGO_FRONT_PERMISSION``: a callable that gets passed a user object, and returns a boolean specifying whether or not the user is allowed to do front-end editing. Defaults to ``lambda u: u and u.is_staff`` 11 | * ``DJANGO_FRONT_EDIT_MODE``: specifies whether the editor should be opened in a lightbox (default) or inline (over the edited element). Valid values are ``'inline'`` and ``'lightbox'``. 12 | * ``DJANGO_FRONT_EDITOR_OPTIONS``: allows for options to be passed on to the editors (works with WYMeditor, Redactor, EpicEditor, CKEditor, Froala, Medium, Summernote). This dictionary will be serialized as JSON and merged with the editor's base options. Defaults to ``{}``. Example, to handle `image uploads in Redactor `_:: 13 | 14 | DJANGO_FRONT_EDITOR_OPTIONS = { 15 | 'imageUpload': '/path/to/image/handling/view/' 16 | } 17 | 18 | * ``DJANGO_FRONT_ALLOWED_EDITORS``: list of allowed editor plugins. Add to this list if you plan on adding a new editor type. Defaults to ``['ace', 'ace-local', 'wymeditor', 'redactor', 'epiceditor', 'ckeditor', 'froala', 'medium', 'summernote', 'default']`` 19 | * ``DJANGO_FRONT_EXTRA_CONTAINER_CLASSES``: string of extra classes that are appended to the div wrapper containing editable markup. Defaults to ``''`` 20 | -------------------------------------------------------------------------------- /docs/setup.rst: -------------------------------------------------------------------------------- 1 | ############## 2 | Setting it up 3 | ############## 4 | 5 | 6 | *************** 7 | Template layout 8 | *************** 9 | 10 | We assume that your site uses a `basic template hierarchy `_, having a base template and multiple "content" templates inheriting from the base one. 11 | 12 | To set up `django-front`, you will need to include a few lines in your base template, then add content placeholders in the child templates. 13 | 14 | ******************* 15 | Your base template… 16 | ******************* 17 | 18 | First, load the `front_tags` module at the top of your base template:: 19 | 20 | {% load front_tags %} 21 | 22 | 23 | Then include jQuery, followed by front-editing scripts somewhere towards the end of your ````, e.g.:: 24 | 25 | 26 | {% front_edit_scripts %} 27 | 28 | .. note:: 29 | 30 | the Redactor editor needs a fairly recent version of jQuery (1.8+), but WYMeditor will need an older one (<= 1.7). Adapt as needed. 31 | 32 | ********************************************** 33 | Defining placeholders in your child templates… 34 | ********************************************** 35 | 36 | Once the ``front_edit_scripts`` scripts are injected (they are only rendered to users who can actually edit the content), you can start adding placeholders to your templates. 37 | 38 | First load the tag library:: 39 | 40 | {% load front_tags %} 41 | 42 | Then define a placeholder:: 43 | 44 | {% front_edit "placeholder_name" request.path request.LANGUAGE_CODE %} 45 |

Default when empty

46 | {% end_front_edit %} 47 | 48 | When no placeholder content is defined for this placeholder, the default content is displayed instead. 49 | 50 | Placeholder scope 51 | ================= 52 | 53 | Any variable passed after the placeholder name will be evaluated. The full identifier (i.e. name and variables) will be hashed and will define the main identifier for this placeholder. 54 | 55 | The scope (visibility) of the rendered content block is defined by the variable names used in the block definition: the content block in the previous example will be rendered only on the page at the current URL, and the current language. 56 | 57 | The following example, on the other hand, would be rendered on every page using the template having this tag, regardless of the language and the URL:: 58 | 59 | {% front_edit "look ma, Im global!" %} 60 |

Default when empty

61 | {% end_front_edit %} 62 | 63 | ****************** 64 | Choosing an editor 65 | ****************** 66 | 67 | The default editor just uses a plain ``'; 227 | }, 228 | 229 | // initializes the editor on the target element, with the given html code 230 | set_html: function(target, html, front_edit_options) { 231 | this.target = target; 232 | this.target.find('.front-edit-container').html(html); 233 | }, 234 | 235 | // returns the edited html code 236 | get_html: function(front_edit_options) { 237 | return this.target.find('.front-edit-container').val(); 238 | }, 239 | 240 | // destroy the editor 241 | destroy_editor: function() { 242 | self.target = null; 243 | } 244 | }; 245 | })(jQuery); 246 | 247 | 248 | 3. Maybe submit a pull request? 249 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | ################## 2 | Using django-front 3 | ################## 4 | 5 | *************** 6 | Editing content 7 | *************** 8 | 9 | 1. As an authorized (and authenticated) user, hover your mouse pointer over a placeholder, it'll get a blue border. 10 | 2. Double-click to start editing. 11 | 3. After you're done editing: 12 | 13 | i) Click "save" to commit the changes to the database and get back to the default view, or 14 | ii) Click "cancel" to revert to the previously saved version, or 15 | iii) Click "history" to fetch the change history from the back-end. The button will turn into a select-box, displaying all the saved timestamps. Selecting a previous version will load the content from that version into the editor. 16 | 17 | 18 | ************************ 19 | Security considerations 20 | ************************ 21 | 22 | By default, only authenticated users having `the staff flag `_ can edit placeholder content. 23 | 24 | You can specify who can edit content in the settings: see ``DJANGO_FRONT_PERMISSION`` under :ref:`settings-section`. 25 | 26 | Django-front will render its JavaScript object only to authenticated users with editing permissions. 27 | 28 | 29 | ************************ 30 | Performance 31 | ************************ 32 | 33 | * The first time a placeholder tag is rendered, its content fetched from the database and stored in Django's cache. Successive hits on that placeholder will get the content from the cache. 34 | * Each time a placeholder is saved, its cache key is invalidated. A ``PlaceholderHistory`` object is also saved (if the content was changed). 35 | 36 | .. warning:: To avoid hitting the database for each placeholder it is critical to use a proper cache back-end, such as `Memcached `_. 37 | 38 | ************************ 39 | Caveats 40 | ************************ 41 | 42 | For users allowed to edit the content, ``django-front`` will wrap the rendered placeholder in a div, to mark it up as editable, e.g.:: 43 | 44 | 45 |
46 |

aaa

47 |

Lorem ipsum dolor sit amet, …

48 |
49 | 50 | 51 | (whitespace added for emphasis) 52 | 53 | To non-authenticated users, the same placeholder will be rendered without the wrapping div:: 54 | 55 | 56 |

aaa

57 |

Lorem ipsum dolor sit amet, …

58 | 59 | 60 | As a consequence, CSS child selectors (e.g. ``body > h1``) will behave differently to authenticated users than non-authenticated ones. As a workaround, just keep in mind to extend the child selector to cover this case, e.g.:: 61 | 62 | body > h1, 63 | body > .editable > h1 { 64 | … 65 | } 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /front/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = (0, 6, 1) 2 | 3 | 4 | def get_version(limit=3): 5 | """Return the version as a human-format string.""" 6 | return ".".join([str(i) for i in VERSION[:limit]]) 7 | -------------------------------------------------------------------------------- /front/conf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbi/django-front/0ed620dcd7f0e4ce11f62b58149b878f3dfb3eb6/front/conf/__init__.py -------------------------------------------------------------------------------- /front/conf/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | DJANGO_FRONT_PERMISSION = getattr(settings, 'DJANGO_FRONT_PERMISSION', lambda u: u and u.is_staff) 4 | DJANGO_FRONT_EDIT_MODE = getattr(settings, 'DJANGO_FRONT_EDIT_MODE', 'lightbox') # ('lightbox' or 'inline') 5 | DJANGO_FRONT_EDITOR_OPTIONS = getattr(settings, 'DJANGO_FRONT_EDITOR_OPTIONS', dict()) 6 | DJANGO_FRONT_ALLOWED_EDITORS = [editor.lower() for editor in getattr(settings, 'DJANGO_FRONT_ALLOWED_EDITORS', ['ace', 'ace-local', 'wymeditor', 'redactor', 'epiceditor', 'ckeditor', 'default', 'froala', 'medium', 'summernote'])] 7 | DJANGO_FRONT_EXTRA_CONTAINER_CLASSES = getattr(settings, 'DJANGO_FRONT_EXTRA_CONTAINER_CLASSES', '') 8 | -------------------------------------------------------------------------------- /front/forms.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbi/django-front/0ed620dcd7f0e4ce11f62b58149b878f3dfb3eb6/front/forms.py -------------------------------------------------------------------------------- /front/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='Placeholder', 15 | fields=[ 16 | ('key', models.CharField(max_length=40, serialize=False, primary_key=True, db_index=True)), 17 | ('value', models.TextField(blank=True)), 18 | ], 19 | options={ 20 | }, 21 | bases=(models.Model,), 22 | ), 23 | migrations.CreateModel( 24 | name='PlaceholderHistory', 25 | fields=[ 26 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 27 | ('value', models.TextField(blank=True)), 28 | ('saved', models.DateTimeField(auto_now_add=True)), 29 | ('placeholder', models.ForeignKey(to='front.Placeholder', on_delete=models.CASCADE)), 30 | ], 31 | options={ 32 | 'ordering': (b'-saved',), 33 | }, 34 | bases=(models.Model,), 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /front/migrations/0002_auto_20150123_0219.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('front', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='placeholderhistory', 16 | name='placeholder', 17 | field=models.ForeignKey(related_name='history', to='front.Placeholder', on_delete=models.CASCADE), 18 | preserve_default=True, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /front/migrations/0003_auto_20170923_1427.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0a1 on 2017-09-23 14:27 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('front', '0002_auto_20150123_0219'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='placeholderhistory', 15 | options={'ordering': ('-saved',)}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /front/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbi/django-front/0ed620dcd7f0e4ce11f62b58149b878f3dfb3eb6/front/migrations/__init__.py -------------------------------------------------------------------------------- /front/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.core.cache import cache 3 | from django.dispatch import receiver 4 | from django.db.models.signals import post_save 5 | import hashlib 6 | import six 7 | 8 | 9 | class Placeholder(models.Model): 10 | key = models.CharField(max_length=40, primary_key=True, db_index=True) 11 | value = models.TextField(blank=True) 12 | 13 | def __unicode__(self): 14 | return self.value 15 | 16 | def cache_key(self): 17 | return "front-edit-%s" % self.key 18 | 19 | @classmethod 20 | def key_for(cls, name, *bits): 21 | return hashlib.new('sha1', six.text_type(name + ''.join([six.text_type(token) for token in bits])).encode('utf8')).hexdigest() 22 | 23 | @classmethod 24 | def copy_content(cls, name, source_bits, target_bits): 25 | source_key = cls.key_for(name, *source_bits) 26 | target_key = cls.key_for(name, *target_bits) 27 | 28 | source = cls.objects.filter(key=source_key) 29 | if source.exists(): 30 | source = source.get() 31 | cls.objects.create(key=target_key, value=source.value) 32 | 33 | 34 | class PlaceholderHistory(models.Model): 35 | placeholder = models.ForeignKey(Placeholder, related_name='history', on_delete=models.CASCADE) 36 | value = models.TextField(blank=True) 37 | saved = models.DateTimeField(auto_now_add=True) 38 | 39 | class Meta: 40 | ordering = ('-saved', ) 41 | 42 | @property 43 | def _as_json(self): 44 | return {'value': self.value, 'saved': self.saved.strftime('%s')} 45 | 46 | 47 | @receiver(post_save, sender=Placeholder) 48 | def save_placeholder(sender, instance, created, raw, *args, **kwargs): 49 | if not raw: 50 | # If we have placeholders, check wheter the content has changed before saving history 51 | if PlaceholderHistory.objects.filter(placeholder=instance).exists(): 52 | ph = PlaceholderHistory.objects.all()[0] 53 | if ph.value != instance.value: 54 | PlaceholderHistory.objects.create(placeholder=instance, value=instance.value) 55 | else: 56 | PlaceholderHistory.objects.create(placeholder=instance, value=instance.value) 57 | 58 | 59 | @receiver(post_save, sender=PlaceholderHistory) 60 | def save_history(sender, instance, created, raw, *args, **kwargs): 61 | cache.delete(instance.placeholder.cache_key()) 62 | -------------------------------------------------------------------------------- /front/static/epiceditor/images/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbi/django-front/0ed620dcd7f0e4ce11f62b58149b878f3dfb3eb6/front/static/epiceditor/images/edit.png -------------------------------------------------------------------------------- /front/static/epiceditor/images/fullscreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbi/django-front/0ed620dcd7f0e4ce11f62b58149b878f3dfb3eb6/front/static/epiceditor/images/fullscreen.png -------------------------------------------------------------------------------- /front/static/epiceditor/images/modules/pulls/dirty-shade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbi/django-front/0ed620dcd7f0e4ce11f62b58149b878f3dfb3eb6/front/static/epiceditor/images/modules/pulls/dirty-shade.png -------------------------------------------------------------------------------- /front/static/epiceditor/images/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbi/django-front/0ed620dcd7f0e4ce11f62b58149b878f3dfb3eb6/front/static/epiceditor/images/preview.png -------------------------------------------------------------------------------- /front/static/epiceditor/js/epiceditor.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * EpicEditor - An Embeddable JavaScript Markdown Editor (https://github.com/OscarGodson/EpicEditor) 3 | * Copyright (c) 2011-2012, Oscar Godson. (MIT Licensed) 4 | */(function(e,t){function n(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])}function r(e,t){for(var n in t)t.hasOwnProperty(n)&&(e.style[n]=t[n])}function i(t,n){var r=t,i=null;return e.getComputedStyle?i=document.defaultView.getComputedStyle(r,null).getPropertyValue(n):r.currentStyle&&(i=r.currentStyle[n]),i}function s(e,t,n){var s={},o;if(t==="save"){for(o in n)n.hasOwnProperty(o)&&(s[o]=i(e,o));r(e,n)}else t==="apply"&&r(e,n);return s}function o(e){var t=parseInt(i(e,"border-left-width"),10)+parseInt(i(e,"border-right-width"),10),n=parseInt(i(e,"padding-left"),10)+parseInt(i(e,"padding-right"),10),r=e.offsetWidth,s;return isNaN(t)&&(t=0),s=t+n+r,s}function u(e){var t=parseInt(i(e,"border-top-width"),10)+parseInt(i(e,"border-bottom-width"),10),n=parseInt(i(e,"padding-top"),10)+parseInt(i(e,"padding-bottom"),10),r=parseInt(i(e,"height"),10),s;return isNaN(t)&&(t=0),s=t+n+r,s}function a(e,t,r){r=r||"";var i=t.getElementsByTagName("head")[0],s=t.createElement("link");n(s,{type:"text/css",id:r,rel:"stylesheet",href:e,name:e,media:"screen"}),i.appendChild(s)}function f(e,t,n){e.className=e.className.replace(t,n)}function l(e){return e.contentDocument||e.contentWindow.document}function c(e){var t;return typeof document.body.innerText=="string"?t=e.innerText:(t=e.innerHTML.replace(/
/gi,"\n"),t=t.replace(/<(?:.|\n)*?>/gm,""),t=t.replace(/</gi,"<"),t=t.replace(/>/gi,">")),t}function h(e,t){return t=t.replace(//g,">"),t=t.replace(/\n/g,"
"),t=t.replace(/
\s/g,"
 "),t=t.replace(/\s\s\s/g,"   "),t=t.replace(/\s\s/g,"  "),t=t.replace(/^ /," "),e.innerHTML=t,!0}function p(e){return e.replace(/\u00a0/g," ").replace(/ /g," ")}function d(){var e=-1,t=navigator.userAgent,n;return navigator.appName=="Microsoft Internet Explorer"&&(n=/MSIE ([0-9]{1,}[\.0-9]{0,})/,n.exec(t)!=null&&(e=parseFloat(RegExp.$1,10))),e}function v(){var t=e.navigator;return t.userAgent.indexOf("Safari")>-1&&t.userAgent.indexOf("Chrome")==-1}function m(){var t=e.navigator;return t.userAgent.indexOf("Firefox")>-1&&t.userAgent.indexOf("Seamonkey")==-1}function g(e){var t={};return e&&t.toString.call(e)==="[object Function]"}function y(){var e=arguments[0]||{},n=1,r=arguments.length,i=!1,s,o,u,a;typeof e=="boolean"&&(i=e,e=arguments[1]||{},n=2),typeof e!="object"&&!g(e)&&(e={}),r===n&&(e=this,--n);for(;n=5||Math.abs(g.x-t.pageX)>=5)h.style.display="block",p&&clearTimeout(p),p=e.setTimeout(function(){h.style.display="none"},1e3);g={y:t.pageY,x:t.pageX}}function M(e){e.keyCode==n.settings.shortcut.modifier&&(N=!0),e.keyCode==17&&(C=!0),N===!0&&e.keyCode==n.settings.shortcut.preview&&!n.is("fullscreen")&&(e.preventDefault(),n.is("edit")&&n._previewEnabled?n.preview():n._editEnabled&&n.edit()),N===!0&&e.keyCode==n.settings.shortcut.fullscreen&&n._fullscreenEnabled&&(e.preventDefault(),n._goFullscreen(T)),N===!0&&e.keyCode!==n.settings.shortcut.modifier&&(N=!1),e.keyCode==27&&n.is("fullscreen")&&n._exitFullscreen(T),C===!0&&e.keyCode==83&&(n.save(),e.preventDefault(),C=!1),e.metaKey&&e.keyCode==83&&(n.save(),e.preventDefault())}function _(e){e.keyCode==n.settings.shortcut.modifier&&(N=!1),e.keyCode==17&&(C=!1)}function D(t){var r;t.clipboardData?(t.preventDefault(),r=t.clipboardData.getData("text/plain"),n.editorIframeDocument.execCommand("insertText",!1,r)):e.clipboardData&&(t.preventDefault(),r=e.clipboardData.getData("Text"),r=r.replace(//g,">"),r=r.replace(/\n/g,"
"),r=r.replace(/\r/g,""),r=r.replace(/
\s/g,"
 "),r=r.replace(/\s\s\s/g,"   "),r=r.replace(/\s\s/g,"  "),n.editorIframeDocument.selection.createRange().pasteHTML(r))}if(this.is("loaded"))return this;var n=this,o,u,f,c,h,p,m,g={y:-1,x:-1},y,b,w=!1,E=!1,S=!1,x=!1,T,N=!1,C=!1,k,L,A;n._eeState.startup=!0,n.settings.useNativeFullscreen&&(E=document.body.webkitRequestFullScreen?!0:!1,S=document.body.mozRequestFullScreen?!0:!1,x=document.body.requestFullscreen?!0:!1,w=E||S||x),v()&&(w=!1,E=!1),!n.is("edit")&&!n.is("preview")&&(n._eeState.edit=!0),t=t||function(){},o={chrome:'
'+(n._previewEnabled?' ':"")+(n._editEnabled?' ':"")+(n._fullscreenEnabled?'':"")+"
"+"
",previewer:'
',editor:""},n.element.innerHTML='',n.element.style.height=n.element.offsetHeight+"px",u=document.getElementById(n._instanceId),n.iframeElement=u,n.iframe=l(u),n.iframe.open(),n.iframe.write(o.chrome),n.editorIframe=n.iframe.getElementById("epiceditor-editor-frame"),n.previewerIframe=n.iframe.getElementById("epiceditor-previewer-frame"),n.editorIframeDocument=l(n.editorIframe),n.editorIframeDocument.open(),n.editorIframeDocument.write(o.editor),n.editorIframeDocument.close(),n.previewerIframeDocument=l(n.previewerIframe),n.previewerIframeDocument.open(),n.previewerIframeDocument.write(o.previewer),f=n.previewerIframeDocument.createElement("base"),f.target="_blank",n.previewerIframeDocument.getElementsByTagName("head")[0].appendChild(f),n.previewerIframeDocument.close(),n.reflow(),a(n.settings.theme.base,n.iframe,"theme"),a(n.settings.theme.editor,n.editorIframeDocument,"theme"),a(n.settings.theme.preview,n.previewerIframeDocument,"theme"),n.iframe.getElementById("epiceditor-wrapper").style.position="relative",n.editorIframe.style.position="absolute",n.previewerIframe.style.position="absolute",n.editor=n.editorIframeDocument.body,n.previewer=n.previewerIframeDocument.getElementById("epiceditor-preview"),n.editor.contentEditable=!0,n.iframe.body.style.height=this.element.offsetHeight+"px",n.previewerIframe.style.left="-999999px",this.editorIframeDocument.body.style.wordWrap="break-word",d()>-1&&(this.previewer.style.height=parseInt(i(this.previewer,"height"),10)+2),this.open(n.settings.file.name),n.settings.focusOnLoad&&n.iframe.addEventListener("readystatechange",function(){n.iframe.readyState=="complete"&&n.focus()}),n.previewerIframeDocument.addEventListener("click",function(t){var r=t.target,i=n.previewerIframeDocument.body;r.nodeName=="A"&&r.hash&&r.hostname==e.location.hostname&&(t.preventDefault(),r.target="_self",i.querySelector(r.hash)&&(i.scrollTop=i.querySelector(r.hash).offsetTop))}),c=n.iframe.getElementById("epiceditor-utilbar"),y={},n._goFullscreen=function(t){this._fixScrollbars("auto");if(n.is("fullscreen")){n._exitFullscreen(t);return}w&&(E?t.webkitRequestFullScreen():S?t.mozRequestFullScreen():x&&t.requestFullscreen()),b=n.is("edit"),n._eeState.fullscreen=!0,n._eeState.edit=!0,n._eeState.preview=!0;var r=e.innerWidth,o=e.innerHeight,u=e.outerWidth,a=e.outerHeight;w||(a=e.innerHeight),y.editorIframe=s(n.editorIframe,"save",{width:u/2+"px",height:a+"px","float":"left",cssFloat:"left",styleFloat:"left",display:"block",position:"static",left:""}),y.previewerIframe=s(n.previewerIframe,"save",{width:u/2+"px",height:a+"px","float":"right",cssFloat:"right",styleFloat:"right",display:"block",position:"static",left:""}),y.element=s(n.element,"save",{position:"fixed",top:"0",left:"0",width:"100%","z-index":"9999",zIndex:"9999",border:"none",margin:"0",background:i(n.editor,"background-color"),height:o+"px"}),y.iframeElement=s(n.iframeElement,"save",{width:u+"px",height:o+"px"}),c.style.visibility="hidden",w||(document.body.style.overflow="hidden"),n.preview(),n.focus(),n.emit("fullscreenenter")},n._exitFullscreen=function(e){this._fixScrollbars(),s(n.element,"apply",y.element),s(n.iframeElement,"apply",y.iframeElement),s(n.editorIframe,"apply",y.editorIframe),s(n.previewerIframe,"apply",y.previewerIframe),n.element.style.width=n._eeState.reflowWidth?n._eeState.reflowWidth:"",n.element.style.height=n._eeState.reflowHeight?n._eeState.reflowHeight:"",c.style.visibility="visible",n._eeState.fullscreen=!1,w?E?document.webkitCancelFullScreen():S?document.mozCancelFullScreen():x&&document.exitFullscreen():document.body.style.overflow="auto",b?n.edit():n.preview(),n.reflow(),n.emit("fullscreenexit")},n.editor.addEventListener("keyup",function(){m&&e.clearTimeout(m),m=e.setTimeout(function(){n.is("fullscreen")&&n.preview()},250)}),T=n.iframeElement,c.addEventListener("click",function(e){var t=e.target.className;t.indexOf("epiceditor-toggle-preview-btn")>-1?n.preview():t.indexOf("epiceditor-toggle-edit-btn")>-1?n.edit():t.indexOf("epiceditor-fullscreen-btn")>-1&&n._goFullscreen(T)}),E?document.addEventListener("webkitfullscreenchange",function(){!document.webkitIsFullScreen&&n._eeState.fullscreen&&n._exitFullscreen(T)},!1):S?document.addEventListener("mozfullscreenchange",function(){!document.mozFullScreen&&n._eeState.fullscreen&&n._exitFullscreen(T)},!1):x&&document.addEventListener("fullscreenchange",function(){document.fullscreenElement==null&&n._eeState.fullscreen&&n._exitFullscreen(T)},!1),h=n.iframe.getElementById("epiceditor-utilbar"),n.settings.button.bar!==!0&&(h.style.display="none"),h.addEventListener("mouseover",function(){p&&clearTimeout(p)}),k=[n.previewerIframeDocument,n.editorIframeDocument];for(L=0;Li&&(n=i,a=!0),a?this._fixScrollbars("auto"):this._fixScrollbars("hidden"),n!=this.oldHeight&&(this.getElement("container").style.height=n+"px",this.reflow(),this.settings.autogrow.scroll&&e.scrollBy(0,n-this.oldHeight),this.oldHeight=n))},b.prototype._fixScrollbars=function(e){var t;this.settings.autogrow?t="hidden":t="auto",t=e||t,this.getElement("editor").documentElement.style.overflow=t,this.getElement("previewer").documentElement.style.overflow=t},b.version="0.2.2",b._data={},e.EpicEditor=b})(window),function(){function t(t){this.tokens=[],this.tokens.links={},this.options=t||f.defaults,this.rules=e.normal,this.options.gfm&&(this.options.tables?this.rules=e.tables:this.rules=e.gfm)}function r(e,t){this.options=t||f.defaults,this.links=e,this.rules=n.normal;if(!this.links)throw new Error("Tokens array requires a `links` property.");this.options.gfm?this.options.breaks?this.rules=n.breaks:this.rules=n.gfm:this.options.pedantic&&(this.rules=n.pedantic)}function i(e){this.tokens=[],this.token=null,this.options=e||f.defaults}function s(e,t){return e.replace(t?/&/g:/&(?!#?\w+;)/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function o(e,t){return e=e.source,t=t||"",function n(r,i){return r?(i=i.source||i,i=i.replace(/(^|[^\[])\^/g,"$1"),e=e.replace(r,i),n):new RegExp(e,t)}}function u(){}function a(e){var t=1,n,r;for(;t[^\n]+(\n[^\n]+)*\n*)+/,list:/^( *)(bull) [\s\S]+?(?:hr|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:/^ *(?:comment|closed|closing) *(?:\n{2,}|\s*$)/,def:/^ *\[([^\]]+)\]: *([^\s]+)(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,table:u,paragraph:/^([^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+\n*/,text:/^[^\n]+/};e.bullet=/(?:[*+-]|\d+\.)/,e.item=/^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/,e.item=o(e.item,"gm")(/bull/g,e.bullet)(),e.list=o(e.list)(/bull/g,e.bullet)("hr",/\n+(?=(?: *[-*_]){3,} *(?:\n+|$))/)(),e._tag="(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|@)\\b",e.html=o(e.html)("comment",//)("closed",/<(tag)[\s\S]+?<\/\1>/)("closing",/])*?>/)(/tag/g,e._tag)(),e.paragraph=o(e.paragraph)("hr",e.hr)("heading",e.heading)("lheading",e.lheading)("blockquote",e.blockquote)("tag","<"+e._tag)("def",e.def)(),e.normal=a({},e),e.gfm=a({},e.normal,{fences:/^ *(`{3,}|~{3,}) *(\w+)? *\n([\s\S]+?)\s*\1 *(?:\n+|$)/,paragraph:/^/}),e.gfm.paragraph=o(e.paragraph)("(?!","(?!"+e.gfm.fences.source.replace("\\1","\\2")+"|")(),e.tables=a({},e.gfm,{nptable:/^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/,table:/^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/}),t.rules=e,t.lex=function(e,n){var r=new t(n);return r.lex(e)},t.prototype.lex=function(e){return e=e.replace(/\r\n|\r/g,"\n").replace(/\t/g," ").replace(/\u00a0/g," ").replace(/\u2424/g,"\n"),this.token(e,!0)},t.prototype.token=function(e,t){var e=e.replace(/^ +$/gm,""),n,r,i,s,o,u,a;while(e){if(i=this.rules.newline.exec(e))e=e.substring(i[0].length),i[0].length>1&&this.tokens.push({type:"space"});if(i=this.rules.code.exec(e)){e=e.substring(i[0].length),i=i[0].replace(/^ {4}/gm,""),this.tokens.push({type:"code",text:this.options.pedantic?i:i.replace(/\n+$/,"")});continue}if(i=this.rules.fences.exec(e)){e=e.substring(i[0].length),this.tokens.push({type:"code",lang:i[2],text:i[3]});continue}if(i=this.rules.heading.exec(e)){e=e.substring(i[0].length),this.tokens.push({type:"heading",depth:i[1].length,text:i[2]});continue}if(t&&(i=this.rules.nptable.exec(e))){e=e.substring(i[0].length),s={type:"table",header:i[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:i[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:i[3].replace(/\n$/,"").split("\n")};for(u=0;u ?/gm,""),this.token(i,t),this.tokens.push({type:"blockquote_end"});continue}if(i=this.rules.list.exec(e)){e=e.substring(i[0].length),this.tokens.push({type:"list_start",ordered:isFinite(i[2])}),i=i[0].match(this.rules.item),n=!1,a=i.length,u=0;for(;u|])/,autolink:/^<([^ >]+(@|:\/)[^ >]+)>/,url:u,tag:/^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,link:/^!?\[(inside)\]\(href\)/,reflink:/^!?\[(inside)\]\s*\[([^\]]*)\]/,nolink:/^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,strong:/^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,em:/^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,code:/^(`+)([\s\S]*?[^`])\1(?!`)/,br:/^ {2,}\n(?!\s*$)/,del:u,text:/^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/,n.link=o(n.link)("inside",n._inside)("href",n._href)(),n.reflink=o(n.reflink)("inside",n._inside)(),n.normal=a({},n),n.pedantic=a({},n.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/}),n.gfm=a({},n.normal,{escape:o(n.escape)("])","~])")(),url:/^(https?:\/\/[^\s]+[^.,:;"')\]\s])/,del:/^~{2,}([\s\S]+?)~{2,}/,text:o(n.text)("]|","~]|")("|","|https?://|")()}),n.breaks=a({},n.gfm,{br:o(n.br)("{2,}","*")(),text:o(n.gfm.text)("{2,}","*")()}),r.rules=n,r.output=function(e,t,n){var i=new r(t,n);return i.output(e)},r.prototype.output=function(e){var t="",n,r,i,o;while(e){if(o=this.rules.escape.exec(e)){e=e.substring(o[0].length),t+=o[1];continue}if(o=this.rules.autolink.exec(e)){e=e.substring(o[0].length),o[2]==="@"?(r=o[1][6]===":"?this.mangle(o[1].substring(7)):this.mangle(o[1]),i=this.mangle("mailto:")+r):(r=s(o[1]),i=r),t+=''+r+"";continue}if(o=this.rules.url.exec(e)){e=e.substring(o[0].length),r=s(o[1]),i=r,t+=''+r+"";continue}if(o=this.rules.tag.exec(e)){e=e.substring(o[0].length),t+=this.options.sanitize?s(o[0]):o[0];continue}if(o=this.rules.link.exec(e)){e=e.substring(o[0].length),t+=this.outputLink(o,{href:o[2],title:o[3]});continue}if((o=this.rules.reflink.exec(e))||(o=this.rules.nolink.exec(e))){e=e.substring(o[0].length),n=(o[2]||o[1]).replace(/\s+/g," "),n=this.links[n.toLowerCase()];if(!n||!n.href){t+=o[0][0],e=o[0].substring(1)+e;continue}t+=this.outputLink(o,n);continue}if(o=this.rules.strong.exec(e)){e=e.substring(o[0].length),t+=""+this.output(o[2]||o[1])+"";continue}if(o=this.rules.em.exec(e)){e=e.substring(o[0].length),t+=""+this.output(o[2]||o[1])+"";continue}if(o=this.rules.code.exec(e)){e=e.substring(o[0].length),t+=""+s(o[2],!0)+"";continue}if(o=this.rules.br.exec(e)){e=e.substring(o[0].length),t+="
";continue}if(o=this.rules.del.exec(e)){e=e.substring(o[0].length),t+=""+this.output(o[1])+"";continue}if(o=this.rules.text.exec(e)){e=e.substring(o[0].length),t+=s(o[0]);continue}if(e)throw new Error("Infinite loop on byte: "+e.charCodeAt(0))}return t},r.prototype.outputLink=function(e,t){return e[0][0]!=="!"?'"+this.output(e[1])+"":''+s(e[1])+'"},r.prototype.mangle=function(e){var t="",n=e.length,r=0,i;for(;r.5&&(i="x"+i.toString(16)),t+="&#"+i+";";return t},i.parse=function(e,t){var n=new i(t);return n.parse(e)},i.prototype.parse=function(e){this.inline=new r(e.links,this.options),this.tokens=e.reverse();var t="";while(this.next())t+=this.tok();return t},i.prototype.next=function(){return this.token=this.tokens.pop()},i.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0},i.prototype.parseText=function(){var e=this.token.text;while(this.peek().type==="text")e+="\n"+this.next().text;return this.inline.output(e)},i.prototype.tok=function(){switch(this.token.type){case"space":return"";case"hr":return"
\n";case"heading":return""+this.inline.output(this.token.text)+"\n";case"code":if(this.options.highlight){var e=this.options.highlight(this.token.text,this.token.lang);e!=null&&e!==this.token.text&&(this.token.escaped=!0,this.token.text=e)}return this.token.escaped||(this.token.text=s(this.token.text,!0)),"
"+this.token.text+"
\n";case"table":var t="",n,r,i,o,u;t+="\n\n";for(r=0;r'+n+"\n":""+n+"\n";t+="\n\n",t+="\n";for(r=0;r\n";for(u=0;u'+o+"\n":""+o+"\n";t+="\n"}return t+="\n","\n"+t+"
\n";case"blockquote_start":var t="";while(this.next().type!=="blockquote_end" 5 | )t+=this.tok();return"
\n"+t+"
\n";case"list_start":var a=this.token.ordered?"ol":"ul",t="";while(this.next().type!=="list_end")t+=this.tok();return"<"+a+">\n"+t+"\n";case"list_item_start":var t="";while(this.next().type!=="list_item_end")t+=this.token.type==="text"?this.parseText():this.tok();return"
  • "+t+"
  • \n";case"loose_item_start":var t="";while(this.next().type!=="list_item_end")t+=this.tok();return"
  • "+t+"
  • \n";case"html":return!this.token.pre&&!this.options.pedantic?this.inline.output(this.token.text):this.token.text;case"paragraph":return"

    "+this.inline.output(this.token.text)+"

    \n";case"text":return"

    "+this.parseText()+"

    \n"}},u.exec=u,f.options=f.setOptions=function(e){return f.defaults=e,f},f.defaults={gfm:!0,tables:!0,breaks:!1,pedantic:!1,sanitize:!1,silent:!1,highlight:null},f.Parser=i,f.parser=i.parse,f.Lexer=t,f.lexer=t.lex,f.InlineLexer=r,f.inlineLexer=r.output,f.parse=f,typeof module!="undefined"?module.exports=f:typeof define=="function"&&define.amd?define(function(){return f}):this.marked=f}.call(function(){return this||(typeof window!="undefined"?window:global)}()); -------------------------------------------------------------------------------- /front/static/epiceditor/themes/base/epiceditor.css: -------------------------------------------------------------------------------- 1 | html, body, iframe, div { 2 | margin:0; 3 | padding:0; 4 | } 5 | 6 | #epiceditor-utilbar { 7 | position:fixed; 8 | bottom:10px; 9 | right:10px; 10 | } 11 | 12 | #epiceditor-utilbar button { 13 | display:block; 14 | float:left; 15 | width:30px; 16 | height:30px; 17 | border:none; 18 | background:none; 19 | } 20 | 21 | #epiceditor-utilbar button.epiceditor-toggle-preview-btn { 22 | background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAf9JREFUeNrsVu1twjAQLZmAEcIGYYKGDWACkgkSkPiQ+CifAoEgyQTQDegEpROQDZoRMgH0HToj1yIglKr90Vg62T6f/e4d9gu50+n09BctlwFnwP8TOAgCA10VRr2ZELaHhbBXx3HCVMC+71voXmD6g4Qi2NB13e1DwAAkZhtmmKYRcxsJhHeBPc8bMMufbDU0PxF4vV4TSyth8477csI6lTV/a71er9tioonBarXaIAmLErliNWyqoM8nrJPpHFNLWLcI4xvj5XKpMo2ZgcvzIvs+75S0wKwPPB/CnpWXsG00Gra2WCwshekOVoC9Sb6IGN1ge2HNsWK+B0iJqxAL5oSpYeDJJW02mxVYLAWSGfDtebylA68Bc4wh+ahK5PcxLh6PR5GUpym/iTOfz89PqNVqhRI4iQf1/o174HNMVYDSGeTDmXQ3znogCGrtdpsYVBhER1aH2Wzm8iE7UR74DMTWGNxUQWmNYqTEzq8APoo9sJ8wKoR5eU7T6VQVjZAvx4YvDJWt1Ol0QsTqkppF8EW8/12OhTnSpT2LCe2/KiCTyUQVkJgPuwgb6XG32w05Xui4q0imLLNDxA/uSuZ4PNaZqZlSsejDYfd6veihj8RoNDK5XOUHAen3Dfr9/j7VZxEJ6AwuxCCvhMTM7oNAARhl/0Ay4Az419qXAAMAfBdK7281j6YAAAAASUVORK5CYII=); 23 | } 24 | 25 | #epiceditor-utilbar button.epiceditor-toggle-edit-btn { 26 | background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnVJREFUeNrsV+lt4lAQNtEWAB2QCtZ0ABWsqYB1BU5AgDgkQOISl9cV4FQQtgLYCuKtIN4K4gogM9H3pMmLTTh24c+O9An7+XnOb8aP1G63M64hN8aV5GqGvxyyyfO8Y3S6hDtc2wTfcZyLRPwdvxEbPSviIwjIRtO49s9ONSRLMIGvwkBI+EPYEEqyQmcZdl2Xo3BgcJ90xPUGDh1veLFYWCBK9oQ6P4BgiXVO6fUjg5zCJcE6kVxsLEN4QTlWzO5yuRwlGp7P55zOxz1RRqK2SfKD8BvOG4IHxUqlEnxop9lsZpITa0KWndGwIeQIXswzHbynpK2xzjXbeBfxdDrlhbUWjYyuiJS9fBJxgL3PItKsprNQrVaD1GQyYUVP2oYelDziPoPnT5+k2QajleEC3usI/exM7oYiXor0hpxS8qhLv5FIVVrcBwkp5ueclbS27qNMvkj7kg3nZX1qtVqAaSUN5OGUwn2M4RUb3263plh700E6I7wTKn1suCAW3PF4zL1r1Ov1SBhXZLEJFqGTQCq5N1BZIp3s+DOi5fXcG7lGo1Ea5DIFSWzcq7a4R6uYqJmlkSqHoeGKeq+w905MtGKj0Yje9TE5ID9pqictQQwmXTfh82cIJ0NML0d0QY8MdhMn13A4zENB0hAJEYn6EnGL3MZ0hsyG3Ww2g70jU8lgMOhqHicJz+KfovVkz3J5/FardfhBgDZzS90SeoJ8cXjQJlUAEmZUCx30kYiTfr9vgFRc72+ChCHSzNH+Qgk+fA7b7fZZJ5AAiIRhT4zUv3/Y07LiaPW9yPE2L5jrI/p/d7wVEZe0U8bJkvr/F+ZS8irAAIorRozUvI0gAAAAAElFTkSuQmCC); 27 | } 28 | 29 | #epiceditor-utilbar button.epiceditor-fullscreen-btn { 30 | background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAY5JREFUeNrsV8tthDAQhYgCtoV04BKghK2En5AQBw4IITjwq2RLgBJcwpawHZAxMpGDYGwTFPaQkUbG1sybj58NmNM0GVfIh3GRXBbYEid932N9d1zXHWWAGAb4m+9VsUA0H5SubKkKIGA4qyUC2qKBxSCe541HKln7dV2nVbHRtq0Nw7CXGNtz/jzwqjZ5odtqTOagQRC82KRpGkcS/L2Ok7lXZV3Xp7Q6DMNR2mqNthMhKXLqzcUCc6Wgn3wU1wlX1PboHs8tjaLoyVtLT7JFK2ZZM6CZvWyE+X1Voaj3la3DMA63epGqqm4wfyCBH8xmz1+Z1WVZ2vyqU2HvHtv9OI4PsRpjL91YV2a7pcB8onmOifYFUhSFLQCTnQtkDpokyYv73JBtcAQsA3zGTXJBEgNXgrF3CcrBZGwnC+4uqxHnH+zN8/ybvexZwvZNhlsHbrt5CyCgDtu1XosUe58K4kuOF9EKnKYp20eVrxDUJssyreM0bDg4kIw0EfCbftvqQ6KKYf7/wvyVfAkwAMjqoFSaWbrBAAAAAElFTkSuQmCC); 31 | } 32 | 33 | @media 34 | only screen and (-webkit-min-device-pixel-ratio: 2), 35 | only screen and ( min--moz-device-pixel-ratio: 2), 36 | only screen and ( -o-min-device-pixel-ratio: 2/1), 37 | only screen and ( min-device-pixel-ratio: 2), 38 | only screen and ( min-resolution: 192dpi), 39 | only screen and ( min-resolution: 2dppx) { 40 | #epiceditor-utilbar button.epiceditor-toggle-preview-btn { 41 | background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAMAAAANIilAAAABrVBMVEWIiIiHh4eIiIiJiYmEhISEhISEhISMjIyNjY2MjIyNjY2MjIyFhYWDg4OFhYWKioqKioqBgYGJiYmJiYmJiYmKioqKioqFhYWIiIiJiYmIiIiJiYmIiIiGhoaIiIiGhoaIiIiHh4eHh4eGhoaFhYWKioqFhYWEhISFhYWEhISDg4ONjY2NjY2MjIyHh4eMjIyLi4uCgoKCgoKCgoKBgYGBgYGOjo6Ojo6BgYGPj4+Ojo6Hh4eNjY2Pj4+BgYGNjY2BgYGHh4eQkJCNjY2FhYWHh4eDg4OQkJCQkJCAgICCgoKAgICAgICQkJCQkJCQkJCAgICQkJCAgICQkJCFhYWHh4eHh4eGhoaKioqGhoaMjIyDg4OEhISDg4OEhISDg4OEhISKioqBgYGJiYmKioqKioqJiYmMjIyEhISCgoKGhoaLi4uPj4+Pj4+BgYGKioqDg4OGhoaGhoaMjIyHh4eEhISEhISDg4OIiIiJiYmGhoaHh4eJiYkAAACAgICFhYWQkJCIiIiEhISLi4uDg4OHh4eGhoaCgoKBgYGJiYmMjIyOjo6KioqNjY2Pj4+o6Lr0AAAAfnRSTlPfvxDPEL8gv5/Pr3AQ3++vn8+/cCCA34/vj8/vMJ+vgJ+vYN/fMIBAv4/v38/v39+PgBBg74DPgGAwMHDvrzBQj1AggJ8wIHC/IM9gjxBQgO+vv89Qz0C/70Bgz89ggIDvYN/fII8wIHDvMJ+/QBBAMBDPnxBgML9AEI/vQACT8cYwAAACXElEQVR42u2WV1caURRGJ73H9J7YW+y99957ARREURAFQRFEOFGMkybxN+eec2cWUwBnrTwli/1yvvnm7PvEuoMAf0FGzsj/nDwxOTUzdyoxNzM1OWFU7h0aPtUxPNRrQPZ0XKagw3OF7Nm9TMOuJ43cQmpavSWV3HRhgKakcmvjhSEaW/VyV/tvg7R3aeU+9ULZ/fLEQ/ndMvXbPrV88EvBvQdOMCsLMzjNd5TFgSTr3SpWOCuUTYWTVVV6m+Sdr0qqqVGxw6pqXUPyZlziFaU9Vi3HVSyzag+D/YlcbXLZLm+85AsOgMK4hkIABz/YkSVVdpS3fnKevQCIYgCcGqKslGd0g3dbIIR5fP8cZCkMdOANWeSLEJZklt5StxEWcmLIuw+AHGE+YiEWE67jGxnlO8xv8OGTEBGRRSACmNtYyBUjgcCCKJPLqjYMAeD04ENEGIjQ7AGikuUFNhdF8Vogj0T5bDyqEjh55AwI4N7/hmRTe4zRxEM+ZuKYFSYeEP9HzPtuEFjm9pKf9W5KQLbKhSVMbkymfHL90i+s/wR5PM9iCaYiLOcLTogCrKEIYwkLD19T25/4bR+unSG3bkOQwiG1QZfV6gryBaqL5c01XCCZ9lbOCOvNUpouUOGishSK+dpKUHMZ2M6Jz7ZHNEO+hOoLUWVZZROx6a8hn+VcRWh1EOtBUuhcPiy+pBdg3fZ3DaOj2ma7LtXVW1uj0XVqTW2aS9/bUP8jJfUN3is+N97mp8nV9Wavka9kZ/e6zuzuNP59Hhkbn53+QkzPjo+NZP6TZOSM/L/LfwAJ69Ba3fXmtwAAAABJRU5ErkJggg==); 42 | background-size: 30px 30px; 43 | } 44 | 45 | #epiceditor-utilbar button.epiceditor-toggle-edit-btn { 46 | background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAMAAAANIilAAAACNFBMVEWIiIiHh4eIiIiGhoaEhISDg4OCgoKDg4OMjIyNjY2MjIyMjIyLi4uMjIyKioqCgoKNjY2Ojo6Dg4ONjY2FhYWFhYWCgoKKioqKioqLi4uLi4uMjIyLi4uFhYWKioqKioqEhISJiYmJiYmEhISEhISFhYWEhISGhoaHh4eJiYmNjY2Hh4eKioqDg4ODg4OCgoKOjo6Ojo6Dg4OOjo6BgYGOjo6AgICCgoKNjY2Dg4OPj4+BgYGBgYGAgICOjo6BgYGOjo6AgICOjo6BgYGBgYGAgICPj4+QkJCQkJCQkJCQkJCPj4+NjY2IiIiIiIiFhYWFhYWEhISEhISGhoaDg4OGhoaDg4OHh4eDg4OCgoKMjIyLi4uLi4uKioqNjY2KioqNjY2NjY2NjY2Dg4OKioqGhoaJiYmIiIiGhoaFhYWEhISFhYWFhYWOjo6Pj4+FhYWGhoaPj4+Hh4eMjIyMjIyMjIyJiYmIiIiIiIiOjo6Ojo6CgoKBgYGPj4+QkJCHh4eHh4eEhISIiIiDg4OEhISDg4ODg4OLi4uHh4eEhISMjIyFhYWGhoaHh4eOjo6IiIiKioqKioqMjIyGhoaIiIiJiYmIiIiFhYWGhoaGhoaHh4eKioqJiYmLi4uIiIiGhoaJiYmHh4eGhoaGhoaJiYmFhYWKioqPj4+IiIiGhoYAAACAgICCgoKQkJCPj4+BgYGDg4OFhYWHh4eOjo6JiYmGhoaLi4uEhISIiIiNjY2MjIyKioqRGRgVAAAAq3RSTlNQz7+Pz4AQr4/vIK+Av99QICBQzyCv37/PcM+f378QgHAwgIBgnxBQMK+PgFCf34Bwn0BQv2Ag7zBwEFDvj4CAv1DvYK/f398gUICA39+fYHAwQHAQn78gz0DfYO8wgO+/YECPIK9gz89Av8/f31AwgK9A72AwIGCAMM+PICCvrxCfr+/fYCAgYO9QgGBQjyCPYM8QcM8wEEAgn58Qn+8w7+/fv0DvQEBA7wCxres+AAAC50lEQVR42u2XVUMbQRSFU3d3d3d3d8EpUKRoKbS40+LuDsEtQOFQKA2FEvhz3XsnS5IlJLB9a/s95J49M99sXpJsNPgD/st/oZwxJJGhTn4+RLxQJ78k9xnUyU+HJV6pk6OGiXR1ciy5sZizfPMrwzl9mIiak5x6/sLnUZmH9+9eqqAQCftyakXkqFUq7MpX6JbW2WBHDnxtmJUAIMiGfD3AYINArDdsCppNfmewxQqx4aRVObFm0ibLAW+aNYkz5ZL4SdusRIkI8SVKOcFyp/cu5ftYA6ySc4Kl3DZuxs4dhfAZV+CDQtNFm0lWuLsBFPoqXF9g9bjSZrlqypwqAC1TClqAtYprIVfLjT+f0gfAXyn7oY9GNZ/KiWVumX17OYYAIUp3O+AnDu7bZqxOk5zU+ZOpPwD0UABNBaEAaPYACBVCZ5Ik14vlg5ClVjFpC0NZ6lplGa0nxN2gSZkg2vtB9FOmKDUNCyemcTStMXXcpmh4yGUl5ToAORMOaGiflhtkoRKCfq41yTw+GFsHKeeIRUfwwbwKOo/eIATi3GQNivREVxyIZsqeADL1+swuvZEiAJ4UmsEUsVEODRAnNp2iulzekrVAbyJLPrYctJTJ7nGQjI7uMULXBIBjIyQWUWLeAGik0E39sQGKYU0QMmp1vGnADSjj0EFtk5uOjuKzOtgok8r34rxasMzEjDFhjdDJNsE7u2VXh9oYDgNllp/n8IgfzJbwXhq9zlRu5soZWtFFl/Zy8SkaljK0R6inJTEinLQo5aSFE889kkqUWvsCdM37ZcnHYnrNBhably5QyoJDtFuJK1xMF3mHgVlkHM2e4eYB02Xxfts/NwXBuSMWLIG7sTmbb/+Hzj3fy1wuQD6N3DMX5/g0VHDDQ3aXAR4jXsEb5/co9fbLN2KdlJbO/zmM5a0qH+KukXxOnfzoO5GmTr5M7mOoktP4xrfUyffIvQ118pNBiTvq5AeDxNV//W+CFX4Doxg1lHC8FNUAAAAASUVORK5CYII=); 47 | background-size: 30px 30px; 48 | } 49 | 50 | #epiceditor-utilbar button.epiceditor-fullscreen-btn { 51 | background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAMAAAANIilAAAAA5FBMVEWPj4+JiYmJiYmCgoKGhoaPj4+Li4uEhISAgICNjY2Li4uNjY2QkJCFhYWEhISLi4uHh4eHh4eIiIiHh4eGhoaHh4eEhISOjo6EhISJiYmFhYWJiYmFhYWNjY2NjY2Hh4eOjo6CgoKGhoaKioqKioqJiYmKioqDg4ODg4OBgYGGhoaKioqKioqLi4uMjIyMjIyIiIiIiIiMjIyOjo6GhoaDg4ONjY2EhISPj4+CgoIAAACJiYmHh4eIiIiFhYWCgoKPj4+Li4uAgICKioqEhISGhoaBgYGOjo6MjIyQkJCNjY2Dg4O2Jy/OAAAAO3RSTlMQz0DPEHDPz0DvMDBAQBBAQBAQz88wMM/v7zAw788Q7xAQQM9AEBDPEEAwMO/v7zDvMEBA70BAQEBAAAENmbYAAAG1SURBVHja7dftM0JREMdxD5GkEEqKpBJRSc8rQq7Q////2Dnm3t85jtkTmmGG75t7Z3c/t9fN0Tf6x1/Dz0ZrZGZf/BJ8a9QjM/vCwks9Px5aBcslC+P3nPVmi8dcczrcHHM2flug1CHRYcoYNd0YFlrAL1yHqPOC9g9IdbAfjHAjYFjoT+FI1MfRiAM/cZdEl0+oVidVvRaMcAOMgJWGBUYSVhrWjdfvjKqrq1Vzsu7Cy1Vo7XXZhYuj0ahwfHY+sjo/Oy7woyhitkTQsESsZawstG6VlvDCfIlUmfSVVjpDqtL8goBLbKFhsRcwalxcB100CDkxLLQbJxKwpvb3At7Y2iRuJzd4V26HuM2tDQkPWMOamu1Awkeetx2qtDy/lvZaCW173pGIWetA/xBbF+ZgiVgjGcdutLJ7xO1l9VnMiWGhJdzl4vx4CNpN+ifJXUy7RPEuZ2C1AIY1NG5kHI4Dx8MynnBtovYkqHzi25OyP8ONiKFh3djQsCIecn2i/lBvMU+UXxwi3HyE832jvD0RsLuP8CN3Oh0+feRmixE+g7CdLb43WiEz++Jb+M//x/gB/ArCl0G4nsFuEQAAAABJRU5ErkJggg==); 52 | background-size: 30px 30px; 53 | } 54 | } 55 | 56 | #epiceditor-utilbar button:last-child { 57 | margin-left:15px; 58 | } 59 | 60 | #epiceditor-utilbar button:hover { 61 | cursor:pointer; 62 | } 63 | 64 | .epiceditor-edit-mode #epiceditor-utilbar button.epiceditor-toggle-edit-btn { 65 | display:none; 66 | } 67 | 68 | .epiceditor-preview-mode #epiceditor-utilbar button.epiceditor-toggle-preview-btn { 69 | display:none; 70 | } 71 | -------------------------------------------------------------------------------- /front/static/epiceditor/themes/editor/epic-dark.css: -------------------------------------------------------------------------------- 1 | html { padding:10px; } 2 | 3 | body { 4 | border:0; 5 | background:rgb(41,41,41); 6 | font-family:monospace; 7 | font-size:14px; 8 | padding:10px; 9 | color:#ddd; 10 | line-height:1.35em; 11 | margin:0; 12 | padding:0; 13 | } 14 | -------------------------------------------------------------------------------- /front/static/epiceditor/themes/editor/epic-light.css: -------------------------------------------------------------------------------- 1 | html { padding:10px; } 2 | 3 | body { 4 | border:0; 5 | background:#fcfcfc; 6 | font-family:monospace; 7 | font-size:14px; 8 | padding:10px; 9 | line-height:1.35em; 10 | margin:0; 11 | padding:0; 12 | } 13 | -------------------------------------------------------------------------------- /front/static/epiceditor/themes/preview/bartik.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Georgia, "Times New Roman", Times, serif; 3 | line-height: 1.5; 4 | font-size: 87.5%; 5 | word-wrap: break-word; 6 | margin: 2em; 7 | padding: 0; 8 | border: 0; 9 | outline: 0; 10 | background: #fff; 11 | } 12 | 13 | h1, 14 | h2, 15 | h3, 16 | h4, 17 | h5, 18 | h6 { 19 | margin: 1.0em 0 0.5em; 20 | font-weight: inherit; 21 | } 22 | 23 | h1 { 24 | font-size: 1.357em; 25 | color: #000; 26 | } 27 | 28 | h2 { 29 | font-size: 1.143em; 30 | } 31 | 32 | p { 33 | margin: 0 0 1.2em; 34 | } 35 | 36 | del { 37 | text-decoration: line-through; 38 | } 39 | 40 | tr:nth-child(odd) { 41 | background-color: #dddddd; 42 | } 43 | 44 | img { 45 | outline: 0; 46 | } 47 | 48 | code { 49 | background-color: #f2f2f2; 50 | background-color: rgba(40, 40, 0, 0.06); 51 | } 52 | 53 | pre { 54 | background-color: #f2f2f2; 55 | background-color: rgba(40, 40, 0, 0.06); 56 | margin: 10px 0; 57 | overflow: hidden; 58 | padding: 15px; 59 | white-space: pre-wrap; 60 | } 61 | 62 | pre code { 63 | font-size: 100%; 64 | background-color: transparent; 65 | } 66 | 67 | blockquote { 68 | background: #f7f7f7; 69 | border-left: 1px solid #bbb; 70 | font-style: italic; 71 | margin: 1.5em 10px; 72 | padding: 0.5em 10px; 73 | } 74 | 75 | blockquote:before { 76 | color: #bbb; 77 | content: "\201C"; 78 | font-size: 3em; 79 | line-height: 0.1em; 80 | margin-right: 0.2em; 81 | vertical-align: -.4em; 82 | } 83 | 84 | blockquote:after { 85 | color: #bbb; 86 | content: "\201D"; 87 | font-size: 3em; 88 | line-height: 0.1em; 89 | vertical-align: -.45em; 90 | } 91 | 92 | blockquote > p:first-child { 93 | display: inline; 94 | } 95 | 96 | table { 97 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 98 | border: 0; 99 | border-spacing: 0; 100 | font-size: 0.857em; 101 | margin: 10px 0; 102 | width: 100%; 103 | } 104 | 105 | table table { 106 | font-size: 1em; 107 | } 108 | 109 | table tr th { 110 | background: #757575; 111 | background: rgba(0, 0, 0, 0.51); 112 | border-bottom-style: none; 113 | } 114 | 115 | table tr th, 116 | table tr th a, 117 | table tr th a:hover { 118 | color: #FFF; 119 | font-weight: bold; 120 | } 121 | 122 | table tbody tr th { 123 | vertical-align: top; 124 | } 125 | 126 | tr td, 127 | tr th { 128 | padding: 4px 9px; 129 | border: 1px solid #fff; 130 | text-align: left; /* LTR */ 131 | } 132 | 133 | tr:nth-child(odd) { 134 | background: #e4e4e4; 135 | background: rgba(0, 0, 0, 0.105); 136 | } 137 | 138 | tr, 139 | tr:nth-child(even) { 140 | background: #efefef; 141 | background: rgba(0, 0, 0, 0.063); 142 | } 143 | 144 | a { 145 | color: #0071B3; 146 | } 147 | 148 | a:hover, 149 | a:focus { 150 | color: #018fe2; 151 | } 152 | 153 | a:active { 154 | color: #23aeff; 155 | } 156 | 157 | a:link, 158 | a:visited { 159 | text-decoration: none; 160 | } 161 | 162 | a:hover, 163 | a:active, 164 | a:focus { 165 | text-decoration: underline; 166 | } 167 | 168 | -------------------------------------------------------------------------------- /front/static/epiceditor/themes/preview/github.css: -------------------------------------------------------------------------------- 1 | html { padding:0 10px; } 2 | 3 | body { 4 | margin:0; 5 | padding:0; 6 | background:#fff; 7 | } 8 | 9 | #epiceditor-wrapper{ 10 | background:white; 11 | } 12 | 13 | #epiceditor-preview{ 14 | padding-top:10px; 15 | padding-bottom:10px; 16 | font-family: Helvetica,arial,freesans,clean,sans-serif; 17 | font-size:13px; 18 | line-height:1.6; 19 | } 20 | 21 | #epiceditor-preview>*:first-child{ 22 | margin-top:0!important; 23 | } 24 | 25 | #epiceditor-preview>*:last-child{ 26 | margin-bottom:0!important; 27 | } 28 | 29 | #epiceditor-preview a{ 30 | color:#4183C4; 31 | text-decoration:none; 32 | } 33 | 34 | #epiceditor-preview a:hover{ 35 | text-decoration:underline; 36 | } 37 | 38 | #epiceditor-preview h1, 39 | #epiceditor-preview h2, 40 | #epiceditor-preview h3, 41 | #epiceditor-preview h4, 42 | #epiceditor-preview h5, 43 | #epiceditor-preview h6{ 44 | margin:20px 0 10px; 45 | padding:0; 46 | font-weight:bold; 47 | -webkit-font-smoothing:antialiased; 48 | } 49 | 50 | #epiceditor-preview h1 tt, 51 | #epiceditor-preview h1 code, 52 | #epiceditor-preview h2 tt, 53 | #epiceditor-preview h2 code, 54 | #epiceditor-preview h3 tt, 55 | #epiceditor-preview h3 code, 56 | #epiceditor-preview h4 tt, 57 | #epiceditor-preview h4 code, 58 | #epiceditor-preview h5 tt, 59 | #epiceditor-preview h5 code, 60 | #epiceditor-preview h6 tt, 61 | #epiceditor-preview h6 code{ 62 | font-size:inherit; 63 | } 64 | 65 | #epiceditor-preview h1{ 66 | font-size:28px; 67 | color:#000; 68 | } 69 | 70 | #epiceditor-preview h2{ 71 | font-size:24px; 72 | border-bottom:1px solid #ccc; 73 | color:#000; 74 | } 75 | 76 | #epiceditor-preview h3{ 77 | font-size:18px; 78 | } 79 | 80 | #epiceditor-preview h4{ 81 | font-size:16px; 82 | } 83 | 84 | #epiceditor-preview h5{ 85 | font-size:14px; 86 | } 87 | 88 | #epiceditor-preview h6{ 89 | color:#777; 90 | font-size:14px; 91 | } 92 | 93 | #epiceditor-preview p, 94 | #epiceditor-preview blockquote, 95 | #epiceditor-preview ul, 96 | #epiceditor-preview ol, 97 | #epiceditor-preview dl, 98 | #epiceditor-preview li, 99 | #epiceditor-preview table, 100 | #epiceditor-preview pre{ 101 | margin:15px 0; 102 | } 103 | 104 | #epiceditor-preview hr{ 105 | background:transparent url('../../images/modules/pulls/dirty-shade.png') repeat-x 0 0; 106 | border:0 none; 107 | color:#ccc; 108 | height:4px; 109 | padding:0; 110 | } 111 | 112 | #epiceditor-preview>h2:first-child, 113 | #epiceditor-preview>h1:first-child, 114 | #epiceditor-preview>h1:first-child+h2, 115 | #epiceditor-preview>h3:first-child, 116 | #epiceditor-preview>h4:first-child, 117 | #epiceditor-preview>h5:first-child, 118 | #epiceditor-preview>h6:first-child{ 119 | margin-top:0; 120 | padding-top:0; 121 | } 122 | 123 | #epiceditor-preview h1+p, 124 | #epiceditor-preview h2+p, 125 | #epiceditor-preview h3+p, 126 | #epiceditor-preview h4+p, 127 | #epiceditor-preview h5+p, 128 | #epiceditor-preview h6+p{ 129 | margin-top:0; 130 | } 131 | 132 | #epiceditor-preview li p.first{ 133 | display:inline-block; 134 | } 135 | 136 | #epiceditor-preview ul, 137 | #epiceditor-preview ol{ 138 | padding-left:30px; 139 | } 140 | 141 | #epiceditor-preview ul li>:first-child, 142 | #epiceditor-preview ol li>:first-child{ 143 | margin-top:0; 144 | } 145 | 146 | #epiceditor-preview ul li>:last-child, 147 | #epiceditor-preview ol li>:last-child{ 148 | margin-bottom:0; 149 | } 150 | 151 | #epiceditor-preview dl{ 152 | padding:0; 153 | } 154 | 155 | #epiceditor-preview dl dt{ 156 | font-size:14px; 157 | font-weight:bold; 158 | font-style:italic; 159 | padding:0; 160 | margin:15px 0 5px; 161 | } 162 | 163 | #epiceditor-preview dl dt:first-child{ 164 | padding:0; 165 | } 166 | 167 | #epiceditor-preview dl dt>:first-child{ 168 | margin-top:0; 169 | } 170 | 171 | #epiceditor-preview dl dt>:last-child{ 172 | margin-bottom:0; 173 | } 174 | 175 | #epiceditor-preview dl dd{ 176 | margin:0 0 15px; 177 | padding:0 15px; 178 | } 179 | 180 | #epiceditor-preview dl dd>:first-child{ 181 | margin-top:0; 182 | } 183 | 184 | #epiceditor-preview dl dd>:last-child{ 185 | margin-bottom:0; 186 | } 187 | 188 | #epiceditor-preview blockquote{ 189 | border-left:4px solid #DDD; 190 | padding:0 15px; 191 | color:#777; 192 | } 193 | 194 | #epiceditor-preview blockquote>:first-child{ 195 | margin-top:0; 196 | } 197 | 198 | #epiceditor-preview blockquote>:last-child{ 199 | margin-bottom:0; 200 | } 201 | 202 | #epiceditor-preview table{ 203 | padding:0; 204 | border-collapse: collapse; 205 | border-spacing: 0; 206 | font-size: 100%; 207 | font: inherit; 208 | } 209 | 210 | #epiceditor-preview table tr{ 211 | border-top:1px solid #ccc; 212 | background-color:#fff; 213 | margin:0; 214 | padding:0; 215 | } 216 | 217 | #epiceditor-preview table tr:nth-child(2n){ 218 | background-color:#f8f8f8; 219 | } 220 | 221 | #epiceditor-preview table tr th{ 222 | font-weight:bold; 223 | } 224 | 225 | #epiceditor-preview table tr th, 226 | #epiceditor-preview table tr td{ 227 | border:1px solid #ccc; 228 | text-align:left; 229 | margin:0; 230 | padding:6px 13px; 231 | } 232 | 233 | #epiceditor-preview table tr th>:first-child, 234 | #epiceditor-preview table tr td>:first-child{ 235 | margin-top:0; 236 | } 237 | 238 | #epiceditor-preview table tr th>:last-child, 239 | #epiceditor-preview table tr td>:last-child{ 240 | margin-bottom:0; 241 | } 242 | 243 | #epiceditor-preview img{ 244 | max-width:100%; 245 | } 246 | 247 | #epiceditor-preview span.frame{ 248 | display:block; 249 | overflow:hidden; 250 | } 251 | 252 | #epiceditor-preview span.frame>span{ 253 | border:1px solid #ddd; 254 | display:block; 255 | float:left; 256 | overflow:hidden; 257 | margin:13px 0 0; 258 | padding:7px; 259 | width:auto; 260 | } 261 | 262 | #epiceditor-preview span.frame span img{ 263 | display:block; 264 | float:left; 265 | } 266 | 267 | #epiceditor-preview span.frame span span{ 268 | clear:both; 269 | color:#333; 270 | display:block; 271 | padding:5px 0 0; 272 | } 273 | 274 | #epiceditor-preview span.align-center{ 275 | display:block; 276 | overflow:hidden; 277 | clear:both; 278 | } 279 | 280 | #epiceditor-preview span.align-center>span{ 281 | display:block; 282 | overflow:hidden; 283 | margin:13px auto 0; 284 | text-align:center; 285 | } 286 | 287 | #epiceditor-preview span.align-center span img{ 288 | margin:0 auto; 289 | text-align:center; 290 | } 291 | 292 | #epiceditor-preview span.align-right{ 293 | display:block; 294 | overflow:hidden; 295 | clear:both; 296 | } 297 | 298 | #epiceditor-preview span.align-right>span{ 299 | display:block; 300 | overflow:hidden; 301 | margin:13px 0 0; 302 | text-align:right; 303 | } 304 | 305 | #epiceditor-preview span.align-right span img{ 306 | margin:0; 307 | text-align:right; 308 | } 309 | 310 | #epiceditor-preview span.float-left{ 311 | display:block; 312 | margin-right:13px; 313 | overflow:hidden; 314 | float:left; 315 | } 316 | 317 | #epiceditor-preview span.float-left span{ 318 | margin:13px 0 0; 319 | } 320 | 321 | #epiceditor-preview span.float-right{ 322 | display:block; 323 | margin-left:13px; 324 | overflow:hidden; 325 | float:right; 326 | } 327 | 328 | #epiceditor-preview span.float-right>span{ 329 | display:block; 330 | overflow:hidden; 331 | margin:13px auto 0; 332 | text-align:right; 333 | } 334 | 335 | #epiceditor-preview code, 336 | #epiceditor-preview tt{ 337 | margin:0 2px; 338 | padding:0 5px; 339 | white-space:nowrap; 340 | border:1px solid #eaeaea; 341 | background-color:#f8f8f8; 342 | border-radius:3px; 343 | } 344 | 345 | #epiceditor-preview pre>code{ 346 | margin:0; 347 | padding:0; 348 | white-space:pre; 349 | border:none; 350 | background:transparent; 351 | } 352 | 353 | #epiceditor-preview .highlight pre, 354 | #epiceditor-preview pre{ 355 | background-color:#f8f8f8; 356 | border:1px solid #ccc; 357 | font-size:13px; 358 | line-height:19px; 359 | overflow:auto; 360 | padding:6px 10px; 361 | border-radius:3px; 362 | } 363 | 364 | #epiceditor-preview pre code, 365 | #epiceditor-preview pre tt{ 366 | background-color:transparent; 367 | border:none; 368 | } 369 | -------------------------------------------------------------------------------- /front/static/epiceditor/themes/preview/preview-dark.css: -------------------------------------------------------------------------------- 1 | html { padding:0 10px; } 2 | 3 | body { 4 | margin:0; 5 | padding:10px 0; 6 | background:#000; 7 | } 8 | 9 | #epiceditor-preview h1, 10 | #epiceditor-preview h2, 11 | #epiceditor-preview h3, 12 | #epiceditor-preview h4, 13 | #epiceditor-preview h5, 14 | #epiceditor-preview h6, 15 | #epiceditor-preview p, 16 | #epiceditor-preview blockquote { 17 | margin: 0; 18 | padding: 0; 19 | } 20 | #epiceditor-preview { 21 | background:#000; 22 | font-family: "Helvetica Neue", Helvetica, "Hiragino Sans GB", Arial, sans-serif; 23 | font-size: 13px; 24 | line-height: 18px; 25 | color: #ccc; 26 | } 27 | #epiceditor-preview a { 28 | color: #fff; 29 | } 30 | #epiceditor-preview a:hover { 31 | color: #00ff00; 32 | text-decoration: none; 33 | } 34 | #epiceditor-preview a img { 35 | border: none; 36 | } 37 | #epiceditor-preview p { 38 | margin-bottom: 9px; 39 | } 40 | #epiceditor-preview h1, 41 | #epiceditor-preview h2, 42 | #epiceditor-preview h3, 43 | #epiceditor-preview h4, 44 | #epiceditor-preview h5, 45 | #epiceditor-preview h6 { 46 | color: #cdcdcd; 47 | line-height: 36px; 48 | } 49 | #epiceditor-preview h1 { 50 | margin-bottom: 18px; 51 | font-size: 30px; 52 | } 53 | #epiceditor-preview h2 { 54 | font-size: 24px; 55 | } 56 | #epiceditor-preview h3 { 57 | font-size: 18px; 58 | } 59 | #epiceditor-preview h4 { 60 | font-size: 16px; 61 | } 62 | #epiceditor-preview h5 { 63 | font-size: 14px; 64 | } 65 | #epiceditor-preview h6 { 66 | font-size: 13px; 67 | } 68 | #epiceditor-preview hr { 69 | margin: 0 0 19px; 70 | border: 0; 71 | border-bottom: 1px solid #ccc; 72 | } 73 | #epiceditor-preview blockquote { 74 | padding: 13px 13px 21px 15px; 75 | margin-bottom: 18px; 76 | font-family:georgia,serif; 77 | font-style: italic; 78 | } 79 | #epiceditor-preview blockquote:before { 80 | content:"\201C"; 81 | font-size:40px; 82 | margin-left:-10px; 83 | font-family:georgia,serif; 84 | color:#eee; 85 | } 86 | #epiceditor-preview blockquote p { 87 | font-size: 14px; 88 | font-weight: 300; 89 | line-height: 18px; 90 | margin-bottom: 0; 91 | font-style: italic; 92 | } 93 | #epiceditor-preview code, #epiceditor-preview pre { 94 | font-family: Monaco, Andale Mono, Courier New, monospace; 95 | } 96 | #epiceditor-preview code { 97 | background-color: #000; 98 | color: #f92672; 99 | padding: 1px 3px; 100 | font-size: 12px; 101 | -webkit-border-radius: 3px; 102 | -moz-border-radius: 3px; 103 | border-radius: 3px; 104 | } 105 | #epiceditor-preview pre { 106 | display: block; 107 | padding: 14px; 108 | color:#66d9ef; 109 | margin: 0 0 18px; 110 | line-height: 16px; 111 | font-size: 11px; 112 | border: 1px solid #d9d9d9; 113 | white-space: pre-wrap; 114 | word-wrap: break-word; 115 | } 116 | #epiceditor-preview pre code { 117 | background-color: #000; 118 | color:#ccc; 119 | font-size: 11px; 120 | padding: 0; 121 | } 122 | -------------------------------------------------------------------------------- /front/static/front/css/front-edit.css: -------------------------------------------------------------------------------- 1 | .editable:hover { 2 | opacity: 1; 3 | /* In Webkit, outline doesn't fit the border curves, and in FF outline 4 | is ugly. Lets recreate it with box-shadow */ 5 | outline: none; 6 | 7 | /* \9 hack for IE8 and below. */ 8 | outline: 2px solid #639ACA\9; 9 | 10 | /* Old Safari doesn't do blur radius */ 11 | -webkit-box-shadow: 0 0 12px #659DCB; 12 | /* but Chrome does... */ 13 | -webkit-box-shadow: 0 0 1px 2px #659DCB, inset 0 0 1px 1px #659DCB; 14 | /* Create webkit-like outlines for FF */ 15 | -moz-box-shadow: 0 0 1px 2px #659DCB, inset 0 0 1px 1px #659DCB; 16 | box-shadow: 0 0 1px 2px #659DCB, inset 0 0 1px 1px #659DCB; 17 | } 18 | .editable { 19 | cursor:pointer; 20 | } 21 | .editable.empty-editable { 22 | min-height:5px; 23 | } 24 | .front-editing .editable, 25 | .front-editing .editable:hover { 26 | outline: none; 27 | -webkit-box-shadow: none; 28 | -moz-box-shadow: none; 29 | box-shadow: none; 30 | } 31 | 32 | .front-edit-buttons { 33 | text-align: right; 34 | } 35 | .front-edit-buttons button { 36 | margin: 3px 0 3px 3px; 37 | } 38 | 39 | .front-editing .editable > textarea, 40 | #front-edit-lightbox textarea { 41 | width: 100%; 42 | height: auto; 43 | min-height: 300px; 44 | font-family: Monaco, "Liberation Mono", Courier, monospace; 45 | font-size:12px; 46 | } 47 | 48 | .front-edit-ace > .front-edit-container { 49 | position: relative; 50 | width: 100%; 51 | height: 400px; 52 | } 53 | .front-edit-wym > .front-edit-container { 54 | width: 100%; 55 | display: block; 56 | } 57 | 58 | #front-edit-lightbox { 59 | width: 92%; 60 | max-width: 92%; 61 | } 62 | 63 | /* layer classes from Bolt */ 64 | 65 | /* .layer */ 66 | .front-edit-layer { 67 | /* Permit possible scrollbars. */ 68 | overflow: auto; 69 | 70 | /* Make layer occupy the whole of the area in its position parent. */ 71 | position: absolute; 72 | top: 0; 73 | right: 0; 74 | bottom: 0; 75 | left: 0; 76 | 77 | /* Height and width override the top, right, bottom, left declaration. 78 | To use height and width we must change the box model to make sure 79 | padding and border do not make the layer bigger than its container. 80 | The technique above does not work for iframes and can break as soon 81 | as height or width are declared, but the technique below will not 82 | work in IE7, where the box model can't be changed. */ 83 | width: 100%; 84 | height: 100%; 85 | 86 | /* Make the corners of layers the same as their parents', allowing you 87 | to use layers as screens over specific elements. */ 88 | -webkit-border-radius: inherit; 89 | -moz-border-radius: inherit; 90 | border-radius: inherit; 91 | } 92 | 93 | 94 | .front-edit-dialog_layer { 95 | position: fixed; 96 | 97 | /* Center inline-block contents horizontally */ 98 | text-align: center; 99 | 100 | background-image: none; 101 | background-color: rgba(31, 32, 33, 0.746094); 102 | 103 | /* Set this to be sure it's the uppermost thing on the page. */ 104 | z-index: 10; 105 | } 106 | 107 | .front-edit-dialog_layer:before { 108 | /* Center inline-block contents vertically */ 109 | display: inline-block; 110 | content: ''; 111 | height: 100%; 112 | width: 0; 113 | vertical-align: middle; 114 | } 115 | 116 | .front-edit-dialog_layer > * { 117 | text-align: left; 118 | } 119 | 120 | 121 | /* .dialog */ 122 | 123 | .front-edit-dialog { 124 | /* Act as position parent */ 125 | position: relative; 126 | 127 | /* Use inline-block to make dialog collapse 128 | to the width of its contents. */ 129 | display: inline-block; 130 | 131 | /* Center the dialog vertically, working in tandem 132 | with the :before rule of .dialog_layer. */ 133 | vertical-align: middle; 134 | 135 | /* But let's make sure it doesn't touch the top or 136 | bottom. Mainly for IE7. */ 137 | margin-top: 1.75em; 138 | margin-bottom: 1.75em; 139 | 140 | padding: 0.8333em; 141 | margin: 0; 142 | max-width: 92%; 143 | 144 | background-color: white; 145 | 146 | -webkit-border-radius: 0.3333em; 147 | -moz-border-radius: 0.3333em; 148 | border-radius: 0.3333em; 149 | 150 | -webkit-box-shadow: 0 2px 24px rgba(0,0,0,0.6); 151 | -moz-box-shadow: 0 2px 24px rgba(0,0,0,0.6); 152 | box-shadow: 0 2px 24px rgba(0,0,0,0.6); 153 | } 154 | 155 | .front-edit-dialog > img, 156 | .front-edit-dialog > svg, 157 | .front-edit-dialog > video, 158 | .front-edit-dialog > canvas, 159 | .front-edit-dialog > object, 160 | .front-edit-dialog > embed, 161 | .front-edit-dialog > iframe { 162 | /* Don't allow media to break out of container bounds. */ 163 | max-width: 100%; 164 | } 165 | 166 | .front-edit-dialog > img, 167 | .front-edit-dialog > svg, 168 | .front-edit-dialog > video, 169 | .front-edit-dialog > canvas { 170 | /* Allow images to rescale freely. */ 171 | height: auto; 172 | } 173 | -------------------------------------------------------------------------------- /front/static/front/js/front-edit.ace-local.js: -------------------------------------------------------------------------------- 1 | (function(jQuery){ 2 | window.front_edit_plugin = { 3 | 4 | target: null, 5 | editor: null, 6 | element_id: null, 7 | 8 | // Returns the html that will contain the editor 9 | get_container_html: function(element_id, front_edit_options) { 10 | this.element_id = element_id; 11 | return '
    '; 12 | }, 13 | 14 | // initializes the editor on the target element, with the given html code 15 | set_html: function(target, html, front_edit_options) { 16 | this.target = target; 17 | 18 | this.target.addClass('front-edit-ace'); 19 | this.editor = ace.edit("edit-" + this.element_id); 20 | this.editor.setTheme("ace/theme/tomorrow_night"); 21 | this.editor.getSession().setValue(html, -1); 22 | this.editor.getSession().setMode("ace/mode/html"); 23 | this.editor.getSession().setUseWrapMode(true); 24 | }, 25 | 26 | // returns the edited html code 27 | get_html: function(front_edit_options) { 28 | return this.editor.getValue(); 29 | }, 30 | 31 | // destroy the editor 32 | destroy_editor: function() { 33 | self.target = null; 34 | self.editor = null; 35 | self.element_id = null; 36 | } 37 | 38 | }; 39 | })(jQuery); 40 | -------------------------------------------------------------------------------- /front/static/front/js/front-edit.ace.js: -------------------------------------------------------------------------------- 1 | (function(jQuery){ 2 | window.front_edit_plugin = { 3 | 4 | target: null, 5 | editor: null, 6 | element_id: null, 7 | 8 | // Returns the html that will contain the editor 9 | get_container_html: function(element_id, front_edit_options) { 10 | this.element_id = element_id; 11 | return '
    '; 12 | }, 13 | 14 | 15 | 16 | // initializes the editor on the target element, with the given html code 17 | set_html: function(target, html, front_edit_options) { 18 | var this_ = this; 19 | this_.target = target; 20 | jQuery.getScript('https://cdnjs.cloudflare.com/ajax/libs/ace/1.1.9/ace.js', function(){ 21 | this_.target.addClass('front-edit-ace'); 22 | this_.editor = ace.edit("edit-" + this_.element_id); 23 | this_.editor.setTheme("ace/theme/monokai"); 24 | this_.editor.$blockScrolling = Infinity; 25 | this_.editor.setValue(html, -1); 26 | this_.editor.getSession().setMode("ace/mode/html"); 27 | this_.editor.getSession().setUseWrapMode(true); 28 | }); 29 | }, 30 | 31 | // returns the edited html code 32 | get_html: function(front_edit_options) { 33 | return this.editor.getValue(); 34 | }, 35 | 36 | // destroy the editor 37 | destroy_editor: function() { 38 | self.target = null; 39 | self.editor = null; 40 | self.element_id = null; 41 | } 42 | 43 | }; 44 | })(jQuery); 45 | -------------------------------------------------------------------------------- /front/static/front/js/front-edit.ckeditor.js: -------------------------------------------------------------------------------- 1 | (function(jQuery){ 2 | window.front_edit_plugin = { 3 | 4 | element_id: null, 5 | editor: null, 6 | 7 | // Returns the html that will contain the editor 8 | get_container_html: function(element_id, front_edit_options) { 9 | this.element_id = "edit-"+ element_id; 10 | return ''; 11 | }, 12 | 13 | // initializes the editor on the target element, with the given html code 14 | set_html: function(target, html, front_edit_options) { 15 | try { 16 | this.editor.setData(html); 17 | } catch(err) { 18 | jQuery('#'+ this.element_id).html(html); 19 | this.editor = CKEDITOR.replace(this.element_id, front_edit_options.editor_options); 20 | } 21 | }, 22 | 23 | // returns the edited html code 24 | get_html: function(front_edit_options) { 25 | return this.editor.getData(); 26 | }, 27 | 28 | // destroy the editor 29 | destroy_editor: function() { 30 | this.editor.destroy(); 31 | this.editor = null; 32 | this.element_id = null; 33 | } 34 | }; 35 | })(jQuery); 36 | -------------------------------------------------------------------------------- /front/static/front/js/front-edit.default.js: -------------------------------------------------------------------------------- 1 | (function(jQuery){ 2 | window.front_edit_plugin = { 3 | 4 | target: null, 5 | 6 | // Returns the html that will contain the editor 7 | get_container_html: function(element_id, front_edit_options) { 8 | return ''; 9 | }, 10 | 11 | // initializes the editor on the target element, with the given html code 12 | set_html: function(target, html, front_edit_options) { 13 | this.target = target; 14 | this.target.find('.front-edit-container').html(html); 15 | }, 16 | 17 | // returns the edited html code 18 | get_html: function(front_edit_options) { 19 | return this.target.find('.front-edit-container').val(); 20 | }, 21 | 22 | // destroy the editor 23 | destroy_editor: function() { 24 | self.target = null; 25 | } 26 | 27 | }; 28 | })(jQuery); 29 | -------------------------------------------------------------------------------- /front/static/front/js/front-edit.epiceditor.js: -------------------------------------------------------------------------------- 1 | (function(jQuery){ 2 | window.front_edit_plugin = { 3 | 4 | target: null, 5 | editor: null, 6 | element_id: null, 7 | 8 | // Returns the html that will contain the editor 9 | get_container_html: function(element_id, front_edit_options) { 10 | this.element_id = element_id; 11 | return '
    '; 12 | }, 13 | 14 | // initializes the editor on the target element, with the given html code 15 | set_html: function(target, html, front_edit_options) { 16 | var this_ = this; 17 | jQuery.when( 18 | jQuery.getScript(front_edit_options.static_root+'epiceditor/js/epiceditor.min.js'), 19 | jQuery.getScript(front_edit_options.static_root+'to-markdown/to-markdown.js'), 20 | jQuery.Deferred(function(deferred) { 21 | jQuery(deferred.resolve); 22 | }) 23 | ).done(function(){ 24 | var opts = jQuery.extend({ 25 | container: 'epiceditor', 26 | textarea: null, 27 | basePath: front_edit_options.static_root+'epiceditor', 28 | clientSideStorage: false, 29 | localStorageName: 'epiceditor', 30 | useNativeFullscreen: true, 31 | parser: marked, 32 | file: { 33 | name: 'epiceditor', 34 | defaultContent: toMarkdown(html), 35 | autoSave: 100 36 | }, 37 | button: { 38 | preview: true, 39 | fullscreen: true 40 | }, 41 | focusOnLoad: false, 42 | shortcut: { 43 | modifier: 18, 44 | fullscreen: 70, 45 | preview: 80 46 | }, 47 | string: { 48 | togglePreview: 'Toggle Preview Mode', 49 | toggleEdit: 'Toggle Edit Mode', 50 | toggleFullscreen: 'Enter Fullscreen' 51 | } 52 | }, front_edit_options.editor_options); 53 | this_.editor = new EpicEditor(opts).load(); 54 | }); 55 | }, 56 | 57 | // returns the edited html code 58 | get_html: function(front_edit_options) { 59 | isMarkdown = true; 60 | return this.editor.exportFile('', 'html'); 61 | }, 62 | 63 | // destroy the editor 64 | destroy_editor: function() { 65 | self.target = null; 66 | self.editor = null; 67 | self.element_id = null; 68 | } 69 | }; 70 | })(jQuery); 71 | -------------------------------------------------------------------------------- /front/static/front/js/front-edit.froala.js: -------------------------------------------------------------------------------- 1 | (function(jQuery){ 2 | window.front_edit_plugin = { 3 | 4 | element_id: null, 5 | editor: null, 6 | 7 | get_container_html: function(element_id, front_edit_options) { 8 | this.element_id = element_id; 9 | return ''; 10 | }, 11 | 12 | set_html: function(target, html, front_edit_options) { 13 | if (this.editor === null) { 14 | var editor_options = jQuery.extend({inlineMode: false}, front_edit_options.editor_options); 15 | this.editor = jQuery('#edit-'+ this.element_id).editable(editor_options); 16 | } 17 | 18 | this.editor.editable('setHTML', html); 19 | }, 20 | 21 | get_html: function(front_edit_options) { 22 | return this.editor.editable('getHTML'); 23 | }, 24 | 25 | // destroy the editor 26 | destroy_editor: function() { 27 | this.editor.editable('destroy'); 28 | this.editor = null; 29 | } 30 | }; 31 | })(jQuery); 32 | -------------------------------------------------------------------------------- /front/static/front/js/front-edit.js: -------------------------------------------------------------------------------- 1 | (function(jQuery){ 2 | var triggerEditor = function(el) { 3 | var body = jQuery('body'), 4 | front_edit_options = document._front_edit, 5 | html = el.html(), 6 | element_id = el.attr('id'), 7 | container, 8 | target; 9 | 10 | container = jQuery( 11 | front_edit_plugin.get_container_html(element_id, front_edit_options) + 12 | '

    ' 13 | ); 14 | 15 | if (body.is('.front-editing')) { 16 | return; 17 | } 18 | body.addClass('front-editing'); 19 | 20 | switch(front_edit_options.edit_mode) { 21 | case 'inline': 22 | el.html(container); 23 | target = el; 24 | break; 25 | 26 | case 'lightbox': 27 | jQuery('
    ').appendTo(jQuery('body')); 28 | var lightbox = jQuery('#front-edit-lightbox'); 29 | lightbox.html(container); 30 | target = lightbox; 31 | break; 32 | } 33 | 34 | front_edit_plugin.set_html(target, html, front_edit_options); 35 | 36 | target.find('.cancel').on('click', function(event) { 37 | el.html(html); 38 | body.removeClass('front-editing'); 39 | front_edit_plugin.destroy_editor(); 40 | jQuery('#front-edit-lightbox-container').remove(); 41 | }); 42 | 43 | target.find('.history').on('click', function(event) { 44 | var btn = jQuery(this); 45 | jQuery.getJSON(front_edit_options.history_url_prefix + element_id + '/', {}, function(json, textStatus) { 46 | if (json.history) { 47 | var current_val = front_edit_plugin.get_html(front_edit_options); 48 | 49 | btn.replaceWith(jQuery('')); 50 | var select = jQuery('.front-edit-history'); 51 | select.append( 52 | jQuery('') 53 | ); 54 | 55 | jQuery.each(json.history, function(index, val) { 56 | select.append( 57 | jQuery('') 58 | ); 59 | }); 60 | 61 | select.on('change', function(event) { 62 | var idx = jQuery(this).val(); 63 | if (idx == 0) { 64 | var html = current_val; 65 | } else { 66 | var html = json.history[idx - 1].value; 67 | } 68 | front_edit_plugin.set_html(target, html, front_edit_options); 69 | }); 70 | 71 | 72 | } else { 73 | btn.replaceWith(jQuery('No history')); 74 | } 75 | }); 76 | }); 77 | 78 | 79 | target.find('.save').on('click', function(event) { 80 | var key = element_id 81 | new_html = front_edit_plugin.get_html(front_edit_options); 82 | 83 | jQuery.post(front_edit_options.save_url, { 84 | key: key, 85 | val: new_html, 86 | csrfmiddlewaretoken: front_edit_options.csrf_token 87 | }, function(data, textStatus, xhr) { 88 | // todo: return val 89 | }); 90 | body.removeClass('front-editing'); 91 | el.html(new_html); 92 | // cleanup callback 93 | front_edit_plugin.destroy_editor(); 94 | jQuery('#front-edit-lightbox-container').remove(); 95 | }); 96 | }; 97 | 98 | jQuery(document).on('dblclick', '.editable', function(event) { 99 | event.preventDefault(); 100 | var el = jQuery(this); 101 | triggerEditor(el); 102 | }); 103 | })(jQuery); 104 | -------------------------------------------------------------------------------- /front/static/front/js/front-edit.medium.js: -------------------------------------------------------------------------------- 1 | (function(jQuery){ 2 | window.front_edit_plugin = { 3 | 4 | target: null, 5 | 6 | // Returns the html that will contain the editor 7 | get_container_html: function(element_id, front_edit_options) { 8 | return '
    '; 9 | }, 10 | 11 | // initializes the editor on the target element, with the given html code 12 | set_html: function(target, html, front_edit_options) { 13 | this.target = target; 14 | this.target.find('.front-edit-container').html(html); 15 | var editor_options = jQuery.extend({ 16 | toolbar: { 17 | buttons: ['bold', 'italic', 'underline', 'anchor', 'h1', 'h2', 'h3', 'quote', 'removeFormat'], 18 | } 19 | }, front_edit_options.editor_options); 20 | var editor = new MediumEditor('.front-edit-container', editor_options); 21 | }, 22 | 23 | // returns the edited html code 24 | get_html: function(front_edit_options) { 25 | return this.target.find('.front-edit-container').html(); 26 | }, 27 | 28 | // destroy the editor 29 | destroy_editor: function() { 30 | self.target = null; 31 | } 32 | 33 | }; 34 | })(jQuery); 35 | -------------------------------------------------------------------------------- /front/static/front/js/front-edit.redactor.js: -------------------------------------------------------------------------------- 1 | (function(jQuery){ 2 | 3 | window.front_edit_plugin = { 4 | 5 | target: null, 6 | 7 | get_container_html: function(element_id, front_edit_options) { 8 | return ''; 9 | }, 10 | 11 | __init_editor: function(target, html, front_edit_options) { 12 | this.target = target; 13 | this.target.addClass('front-edit-redactor'); 14 | var editor_options = jQuery.extend({minHeight:400}, front_edit_options.editor_options); 15 | this.target.find('.front-edit-container').html(html).redactor(editor_options); 16 | this.initialized = true; 17 | }, 18 | 19 | set_html: function(target, html, front_edit_options) { 20 | try { 21 | this.target.find('.front-edit-container').redactor('set', html); 22 | } catch(err) { 23 | try { 24 | this.target.find('.front-edit-container').redactor('code.set', html); 25 | } catch(err) { 26 | this.__init_editor(target, html, front_edit_options); 27 | } 28 | } 29 | }, 30 | 31 | get_html: function(front_edit_options) { 32 | var new_html = null; 33 | 34 | // there doesn't seem to be a way of finding the 35 | // version of the currently installed Redactor API 36 | // fall down to trial and error 37 | try { 38 | // redactor 0.8+ 39 | new_html = this.target.find('.front-edit-container').getCode(); 40 | } catch(err) { 41 | // redactor 0.9+ 42 | try { 43 | new_html = this.target.find('.front-edit-container').redactor('get'); 44 | } catch(err) { 45 | // redactor 10+ 46 | new_html = this.target.find('.front-edit-container').redactor('code.get'); 47 | } 48 | } 49 | 50 | return new_html; 51 | }, 52 | 53 | // destroy the editor 54 | destroy_editor: function() { 55 | self.target = null; 56 | } 57 | 58 | }; 59 | })(jQuery); 60 | -------------------------------------------------------------------------------- /front/static/front/js/front-edit.summernote.js: -------------------------------------------------------------------------------- 1 | (function(jQuery){ 2 | window.front_edit_plugin = { 3 | 4 | element_id: null, 5 | editor: null, 6 | 7 | get_container_html: function(element_id, front_edit_options) { 8 | this.element_id = element_id; 9 | return '
    '; 10 | }, 11 | 12 | set_html: function(target, html, front_edit_options) { 13 | if (this.editor === null) { 14 | var editor_options = jQuery.extend({ 15 | height: 300, 16 | minHeight: null, 17 | maxHeight: null, 18 | focus: true 19 | }, front_edit_options.editor_options); 20 | this.editor = jQuery('#edit-'+ this.element_id).summernote(editor_options); 21 | } 22 | 23 | this.editor.summernote('code', html); 24 | }, 25 | 26 | get_html: function(front_edit_options) { 27 | return this.editor.summernote('code'); 28 | }, 29 | 30 | // destroy the editor 31 | destroy_editor: function() { 32 | if (this.editor !== null) { 33 | this.editor.summernote('destroy'); 34 | this.editor = null; 35 | } 36 | } 37 | }; 38 | })(jQuery); 39 | -------------------------------------------------------------------------------- /front/static/front/js/front-edit.wymeditor.js: -------------------------------------------------------------------------------- 1 | (function(jQuery){ 2 | window.front_edit_plugin = { 3 | 4 | target: null, 5 | editor: null, 6 | element_id: null, 7 | 8 | // Returns the html that will contain the editor 9 | get_container_html: function(element_id, front_edit_options) { 10 | this.element_id = element_id; 11 | return ''; 12 | }, 13 | 14 | // initializes the editor on the target element, with the given html code 15 | set_html: function(target, html, front_edit_options) { 16 | this.target = target; 17 | var this_ = this; 18 | 19 | try { 20 | jQuery.wymeditors(0).html(html); 21 | } catch(err) { 22 | this_.target.find('.front-edit-container').html(html); 23 | jQuery.getScript(front_edit_options.static_root + 'wymeditor/jquery.wymeditor.min.js', function(){ 24 | this_.target.addClass('front-edit-wym'); 25 | var base_path = front_edit_options.static_root+'wymeditor/'; 26 | var editor_options = jQuery.extend({ 27 | updateSelector: "input:submit", 28 | updateEvent: "click", 29 | logoHtml: '', 30 | skin: 'django', 31 | classesItems: [ 32 | {'name': 'image', 'title': 'DIV: Image w/ Caption', 'expr': 'div'}, 33 | {'name': 'caption', 'title': 'P: Caption', 'expr': 'p'}, 34 | {'name': 'align-left', 'title': 'Float: Left', 'expr': 'p, div, img'}, 35 | {'name': 'align-right', 'title': 'Float: Right', 'expr': 'p, div, img'} 36 | ], 37 | basePath: base_path, 38 | wymPath: base_path + 'jquery.wymeditor.min.js', 39 | skinPath: front_edit_options.static_root + 'wym/django/' 40 | 41 | }, front_edit_options.editor_options); 42 | jQuery('#edit-' + this_.element_id).wymeditor(editor_options); 43 | }); 44 | } 45 | }, 46 | 47 | // returns the edited html code 48 | get_html: function(front_edit_options) { 49 | return jQuery.wymeditors(0).xhtml(); 50 | }, 51 | 52 | // destroy the editor 53 | destroy_editor: function() { 54 | self.target = null; 55 | self.editor = null; 56 | self.element_id = null; 57 | jQuery.wymeditors = null; 58 | } 59 | }; 60 | })(jQuery); 61 | -------------------------------------------------------------------------------- /front/static/to-markdown/to-markdown.js: -------------------------------------------------------------------------------- 1 | /* 2 | * to-markdown - an HTML to Markdown converter 3 | * 4 | * Copyright 2011, Dom Christie 5 | * Licenced under the MIT licence 6 | * 7 | */ 8 | 9 | var toMarkdown = function(string) { 10 | 11 | var ELEMENTS = [ 12 | { 13 | patterns: 'p', 14 | replacement: function(str, attrs, innerHTML) { 15 | return innerHTML ? '\n\n' + innerHTML + '\n' : ''; 16 | } 17 | }, 18 | { 19 | patterns: 'br', 20 | type: 'void', 21 | replacement: '\n' 22 | }, 23 | { 24 | patterns: 'h([1-6])', 25 | replacement: function(str, hLevel, attrs, innerHTML) { 26 | var hPrefix = ''; 27 | for(var i = 0; i < hLevel; i++) { 28 | hPrefix += '#'; 29 | } 30 | return '\n\n' + hPrefix + ' ' + innerHTML + '\n'; 31 | } 32 | }, 33 | { 34 | patterns: 'hr', 35 | type: 'void', 36 | replacement: '\n\n* * *\n' 37 | }, 38 | { 39 | patterns: 'a', 40 | replacement: function(str, attrs, innerHTML) { 41 | var href = attrs.match(attrRegExp('href')), 42 | title = attrs.match(attrRegExp('title')); 43 | return href ? '[' + innerHTML + ']' + '(' + href[1] + (title && title[1] ? ' "' + title[1] + '"' : '') + ')' : str; 44 | } 45 | }, 46 | { 47 | patterns: ['b', 'strong'], 48 | replacement: function(str, attrs, innerHTML) { 49 | return innerHTML ? '**' + innerHTML + '**' : ''; 50 | } 51 | }, 52 | { 53 | patterns: ['i', 'em'], 54 | replacement: function(str, attrs, innerHTML) { 55 | return innerHTML ? '_' + innerHTML + '_' : ''; 56 | } 57 | }, 58 | { 59 | patterns: 'code', 60 | replacement: function(str, attrs, innerHTML) { 61 | return innerHTML ? '`' + innerHTML + '`' : ''; 62 | } 63 | }, 64 | { 65 | patterns: 'img', 66 | type: 'void', 67 | replacement: function(str, attrs, innerHTML) { 68 | var src = attrs.match(attrRegExp('src')), 69 | alt = attrs.match(attrRegExp('alt')), 70 | title = attrs.match(attrRegExp('title')); 71 | return '![' + (alt && alt[1] ? alt[1] : '') + ']' + '(' + src[1] + (title && title[1] ? ' "' + title[1] + '"' : '') + ')'; 72 | } 73 | } 74 | ]; 75 | 76 | for(var i = 0, len = ELEMENTS.length; i < len; i++) { 77 | if(typeof ELEMENTS[i].patterns === 'string') { 78 | string = replaceEls(string, { tag: ELEMENTS[i].patterns, replacement: ELEMENTS[i].replacement, type: ELEMENTS[i].type }); 79 | } 80 | else { 81 | for(var j = 0, pLen = ELEMENTS[i].patterns.length; j < pLen; j++) { 82 | string = replaceEls(string, { tag: ELEMENTS[i].patterns[j], replacement: ELEMENTS[i].replacement, type: ELEMENTS[i].type }); 83 | } 84 | } 85 | } 86 | 87 | function replaceEls(html, elProperties) { 88 | var pattern = elProperties.type === 'void' ? '<' + elProperties.tag + '\\b([^>]*)\\/?>' : '<' + elProperties.tag + '\\b([^>]*)>([\\s\\S]*?)<\\/' + elProperties.tag + '>', 89 | regex = new RegExp(pattern, 'gi'), 90 | markdown = ''; 91 | if(typeof elProperties.replacement === 'string') { 92 | markdown = html.replace(regex, elProperties.replacement); 93 | } 94 | else { 95 | markdown = html.replace(regex, function(str, p1, p2, p3) { 96 | return elProperties.replacement.call(this, str, p1, p2, p3); 97 | }); 98 | } 99 | return markdown; 100 | } 101 | 102 | function attrRegExp(attr) { 103 | return new RegExp(attr + '\\s*=\\s*["\']?([^"\']*)["\']?', 'i'); 104 | } 105 | 106 | // Pre code blocks 107 | 108 | string = string.replace(/]*>`([\s\S]*)`<\/pre>/gi, function(str, innerHTML) { 109 | innerHTML = innerHTML.replace(/^\t+/g, ' '); // convert tabs to spaces (you know it makes sense) 110 | innerHTML = innerHTML.replace(/\n/g, '\n '); 111 | return '\n\n ' + innerHTML + '\n'; 112 | }); 113 | 114 | // Lists 115 | 116 | // Escape numbers that could trigger an ol 117 | // If there are more than three spaces before the code, it would be in a pre tag 118 | // Make sure we are escaping the period not matching any character 119 | string = string.replace(/^(\s{0,3}\d+)\. /g, '$1\\. '); 120 | 121 | // Converts lists that have no child lists (of same type) first, then works it's way up 122 | var noChildrenRegex = /<(ul|ol)\b[^>]*>(?:(?!/gi; 123 | while(string.match(noChildrenRegex)) { 124 | string = string.replace(noChildrenRegex, function(str) { 125 | return replaceLists(str); 126 | }); 127 | } 128 | 129 | function replaceLists(html) { 130 | 131 | html = html.replace(/<(ul|ol)\b[^>]*>([\s\S]*?)<\/\1>/gi, function(str, listType, innerHTML) { 132 | var lis = innerHTML.split(''); 133 | lis.splice(lis.length - 1, 1); 134 | 135 | for(i = 0, len = lis.length; i < len; i++) { 136 | if(lis[i]) { 137 | var prefix = (listType === 'ol') ? (i + 1) + ". " : "* "; 138 | lis[i] = lis[i].replace(/\s*]*>([\s\S]*)/i, function(str, innerHTML) { 139 | 140 | innerHTML = innerHTML.replace(/^\s+/, ''); 141 | innerHTML = innerHTML.replace(/\n\n/g, '\n\n '); 142 | // indent nested lists 143 | innerHTML = innerHTML.replace(/\n([ ]*)+(\*|\d+\.) /g, '\n$1 $2 '); 144 | return prefix + innerHTML; 145 | }); 146 | } 147 | } 148 | return lis.join('\n'); 149 | }); 150 | return '\n\n' + html.replace(/[ \t]+\n|\s+$/g, ''); 151 | } 152 | 153 | // Blockquotes 154 | var deepest = /]*>((?:(?!/gi; 155 | while(string.match(deepest)) { 156 | string = string.replace(deepest, function(str) { 157 | return replaceBlockquotes(str); 158 | }); 159 | } 160 | 161 | function replaceBlockquotes(html) { 162 | html = html.replace(/]*>([\s\S]*?)<\/blockquote>/gi, function(str, inner) { 163 | inner = inner.replace(/^\s+|\s+$/g, ''); 164 | inner = cleanUp(inner); 165 | inner = inner.replace(/^/gm, '> '); 166 | inner = inner.replace(/^(>([ \t]{2,}>)+)/gm, '> >'); 167 | return inner; 168 | }); 169 | return html; 170 | } 171 | 172 | function cleanUp(string) { 173 | string = string.replace(/^[\t\r\n]+|[\t\r\n]+$/g, ''); // trim leading/trailing whitespace 174 | string = string.replace(/\n\s+\n/g, '\n\n'); 175 | string = string.replace(/\n{3,}/g, '\n\n'); // limit consecutive linebreaks to 2 176 | return string; 177 | } 178 | 179 | return cleanUp(string); 180 | }; 181 | 182 | if (typeof exports === 'object') { 183 | exports.toMarkdown = toMarkdown; 184 | } 185 | -------------------------------------------------------------------------------- /front/static/wym/django/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbi/django-front/0ed620dcd7f0e4ce11f62b58149b878f3dfb3eb6/front/static/wym/django/icons.png -------------------------------------------------------------------------------- /front/static/wym/django/skin.css: -------------------------------------------------------------------------------- 1 | /* 2 | * WYMeditor : what you see is What You Mean web-based editor 3 | * Copyright (c) 2008 Jean-Francois Hovinne, http://www.wymeditor.org/ 4 | * Dual licensed under the MIT (MIT-license.txt) 5 | * and GPL (GPL-license.txt) licenses. 6 | * 7 | * For further information visit: 8 | * http://www.wymeditor.org/ 9 | * 10 | * File Name: 11 | * skin.css 12 | * main stylesheet for the django WYMeditor skin 13 | * See the documentation for more info. 14 | * 15 | * File Authors: 16 | * Daniel Reszka (d.reszka a-t wymeditor dotorg) 17 | * Jannis Leidel (jannis@leidel.info) 18 | */ 19 | 20 | /*TRYING TO RESET STYLES THAT MAY INTERFERE WITH WYMEDITOR*/ 21 | .wym_skin_django p, .wym_skin_django h2, .wym_skin_django h3, 22 | .wym_skin_django ul, .wym_skin_django li { background: transparent; margin: 0; padding: 0; border-width:0; list-style: none; } 23 | 24 | 25 | /*HIDDEN BY DEFAULT*/ 26 | .wym_skin_django .wym_area_left { display: none; } 27 | .wym_skin_django .wym_area_right { display: block; } 28 | 29 | 30 | /*TYPO*/ 31 | .wym_skin_django { font-size: 62.5%; font-family: "Lucida Grande", "DejaVu Sans", "Bitstream Vera Sans", Verdana, Arial, sans-serif } 32 | .wym_skin_django h2 { font-size: 110%; /* = 11px */} 33 | .wym_skin_django h3 { font-size: 100%; /* = 10px */} 34 | .wym_skin_django li { font-size: 100%; /* = 10px */} 35 | 36 | 37 | /*WYM_BOX*/ 38 | .wym_skin_django { border: 0px; background: none; padding: 10px} 39 | 40 | /*auto-clear the wym_box*/ 41 | .wym_skin_django:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } 42 | * html .wym_skin_django { height: 1%;} 43 | 44 | 45 | /*WYM_HTML*/ 46 | .wym_skin_django .wym_html { width: 98%;} 47 | .wym_skin_django .wym_html textarea { width: 100%; height: 200px; border: 1px solid #ccc; background: white; } 48 | 49 | 50 | /*WYM_IFRAME*/ 51 | .wym_skin_django .wym_iframe { width: 98%;} 52 | .wym_skin_django .wym_iframe iframe { width: 100%; height: 200px; border: 1px solid #ccc; background: white } 53 | 54 | 55 | /*AREAS*/ 56 | .wym_skin_django .wym_area_left { width: 150px; float: left;} 57 | .wym_skin_django .wym_area_right { width: 150px; float: right;} 58 | .wym_skin_django .wym_area_bottom { clear: both; display: none;} 59 | * html .wym_skin_django .wym_area_main { height: 1%;} 60 | * html .wym_skin_django .wym_area_top { height: 1%;} 61 | *+html .wym_skin_django .wym_area_top { height: 1%;} 62 | 63 | /*SECTIONS SYSTEM*/ 64 | 65 | /*common defaults for all sections*/ 66 | .wym_skin_django .wym_section { margin-bottom: 5px; } 67 | .wym_skin_django .wym_section h2, 68 | .wym_skin_django .wym_section h3 { padding: 1px 3px; margin: 0; color:#333; } 69 | .wym_skin_django .wym_section a { padding: 0 3px; display: block; text-decoration: none; color: black; } 70 | .wym_skin_django .wym_section a:hover { background-color: #eaeaea; } 71 | /*hide section titles by default*/ 72 | .wym_skin_django .wym_section h2 { display: none; } 73 | /*disable any margin-collapse*/ 74 | .wym_skin_django .wym_section { padding-top: 1px; padding-bottom: 1px; } 75 | /*auto-clear sections*/ 76 | .wym_skin_django .wym_section ul:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } 77 | * html .wym_skin_django .wym_section ul { height: 1%;} 78 | 79 | /*option: add this class to a section to make it render as a panel*/ 80 | .wym_skin_django .wym_panel { } 81 | .wym_skin_django .wym_panel h2 { display: block; } 82 | 83 | /*option: add this class to a section to make it render as a dropdown menu*/ 84 | .wym_skin_django .wym_dropdown h2 { display: block; } 85 | .wym_skin_django .wym_dropdown ul { display: none; position: absolute; background: white; } 86 | .wym_skin_django .wym_dropdown:hover ul, 87 | .wym_skin_django .wym_dropdown.hover ul { display: block; } 88 | 89 | .wym_skin_django .wym_tools ul { padding-left: 0px !important; margin-left: 0px; } 90 | 91 | /*option: add this class to a section to make its elements render buttons (icons are only available for the wym_tools section for now)*/ 92 | .wym_skin_django .wym_buttons li { float:left;} 93 | .wym_skin_django .wym_buttons a { width: 20px; height: 20px; overflow: hidden; padding: 2px } 94 | /*image replacements*/ 95 | .wym_skin_django .wym_buttons li a { background: url(icons.png) no-repeat; text-indent: -9999px;} 96 | .wym_skin_django .wym_buttons li.wym_tools_strong a { background-position: 0 -382px;} 97 | .wym_skin_django .wym_buttons li.wym_tools_emphasis a { background-position: 0 -22px;} 98 | .wym_skin_django .wym_buttons li.wym_tools_superscript a { background-position: 0 -430px;} 99 | .wym_skin_django .wym_buttons li.wym_tools_subscript a { background-position: 0 -454px;} 100 | .wym_skin_django .wym_buttons li.wym_tools_ordered_list a { background-position: 0 -48px;} 101 | .wym_skin_django .wym_buttons li.wym_tools_unordered_list a{ background-position: 0 -72px;} 102 | .wym_skin_django .wym_buttons li.wym_tools_indent a { background-position: 0 -574px;} 103 | .wym_skin_django .wym_buttons li.wym_tools_outdent a { background-position: 0 -598px;} 104 | .wym_skin_django .wym_buttons li.wym_tools_undo a { background-position: 0 -502px;} 105 | .wym_skin_django .wym_buttons li.wym_tools_redo a { background-position: 0 -526px;} 106 | .wym_skin_django .wym_buttons li.wym_tools_link a { background-position: 0 -96px;} 107 | .wym_skin_django .wym_buttons li.wym_tools_unlink a { background-position: 0 -168px;} 108 | .wym_skin_django .wym_buttons li.wym_tools_image a { background-position: 0 -121px;} 109 | .wym_skin_django .wym_buttons li.wym_tools_table a { background-position: 0 -144px;} 110 | .wym_skin_django .wym_buttons li.wym_tools_paste a { background-position: 0 -552px;} 111 | .wym_skin_django .wym_buttons li.wym_tools_html a { background-position: 0 -193px;} 112 | .wym_skin_django .wym_buttons li.wym_tools_preview a { background-position: 0 -408px;} 113 | 114 | /*DECORATION*/ 115 | .wym_skin_django .wym_section h2 { background: #fff;} 116 | .wym_skin_django .wym_section h2 span { color: gray;} 117 | .wym_skin_django .wym_panel { padding: 0; background: white;} 118 | .wym_skin_django .wym_panel ul { margin: 2px 0 5px; } 119 | .wym_skin_django .wym_dropdown { padding: 0; border: solid gray; border-width: 1px 1px 0 1px; } 120 | .wym_skin_django .wym_dropdown ul { border: solid gray; border-width: 0 1px 1px 1px; margin-left: -1px; padding: 5px 10px 5px 3px;} 121 | 122 | /*DIALOGS*/ 123 | .wym_dialog div.row { margin-bottom: 5px;} 124 | .wym_dialog div.row input { margin-right: 5px;} 125 | .wym_dialog div.row label { float: left; width: 150px; display: block; text-align: right; margin-right: 10px; } 126 | .wym_dialog div.row-indent { padding-left: 160px; } 127 | /*autoclearing*/ 128 | .wym_dialog div.row:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } 129 | .wym_dialog div.row { display: inline-block; } 130 | /* Hides from IE-mac \*/ 131 | * html .wym_dialog div.row { height: 1%; } 132 | .wym_dialog div.row { display: block; } 133 | /* End hide from IE-mac */ 134 | 135 | /*WYMEDITOR_LINK*/ 136 | a.wym_wymeditor_link { text-indent: -9999px; float: right; display: block; width: 50px; height: 15px; background: url(../wymeditor_icon.png); overflow: hidden; text-decoration: none; } 137 | 138 | /* PAGELINK */ 139 | .wym_select_pagelink { width: 330px; } 140 | .wym_select_pagelink option.selected { background-color: #e3d2c5; } 141 | 142 | -------------------------------------------------------------------------------- /front/static/wym/django/skin.js: -------------------------------------------------------------------------------- 1 | WYMeditor.SKINS['django'] = { 2 | 3 | init: function(wym) { 4 | 5 | //render following sections as panels 6 | jQuery(wym._box).find(wym._options.classesSelector) 7 | .addClass("wym_panel"); 8 | 9 | //render following sections as buttons 10 | jQuery(wym._box).find(wym._options.toolsSelector) 11 | .addClass("wym_buttons"); 12 | 13 | //render following sections as dropdown menus 14 | jQuery(wym._box).find(wym._options.containersSelector) 15 | .addClass("wym_panel") 16 | .find(WYMeditor.H2) 17 | .append(" >"); 18 | 19 | // auto add some margin to the main area sides if left area 20 | // or right area are not empty (if they contain sections) 21 | jQuery(wym._box).find("div.wym_area_right ul") 22 | .parents("div.wym_area_right").show() 23 | .parents(wym._options.boxSelector) 24 | .find("div.wym_area_main") 25 | .css({"margin-right": "155px"}); 26 | 27 | jQuery(wym._box).find("div.wym_area_left ul") 28 | .parents("div.wym_area_left").show() 29 | .parents(wym._options.boxSelector) 30 | .find("div.wym_area_main") 31 | .css({"margin-left": "155px"}); 32 | 33 | //make hover work under IE < 7 34 | jQuery(wym._box).find(".wym_section").hover(function(){ 35 | jQuery(this).addClass("hover"); 36 | },function(){ 37 | jQuery(this).removeClass("hover"); 38 | }); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /front/static/wym/wymeditor_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbi/django-front/0ed620dcd7f0e4ce11f62b58149b878f3dfb3eb6/front/static/wym/wymeditor_icon.png -------------------------------------------------------------------------------- /front/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbi/django-front/0ed620dcd7f0e4ce11f62b58149b878f3dfb3eb6/front/templatetags/__init__.py -------------------------------------------------------------------------------- /front/templatetags/front_tags.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django import template 4 | from django.core.cache import cache 5 | from django.core.exceptions import ImproperlyConfigured 6 | from django.urls import NoReverseMatch, reverse # NOQA 7 | from django.utils.encoding import smart_str 8 | from django.utils.html import strip_tags 9 | 10 | import six 11 | from classytags.arguments import Argument, KeywordArgument, MultiValueArgument 12 | from classytags.core import Options, Tag 13 | 14 | from ..conf import settings as django_front_settings 15 | from ..models import Placeholder 16 | 17 | 18 | register = template.Library() 19 | 20 | 21 | class FrontEditTag(Tag): 22 | name = 'front_edit' 23 | options = Options( 24 | Argument('name', resolve=False, required=True), 25 | MultiValueArgument('extra_bits', required=False, resolve=True), 26 | blocks=[('end_front_edit', 'nodelist')], 27 | ) 28 | 29 | def render_tag(self, context, name, extra_bits, nodelist=None): 30 | hash_val = Placeholder.key_for(name, *extra_bits) 31 | cache_key = "front-edit-%s" % hash_val 32 | 33 | val = cache.get(cache_key) 34 | if val is None: 35 | try: 36 | val = Placeholder.objects.get(key=hash_val).value 37 | cache.set(cache_key, val, 3600 * 24) 38 | except Placeholder.DoesNotExist: 39 | pass 40 | 41 | if val is None and nodelist: 42 | val = nodelist.render(context) 43 | 44 | classes = ['editable'] 45 | 46 | if getattr(django_front_settings, 'DJANGO_FRONT_EXTRA_CONTAINER_CLASSES', None): 47 | classes.append( 48 | six.text_type(django_front_settings.DJANGO_FRONT_EXTRA_CONTAINER_CLASSES) 49 | ) 50 | 51 | user = context.get('request', None) and context.get('request').user 52 | if django_front_settings.DJANGO_FRONT_PERMISSION(user): 53 | render = six.text_type(smart_str(val)).strip() 54 | if not strip_tags(render).strip(): 55 | classes.append('empty-editable') 56 | 57 | return '
    %s
    ' % ( 58 | ' '.join(classes), 59 | hash_val, 60 | render, 61 | ) 62 | return val or '' 63 | 64 | 65 | class FrontEditJS(Tag): 66 | name = 'front_edit_scripts' 67 | options = Options( 68 | KeywordArgument('editor', resolve=False, required=False, defaultkey='editor') 69 | ) 70 | 71 | def render_tag(self, context, editor=''): 72 | try: 73 | save_url = reverse('front-placeholder-save') 74 | history_url = reverse('front-placeholder-history', args=('0000',)) 75 | except NoReverseMatch: 76 | raise ImproperlyConfigured( 77 | 'You must add an urlconf entry for django-front to work, see: http://django-front.readthedocs.org/en/latest/installation.html' 78 | ) 79 | 80 | static_url = context.get('STATIC_URL', '/static/') 81 | user = context.get('request', None) and context.get('request').user 82 | token = six.text_type(context.get('csrf_token')) 83 | plugin = ( 84 | editor.get('editor').lower() 85 | if editor.get('editor') 86 | and editor.get('editor').lower() 87 | in django_front_settings.DJANGO_FRONT_ALLOWED_EDITORS 88 | else 'default' 89 | ) 90 | edit_mode = ( 91 | django_front_settings.DJANGO_FRONT_EDIT_MODE 92 | if django_front_settings.DJANGO_FRONT_EDIT_MODE in ('lightbox', 'inline') 93 | else 'lightbox' 94 | ) 95 | 96 | if django_front_settings.DJANGO_FRONT_PERMISSION(user): 97 | return """ 98 | 99 | 110 | 111 | """.strip() % ( 112 | static_url, 113 | save_url, 114 | history_url.replace('0000/', ''), 115 | token, 116 | plugin, 117 | static_url, 118 | edit_mode, 119 | json.dumps(django_front_settings.DJANGO_FRONT_EDITOR_OPTIONS), 120 | static_url, 121 | plugin, 122 | static_url, 123 | ) 124 | else: 125 | return '' 126 | 127 | 128 | register.tag(FrontEditTag) 129 | register.tag(FrontEditJS) 130 | -------------------------------------------------------------------------------- /front/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from .tests import FrontTestCase # noqa 2 | -------------------------------------------------------------------------------- /front/tests/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import re 4 | 5 | import six 6 | from django.contrib.auth.models import User 7 | from django.core.cache import cache 8 | from django.core.exceptions import ImproperlyConfigured 9 | from django.test import TestCase, override_settings 10 | from django.urls import reverse 11 | from front.models import Placeholder, PlaceholderHistory 12 | from front.conf import settings as django_front_settings 13 | 14 | 15 | @override_settings(ROOT_URLCONF='front.tests.urls') 16 | class FrontTestCase(TestCase): 17 | 18 | def setUp(self): 19 | self.admin_user = User.objects.create_user('admin_user', 'admin@admin.com', 'admin_user') 20 | self.admin_user.is_staff = True 21 | self.admin_user.is_superuser = True 22 | self.admin_user.save() 23 | 24 | def tearDown(self): 25 | cache.clear() 26 | 27 | def test_1_anonymous_user_cant_see_tags(self): 28 | resp = self.client.get(reverse('front-test')) 29 | self.assertTrue('document._front_edit' not in six.text_type(resp.content)) 30 | 31 | def test_2_root_user_does(self): 32 | self.client.login(username='admin_user', password='admin_user') 33 | resp = self.client.get(reverse('front-test')) 34 | self.assertTrue('document._front_edit' in six.text_type(resp.content)) 35 | self.assertTrue(six.text_type("plugin: 'ace'") in six.text_type(resp.content.decode('utf8'))) 36 | 37 | def test_3_anonymous_user_cant_post_either(self): 38 | resp = self.client.post(reverse('front-placeholder-save'), {'key': '123123', 'val': '

    booh!

    '}) 39 | self.assertTrue('0' in six.text_type(resp.content)) 40 | self.assertEqual(0, Placeholder.objects.count()) 41 | 42 | def test_4_but_admin_shall_post(self): 43 | self.client.login(username='admin_user', password='admin_user') 44 | resp = self.client.post(reverse('front-placeholder-save'), {'key': '123123', 'val': '

    booh!

    '}) 45 | self.assertTrue('1' in six.text_type(resp.content)) 46 | self.assertEqual(1, Placeholder.objects.count()) 47 | 48 | def test_5_workflow_baby(self): 49 | self.client.login(username='admin_user', password='admin_user') 50 | resp = self.client.get(reverse('front-test')) 51 | ids = re.findall(r'
    ', six.text_type(resp.content)) 52 | self.assertTrue(len(ids)) 53 | 54 | self.assertTrue('global base content' in six.text_type(resp.content)) 55 | resp = self.client.post(reverse('front-placeholder-save'), {'key': ids[0], 'val': '

    booh!

    '}) 56 | resp = self.client.post(reverse('front-placeholder-save'), {'key': ids[1], 'val': '

    english!

    '}) 57 | 58 | resp = self.client.get(reverse('front-test')) 59 | self.assertFalse('global base content' in six.text_type(resp.content)) 60 | self.assertTrue('booh!' in six.text_type(resp.content)) 61 | self.assertTrue('english!' in six.text_type(resp.content)) 62 | self.assertTrue('locale base content' not in six.text_type(resp.content)) 63 | 64 | # Parlez vous francais? 65 | # The global placholder should display the same in english, but not the localized one 66 | self.client.post('/i18n/setlang/', {'language': 'fr'}) 67 | resp = self.client.get(reverse('front-test')) 68 | self.assertFalse('global base content' in six.text_type(resp.content)) 69 | self.assertTrue('booh!' in six.text_type(resp.content)) 70 | self.assertFalse('english!' in six.text_type(resp.content)) 71 | self.assertFalse('locale base content' not in six.text_type(resp.content)) 72 | 73 | def test_6_warn_when_missing_urlconf(self): 74 | with self.settings(ROOT_URLCONF='front.tests.urls_no_save'): 75 | self.client.login(username='admin_user', password='admin_user') 76 | try: 77 | self.client.get(reverse('front-test')) 78 | self.fail() 79 | except ImproperlyConfigured: 80 | pass 81 | 82 | def test_7_empty_content(self): 83 | self.client.login(username='admin_user', password='admin_user') 84 | resp = self.client.get(reverse('front-test')) 85 | self.assertFalse('empty-editable' in six.text_type(resp.content)) 86 | ids = re.findall(r'
    ', six.text_type(resp.content)) 87 | 88 | resp = self.client.post(reverse('front-placeholder-save'), {'key': ids[0], 'val': ''}) 89 | self.assertTrue('1' in six.text_type(resp.content)) 90 | resp = self.client.get(reverse('front-test')) 91 | self.assertTrue('empty-editable' in six.text_type(resp.content)) 92 | 93 | resp = self.client.post(reverse('front-placeholder-save'), {'key': ids[0], 'val': '

    '}) 94 | self.assertTrue('1' in six.text_type(resp.content)) 95 | resp = self.client.get(reverse('front-test')) 96 | self.assertTrue('empty-editable' in six.text_type(resp.content)) 97 | 98 | resp = self.client.post(reverse('front-placeholder-save'), {'key': ids[0], 'val': '

    booh

    '}) 99 | self.assertTrue('1' in six.text_type(resp.content)) 100 | resp = self.client.get(reverse('front-test')) 101 | self.assertFalse('empty-editable' in six.text_type(resp.content)) 102 | 103 | def test_8_save_history(self): 104 | self.assertEqual(0, PlaceholderHistory.objects.count()) 105 | 106 | self.client.login(username='admin_user', password='admin_user') 107 | resp = self.client.get(reverse('front-test')) 108 | self.assertFalse('empty-editable' in six.text_type(resp.content)) 109 | ids = re.findall(r'
    ', six.text_type(resp.content)) 110 | key = ids[0] 111 | 112 | resp = self.client.post(reverse('front-placeholder-save'), {'key': key, 'val': '

    booh

    '}) 113 | self.assertTrue('1' in six.text_type(resp.content)) 114 | resp = self.client.get(reverse('front-test')) 115 | 116 | self.assertEqual(1, PlaceholderHistory.objects.filter(placeholder__key=key).count()) 117 | 118 | resp = self.client.post(reverse('front-placeholder-save'), {'key': key, 'val': '

    booh, too

    '}) 119 | resp = self.client.get(reverse('front-test')) 120 | 121 | self.assertEqual(2, PlaceholderHistory.objects.filter(placeholder__key=key).count()) 122 | 123 | resp = self.client.post(reverse('front-placeholder-save'), {'key': key, 'val': ''}) 124 | self.assertEqual(3, PlaceholderHistory.objects.filter(placeholder__key=key).count()) 125 | 126 | def test_9_get_history(self): 127 | self.client.login(username='admin_user', password='admin_user') 128 | resp = self.client.get(reverse('front-test')) 129 | self.assertFalse('empty-editable' in six.text_type(resp.content)) 130 | ids = re.findall(r'
    ', six.text_type(resp.content)) 131 | key = ids[0] 132 | 133 | resp = self.client.post(reverse('front-placeholder-save'), {'key': key, 'val': '

    booh

    '}) 134 | resp = self.client.post(reverse('front-placeholder-save'), {'key': key, 'val': '

    booh, too

    '}) 135 | resp = self.client.post(reverse('front-placeholder-save'), {'key': key, 'val': ''}) 136 | resp = self.client.post(reverse('front-placeholder-save'), {'key': key, 'val': u'

    aéaéaàà

    '}) 137 | 138 | self.assertEqual(4, PlaceholderHistory.objects.filter(placeholder__key=key).count()) 139 | 140 | resp = self.client.get(reverse('front-placeholder-history', args=(key, ))) 141 | self.assertTrue('application/json' in resp['Content-Type']) 142 | 143 | self.assertTrue(u'

    aéaéaàà

    ' in [ph.get('value') for ph in json.loads(resp.content.decode("utf8")).get('history')]) 144 | 145 | def test_10_history_only_saves_changes(self): 146 | 147 | self.client.login(username='admin_user', password='admin_user') 148 | resp = self.client.get(reverse('front-test')) 149 | self.assertFalse('empty-editable' in six.text_type(resp.content)) 150 | ids = re.findall(r'
    ', six.text_type(resp.content)) 151 | key = ids[0] 152 | 153 | resp = self.client.post(reverse('front-placeholder-save'), {'key': key, 'val': '

    booh

    '}) 154 | resp = self.client.post(reverse('front-placeholder-save'), {'key': key, 'val': '

    booh, too

    '}) 155 | 156 | self.assertEqual(2, PlaceholderHistory.objects.filter(placeholder__key=key).count()) 157 | 158 | resp = self.client.post(reverse('front-placeholder-save'), {'key': key, 'val': '

    booh, too

    '}) 159 | self.assertEqual(2, PlaceholderHistory.objects.filter(placeholder__key=key).count()) 160 | 161 | resp = self.client.post(reverse('front-placeholder-save'), {'key': key, 'val': '

    booh

    '}) 162 | self.assertEqual(3, PlaceholderHistory.objects.filter(placeholder__key=key).count()) 163 | 164 | resp = self.client.post(reverse('front-placeholder-save'), {'key': key, 'val': '

    booh

    '}) 165 | self.assertEqual(3, PlaceholderHistory.objects.filter(placeholder__key=key).count()) 166 | 167 | def test_11_invalid_tag(self): 168 | self.client.login(username='admin_user', password='admin_user') 169 | resp = self.client.get(reverse('front-test_invalid_template_tag')) 170 | self.assertTrue(six.text_type(u"plugin: 'default'") in six.text_type(resp.content.decode('utf8'))) 171 | 172 | def test_12_calculate_keys(self): 173 | self.client.login(username='admin_user', password='admin_user') 174 | resp = self.client.get(reverse('front-test')) 175 | self.assertFalse('empty-editable' in six.text_type(resp.content)) 176 | ids = re.findall(r'
    ', six.text_type(resp.content)) 177 | self.assertEqual(six.text_type(ids[0]), six.text_type('d577f15230caa8d39fb651d5b1ea34743f56edff')) 178 | 179 | def test_13_copy_content(self): 180 | self.assertEqual(0, Placeholder.objects.count()) 181 | self.client.login(username='admin_user', password='admin_user') 182 | 183 | resp = self.client.get(reverse('front-test')) 184 | key = Placeholder.key_for('global-ph') 185 | self.assertTrue(key in six.text_type(resp.content)) 186 | key = Placeholder.key_for('some-other-ph', 'hello') 187 | self.assertTrue(key in six.text_type(resp.content)) 188 | 189 | resp = self.client.post(reverse('front-placeholder-save'), {'key': key, 'val': '

    booh

    '}) 190 | self.assertEqual(1, Placeholder.objects.count()) 191 | 192 | Placeholder.copy_content('some-other-ph', ['hello'], ['jello']) 193 | self.assertEqual(2, Placeholder.objects.count()) 194 | 195 | jello_key = Placeholder.key_for('some-other-ph', 'jello') 196 | hello_key = Placeholder.key_for('some-other-ph', 'hello') 197 | self.assertTrue(Placeholder.objects.filter(key=jello_key).exists()) 198 | self.assertTrue(Placeholder.objects.filter(key=hello_key).exists()) 199 | self.assertEqual( 200 | Placeholder.objects.get(key=jello_key).value, 201 | Placeholder.objects.get(key=jello_key).value 202 | ) 203 | 204 | def test_14_extra_container_classes(self): 205 | _setting = getattr(django_front_settings, 'DJANGO_FRONT_EXTRA_CONTAINER_CLASSES', '') 206 | django_front_settings.DJANGO_FRONT_EXTRA_CONTAINER_CLASSES = 'some extra classes' 207 | 208 | self.client.login(username='admin_user', password='admin_user') 209 | resp = self.client.get(reverse('front-test')) 210 | self.assertTrue('some extra classes' in six.text_type(resp.content)) 211 | 212 | django_front_settings.DJANGO_FRONT_EXTRA_CONTAINER_CLASSES = _setting 213 | -------------------------------------------------------------------------------- /front/tests/urls.py: -------------------------------------------------------------------------------- 1 | import django 2 | 3 | from .views import test, test_invalid_template_tag 4 | 5 | 6 | if django.VERSION >= (3, 1, 0): 7 | from django.urls import re_path as url, include 8 | else: 9 | from django.conf.urls import url, include 10 | 11 | urlpatterns = [ 12 | url(r'^test/$', test, name='front-test'), 13 | url( 14 | r'^test-invalid/$', 15 | test_invalid_template_tag, 16 | name='front-test_invalid_template_tag', 17 | ), 18 | url(r'^i18n/', include('django.conf.urls.i18n')), 19 | url(r'^front-edit/', include('front.urls')), 20 | ] 21 | -------------------------------------------------------------------------------- /front/tests/urls_no_save.py: -------------------------------------------------------------------------------- 1 | import django 2 | 3 | from .views import test 4 | 5 | 6 | if django.VERSION >= (3, 1, 0): 7 | from django.urls import re_path as url, include 8 | else: 9 | from django.conf.urls import url, include 10 | 11 | urlpatterns = [ 12 | url(r'^test/$', test, name='front-test'), 13 | url(r'^i18n/', include('django.conf.urls.i18n')), 14 | ] 15 | -------------------------------------------------------------------------------- /front/tests/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | try: 3 | from django.template import engines 4 | __is_18 = True 5 | except ImportError: 6 | from django.template import loader, RequestContext 7 | __is_18 = False 8 | 9 | 10 | TEST_TEMPLATE = r''' 11 | {% load front_tags %} 12 | 13 | 14 | 15 | 16 | front test 17 | 18 | 19 |

    Hello, {% if request.user.is_authenticated %}{{request.user}}{% else %}Anon{% endif %}!

    20 |
    {% front_edit "global-ph" %}global base content{% end_front_edit %}
    21 |
    {% front_edit "locale-ph" request.LANGUAGE_CODE %}locale base content{% end_front_edit %}
    22 |
    {% front_edit "some-other-ph" arg1 %}argument based content{% end_front_edit %}
    23 | {% front_edit_scripts editor="ace" %} 24 | 25 | 26 | ''' 27 | 28 | 29 | TEST_TEMPLATE_INVALID_EDITOR = r''' 30 | {% load front_tags %} 31 | {% front_edit_scripts editor="dummy" %} 32 | ''' 33 | 34 | 35 | def _render(template_string, context, request): 36 | if __is_18: 37 | return engines['django'].from_string(template_string).render(context=context, request=request) 38 | else: 39 | return loader.get_template_from_string(template_string).render(RequestContext(request, context)) 40 | 41 | 42 | def test(request): 43 | return HttpResponse(_render(TEST_TEMPLATE, context=dict(arg1='hello'), request=request)) 44 | 45 | 46 | def test_invalid_template_tag(request): 47 | return HttpResponse(_render(TEST_TEMPLATE_INVALID_EDITOR, context={}, request=request)) 48 | -------------------------------------------------------------------------------- /front/urls.py: -------------------------------------------------------------------------------- 1 | import django 2 | 3 | from .views import do_save, get_history 4 | 5 | 6 | if django.VERSION >= (3, 1, 0): 7 | from django.urls import re_path as url 8 | else: 9 | from django.conf.urls import url 10 | 11 | 12 | urlpatterns = [ 13 | url(r'^save/$', do_save, name='front-placeholder-save'), 14 | url( 15 | r'^hist/(?P[0-9a-f]{1,40})/$', get_history, name='front-placeholder-history' 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /front/views.py: -------------------------------------------------------------------------------- 1 | from .conf import settings as django_front_settings 2 | from .models import Placeholder, PlaceholderHistory 3 | from django.core.serializers.json import DjangoJSONEncoder 4 | from django.http import HttpResponse 5 | from django.views.decorators.http import require_POST, require_GET 6 | import json 7 | 8 | 9 | class JsonHttpResponse(HttpResponse): 10 | def __init__(self, **kwargs): 11 | super(JsonHttpResponse, self).__init__(json.dumps(kwargs, cls=DjangoJSONEncoder, ensure_ascii=False), content_type='application/json') 12 | 13 | 14 | @require_POST 15 | def do_save(request): 16 | if request.POST and django_front_settings.DJANGO_FRONT_PERMISSION(request.user): 17 | key, val = request.POST.get('key'), request.POST.get('val') 18 | placeholder, created = Placeholder.objects.get_or_create(key=key, defaults=dict(value=val)) 19 | if not created: 20 | placeholder.value = val 21 | placeholder.save() 22 | 23 | return HttpResponse('1') 24 | return HttpResponse('0') 25 | 26 | 27 | @require_GET 28 | def get_history(request, key): 29 | return JsonHttpResponse(history=[ph._as_json for ph in PlaceholderHistory.objects.filter(placeholder__key=key)]) 30 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from setuptools import find_packages, setup 4 | from setuptools.command.test import test as test_command 5 | 6 | 7 | class Tox(test_command): 8 | user_options = [("tox-args=", "a", "Arguments to pass to tox")] 9 | 10 | def initialize_options(self): 11 | test_command.initialize_options(self) 12 | self.tox_args = None 13 | 14 | def finalize_options(self): 15 | test_command.finalize_options(self) 16 | self.test_args = [] 17 | self.test_suite = True 18 | 19 | def run_tests(self): 20 | # import here, cause outside the eggs aren't loaded 21 | import shlex 22 | 23 | import tox 24 | 25 | args = self.tox_args 26 | if args: 27 | args = shlex.split(self.tox_args) 28 | errno = tox.cmdline(args=args) 29 | sys.exit(errno) 30 | 31 | 32 | with open("README.rst") as readme: 33 | long_description = readme.read() 34 | 35 | setup( 36 | name="django-front", 37 | version=__import__("front").get_version(limit=3), 38 | description="A Django application to allow of front-end editing", 39 | long_description=long_description, 40 | author="Marco Bonetti", 41 | author_email="mbonetti@gmail.com", 42 | url="https://github.com/mbi/django-front", 43 | license="MIT", 44 | packages=find_packages(exclude=["test_project", "test_project.*"]), 45 | classifiers=[ 46 | "Development Status :: 4 - Beta", 47 | "Environment :: Web Environment", 48 | "Intended Audience :: Developers", 49 | "License :: OSI Approved :: MIT License", 50 | "Operating System :: OS Independent", 51 | "Programming Language :: Python", 52 | "Framework :: Django", 53 | "Framework :: Django :: 4.2", 54 | "Framework :: Django :: 5.0", 55 | "Framework :: Django :: 5.2", 56 | "Programming Language :: Python :: 3.9", 57 | "Programming Language :: Python :: 3.10", 58 | "Programming Language :: Python :: 3.11", 59 | "Programming Language :: Python :: 3.12", 60 | ], 61 | include_package_data=True, 62 | zip_safe=False, 63 | install_requires=["django-classy-tags >= 1.0", "Django >= 4.2", "six"], 64 | tests_require=["tox~=4.11.4"], 65 | cmdclass={"test": Tox}, 66 | ) 67 | -------------------------------------------------------------------------------- /test_project/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = 4 | front 5 | omit = 6 | ../*migrations* 7 | ../*tests* 8 | [report] 9 | precision = 2 10 | -------------------------------------------------------------------------------- /test_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | sys.path.append('..') 6 | 7 | if __name__ == "__main__": 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") 9 | 10 | from django.core.management import execute_from_command_line 11 | 12 | execute_from_command_line(sys.argv) 13 | -------------------------------------------------------------------------------- /test_project/static/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbi/django-front/0ed620dcd7f0e4ce11f62b58149b878f3dfb3eb6/test_project/static/.gitignore -------------------------------------------------------------------------------- /test_project/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load i18n front_tags %} 2 | 3 | 4 | 5 | {% front_edit "global" LANGUAGE_CODE %}aaa{% end_front_edit %} 6 | {% front_edit_scripts editor="ace" %} 7 | 8 | 9 | -------------------------------------------------------------------------------- /test_project/test_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbi/django-front/0ed620dcd7f0e4ce11f62b58149b878f3dfb3eb6/test_project/test_project/__init__.py -------------------------------------------------------------------------------- /test_project/test_project/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for test_project project. 2 | import os 3 | 4 | DEBUG = True 5 | DATABASES = { 6 | "default": { 7 | "ENGINE": "django.db.backends.sqlite3", 8 | "NAME": os.path.join(os.path.dirname(__file__), "test_project.db"), 9 | } 10 | } 11 | 12 | USE_I18N = True 13 | SECRET_KEY = "lol I dont even care" 14 | USE_TZ = True 15 | 16 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 17 | 18 | MIDDLEWARE = ( 19 | "django.middleware.common.CommonMiddleware", 20 | "django.contrib.sessions.middleware.SessionMiddleware", 21 | "django.middleware.csrf.CsrfViewMiddleware", 22 | "django.contrib.auth.middleware.AuthenticationMiddleware", 23 | "django.contrib.messages.middleware.MessageMiddleware", 24 | "django.middleware.locale.LocaleMiddleware", 25 | ) 26 | 27 | TEMPLATES = [ 28 | { 29 | "BACKEND": "django.template.backends.django.DjangoTemplates", 30 | "APP_DIRS": True, 31 | "OPTIONS": { 32 | "context_processors": ( 33 | "django.contrib.auth.context_processors.auth", 34 | "django.template.context_processors.debug", 35 | "django.template.context_processors.i18n", 36 | "django.template.context_processors.media", 37 | "django.template.context_processors.static", 38 | "django.template.context_processors.tz", 39 | "django.contrib.messages.context_processors.messages", 40 | "django.template.context_processors.request", 41 | ), 42 | "debug": False, 43 | }, 44 | } 45 | ] 46 | STATIC_URL = "/static/" 47 | 48 | ROOT_URLCONF = "test_project.urls" 49 | 50 | # Python dotted path to the WSGI application used by Django's runserver. 51 | WSGI_APPLICATION = "test_project.wsgi.application" 52 | 53 | INSTALLED_APPS = ( 54 | "django.contrib.auth", 55 | "django.contrib.contenttypes", 56 | "django.contrib.sessions", 57 | "django.contrib.sites", 58 | "django.contrib.staticfiles", 59 | "front", 60 | ) 61 | 62 | SITE_ID = 1 63 | -------------------------------------------------------------------------------- /test_project/test_project/urls.py: -------------------------------------------------------------------------------- 1 | # from django.conf.urls import include, url 2 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 3 | 4 | urlpatterns = [] 5 | 6 | urlpatterns += staticfiles_urlpatterns() 7 | -------------------------------------------------------------------------------- /test_project/test_project/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import login_required 2 | from django.http import HttpResponse 3 | from django.shortcuts import render_to_response 4 | from django.http import HttpResponse, HttpResponseRedirect 5 | from django.template import RequestContext 6 | 7 | 8 | def home(request): 9 | return render_to_response('base.html', dict( 10 | ), context_instance=RequestContext(request)) 11 | -------------------------------------------------------------------------------- /test_project/test_project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for test_project project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | 18 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") 19 | 20 | # This application object is used by any WSGI server configured to use this 21 | # file. This includes Django's development server, if the WSGI_APPLICATION 22 | # setting points here. 23 | from django.core.wsgi import get_wsgi_application 24 | application = get_wsgi_application() 25 | 26 | # Apply WSGI middleware here. 27 | # from helloworld.wsgi import HelloWorldApplication 28 | # application = HelloWorldApplication(application) 29 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{39,310}-django42, 4 | py{310,311,312}-django50, 5 | py{310,311,312}-django52, 6 | flake8, 7 | docs 8 | skipsdist = True 9 | 10 | 11 | [gh-actions] 12 | python = 13 | 3.9: py39-django42, flake8, docs 14 | 3.10: py310-django42, py310-django50, py310-django52, 15 | 3.11: py311-django42, py311-django50, py311-django52, 16 | 3.12: py312-django50, py312-django52 17 | 18 | 19 | [testenv] 20 | changedir = test_project 21 | commands = 22 | python -Wd manage.py test front 23 | 24 | setenv = 25 | PYTHONDONTWRITEBYTECODE=1 26 | 27 | deps = 28 | django42: Django>=4.2a,<4.3 29 | django50: Django>=5.0,<5.1 30 | django52: Django>=5.2,<5.3 31 | 32 | pymemcache 33 | coverage 34 | django-classy-tags 35 | south 36 | django-wymeditor 37 | six 38 | 39 | [testenv:flake8] 40 | basepython = python3 41 | deps = flake8 42 | commands= 43 | flake8 {toxinidir}/front 44 | 45 | [testenv:docs] 46 | deps = 47 | sphinx 48 | sphinx-book-theme 49 | 50 | changedir = docs 51 | commands= 52 | sphinx-build -W -b html . _build/html 53 | 54 | 55 | [flake8] 56 | ignore = E501,W503 57 | --------------------------------------------------------------------------------