├── .gitignore ├── LICENSE ├── MANIFEST.in ├── Pipfile ├── Pipfile.lock ├── README.md ├── docs ├── blocks.md ├── index.md ├── usage.md └── wagtail.md ├── hypereditor ├── __init__.py ├── admin.py ├── apps.py ├── blocks │ ├── __init__.py │ ├── base.py │ └── chooser.py ├── fields.py ├── hyper_blocks.py ├── migrations │ └── __init__.py ├── models.py ├── settings.py ├── static │ └── hypereditor │ │ ├── hyper_editor.css │ │ └── hyper_editor.js ├── templates │ └── hypereditor │ │ ├── blocks │ │ ├── column │ │ │ └── default.html │ │ ├── contentbox │ │ │ └── default.html │ │ ├── heading │ │ │ └── default.html │ │ ├── image │ │ │ └── default.html │ │ ├── link │ │ │ └── default.html │ │ ├── row │ │ │ └── default.html │ │ ├── section │ │ │ └── default.html │ │ ├── slider │ │ │ └── default.html │ │ ├── tab │ │ │ └── default.html │ │ └── text │ │ │ └── default.html │ │ ├── edit_handlers │ │ └── hyper_editor_field_panel.html │ │ ├── hyper_editor.html │ │ ├── js │ │ ├── blocks.js │ │ └── fields.js │ │ ├── partials │ │ └── editor_iframe.html │ │ ├── preview.html │ │ └── widgets │ │ └── hyper_widget.html ├── templatetags │ └── hyper_tags.py ├── tests │ ├── __init__.py │ ├── mocks.py │ ├── test_chooser.py │ └── test_templatetags.py ├── urls.py ├── utils.py └── views.py ├── manage.py ├── mkdocs.yml ├── readthedocs.yml ├── sandbox ├── __init__.py ├── example │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── hyper_blocks.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── templates │ │ └── test_form.html │ ├── tests.py │ └── views.py ├── settings.py ├── urls.py └── wsgi.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### OSX ### 4 | .DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | 8 | # Icon must end with two \r 9 | Icon 10 | 11 | 12 | # Thumbnails 13 | ._* 14 | 15 | # Files that might appear on external disk 16 | .Spotlight-V100 17 | .Trashes 18 | 19 | # Directories potentially created on remote AFP share 20 | .AppleDB 21 | .AppleDesktop 22 | Network Trash Folder 23 | Temporary Items 24 | .apdisk 25 | 26 | 27 | ### Python ### 28 | # Byte-compiled / optimized / DLL files 29 | __pycache__/ 30 | *.py[cod] 31 | 32 | # C extensions 33 | *.so 34 | 35 | # Distribution / packaging 36 | .Python 37 | env/ 38 | build/ 39 | develop-eggs/ 40 | dist/ 41 | downloads/ 42 | eggs/ 43 | lib/ 44 | lib64/ 45 | parts/ 46 | sdist/ 47 | var/ 48 | *.egg-info/ 49 | .installed.cfg 50 | *.egg 51 | 52 | # PyInstaller 53 | # Usually these files are written by a python script from a template 54 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 55 | *.manifest 56 | *.spec 57 | 58 | # Installer logs 59 | pip-log.txt 60 | pip-delete-this-directory.txt 61 | 62 | # Unit test / coverage reports 63 | htmlcov/ 64 | .tox/ 65 | .coverage 66 | .cache 67 | nosetests.xml 68 | coverage.xml 69 | 70 | # Translations 71 | *.mo 72 | *.pot 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | 81 | ### Django ### 82 | *.log 83 | *.pot 84 | *.pyc 85 | __pycache__/ 86 | local_settings.py 87 | 88 | .env 89 | db.sqlite3 90 | 91 | .vscode/ 92 | .idea/ 93 | .env/ 94 | .venv/ 95 | 96 | website/static/themes/**/scss/*.css* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Divine IT Limited 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | recursive-include hypereditor/ * 3 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | django = "*" 10 | mkdocs = "*" 11 | 12 | [requires] 13 | python_version = "3.6" 14 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "ea6d81cbef694b6d15e65dd92b6e0547bc1a0f8342404bfc6307c420bb6ef233" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "click": { 20 | "hashes": [ 21 | "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", 22 | "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" 23 | ], 24 | "version": "==7.0" 25 | }, 26 | "django": { 27 | "hashes": [ 28 | "sha256:1226168be1b1c7efd0e66ee79b0e0b58b2caa7ed87717909cd8a57bb13a7079a", 29 | "sha256:9a4635813e2d498a3c01b10c701fe4a515d76dd290aaa792ccb65ca4ccb6b038" 30 | ], 31 | "index": "pypi", 32 | "version": "==2.2.10" 33 | }, 34 | "jinja2": { 35 | "hashes": [ 36 | "sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250", 37 | "sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49" 38 | ], 39 | "version": "==2.11.1" 40 | }, 41 | "livereload": { 42 | "hashes": [ 43 | "sha256:78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b", 44 | "sha256:89254f78d7529d7ea0a3417d224c34287ebfe266b05e67e51facaf82c27f0f66" 45 | ], 46 | "version": "==2.6.1" 47 | }, 48 | "markdown": { 49 | "hashes": [ 50 | "sha256:5ad7180c3ec16422a764568ad6409ec82460c40d1631591fa53d597033cc98bf", 51 | "sha256:9c71241ec237505535eabff7a38b1307250f16cea174bb1e505c3e032f108867" 52 | ], 53 | "version": "==3.2" 54 | }, 55 | "markupsafe": { 56 | "hashes": [ 57 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 58 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 59 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 60 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 61 | "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", 62 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 63 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 64 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 65 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 66 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 67 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 68 | "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", 69 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 70 | "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", 71 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 72 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 73 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 74 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 75 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 76 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 77 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 78 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 79 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 80 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 81 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 82 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 83 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 84 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 85 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 86 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 87 | "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", 88 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", 89 | "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" 90 | ], 91 | "version": "==1.1.1" 92 | }, 93 | "mkdocs": { 94 | "hashes": [ 95 | "sha256:17d34329aad75d5de604b9ed4e31df3a4d235afefdc46ce7b1964fddb2e1e939", 96 | "sha256:8cc8b38325456b9e942c981a209eaeb1e9f3f77b493ad755bfef889b9c8d356a" 97 | ], 98 | "index": "pypi", 99 | "version": "==1.0.4" 100 | }, 101 | "pytz": { 102 | "hashes": [ 103 | "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", 104 | "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be" 105 | ], 106 | "version": "==2019.3" 107 | }, 108 | "pyyaml": { 109 | "hashes": [ 110 | "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6", 111 | "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf", 112 | "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5", 113 | "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e", 114 | "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811", 115 | "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e", 116 | "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d", 117 | "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20", 118 | "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689", 119 | "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994", 120 | "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615" 121 | ], 122 | "version": "==5.3" 123 | }, 124 | "six": { 125 | "hashes": [ 126 | "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", 127 | "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" 128 | ], 129 | "version": "==1.14.0" 130 | }, 131 | "sqlparse": { 132 | "hashes": [ 133 | "sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177", 134 | "sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873" 135 | ], 136 | "version": "==0.3.0" 137 | }, 138 | "tornado": { 139 | "hashes": [ 140 | "sha256:349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c", 141 | "sha256:398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60", 142 | "sha256:4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281", 143 | "sha256:559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5", 144 | "sha256:abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7", 145 | "sha256:c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9", 146 | "sha256:c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5" 147 | ], 148 | "version": "==6.0.3" 149 | } 150 | }, 151 | "develop": {} 152 | } 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Hyper Editor 2 | 3 | Django integration for [Hyper Editor](https://github.com/divineitlimited/hyper-editor). 4 | 5 | [![Documentation Status](https://readthedocs.org/projects/django-hyper-editor/badge/?version=latest)](https://django-hyper-editor.readthedocs.io/en/latest/?badge=latest) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 7 | 8 | ## Installation 9 | 10 | Install via pip 11 | 12 | ```sybase 13 | pip install hypereditor 14 | ``` 15 | 16 | Add to ``settings.py`` 17 | ```python 18 | INSTALLED_APPS = [ 19 | ... 20 | 'hypereditor' 21 | ] 22 | ``` 23 | 24 | Add to ``urls.py`` 25 | ```python 26 | urlpatterns = [ 27 | ... 28 | path('hypereditor/', include('hypereditor.urls')), 29 | ] 30 | ``` 31 | 32 | ## Documentation 33 | 34 | Please check the documentation site - [https://django-hyper-editor.readthedocs.io/en/latest/](https://django-hyper-editor.readthedocs.io/en/latest/) 35 | 36 | ## TODO 37 | - [x] Hyper Editor Output Parser 38 | - [x] Block System 39 | - [x] Create simple block without using js 40 | - [x] Django Model Integration ``HyperField`` 41 | - [x] Django Admin Integration 42 | - [x] Django Form Integration 43 | - [x] Wagtail Integration ``HyperFieldPanel`` 44 | - [x] Template tag for preview and render 45 | - [x] Documentation 46 | - [ ] Media Handling 47 | 48 | ## License 49 | MIT 50 | 51 | Made with :heart: at [Divine IT Limited](https://divineit.net/), Dhaka, Bangladesh -------------------------------------------------------------------------------- /docs/blocks.md: -------------------------------------------------------------------------------- 1 | # Blocks 2 | 3 | [Hyper Editor](https://github.com/DivineITLimited/hyper-editor) is a block based content editor. 4 | You might want to create different kinds of block that then be used in the frontend editor. 5 | 6 | There are two different way you can create block. In both cases you are going to extend 7 | ``hypereditor.blocks.base.Block`` class. 8 | 9 | ## hyper_blocks.py 10 | 11 | It might be better to separate Hyper Editor blocks from other codes of your project. 12 | That is why we will use ``hyper_blocks.py`` file to keep our blocks. 13 | 14 | Create a file named ``hyper_blocks.py`` in your django app where ``models.py``, ``views.py`` resides (in application folder). 15 | 16 | ## Simple Blocks 17 | 18 | Lets create a simple heading block. 19 | 20 | ```python 21 | from hypereditor.blocks.base import Block 22 | from hypereditor.blocks import register 23 | 24 | 25 | # register block with a block_type parameter 26 | @register('heading') 27 | class Heading(Block): 28 | 29 | # Title & Description of your block as seen at HyperEditor block chooser 30 | title = 'Heading' 31 | description = 'Heading Block' 32 | 33 | # Your settings fields for block 34 | schema = { 35 | "fields": [ 36 | { 37 | "type": "input", 38 | "inputType": "text", 39 | "label": "Heading Text", 40 | "model": "text" 41 | }, 42 | { 43 | "type": "select", 44 | "label": "Heading Type", 45 | "model": "type", 46 | "values": ['h{0}'.format(i) for i in range(1, 7)] 47 | } 48 | ] 49 | } 50 | 51 | # Initial values for your settings field 52 | initial = { 53 | "type": 'h3' 54 | } 55 | ``` 56 | 57 | You should see your Heading block is now showing in Hyper Editor. 58 | 59 | ## Block Templates 60 | 61 | Each block must have at least one **template**. **Templates** are related to **Block Styles** in Hyper Editor. 62 | 63 | Lets create a default template for our heading block. 64 | 65 | - Create a directory called ``hypereditor`` in your ``templates`` folder. 66 | 67 | - Create another directory called ``blocks`` inside ``hypereditor`` folder. 68 | 69 | - Create another directory with the same name of your ``block_type`` 70 | 71 | - Create a template named ``default.html`` 72 | 73 | Your folder structure should look like following - 74 | ```sybase 75 | ├── templates 76 | │   └── hypereditor 77 | │   ├── blocks 78 | │   │   ├── heading 79 | │   │   │   └── default.html 80 | ``` 81 | 82 | It might look like a bit much, but we found it helps us separating when there are lots of blocks. 83 | 84 | Now add following in your template - 85 | ```html 86 | <{{ obj.settings.type }}> 87 | 88 | {{ obj.settings.text }} 89 | 90 | 91 | ``` 92 | 93 | ### Template Context 94 | 95 | Yet to be documented 96 | 97 | ## Advance Blocks 98 | 99 | Yet to be documented. -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Django Hyper Editor is the official Django Integration of [Hyper Editor](https://github.com/DivineITLimited/hyper-editor). 4 | 5 | Django Hyper Editor aims to provide an easy to use api for Hyper Editor blocks to cover most use cases. 6 | 7 |
8 | 9 | 10 |
11 | Project supported by
12 | Divine IT Limited 13 |
14 |
15 | 16 |
17 | # Installation 18 | 19 | - Install Hyper Editor from pypi 20 | 21 | ```sybase 22 | pip install hypereditor 23 | ``` 24 | 25 | - Add Hyper Editor to your ``INSTALLED_APPS`` 26 | 27 | ```python 28 | INSTALLED_APPS = [ 29 | ... 30 | 'hypereditor', 31 | ] 32 | ``` 33 | 34 | - Add Hyper Editor to your ``urls.py`` 35 | 36 | ```python 37 | urlpatterns = [ 38 | path('hypereditor/', include('hypereditor.urls')), 39 | # if you are using Wagtail, then please add hypereditor urls before wagtail 40 | ] 41 | ``` 42 | 43 | Hyper Editor is installed in your project. 44 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | Using Hyper Editor is pretty easy. Checkout appropriate section that you are interested in. 4 | 5 | ## Django Model 6 | 7 | You can easily use Hyper Editor as a Model field like following - 8 | 9 | ```python 10 | from django.db import models 11 | 12 | # import Hyper Editor Field 13 | from hypereditor.fields import HyperField 14 | 15 | 16 | class Page(models.Model): 17 | title = models.CharField(max_length=255) 18 | 19 | # Use just like any other field 20 | content = HyperField(default=None) 21 | 22 | def __str__(self): 23 | return self.title 24 | ``` 25 | 26 | ## Django Forms 27 | 28 | Model Forms will work out of the box. For example for the previous model ``Page`` 29 | 30 | ```python 31 | from django import forms 32 | 33 | 34 | class PageForm(forms.ModelForm): 35 | 36 | class Meta: 37 | model = models.Page 38 | exclude = [] 39 | ``` 40 | 41 | For Regular Form you can use it like following - 42 | 43 | ```python 44 | from django import forms 45 | from hypereditor.fields import HyperFormField 46 | 47 | 48 | class TestForm(forms.Form): 49 | 50 | content = HyperFormField() 51 | 52 | ``` 53 | 54 | ## Django Admin 55 | 56 | Works out of the box just like ``ModelForm``. Just register your model. 57 | 58 | ```python 59 | from django.contrib import admin 60 | from sandbox.example.models import * 61 | 62 | 63 | @admin.register(Page) 64 | class PageAdmin(admin.ModelAdmin): 65 | pass 66 | ``` 67 | 68 | ## Rendering 69 | 70 | You need to use ``hyper_tags`` in your template in order to display Hyper Editor Generated Contents. 71 | 72 | ```html 73 | {% load hyper_tags %} 74 | 75 | {% hyper_render page.content %} 76 | ``` 77 | -------------------------------------------------------------------------------- /docs/wagtail.md: -------------------------------------------------------------------------------- 1 | # Wagtail 2 | 3 | Django Hyper Editor has good support for wagtail too. 4 | 5 | While using with Wagtail use ``HyperFieldPanel`` instead of ``FieldPanel`` 6 | 7 | ```python 8 | from hypereditor.fields import HyperFieldPanel 9 | 10 | 11 | content_panels = [ 12 | # Other panels 13 | HyperFieldPanel('field_name') 14 | ] 15 | ``` -------------------------------------------------------------------------------- /hypereditor/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = "hypereditor.apps.HyperEditorConfig" -------------------------------------------------------------------------------- /hypereditor/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /hypereditor/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from hypereditor.utils import load_hyper_blocks 3 | 4 | 5 | class HyperEditorConfig(AppConfig): 6 | name = 'hypereditor' 7 | 8 | def ready(self): 9 | super().ready() 10 | # load all hyper_blocks.py 11 | list(load_hyper_blocks()) 12 | -------------------------------------------------------------------------------- /hypereditor/blocks/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.conf import settings 4 | 5 | from hypereditor.blocks.base import Block 6 | from hypereditor.blocks.chooser import Chooser 7 | 8 | BLOCK_REGISTRY = {} 9 | 10 | EXCLUDED_LIST = getattr(settings, 'HYPER_EDITOR_EXCLUDE_BLOCKS', []) 11 | 12 | CHOOSER_REGISTRY = {} 13 | 14 | 15 | def register_block(block_type, block_class): 16 | """Register a block 17 | :param block_type: str 18 | :param block_class: Block 19 | :return: None 20 | """ 21 | if issubclass(block_class, Block): 22 | if block_type not in EXCLUDED_LIST: # Check if not explicitly excluded 23 | BLOCK_REGISTRY[block_type] = block_class 24 | else: 25 | raise Exception('%s is not a valid block class' % block_class) 26 | 27 | 28 | def register_chooser_field(page_chooser, chooser_cls): 29 | if issubclass(chooser_cls, Chooser): 30 | CHOOSER_REGISTRY[page_chooser] = chooser_cls 31 | else: 32 | raise Exception('%s is not a valid chooser' % chooser_cls) 33 | 34 | 35 | def get_block_class_for(block_type): 36 | """Get a block from registry by type 37 | :param block_type: str 38 | :return: Block 39 | """ 40 | return BLOCK_REGISTRY.get(block_type, Block) 41 | 42 | 43 | def get_chooser_for(chooser_type): 44 | return CHOOSER_REGISTRY.get(chooser_type) 45 | 46 | 47 | def js_variable_str(): 48 | js_variables = {} 49 | for k, v in BLOCK_REGISTRY.items(): 50 | if issubclass(v, Block): 51 | if callable(v.js_variables): 52 | to_add = v.js_variables() 53 | else: 54 | to_add = v.js_variables 55 | js_variables.update(to_add) 56 | return '\n'.join(['%s = `%s`;' % (k, v) for k, v in js_variables.items()]) 57 | 58 | 59 | def get_js_plugins(): 60 | js_plugins = [] 61 | for k, v in BLOCK_REGISTRY.items(): 62 | if issubclass(v, Block): 63 | if v.js_files is not None: 64 | if isinstance(v.js_files, list): 65 | js_plugins = js_plugins + v.js_files 66 | else: 67 | js_plugins.append(v.js_files) 68 | return js_plugins 69 | 70 | 71 | def get_simpler_blocks(): 72 | result = {} 73 | for k, v in BLOCK_REGISTRY.items(): 74 | config = v().get_block_config() 75 | if config: 76 | result[k] = json.dumps(config) 77 | return result 78 | 79 | 80 | def register(block_type): 81 | """Convenient decorator for registering a block 82 | :param block_type: str 83 | """ 84 | def wrap(cls): 85 | register_block(block_type, cls) 86 | return wrap 87 | 88 | 89 | def register_chooser(chooser_type): 90 | def wrap(cls): 91 | register_chooser_field(chooser_type, cls) 92 | return wrap 93 | -------------------------------------------------------------------------------- /hypereditor/blocks/base.py: -------------------------------------------------------------------------------- 1 | from django.template.loader import render_to_string 2 | from django.utils.safestring import mark_safe 3 | from django.template import Template, Context, RequestContext 4 | from hypereditor.utils import dict_to_css 5 | 6 | 7 | class CodeRenderer(object): 8 | """An Utility Class for Collecting CSS and JS from all Blocks in Node Tree""" 9 | 10 | css = [] 11 | js = [] 12 | 13 | def add_css(self, css): 14 | if css: 15 | css = css.strip() 16 | if len(css) > 0: 17 | self.css.append(css) 18 | 19 | def add_js(self, js): 20 | if js: 21 | js = js.strip() 22 | if len(js) > 0: 23 | self.js.append(js) 24 | 25 | def render_css(self): 26 | return '\n'.join(self.css) 27 | 28 | def render_css_with_tag(self): 29 | return '' 30 | 31 | def render_js(self): 32 | return '\n'.join(self.js) 33 | 34 | def render_js_with_tag(self): 35 | return '' 36 | 37 | 38 | def _build_background_image(image): 39 | return { 40 | 'background': 'url("' + image['url'] + '")', 41 | 'background-repeat': 'no-repeat', 42 | 'background-size': 'cover' 43 | } 44 | 45 | 46 | def _build_background_color(bgcolor): 47 | return { 48 | 'background-color': 'rgba({r}, {g}, {b}, {a})'.format( 49 | r=bgcolor.get('r'), 50 | g=bgcolor.get('g'), 51 | b=bgcolor.get('b'), 52 | a=bgcolor.get('a') 53 | ) 54 | } 55 | 56 | 57 | def _build_foreground_color(fgcolor): 58 | return { 59 | 'color': 'rgba({r}, {g}, {b}, {a})'.format( 60 | r=fgcolor.get('r'), 61 | g=fgcolor.get('g'), 62 | b=fgcolor.get('b'), 63 | a=fgcolor.get('a') 64 | ) 65 | } 66 | 67 | 68 | def _build_padding(padding): 69 | padding_dict = {} 70 | for key, val in padding.items(): 71 | if key != 'unit' and val is not None: 72 | padding_dict['padding-'+key] = val + padding['unit'] 73 | return padding_dict 74 | 75 | 76 | def _build_margin(margin): 77 | margin_dict = {} 78 | for key, val in margin.items(): 79 | if key != 'unit' and val is not None: 80 | margin_dict['margin-'+key] = val + margin['unit'] 81 | return margin_dict 82 | 83 | 84 | class Block(object): 85 | """Base Block Class""" 86 | 87 | # template variable to be used 88 | template_var = 'obj' 89 | 90 | # if any js variables required to initialize 91 | js_variables = {} 92 | 93 | # if any js file needs to be initialized 94 | js_files = None 95 | 96 | # for simple block following variables can be used 97 | external = False 98 | title = None 99 | description = None 100 | schema = None 101 | initial = None 102 | general_initial = None 103 | styles = None 104 | 105 | def __init__(self, obj={}, code_renderer=None): 106 | """ 107 | Initialize block 108 | :param obj: dict - The value object for this block in tree 109 | :param code_renderer: instance of CodeRenderer 110 | """ 111 | self.obj = obj 112 | if code_renderer and isinstance(code_renderer, CodeRenderer): 113 | self.codeRenderer = code_renderer 114 | elif code_renderer: 115 | raise Exception('Invalid Renderer') 116 | 117 | def get_template(self): 118 | """ 119 | Determine which template to use. A block might have multiple style. 120 | Each style has its own template. 121 | :return: str - template name 122 | """ 123 | style = self.obj.get('general', {}).get('style', 'default') 124 | return 'hypereditor/blocks/{type}/{style}.html'.format(type=self.obj['type'], style=style) 125 | 126 | def get_context(self, parent_context=None): 127 | """ 128 | Build Block Context. Assign parent context if has any. 129 | :param parent_context: dict 130 | :return: dict 131 | """ 132 | context = parent_context or {} 133 | context.update({ 134 | 'self': self.obj, 135 | self.template_var: self.obj, 136 | }) 137 | return context 138 | 139 | def get_rendered_children(self, context=None): 140 | """ 141 | Blocks might have child blocks. Render children. 142 | :param context: dict context for children 143 | :return: safe str - Rendered string 144 | """ 145 | from hypereditor.blocks import get_block_class_for 146 | rendered_child = '' 147 | if self.obj.get('children') is not None: 148 | for child in self.obj.get('children'): 149 | bl_class = get_block_class_for(child.get('type', 'INVALID_PLUGIN_WITH_NO_TYPE')) 150 | if bl_class: 151 | instance = bl_class(child, self.codeRenderer) 152 | rendered_child = rendered_child + instance.render(context) 153 | return mark_safe(rendered_child) 154 | 155 | def _prepare_custom_codes(self): 156 | """ 157 | If any extra css or js added from editor add them to code_renderer 158 | :return: None 159 | """ 160 | if self.obj.get('extra'): 161 | if self.obj['extra'].get('cssCode'): 162 | self.codeRenderer.add_css(self.obj['extra']['cssCode']) 163 | if self.obj['extra'].get('jsCode'): 164 | self.codeRenderer.add_css(self.obj['extra']['jsCode']) 165 | 166 | def render(self, context=None): 167 | """ 168 | Render Block 169 | :param context: dict - block context 170 | :return: safe str: Rendered str 171 | """ 172 | if isinstance(context, Context) or isinstance(context, RequestContext): 173 | context = context.flatten() 174 | self._prepare_custom_codes() 175 | self.build_general_settings() 176 | 177 | rendered_child = self.get_rendered_children(context) 178 | 179 | if context is None: 180 | new_context = self.get_context() 181 | else: 182 | new_context = self.get_context(parent_context=context) 183 | 184 | new_context['children'] = rendered_child 185 | 186 | if self.obj.get('extra', {}).get('htmlCode'): 187 | html_code = self.obj['extra'].get('htmlCode').strip() 188 | if len(html_code) > 0: 189 | template = Template(html_code) 190 | c = Context(new_context) 191 | return mark_safe(template.render(c)) 192 | 193 | template = self.get_template() 194 | 195 | return mark_safe(render_to_string(template, new_context)) 196 | 197 | def build_general_settings(self): 198 | """ 199 | General settings contains CSS properties. Add them to code_renderer 200 | :return: None 201 | """ 202 | inline_css = {} 203 | if self.obj.get('general'): 204 | general = self.obj['general'] 205 | if general.get('padding'): 206 | inline_css.update(_build_padding(general['padding'])) 207 | if general.get('margin'): 208 | inline_css.update(_build_padding(general['margin'])) 209 | if general.get('backgroundImage'): 210 | inline_css.update(_build_background_image(general['backgroundImage'])) 211 | if general.get('backgroundColor'): 212 | inline_css.update(_build_background_color(general['backgroundColor'])) 213 | if general.get('foregroundColor'): 214 | inline_css.update(_build_foreground_color(general['foregroundColor'])) 215 | if general.get('textAlignment'): 216 | inline_css.update({'text-align': general['textAlignment']}) 217 | if len(inline_css) > 0: 218 | self.codeRenderer.add_css(dict_to_css({'#%s' % self.obj['id']: inline_css})) 219 | 220 | def get_block_config(self): 221 | """ 222 | Build minimal block using python 223 | :return: dict / None - containing block info or None 224 | """ 225 | if not self.external: 226 | default_values = { 227 | 'settings': self.initial if self.initial else {}, 228 | 'general': self.general_initial if self.general_initial else {} 229 | } 230 | 231 | result = { 232 | "title": self.title, 233 | "description": self.description, 234 | "settings_schema": self.schema, 235 | "default_values": default_values, 236 | "config": { 237 | "styles": self.styles if self.styles else [{"id": 'default', "name": 'Default'}] 238 | } 239 | 240 | } 241 | return result 242 | else: 243 | return None 244 | -------------------------------------------------------------------------------- /hypereditor/blocks/chooser.py: -------------------------------------------------------------------------------- 1 | from django.core.paginator import Paginator 2 | from django.forms.models import model_to_dict 3 | from django.db.models import Q, Model 4 | import functools 5 | 6 | 7 | class Chooser(object): 8 | queryset = None 9 | fields = None 10 | search_fields = None 11 | ordering = '-id' 12 | 13 | def get_queryset(self): 14 | return self.queryset 15 | 16 | def get_name(self): 17 | queryset = self.get_queryset() 18 | return '%s|%s' % queryset.model._meta.app_label, queryset.model.__name__ 19 | 20 | def filter(self, q, queryset): 21 | if isinstance(self.search_fields, list): 22 | queries = [Q(**{"%s__icontains" % field: q}) for field in self.search_fields] 23 | queryset = queryset.filter(functools.reduce(lambda x, y: x | y, queries)) 24 | return queryset 25 | 26 | def get_ordering(self, queryset): 27 | return queryset.order_by(self.ordering) 28 | 29 | def get_fields(self, queryset): 30 | if isinstance(self.fields, list): 31 | return queryset.values(*self.fields) 32 | return queryset 33 | 34 | def get_paginator(self, q=None, per_page=20): 35 | queryset = self.get_queryset() 36 | queryset = self.filter(q, queryset) 37 | queryset = self.get_ordering(queryset) 38 | queryset = self.get_fields(queryset) 39 | paginator = Paginator(queryset, per_page=per_page) 40 | return paginator 41 | 42 | def serialize(self, item): 43 | if isinstance(item, Model): 44 | return model_to_dict(item) 45 | else: 46 | return item 47 | 48 | def paginate(self, request, q, page=1): 49 | paginator = self.get_paginator(q) 50 | page_obj = paginator.page(page) 51 | result = [self.serialize(item) for item in page_obj.object_list] 52 | return { 53 | 'total': paginator.count, 54 | 'per_page': paginator.per_page, 55 | 'current_page': page, 56 | 'result': result, 57 | } -------------------------------------------------------------------------------- /hypereditor/fields.py: -------------------------------------------------------------------------------- 1 | import json 2 | import random 3 | import string 4 | 5 | from django import forms, template 6 | from django.db import models 7 | from django.utils.safestring import mark_safe 8 | 9 | from hypereditor.blocks import get_block_class_for 10 | from hypereditor.blocks.base import CodeRenderer 11 | from hypereditor import settings 12 | from django.template.loader import render_to_string 13 | 14 | 15 | class HyperWidget(forms.widgets.Textarea): 16 | template_name = 'hypereditor/widgets/hyper_widget.html' 17 | 18 | def get_context(self, name, value, attrs): 19 | context = super().get_context(name, value, attrs) 20 | context['identifier'] = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) 21 | return context 22 | 23 | 24 | class HyperFormField(forms.CharField): 25 | def __init__(self, *args, **kwargs): 26 | kwargs['widget'] = HyperWidget(attrs={'id': 'hyperHiddenField'}) 27 | super(HyperFormField, self).__init__(*args, **kwargs) 28 | 29 | 30 | class HyperFieldResponse(object): 31 | 32 | def __init__(self, value_dict): 33 | self.data = value_dict 34 | 35 | def from_str(str_data): 36 | data = json.loads(str_data) 37 | return HyperFieldResponse(data) 38 | 39 | def render(self, contex=None): 40 | code_renderer, rendered_data = self.__render(context=contex) 41 | rendered_data = code_renderer.render_css_with_tag() + rendered_data + code_renderer.render_js_with_tag() 42 | return mark_safe(rendered_data) 43 | 44 | def api_render(self, context=None): 45 | code_renderer, rendered_data = self.__render(context=context) 46 | return { 47 | 'css': mark_safe(code_renderer.render_css()), 48 | 'html': mark_safe('rendered_data'), 49 | 'js': mark_safe(code_renderer.render_js()) 50 | } 51 | 52 | def __render(self, context=None): 53 | rendered_data = '' 54 | if isinstance(context, template.Context) or isinstance(context, template.RequestContext): 55 | context = context.flatten() 56 | code_renderer = CodeRenderer() 57 | for item in self.data: 58 | bl_class = get_block_class_for(item.get('type', 'INVALID_PLUGIN_WITH_NO_TYPE')) 59 | if bl_class: 60 | instance = bl_class(item, code_renderer) 61 | rendered_data = rendered_data + instance.render(context) 62 | return code_renderer, rendered_data 63 | 64 | 65 | def get_prep_value(self): 66 | return json.dumps(self.data) 67 | 68 | def __str__(self): 69 | return self.get_prep_value() 70 | 71 | 72 | if settings.WAGTAIL_EXISTS: 73 | from wagtail.admin.edit_handlers import FieldPanel 74 | 75 | 76 | class HyperFieldPanel(FieldPanel): 77 | object_template = "hypereditor/edit_handlers/hyper_editor_field_panel.html" 78 | 79 | def render_as_object(self): 80 | return mark_safe(render_to_string(self.object_template, { 81 | 'self': self, 82 | self.TEMPLATE_VAR: self, 83 | 'field': self.bound_field, 84 | 'identifier': ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) 85 | })) 86 | 87 | 88 | class HyperField(models.Field): 89 | 90 | def get_internal_type(self): 91 | return 'TextField' 92 | 93 | def formfield(self, **kwargs): 94 | defaults = {'form_class': HyperFormField} 95 | defaults.update(kwargs) 96 | return super().formfield(**defaults) 97 | 98 | def to_python(self, value): 99 | if isinstance(value, HyperFieldResponse): 100 | return value 101 | 102 | if value is not None and value != '': 103 | try: 104 | value_dict = json.loads(value) 105 | return HyperFieldResponse(value_dict) 106 | except Exception as e: 107 | print(value) 108 | return HyperFieldResponse(None) 109 | else: 110 | return [] 111 | 112 | def from_db_value(self, value, expression, connection, context): 113 | return self.to_python(value) 114 | 115 | def get_prep_value(self, value): 116 | if isinstance(value, HyperFieldResponse): 117 | return value.get_prep_value() 118 | else: 119 | return value 120 | 121 | def value_to_string(self, obj): 122 | value = self.value_from_object(obj) 123 | return self.get_prep_value(value) 124 | 125 | if settings.WAGTAIL_EXISTS: 126 | 127 | def get_panel(self): 128 | return HyperFieldPanel 129 | -------------------------------------------------------------------------------- /hypereditor/hyper_blocks.py: -------------------------------------------------------------------------------- 1 | from hypereditor.blocks.base import Block 2 | from hypereditor.blocks import get_block_class_for, register 3 | 4 | 5 | @register('column') 6 | class ColumnBlock(Block): 7 | 8 | external = True 9 | 10 | def _build_col_class(self, colSettings): 11 | col_class = [] 12 | if colSettings: 13 | for sz in ['XS', 'SM', 'MD', 'LG']: 14 | col_sz = colSettings.get('size' + sz, '12') 15 | tmp_sz = ['col'] 16 | if sz != 'XS': 17 | tmp_sz.append(sz.lower()) 18 | tmp_sz.append(str(col_sz)) 19 | col_class.append('-'.join(tmp_sz)) 20 | 21 | tmp_off = ['offset'] 22 | col_off = colSettings.get('offset' + sz) 23 | if col_off and col_off != '': 24 | if sz != 'XS': 25 | tmp_off.append(sz.lower()) 26 | tmp_off.append(str(col_off)) 27 | col_class.append('-'.join(tmp_off)) 28 | return ' '.join(col_class) 29 | 30 | def get_context(self, parent_context=None): 31 | value = parent_context.get('obj') 32 | value['colClass'] = self._build_col_class(value.get('settings')) 33 | context = super().get_context(parent_context=parent_context) 34 | return context 35 | 36 | 37 | @register('tab') 38 | class TabBlock(Block): 39 | 40 | external = True 41 | 42 | def get_rendered_children(self, obj, context): 43 | if obj.get('children') is not None: 44 | 45 | total_childes = [] 46 | for child in obj.get('children'): 47 | bl_class = get_block_class_for(child.get('type', 'INVALID_PLUGIN_WITH_NO_TYPE')) 48 | if bl_class: 49 | instance = bl_class(self.codeRenderer) 50 | rendered_child = instance.render(child, context) 51 | total_childes.append({ 52 | 'child': child, 53 | 'rendered': rendered_child 54 | }) 55 | return total_childes 56 | return [] 57 | 58 | 59 | @register('heading') 60 | class Heading(Block): 61 | 62 | title = 'Heading' 63 | description = 'Heading Block' 64 | 65 | schema = { 66 | "fields": [ 67 | { 68 | "type": "input", 69 | "inputType": "text", 70 | "label": "Heading Text", 71 | "model": "text" 72 | }, 73 | { 74 | "type": "select", 75 | "label": "Heading Type", 76 | "model": "type", 77 | "values": [ 78 | "H1", "H2", "H3", "H4", "H5", "H6" 79 | ] 80 | } 81 | ] 82 | } 83 | 84 | initial = { 85 | "type": 'H3' 86 | } -------------------------------------------------------------------------------- /hypereditor/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DivineITLimited/django-hyper-editor/666f5b0f4851adb399a97276a9d4f15737402ebf/hypereditor/migrations/__init__.py -------------------------------------------------------------------------------- /hypereditor/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /hypereditor/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.apps import apps 3 | from hypereditor import utils 4 | 5 | WAGTAIL_EXISTS = apps.is_installed('wagtail.core') 6 | 7 | HYPER_SETTINGS = { 8 | 'BLOCK_CONFIG': {}, 9 | 'STYLESHEETS': [], 10 | 'IMAGE_API_URL': '#', 11 | 'AUTHENTICATION_MIXIN': 'hypereditor.views.AuthMixin' 12 | } 13 | 14 | utils.merge_dict(HYPER_SETTINGS, getattr(settings, 'HYPER_EDITOR', {})) 15 | 16 | # A mixin class that will be used auth and permission of HyperEditor views 17 | AUTHENTICATION_MIXIN = HYPER_SETTINGS['AUTHENTICATION_MIXIN'] 18 | 19 | # Provide styles or other things to block via settings 20 | BLOCK_CONFIG = HYPER_SETTINGS['BLOCK_CONFIG'] 21 | 22 | # user defined stylesheet to look pretty 23 | STYLESHEETS = HYPER_SETTINGS['STYLESHEETS'] 24 | 25 | # url that will be used as image api 26 | IMAGE_API_URL = HYPER_SETTINGS['IMAGE_API_URL'] 27 | -------------------------------------------------------------------------------- /hypereditor/templates/hypereditor/blocks/column/default.html: -------------------------------------------------------------------------------- 1 |
2 | {{ children }} 3 |
-------------------------------------------------------------------------------- /hypereditor/templates/hypereditor/blocks/contentbox/default.html: -------------------------------------------------------------------------------- 1 |
2 | {% if obj.settings.imageOrientation == 'left' %} 3 |
4 |
5 | 6 |
7 |
8 |

{{ obj.settings.title }} {{ obj.settings.subtitle }}

9 |
10 | {{ obj.settings.description|safe }} 11 |
12 |
13 |
14 | {% elif obj.settings.imageOrientation == 'right' %} 15 |
16 |
17 |

{{ obj.settings.title }} {{ obj.settings.subtitle }}

18 |
19 | {{ obj.settings.description|safe }} 20 |
21 |
22 |
23 | 24 |
25 |
26 | {% elif obj.settings.imageOrientation == 'top' %} 27 |
28 |
29 | 30 |
31 |
32 |

{{ obj.settings.title }} {{ obj.settings.subtitle }}

33 |
34 | {{ obj.settings.description|safe }} 35 |
36 |
37 |
38 | {% else %} 39 |
40 |
41 |

{{ obj.settings.title }} {{ obj.settings.subtitle }}

42 |
43 | {{ obj.settings.description|safe }} 44 |
45 |
46 |
47 | 48 |
49 |
50 | {% endif %} 51 |
-------------------------------------------------------------------------------- /hypereditor/templates/hypereditor/blocks/heading/default.html: -------------------------------------------------------------------------------- 1 | <{{ obj.settings.type }}>{{ obj.settings.text }} -------------------------------------------------------------------------------- /hypereditor/templates/hypereditor/blocks/image/default.html: -------------------------------------------------------------------------------- 1 | {% load hyper_tags %} 2 | 3 |
4 | 5 |
6 | 7 | 8 | -------------------------------------------------------------------------------- /hypereditor/templates/hypereditor/blocks/link/default.html: -------------------------------------------------------------------------------- 1 | 2 | {{ children }} 3 | -------------------------------------------------------------------------------- /hypereditor/templates/hypereditor/blocks/row/default.html: -------------------------------------------------------------------------------- 1 |
2 | {{ children }} 3 |
-------------------------------------------------------------------------------- /hypereditor/templates/hypereditor/blocks/section/default.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ children }} 4 |
5 |
-------------------------------------------------------------------------------- /hypereditor/templates/hypereditor/blocks/slider/default.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /hypereditor/templates/hypereditor/blocks/tab/default.html: -------------------------------------------------------------------------------- 1 | 2 | 9 |
10 | {% for item in children %} 11 | {% with obj.settings.tabItems|first as selected %} 12 |
13 | {{ item.rendered|safe }} 14 |
15 | {% endwith %} 16 | {% endfor %} 17 |
-------------------------------------------------------------------------------- /hypereditor/templates/hypereditor/blocks/text/default.html: -------------------------------------------------------------------------------- 1 |
2 | {{ obj.settings.text|safe }} 3 |
4 | -------------------------------------------------------------------------------- /hypereditor/templates/hypereditor/edit_handlers/hyper_editor_field_panel.html: -------------------------------------------------------------------------------- 1 | {% load wagtailadmin_tags static %} 2 | 3 | {% with 'hef_'|add:identifier as field_id %} 4 | {% with 'hei_'|add:identifier as iframe_id %} 5 | 6 |
7 | 8 | {{ self.heading }} 9 | 10 | 15 | 16 | {% include 'hypereditor/partials/editor_iframe.html' with field_id=field_id iframe_id=iframe_id %} 17 | 18 |
19 | 20 | {% endwith %} 21 | {% endwith %} 22 | 23 | -------------------------------------------------------------------------------- /hypereditor/templates/hypereditor/hyper_editor.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | HyperEditor 10 | 11 | 12 | 13 | {% for st in user_stylesheets %} 14 | 15 | {% endfor %} 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 116 | 117 | 118 | {% for js_plugin in js_plugins %} 119 | 120 | {% endfor %} 121 | 122 | 123 | 124 | 125 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /hypereditor/templates/hypereditor/js/blocks.js: -------------------------------------------------------------------------------- 1 | {% for block_type, config in blocks.items %} 2 | hyperEditor.registerBlock('{{ block_type }}', JSON.parse('{{ config|safe }}')) 3 | {% endfor %} -------------------------------------------------------------------------------- /hypereditor/templates/hypereditor/js/fields.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var MenuChooserField = function (page_search_url) { 3 | return { 4 | type: 'chooser', 5 | label: 'Menu Chooser', 6 | model: 'menu', 7 | chooserButtonText: 'Choose Menu', 8 | getItems: function(query, page, perPage, callback) { 9 | var str = 'q='+query + '&page='+page + '&perPage='+perPage 10 | fetch( page_search_url + '?' + str, {credentials: 'include'}) 11 | .then(function(response) { return response.json() }) 12 | .then(function(data) { 13 | callback({ 14 | total: data.total, 15 | currentPage: data.current_page, 16 | result: data.result, 17 | }) 18 | }) 19 | }, 20 | 21 | displayItem: function(item) { 22 | return '
'+ item.title +'
' 23 | }, 24 | 25 | displaySelectedItem: function (item) { 26 | return '
'+ item.title +'
' 27 | } 28 | } 29 | } 30 | })() 31 | -------------------------------------------------------------------------------- /hypereditor/templates/hypereditor/partials/editor_iframe.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /hypereditor/templates/hypereditor/preview.html: -------------------------------------------------------------------------------- 1 | {% load hyper_tags %} 2 | 3 | {% hyper_preview value %} -------------------------------------------------------------------------------- /hypereditor/templates/hypereditor/widgets/hyper_widget.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | {% with 'hef_'|add:identifier as field_id %} 4 | {% with 'hei_'|add:identifier as iframe_id %} 5 | 6 |
7 | 8 | 10 | 11 | {% include 'hypereditor/partials/editor_iframe.html' with field_id=field_id iframe_id=iframe_id %} 12 |
13 | {% endwith %} 14 | {% endwith %} 15 | -------------------------------------------------------------------------------- /hypereditor/templatetags/hyper_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from hypereditor.blocks import get_block_class_for 3 | from hypereditor.blocks.base import CodeRenderer 4 | from hypereditor.fields import HyperFieldResponse 5 | from django.template.defaulttags import token_kwargs 6 | register = template.Library() 7 | 8 | 9 | class HyperNode(template.Node): 10 | 11 | def __init__(self, block_var, extra_context, use_parent_context): 12 | self.block_var = block_var 13 | self.extra_context = extra_context 14 | self.use_parent_context = use_parent_context 15 | 16 | def render(self, context): 17 | try: 18 | value = self.block_var.resolve(context) 19 | except template.VariableDoesNotExist: 20 | return '' 21 | 22 | if isinstance(value, dict): # its an preview 23 | code_renderer = CodeRenderer() 24 | bl_class = get_block_class_for(value.get('type', 'INVALID_PLUGIN_WITH_NO_TYPE')) 25 | if bl_class: 26 | instance = bl_class(value, code_renderer) 27 | return instance.render(context) 28 | else: 29 | return '' 30 | elif isinstance(value, str): 31 | try: 32 | value = HyperFieldResponse.from_str(value) 33 | return value.render(context) 34 | except Exception as e: 35 | print(e) 36 | return '' 37 | else: 38 | return value.render(context) 39 | 40 | 41 | def __render_helper(parser, token): 42 | 43 | tokens = token.split_contents() 44 | 45 | try: 46 | tag_name = tokens.pop(0) 47 | block_var_token = tokens.pop(0) 48 | except IndexError: 49 | raise template.TemplateSyntaxError("%r tag requires at least one argument" % tag_name) 50 | 51 | block_var = parser.compile_filter(block_var_token) 52 | 53 | if tokens and tokens[0] == 'with': 54 | tokens.pop(0) 55 | extra_context = token_kwargs(tokens, parser) 56 | else: 57 | extra_context = None 58 | 59 | use_parent_context = True 60 | if tokens and tokens[0] == 'only': 61 | tokens.pop(0) 62 | use_parent_context = False 63 | 64 | if tokens: 65 | raise template.TemplateSyntaxError("Unexpected argument to %r tag: %r" % (tag_name, tokens[0])) 66 | 67 | return HyperNode(block_var, extra_context, use_parent_context) 68 | 69 | 70 | @register.tag 71 | def hyper_render(parser, token): 72 | return __render_helper(parser, token) 73 | 74 | 75 | @register.tag 76 | def hyper_preview(parser, token): 77 | return __render_helper(parser, token) 78 | 79 | -------------------------------------------------------------------------------- /hypereditor/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DivineITLimited/django-hyper-editor/666f5b0f4851adb399a97276a9d4f15737402ebf/hypereditor/tests/__init__.py -------------------------------------------------------------------------------- /hypereditor/tests/mocks.py: -------------------------------------------------------------------------------- 1 | TEXT_BLOCK = { 2 | "type": "text", 3 | "children": [], 4 | "settings": { 5 | "text": "Sample Text" 6 | }, 7 | "id": "i1554530213_4", 8 | "general": { 9 | "padding": { 10 | "unit": "px", 11 | "left": None, 12 | "top": None, 13 | "right": None, 14 | "bottom": None 15 | }, 16 | "margin": { 17 | "unit": "px", 18 | "left": None, 19 | "top": None, 20 | "right": None, 21 | "bottom": None 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /hypereditor/tests/test_chooser.py: -------------------------------------------------------------------------------- 1 | from django.test import TransactionTestCase 2 | from sandbox.example.models import Page 3 | from hypereditor.blocks.chooser import Chooser 4 | 5 | 6 | class PageChooser(Chooser): 7 | queryset = Page.objects.filter() 8 | 9 | 10 | class PageChooser2(Chooser): 11 | queryset = Page.objects.filter() 12 | fields = ['title', 'id'] 13 | 14 | 15 | class ChooserTestCase(TransactionTestCase): 16 | 17 | def setUp(self): 18 | for count in range(0, 40): 19 | Page.objects.create(title='Test %s' % count, content='[]') 20 | 21 | def test_chooser(self): 22 | page_chooser = PageChooser() 23 | result = page_chooser.paginate(None, None) 24 | self.assertEqual(result['total'], 40) 25 | 26 | def test_chooser2(self): 27 | page_chooser = PageChooser2() 28 | result = page_chooser.paginate(None, None) 29 | self.assertEqual(result['total'], 40) 30 | -------------------------------------------------------------------------------- /hypereditor/tests/test_templatetags.py: -------------------------------------------------------------------------------- 1 | import json 2 | from django.test import SimpleTestCase 3 | from django.template import Context, Template 4 | from hypereditor.tests import mocks 5 | 6 | 7 | class HyperRenderTemplateTagTest(SimpleTestCase): 8 | """Tests for hyper_render template tag""" 9 | 10 | def test_render(self): 11 | context = Context({'content': json.dumps([mocks.TEXT_BLOCK])}) 12 | template_to_render = Template( 13 | '{% load hyper_tags %}' 14 | '{% hyper_render content %}' 15 | ) 16 | rendered_template = template_to_render.render(context) 17 | self.assertInHTML('
\nSample Text\n
', rendered_template) 18 | 19 | 20 | class HyperPreviewTemplateTagTest(SimpleTestCase): 21 | """Tests for hyper_preview template tag""" 22 | 23 | def test_preview(self): 24 | context = Context({'content': mocks.TEXT_BLOCK}) 25 | template_to_render = Template( 26 | '{% load hyper_tags %}' 27 | '{% hyper_preview content %}' 28 | ) 29 | rendered_template = template_to_render.render(context) 30 | self.assertInHTML('
\nSample Text\n
', rendered_template) 31 | 32 | -------------------------------------------------------------------------------- /hypereditor/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | from hypereditor.views import * 3 | 4 | app_name = 'hypereditor' 5 | 6 | urlpatterns = [ 7 | url(r'editor/$', EditorView.as_view(), name='editor'), 8 | url(r'preview/', PreviewView.as_view(), name='preview'), 9 | url(r'chooser-api/(?P\w{0,50})/', ChooserAPIView.as_view(), name='chooser_api'), 10 | url(r'js/blocks', GenerateBlock.as_view(), name='blocks') 11 | ] 12 | -------------------------------------------------------------------------------- /hypereditor/utils.py: -------------------------------------------------------------------------------- 1 | from importlib import import_module 2 | import collections 3 | from django.apps import apps 4 | from django.utils.module_loading import module_has_submodule 5 | 6 | 7 | def dict_to_css(css_dict, pretty=False): 8 | """Takes a dictionary and creates CSS from it 9 | :param css_dict: python dictionary containing css rules 10 | :param pretty: if css should be generated as pretty 11 | :return: css as string 12 | """ 13 | seperator = '\n' 14 | tab = '\t' 15 | if not pretty: 16 | seperator = '' 17 | tab = '' 18 | 19 | css_rules = [] 20 | for selector, rules in css_dict.items(): 21 | tmp = selector + '{' + seperator 22 | tmp_rules = [] 23 | if isinstance(rules, dict): 24 | for rule, value in rules.items(): 25 | tmp_rules.append(tab + rule + ':' + value + ';') 26 | tmp += seperator.join(tmp_rules) 27 | tmp = tmp + '}' 28 | css_rules.append(tmp) 29 | 30 | return seperator.join(css_rules) 31 | 32 | 33 | def dynamic_import(val): 34 | """Imports a python module dynamically 35 | :param val: String module path 36 | :return: None 37 | """ 38 | try: 39 | # Nod to tastypie's use of importlib. 40 | parts = val.split('.') 41 | module_path, class_name = '.'.join(parts[:-1]), parts[-1] 42 | module = import_module(module_path) 43 | return getattr(module, class_name) 44 | except ImportError as e: 45 | msg = "Could not import '%s' for setting. %s: %s." % (val, e.__class__.__name__, e) 46 | raise ImportError(msg) 47 | 48 | 49 | def get_app_modules(): 50 | for app in apps.get_app_configs(): 51 | yield app.name, app.module 52 | 53 | 54 | def load_hyper_blocks(): 55 | submodule_name = 'hyper_blocks' 56 | for name, module in get_app_modules(): 57 | if module_has_submodule(module, submodule_name): 58 | yield name, import_module('%s.%s' % (name, submodule_name)) 59 | 60 | 61 | def merge_dict(d, m_d): 62 | """ 63 | Given two dictionary, merge m_d dictionary into d 64 | :param d: dict 65 | :param m_d: dict 66 | :return: None 67 | """ 68 | for k, v in m_d.items(): 69 | if k in d and isinstance(d[k], dict) and isinstance(d[k], collections.Mapping): 70 | merge_dict(d[k], m_d[k]) 71 | else: 72 | d[k] = m_d[k] 73 | -------------------------------------------------------------------------------- /hypereditor/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.contrib.auth.mixins import UserPassesTestMixin, LoginRequiredMixin 4 | from django.utils.module_loading import import_string 5 | from django.views.generic import TemplateView, View 6 | from django.http.response import JsonResponse 7 | 8 | from hypereditor import settings 9 | from hypereditor.blocks import js_variable_str, get_js_plugins, get_simpler_blocks, get_chooser_for 10 | 11 | 12 | class AuthMixin(UserPassesTestMixin, LoginRequiredMixin): 13 | """Helper mixin for authentication on hyper editor urls""" 14 | 15 | def test_func(self): 16 | return self.request.user.is_superuser 17 | 18 | 19 | AuthenticationMixin = import_string(settings.AUTHENTICATION_MIXIN) 20 | 21 | 22 | class EditorView(AuthenticationMixin, TemplateView): 23 | """View for hyper editor inside iframe""" 24 | 25 | template_name = 'hypereditor/hyper_editor.html' 26 | 27 | def get_context_data(self, **kwargs): 28 | context = super().get_context_data(**kwargs) 29 | context['block_settings'] = json.dumps(settings.BLOCK_CONFIG) 30 | context['user_stylesheets'] = settings.STYLESHEETS 31 | context['js_variables'] = js_variable_str 32 | context['js_plugins'] = get_js_plugins() 33 | context['image_api_url'] = settings.IMAGE_API_URL 34 | return context 35 | 36 | 37 | class PreviewView(AuthenticationMixin, TemplateView): 38 | """View for preview of a single block""" 39 | 40 | http_method_names = ['post'] 41 | template_name = 'hypereditor/preview.html' 42 | 43 | def post(self, request, **kwargs): 44 | context = super().get_context_data(**kwargs) 45 | context['value'] = json.loads(request.body) 46 | return self.render_to_response(context=context) 47 | 48 | 49 | class GenerateBlock(AuthenticationMixin, TemplateView): 50 | template_name = 'hypereditor/js/blocks.js' 51 | 52 | def options(self, request, *args, **kwargs): 53 | response = super().options(request, *args, **kwargs) 54 | response['Content-Type'] = 'application/javascript' 55 | return response 56 | 57 | def get(self, request, *args, **kwargs): 58 | return self.render_to_response(context={ 59 | 'blocks': get_simpler_blocks() 60 | }) 61 | 62 | 63 | class ChooserAPIView(AuthenticationMixin, View): 64 | 65 | def get(self, request, chooser_type, *args, **kwargs): 66 | chooser_cls = get_chooser_for(chooser_type) 67 | q = request.GET.get('q') 68 | page = request.GET.get('page', 1) 69 | 70 | if chooser_cls: 71 | chooser = chooser_cls() 72 | result = chooser.paginate(request, q, page=page) 73 | 74 | return JsonResponse(result, status=200) 75 | else: 76 | return JsonResponse({'message': 'Invalid chooser'}, status=400) -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sandbox.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Django Hyper Editor 2 | site_description: Django integration of Hyper Editor 3 | site_author: Shimul Chowdhury 4 | copyright: Copyright © 2019 Divine IT Limited 5 | repo_url: https://github.com/DivineITLimited/django-hyper-editor 6 | theme: readthedocs 7 | nav: 8 | - 'Introduction': 'index.md' 9 | - 'Usage': 'usage.md' 10 | - 'Blocks': 'blocks.md' 11 | - 'Wagtail': 'wagtail.md' -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | build: 2 | image: latest 3 | 4 | python: 5 | setup_py_install: true 6 | -------------------------------------------------------------------------------- /sandbox/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DivineITLimited/django-hyper-editor/666f5b0f4851adb399a97276a9d4f15737402ebf/sandbox/__init__.py -------------------------------------------------------------------------------- /sandbox/example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DivineITLimited/django-hyper-editor/666f5b0f4851adb399a97276a9d4f15737402ebf/sandbox/example/__init__.py -------------------------------------------------------------------------------- /sandbox/example/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from sandbox.example.models import * 3 | 4 | 5 | @admin.register(Page) 6 | class PageAdmin(admin.ModelAdmin): 7 | pass 8 | -------------------------------------------------------------------------------- /sandbox/example/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ExampleConfig(AppConfig): 5 | name = 'example' 6 | -------------------------------------------------------------------------------- /sandbox/example/hyper_blocks.py: -------------------------------------------------------------------------------- 1 | from sandbox.example.models import Page 2 | from hypereditor.blocks import Chooser, register_chooser 3 | 4 | 5 | @register_chooser('page_chooser') 6 | class PageChooser(Chooser): 7 | queryset = Page.objects.filter() 8 | fields = ['id', 'title'] 9 | search_fields = ['title'] -------------------------------------------------------------------------------- /sandbox/example/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2019-04-07 17:28 2 | 3 | from django.db import migrations, models 4 | import hypereditor.fields 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Page', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('title', models.CharField(max_length=255)), 20 | ('content', hypereditor.fields.HyperField(default=None)), 21 | ], 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /sandbox/example/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DivineITLimited/django-hyper-editor/666f5b0f4851adb399a97276a9d4f15737402ebf/sandbox/example/migrations/__init__.py -------------------------------------------------------------------------------- /sandbox/example/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from hypereditor.fields import HyperField 3 | 4 | 5 | class Page(models.Model): 6 | title = models.CharField(max_length=255) 7 | content = HyperField(default=None) 8 | 9 | def __str__(self): 10 | return self.title -------------------------------------------------------------------------------- /sandbox/example/templates/test_form.html: -------------------------------------------------------------------------------- 1 | {{ form }} -------------------------------------------------------------------------------- /sandbox/example/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /sandbox/example/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import FormView 2 | from django import forms 3 | from sandbox.example import models 4 | 5 | 6 | class TestForm(forms.ModelForm): 7 | class Meta: 8 | model = models.Page 9 | exclude = [] 10 | 11 | 12 | class TestFromView(FormView): 13 | form_class = TestForm 14 | template_name = 'test_form.html' 15 | 16 | -------------------------------------------------------------------------------- /sandbox/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for sandbox project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '4jzu^=bygr0*4cgvn&@q6ey(#4i3l3h$jm@bb$c0&x@*hgjqnd' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 41 | 'hypereditor', 42 | 43 | 'sandbox.example' 44 | ] 45 | 46 | MIDDLEWARE = [ 47 | 'django.middleware.security.SecurityMiddleware', 48 | 'django.contrib.sessions.middleware.SessionMiddleware', 49 | 'django.middleware.common.CommonMiddleware', 50 | 'django.middleware.csrf.CsrfViewMiddleware', 51 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 52 | 'django.contrib.messages.middleware.MessageMiddleware', 53 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 54 | ] 55 | 56 | ROOT_URLCONF = 'sandbox.urls' 57 | 58 | TEMPLATES = [ 59 | { 60 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 61 | 'DIRS': [], 62 | 'APP_DIRS': True, 63 | 'OPTIONS': { 64 | 'context_processors': [ 65 | 'django.template.context_processors.debug', 66 | 'django.template.context_processors.request', 67 | 'django.contrib.auth.context_processors.auth', 68 | 'django.contrib.messages.context_processors.messages', 69 | ], 70 | }, 71 | }, 72 | ] 73 | 74 | WSGI_APPLICATION = 'sandbox.wsgi.application' 75 | 76 | 77 | # Database 78 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases 79 | 80 | DATABASES = { 81 | 'default': { 82 | 'ENGINE': 'django.db.backends.sqlite3', 83 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 84 | } 85 | } 86 | 87 | 88 | # Password validation 89 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators 90 | 91 | AUTH_PASSWORD_VALIDATORS = [ 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 100 | }, 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 103 | }, 104 | ] 105 | 106 | 107 | # Internationalization 108 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 109 | 110 | LANGUAGE_CODE = 'en-us' 111 | 112 | TIME_ZONE = 'UTC' 113 | 114 | USE_I18N = True 115 | 116 | USE_L10N = True 117 | 118 | USE_TZ = True 119 | 120 | 121 | # Static files (CSS, JavaScript, Images) 122 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 123 | 124 | STATIC_URL = '/static/' 125 | -------------------------------------------------------------------------------- /sandbox/urls.py: -------------------------------------------------------------------------------- 1 | """sandbox URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.conf import settings 17 | from django.contrib import admin 18 | from django.urls import path, include 19 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 20 | from sandbox.example import views 21 | 22 | urlpatterns = [ 23 | path('admin/', admin.site.urls), 24 | path('hypereditor/', include('hypereditor.urls')), 25 | path('', views.TestFromView.as_view()) 26 | ] 27 | 28 | if settings.DEBUG: 29 | urlpatterns += staticfiles_urlpatterns() 30 | 31 | -------------------------------------------------------------------------------- /sandbox/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for sandbox project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sandbox.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from setuptools import find_packages, setup 3 | 4 | HERE = pathlib.Path(__file__).parent 5 | 6 | README = (HERE / "README.md").read_text() 7 | 8 | setup( 9 | name='hypereditor', 10 | version='0.1.0', 11 | packages=find_packages(), 12 | include_package_data=True, 13 | license='MIT', 14 | description='Hyper Editor Integration With Django', 15 | long_description=README, 16 | long_description_content_type='text/markdown', 17 | author='Shimul Chowdhury', 18 | author_email='shimul@divine-it.net', 19 | install_requires=[ 20 | "Django>=1.11,<2.3", 21 | ], 22 | classifiers=[ 23 | 'Environment :: Web Environment', 24 | 'Intended Audience :: Developers', 25 | 'Operating System :: OS Independent', 26 | 'Programming Language :: Python', 27 | 'Programming Language :: Python :: 3', 28 | 'Programming Language :: Python :: 3.4', 29 | 'Programming Language :: Python :: 3.5', 30 | 'Programming Language :: Python :: 3.6', 31 | 'Programming Language :: Python :: 3.7', 32 | 'Framework :: Django', 33 | 'Framework :: Django :: 2.0', 34 | 'Framework :: Django :: 2.1', 35 | 'Framework :: Django :: 2.2', 36 | ], 37 | ) 38 | --------------------------------------------------------------------------------