Add Vue project generation options

This commit is contained in:
ilikerobots 2020-07-16 13:00:38 +03:00
parent ca4b3262fd
commit 7d993c5db1
60 changed files with 1301 additions and 282 deletions

View File

@ -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,

View File

@ -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 `<script>` includes, but then lost is the ability to use Vue's Single File Components.
Powered by Cookiecutter_, Cookiecutter Django is a framework for jumpstarting
production-ready Django projects quickly.
This project utilizes a different approach, melding these two technologies more naturally. As a result, not only are the typical compromises eliminated, but additional distinct advantages are realized:
* Documentation: https://cookiecutter-django.readthedocs.io/en/latest/
* See Troubleshooting_ for common errors and obstacles
* If you have problems with Cookiecutter Django, please open issues_ don't send
emails to the maintainers.
.. _Troubleshooting: https://cookiecutter-django.readthedocs.io/en/latest/troubleshooting.html
.. _528: https://github.com/pydanny/cookiecutter-django/issues/528#issuecomment-212650373
.. _issues: https://github.com/pydanny/cookiecutter-django/issues/new
* Increased flexibility: The developer is free to use Django Templates or Vue as appropriate, choosing the right tool for the job
* Increased development speed: Reduce time spent fighting the framework by using Django and Vue where each excels
* Increased performance: Leverage Django's powerful caching backend to deliver content-rich pages quickly with little or no Javascript, while deferring complex and interactive Vue functionality until after page load
Features
---------
* For Django 3.0
* Works with Python 3.8
* Renders Django projects with 100% starting test coverage
* Twitter Bootstrap_ v4 (`maintained Foundation fork`_ also available)
* 12-Factor_ based settings via django-environ_
* Secure by default. We believe in SSL.
* Optimized development and production settings
* Registration via django-allauth_
* Comes with custom user model ready to go
* Optional basic ASGI setup for Websockets
* Optional custom static build using Gulp and livereload
* Send emails via Anymail_ (using Mailgun_ by default or Amazon SES if AWS is selected cloud provider, but switchable)
* Media storage using Amazon S3 or Google Cloud Storage
* Docker support using docker-compose_ for development and production (using Traefik_ with LetsEncrypt_ support)
* Procfile_ for deploying to Heroku
* Instructions for deploying to PythonAnywhere_
* Run tests with unittest or pytest
* Customizable PostgreSQL version
* Default integration with pre-commit_ for identifying simple issues before submission to code review
* All the features of the wonderful cookiecutter-django_
* 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 Template -> Vue Component
* Sass/SCSS pre-compilation of Vue Components
* Vue DevTools support
* Chunked resource loading via webpack
* Deferred loading of Vue and/or Vue components
* Shared Vuex state across components on the same page
* Persistent state across page loads
* REST support via Axios -> DRF
* Sample application illustrating all of the above
.. _`maintained Foundation fork`: https://github.com/Parbhat/cookiecutter-django-foundation
Optional Integrations
---------------------
*These features can be enabled during initial project setup.*
* Serve static files from Amazon S3, Google Cloud Storage or Whitenoise_
* Configuration for Celery_ and Flower_ (the latter in Docker setup only)
* Integration with MailHog_ for local email testing
* Integration with Sentry_ for error logging
.. _Bootstrap: https://github.com/twbs/bootstrap
.. _django-environ: https://github.com/joke2k/django-environ
.. _12-Factor: http://12factor.net/
.. _django-allauth: https://github.com/pennersr/django-allauth
.. _django-avatar: https://github.com/grantmcconnaughey/django-avatar
.. _Procfile: https://devcenter.heroku.com/articles/procfile
.. _Mailgun: http://www.mailgun.com/
.. _Whitenoise: https://whitenoise.readthedocs.io/
.. _Celery: http://www.celeryproject.org/
.. _Flower: https://github.com/mher/flower
.. _Anymail: https://github.com/anymail/django-anymail
.. _MailHog: https://github.com/mailhog/MailHog
.. _Sentry: https://sentry.io/welcome/
.. _docker-compose: https://github.com/docker/compose
.. _PythonAnywhere: https://www.pythonanywhere.com/
.. _Traefik: https://traefik.io/
.. _LetsEncrypt: https://letsencrypt.org/
.. _pre-commit: https://github.com/pre-commit/pre-commit
Constraints
-----------
* Only maintained 3rd party libraries are used.
* Uses PostgreSQL everywhere (9.4 - 11.3)
* Environment variables for configuration (This won't work with Apache/mod_wsgi).
Support this Project!
----------------------
This project is run by volunteers. Please support them in their efforts to maintain and improve Cookiecutter Django:
* Daniel Roy Greenfeld, Project Lead (`GitHub <https://github.com/pydanny>`_, `Patreon <https://www.patreon.com/danielroygreenfeld>`_): expertise in Django and AWS ELB.
* Nikita Shupeyko, Core Developer (`GitHub <https://github.com/webyneter>`_): expertise in Python/Django, hands-on DevOps and frontend experience.
Projects that provide financial support to the maintainers:
Django Crash Course
~~~~~~~~~~~~~~~~~~~~~~~~~
.. image:: https://cdn.shopify.com/s/files/1/0304/6901/files/Django-Crash-Course-300x436.jpg
:name: Django Crash Course: Covers Django 3.0 and Python 3.8
:align: center
:alt: Django Crash Course
:target: https://www.roygreenfeld.com/products/django-crash-course
Django Crash Course for Django 3.0 and Python 3.8 is the best cheese-themed Django reference in the universe!
pyup
~~~~~~~~~~~~~~~~~~
.. image:: https://pyup.io/static/images/logo.png
:name: pyup
:align: center
:alt: pyup
:target: https://pyup.io/
Pyup brings you automated security and dependency updates used by Google and other organizations. Free for open source projects!
.. _cookiecutter-django: https://github.com/pydanny/cookiecutter-django
Usage
------
Let's pretend you want to create a Django project called "redditclone". Rather than using ``startproject``
and then editing the results to include your name, email, and various configuration issues that always get forgotten until the worst possible moment, get cookiecutter_ to do all the work.
First, get Cookiecutter. Trust me, it's awesome::
$ pip install "cookiecutter>=1.7.0"
Now run it against this repo::
$ cookiecutter https://github.com/pydanny/cookiecutter-django
You'll be prompted for some values. Provide them, then a Django project will be created for you.
**Warning**: After this point, change 'Daniel Greenfeld', 'pydanny', etc to your own information.
Answer the prompts with your own desired options_. For example::
Cloning into 'cookiecutter-django'...
remote: Counting objects: 550, done.
remote: Compressing objects: 100% (310/310), done.
remote: Total 550 (delta 283), reused 479 (delta 222)
Receiving objects: 100% (550/550), 127.66 KiB | 58 KiB/s, done.
Resolving deltas: 100% (283/283), done.
project_name [Project Name]: Reddit Clone
project_slug [reddit_clone]: reddit
author_name [Daniel Roy Greenfeld]: Daniel Greenfeld
email [you@example.com]: pydanny@gmail.com
description [Behold My Awesome Project!]: A reddit clone.
domain_name [example.com]: myreddit.com
version [0.1.0]: 0.0.1
timezone [UTC]: America/Los_Angeles
use_whitenoise [n]: n
use_celery [n]: y
use_mailhog [n]: n
use_sentry [n]: y
use_pycharm [n]: y
windows [n]: n
use_docker [n]: n
use_heroku [n]: y
use_compressor [n]: y
Select postgresql_version:
1 - 12.3
2 - 11.8
3 - 10.8
4 - 9.6
5 - 9.5
Choose from 1, 2, 3, 4, 5 [1]: 1
Select js_task_runner:
1 - None
2 - Gulp
Choose from 1, 2 [1]: 1
Select cloud_provider:
1 - AWS
2 - GCP
3 - None
Choose from 1, 2, 3 [1]: 1
custom_bootstrap_compilation [n]: n
Select open_source_license:
1 - MIT
2 - BSD
3 - GPLv3
4 - Apache Software License 2.0
5 - Not open source
Choose from 1, 2, 3, 4, 5 [1]: 1
keep_local_envs_in_vcs [y]: y
debug[n]: n
Enter the project and take a look around::
$ cd reddit/
$ ls
Create a git repo and push it there::
$ git init
$ git add .
$ git commit -m "first awesome commit"
$ git remote add origin git@github.com:pydanny/redditclone.git
$ git push -u origin master
Now take a look at your repo. Don't forget to carefully look at the generated README. Awesome, right?
For local development, see the following:
* `Developing locally`_
* `Developing locally using docker`_
.. _options: http://cookiecutter-django.readthedocs.io/en/latest/project-generation-options.html
.. _`Developing locally`: http://cookiecutter-django.readthedocs.io/en/latest/developing-locally.html
.. _`Developing locally using docker`: http://cookiecutter-django.readthedocs.io/en/latest/developing-locally-docker.html
Community
-----------
* Have questions? **Before you ask questions anywhere else**, please post your question on `Stack Overflow`_ under the *cookiecutter-django* tag. We check there periodically for questions.
* If you think you found a bug or want to request a feature, please open an issue_.
* For anything else, you can chat with us on `Slack`_.
.. _`Stack Overflow`: http://stackoverflow.com/questions/tagged/cookiecutter-django
.. _`issue`: https://github.com/pydanny/cookiecutter-django/issues
.. _`Slack`: https://join.slack.com/t/cookie-cutter/shared_invite/enQtNzI0Mzg5NjE5Nzk5LTRlYWI2YTZhYmQ4YmU1Y2Q2NmE1ZjkwOGM0NDQyNTIwY2M4ZTgyNDVkNjMxMDdhZGI5ZGE5YmJjM2M3ODJlY2U
For Readers of Two Scoops of Django
--------------------------------------------
You may notice that some elements of this project do not exactly match what we describe in chapter 3. The reason for that is this project, amongst other things, serves as a test bed for trying out new ideas and concepts. Sometimes they work, sometimes they don't, but the end result is that it won't necessarily match precisely what is described in the book I co-authored.
For pyup.io Users
-----------------
If you are using `pyup.io`_ to keep your dependencies updated and secure, use the code *cookiecutter* during checkout to get 15% off every month.
.. _`pyup.io`: https://pyup.io
"Your Stuff"
-------------
Scattered throughout the Python and HTML of this project are places marked with "your stuff". This is where third-party libraries are to be integrated with your project.
Releases
--------
Need a stable release? You can find them at https://github.com/pydanny/cookiecutter-django/releases
$ cookiecutter https://github.com/ilikerobots/cookiecutter-vue-django
Not Exactly What You Want?
---------------------------
You'll be prompted for some values. Provide them, then a Django project will be created for you. Don't forget to carefully look at the generated README.
This is what I want. *It might not be what you want.* Don't worry, you have options:
Fork This
~~~~~~~~~~
If you have differences in your preferred setup, I encourage you to fork this to create your own version.
Once you have your fork working, let me know and I'll add it to a '*Similar Cookiecutter Templates*' list here.
It's up to you whether or not to rename your fork.
If you do rename your fork, I encourage you to submit it to the following places:
* cookiecutter_ so it gets listed in the README as a template.
* The cookiecutter grid_ on Django Packages.
For more detailed instructions, see upstream cookiecutter-django_
.. _cookiecutter: https://github.com/cookiecutter/cookiecutter
.. _grid: https://www.djangopackages.com/grids/g/cookiecutters/
.. _cookiecutter-django: https://github.com/pydanny/cookiecutter-django
Submit a Pull Request
~~~~~~~~~~~~~~~~~~~~~~
Issues
-----------
We accept pull requests if they're small, atomic, and make our own project development
experience better.
* If you think you found a bug or want to request a feature, please open an issue_.
.. _`issue`: https://github.com/ilikerobots/cookiecutter-vue-django/issues
Articles
---------
* `Using cookiecutter-django with Google Cloud Storage`_ - Mar. 12, 2019
* `cookiecutter-django with Nginx, Route 53 and ELB`_ - Feb. 12, 2018
* `cookiecutter-django and Amazon RDS`_ - Feb. 7, 2018
* `Using Cookiecutter to Jumpstart a Django Project on Windows with PyCharm`_ - May 19, 2017
* `Exploring with Cookiecutter`_ - Dec. 3, 2016
* `Introduction to Cookiecutter-Django`_ - Feb. 19, 2016
* `Django and GitLab - Running Continuous Integration and tests with your FREE account`_ - May. 11, 2016
* `Development and Deployment of Cookiecutter-Django on Fedora`_ - Jan. 18, 2016
* `Development and Deployment of Cookiecutter-Django via Docker`_ - Dec. 29, 2015
* `How to create a Django Application using Cookiecutter and Django 1.8`_ - Sept. 12, 2015
This cookiecutter is based on the methods described in the following articles
Have a blog or online publication? Write about your cookiecutter-django tips and tricks, then send us a pull request with the link.
* `Vue + Django — Best of Both Frontends`_ - 26 May 2019 by Mike Hoolehan
* `Vue + Django — Best of Both Frontends, Part 2`_ - 4 Dec 2019 by Mike Hoolehan
* `Django + Vue — Blazing Content, Rich Interactivity`_ - 23 Apr 2020 by Mike Hoolehan
.. _`Using cookiecutter-django with Google Cloud Storage`: https://ahhda.github.io/cloud/gce/django/2019/03/12/using-django-cookiecutter-cloud-storage.html
.. _`cookiecutter-django with Nginx, Route 53 and ELB`: https://msaizar.com/blog/cookiecutter-django-nginx-route-53-and-elb/
.. _`cookiecutter-django and Amazon RDS`: https://msaizar.com/blog/cookiecutter-django-and-amazon-rds/
.. _`Exploring with Cookiecutter`: http://www.snowboardingcoder.com/django/2016/12/03/exploring-with-cookiecutter/
.. _`Using Cookiecutter to Jumpstart a Django Project on Windows with PyCharm`: https://joshuahunter.com/posts/using-cookiecutter-to-jumpstart-a-django-project-on-windows-with-pycharm/
.. _`Vue + Django — Best of Both Frontends`: https://medium.com/js-dojo/vue-django-best-of-both-frontends-701307871478
.. _`Vue + Django — Best of Both Frontends, Part 2`: https://medium.com/js-dojo/django-vue-vuex-best-of-both-frontends-part-2-1dcb78215575
.. _`Django + Vue — Blazing Content, Rich Interactivity`: https://medium.com/js-dojo/django-vue-blazing-content-rich-interactivity-b34e45d8c602
Show your Support
-----------------
If you find this repository useful, then please consider leaving a star so this project can reach more people. Also, if the articles above were helpful, then a clap on those platforms would also be appreciated. Thanks!
.. _`Development and Deployment of Cookiecutter-Django via Docker`: https://realpython.com/blog/python/development-and-deployment-of-cookiecutter-django-via-docker/
.. _`Development and Deployment of Cookiecutter-Django on Fedora`: https://realpython.com/blog/python/development-and-deployment-of-cookiecutter-django-on-fedora/
.. _`How to create a Django Application using Cookiecutter and Django 1.8`: https://www.swapps.io/blog/how-to-create-a-django-application-using-cookiecutter-and-django-1-8/
.. _`Introduction to Cookiecutter-Django`: http://krzysztofzuraw.com/blog/2016/django-cookiecutter.html
.. _`Django and GitLab - Running Continuous Integration and tests with your FREE account`: http://dezoito.github.io/2016/05/11/django-gitlab-continuous-integration-phantomjs.html
Code of Conduct
---------------
Everyone interacting in the Cookiecutter project's codebases, issue trackers, chat
Everyone interacting in the this project's codebases, issue trackers, chat
rooms, and mailing lists is expected to follow the `PyPA Code of Conduct`_.

View File

@ -2,7 +2,7 @@
"project_name": "My Awesome Project",
"project_slug": "{{ cookiecutter.project_name.lower()|replace(' ', '_')|replace('-', '_')|replace('.', '_')|trim() }}",
"description": "Behold My Awesome Project!",
"author_name": "Daniel Roy Greenfeld",
"author_name": "Mordecai Brown",
"domain_name": "example.com",
"email": "{{ cookiecutter.author_name.lower()|replace(' ', '-') }}@example.com",
"version": "0.1.0",
@ -45,8 +45,11 @@
"Other SMTP"
],
"use_async": "n",
"use_drf": "n",
"use_drf": "y",
"custom_bootstrap_compilation": "n",
"use_vue": "y",
"use_vuex": "y",
"use_fruit_demo": "y",
"use_compressor": "n",
"use_celery": "n",
"use_mailhog": "n",

View File

@ -115,6 +115,49 @@ def remove_async_files():
os.remove(file_name)
def remove_fruit_files():
shutil.rmtree("fruit")
shutil.rmtree("vue_frontend/src/fruit")
def remove_vuex_files():
shutil.rmtree("vue_frontend/src/store")
shutil.rmtree("vue_frontend/src/rest")
def remove_vue_drf_files():
shutil.rmtree("vue_frontend/src/rest")
file_names = [
os.path.join(
"vue_frontend", "src", "fruit", "components", "FruitInspector.vue"
),
os.path.join("vue_frontend", "src", "fruit", "entry", "fruit_list.js"),
os.path.join("fruit", "templates", "fruit", "fruit_list.html"),
]
for file_name in file_names:
os.remove(file_name)
def remove_vue_files():
shutil.rmtree("vue_frontend")
shutil.rmtree("{{ cookiecutter.project_slug }}/templates/webpack_bundle")
shutil.rmtree("{{ cookiecutter.project_slug }}/webpack_bundle")
os.remove(
os.path.join(
"{{ cookiecutter.project_slug }}", "static", "images", "django_logo.png"
)
)
def remove_vue_pycharm_files():
file_names = [
os.path.join(".idea", "runConfigurations", "vue_build.xml"),
os.path.join(".idea", "runConfigurations", "vue_serve.xml"),
]
for file_name in file_names:
os.remove(file_name)
def remove_dottravisyml_file():
os.remove(".travis.yml")
@ -397,10 +440,26 @@ def main():
if "{{ cookiecutter.use_drf }}".lower() == "n":
remove_drf_starter_files()
remove_vue_drf_files()
if "{{ cookiecutter.use_async }}".lower() == "n":
remove_async_files()
if "{{ cookiecutter.use_fruit_demo }}".lower() == "n":
remove_fruit_files()
if "{{ cookiecutter.use_vuex }}".lower() == "n":
remove_vuex_files()
if "{{ cookiecutter.use_vue }}".lower() == "n":
remove_vue_files()
if (
"{{ cookiecutter.use_vue }}".lower() == "n"
and "{{cookiecutter.use_pycharm }}".lower() == "y"
):
remove_vue_pycharm_files()
print(SUCCESS + "Project initialized, keep up the good work!" + TERMINATOR)

View File

@ -80,3 +80,14 @@ if (
"You should either use AWS or select a different Mail Service for sending emails."
)
sys.exit(1)
if "{{ cookiecutter.use_vue }}".lower() == "n" and "{{ cookiecutter.use_vuex }}" == "y":
print("Use of Vuex requires use of Vue.")
sys.exit(1)
if (
"{{ cookiecutter.use_vuex }}".lower() == "n"
or "{{ cookiecutter.use_vue }}".lower() == "n"
) and "{{ cookiecutter.use_fruit_demo }}" == "y":
print("The fruit demo app requires use of Vue, Vuex, and DRF.")
sys.exit(1)

View File

@ -77,6 +77,9 @@ SUPPORTED_COMBINATIONS = [
{"use_async": "n"},
{"use_drf": "y"},
{"use_drf": "n"},
{"use_vue": "y", "use_vuex": "y", "use_fruit_demo": "y"},
{"use_vue": "y", "use_vuex": "y", "use_fruit_demo": "n"},
{"use_vue": "y", "use_vuex": "n", "use_fruit_demo": "n"},
{"js_task_runner": "None"},
{"js_task_runner": "Gulp"},
{"custom_bootstrap_compilation": "y"},
@ -106,6 +109,8 @@ UNSUPPORTED_COMBINATIONS = [
{"cloud_provider": "None", "use_whitenoise": "n"},
{"cloud_provider": "GCP", "mail_service": "Amazon SES"},
{"cloud_provider": "None", "mail_service": "Amazon SES"},
{"use_vue": "n", "use_vuex": "y", "use_fruit_demo": "y"},
{"use_vuex": "n", "use_fruit_demo": "y"},
]

View File

@ -53,6 +53,12 @@ coverage.xml
# Django stuff:
staticfiles/
{% if cookiecutter.use_vue == 'y' -%}
# Vue stuff:
!/{{ cookiecutter.project_slug }}/static/vue/
{% endif -%}
# Sphinx documentation
docs/_build/

View File

@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Vue build" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/vue_frontend/package.json" />
<command value="run" />
<scripts>
<script value="build" />
</scripts>
<node-interpreter value="/usr/bin/node" />
<envs />
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Vue serve" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/vue_frontend/package.json" />
<command value="run" />
<scripts>
<script value="serve" />
</scripts>
<node-interpreter value="/usr/bin/node" />
<envs />
<method v="2" />
</configuration>
</component>

View File

@ -170,4 +170,27 @@ Bootstrap's javascript as well as its dependencies is concatenated into a single
.. _in the bootstrap source: https://github.com/twbs/bootstrap/blob/v4-dev/scss/_variables.scss
.. _Bootstrap docs: https://getbootstrap.com/docs/4.1/getting-started/theming/
{% endif %}
{% if cookiecutter.use_vue == "y" %}
Vue
^^^
This app integrates with a Vue multi-page app (MPA) located in ``vue_frontend``.
To initialize the frontend, from the ``vue_frontend`` directory, run::
$ npm install
To serve the Vue frontend in hot-reloading development mode::
$ npm run serve
And to build for deployment::
$ npm run serve
For more information, see ``vue_frontend/README.md``.
{% endif %}

View File

@ -1,7 +1,11 @@
from django.conf import settings
from rest_framework.routers import DefaultRouter, SimpleRouter
from {{ cookiecutter.project_slug }}.users.api.views import UserViewSet
from {{ cookiecutter.project_slug }}.users.api.views import UserViewSet # isort:skip
{% if cookiecutter.use_fruit_demo == "y" -%}
from fruit.api.views import FruitViewSet # isort:skip
{% endif -%}
if settings.DEBUG:
router = DefaultRouter()
@ -9,7 +13,9 @@ else:
router = SimpleRouter()
router.register("users", UserViewSet)
{% if cookiecutter.use_fruit_demo == "y" -%}
router.register("fruits", FruitViewSet)
{%- endif %}
app_name = "api"
urlpatterns = router.urls

View File

@ -81,10 +81,19 @@ THIRD_PARTY_APPS = [
"rest_framework",
"rest_framework.authtoken",
{%- endif %}
{%- if cookiecutter.use_vue == "y" %}
"webpack_loader",
{%- endif %}
]
LOCAL_APPS = [
"{{ cookiecutter.project_slug }}.users.apps.UsersConfig",
{%- if cookiecutter.use_vue == "y" %}
"{{ cookiecutter.project_slug }}.webpack_bundle.apps.WebpackBundleConfig",
{%- endif %}
{%- if cookiecutter.use_fruit_demo == "y" %}
"fruit.apps.FruitConfig",
{%- endif %}
# Your stuff: custom apps go here
]
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
@ -322,5 +331,22 @@ REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
}
{%- endif %}
{% if cookiecutter.use_vue == "y" -%}
# Vue
# -------------------------------------------------------------------------------
VUE_FRONTEND_DIR = Path(ROOT_DIR, "vue_frontend")
WEBPACK_LOADER = {
"DEFAULT": {
"CACHE": not DEBUG,
"BUNDLE_DIR_NAME": "vue/", # must end with slash
"STATS_FILE": Path(VUE_FRONTEND_DIR, "webpack-stats.json"),
"POLL_INTERVAL": 0.1,
"TIMEOUT": None,
"IGNORE": [r".+\.hot-update.js", r".+\.map"],
}
}
{% endif -%}
# Your stuff...
# ------------------------------------------------------------------------------

View File

@ -16,6 +16,10 @@ urlpatterns = [
path(
"about/", TemplateView.as_view(template_name="pages/about.html"), name="about"
),
{%- if cookiecutter.use_fruit_demo == 'y' %}
# Fruit demo
path("fruits/", include("fruit.urls", namespace="fruits")),
{%- endif %}
# Django Admin, use {% raw %}{% url 'admin:index' %}{% endraw %}
path(settings.ADMIN_URL, admin.site.urls),
# User management

View File

@ -0,0 +1,8 @@
from django.contrib import admin
from fruit.models import Fruit
@admin.register(Fruit)
class FruitAdmin(admin.ModelAdmin):
list_display = ["name", "description"]
search_fields = ["name"]

View File

@ -0,0 +1,14 @@
from fruit.models import Fruit
from rest_framework import serializers
class FruitDefaultSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Fruit
fields = ["id", "name", "description"]
class FruitDetailSerializer(FruitDefaultSerializer):
class Meta:
model = FruitDefaultSerializer.Meta.model
fields = FruitDefaultSerializer.Meta.fields + ["detail", "detail_link"]

View File

@ -0,0 +1,20 @@
from fruit.api.serializers import FruitDefaultSerializer, FruitDetailSerializer
from fruit.models import Fruit
from rest_framework import viewsets
from rest_framework.permissions import SAFE_METHODS, BasePermission, IsAuthenticated
class ReadOnly(BasePermission):
def has_permission(self, request, view):
return request.method in SAFE_METHODS
class FruitViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [IsAuthenticated | ReadOnly]
queryset = Fruit.objects.all()
def get_serializer_class(self):
if self.action == "retrieve":
return FruitDetailSerializer
else:
return FruitDefaultSerializer

View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class FruitConfig(AppConfig):
name = "fruit"

View File

@ -0,0 +1,24 @@
# Generated by Django 3.0.7 on 2020-07-13 08:54
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Fruit',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('description', models.CharField(max_length=1024)),
('detail', models.TextField()),
('detail_link', models.URLField(max_length=512)),
],
),
]

View File

@ -0,0 +1,39 @@
from django.db import migrations
def add_fruits(apps, schema_editor):
Fruit = apps.get_model('fruit', 'Fruit')
db_alias = schema_editor.connection.alias
Fruit.objects.using(db_alias).create(
name="Apple", description="Round, usually red or green",
detail_link="https://en.wikipedia.org/wiki/Apple",
detail="An apple is an edible fruit produced by an apple tree (Malus domestica). Apple trees are cultivated worldwide and are the most widely grown species in the genus Malus. The tree originated in Central Asia, where its wild ancestor, Malus sieversii, is still found today. Apples have been grown for thousands of years in Asia and Europe and were brought to North America by European colonists. Apples have religious and mythological significance in many cultures, including Norse, Greek and European Christian tradition.\n\nSource: Wikipedia article \"Apple\", released under the Creative Commons Attribution-Share-Alike License 3.0.")
Fruit.objects.using(db_alias).create(
name="Pear", description="Tapered top, light green",
detail_link="https://en.wikipedia.org/wiki/Pear",
detail="The pear tree and shrub are a species of genus Pyrus, in the family Rosaceae, bearing the pomaceous fruit of the same name. Several species of pear are valued for their edible fruit and juices while others are cultivated as trees. The tree is medium-sized and native to coastal as well as mildly temperate regions of Europe, north Africa and Asia. Pear wood is one of the preferred materials in the manufacture of high-quality woodwind instruments and furniture.\n\nSource: Wikipedia article \"Pear\", released under the Creative Commons Attribution-Share-Alike License 3.0.")
Fruit.objects.using(db_alias).create(
name="Strawberry", description="Bright red, soft, covered in tiny seeds",
detail_link="https://en.wikipedia.org/wiki/Strawberry",
detail="The garden strawberry (or simply strawberry) is a widely grown hybrid species of the genus Fragaria, collectively known as the strawberries, which are cultivated worldwide for their fruit. The fruit is widely appreciated for its characteristic aroma, bright red color, juicy texture, and sweetness. It is consumed in large quantities, either fresh or in such prepared foods as jam, juice, pies, ice cream, milkshakes, and chocolates. Artificial strawberry flavorings and aromas are also widely used in products such as candy, soap, lip gloss, perfume, and many others.\n\nSource: Wikipedia article \"Strawberry\", released under the Creative Commons Attribution-Share-Alike License 3.0.")
def remove_fruits(apps, schema_editor):
Fruit = apps.get_model('fruit', 'Fruit')
db_alias = schema_editor.connection.alias
Fruit.objects.filter(name='Apple').using(db_alias).delete()
Fruit.objects.filter(name='Pear').using(db_alias).delete()
Fruit.objects.filter(name='Strawberry').using(db_alias).delete()
class Migration(migrations.Migration):
dependencies = [
('fruit', '0001_initial'),
]
operations = [
migrations.RunPython(add_fruits, remove_fruits),
]

View File

@ -0,0 +1,8 @@
from django.db import models
class Fruit(models.Model):
name = models.CharField(max_length=100)
description = models.CharField(max_length=1024)
detail = models.TextField()
detail_link = models.URLField(max_length=512)

View File

@ -0,0 +1,23 @@
{% raw %}{% extends "base.html" %}
{% load render_bundle from webpack_loader %}
{% block content %}
<div class="row justify-content-around mt-5">
<div class="col-9 text-center">
<div id="app">
<counter msg="{{ counter_message }}"></counter>
</div>
</div>
<div class="col-9 text-center">
<div id="counter_banner">
<counter-banner></counter-banner>
</div>
</div>
</div>
{% render_bundle 'chunk-common' %}
{% render_bundle 'chunk-state' %}
{% render_bundle 'fruit-counter' %}
{% endblock content %}{% endraw %}

View File

@ -0,0 +1,36 @@
{% raw %}{% extends "base.html" %}
{% load lazy_render_bundle from webpack_bundle %}
{% block content %}
<h2>Directory of Fruits</h2>
<p>The directory contains {{ object_list.count }} fruit{{ object_list.count|pluralize }}.</p>
{% if object_list %}
<ul>
{% for fruit in object_list %}
<li>{{ fruit.name }}</li>
{% endfor %}
</ul>
{% endif %}
<div class="row justify-content-around mt-5">
<div id="button_loader" class="col-9 text-center">
<input class="btn btn-primary" id="z_button_loader" type="button" value="Load Fruit Inspector">
</div>
<div class="col-9">
<div id="app">
<fruit-inspector title="{{ title }}"></fruit-inspector>
</div>
</div>
</div>
<script>
let loaderBtn = document.getElementById('button_loader');
loaderBtn.addEventListener('click', function (e) {
loaderBtn.style.display = "none";
{% lazy_render_bundle 'chunk-common' %}
{% lazy_render_bundle 'chunk-state' %}
{% lazy_render_bundle 'fruit-list' %}
});
</script>
{% endblock %}{% endraw %}

View File

@ -0,0 +1 @@
# Create your tests here.

View File

@ -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",
),
]

View File

@ -0,0 +1,6 @@
from django.views.generic import ListView
from fruit.models import Fruit
class FruitList(ListView):
model = Fruit

View File

@ -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 %}

View File

@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

View File

@ -0,0 +1,3 @@
VUE_APP_STATIC_ROOT=http://localhost:8000/static
VUE_APP_API_ROOT=http://localhost:8000/api

View File

@ -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'
}
}

View File

@ -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?

View File

@ -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/).

View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

View File

@ -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"
}
}

View File

@ -0,0 +1,56 @@
<template>
<div>
<h1>{% raw %}{{ msg }}{% endraw %}</h1>
<br/>
<div class="adjustor d-inline-block">
<counter-adjustor mode="dec"></counter-adjustor>
</div>
<div class="display d-inline-block">
<counter-display></counter-display>
</div>
<div class="adjustor d-inline-block">
<counter-adjustor mode="inc"></counter-adjustor>
</div>
</div>
</template>
<script>
import CounterAdjustor from "../components/CounterAdjustor";
import CounterDisplay from "../components/CounterDisplay";
export default {
name: 'Counter',
components: {CounterAdjustor, CounterDisplay},
props: {
msg: String
},
computed: {},
methods: {},
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
{% if cookiecutter.custom_bootstrap_compilation == 'y' -%}
/* We may import partials from our source to use variables, mixins, etc */
@import '../../../../{{cookiecutter.project_slug}}/static/sass/custom_bootstrap_vars';
{% endif -%}
h1 {
margin: 40px 0 0;
{%- if cookiecutter.custom_bootstrap_compilation == 'y' %}
color: $vue-green;
{% else %}
color: #42b883;
{%- endif %}
}
.adjustor {
height: 5rem;
padding-left:2rem;
padding-right:2rem;
vertical-align: middle;
}
</style>

View File

@ -0,0 +1,55 @@
<template>
<div>
<!-- Because project css is loaded on our base tempalte, we can use those styles -->
<div class="btn-group" role="group">
<button v-if="this.showDecrement()" class="btn btn-outline-primary" @click="decrement">-</button>
<button v-if="this.showIncrement()" class="btn btn-outline-primary" @click="increment">+</button>
</div>
</div>
</template>
<script>
import {ACTION_DECREMENT_COUNTER, ACTION_INCREMENT_COUNTER} from "../store/module_fruit";
export default {
name: 'CounterAdjustor',
props: {
mode: {
type: String,
default: 'all'
}
},
methods: {
showDecrement() {
return this.mode === 'dec' || this.mode === 'all'
},
showIncrement() {
return this.mode === 'inc' || this.mode === 'all'
},
increment() {
this.$store.dispatch(ACTION_INCREMENT_COUNTER)
},
decrement() {
this.$store.dispatch(ACTION_DECREMENT_COUNTER)
}
},
}
</script>
<style scoped lang="scss">
button {
font-size: 2rem;
line-height: 2rem;
font-weight: bold;
width: 3rem;
height: 3rem;
color: #5a5a5a;
border-color: #5a5a5a;
}
</style>

View File

@ -0,0 +1,34 @@
<template>
<div>
<p class="">{% raw %}{{ message }}{% endraw %}</p>
</div>
</template>
<script>
export default {
name: 'CounterBanner',
props: { },
computed: {
count() { return this.$store.state.fruit.count },
message() {
if (this.count <= 3) {
return "You like fruit, don't you?";
} else if (this.count <= 6) {
return "Just right.";
} else if (this.count <= 12) {
return "You must be hungry!";
} else {
return "You must be very hungry!";
}
}
},
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
p {
font-style: italic;
font-size: 2rem;
}
</style>

View File

@ -0,0 +1,43 @@
<template>
<div>
<!-- Because our project css is loaded from our Django base template, we may use those inherited styles -->
<div id="display" class="display-4 rounded-circle">{% raw %}{{ count }}{% endraw %}</div>
</div>
</template>
<script>
export default {
name: 'CounterDisplay',
props: {
msg: String
},
computed: {
count() { return this.$store.state.fruit.count }
},
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
{% if cookiecutter.custom_bootstrap_compilation == 'y' -%}
/* We may import partials from our source to use variables, mixins, etc */
@import '../../../../{{cookiecutter.project_slug}}/static/sass/custom_bootstrap_vars';
{% endif -%}
#display {
{%- if cookiecutter.custom_bootstrap_compilation == 'y' %}
background-color: $vue-green;
{% else %}
background-color: #42b883;
{%- endif %}
color: white;
font-size: 4rem;
font-weight: bold;
line-height: 6rem;
height: 6rem;
width: 6rem;
text-align: center;
vertical-align: baseline;
}
</style>

View File

@ -0,0 +1,106 @@
<template>
<div class="card bg-light mb-3">
<div class="card-header"><h3>{% raw %}{{ title }}{% endraw %}</h3></div>
<div class="card-body">
<form>
<div class="form-group row">
<label for="fruitChoice" class="col-sm-3 col-form-label font-weight-bold">Fruit</label>
<div class="col-sm-9">
<select id="fruitChoice"
class="form-control"
@change="setActiveFruit"
:value="activeFruit ? activeFruit.id: -1"
>
<option v-if="activeFruit == null" :value="-1">Select a fruit</option>
<option
v-for="f in fruits"
:key="f.id"
:selected="activeFruit != null ? f.id === activeFruit.id: false"
:value="f.id"
>
{% raw %}{{ f.name }}{% endraw %}
</option>
</select>
</div>
</div>
<div class="form-group row" v-if="activeFruit">
<label for="fruitDescription" class="col-sm-3 col-form-label font-weight-bold">Description</label>
<div class="col-sm-9">
<input type="text" readonly class="form-control-plaintext" id="fruitDescription"
:value="activeFruit.description"
>
</div>
</div>
<div class="form-group row" v-if="activeFruit">
<label for="fruitDetail" class="col-sm-3 col-form-label font-weight-bold">Detail</label>
<div class="col-sm-9">
<textarea type="text" readonly class="form-control-plaintext"
rows=8
id="fruitDetail"
:value="activeFruit.detail"
></textarea>
</div>
</div>
<div class="form-group row" v-if="activeFruit">
<label for="fruitLink" class="col-sm-3 col-form-label font-weight-bold">Detail Source Link</label>
<div class="col-sm-9">
<a id="fruitLink" :href="activeFruit.detail_link"
target="_blank">{% raw %}{{ activeFruit.detail_link }}{% endraw %}</a>
</div>
</div>
</form>
</div>
</div>
</template>
<script>
import {ACTION_GET_FRUITS, ACTION_SET_ACTIVE_FRUIT} from "../store/module_fruit";
export default {
name: 'FruitInspector',
components: {},
mounted() {
this.$store.dispatch(ACTION_GET_FRUITS);
},
props: {
title: String,
},
computed: {
fruits() {
return this.$store.state.fruit.fruits
},
activeFruit() {
// TODO: should check if active fruit is in list of fruits
return this.$store.state.fruit.activeFruit
},
},
methods: {
setActiveFruit(e) {
this.$store.dispatch(ACTION_SET_ACTIVE_FRUIT, e.target.value);
}
},
}
</script>
<style scoped lang="scss">
{% if cookiecutter.custom_bootstrap_compilation == 'y' -%}
/* We may import partials from our source to use variables, mixins, etc */
@import '../../../../{{cookiecutter.project_slug}}/static/sass/custom_bootstrap_vars';
{% endif -%}
.card-header {
{%- if cookiecutter.custom_bootstrap_compilation == 'y' %}
background-color: $vue-green;
{% else %}
background-color: #42b883;
{%- endif %}
color: white;
}
</style>

View File

@ -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}
});

View File

@ -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}
});

View File

@ -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);
}
}
},
}

View File

@ -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 */
}

View File

@ -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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,116 @@
<template>
<div id="app" class="text-center">
<img class="logo" alt="Vue logo" src="../assets/logo.png">
<span class="plus">+</span>
<img class="logo" alt="Django logo" :src="`${staticRoot}/images/django_logo.png`">
<div class="hello">
<h1>{% raw %}{{ msg }}{% endraw %}</h1>
<p>
This cookiecutter implements the techniques discussed in the series of articles "Vue + Django: The Best of Both Frontends."
</p>
<ul class="articles">
<li><a href="https://medium.com/js-dojo/vue-django-best-of-both-frontends-701307871478" target="_blank" rel="noopener">Part 1: Basics</a></li>
<li><a href="https://medium.com/js-dojo/django-vue-vuex-best-of-both-frontends-part-2-1dcb78215575" target="_blank" rel="noopener">Part 2: Persistent State</a></li>
<li><a href="https://medium.com/js-dojo/django-vue-blazing-content-rich-interactivity-b34e45d8c602" target="_blank" rel="noopener">Part 3: Deferred Loading</a></li>
</ul>
<p>
In addition to the base options provided by the wonderful <a href="https://github.com/pydanny/cookiecutter-django">Cookiecutter Django</a>, this cookiecutter providees the following options:
</p>
<div class="container col-md-8 text-left">
<ul class="options">
<li>
<span class="opt">use_vue</span> -- <span class="expl">provides Vue integration using <a href="https://github.com/owais/django-webpack-loader">django-webpack-loader</a>, featuring:</span>
<ul>
<li>Harmonious coexistence of Django templates and Vue components</li>
<li>Vue Single File Components (SFCs)</li>
<li>Multi-page App (MPA) layout</li>
<li>Vue Loader Hot Reload</li>
<li>Property passing from Django -> Vue</li>
<li>Sass/SCSS pre-compilation</li>
<li>Vue DevTools support</li>
<li>Chunked resource loading</li>
<li>Deferred loading of Vue and/or Vue components</li>
</ul>
</li>
<li>
<span class="opt">use_vuex</span> -- <span class="expl">provides <a href="https://vuex.vuejs.org/">Vuex</a> integration, featuring:</span>
<ul>
<li>Shared state across components on the same page</li>
<li>Persistent state across pages</li>
<li>Rest support via <a href="https://github.com/axios/axios">Axios</a> -> <a href="https://www.django-rest-framework.org/">DRF</a> (if <i>use_drf</i> enabled)</li>
</ul>
</li>
<li>
<span class="opt">use_fruit_demo</span> -- <span class="expl">provides a sample app, demonstrating:</span>
<ul>
<li>All basic features from selected options above</li>
<li>Deferred loading </li>
<li>Static asset loading from Django or Vue</li>
<li>If <i>use_drf</i> enabled, a sample API integration</li>
<li>If <i>custom_bootstrap_compilation</i> enabled, SCSS partials import in SFCs</li>
<li>Useless information about fruit</li>
</ul>
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
},
data () {
return {
staticRoot: process.env.VUE_APP_STATIC_ROOT
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
h1 {
margin: 40px 0 0;
font-style: italic;
}
h3 {
margin: 40px 0 0;
}
ul.articles {
list-style-type: none;
padding: 0;
li {
display: inline-block;
margin: 0 10px;
}
}
.opt {
font-weight: bold;
}
p {
margin-top: 1.5em;
margin-bottom: .5em;
}
a {
color: #42b983;
}
.logo {
height: 150px;
margin-top: 25px;
}
.plus {
font-size: 44px;
padding-left: 10px;
padding-right: 20px;
}
</style>

View File

@ -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}
});

View File

@ -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": ["*"]})
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -1 +1,19 @@
{%- if cookiecutter.use_vue == 'y' %}
function load_bundle_file(url, filetype) {
let fileref;
if (filetype === "js") { // build js script tag
fileref = document.createElement('script');
fileref.setAttribute("type", "text/javascript");
fileref.setAttribute("src", url);
} else if (filetype === "css") { // build css link tag
fileref = document.createElement("link");
fileref.setAttribute("rel", "stylesheet");
fileref.setAttribute("type", "text/css");
fileref.setAttribute("href", url);
}
if (typeof fileref != "undefined")
document.getElementsByTagName("head")[0].appendChild(fileref);
}
{% endif -%}
/* Project specific Javascript goes here. */

View File

@ -0,0 +1,4 @@
{%- if cookiecutter.use_vue == 'y' -%}
$vue-blue: #34495E;
$vue-green: #41B883;
{% endif -%}

View File

@ -50,7 +50,13 @@
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'about' %}">About</a>
</li>
</li>{% endraw %}{% if cookiecutter.use_fruit_demo == "y" %}{% raw %}
<li class="nav-item">
<a class="nav-link" href="{% url 'fruit:counter' %}">Fruit Counter</a>
</li>{% endraw %}{% endif %}{% if cookiecutter.use_fruit_demo == "y" and cookiecutter.use_drf == "y"%}{% raw %}
<li class="nav-item">
<a class="nav-link" href="{% url 'fruit:list' %}">Fruit List</a>
</li>{% endraw %}{% endif %}{% raw %}
{% if request.user.is_authenticated %}
<li class="nav-item">
{# URL provided by django-allauth/account/urls.py #}

View File

@ -1 +1,14 @@
{% raw %}{% extends "base.html" %}{% endraw %}
{% raw %}{% extends "base.html" %}{% endraw %}{%- if cookiecutter.use_vue == 'y' %}{% raw %}
{% load render_bundle from webpack_loader %}
{% block content %}
<div id="app">
<hello-world msg="Hello from Django and Vue!"></hello-world>
</div>
{% render_bundle 'chunk-common' %}
{% render_bundle 'main' %}
{% endblock content %}{% endraw %}{% endif -%}

View File

@ -0,0 +1,10 @@
{% raw %}{% load get_files from webpack_loader %}
{% get_files bundle as bundle_files %}
{% for f in bundle_files %}
{% if f.url|default:""|slice:"-2:" == "js" %}
load_bundle_file("{{ f.url }}", "js");
{% else %}
load_bundle_file("{{ f.url }}", "css");
{% endif %}
{% endfor %}{% endraw %}

View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class WebpackBundleConfig(AppConfig):
name = "{{ cookiecutter.project_slug }}.webpack_bundle"
verbose_name = _("Webpack Bundle")

View File

@ -0,0 +1,10 @@
from typing import Dict
from django import template
register = template.Library()
@register.inclusion_tag("webpack_bundle/lazy_render_bundle.html", takes_context=False)
def lazy_render_bundle(bundle: Dict[str, str]) -> Dict[str, Dict[str, str]]:
return {"bundle": bundle}