From 7d993c5db14e5bc86faf85c77a13b84ca3d23496 Mon Sep 17 00:00:00 2001 From: ilikerobots Date: Thu, 16 Jul 2020 13:00:38 +0300 Subject: [PATCH] Add Vue project generation options --- LICENSE | 2 +- README.rst | 327 +++--------------- cookiecutter.json | 7 +- hooks/post_gen_project.py | 59 ++++ hooks/pre_gen_project.py | 11 + tests/test_cookiecutter_generation.py | 5 + {{cookiecutter.project_slug}}/.gitignore | 6 + .../.idea/runConfigurations/vue_build.xml | 12 + .../.idea/runConfigurations/vue_serve.xml | 12 + {{cookiecutter.project_slug}}/README.rst | 23 ++ .../config/api_router.py | 10 +- .../config/settings/base.py | 26 ++ {{cookiecutter.project_slug}}/config/urls.py | 4 + .../fruit/__init__.py | 0 {{cookiecutter.project_slug}}/fruit/admin.py | 8 + .../fruit/api/__init__.py | 0 .../fruit/api/serializers.py | 14 + .../fruit/api/views.py | 20 ++ {{cookiecutter.project_slug}}/fruit/apps.py | 5 + .../fruit/migrations/0001_initial.py | 24 ++ .../migrations/0002_add_initial_fruits.py | 39 +++ .../fruit/migrations/__init__.py | 0 {{cookiecutter.project_slug}}/fruit/models.py | 8 + .../fruit/templates/fruit/counter.html | 23 ++ .../fruit/templates/fruit/fruit_list.html | 36 ++ {{cookiecutter.project_slug}}/fruit/tests.py | 1 + {{cookiecutter.project_slug}}/fruit/urls.py | 25 ++ {{cookiecutter.project_slug}}/fruit/views.py | 6 + .../requirements/base.txt | 3 + .../vue_frontend/.browserslistrc | 3 + .../vue_frontend/.env | 3 + .../vue_frontend/.eslintrc.js | 17 + .../vue_frontend/.gitignore | 23 ++ .../vue_frontend/README.md | 24 ++ .../vue_frontend/babel.config.js | 5 + .../vue_frontend/package.json | 31 ++ .../src/fruit/components/Counter.vue | 56 +++ .../src/fruit/components/CounterAdjustor.vue | 55 +++ .../src/fruit/components/CounterBanner.vue | 34 ++ .../src/fruit/components/CounterDisplay.vue | 43 +++ .../src/fruit/components/FruitInspector.vue | 106 ++++++ .../src/fruit/entry/fruit_counter.js | 34 ++ .../src/fruit/entry/fruit_list.js | 30 ++ .../src/fruit/store/module_fruit.js | 77 +++++ .../vue_frontend/src/rest/rest.js | 23 ++ .../src/store/vuex_store_as_plugin.js | 17 + .../assets/logo.png | Bin 0 -> 6849 bytes .../components/HelloWorld.vue | 116 +++++++ .../entry/main.js | 10 + .../vue_frontend/vue.config.js | 88 +++++ .../static/images/django_logo.png | Bin 0 -> 8906 bytes .../static/js/project.js | 18 + .../static/sass/custom_bootstrap_vars.scss | 4 + .../templates/base.html | 8 +- .../templates/pages/home.html | 15 +- .../webpack_bundle/lazy_render_bundle.html | 10 + .../webpack_bundle/__init__.py | 0 .../webpack_bundle/apps.py | 7 + .../webpack_bundle/templatetags/__init__.py | 0 .../templatetags/webpack_bundle.py | 10 + 60 files changed, 1301 insertions(+), 282 deletions(-) create mode 100644 {{cookiecutter.project_slug}}/.idea/runConfigurations/vue_build.xml create mode 100644 {{cookiecutter.project_slug}}/.idea/runConfigurations/vue_serve.xml create mode 100644 {{cookiecutter.project_slug}}/fruit/__init__.py create mode 100644 {{cookiecutter.project_slug}}/fruit/admin.py create mode 100644 {{cookiecutter.project_slug}}/fruit/api/__init__.py create mode 100644 {{cookiecutter.project_slug}}/fruit/api/serializers.py create mode 100644 {{cookiecutter.project_slug}}/fruit/api/views.py create mode 100644 {{cookiecutter.project_slug}}/fruit/apps.py create mode 100644 {{cookiecutter.project_slug}}/fruit/migrations/0001_initial.py create mode 100644 {{cookiecutter.project_slug}}/fruit/migrations/0002_add_initial_fruits.py create mode 100644 {{cookiecutter.project_slug}}/fruit/migrations/__init__.py create mode 100644 {{cookiecutter.project_slug}}/fruit/models.py create mode 100644 {{cookiecutter.project_slug}}/fruit/templates/fruit/counter.html create mode 100644 {{cookiecutter.project_slug}}/fruit/templates/fruit/fruit_list.html create mode 100644 {{cookiecutter.project_slug}}/fruit/tests.py create mode 100644 {{cookiecutter.project_slug}}/fruit/urls.py create mode 100644 {{cookiecutter.project_slug}}/fruit/views.py create mode 100644 {{cookiecutter.project_slug}}/vue_frontend/.browserslistrc create mode 100644 {{cookiecutter.project_slug}}/vue_frontend/.env create mode 100644 {{cookiecutter.project_slug}}/vue_frontend/.eslintrc.js create mode 100644 {{cookiecutter.project_slug}}/vue_frontend/.gitignore create mode 100644 {{cookiecutter.project_slug}}/vue_frontend/README.md create mode 100644 {{cookiecutter.project_slug}}/vue_frontend/babel.config.js create mode 100644 {{cookiecutter.project_slug}}/vue_frontend/package.json create mode 100644 {{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/Counter.vue create mode 100644 {{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/CounterAdjustor.vue create mode 100644 {{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/CounterBanner.vue create mode 100644 {{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/CounterDisplay.vue create mode 100644 {{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/FruitInspector.vue create mode 100644 {{cookiecutter.project_slug}}/vue_frontend/src/fruit/entry/fruit_counter.js create mode 100644 {{cookiecutter.project_slug}}/vue_frontend/src/fruit/entry/fruit_list.js create mode 100644 {{cookiecutter.project_slug}}/vue_frontend/src/fruit/store/module_fruit.js create mode 100644 {{cookiecutter.project_slug}}/vue_frontend/src/rest/rest.js create mode 100644 {{cookiecutter.project_slug}}/vue_frontend/src/store/vuex_store_as_plugin.js create mode 100644 {{cookiecutter.project_slug}}/vue_frontend/src/{{cookiecutter.project_slug}}/assets/logo.png create mode 100644 {{cookiecutter.project_slug}}/vue_frontend/src/{{cookiecutter.project_slug}}/components/HelloWorld.vue create mode 100644 {{cookiecutter.project_slug}}/vue_frontend/src/{{cookiecutter.project_slug}}/entry/main.js create mode 100644 {{cookiecutter.project_slug}}/vue_frontend/vue.config.js create mode 100644 {{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/images/django_logo.png create mode 100644 {{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/webpack_bundle/lazy_render_bundle.html create mode 100644 {{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/webpack_bundle/__init__.py create mode 100644 {{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/webpack_bundle/apps.py create mode 100644 {{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/webpack_bundle/templatetags/__init__.py create mode 100644 {{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/webpack_bundle/templatetags/webpack_bundle.py diff --git a/LICENSE b/LICENSE index a67e4da21..efbb72870 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2013-2020, Daniel Roy Greenfeld +Copyright (c) 2013-2020, Daniel Roy Greenfeld, Mike Hoolehan All rights reserved. Redistribution and use in source and binary forms, with or without modification, diff --git a/README.rst b/README.rst index 47f2bfb7d..45d2b7dff 100644 --- a/README.rst +++ b/README.rst @@ -1,323 +1,100 @@ -Cookiecutter Django +Cookiecutter Vue Django ======================= -.. image:: https://travis-ci.org/pydanny/cookiecutter-django.svg?branch=master - :target: https://travis-ci.org/pydanny/cookiecutter-django?branch=master +.. image:: https://travis-ci.com/ilikerobots/cookiecutter-vue-django.svg?branch=master + :target: https://travis-ci.com/ilikerobots/cookiecutter-vue-django?branch=master :alt: Build Status -.. image:: https://readthedocs.org/projects/cookiecutter-django/badge/?version=latest - :target: https://cookiecutter-django.readthedocs.io/en/latest/?badge=latest +.. image:: https://readthedocs.org/projects/cookiecutter-vue-django/badge/?version=latest + :target: https://cookiecutter-vue-django.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status -.. image:: https://pyup.io/repos/github/pydanny/cookiecutter-django/shield.svg - :target: https://pyup.io/repos/github/pydanny/cookiecutter-django/ +.. image:: https://pyup.io/repos/github/ilikerobots/cookiecutter-vue-django/shield.svg + :target: https://pyup.io/repos/github/ilikerobots/cookiecutter-vue-django/ :alt: Updates -.. image:: https://img.shields.io/badge/cookiecutter-Join%20on%20Slack-green?style=flat&logo=slack - :target: https://join.slack.com/t/cookie-cutter/shared_invite/enQtNzI0Mzg5NjE5Nzk5LTRlYWI2YTZhYmQ4YmU1Y2Q2NmE1ZjkwOGM0NDQyNTIwY2M4ZTgyNDVkNjMxMDdhZGI5ZGE5YmJjM2M3ODJlY2U +Vue + Django with no compromise. -.. image:: https://www.codetriage.com/pydanny/cookiecutter-django/badges/users.svg - :target: https://www.codetriage.com/pydanny/cookiecutter-django - :alt: Code Helpers Badge +Cookiecutter Vue Django is a framework for jumpstarting production-ready Django + Vue projects quickly. Expanding on the the wonderful Cookiecutter Django, this project template allows the intermingling of both Django Templates and Vue, even on the same page, without compromising the full power of either. -.. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/ambv/black - :alt: Code style: black +Typical solutions to integrating Django and Vue forgo much of the strengths of one lieu of the other. For example, a common approach is using Django Rest Framework as backend and writing the entire front end in Vue, making it difficult to utilize Django templates in places it could be expedient. A second approach is to use Vue within Django templates using browser ` + +{% endblock %}{% endraw %} diff --git a/{{cookiecutter.project_slug}}/fruit/tests.py b/{{cookiecutter.project_slug}}/fruit/tests.py new file mode 100644 index 000000000..a39b155ac --- /dev/null +++ b/{{cookiecutter.project_slug}}/fruit/tests.py @@ -0,0 +1 @@ +# Create your tests here. diff --git a/{{cookiecutter.project_slug}}/fruit/urls.py b/{{cookiecutter.project_slug}}/fruit/urls.py new file mode 100644 index 000000000..b786a24ad --- /dev/null +++ b/{{cookiecutter.project_slug}}/fruit/urls.py @@ -0,0 +1,25 @@ +from django.urls import path +from django.views.generic import TemplateView +{%- if cookiecutter.use_drf == "y" %} +from fruit.views import FruitList +{%- endif %} + +app_name = "fruit" + +urlpatterns = [ + {%- if cookiecutter.use_drf == "y" %} + path( + "fruits/", + FruitList.as_view(extra_context={"title": "Fruit Inspector"}), + name="list", + ), + {% endif -%} + path( + "fruit-counter/", + TemplateView.as_view( + template_name="fruit/counter.html", + extra_context={"counter_message": "How many fruits could you eat?"}, + ), + name="counter", + ), +] diff --git a/{{cookiecutter.project_slug}}/fruit/views.py b/{{cookiecutter.project_slug}}/fruit/views.py new file mode 100644 index 000000000..6a38b3ec8 --- /dev/null +++ b/{{cookiecutter.project_slug}}/fruit/views.py @@ -0,0 +1,6 @@ +from django.views.generic import ListView +from fruit.models import Fruit + + +class FruitList(ListView): + model = Fruit diff --git a/{{cookiecutter.project_slug}}/requirements/base.txt b/{{cookiecutter.project_slug}}/requirements/base.txt index ba62bae08..9daeea66f 100644 --- a/{{cookiecutter.project_slug}}/requirements/base.txt +++ b/{{cookiecutter.project_slug}}/requirements/base.txt @@ -42,3 +42,6 @@ django-redis==4.12.1 # https://github.com/jazzband/django-redis # Django REST Framework djangorestframework==3.11.0 # https://github.com/encode/django-rest-framework {%- endif %} +{%- if cookiecutter.use_vue == "y" %} +django-webpack-loader==0.6.0 # https://github.com/owais/django-webpack-loader +{%- endif %} diff --git a/{{cookiecutter.project_slug}}/vue_frontend/.browserslistrc b/{{cookiecutter.project_slug}}/vue_frontend/.browserslistrc new file mode 100644 index 000000000..214388fe4 --- /dev/null +++ b/{{cookiecutter.project_slug}}/vue_frontend/.browserslistrc @@ -0,0 +1,3 @@ +> 1% +last 2 versions +not dead diff --git a/{{cookiecutter.project_slug}}/vue_frontend/.env b/{{cookiecutter.project_slug}}/vue_frontend/.env new file mode 100644 index 000000000..8b0726f30 --- /dev/null +++ b/{{cookiecutter.project_slug}}/vue_frontend/.env @@ -0,0 +1,3 @@ +VUE_APP_STATIC_ROOT=http://localhost:8000/static +VUE_APP_API_ROOT=http://localhost:8000/api + diff --git a/{{cookiecutter.project_slug}}/vue_frontend/.eslintrc.js b/{{cookiecutter.project_slug}}/vue_frontend/.eslintrc.js new file mode 100644 index 000000000..8ad90db46 --- /dev/null +++ b/{{cookiecutter.project_slug}}/vue_frontend/.eslintrc.js @@ -0,0 +1,17 @@ +module.exports = { + root: true, + env: { + node: true + }, + 'extends': [ + 'plugin:vue/essential', + 'eslint:recommended' + ], + parserOptions: { + parser: 'babel-eslint' + }, + rules: { + 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', + 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' + } +} diff --git a/{{cookiecutter.project_slug}}/vue_frontend/.gitignore b/{{cookiecutter.project_slug}}/vue_frontend/.gitignore new file mode 100644 index 000000000..fc546a5b9 --- /dev/null +++ b/{{cookiecutter.project_slug}}/vue_frontend/.gitignore @@ -0,0 +1,23 @@ +.DS_Store +node_modules +/dist + +# local env files +.env.local +.env.*.local +!.env + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/{{cookiecutter.project_slug}}/vue_frontend/README.md b/{{cookiecutter.project_slug}}/vue_frontend/README.md new file mode 100644 index 000000000..fd2a3556a --- /dev/null +++ b/{{cookiecutter.project_slug}}/vue_frontend/README.md @@ -0,0 +1,24 @@ +# vue_frontend + +## Project setup +`yarn install` or `npm install` + +### Compiles and hot-reloads for development +`yarn serve` or `npm run serve` + +{% if cookiecutter.use_pycharm == 'y' -%} +Or use the PyCharm run configuration *Vue serve*. +{%- endif %} + +### Compiles and minifies for production +`yarn build` or `npm run build` + +{% if cookiecutter.use_pycharm == 'y' -%} +Or use the PyCharm run configuration *Vue build*. +{%- endif %} + +### Lints and fixes files +`yarn lint` or `npm run lint` + +### Customize configuration +See [Configuration Reference](https://cli.vuejs.org/config/). diff --git a/{{cookiecutter.project_slug}}/vue_frontend/babel.config.js b/{{cookiecutter.project_slug}}/vue_frontend/babel.config.js new file mode 100644 index 000000000..e9558405f --- /dev/null +++ b/{{cookiecutter.project_slug}}/vue_frontend/babel.config.js @@ -0,0 +1,5 @@ +module.exports = { + presets: [ + '@vue/cli-plugin-babel/preset' + ] +} diff --git a/{{cookiecutter.project_slug}}/vue_frontend/package.json b/{{cookiecutter.project_slug}}/vue_frontend/package.json new file mode 100644 index 000000000..2622edb3c --- /dev/null +++ b/{{cookiecutter.project_slug}}/vue_frontend/package.json @@ -0,0 +1,31 @@ +{ + "name": "vue_frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "serve": "vue-cli-service serve", + "build": "vue-cli-service build", + "lint": "vue-cli-service lint" + }, + "dependencies": { + "axios": "^0.19.2", + "core-js": "^3.6.5", + "vue": "^2.6.11"{% if cookiecutter.use_vuex == 'y' -%}, + "vuex": "^3.4.0", + "vuex-persistedstate": "^3.0.1" + {%- endif %} +}, + "devDependencies": { + "@vue/cli-plugin-babel": "~4.4.0", + "@vue/cli-plugin-eslint": "~4.4.0", + "@vue/cli-plugin-vuex": "~4.4.0", + "@vue/cli-service": "~4.4.0", + "babel-eslint": "^10.1.0", + "eslint": "^6.7.2", + "eslint-plugin-vue": "^6.2.2", + "node-sass": "^4.12.0", + "sass-loader": "^8.0.2", + "vue-template-compiler": "^2.6.11", + "webpack-bundle-tracker": "^0.4.3" + } +} diff --git a/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/Counter.vue b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/Counter.vue new file mode 100644 index 000000000..56df8292f --- /dev/null +++ b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/Counter.vue @@ -0,0 +1,56 @@ + + + + + + diff --git a/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/CounterAdjustor.vue b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/CounterAdjustor.vue new file mode 100644 index 000000000..09ae9d2a7 --- /dev/null +++ b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/CounterAdjustor.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/CounterBanner.vue b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/CounterBanner.vue new file mode 100644 index 000000000..3ff84010c --- /dev/null +++ b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/CounterBanner.vue @@ -0,0 +1,34 @@ + + + + + + \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/CounterDisplay.vue b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/CounterDisplay.vue new file mode 100644 index 000000000..d368e1415 --- /dev/null +++ b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/CounterDisplay.vue @@ -0,0 +1,43 @@ + + + + + + diff --git a/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/FruitInspector.vue b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/FruitInspector.vue new file mode 100644 index 000000000..be14b05f5 --- /dev/null +++ b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/FruitInspector.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/entry/fruit_counter.js b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/entry/fruit_counter.js new file mode 100644 index 000000000..c9301a7ae --- /dev/null +++ b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/entry/fruit_counter.js @@ -0,0 +1,34 @@ +import Vue from "vue/dist/vue.js"; +import storePlugin from "../../../../vue_frontend/src/store/vuex_store_as_plugin"; +import createPersistedState from "vuex-persistedstate"; +import FruitModule from "../store/module_fruit" +const Counter = () => import( /* webpackChunkName: "chunk-counter" */ "../components/Counter.vue"); +const CounterBanner = () => import( /* webpackChunkName: "chunk-counter-banner" */ "../components/CounterBanner.vue"); + +Vue.config.productionTip = false + +// Vuex state will be used in this entry point +Vue.use(storePlugin); + +// Include Vuex modules as needed for this entry point +Vue.prototype.$store.registerModule('fruit', FruitModule); + +// Designate what state should persist across page loads +createPersistedState({ + paths: [ + "fruit.count", + "fruit.activeFruit", + ] + } +)(Vue.prototype.$store); + +// Mount top level components +new Vue({ + el: "#app", + components: {Counter} +}); + +new Vue({ + el: "#counter_banner", + components: {CounterBanner} +}); diff --git a/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/entry/fruit_list.js b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/entry/fruit_list.js new file mode 100644 index 000000000..0d286e9b0 --- /dev/null +++ b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/entry/fruit_list.js @@ -0,0 +1,30 @@ +import Vue from "vue/dist/vue.js"; +import storePlugin from "../../store/vuex_store_as_plugin"; +import createPersistedState from "vuex-persistedstate"; +import FruitModule from "../store/module_fruit" +const FruitInspector = () => import( /* webpackChunkName: "chunk-fruit-inspector" */ "../components/FruitInspector.vue"); + +Vue.config.productionTip = false + +// Vuex state will be used in this entry point +Vue.use(storePlugin); + +// Include Vuex modules as needed for this entry point +Vue.prototype.$store.registerModule('fruit', FruitModule); + + +// Designate what state should persist across page loads +createPersistedState({ + paths: [ + "fruit.count", + "fruit.activeFruit", + ] + } +)(Vue.prototype.$store); + +// Mount top level components +new Vue({ + el: "#app", + components: {FruitInspector} +}); + diff --git a/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/store/module_fruit.js b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/store/module_fruit.js new file mode 100644 index 000000000..1728f03b5 --- /dev/null +++ b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/store/module_fruit.js @@ -0,0 +1,77 @@ +{%- if cookiecutter.use_drf == 'y' -%} +import api from "../../rest/rest"; + +{%- endif -%} +export const MAX_COUNT = 42 +export const MIN_COUNT = 0 + +{%- if cookiecutter.use_drf == 'y' %} +export const ACTION_GET_FRUITS = 'ACT_GET_FRUITS' +export const ACTION_SET_ACTIVE_FRUIT = 'ACT_SET_ACTIVE_FRUIT' +{%- endif %} +export const ACTION_INCREMENT_COUNTER = 'ACT_INC_COUNT' +export const ACTION_DECREMENT_COUNTER = 'ACT_DEC_COUNT' + +{%- if cookiecutter.use_drf == 'y' %} +const MUTATION_SET_FRUITS = 'MUT_SET_FRUITS' +const MUTATION_SET_ACTIVE_FRUIT = 'MUT_SET_ACTIVE_FRUIT' +{%- endif %} +const MUTATION_INCREMENT_COUNTER = 'MUT_INC_COUNT' +const MUTATION_DECREMENT_COUNTER = 'MUT_DEC_COUNT' + +export default { + state: { + count: 0, + {%- if cookiecutter.use_drf == 'y' %} + fruits: [], + activeFruit: null, + {%- endif %} + }, + mutations: { + {%- if cookiecutter.use_drf == 'y' %} + [MUTATION_SET_FRUITS](state, fruitList) { + state.fruits = fruitList; + }, + [MUTATION_SET_ACTIVE_FRUIT](state, fruit) { + state.activeFruit = fruit; + }, + {%- endif %} + [MUTATION_INCREMENT_COUNTER]: state => state.count++, + [MUTATION_DECREMENT_COUNTER]: state => state.count-- + }, + actions: { + {%- if cookiecutter.use_drf == 'y' %} + [ACTION_GET_FRUITS](context) { + api.getFruits() + .then(function (response) { + context.commit(MUTATION_SET_FRUITS, response.data); + }) + .catch(function (error) { + // handle error + console.log(error); + }); + }, + [ACTION_SET_ACTIVE_FRUIT](context, fruitId) { + api.getFruit(fruitId) + .then(function (response) { + context.commit(MUTATION_SET_ACTIVE_FRUIT, response.data); + }) + .catch(function (error) { + // handle error + console.log(error); + }); + }, + {%- endif %} + [ACTION_INCREMENT_COUNTER](context) { + if (context.state.count < MAX_COUNT) { + context.commit(MUTATION_INCREMENT_COUNTER); + } + }, + [ACTION_DECREMENT_COUNTER](context) { + if (context.state.count > MIN_COUNT) { + context.commit(MUTATION_DECREMENT_COUNTER); + } + } + + }, +} \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/vue_frontend/src/rest/rest.js b/{{cookiecutter.project_slug}}/vue_frontend/src/rest/rest.js new file mode 100644 index 000000000..9ebe64912 --- /dev/null +++ b/{{cookiecutter.project_slug}}/vue_frontend/src/rest/rest.js @@ -0,0 +1,23 @@ +import axios from "axios"; + +// axios settings +axios.defaults.baseURL = process.env.VUE_APP_API_ROOT; +axios.defaults.xsrfHeaderName = "X-CSRFToken"; +axios.defaults.xsrfCookieName = 'csrftoken'; + + +const api = axios.create({}); + + +export default { +{%- if cookiecutter.use_fruit_demo == "y" %} + getFruits: function () { + return api.get('/fruits/') + }, + getFruit: function (id) { + return api.get('/fruits/' + id + '/') + } +{%- endif %} + /* Include additional API calls here */ +} + diff --git a/{{cookiecutter.project_slug}}/vue_frontend/src/store/vuex_store_as_plugin.js b/{{cookiecutter.project_slug}}/vue_frontend/src/store/vuex_store_as_plugin.js new file mode 100644 index 000000000..c51c5ac95 --- /dev/null +++ b/{{cookiecutter.project_slug}}/vue_frontend/src/store/vuex_store_as_plugin.js @@ -0,0 +1,17 @@ +import Vue from "vue/dist/vue.js"; +import Vuex from "vuex"; + +Vue.use(Vuex); + +let store = new Vuex.Store({ + modules: { + }, + strict: process.env.NODE_ENV !== "production", +}); + +export default { + store, + install(Vue) { //resetting the default store to use this plugin store + Vue.prototype.$store = store; + } +} \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/vue_frontend/src/{{cookiecutter.project_slug}}/assets/logo.png b/{{cookiecutter.project_slug}}/vue_frontend/src/{{cookiecutter.project_slug}}/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f3d2503fc2a44b5053b0837ebea6e87a2d339a43 GIT binary patch literal 6849 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 + +
+ + + + +
+

{% raw %}{{ msg }}{% endraw %}

+

+ This cookiecutter implements the techniques discussed in the series of articles "Vue + Django: The Best of Both Frontends." +

+ + +

+ In addition to the base options provided by the wonderful Cookiecutter Django, this cookiecutter providees the following options: +

+
+
    + +
  • + use_vue -- provides Vue integration using django-webpack-loader, featuring: +
      +
    • Harmonious coexistence of Django templates and Vue components
    • +
    • Vue Single File Components (SFCs)
    • +
    • Multi-page App (MPA) layout
    • +
    • Vue Loader Hot Reload
    • +
    • Property passing from Django -> Vue
    • +
    • Sass/SCSS pre-compilation
    • +
    • Vue DevTools support
    • +
    • Chunked resource loading
    • +
    • Deferred loading of Vue and/or Vue components
    • +
    +
  • + +
  • + use_vuex -- provides Vuex integration, featuring: +
      +
    • Shared state across components on the same page
    • +
    • Persistent state across pages
    • +
    • Rest support via Axios -> DRF (if use_drf enabled)
    • +
    +
  • +
  • + use_fruit_demo -- provides a sample app, demonstrating: +
      +
    • All basic features from selected options above
    • +
    • Deferred loading
    • +
    • Static asset loading from Django or Vue
    • +
    • If use_drf enabled, a sample API integration
    • +
    • If custom_bootstrap_compilation enabled, SCSS partials import in SFCs
    • +
    • Useless information about fruit
    • +
    +
  • +
+
+ + +
+
+ + + + + + diff --git a/{{cookiecutter.project_slug}}/vue_frontend/src/{{cookiecutter.project_slug}}/entry/main.js b/{{cookiecutter.project_slug}}/vue_frontend/src/{{cookiecutter.project_slug}}/entry/main.js new file mode 100644 index 000000000..6ba1a968e --- /dev/null +++ b/{{cookiecutter.project_slug}}/vue_frontend/src/{{cookiecutter.project_slug}}/entry/main.js @@ -0,0 +1,10 @@ +import Vue from "vue/dist/vue.js"; +const HelloWorld = () => import( /* webpackChunkName: "chunk-hello-world" */ "../components/HelloWorld.vue"); + +Vue.config.productionTip = false + +// Mount top level components +new Vue({ + el: "#app", + components: {HelloWorld} +}); diff --git a/{{cookiecutter.project_slug}}/vue_frontend/vue.config.js b/{{cookiecutter.project_slug}}/vue_frontend/vue.config.js new file mode 100644 index 000000000..c90b0b941 --- /dev/null +++ b/{{cookiecutter.project_slug}}/vue_frontend/vue.config.js @@ -0,0 +1,88 @@ +const BundleTracker = require("webpack-bundle-tracker"); +const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin; + + +const pages = { + 'main': { + entry: './src/{{cookiecutter.project_slug}}/entry/main.js', + chunks: ['chunk-common'] + }, + {%- if cookiecutter.use_fruit_demo == 'y' %} + 'fruit-counter': { + entry: './src/fruit/entry/fruit_counter.js', + chunks: ['chunk-common', 'chunk-state'] + }, + {%- endif %}{%- if cookiecutter.use_fruit_demo == 'y' and cookiecutter.use_drf == 'y' %} + 'fruit-list': { + entry: './src/fruit/entry/fruit_list.js', + chunks: ['chunk-common', 'chunk-state'] + }, + {%- endif %} +} + +module.exports = { + pages: pages, + filenameHashing: true, + productionSourceMap: false, + publicPath: process.env.NODE_ENV === 'production' + ? '/static/vue' + : 'http://localhost:8080/', + outputDir: '../{{cookiecutter.project_slug}}/static/vue/', + + chainWebpack: config => { + + config.optimization + .splitChunks({ + cacheGroups: { + state: { + /* As vuex state is not needed in all our entry points, we isolate it + * in a separate chunk to be loaded only where needed. + */ + test: /[\\/]node_modules[\\/](vuex|vuex-persisted-state)/, + name: "chunk-state", + chunks: "all", + priority: 5 + }, + vendor: { + /* This chunk contains modules that may be used in all entry points, + * including Vue itself + */ + test: /[\\/]node_modules[\\/]/, + name: "chunk-common", + chunks: "all", + priority: 1 + }, + }, + }); + + Object.keys(pages).forEach(page => { + config.plugins.delete(`html-${page}`); + config.plugins.delete(`preload-${page}`); + config.plugins.delete(`prefetch-${page}`); + }) + + config + .plugin('BundleTracker') + .use(BundleTracker, [{ + path: '../vue_frontend/', + filename: 'webpack-stats.json' + }]); + + // Uncomment below to analyze bundle sizes + // config.plugin("BundleAnalyzerPlugin").use(BundleAnalyzerPlugin); + + // config.resolve.alias + // .set('__STATIC__', 'static') + + config.devServer + .public('http://localhost:8080') + .host('localhost') + .port(8080) + .hotOnly(true) + .watchOptions({poll: 1000}) + .https(false) + .headers({"Access-Control-Allow-Origin": ["*"]}) + + } +}; + diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/images/django_logo.png b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/images/django_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..68706a474a61ee464a16c564b89c29fee01f680c GIT binary patch literal 8906 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 s-Inc>6)Kc7mf5**I7Snb|4VegW@qR0{H+}L + {% endraw %}{% if cookiecutter.use_fruit_demo == "y" %}{% raw %} + {% endraw %}{% endif %}{% if cookiecutter.use_fruit_demo == "y" and cookiecutter.use_drf == "y"%}{% raw %} + {% endraw %}{% endif %}{% raw %} {% if request.user.is_authenticated %}