├── .coveragerc ├── .editorconfig ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── AUTHORS.rst ├── CONTRIBUTING.rst ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── djangocms_spa_vue_js ├── __init__.py ├── cms_menus.py ├── menu_helpers.py ├── middleware.py ├── models.py ├── router_helpers.py ├── templatetags │ ├── __init__.py │ └── router_tags.py └── views.py ├── manage.py ├── requirements.txt ├── requirements_dev.txt ├── requirements_test.txt ├── runtests.py ├── setup.cfg ├── setup.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = true 3 | 4 | [report] 5 | omit = 6 | *site-packages* 7 | *tests* 8 | *.tox* 9 | show_missing = True 10 | exclude_lines = 11 | raise NotImplementedError 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{py,rst,ini}] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.{html,css,scss,json,yml}] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | 22 | [Makefile] 23 | indent_style = tab 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * djangocms-spa-vue-js version: 2 | * Django version: 3 | * Python version: 4 | * Operating System: 5 | 6 | ### Description 7 | 8 | Describe what you were trying to get done. 9 | Tell us what happened, what went wrong, and what you expected to happen. 10 | 11 | ### What I Did 12 | 13 | ``` 14 | Paste the command(s) you ran and the output. 15 | If there was a crash, please include the traceback here. 16 | ``` 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | # PyCharm 92 | .idea 93 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at travis-ci.org 2 | 3 | language: python 4 | 5 | python: 6 | - "3.5" 7 | 8 | env: 9 | - TOX_ENV=py35-django-18 10 | - TOX_ENV=py34-django-18 11 | - TOX_ENV=py33-django-18 12 | - TOX_ENV=py27-django-18 13 | - TOX_ENV=py35-django-19 14 | - TOX_ENV=py34-django-19 15 | - TOX_ENV=py27-django-19 16 | - TOX_ENV=py35-django-110 17 | - TOX_ENV=py34-django-110 18 | - TOX_ENV=py27-django-110 19 | 20 | matrix: 21 | fast_finish: true 22 | 23 | # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors 24 | install: pip install -r requirements_test.txt 25 | 26 | # command to run tests using coverage, e.g. python setup.py test 27 | script: tox -e $TOX_ENV 28 | 29 | after_success: 30 | - codecov -e TOX_ENV 31 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * dreipol GmbH 9 | 10 | Contributors 11 | ------------ 12 | 13 | None yet. Why not be the first? 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | You can contribute in many ways: 9 | 10 | Types of Contributions 11 | ---------------------- 12 | 13 | Report Bugs 14 | ~~~~~~~~~~~ 15 | 16 | Report bugs at https://github.com/dreipol/djangocms-spa-vue-js/issues. 17 | 18 | If you are reporting a bug, please include: 19 | 20 | * Your operating system name and version. 21 | * Any details about your local setup that might be helpful in troubleshooting. 22 | * Detailed steps to reproduce the bug. 23 | 24 | Fix Bugs 25 | ~~~~~~~~ 26 | 27 | Look through the GitHub issues for bugs. Anything tagged with "bug" 28 | is open to whoever wants to implement it. 29 | 30 | Implement Features 31 | ~~~~~~~~~~~~~~~~~~ 32 | 33 | Look through the GitHub issues for features. Anything tagged with "feature" 34 | is open to whoever wants to implement it. 35 | 36 | Write Documentation 37 | ~~~~~~~~~~~~~~~~~~~ 38 | 39 | djangocms-spa-vue-js could always use more documentation, whether as part of the 40 | official djangocms-spa-vue-js docs, in docstrings, or even on the web in blog posts, 41 | articles, and such. 42 | 43 | Submit Feedback 44 | ~~~~~~~~~~~~~~~ 45 | 46 | The best way to send feedback is to file an issue at https://github.com/dreipol/djangocms-spa-vue-js/issues. 47 | 48 | If you are proposing a feature: 49 | 50 | * Explain in detail how it would work. 51 | * Keep the scope as narrow as possible, to make it easier to implement. 52 | * Remember that this is a volunteer-driven project, and that contributions 53 | are welcome :) 54 | 55 | Get Started! 56 | ------------ 57 | 58 | Ready to contribute? Here's how to set up `djangocms-spa-vue-js` for local development. 59 | 60 | 1. Fork the `djangocms-spa-vue-js` repo on GitHub. 61 | 2. Clone your fork locally:: 62 | 63 | $ git clone git@github.com:your_name_here/djangocms-spa-vue-js.git 64 | 65 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: 66 | 67 | $ mkvirtualenv djangocms-spa-vue-js 68 | $ cd djangocms-spa-vue-js/ 69 | $ python setup.py develop 70 | 71 | 4. Create a branch for local development:: 72 | 73 | $ git checkout -b name-of-your-bugfix-or-feature 74 | 75 | Now you can make your changes locally. 76 | 77 | 5. When you're done making changes, check that your changes pass flake8 and the 78 | tests, including testing other Python versions with tox:: 79 | 80 | $ flake8 djangocms_spa_vue_js tests 81 | $ python setup.py test 82 | $ tox 83 | 84 | To get flake8 and tox, just pip install them into your virtualenv. 85 | 86 | 6. Commit your changes and push your branch to GitHub:: 87 | 88 | $ git add . 89 | $ git commit -m "Your detailed description of your changes." 90 | $ git push origin name-of-your-bugfix-or-feature 91 | 92 | 7. Submit a pull request through the GitHub website. 93 | 94 | Pull Request Guidelines 95 | ----------------------- 96 | 97 | Before you submit a pull request, check that it meets these guidelines: 98 | 99 | 1. The pull request should include tests. 100 | 2. If the pull request adds functionality, the docs should be updated. Put 101 | your new functionality into a function with a docstring, and add the 102 | feature to the list in README.rst. 103 | 3. The pull request should work for Python 2.6, 2.7, and 3.3, and for PyPy. Check 104 | https://travis-ci.org/dreipol/djangocms-spa-vue-js/pull_requests 105 | and make sure that the tests pass for all supported Python versions. 106 | 107 | Tips 108 | ---- 109 | 110 | To run a subset of tests:: 111 | 112 | $ python -m unittest tests.test_djangocms_spa_vue_js 113 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | History 4 | ------- 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 dreipol 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. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include README.rst 6 | recursive-include djangocms_spa_vue_js *.html *.png *.gif *js *.css *jpg *jpeg *svg *py 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | define BROWSER_PYSCRIPT 4 | import os, webbrowser, sys 5 | try: 6 | from urllib import pathname2url 7 | except: 8 | from urllib.request import pathname2url 9 | 10 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 11 | endef 12 | export BROWSER_PYSCRIPT 13 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 14 | 15 | help: 16 | @perl -nle'print $& if m{^[a-zA-Z_-]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-25s\033[0m %s\n", $$1, $$2}' 17 | 18 | clean: clean-build clean-pyc 19 | 20 | clean-build: ## remove build artifacts 21 | rm -fr build/ 22 | rm -fr dist/ 23 | rm -fr *.egg-info 24 | 25 | clean-pyc: ## remove Python file artifacts 26 | find . -name '*.pyc' -exec rm -f {} + 27 | find . -name '*.pyo' -exec rm -f {} + 28 | find . -name '*~' -exec rm -f {} + 29 | 30 | lint: ## check style with flake8 31 | flake8 djangocms_spa_vue_js tests 32 | 33 | test: ## run tests quickly with the default Python 34 | python runtests.py tests 35 | 36 | test-all: ## run tests on every Python version with tox 37 | tox 38 | 39 | coverage: ## check code coverage quickly with the default Python 40 | coverage run --source djangocms_spa_vue_js runtests.py tests 41 | coverage report -m 42 | coverage html 43 | open htmlcov/index.html 44 | 45 | docs: ## generate Sphinx HTML documentation, including API docs 46 | rm -f docs/djangocms-spa-vue-js.rst 47 | rm -f docs/modules.rst 48 | sphinx-apidoc -o docs/ djangocms_spa_vue_js 49 | $(MAKE) -C docs clean 50 | $(MAKE) -C docs html 51 | $(BROWSER) docs/_build/html/index.html 52 | 53 | release: clean ## package and upload a release 54 | python setup.py sdist upload 55 | python setup.py bdist_wheel upload 56 | 57 | sdist: clean ## package 58 | python setup.py sdist 59 | ls -l dist 60 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ==================== 2 | djangocms-spa-vue-js 3 | ==================== 4 | 5 | This package prepares your django CMS and vue.js project to create a single-page application (SPA). Use it together 6 | with the base package `djangocms-spa`_. 7 | 8 | A template tag renders a list of all available routes that are used by vue-router. Contents of other pages are 9 | requested asynchronously and delivered as JSON through a REST-API. 10 | 11 | Make sure you read the docs of djangocms-spa. 12 | 13 | .. _`djangocms-spa`: https://github.com/dreipol/djangocms-spa/ 14 | 15 | 16 | Quickstart 17 | ---------- 18 | 19 | Install djangocms-spa-vue-js:: 20 | 21 | pip install djangocms-spa-vue-js 22 | 23 | Add it to your ``INSTALLED_APPS``: 24 | 25 | .. code-block:: python 26 | 27 | INSTALLED_APPS = ( 28 | ... 29 | 'djangocms_spa', 30 | 'djangocms_spa_vue_js', 31 | ... 32 | ) 33 | 34 | Add the URL pattern form the API: 35 | 36 | .. code-block:: python 37 | 38 | urlpatterns = [ 39 | ... 40 | url(r'^api/', include('djangocms_spa.urls', namespace='api')), 41 | ... 42 | ] 43 | 44 | Render your vue.js router in your template:: 45 | 46 | {% load router_tags %} 47 | {% vue_js_router %} 48 | 49 | 50 | 51 | Apphooks 52 | -------- 53 | 54 | You need to consider a couple of things when using apphooks. Let's assume you have an event model. 55 | 56 | .. code-block:: python 57 | 58 | class Event(DjangocmsVueJsMixin): 59 | name = models.CharField(max_length=255, verbose_name=_('Name')) 60 | 61 | def get_frontend_list_data_dict(self, request, editable=False, placeholder_name=''): 62 | # Returns the data for your list view. 63 | data = super(Event, self).get_frontend_list_data_dict(request=request, editable=editable, placeholder_name=placeholder_name) 64 | data['content'].update({ 65 | 'name': self.name, 66 | }) 67 | return data 68 | 69 | def get_frontend_detail_data_dict(self, request, editable=False): 70 | # Returns the data for your detail view. 71 | data = super(Event, self).get_frontend_detail_data_dict(request, editable) 72 | 73 | # Prepare the content of your model instance. We use the same structure like the placeholder data of a CMS page. 74 | content_container = { 75 | 'type': 'generic', 76 | 'content': { 77 | 'name': self.name 78 | } 79 | } 80 | 81 | # Add support for the CMS frontend editing 82 | if editable: 83 | content_container.update( 84 | self.get_cms_placeholder_json(request=request, placeholder_name='cms-plugin-events-content') 85 | ) 86 | 87 | # Put the data inside a container like any other CMS placeholder data. 88 | data['containers']['content'] = content_container 89 | 90 | return data 91 | 92 | def get_absolute_url(self): 93 | # Return the URL of your detail view. 94 | return reverse('event_detail', kwargs={'pk': self.pk}) 95 | 96 | def get_api_detail_url(self): 97 | # Return the API URL of your detail view. 98 | return reverse('event_detail_api', kwargs={'pk': self.pk}) 99 | 100 | def get_detail_view_component(self): 101 | # Return the name of your vue component. 102 | return 'cmp-event-detail' 103 | 104 | def get_detail_path_pattern(self): 105 | # Return the path pattern of your named vue route. 106 | return 'events/:pk' 107 | 108 | def get_url_params(self): 109 | # Return the params that are needed to access your named vue route. 110 | return { 111 | 'pk': self.pk 112 | } 113 | 114 | 115 | All of your views need to be attached to the menu, even if they are not actually rendered in your site navigation. 116 | If the CMS page holding your apphook uses a custom view, you need this configuration: 117 | 118 | .. code-block:: python 119 | 120 | DJANGOCMS_SPA_VUE_JS_APPHOOKS_WITH_ROOT_URL = [''] 121 | 122 | 123 | Your ``cms_menus.py`` might looks like this: 124 | 125 | .. code-block:: python 126 | 127 | class EventMenu(CMSAttachMenu): 128 | name = _('Events') 129 | 130 | def get_nodes(self, request): 131 | nodes = [] 132 | counter = 1 133 | is_draft = self.instance.publisher_is_draft 134 | is_edit = hasattr(request, 'toolbar') and request.user.is_staff and request.toolbar.edit_mode 135 | 136 | # We don't want to parse the instance in live and draft mode. Depending on the request user we return the 137 | # corresponding version. 138 | if (not is_edit and not is_draft) or (is_edit and is_draft): 139 | # Let's add the list view 140 | nodes.append( 141 | NavigationNode( 142 | title='Event List', 143 | url=reverse('event_list'), 144 | id=1, 145 | attr={ 146 | 'component': 'cmp-event-list', 147 | 'vue_js_router_name': 'event-list', 148 | 'fetch_url': reverse('event_list_api'), 149 | 'absolute_url': reverse('event_list'), 150 | 'named_route_path_pattern': ':pk', # Used to group routes (dynamic route matching) 151 | 'login_required': True # Hide a navigation node for unauthorized users 152 | } 153 | ) 154 | ) 155 | counter += 1 156 | 157 | for event in Event.objects.all(): 158 | nodes.append( 159 | NavigationNode( 160 | title=event.name, 161 | url=event.get_absolute_url(), 162 | id=counter, 163 | attr=event.get_cms_menu_node_attributes(), 164 | parent_id=1 165 | ) 166 | ) 167 | counter += 1 168 | 169 | return nodes 170 | 171 | menu_pool.register_menu(EventMenu) 172 | 173 | 174 | This is an example of a simple template view. Each view that you have needs an API view that returns the JSON data only. 175 | 176 | .. code-block:: python 177 | 178 | from djangocms_spa.views import SpaApiView 179 | from djangocms_spa_vue_js.views import VueRouterView 180 | 181 | class ContentMixin(object): 182 | template_name = 'index.html' 183 | 184 | def get_fetched_data(self): 185 | data = { 186 | 'containers': { 187 | 'content': { 188 | 'type': 'generic', 189 | 'content': { 190 | 'key': 'value' 191 | } 192 | } 193 | } 194 | } 195 | return data 196 | 197 | 198 | class MyTemplateView(ContentMixin, VueRouterView): 199 | fetch_url = reverse_lazy('content_api') # URL of the API view. 200 | 201 | 202 | class MyTemplateApiView(ContentMixin, SpaApiView): 203 | pass 204 | 205 | 206 | Your list view looks like this: 207 | 208 | .. code-block:: python 209 | 210 | from djangocms_spa.views import SpaListApiView 211 | from djangocms_spa_vue_js.views import VueRouterListView 212 | 213 | class EventListView(VueRouterListView): 214 | fetch_url = reverse_lazy('event_list_api') 215 | model = Event 216 | template_name = 'event_list.html' 217 | 218 | 219 | class EventListAPIView(SpaListApiView): 220 | model = Event 221 | template_name = 'event_list.html' 222 | 223 | 224 | Your detail view looks like this: 225 | 226 | .. code-block:: python 227 | 228 | from djangocms_spa.views import SpaDetailApiView 229 | from djangocms_spa_vue_js.views import VueRouterDetailView 230 | 231 | class EventDetailView(VueRouterDetailView): 232 | model = Event 233 | template_name = 'event_detail.html' 234 | 235 | def get_fetch_url(self): 236 | return reverse('event_detail_api', kwargs={'pk': self.object.pk}) 237 | 238 | 239 | class EventDetailAPIView(SpaDetailApiView): 240 | model = Event 241 | template_name = 'event_detail.html' 242 | 243 | 244 | The router object 245 | ----------------- 246 | 247 | The server needs to prepare the routes for the frontend. The easiest way to do this is by iterating over the CMS 248 | menu. In order to bring all available routes to the menu, you have to register all your custom URLs as a menu too. 249 | A template tag renders a JS object like this. 250 | 251 | .. code-block:: json 252 | 253 | { 254 | "routes": [ 255 | { 256 | "api": { 257 | "fetch": "/api/pages/", 258 | "query": { 259 | "partials": ["menu", "footer"] 260 | } 261 | }, 262 | "component": "index", 263 | "name": "cms-page-1", 264 | "path": "/" 265 | }, 266 | { 267 | "api": { 268 | "fetched": { 269 | "partials": { 270 | "menu": { 271 | "type": "generic", 272 | "content": { 273 | "menu": [ 274 | { 275 | "path": "/", 276 | "label": "Home", 277 | "children": [ 278 | { 279 | "path": "/about", 280 | "label": "About", 281 | "children": [ 282 | { 283 | "path": "/contact", 284 | "label": "Contact" 285 | } 286 | ] 287 | } 288 | ] 289 | } 290 | ] 291 | } 292 | }, 293 | "footer": { 294 | "type": "cmp-footer", 295 | "plugins": [ 296 | { 297 | "type": "cmp-footer-text", 298 | "position": 0, 299 | "content": { 300 | "text": "Lorem ipsum dolor sit amet, nam et modus tollit." 301 | } 302 | } 303 | ] 304 | } 305 | }, 306 | "data": { 307 | "meta": { 308 | "description": "", 309 | "title": "Content-Plugins" 310 | }, 311 | "containers": { 312 | "main": { 313 | "type": "cmp-main", 314 | "plugins": [ 315 | { 316 | "type": "cmp-text", 317 | "position": 0, 318 | "content": { 319 | "text": "Ex vim saperet habemus, et eum impetus mentitum, cum purto dolores similique ei." 320 | } 321 | } 322 | ] 323 | } 324 | }, 325 | "title": "About" 326 | } 327 | }, 328 | "query": { 329 | "partials": ["menu", "footer"] 330 | } 331 | }, 332 | "component": "content-with-section-navigation", 333 | "name": "cms-page-2", 334 | "path": "/about" 335 | }, 336 | { 337 | "api": { 338 | "fetch": "/api/pages/about/contact", 339 | "query": { 340 | "partials": ["menu", "meta", "footer"] 341 | } 342 | }, 343 | "component": "content-with-section-navigation", 344 | "name": "cms-page-3", 345 | "path": "/about/contact" 346 | } 347 | ] 348 | } 349 | 350 | 351 | Debugging 352 | --------- 353 | 354 | If you need to debug the router object, this middleware is probably pretty helpful: 355 | 356 | .. code-block:: python 357 | 358 | MIDDLEWARE += ( 359 | 'djangocms_spa_vue_js.middleware.RouterDebuggingMiddleware', 360 | ) 361 | 362 | 363 | Credits 364 | ------- 365 | 366 | Tools used in rendering this package: 367 | 368 | * Cookiecutter_ 369 | * `cookiecutter-djangopackage`_ 370 | 371 | .. _Cookiecutter: https://github.com/audreyr/cookiecutter 372 | .. _`cookiecutter-djangopackage`: https://github.com/pydanny/cookiecutter-djangopackage 373 | -------------------------------------------------------------------------------- /djangocms_spa_vue_js/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.1.29' 2 | -------------------------------------------------------------------------------- /djangocms_spa_vue_js/cms_menus.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from cms.models import Page 4 | from django.utils.text import slugify 5 | from menus.base import Modifier 6 | from menus.menu_pool import menu_pool 7 | 8 | from djangocms_spa_vue_js.menu_helpers import get_node_route 9 | 10 | 11 | @dataclass 12 | class RouterCMSPage: 13 | pk: int 14 | template: str 15 | reverse_id: str 16 | application_urls: str 17 | title_path: str 18 | title_slug: str 19 | 20 | 21 | class VueJsMenuModifier(Modifier): 22 | """ 23 | This menu modifier extends the nodes with data that is needed by the Vue JS route object and by the frontend to 24 | render all contents. Make sure all your custom models are attached to the CMS menu. 25 | 26 | Expected menu structure: 27 | - Home 28 | - Page A 29 | - Page B 30 | - Page B1 31 | - Page C 32 | - News list (App hook) 33 | - News detail A 34 | - News detail B 35 | """ 36 | 37 | def modify(self, request, nodes, namespace, root_id, post_cut, breadcrumb): 38 | # If the menu is not yet cut, don't do anything. 39 | if post_cut: 40 | return nodes 41 | 42 | # Prevent parsing all this again when rendering the menu in a second step. 43 | if hasattr(self.renderer, 'vue_js_structure_started'): 44 | return nodes 45 | else: 46 | self.renderer.vue_js_structure_started = True 47 | 48 | router_nodes = [] 49 | named_route_path_patterns = {} 50 | 51 | page_nodes = {n.id: n for n in nodes if n.attr['is_page']} 52 | pages = Page.objects.filter(id__in=page_nodes.keys()).prefetch_related('title_set') 53 | router_pages = { 54 | page.pk: RouterCMSPage( 55 | pk=page.pk, 56 | template=page.get_template(), 57 | reverse_id=page.reverse_id, 58 | application_urls=page.application_urls, 59 | title_path=page.title_set.first().path, 60 | title_slug=page.title_set.first().slug 61 | ) for page in pages 62 | } 63 | 64 | for node in nodes: 65 | if node.attr.get('login_required') and not request.user.is_authenticated: 66 | continue 67 | 68 | if node.attr.get('is_page'): 69 | node.attr['router_page'] = router_pages.get(node.id) 70 | 71 | node_route = get_node_route(request=request, node=node, renderer=self.renderer) 72 | 73 | named_route_path_pattern = node.attr.get('named_route_path_pattern') 74 | if named_route_path_pattern: 75 | named_route_path = node.attr.get('named_route_path') 76 | if named_route_path: 77 | path = named_route_path 78 | else: 79 | # Override the path with the pattern (e.g. 'parent/foo' to 'parent/:my_path_pattern') 80 | path = '{parent_url}{path_pattern}/'.format(parent_url=node.parent.get_absolute_url(), 81 | path_pattern=named_route_path_pattern) 82 | node_route['path'] = path 83 | node_route['name'] = slugify(path) # Use the same name for all nodes of this route. 84 | 85 | if named_route_path_pattern not in named_route_path_patterns.keys(): 86 | # Store the index of this route in a dict of patterns. We need this to be able to override the 87 | # named route with the selected node (see the next condition). 88 | named_route_path_patterns[named_route_path_pattern] = len(router_nodes) 89 | elif node.selected: 90 | # Update the router config with the fetched data of the selected node. 91 | index_of_first_named_route = named_route_path_patterns[named_route_path_pattern] 92 | node.attr['vue_js_route'] = node_route 93 | router_nodes[index_of_first_named_route] = node 94 | continue # Skip this iteration, we don't need to add a named route twice. 95 | else: 96 | continue # Ignore named routes for path patterns that have already been processed. 97 | 98 | node.attr['vue_js_route'] = node_route 99 | router_nodes.append(node) 100 | 101 | return router_nodes 102 | 103 | 104 | menu_pool.register_modifier(VueJsMenuModifier) 105 | -------------------------------------------------------------------------------- /djangocms_spa_vue_js/menu_helpers.py: -------------------------------------------------------------------------------- 1 | from cms.models import Page 2 | from django.conf import settings 3 | from django.urls import Resolver404, resolve, reverse 4 | from django.utils.encoding import force_str 5 | from djangocms_spa.content_helpers import (get_frontend_data_dict_for_cms_page, get_frontend_data_dict_for_partials, 6 | get_partial_names_for_template) 7 | from djangocms_spa.utils import get_frontend_component_name_by_template, get_view_from_url 8 | from menus.menu_pool import menu_pool 9 | 10 | from .router_helpers import get_vue_js_router_name_for_cms_page 11 | 12 | 13 | def get_vue_js_router(context=None, request=None): 14 | """ 15 | Returns a list of all routes (CMS pages, projects, team members, etc.) that are in the menu of django CMS. The list 16 | contains a dict structure that is used by the Vue JS router. 17 | """ 18 | vue_routes = [] 19 | menu_renderer = get_menu_renderer(context=context, request=request) 20 | 21 | # For the usage of template tags inside our menu modifier we need to make it available on our menu_renderer. 22 | # The `set_context` method is a monkey patch and no part of the original class. 23 | menu_renderer.set_context(context) 24 | 25 | menu_nodes = menu_renderer.get_nodes() 26 | for node in menu_nodes: 27 | if node.attr.get('vue_js_route'): 28 | vue_routes.append(node.attr.get('vue_js_route')) 29 | 30 | return {'routes': vue_routes} 31 | 32 | 33 | def get_menu_renderer(context=None, request=None): 34 | menu_renderer = None 35 | 36 | if context: 37 | menu_renderer = context.get('cms_menu_renderer') 38 | 39 | if not menu_renderer: 40 | menu_renderer = menu_pool.get_renderer(request) 41 | 42 | return menu_renderer 43 | 44 | 45 | def get_node_template_name(node): 46 | try: 47 | view = get_view_from_url(node.get_absolute_url()) 48 | except (AttributeError, Resolver404): 49 | return settings.DJANGOCMS_SPA_VUE_JS_ERROR_404_TEMPLATE 50 | if view.__module__ == 'cms.views': 51 | template = node.attr.get('template') 52 | if template: 53 | return template 54 | else: 55 | try: 56 | return node.attr.get('router_page').template 57 | except: 58 | return settings.DJANGOCMS_SPA_VUE_JS_ERROR_404_TEMPLATE 59 | else: 60 | try: 61 | return view.template_name 62 | except AttributeError: 63 | return settings.DJANGOCMS_SPA_DEFAULT_TEMPLATE 64 | 65 | 66 | def get_node_route(request, node, renderer, template=''): 67 | # Initial data of the route. 68 | route_data = { 69 | 'api': {}, 70 | } 71 | 72 | if node.attr.get('is_page'): 73 | route = get_node_route_for_cms_page(request, node, route_data, node.attr.get('router_page')) 74 | else: 75 | route = get_node_route_for_app_model(request, node, route_data) 76 | 77 | if not node.attr.get('use_cache', True): 78 | route['api']['fetch']['useCache'] = False 79 | 80 | if node.selected and node.get_absolute_url() == request.path: 81 | if not template: 82 | template = get_node_template_name(node) 83 | 84 | # Static CMS placeholders and other global page elements (e.g. menu) go into the `partials` dict. 85 | partial_names = get_partial_names_for_template(template=template) 86 | route['api']['fetched']['response']['partials'] = get_frontend_data_dict_for_partials( 87 | partials=partial_names, 88 | request=request, 89 | editable=request.user.has_perm('cms.edit_static_placeholder'), 90 | renderer=renderer, 91 | ) 92 | 93 | # Add query params 94 | template_path = get_node_template_name(node) 95 | try: 96 | partials = settings.DJANGOCMS_SPA_TEMPLATES[template_path]['partials'] 97 | except KeyError: 98 | partials = [] 99 | if partials: 100 | route_data['api']['fetch'].setdefault('query', {}).update({'partials': partials}) 101 | 102 | if node.attr.get('redirect_url'): 103 | del route_data['api'] 104 | 105 | return route 106 | 107 | 108 | def get_node_route_for_cms_page(request, node, route_data, router_page): 109 | # Set name and component of the route. 110 | route_data['name'] = get_vue_js_router_name_for_cms_page(router_page.pk) 111 | if not node.attr.get('redirect_url'): 112 | try: 113 | component = get_frontend_component_name_by_template(router_page.template) 114 | except KeyError: 115 | component = settings.DJANGOCMS_SPA_TEMPLATES[settings.DJANGOCMS_SPA_DEFAULT_TEMPLATE][ 116 | 'frontend_component_name'] 117 | route_data['component'] = component 118 | 119 | # Add the link to fetch the data from the API. 120 | if router_page.application_urls not in settings.DJANGOCMS_SPA_VUE_JS_APPHOOKS_WITH_ROOT_URL: 121 | if not router_page.title_path: # The home page does not have a path 122 | if hasattr(settings, 'DJANGOCMS_SPA_USE_SERIALIZERS') and settings.DJANGOCMS_SPA_USE_SERIALIZERS: 123 | fetch_url = reverse('api:cms_page_detail', kwargs={'path': settings.DJANGOCMS_SPA_HOME_PATH}) 124 | else: 125 | fetch_url = reverse('api:cms_page_detail_home') 126 | elif node.attr.get('named_route_path_pattern'): 127 | # Get the fetch_url of the parent node through the path of the parent node 128 | parent_node_path = router_page.title_path.replace('/%s' % router_page.title_slug, '') 129 | fetch_url_of_parent_node = reverse('api:cms_page_detail', kwargs={'path': parent_node_path}) 130 | fetch_url = '{parent_url}{path_pattern}/'.format(parent_url=fetch_url_of_parent_node, 131 | path_pattern=node.attr.get('named_route_path_pattern')) 132 | else: 133 | fetch_url = reverse('api:cms_page_detail', kwargs={'path': router_page.title_path}) 134 | 135 | # Add redirect url if available. 136 | if node.attr.get('redirect_url'): 137 | route_data['redirect'] = node.attr['redirect_url'] 138 | 139 | else: 140 | # Apphooks use a view that has a custom API URL to fetch data from. 141 | view = get_view_from_url(node.get_absolute_url()) 142 | fetch_url = force_str(view().get_fetch_url()) 143 | 144 | route_data['api']['fetch'] = { 145 | 'url': fetch_url, 146 | } 147 | 148 | if router_page.reverse_id: 149 | route_data['meta'] = { 150 | 'id': router_page.reverse_id 151 | } 152 | 153 | # Add initial data for the selected page. 154 | if node.selected and node.get_absolute_url() == request.path: 155 | cms_page = Page.objects.get(pk=router_page.pk) 156 | if hasattr(settings, 'DJANGOCMS_SPA_USE_SERIALIZERS') and settings.DJANGOCMS_SPA_USE_SERIALIZERS: 157 | from djangocms_spa.serializers import PageSerializer 158 | data = PageSerializer(instance=cms_page).data 159 | else: 160 | data = get_frontend_data_dict_for_cms_page( 161 | cms_page=cms_page, 162 | cms_page_title=cms_page.title_set.get(language=request.LANGUAGE_CODE), 163 | request=request, 164 | editable=request.user.has_perm('cms.change_page') 165 | ) 166 | 167 | fetched_data = { 168 | 'response': { 169 | 'data': data 170 | } 171 | } 172 | if node.attr.get('named_route_path_pattern'): 173 | url_param = node.attr['named_route_path_pattern'].replace(':', '') 174 | if url_param: 175 | fetched_data.update({ 176 | 'params': { 177 | url_param: router_page.title_slug 178 | } 179 | }) 180 | route_data['api']['fetched'] = fetched_data 181 | 182 | if settings.DJANGOCMS_SPA_VUE_JS_USE_I18N_PATTERNS: 183 | route_data['path'] = '/%s/%s' % (request.LANGUAGE_CODE, router_page.title_path) 184 | else: 185 | route_data['path'] = '/%s' % router_page.title_path 186 | 187 | return route_data 188 | 189 | 190 | def get_node_route_for_app_model(request, node, route_data): 191 | # Set name and component of the route. 192 | route_data['component'] = node.attr.get('component') 193 | route_data['name'] = node.attr.get('vue_js_router_name') 194 | 195 | # Add the link to fetch the data from the API. 196 | route_data['api']['fetch'] = { 197 | 'url': node.attr.get('fetch_url'), 198 | } 199 | 200 | try: 201 | request_url_name = resolve(request.path).url_name 202 | node_url_name = resolve(node.get_absolute_url()).url_name 203 | except Resolver404: 204 | resolver_match = False 205 | else: 206 | resolver_match = request_url_name == node_url_name 207 | 208 | is_selected_node = request.path == node.get_absolute_url() or resolver_match 209 | if is_selected_node: 210 | # We need to prepare the initial structure of the fetched data. The actual data is added by the view. 211 | route_data['api']['fetched'] = { 212 | 'response': { 213 | 'data': {} 214 | } 215 | } 216 | route_data['params'] = node.attr.get('url_params', {}) 217 | 218 | meta_id = node.attr.get('id') 219 | if meta_id: 220 | route_data['meta'] = { 221 | 'id': meta_id 222 | } 223 | 224 | route_data['path'] = node.get_absolute_url() 225 | return route_data 226 | -------------------------------------------------------------------------------- /djangocms_spa_vue_js/middleware.py: -------------------------------------------------------------------------------- 1 | from django.http import JsonResponse 2 | 3 | from djangocms_spa_vue_js.menu_helpers import get_vue_js_router 4 | 5 | 6 | class RouterDebuggingMiddleware(object): 7 | def __init__(self, get_response): 8 | self.get_response = get_response 9 | 10 | def __call__(self, request): 11 | if request.user.is_authenticated(): 12 | return self.get_response(request) 13 | else: 14 | vue_js_router = get_vue_js_router(request=request) 15 | return JsonResponse(vue_js_router) 16 | -------------------------------------------------------------------------------- /djangocms_spa_vue_js/models.py: -------------------------------------------------------------------------------- 1 | from appconf import AppConf 2 | from django.views.defaults import ERROR_404_TEMPLATE_NAME 3 | from djangocms_spa.content_helpers import get_frontend_data_dict_for_placeholders, get_global_placeholder_data 4 | from djangocms_spa.models import DjangoCmsMixin 5 | 6 | 7 | class DjangoCmsSPAVueJSConf(AppConf): 8 | ERROR_404_TEMPLATE = ERROR_404_TEMPLATE_NAME 9 | APPHOOKS_WITH_ROOT_URL = [] # list of apphooks that use a custom view on the root url (e.g. "/en//") 10 | USE_I18N_PATTERNS = False 11 | 12 | 13 | class DjangocmsVueJsMixin(DjangoCmsMixin): 14 | """ 15 | This mixin prepares the data of a model to be ready for the frontend. 16 | """ 17 | vue_js_router_component = 'topic-detail' 18 | 19 | class Meta: 20 | abstract = True 21 | 22 | @property 23 | def vue_js_router_name(self): 24 | return '%s-%s' % (self._meta.app_label, self._meta.model_name) 25 | 26 | def get_frontend_list_data_dict(self, request, editable=False, placeholder_name=''): 27 | data = {} 28 | 29 | if editable: 30 | data.update(self.get_cms_placeholder_json(request=request, placeholder_name=placeholder_name)) 31 | 32 | data.update({ 33 | 'content': { 34 | 'id': self.pk, 35 | 'link': self.get_vue_js_link_dict(), 36 | } 37 | }) 38 | return data 39 | 40 | def get_frontend_detail_data_dict(self, request, editable=False): 41 | data = {} 42 | 43 | # Add all placeholder fields. 44 | placeholder_field_names = self.get_placeholder_field_names() 45 | placeholders = [getattr(self, placeholder_field_name) for placeholder_field_name in placeholder_field_names] 46 | placeholder_frontend_data_dict = get_frontend_data_dict_for_placeholders( 47 | placeholders=placeholders, 48 | request=request, 49 | editable=editable 50 | ) 51 | global_placeholder_data_dict = get_global_placeholder_data(placeholder_frontend_data_dict) 52 | data['containers'] = placeholder_frontend_data_dict 53 | 54 | if global_placeholder_data_dict: 55 | data['global_placeholder_data'] = global_placeholder_data_dict 56 | 57 | return data 58 | 59 | def get_vue_js_link_dict(self): 60 | return { 61 | 'name': self.vue_js_router_name, 62 | 'fetch': self.get_api_detail_url() 63 | } 64 | 65 | def get_cms_menu_node_attributes(self): 66 | return { 67 | 'component': self.get_detail_view_component(), 68 | 'vue_js_router_name': self.vue_js_router_name, 69 | 'absolute_url': self.get_absolute_url(), 70 | 'fetch_url': self.get_api_detail_url(), 71 | 'named_route_path_pattern': self.get_detail_path_pattern(), 72 | 'url_params': self.get_url_params(), 73 | } 74 | 75 | def get_absolute_url(self): 76 | # Override this method in your model. 77 | return '' 78 | 79 | def get_api_detail_url(self): 80 | # Override this method in your model. 81 | return '' 82 | 83 | def get_detail_view_component(self): 84 | # Override this method in your model. 85 | return '' 86 | 87 | def get_detail_path_pattern(self): 88 | # Used to group routes (dynamic route matching). Override this method in your model. 89 | return ':slug' 90 | 91 | def get_url_params(self): 92 | # Override this method in your model. 93 | return { 94 | 'slug': '' 95 | } 96 | -------------------------------------------------------------------------------- /djangocms_spa_vue_js/router_helpers.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | 3 | 4 | def get_vue_js_link_dict(cms_page=None, instance=None, external_link=None): 5 | if cms_page: 6 | try: 7 | slug = cms_page.title_set.first().slug 8 | return { 9 | 'fetch': reverse('api:cms_page_detail', kwargs={'slug': slug}), 10 | 'name': get_vue_js_router_name_for_cms_page(slug) 11 | } 12 | except: 13 | return {} 14 | elif instance: 15 | return instance.get_vue_js_link_dict() 16 | elif external_link: 17 | return { 18 | 'fetch': external_link 19 | } 20 | else: 21 | return {} 22 | 23 | 24 | def get_vue_js_router_name_for_cms_page(pk): 25 | return 'cms-page-%d' % pk 26 | -------------------------------------------------------------------------------- /djangocms_spa_vue_js/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreipol/djangocms-spa-vue-js/b85492c280a54ff2922a0e662d61e46f7c66b276/djangocms_spa_vue_js/templatetags/__init__.py -------------------------------------------------------------------------------- /djangocms_spa_vue_js/templatetags/router_tags.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django import template 4 | from django.conf import settings 5 | from django.utils.safestring import mark_safe 6 | 7 | from ..menu_helpers import get_vue_js_router 8 | 9 | register = template.Library() 10 | 11 | 12 | @register.simple_tag(takes_context=True) 13 | def vue_js_router(context): 14 | if 'vue_js_router' in context: 15 | router = context['vue_js_router'] 16 | else: 17 | router = get_vue_js_router(context=context) 18 | 19 | router_json = json.dumps(router, cls=settings.DJANGOCMS_SPA_JSON_ENCODER) 20 | return mark_safe(router_json) 21 | -------------------------------------------------------------------------------- /djangocms_spa_vue_js/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.exceptions import ImproperlyConfigured 3 | from django.views.generic import TemplateView 4 | from djangocms_spa.content_helpers import get_frontend_data_dict_for_partials, get_partial_names_for_template 5 | from djangocms_spa.decorators import cache_view 6 | from djangocms_spa.views import MultipleObjectSpaMixin, SingleObjectSpaMixin 7 | 8 | from .menu_helpers import get_vue_js_router 9 | 10 | 11 | class VueRouterView(TemplateView): 12 | fetch_url = None 13 | add_language_code = len(settings.LANGUAGES) > 1 14 | 15 | @cache_view 16 | def dispatch(self, request, **kwargs): 17 | return super(VueRouterView, self).dispatch(request, **kwargs) 18 | 19 | def get_cache_key(self): 20 | return None 21 | 22 | def get_context_data(self, **kwargs): 23 | return { 24 | 'vue_js_router': self.get_vue_js_router_including_fetched_data() 25 | } 26 | 27 | def get_vue_js_router_including_fetched_data(self): 28 | vue_js_router = self.get_vue_js_router(request=self.request) 29 | 30 | # Put the context data of this view into the active route. 31 | active_route = self.get_active_route(vue_js_router['routes']) 32 | if active_route: 33 | active_route['api']['fetched']['response']['data'].update(self.get_fetched_data()) 34 | partial_names = get_partial_names_for_template(template=self.template_name) 35 | active_route['api']['fetched']['response']['partials'] = self.get_view_partials(partial_names=partial_names) 36 | 37 | url_params_for_active_route = self.get_url_params_for_active_route() 38 | if url_params_for_active_route: 39 | active_route['api']['fetched']['params'] = url_params_for_active_route 40 | 41 | return vue_js_router 42 | 43 | def get_vue_js_router(self, request): 44 | return get_vue_js_router(request=request) 45 | 46 | def get_fetched_data(self): 47 | # Override this method if you need further context data. 48 | return {} 49 | 50 | def get_view_partials(self, partial_names): 51 | return get_frontend_data_dict_for_partials( 52 | partials=partial_names, 53 | request=self.request, 54 | editable=self.request.user.has_perm('cms.edit_static_placeholder'), 55 | ) 56 | 57 | def get_active_route(self, routes): 58 | for route in routes: 59 | is_active_route = 'api' in route and 'fetched' in route['api'] 60 | if is_active_route: 61 | return route 62 | 63 | return None 64 | 65 | def get_fetch_url(self): 66 | if self.fetch_url: 67 | return self.fetch_url 68 | else: 69 | raise ImproperlyConfigured('No fetch URL to get the data. Provide a fetch_url.') 70 | 71 | def get_url_params_for_active_route(self): 72 | return {} 73 | 74 | 75 | class VueRouterListView(MultipleObjectSpaMixin, VueRouterView): 76 | pass 77 | 78 | 79 | class VueRouterDetailView(SingleObjectSpaMixin, VueRouterView): 80 | pass 81 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import unicode_literals, absolute_import 4 | 5 | import os 6 | import sys 7 | 8 | if __name__ == "__main__": 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") 10 | from django.core.management import execute_from_command_line 11 | 12 | execute_from_command_line(sys.argv) 13 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django>=1.8 2 | django-appconf>=1.0.1 3 | django-cms>=3.0 4 | djangocms-spa>=0.1.16 5 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | bumpversion==0.5.3 2 | wheel==0.29.0 3 | 4 | -------------------------------------------------------------------------------- /requirements_test.txt: -------------------------------------------------------------------------------- 1 | coverage==4.3.3 2 | mock>=1.0.1 3 | flake8>=2.1.0 4 | tox>=1.7.0 5 | codecov>=2.0.0 6 | 7 | 8 | # Additional test requirements go here 9 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 3 | from __future__ import unicode_literals, absolute_import 4 | 5 | import os 6 | import sys 7 | 8 | import django 9 | from django.conf import settings 10 | from django.test.utils import get_runner 11 | 12 | 13 | def run_tests(*test_args): 14 | if not test_args: 15 | test_args = ['tests'] 16 | 17 | os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' 18 | django.setup() 19 | TestRunner = get_runner(settings) 20 | test_runner = TestRunner() 21 | failures = test_runner.run_tests(test_args) 22 | sys.exit(bool(failures)) 23 | 24 | 25 | if __name__ == '__main__': 26 | run_tests(*sys.argv[1:]) 27 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.1.29 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | 8 | [bumpversion:file:djangocms_spa_vue_js/__init__.py] 9 | 10 | [wheel] 11 | universal = 1 12 | 13 | [flake8] 14 | ignore = D203 15 | exclude = 16 | .git, 17 | .tox 18 | docs/source/conf.py, 19 | build, 20 | dist 21 | max-line-length = 119 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import re 5 | import sys 6 | 7 | try: 8 | from setuptools import setup 9 | except ImportError: 10 | from distutils.core import setup 11 | 12 | 13 | def get_version(*file_paths): 14 | """Retrieves the version from djangocms_spa_vue_js/__init__.py""" 15 | filename = os.path.join(os.path.dirname(__file__), *file_paths) 16 | version_file = open(filename).read() 17 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", 18 | version_file, re.M) 19 | if version_match: 20 | return version_match.group(1) 21 | raise RuntimeError('Unable to find version string.') 22 | 23 | 24 | version = get_version("djangocms_spa_vue_js", "__init__.py") 25 | 26 | 27 | if sys.argv[-1] == 'publish': 28 | try: 29 | import wheel 30 | print("Wheel version: ", wheel.__version__) 31 | except ImportError: 32 | print('Wheel library missing. Please run "pip install wheel"') 33 | sys.exit() 34 | os.system('python setup.py sdist upload') 35 | os.system('python setup.py bdist_wheel upload') 36 | sys.exit() 37 | 38 | if sys.argv[-1] == 'tag': 39 | print("Tagging the version on git:") 40 | os.system("git tag -a %s -m 'version %s'" % (version, version)) 41 | os.system("git push --tags") 42 | sys.exit() 43 | 44 | readme = open('README.rst').read() 45 | history = open('HISTORY.rst').read().replace('.. :changelog:', '') 46 | 47 | setup( 48 | name='djangocms-spa-vue-js', 49 | version=version, 50 | description="""This package prepares your django CMS and vue.js project to create a single-page application (SPA).""", 51 | long_description=readme + '\n\n' + history, 52 | author='dreipol GmbH', 53 | author_email='dev@dreipol.ch', 54 | url='https://github.com/dreipol/djangocms-spa-vue-js', 55 | packages=[ 56 | 'djangocms_spa_vue_js', 57 | ], 58 | include_package_data=True, 59 | install_requires=[ 60 | 'djangocms-spa' 61 | ], 62 | license="MIT", 63 | zip_safe=False, 64 | keywords='djangocms-spa-vue-js', 65 | classifiers=[ 66 | 'Development Status :: 3 - Alpha', 67 | 'Framework :: Django', 68 | 'Framework :: Django :: 2.2', 69 | 'Intended Audience :: Developers', 70 | 'License :: OSI Approved :: BSD License', 71 | 'Natural Language :: English', 72 | 'Programming Language :: Python :: 3', 73 | 'Programming Language :: Python :: 3.7', 74 | 'Programming Language :: Python :: 3.8', 75 | 'Programming Language :: Python :: 3.9', 76 | 'Programming Language :: Python :: 3.10', 77 | 'Programming Language :: Python :: 3.11', 78 | ], 79 | ) 80 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | {py27,py33,py34,py35}-django-18 4 | {py27,py34,py35}-django-19 5 | {py27,py34,py35}-django-110 6 | 7 | [testenv] 8 | setenv = 9 | PYTHONPATH = {toxinidir}:{toxinidir}/djangocms_spa_vue_js 10 | commands = coverage run --source djangocms_spa_vue_js runtests.py 11 | deps = 12 | django-18: Django>=1.8,<1.9 13 | django-19: Django>=1.9,<1.10 14 | django-110: Django>=1.10 15 | -r{toxinidir}/requirements_test.txt 16 | basepython = 17 | py35: python3.5 18 | py34: python3.4 19 | py33: python3.3 20 | py27: python2.7 21 | --------------------------------------------------------------------------------