├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── cookiecutter.json
├── hooks
└── post_gen_project.py
├── requirements.txt
├── tests
├── test_docker.sh
└── test_generation.py
└── {{cookiecutter.project_slug}}
├── .editorconfig
├── .gitignore
├── .graphqlrc
├── README.md
├── backend
├── Dockerfile
├── __init__.py
├── apps
│ ├── __init__.py
│ └── users
│ │ ├── __init__.py
│ │ ├── admin.py
│ │ ├── apps.py
│ │ ├── forms.py
│ │ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ │ ├── models.py
│ │ ├── schema.py
│ │ ├── serializers.py
│ │ ├── templates
│ │ └── mail
│ │ │ └── password_reset.txt
│ │ └── views.py
├── config
│ ├── __init__.py
│ ├── api.py
│ ├── schema.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
├── requirements.txt
├── scripts
│ ├── entrypoint.sh
│ ├── gunicorn.sh
│ └── start.sh
└── static
│ └── .gitkeep
├── docker-compose-prod.yml
├── docker-compose.yml
├── env.example
├── frontend
├── babel.config.js
├── package-lock.json
├── package.json
├── public
│ ├── img
│ │ └── icons
│ │ │ ├── android-chrome-192x192.png
│ │ │ ├── android-chrome-512x512.png
│ │ │ ├── apple-touch-icon-120x120.png
│ │ │ ├── apple-touch-icon-152x152.png
│ │ │ ├── apple-touch-icon-180x180.png
│ │ │ ├── apple-touch-icon-60x60.png
│ │ │ ├── apple-touch-icon-76x76.png
│ │ │ ├── apple-touch-icon.png
│ │ │ ├── favicon-16x16.png
│ │ │ ├── favicon-32x32.png
│ │ │ ├── msapplication-icon-144x144.png
│ │ │ ├── mstile-150x150.png
│ │ │ └── safari-pinned-tab.svg
│ ├── index.html
│ ├── manifest.json
│ └── robots.txt
├── src
│ ├── App.vue
│ ├── apollo.js
│ ├── components
│ │ └── ExampleComponent.vue
│ ├── graphql
│ │ ├── mutations
│ │ │ ├── login.gql
│ │ │ ├── logout.gql
│ │ │ ├── refreshToken.gql
│ │ │ ├── register.gql
│ │ │ ├── resetPassword.gql
│ │ │ ├── resetPasswordConfirm.gql
│ │ │ └── verifyToken.gql
│ │ └── queries
│ │ │ └── profile.gql
│ ├── main.js
│ ├── registerServiceWorker.js
│ ├── router.js
│ ├── store.js
│ └── store
│ │ ├── index.js
│ │ ├── modules
│ │ └── auth.js
│ │ └── services
│ │ └── users.js
├── tests
│ └── unit
│ │ └── .eslintrc.js
└── vue.config.js
├── jsconfig.json
└── nginx
├── Dockerfile
├── dev.conf
└── prod.conf
/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | .DS_Store
3 | .AppleDouble
4 | .LSOverride
5 |
6 | # ## Editors ##
7 | # SublimeText
8 | *.tmlanguage.cache
9 | *.tmPreferences.cache
10 | *.stTheme.cache
11 |
12 | *.sublime-project
13 | *.sublime-workspace
14 |
15 | # Pycharm
16 | .idea/*
17 |
18 | # Vim
19 | *~
20 | *.swp
21 | *.swo
22 |
23 | # VScode
24 | .vscode
25 |
26 | # ## NodeJS & Webpack ##
27 | # npm
28 | node_modules/
29 |
30 | # Webpack
31 | webpack-stats.json
32 | dist/
33 |
34 | # Webpack tracker
35 | webpack.json
36 |
37 | # ## Python ##
38 | # Basics
39 | *.py[cod]
40 | __pycache__
41 |
42 | # virtual environments
43 | .env
44 | venv
45 |
46 | # Pyenv
47 | .python-version
48 |
49 | # Unit test / coverage reports
50 | .coverage
51 | .tox
52 | nosetests.xml
53 | htmlcov
54 |
55 | # ## Django ##
56 | # Translations
57 | *.mo
58 | *.pot
59 |
60 | # User-uploaded media
61 | */media/
62 | */staticfiles/
63 |
64 | # ## Other things
65 | # sftp configuration file
66 | sftp-config.json
67 |
68 | # Logs
69 | logs
70 | *.log
71 | pip-log.txt
72 | npm-debug.log*
73 |
74 | # Compass
75 | .sass-cache
76 |
77 | # Cache
78 | .cache/
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: required
2 | dist: trusty
3 | group: edge
4 | services: docker
5 | language: python
6 | python: 3.7
7 | cache: pip
8 | script:
9 | - py.test tests
10 | - sh tests/test_docker.sh
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013-2016, Daniel Greenfeld
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification,
5 | are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | * Redistributions in binary form must reproduce the above copyright notice, this
11 | list of conditions and the following disclaimer in the documentation and/or
12 | other materials provided with the distribution.
13 |
14 | * Neither the name of Cookiecutter Django nor the names of its contributors may
15 | be used to endorse or promote products derived from this software without
16 | specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
21 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
22 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
23 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
25 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
26 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
27 | OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Cookiecutter Django-Vue
6 | =======================
7 |
8 | Powered by [Cookiecutter](https://github.com/audreyr/cookiecutter),
9 | inspired by [Cookiecutter Django](https://github.com/pydanny/cookiecutter-django).
10 |
11 |
12 |
13 |
14 |
15 | Features
16 | --------
17 |
18 | - [Docker](https://www.docker.com/)
19 | - [12 Factor](http://12factor.net/)
20 | - Server: [Nginx](https://nginx.org/)
21 | - Frontend: [Vue](https://vuejs.org/) + [vue-cli](https://cli.vuejs.org/) + [PWA](https://developers.google.com/web/progressive-web-apps/)
22 | - Backend: [Django](https://www.djangoproject.com/)
23 | - Database: [PostgreSQL](https://www.postgresql.org/)
24 | - API: REST or GraphQL
25 |
26 | Optional Integrations
27 | ---------------------
28 |
29 | *These features can be enabled during initial project setup.*
30 |
31 | - Integration with [MailHog](https://github.com/mailhog/MailHog) for local email testing
32 | - Integration with [Sentry](https://sentry.io/welcome/) for frontend and backend errors logging
33 | - Integration with [Google Analytics](https://www.google.com/analytics/) or [Yandex Metrika](https://tech.yandex.ru/metrika/) for web-analytics
34 | - Automatic database backups
35 |
36 | Usage
37 | -----
38 |
39 | First, get `cookiecutter`:
40 |
41 | $ pip install cookiecutter
42 |
43 | Now run it against this repo:
44 |
45 | $ cookiecutter gh:vchaptsev/cookiecutter-django-vue
46 |
47 | You'll be prompted for some values. Provide them, then a project
48 | will be created for you.
49 |
50 | Answer the prompts with your own desired options. For example:
51 |
52 | ======================== INFO ======================= [ ]:
53 | project_name [Project Name]: Website
54 | project_slug [website]: website
55 | description [Short description]: My awesome website
56 | author [Your Name]: Your Name
57 | email []:
58 | ====================== GENERAL ====================== [ ]:
59 | Select api:
60 | 1 - REST
61 | 2 - GraphQL
62 | Choose from 1, 2 [1]: 2
63 | backups [y]: y
64 | ==================== INTEGRATIONS =================== [ ]:
65 | use_sentry [y]: y
66 | use_mailhog [y]: y
67 | Select analytics:
68 | 1 - Google Analytics
69 | 2 - Yandex Metrika
70 | 3 - None
71 | Choose from 1, 2, 3 [1]: 2
72 |
73 | Project creation will cause some odd newlines and linter errors, so I'd recommend:
74 |
75 | $ pip install autopep8
76 | $ autopep8 -r --in-place --aggressive --aggressive backend
77 | $ cd frontend && npm i && npm run lint --fix
78 |
79 | Now you can start project with
80 | [docker-compose](https://docs.docker.com/compose/):
81 |
82 | $ docker-compose up --build
83 |
84 | For production you'll need to fill out `.env` file and use
85 | `docker-compose-prod.yml` file:
86 |
87 | $ docker-compose -f docker-compose-prod.yml up --build -d
88 |
89 |
90 | Contributing
91 | ------------
92 |
93 | Help and feedback are welcome :)
94 |
--------------------------------------------------------------------------------
/cookiecutter.json:
--------------------------------------------------------------------------------
1 | {
2 | "======================== INFO =======================": " ",
3 | "project_name": "Project Name",
4 | "project_slug": "{{ cookiecutter.project_name.lower()|replace(' ', '_')|replace('-', '_') }}",
5 | "domain": "{{ cookiecutter.project_slug }}.com",
6 | "description": "Short description",
7 |
8 | "author": "Your Name",
9 | "email": "admin@{{ cookiecutter.domain }}",
10 |
11 | "====================== GENERAL ======================": " ",
12 | "api": ["REST", "GraphQL"],
13 | "backups": "y",
14 |
15 | "==================== INTEGRATIONS ===================": " ",
16 | "use_sentry": "y",
17 | "use_mailhog": "y",
18 | "analytics": ["Google Analytics", "Yandex Metrika", "None"]
19 | }
20 |
--------------------------------------------------------------------------------
/hooks/post_gen_project.py:
--------------------------------------------------------------------------------
1 | """
2 | 1. Generates and saves random secret key
3 | 2. Renames env.example to .env
4 | 3. Deletes unused API files
5 | """
6 | import os
7 | import random
8 | import string
9 | import shutil
10 |
11 | # Get the root project directory
12 | PROJECT_DIRECTORY = os.path.realpath(os.path.curdir)
13 |
14 |
15 | def set_secret_key():
16 | """ Generates and saves random secret key """
17 | with open(os.path.join(PROJECT_DIRECTORY, 'env.example')) as f:
18 | file_ = f.read()
19 |
20 | punctuation = string.punctuation.replace('"', '').replace("'", '').replace('\\', '')
21 | secret = ''.join(random.choice(string.digits + string.ascii_letters + punctuation) for i in range(50))
22 | file_ = file_.replace('CHANGEME!!!', secret, 1)
23 |
24 | # Write the results
25 | with open(os.path.join(PROJECT_DIRECTORY, 'env.example'), 'w') as f:
26 | f.write(file_)
27 |
28 |
29 | def rename_env_file():
30 | """ Renames env file """
31 | os.rename(os.path.join(PROJECT_DIRECTORY, 'env.example'), os.path.join(PROJECT_DIRECTORY, '.env'))
32 |
33 |
34 | def delete_api_files():
35 | """ Deletes unused API files """
36 | if '{{ cookiecutter.api }}' == 'REST':
37 | files = [
38 | '.graphqlrc',
39 | 'backend/config/schema.py',
40 | 'backend/apps/users/schema.py',
41 | 'frontend/src/apollo.js',
42 | ]
43 | shutil.rmtree(os.path.join(PROJECT_DIRECTORY, 'frontend/src/graphql'))
44 | else:
45 | files = [
46 | 'backend/config/api.py',
47 | 'backend/apps/users/views.py',
48 | 'backend/apps/users/serializers.py',
49 | ]
50 | shutil.rmtree(os.path.join(PROJECT_DIRECTORY, 'frontend/src/store'))
51 |
52 | for filename in files:
53 | os.remove(os.path.join(PROJECT_DIRECTORY, filename))
54 |
55 |
56 | set_secret_key()
57 | rename_env_file()
58 | delete_api_files()
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | cookiecutter==1.7.2
2 |
3 | # Testing
4 | pytest==5.4.1
5 | pytest-cookies==0.5.1
6 |
--------------------------------------------------------------------------------
/tests/test_docker.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # install test requirements
4 | pip install -r requirements.txt
5 |
6 | # create a cache directory
7 | mkdir -p .cache/docker && cd .cache/docker
8 |
9 | #======================================================================
10 | # DEFAULT SETTINGS
11 | cookiecutter ../../ --no-input --overwrite-if-exists && cd project_name
12 |
13 | docker-compose run backend python manage.py check
14 |
--------------------------------------------------------------------------------
/tests/test_generation.py:
--------------------------------------------------------------------------------
1 | def test_default(cookies):
2 | """
3 | Checks if default configuration is working
4 | """
5 | result = cookies.bake()
6 |
7 | assert result.exit_code == 0
8 | assert result.project.isdir()
9 | assert result.exception is None
10 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_size = 2
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_style = space
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.py]
12 | indent_size = 4
13 | max_line_length = 120
14 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/.gitignore:
--------------------------------------------------------------------------------
1 | ### OSX ###
2 | .DS_Store
3 | .AppleDouble
4 | .LSOverride
5 |
6 | ### SublimeText ###
7 | # cache files for sublime text
8 | *.tmlanguage.cache
9 | *.tmPreferences.cache
10 | *.stTheme.cache
11 |
12 | *.sublime-project
13 | *.sublime-workspace
14 |
15 | # sftp configuration file
16 | sftp-config.json
17 |
18 | # Basics
19 | *.py[cod]
20 | __pycache__
21 |
22 | # Logs
23 | logs
24 | *.log
25 | pip-log.txt
26 | npm-debug.log*
27 | yarn-debug.log*
28 | yarn-error.log*
29 |
30 | # Unit test / coverage reports
31 | .coverage
32 | .tox
33 | nosetests.xml
34 | htmlcov
35 |
36 | # Translations
37 | *.mo
38 | *.pot
39 |
40 | # Webpack
41 | webpack-stats.json
42 | dist/
43 |
44 | # Vim
45 | *~
46 | *.swp
47 | *.swo
48 |
49 | # npm
50 | node_modules
51 |
52 | # Compass
53 | .sass-cache
54 |
55 | # User-uploaded media
56 | media/
57 |
58 | # Collected staticfiles
59 | */staticfiles/
60 |
61 | .cache/
62 | **/certs
63 |
64 | # Editor directories and files
65 | .idea
66 | .vscode
67 | *.suo
68 | *.ntvs*
69 | *.njsproj
70 | *.sln
71 | *.sw*
72 |
73 | # VS code
74 | .vscode
75 | .pythonconfig
76 |
77 | # Venv
78 | venv
79 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/.graphqlrc:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "url": "http://localhost:8000/graphql"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/README.md:
--------------------------------------------------------------------------------
1 | {{cookiecutter.project_name}}
2 | {{ '=' * cookiecutter.project_name|length }}
3 |
4 | {{cookiecutter.description}}
5 |
6 |
7 |
8 |
9 |
10 |
11 | ## Development
12 |
13 | Install [Docker](https://docs.docker.com/install/) and [Docker-Compose](https://docs.docker.com/compose/). Start your virtual machines with the following shell command:
14 |
15 | `docker-compose up --build`
16 |
17 | If all works well, you should be able to create an admin account with:
18 |
19 | `docker-compose run backend python manage.py createsuperuser`
20 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.7
2 |
3 | # python envs
4 | ENV PYTHONFAULTHANDLER=1 \
5 | PYTHONUNBUFFERED=1 \
6 | PYTHONHASHSEED=random \
7 | PIP_NO_CACHE_DIR=off \
8 | PIP_DISABLE_PIP_VERSION_CHECK=on \
9 | PIP_DEFAULT_TIMEOUT=100
10 |
11 | # python dependencies
12 | COPY ./requirements.txt /
13 | RUN pip install -r ./requirements.txt
14 |
15 | # upload scripts
16 | COPY ./scripts/entrypoint.sh ./scripts/start.sh ./scripts/gunicorn.sh /
17 |
18 | # Fix windows docker bug, convert CRLF to LF
19 | RUN sed -i 's/\r$//g' /start.sh && chmod +x /start.sh && sed -i 's/\r$//g' /entrypoint.sh && chmod +x /entrypoint.sh &&\
20 | sed -i 's/\r$//g' /gunicorn.sh && chmod +x /gunicorn.sh
21 |
22 | WORKDIR /app
23 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vchaptsev/cookiecutter-django-vue/75c82e0c9fb3a15885f9ead2895bc57fc7a4edf7/{{cookiecutter.project_slug}}/backend/__init__.py
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/apps/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vchaptsev/cookiecutter-django-vue/75c82e0c9fb3a15885f9ead2895bc57fc7a4edf7/{{cookiecutter.project_slug}}/backend/apps/__init__.py
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/apps/users/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vchaptsev/cookiecutter-django-vue/75c82e0c9fb3a15885f9ead2895bc57fc7a4edf7/{{cookiecutter.project_slug}}/backend/apps/users/__init__.py
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/apps/users/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.contrib.auth.models import Group
3 | from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
4 |
5 | from apps.users.models import User
6 | from apps.users.forms import UserChangeForm, UserCreationForm
7 |
8 |
9 | class UserAdmin(BaseUserAdmin):
10 | form = UserChangeForm
11 | add_form = UserCreationForm
12 |
13 | # The fields to be used in displaying the User model.
14 | # These override the definitions on the base UserAdmin
15 | # that reference specific fields on auth.User.
16 | list_display = ['full_name', 'email']
17 | fieldsets = [
18 | ['Auth', {'fields': ['email', 'password']}],
19 | ['Personal info', {'fields': ['last_name', 'first_name', 'avatar']}],
20 | ['Settings', {'fields': ['groups', 'is_admin', 'is_active', 'is_staff', 'is_superuser']}],
21 | ['Important dates', {'fields': ['last_login', 'registered_at']}],
22 | ]
23 | # add_fieldsets is not a standard ModelAdmin attribute. UserAdmin
24 | # overrides get_fieldsets to use this attribute when creating a user.
25 | add_fieldsets = [
26 | [None, {'classes': ['wide'],
27 | 'fields': ['email', 'first_name', 'last_name', 'password1', 'password2']}],
28 | ]
29 | search_fields = ['email']
30 | ordering = ['email']
31 | readonly_fields = ['last_login', 'registered_at']
32 |
33 |
34 | # Now register the new UserAdmin...
35 | admin.site.register(User, UserAdmin)
36 | # Unregister the Group model from admin.
37 | admin.site.unregister(Group)
38 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/apps/users/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class UsersConfig(AppConfig):
5 | name = 'apps.users'
6 | verbose_name = 'Users'
7 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/apps/users/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.contrib.auth.forms import ReadOnlyPasswordHashField
3 |
4 | from apps.users.models import User
5 |
6 |
7 | class UserCreationForm(forms.ModelForm):
8 | """
9 | A form for creating new users.
10 | Includes all the required fields, plus a repeated password
11 | """
12 | password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
13 | password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput)
14 |
15 | class Meta:
16 | model = User
17 | fields = ['email']
18 |
19 | def clean_password2(self):
20 | # Check that the two password entries match
21 | password1 = self.cleaned_data.get("password1")
22 | password2 = self.cleaned_data.get("password2")
23 | if password1 and password2 and password1 != password2:
24 | raise forms.ValidationError("Passwords don't match")
25 | return password2
26 |
27 | def save(self, commit=True):
28 | # Save the provided password in hashed format
29 | user = super().save(commit=False)
30 | user.set_password(self.cleaned_data["password1"])
31 | if commit:
32 | user.save()
33 | return user
34 |
35 |
36 | class UserChangeForm(forms.ModelForm):
37 | """
38 | A form for updating users. Includes all the fields on
39 | the user, but replaces the password field with admin's
40 | password hash display field.
41 | """
42 | help_text = """Raw passwords are not stored, so there is no way to see this user's password,
43 | but you can change the password using this form."""
44 | password = ReadOnlyPasswordHashField(label='Password', help_text=help_text)
45 |
46 | class Meta:
47 | model = User
48 | fields = ['email', 'password', 'is_active', 'is_admin']
49 |
50 | def clean_password(self):
51 | # Regardless of what the user provides, return the initial value.
52 | # This is done here, rather than on the field, because the
53 | # field does not have access to the initial value
54 | return self.initial["password"]
55 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/apps/users/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | import uuid
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | ('auth', '0008_alter_user_username_max_length'),
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='User',
19 | fields=[
20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21 | ('password', models.CharField(max_length=128, verbose_name='password')),
22 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
23 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
24 | ('email', models.EmailField(max_length=255, unique=True, verbose_name='Email')),
25 | ('first_name', models.CharField(default='first', max_length=30, verbose_name='First name')),
26 | ('last_name', models.CharField(default='last', max_length=30, verbose_name='Last name')),
27 | ('avatar', models.ImageField(blank=True, upload_to='', verbose_name='Avatar')),
28 | ('token', models.UUIDField(default=uuid.uuid4, editable=False, verbose_name='Token')),
29 | ('is_admin', models.BooleanField(default=False, verbose_name='Admin')),
30 | ('is_active', models.BooleanField(default=True, verbose_name='Active')),
31 | ('is_staff', models.BooleanField(default=False, verbose_name='Staff')),
32 | ('registered_at', models.DateTimeField(auto_now_add=True, verbose_name='Registered at')),
33 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
34 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
35 | ],
36 | options={
37 | 'verbose_name': 'User',
38 | 'verbose_name_plural': 'Users',
39 | },
40 | ),
41 | ]
42 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/apps/users/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vchaptsev/cookiecutter-django-vue/75c82e0c9fb3a15885f9ead2895bc57fc7a4edf7/{{cookiecutter.project_slug}}/backend/apps/users/migrations/__init__.py
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/apps/users/models.py:
--------------------------------------------------------------------------------
1 | from uuid import uuid4
2 |
3 | from django.contrib.auth.models import BaseUserManager, AbstractBaseUser, PermissionsMixin
4 | from django.db import models
5 | from django.utils import timezone
6 |
7 |
8 | class UserManager(BaseUserManager):
9 | def _create_user(self, email, password, is_staff, is_superuser, **extra_fields):
10 | """
11 | Creates and saves a User with the given username, email and password.
12 | """
13 | user = self.model(email=self.normalize_email(email),
14 | is_active=True,
15 | is_staff=is_staff,
16 | is_superuser=is_superuser,
17 | last_login=timezone.now(),
18 | registered_at=timezone.now(),
19 | **extra_fields)
20 | user.set_password(password)
21 | user.save(using=self._db)
22 | return user
23 |
24 | def create_user(self, email=None, password=None, **extra_fields):
25 | is_staff = extra_fields.pop('is_staff', False)
26 | is_superuser = extra_fields.pop('is_superuser', False)
27 | return self._create_user(email, password, is_staff, is_superuser, **extra_fields)
28 |
29 | def create_superuser(self, email, password, **extra_fields):
30 | return self._create_user(email, password, is_staff=True, is_superuser=True, **extra_fields)
31 |
32 |
33 | class User(AbstractBaseUser, PermissionsMixin):
34 | email = models.EmailField(verbose_name='Email', unique=True, max_length=255)
35 | first_name = models.CharField(verbose_name='First name', max_length=30, default='first')
36 | last_name = models.CharField(verbose_name='Last name', max_length=30, default='last')
37 | avatar = models.ImageField(verbose_name='Avatar', blank=True)
38 | token = models.UUIDField(verbose_name='Token', default=uuid4, editable=False)
39 |
40 | is_admin = models.BooleanField(verbose_name='Admin', default=False)
41 | is_active = models.BooleanField(verbose_name='Active', default=True)
42 | is_staff = models.BooleanField(verbose_name='Staff', default=False)
43 | registered_at = models.DateTimeField(verbose_name='Registered at', auto_now_add=timezone.now)
44 |
45 | # Fields settings
46 | EMAIL_FIELD = 'email'
47 | USERNAME_FIELD = 'email'
48 |
49 | objects = UserManager()
50 |
51 | class Meta:
52 | verbose_name = 'User'
53 | verbose_name_plural = 'Users'
54 |
55 | @property
56 | def full_name(self):
57 | return f'{self.first_name} {self.last_name}'
58 | full_name.fget.short_description = 'Full name'
59 |
60 | @property
61 | def short_name(self):
62 | return f'{self.last_name} {self.first_name[0]}.'
63 | short_name.fget.short_description = 'Short name'
64 |
65 | def get_full_name(self):
66 | return self.full_name
67 |
68 | def get_short_name(self):
69 | return self.short_name
70 |
71 | def __str__(self):
72 | return self.full_name
73 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/apps/users/schema.py:
--------------------------------------------------------------------------------
1 | import graphene
2 | import graphql_jwt
3 | from graphene_django import DjangoObjectType
4 | from uuid import uuid4
5 |
6 | from django.contrib.auth import logout
7 | from django.conf import settings
8 | from django.core.mail import send_mail
9 | from django.template.loader import render_to_string
10 |
11 | from apps.users.models import User
12 |
13 |
14 | class UserType(DjangoObjectType):
15 | """ User type object """
16 |
17 | class Meta:
18 | model = User
19 | only_fields = [
20 | 'id',
21 | 'email',
22 | 'first_name',
23 | 'last_name',
24 | 'registered_at',
25 | ]
26 |
27 |
28 | class Query(object):
29 | user = graphene.Field(UserType, id=graphene.Int(required=True))
30 | users = graphene.List(UserType)
31 | profile = graphene.Field(UserType)
32 |
33 | @staticmethod
34 | def resolve_user(cls, info, **kwargs):
35 | return User.objects.get(id=kwargs.get('id'))
36 |
37 | @staticmethod
38 | def resolve_users(cls, info, **kwargs):
39 | return User.objects.all()
40 |
41 | @staticmethod
42 | def resolve_profile(cls, info, **kwargs):
43 | if info.context.user.is_authenticated:
44 | return info.context.user
45 |
46 |
47 | class Register(graphene.Mutation):
48 | """ Mutation to register a user """
49 | success = graphene.Boolean()
50 | errors = graphene.List(graphene.String)
51 |
52 | class Arguments:
53 | email = graphene.String(required=True)
54 | password = graphene.String(required=True)
55 | first_name = graphene.String(required=True)
56 | last_name = graphene.String(required=True)
57 |
58 | def mutate(self, info, email, password, first_name, last_name):
59 | if User.objects.filter(email__iexact=email).exists():
60 | errors = ['emailAlreadyExists']
61 | return Register(success=False, errors=errors)
62 |
63 | # create user
64 | user = User.objects.create(
65 | email=email,
66 | last_name=last_name,
67 | first_name=first_name,
68 | )
69 | user.set_password(password)
70 | user.save()
71 | return Register(success=True)
72 |
73 |
74 | class Logout(graphene.Mutation):
75 | """ Mutation to logout a user """
76 | success = graphene.Boolean()
77 |
78 | def mutate(self, info):
79 | logout(info.context)
80 | return Logout(success=True)
81 |
82 |
83 | class ResetPassword(graphene.Mutation):
84 | """ Mutation for requesting a password reset email """
85 | success = graphene.Boolean()
86 |
87 | class Arguments:
88 | email = graphene.String(required=True)
89 |
90 | def mutate(self, info, email):
91 | try:
92 | user = User.objects.get(email=email)
93 | except User.DoesNotExist:
94 | errors = ['emailDoesNotExists']
95 | return ResetPassword(success=False, errors=errors)
96 |
97 | params = {
98 | 'user': user,
99 | 'DOMAIN': settings.DOMAIN,
100 | }
101 | send_mail(
102 | subject='Password reset',
103 | message=render_to_string('mail/password_reset.txt', params),
104 | from_email=settings.DEFAULT_FROM_EMAIL,
105 | recipient_list=[email],
106 | )
107 | return ResetPassword(success=True)
108 |
109 |
110 | class ResetPasswordConfirm(graphene.Mutation):
111 | """ Mutation for requesting a password reset email """
112 | success = graphene.Boolean()
113 | errors = graphene.List(graphene.String)
114 |
115 | class Arguments:
116 | token = graphene.String(required=True)
117 | password = graphene.String(required=True)
118 |
119 | def mutate(self, info, token, password):
120 | try:
121 | user = User.objects.get(token=token)
122 | except User.DoesNotExist:
123 | errors = ['wrongToken']
124 | return ResetPasswordConfirm(success=False, errors=errors)
125 |
126 | user.set_password(password)
127 | user.token = uuid4()
128 | user.save()
129 | return ResetPasswordConfirm(success=True)
130 |
131 |
132 | class Mutation(object):
133 | login = graphql_jwt.ObtainJSONWebToken.Field()
134 | verify_token = graphql_jwt.Verify.Field()
135 | refresh_token = graphql_jwt.Refresh.Field()
136 | register = Register.Field()
137 | logout = Logout.Field()
138 | reset_password = ResetPassword.Field()
139 | reset_password_confirm = ResetPasswordConfirm.Field()
140 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/apps/users/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 |
3 | from django.conf import settings
4 |
5 | from apps.users.models import User
6 |
7 |
8 | class UserSerializer(serializers.ModelSerializer):
9 | registered_at = serializers.DateTimeField(format='%H:%M %d.%m.%Y', read_only=True)
10 |
11 | avatar = serializers.SerializerMethodField(read_only=True)
12 | full_name = serializers.SerializerMethodField(read_only=True)
13 | short_name = serializers.SerializerMethodField(read_only=True)
14 |
15 | def get_avatar(self, obj):
16 | return obj.avatar.url if obj.avatar else settings.STATIC_URL + 'images/default_avatar.png'
17 |
18 | def get_full_name(self, obj):
19 | return obj.full_name
20 |
21 | def get_short_name(self, obj):
22 | return obj.short_name
23 |
24 | class Meta:
25 | model = User
26 | fields = ['email', 'avatar', 'full_name', 'short_name', 'registered_at']
27 |
28 |
29 | class UserWriteSerializer(serializers.ModelSerializer):
30 |
31 | class Meta:
32 | model = User
33 | fields = ['email', 'password', 'first_name', 'last_name']
34 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/apps/users/templates/mail/password_reset.txt:
--------------------------------------------------------------------------------
1 | Hey, {% raw %}{{ user.first_name }}{% endraw %}!
2 |
3 | A password reset was requested for your account ({% raw %}{{ user.email }}{% endraw %}) on {{cookiecutter.project_name}}.
4 | If you did not authorize this, you may simply ignore this email.
5 |
6 | To continue with your password reset, simply click the link below, and you will be able to change your password.
7 |
8 | {% raw %}{{ DOMAIN }}/password_change/{{ user.token }}{% endraw %}
9 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/apps/users/views.py:
--------------------------------------------------------------------------------
1 | import requests
2 | from uuid import uuid4
3 |
4 | from django.contrib.auth import authenticate, login
5 | from django.conf import settings
6 | from django.core.mail import send_mail
7 | from django.template.loader import render_to_string
8 |
9 | from rest_framework import viewsets, status
10 | from rest_framework.decorators import action
11 | from rest_framework.response import Response
12 |
13 | from apps.users.models import User
14 | from apps.users.serializers import UserSerializer, UserWriteSerializer
15 |
16 |
17 | class UserViewSet(viewsets.ModelViewSet):
18 | queryset = User.objects.all()
19 | serializer_class = UserSerializer
20 | permission_classes = []
21 |
22 | def get_serializer_class(self):
23 | if self.action in ['list', 'retrieve']:
24 | return UserSerializer
25 | return UserWriteSerializer
26 |
27 | def perform_create(self, serializer):
28 | user = serializer.save()
29 | user.set_password(self.request.data.get('password'))
30 | user.save()
31 |
32 | def perform_update(self, serializer):
33 | user = serializer.save()
34 | if 'password' in self.request.data:
35 | user.set_password(self.request.data.get('password'))
36 | user.save()
37 |
38 | def perform_destroy(self, instance):
39 | instance.is_active = False
40 | instance.save()
41 |
42 | @action(methods=['GET'], detail=False)
43 | def profile(self, request):
44 | if request.user.is_authenticated:
45 | serializer = self.serializer_class(request.user)
46 | return Response(status=status.HTTP_200_OK, data=serializer.data)
47 | return Response(status=status.HTTP_401_UNAUTHORIZED)
48 |
49 | @action(methods=['POST'], detail=False)
50 | def login(self, request, format=None):
51 | email = request.data.get('email', None)
52 | password = request.data.get('password', None)
53 | user = authenticate(username=email, password=password)
54 |
55 | if user:
56 | login(request, user)
57 | return Response(status=status.HTTP_200_OK)
58 | return Response(status=status.HTTP_404_NOT_FOUND)
59 |
60 | @action(methods=['POST'], detail=False)
61 | def register(self, request):
62 | last_name = request.data.get('last_name', None)
63 | first_name = request.data.get('first_name', None)
64 | email = request.data.get('email', None)
65 | password = request.data.get('password', None)
66 |
67 | if User.objects.filter(email__iexact=email).exists():
68 | return Response({'status': 210})
69 |
70 | # user creation
71 | user = User.objects.create(
72 | email=email,
73 | password=password,
74 | last_name=last_name,
75 | first_name=first_name,
76 | is_admin=False,
77 | )
78 | return Response(UserSerializer(user).data, status=status.HTTP_201_CREATED)
79 |
80 | @action(methods=['POST'], detail=False)
81 | def password_reset(self, request, format=None):
82 | if User.objects.filter(email=request.data['email']).exists():
83 | user = User.objects.get(email=request.data['email'])
84 | params = {'user': user, 'DOMAIN': settings.DOMAIN}
85 | send_mail(
86 | subject='Password reset',
87 | message=render_to_string('mail/password_reset.txt', params),
88 | from_email=settings.DEFAULT_FROM_EMAIL,
89 | recipient_list=[request.data['email']],
90 | )
91 | return Response(status=status.HTTP_200_OK)
92 | else:
93 | return Response(status=status.HTTP_404_NOT_FOUND)
94 |
95 | @action(methods=['POST'], detail=False)
96 | def password_change(self, request, format=None):
97 | if User.objects.filter(token=request.data['token']).exists():
98 | user = User.objects.get(token=request.data['token'])
99 | user.set_password(request.data['password'])
100 | user.token = uuid4()
101 | user.save()
102 | return Response(status=status.HTTP_200_OK)
103 | else:
104 | return Response(status=status.HTTP_404_NOT_FOUND)
105 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/config/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vchaptsev/cookiecutter-django-vue/75c82e0c9fb3a15885f9ead2895bc57fc7a4edf7/{{cookiecutter.project_slug}}/backend/config/__init__.py
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/config/api.py:
--------------------------------------------------------------------------------
1 | from rest_framework import routers
2 | from apps.users.views import UserViewSet
3 |
4 | # Settings
5 | api = routers.DefaultRouter()
6 | api.trailing_slash = '/?'
7 |
8 | # Users API
9 | api.register(r'users', UserViewSet)
10 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/config/schema.py:
--------------------------------------------------------------------------------
1 | import graphene
2 | from graphene_django.debug import DjangoDebug
3 |
4 | import apps.users.schema
5 |
6 |
7 | class Query(apps.users.schema.Query, graphene.ObjectType):
8 | debug = graphene.Field(DjangoDebug, name='__debug')
9 |
10 |
11 | class Mutation(apps.users.schema.Mutation, graphene.ObjectType):
12 | ...
13 |
14 |
15 | schema = graphene.Schema(query=Query, mutation=Mutation)
16 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/config/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for {{ cookiecutter.project_name }} project.
3 |
4 | For more information on this file, see
5 | https://docs.djangoproject.com/en/dev/topics/settings/
6 |
7 | For the full list of settings and their values, see
8 | https://docs.djangoproject.com/en/dev/ref/settings/
9 | """
10 | import environ
11 | from datetime import timedelta
12 | {% if cookiecutter.use_sentry == 'y' %}
13 | import sentry_sdk
14 | from sentry_sdk.integrations.django import DjangoIntegration
15 | {% endif %}
16 | ROOT_DIR = environ.Path(__file__) - 2
17 |
18 | # Load operating system environment variables and then prepare to use them
19 | env = environ.Env()
20 |
21 | {% if cookiecutter.use_sentry == 'y' %}
22 | # See https://docs.sentry.io/platforms/python/guides/django/
23 |
24 | # Sentry Configuration
25 | sentry_sdk.init(
26 | dsn=env.str('SENTRY_DSN'),
27 | integrations=[DjangoIntegration()],
28 |
29 | # Set traces_sample_rate to 1.0 to capture 100%
30 | # of transactions for performance monitoring.
31 | # We recommend adjusting this value in production,
32 | traces_sample_rate=1.0,
33 |
34 | # If you wish to associate users to errors (assuming you are using
35 | # django.contrib.auth) you may enable sending PII data.
36 | send_default_pii=True,
37 |
38 | # By default the SDK will try to use the SENTRY_RELEASE
39 | # environment variable, or infer a git commit
40 | # SHA as release, however you may want to set
41 | # something more human-readable.
42 | # release="myapp@1.0.0",
43 | )
44 | {% endif %}
45 |
46 | # APP CONFIGURATION
47 | # ------------------------------------------------------------------------------
48 | DJANGO_APPS = [
49 | 'django.contrib.auth',
50 | 'django.contrib.contenttypes',
51 | 'django.contrib.sessions',
52 | 'django.contrib.messages',
53 | 'django.contrib.staticfiles',
54 | 'django.contrib.admin',
55 | ]
56 |
57 | THIRD_PARTY_APPS = [
58 | {% if cookiecutter.api == 'REST' %}
59 | 'rest_framework',
60 | {% elif cookiecutter.api == 'GraphQL' %}
61 | 'graphene_django',
62 | {% endif %}
63 | 'django_extensions',
64 | ]
65 |
66 | LOCAL_APPS = [
67 | 'apps.users',
68 | ]
69 |
70 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
71 | INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
72 |
73 | # MIDDLEWARE CONFIGURATION
74 | # ------------------------------------------------------------------------------
75 | MIDDLEWARE = [
76 | 'django.middleware.security.SecurityMiddleware',
77 | 'django.contrib.sessions.middleware.SessionMiddleware',
78 | 'django.middleware.common.CommonMiddleware',
79 | 'django.middleware.csrf.CsrfViewMiddleware',
80 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
81 | 'django.contrib.messages.middleware.MessageMiddleware',
82 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
83 | ]
84 |
85 | # DEBUG
86 | # ------------------------------------------------------------------------------
87 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#debug
88 | DEBUG = env.bool('DEBUG')
89 | SECRET_KEY = env.str('SECRET_KEY')
90 |
91 | # DOMAINS
92 | ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=['*'])
93 | DOMAIN = env.str('DOMAIN')
94 |
95 | # EMAIL CONFIGURATION
96 | # ------------------------------------------------------------------------------
97 | EMAIL_PORT = env.int('EMAIL_PORT', default='1025')
98 | EMAIL_HOST = env.str('EMAIL_HOST', default='mailhog')
99 |
100 | # MANAGER CONFIGURATION
101 | # ------------------------------------------------------------------------------
102 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#admins
103 | ADMINS = [
104 | ('{{ cookiecutter.author }}', '{{ cookiecutter.email }}'),
105 | ]
106 |
107 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#managers
108 | MANAGERS = ADMINS
109 |
110 | # DATABASE CONFIGURATION
111 | # ------------------------------------------------------------------------------
112 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#databases
113 | DATABASES = {
114 | 'default': {
115 | 'ENGINE': 'django.db.backends.postgresql_psycopg2',
116 | 'NAME': env.str('POSTGRES_DB'),
117 | 'USER': env.str('POSTGRES_USER'),
118 | 'PASSWORD': env.str('POSTGRES_PASSWORD'),
119 | 'HOST': 'postgres',
120 | 'PORT': 5432,
121 | },
122 | }
123 |
124 | # GENERAL CONFIGURATION
125 | # ------------------------------------------------------------------------------
126 | # Local time zone for this installation. Choices can be found here:
127 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
128 | # although not all choices may be available on all operating systems.
129 | # In a Windows environment this must be set to your system time zone.
130 | TIME_ZONE = 'UTC'
131 |
132 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#language-code
133 | LANGUAGE_CODE = 'en-us'
134 |
135 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n
136 | USE_I18N = True
137 |
138 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#use-l10n
139 | USE_L10N = True
140 |
141 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#use-tz
142 | USE_TZ = True
143 |
144 | # STATIC FILE CONFIGURATION
145 | # ------------------------------------------------------------------------------
146 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#static-root
147 | STATIC_ROOT = str(ROOT_DIR('staticfiles'))
148 |
149 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url
150 | STATIC_URL = '/staticfiles/'
151 |
152 | # See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS
153 | STATICFILES_DIRS = [
154 | str(ROOT_DIR('static')),
155 | ]
156 |
157 | # See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders
158 | STATICFILES_FINDERS = [
159 | 'django.contrib.staticfiles.finders.FileSystemFinder',
160 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
161 | ]
162 |
163 | # MEDIA CONFIGURATION
164 | # ------------------------------------------------------------------------------
165 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#media-root
166 | MEDIA_ROOT = str(ROOT_DIR('media'))
167 |
168 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#media-url
169 | MEDIA_URL = '/media/'
170 |
171 | # URL Configuration
172 | # ------------------------------------------------------------------------------
173 | ROOT_URLCONF = 'config.urls'
174 |
175 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application
176 | WSGI_APPLICATION = 'config.wsgi.application'
177 |
178 | # TEMPLATE CONFIGURATION
179 | # ------------------------------------------------------------------------------
180 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#templates
181 | TEMPLATES = [
182 | {
183 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
184 | 'DIRS': STATICFILES_DIRS,
185 | 'OPTIONS': {
186 | 'debug': DEBUG,
187 | 'loaders': [
188 | 'django.template.loaders.filesystem.Loader',
189 | 'django.template.loaders.app_directories.Loader',
190 | ],
191 | 'context_processors': [
192 | 'django.template.context_processors.debug',
193 | 'django.template.context_processors.request',
194 | 'django.contrib.auth.context_processors.auth',
195 | 'django.template.context_processors.i18n',
196 | 'django.template.context_processors.media',
197 | 'django.template.context_processors.static',
198 | 'django.template.context_processors.tz',
199 | 'django.contrib.messages.context_processors.messages',
200 | ],
201 | },
202 | },
203 | ]
204 |
205 |
206 | # PASSWORD STORAGE SETTINGS
207 | # ------------------------------------------------------------------------------
208 | # See https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers
209 | PASSWORD_HASHERS = [
210 | 'django.contrib.auth.hashers.PBKDF2PasswordHasher',
211 | 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
212 | 'django.contrib.auth.hashers.Argon2PasswordHasher',
213 | 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
214 | 'django.contrib.auth.hashers.BCryptPasswordHasher',
215 | ]
216 |
217 | # PASSWORD VALIDATION
218 | # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators
219 | # ------------------------------------------------------------------------------
220 | AUTH_PASSWORD_VALIDATORS = [
221 | {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
222 | {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
223 | {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
224 | {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
225 | ]
226 |
227 | # AUTHENTICATION CONFIGURATION
228 | # ------------------------------------------------------------------------------
229 | AUTHENTICATION_BACKENDS = [
230 | {% if cookiecutter.api == 'GraphQL' %}'graphql_jwt.backends.JSONWebTokenBackend',{% endif %}
231 | 'django.contrib.auth.backends.ModelBackend',
232 | ]
233 |
234 | # Custom user app defaults
235 | # Select the correct user model
236 | AUTH_USER_MODEL = 'users.User'
237 |
238 |
239 | {% if cookiecutter.api == 'REST' %}
240 | # DJANGO REST FRAMEWORK
241 | # ------------------------------------------------------------------------------
242 | REST_FRAMEWORK = {
243 | 'UPLOADED_FILES_USE_URL': False,
244 | 'DEFAULT_AUTHENTICATION_CLASSES': [
245 | 'rest_framework.authentication.SessionAuthentication',
246 | 'rest_framework.authentication.BasicAuthentication'
247 | ],
248 | 'DEFAULT_PERMISSION_CLASSES': [],
249 | 'DEFAULT_PARSER_CLASSES': [
250 | 'rest_framework.parsers.JSONParser',
251 | 'rest_framework.parsers.FormParser',
252 | 'rest_framework.parsers.MultiPartParser',
253 | 'rest_framework.parsers.FileUploadParser'
254 | ]
255 | }
256 | {% elif cookiecutter.api == 'GraphQL' %}
257 | # Graphene
258 | GRAPHENE = {
259 | 'SCHEMA': 'config.schema.schema',
260 | 'MIDDLEWARE': [
261 | 'graphql_jwt.middleware.JSONWebTokenMiddleware',
262 | ],
263 | }
264 |
265 | if DEBUG:
266 | GRAPHENE['MIDDLEWARE'] = [
267 | 'graphene_django.debug.DjangoDebugMiddleware',
268 | 'graphql_jwt.middleware.JSONWebTokenMiddleware',
269 | ]
270 |
271 | GRAPHQL_JWT = {
272 | 'JWT_EXPIRATION_DELTA': timedelta(days=30),
273 | 'JWT_AUTH_HEADER': 'authorization',
274 | 'JWT_AUTH_HEADER_PREFIX': 'Bearer',
275 | }
276 | {% endif %}
277 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/config/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from django.contrib import admin
3 | from django.contrib.auth import logout
4 | {% if cookiecutter.api == 'REST' %}
5 | from django.conf.urls import include
6 |
7 | from config.api import api
8 | {% elif cookiecutter.api == 'GraphQL' %}
9 | from django.conf import settings
10 | from django.views.decorators.csrf import csrf_exempt
11 |
12 | from graphene_django.views import GraphQLView
13 | {% endif %}
14 |
15 |
16 | urlpatterns = [
17 | path('admin/', admin.site.urls, name='admin'),
18 | path('logout/', logout, {'next_page': '/'}, name='logout'),
19 | {% if cookiecutter.api == 'REST' %}
20 | path('api/', include(api.urls)),
21 | path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
22 | {% elif cookiecutter.api == 'GraphQL' %}
23 | path('graphql', csrf_exempt(GraphQLView.as_view(graphiql=settings.DEBUG))),
24 | {% endif %}
25 | ]
26 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/config/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for serenity-escrow project.
3 |
4 | This module contains the WSGI application used by Django's development server
5 | and any production WSGI deployments. It should expose a module-level variable
6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
7 | this application via the ``WSGI_APPLICATION`` setting.
8 | """
9 |
10 | import os
11 | import sys
12 |
13 | from django.core.wsgi import get_wsgi_application
14 |
15 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
16 | # This allows easy placement of apps within the interior serenity directory.
17 | current_path = os.path.dirname(os.path.abspath(__file__)).replace('/config', '')
18 | sys.path.append(current_path)
19 | sys.path.append(os.path.join(current_path, 'apps'))
20 |
21 | # This application object is used by any WSGI server configured to use this
22 | # file. This includes Django's development server, if the WSGI_APPLICATION
23 | # setting points here.
24 | application = get_wsgi_application()
25 |
26 | {% if cookiecutter.use_sentry == 'y' -%}
27 | from raven.contrib.django.raven_compat.middleware.wsgi import Sentry
28 | application = Sentry(application)
29 | {% endif %}
30 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == '__main__':
6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
7 |
8 | try:
9 | from django.core.management import execute_from_command_line
10 | except ImportError:
11 | # The above import may fail for some other reason. Ensure that the
12 | # issue is really that Django is missing to avoid masking other
13 | # exceptions on Python 2.
14 | try:
15 | import django # noqa
16 | except ImportError:
17 | raise ImportError(
18 | """Couldn't import Django. Are you sure it's installed and
19 | available on your PYTHONPATH environment variable? Did you
20 | forget to activate a virtual environment?""",
21 | )
22 | raise
23 |
24 | current_path = os.path.dirname(os.path.abspath(__file__))
25 | sys.path.append(current_path)
26 | sys.path.append(os.path.join(current_path, 'apps'))
27 | execute_from_command_line(sys.argv)
28 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/requirements.txt:
--------------------------------------------------------------------------------
1 | autopep8==1.5.7
2 | attrs==20.3.0
3 | backcall==0.2.0
4 | certifi==2020.12.5
5 | chardet==4.0.0
6 | decorator==4.0.7
7 | Django==3.2
8 | django-environ==0.4.5
9 | django-extensions==3.1.3
10 | django-redis==4.12.1
11 | flake8==3.9.1
12 | flake8-commas==2.0.0
13 | flake8-mypy==17.8.0
14 | gevent==21.1.2
15 | greenlet==1.0.0
16 | gunicorn==20.1.0
17 | idna==3.1
18 | ipython==7.30.0
19 | ipython-genutils==0.2.0
20 | jedi==0.18.0
21 | mccabe==0.6.1
22 | mypy==0.812
23 | pexpect==4.8.0
24 | pickleshare==0.7.5
25 | Pillow==8.2.0
26 | prompt-toolkit==3.0.18
27 | psycopg2-binary==2.8.6
28 | ptyprocess==0.7.0
29 | pycodestyle==2.7.0
30 | pyflakes==2.3.1
31 | Pygments==2.8.1
32 | pytz==2021.1
33 | redis==3.5.3
34 | requests==2.25.1
35 | rope==0.19.0
36 | simplegeneric==0.8.1
37 | six==1.15.0
38 | traitlets==5.0.5
39 | typed-ast==1.4.3
40 | urllib3==1.26.4
41 | wcwidth==0.2.5
42 | Werkzeug==1.0.1
43 | {% if cookiecutter.use_sentry == 'y' %}
44 | sentry-sdk==1.0.0
45 | {% endif %}
46 | {% if cookiecutter.api == 'REST' %}
47 | djangorestframework==3.12.4
48 | {% elif cookiecutter.api == 'GraphQL' %}
49 | django-graphql-jwt==0.3.2
50 | graphene==2.1.8
51 | graphene-django==2.15.0
52 | {% endif %}
53 |
54 |
55 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/scripts/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -o errexit
4 | set -o pipefail
5 | cmd="$@"
6 |
7 | function postgres_ready(){
8 | python << END
9 | import sys
10 | import psycopg2
11 | import environ
12 |
13 | try:
14 | env = environ.Env()
15 | dbname = env.str('POSTGRES_DB')
16 | user = env.str('POSTGRES_USER')
17 | password = env.str('POSTGRES_PASSWORD')
18 | conn = psycopg2.connect(dbname=dbname, user=user, password=password, host='postgres', port=5432)
19 | except psycopg2.OperationalError:
20 | sys.exit(-1)
21 | sys.exit(0)
22 | END
23 | }
24 |
25 | until postgres_ready; do
26 | >&2 echo "Postgres is unavailable - sleeping"
27 | sleep 1
28 | done
29 |
30 | >&2 echo "Postgres is up - continuing..."
31 | exec $cmd
32 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/scripts/gunicorn.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -o errexit
4 | set -o pipefail
5 | set -o nounset
6 |
7 | python manage.py migrate
8 | python manage.py collectstatic --noinput --verbosity 0
9 | gunicorn config.wsgi -w 4 --worker-class gevent -b 0.0.0.0:8000 --chdir=/app
10 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/scripts/start.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -o errexit
4 | set -o pipefail
5 | set -o nounset
6 | set -o xtrace
7 |
8 | python manage.py migrate
9 | python manage.py collectstatic --noinput --verbosity 0
10 | python manage.py runserver_plus 0.0.0.0:8000
11 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/static/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vchaptsev/cookiecutter-django-vue/75c82e0c9fb3a15885f9ead2895bc57fc7a4edf7/{{cookiecutter.project_slug}}/backend/static/.gitkeep
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/docker-compose-prod.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 |
3 | volumes:
4 | postgres_data: {}
5 |
6 |
7 | services:
8 | backend:
9 | build:
10 | context: ./backend
11 | depends_on:
12 | - postgres
13 | volumes:
14 | - ./backend:/app
15 | command: /gunicorn.sh
16 | entrypoint: /entrypoint.sh
17 | restart: on-failure
18 | env_file: .env
19 |
20 | postgres:
21 | image: postgres:10-alpine
22 | volumes:
23 | - postgres_data:/var/lib/postgresql/data
24 | env_file: .env
25 |
26 | nginx:
27 | build:
28 | context: .
29 | dockerfile: nginx/Dockerfile
30 | ports:
31 | - "8000:80"
32 | depends_on:
33 | - backend
34 | volumes:
35 | - ./backend/media/:/media/
36 | - ./backend/staticfiles/:/staticfiles/
37 | - ./nginx/prod.conf:/etc/nginx/nginx.conf:ro
38 |
39 |
40 | {% if cookiecutter.backups == 'y' %}
41 | backups:
42 | image: prodrigestivill/postgres-backup-local
43 | restart: on-failure
44 | depends_on:
45 | - postgres
46 | volumes:
47 | - /tmp/backups/:/backups/
48 | {% endif %}
49 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 |
3 | volumes:
4 | {{cookiecutter.project_slug}}_data: {}
5 |
6 | services:
7 | backend:
8 | build:
9 | context: ./backend
10 | depends_on:
11 | - postgres
12 | volumes:
13 | - ./backend:/app
14 | command: /start.sh
15 | entrypoint: /entrypoint.sh
16 | restart: on-failure
17 | env_file: .env
18 |
19 | frontend:
20 | image: node:10-alpine
21 | command: npm run serve
22 | volumes:
23 | - ./.env:/app/.env:ro
24 | - ./frontend:/app
25 | working_dir: /app
26 | restart: on-failure
27 |
28 | postgres:
29 | image: postgres:10-alpine
30 | volumes:
31 | - {{cookiecutter.project_slug}}_data:/var/lib/postgresql/data
32 | env_file: .env
33 |
34 | {% if cookiecutter.use_mailhog == 'y' %}
35 | mailhog:
36 | image: mailhog/mailhog
37 | ports:
38 | - "8025:8025"
39 | logging:
40 | driver: none
41 | {% endif %}
42 |
43 | nginx:
44 | image: nginx:alpine
45 | ports:
46 | - "8000:80"
47 | depends_on:
48 | - backend
49 | volumes:
50 | - ./backend/media/:/media/
51 | - ./backend/staticfiles/:/staticfiles/
52 | - ./nginx/dev.conf:/etc/nginx/nginx.conf:ro
53 | logging:
54 | driver: none
55 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/env.example:
--------------------------------------------------------------------------------
1 | # NOTICE:
2 | # Vue app will only detect VUE_APP_* envs
3 |
4 | # Django
5 | DEBUG=True
6 | SECRET_KEY=CHANGEME!!!
7 |
8 | DOMAIN=http://localhost:8000
9 | ALLOWED_HOSTS=*
10 |
11 | # Email settings, defaults to 1025 and mailhog
12 | #EMAIL_PORT=25
13 | #EMAIL_HOST=localhost
14 |
15 | # PostgreSQL
16 | POSTGRES_DB={{cookiecutter.project_slug}}
17 | POSTGRES_PASSWORD=mysecretpass
18 | POSTGRES_USER=postgresuser
19 |
20 | {% if cookiecutter.use_sentry == 'y' -%}
21 | # Sentry
22 | SENTRY_DSN=
23 | SENTRY_PUBLIC_DSN=
24 | VUE_APP_SENTRY_PUBLIC_DSN=
25 | {% endif %}
26 |
27 | {% if cookiecutter.analytics == 'Google Analytics' -%}
28 | # Google Analytics
29 | VUE_APP_GOOGLE_ANALYTICS=UA-XXXXXXXXX-X
30 | {% endif %}
31 |
32 | {% if cookiecutter.analytics == 'Yandex Metrika' -%}
33 | # Yandex Metrika
34 | VUE_APP_YANDEX_METRIKA=
35 | {% endif %}
36 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/app'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/package-lock.json:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vchaptsev/cookiecutter-django-vue/75c82e0c9fb3a15885f9ead2895bc57fc7a4edf7/{{cookiecutter.project_slug}}/frontend/package-lock.json
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "npm i && vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint",
9 | "test:unit": "vue-cli-service test:unit"
10 | },
11 | "dependencies": {
12 | "axios": "^0.18.0",
13 | "register-service-worker": "^1.5.2",
14 | "vue": "^2.5.17",
15 | {% if cookiecutter.api == "GraphQL" %}"vue-apollo": "^3.0.0-beta.20",{% endif %}
16 | "vue-router": "^3.0.1",
17 | {% if cookiecutter.use_sentry == 'y' %}
18 | "@sentry/vue": "^6.3.5",
19 | "@sentry/tracing": "^6.3.5",
20 | {% endif %}
21 | {% if cookiecutter.analytics == "Google Analytics" %}"vue-analytics": "^5.16.0",{% endif %}
22 | {% if cookiecutter.analytics == "Yandex Metrika" %}{% endif %}
23 | "vuex": "^3.0.1"
24 | },
25 | "devDependencies": {
26 | "@vue/cli-plugin-babel": "^3.0.1",
27 | "@vue/cli-plugin-eslint": "^3.0.1",
28 | "@vue/cli-plugin-pwa": "^3.0.1",
29 | "@vue/cli-plugin-unit-jest": "^3.0.1",
30 | "@vue/cli-service": "^3.0.1",
31 | "@vue/eslint-config-standard": "^3.0.1",
32 | "@vue/test-utils": "^1.0.0-beta.24",
33 | "babel-core": "^6.26.3",
34 | "babel-jest": "^23.4.2",
35 | "node-sass": "^4.9.3",
36 | {% if cookiecutter.api == "GraphQL" %}
37 | "graphql-tag": "^2.9.2",
38 | "vue-cli-plugin-apollo": "^0.16.4",
39 | {% endif %}
40 | "sass-loader": "^7.1.0",
41 | "vue-template-compiler": "^2.5.17"
42 | },
43 | "eslintConfig": {
44 | "root": true,
45 | "env": {
46 | "node": true
47 | },
48 | "extends": [
49 | "plugin:vue/essential",
50 | "@vue/standard"
51 | ],
52 | "rules": {},
53 | "parserOptions": {
54 | "parser": "babel-eslint"
55 | }
56 | },
57 | "postcss": {
58 | "plugins": {
59 | "autoprefixer": {}
60 | }
61 | },
62 | "browserslist": [
63 | "> 1%",
64 | "last 2 versions",
65 | "not ie <= 8"
66 | ],
67 | "jest": {
68 | "moduleFileExtensions": [
69 | "js",
70 | "jsx",
71 | "json",
72 | "vue"
73 | ],
74 | "transform": {
75 | "^.+\\.vue$": "vue-jest",
76 | ".+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$": "jest-transform-stub",
77 | "^.+\\.jsx?$": "babel-jest"
78 | },
79 | "moduleNameMapper": {
80 | "^@/(.*)$": "/src/$1"
81 | },
82 | "snapshotSerializers": [
83 | "jest-serializer-vue"
84 | ],
85 | "testMatch": [
86 | "/(tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx))"
87 | ]
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/img/icons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vchaptsev/cookiecutter-django-vue/75c82e0c9fb3a15885f9ead2895bc57fc7a4edf7/{{cookiecutter.project_slug}}/frontend/public/img/icons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/img/icons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vchaptsev/cookiecutter-django-vue/75c82e0c9fb3a15885f9ead2895bc57fc7a4edf7/{{cookiecutter.project_slug}}/frontend/public/img/icons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vchaptsev/cookiecutter-django-vue/75c82e0c9fb3a15885f9ead2895bc57fc7a4edf7/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-120x120.png
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vchaptsev/cookiecutter-django-vue/75c82e0c9fb3a15885f9ead2895bc57fc7a4edf7/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-152x152.png
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vchaptsev/cookiecutter-django-vue/75c82e0c9fb3a15885f9ead2895bc57fc7a4edf7/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-180x180.png
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vchaptsev/cookiecutter-django-vue/75c82e0c9fb3a15885f9ead2895bc57fc7a4edf7/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-60x60.png
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vchaptsev/cookiecutter-django-vue/75c82e0c9fb3a15885f9ead2895bc57fc7a4edf7/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-76x76.png
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vchaptsev/cookiecutter-django-vue/75c82e0c9fb3a15885f9ead2895bc57fc7a4edf7/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon.png
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/img/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vchaptsev/cookiecutter-django-vue/75c82e0c9fb3a15885f9ead2895bc57fc7a4edf7/{{cookiecutter.project_slug}}/frontend/public/img/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/img/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vchaptsev/cookiecutter-django-vue/75c82e0c9fb3a15885f9ead2895bc57fc7a4edf7/{{cookiecutter.project_slug}}/frontend/public/img/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/img/icons/msapplication-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vchaptsev/cookiecutter-django-vue/75c82e0c9fb3a15885f9ead2895bc57fc7a4edf7/{{cookiecutter.project_slug}}/frontend/public/img/icons/msapplication-icon-144x144.png
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/img/icons/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vchaptsev/cookiecutter-django-vue/75c82e0c9fb3a15885f9ead2895bc57fc7a4edf7/{{cookiecutter.project_slug}}/frontend/public/img/icons/mstile-150x150.png
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/img/icons/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
150 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | {{ cookiecutter.project_name }}
15 |
16 |
17 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "{{ cookiecutter.project_slug }}",
3 | "short_name": "{{ cookiecutter.project_slug }}",
4 | "icons": [
5 | {
6 | "src": "/img/icons/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/img/icons/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "start_url": "",
17 | "display": "standalone",
18 | "background_color": "#000000",
19 | "theme_color": "#4DBA87"
20 | }
21 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/apollo.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueApollo from 'vue-apollo'
3 | import { createApolloClient, restartWebsockets } from 'vue-cli-plugin-apollo/graphql-client'
4 |
5 | // Install the vue plugin
6 | Vue.use(VueApollo)
7 |
8 | // Name of the localStorage item
9 | const AUTH_TOKEN = 'jwt-token'
10 |
11 | // Config
12 | const defaultOptions = {
13 | httpEndpoint: '/graphql',
14 | wsEndpoint: null,
15 | tokenName: AUTH_TOKEN,
16 | persisting: false,
17 | websocketsOnly: false,
18 | ssr: false
19 | }
20 |
21 | // Call this in the Vue app file
22 | export function createProvider (options = {}) {
23 | // Create apollo client
24 | const { apolloClient, wsClient } = createApolloClient({
25 | ...defaultOptions,
26 | ...options
27 | })
28 | apolloClient.wsClient = wsClient
29 |
30 | // Create vue apollo provider
31 | const apolloProvider = new VueApollo({
32 | defaultClient: apolloClient,
33 | defaultOptions: {
34 | $query: {
35 | loadingKey: 'loading',
36 | fetchPolicy: 'cache-and-network'
37 | }
38 | },
39 | errorHandler (error) {
40 | // eslint-disable-next-line no-console
41 | console.log('%cError', 'background: red; color: white; padding: 2px 4px; border-radius: 3px; font-weight: bold;', error.message)
42 | }
43 | })
44 |
45 | return apolloProvider
46 | }
47 |
48 | // Manually call this when user log in
49 | export async function onLogin (apolloClient, token) {
50 | localStorage.setItem(AUTH_TOKEN, token)
51 | if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient)
52 | try {
53 | await apolloClient.resetStore()
54 | } catch (e) {
55 | // eslint-disable-next-line no-console
56 | console.log('%cError on cache reset (login)', 'color: orange;', e.message)
57 | }
58 | }
59 |
60 | // Manually call this when user log out
61 | export async function onLogout (apolloClient) {
62 | localStorage.removeItem(AUTH_TOKEN)
63 | if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient)
64 | try {
65 | await apolloClient.resetStore()
66 | } catch (e) {
67 | // eslint-disable-next-line no-console
68 | console.log('%cError on cache reset (logout)', 'color: orange;', e.message)
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/components/ExampleComponent.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Cookiecutter Django Vue
4 |
5 |
6 |
7 |
8 |
9 |
10 |
15 |
16 |
21 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/login.gql:
--------------------------------------------------------------------------------
1 | mutation login ($email: String!, $password: String!) {
2 | login (email: $email, password: $password) {
3 | token
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/logout.gql:
--------------------------------------------------------------------------------
1 | mutation logout {
2 | logout {
3 | success
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/refreshToken.gql:
--------------------------------------------------------------------------------
1 | mutation refreshToken ($token: String!) {
2 | refreshToken (token: $token) {
3 | token
4 | payload
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/register.gql:
--------------------------------------------------------------------------------
1 | mutation register ($email: String!, $password: String!, $firstName: String!, $lastName: String!) {
2 | register (email: $email, password: $password, firstName: $firstName, lastName: $lastName) {
3 | success
4 | errors
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/resetPassword.gql:
--------------------------------------------------------------------------------
1 | mutation resetPassword ($email: String!) {
2 | resetPassword (email: $email) {
3 | success
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/resetPasswordConfirm.gql:
--------------------------------------------------------------------------------
1 | mutation resetPasswordConfirm ($token: String!, $password: String!) {
2 | resetPasswordConfirm (token: $token, password: $password) {
3 | success
4 | errors
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/verifyToken.gql:
--------------------------------------------------------------------------------
1 | mutation verifyToken ($token: String!) {
2 | verifyToken (token: $token) {
3 | payload
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/graphql/queries/profile.gql:
--------------------------------------------------------------------------------
1 | query profile {
2 | profile {
3 | id
4 | email
5 | firstName
6 | lastName
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import store from '@/store'
3 | import router from '@/router'
4 |
5 | {% if cookiecutter.api == "REST" %}
6 | import axios from 'axios'
7 | axios.defaults.xsrfCookieName = 'csrftoken'
8 | axios.defaults.xsrfHeaderName = 'X-CSRFToken'
9 | {% elif cookiecutter.api == "GraphQL" %}
10 | import { createProvider } from '@/apollo'
11 | {% endif %}
12 |
13 | {% if cookiecutter.analytics == 'Google Analytics' %} import VueAnalytics from 'vue-analytics'{% endif %}
14 | {% if cookiecutter.analytics == 'Yandex Metrika' %} import VueYandexMetrika from 'vue-yandex-metrika'{% endif %}
15 | {% if cookiecutter.use_sentry == 'y' %}
16 | import * as Sentry from "@sentry/vue"
17 | import { Integrations } from "@sentry/tracing";
18 | {% endif %}
19 |
20 | import App from '@/App.vue'
21 | import './registerServiceWorker'
22 |
23 | Vue.config.productionTip = false
24 |
25 | {% if cookiecutter.use_sentry == 'y' %}
26 | // Sentry for logging frontend errors
27 | Sentry.init({
28 | Vue: Vue,
29 | dsn: process.env.VUE_APP_SENTRY_PUBLIC_DSN,
30 | integrations: [new Integrations.BrowserTracing()],
31 | tracingOptions: {
32 | trackComponents: true,
33 | },
34 | logError: process.env.NODE_ENV === 'development'
35 | });
36 | {% endif %}
37 |
38 | {% if cookiecutter.analytics == 'Google Analytics' %}
39 | // more info: https://github.com/MatteoGabriele/vue-analytics
40 | Vue.use(VueAnalytics, {
41 | id: process.env.VUE_APP_GOOGLE_ANALYTICS,
42 | router
43 | })
44 | {% endif %}
45 |
46 | {% if cookiecutter.analytics == 'Yandex Metrika' %}
47 | // more info: https://github.com/vchaptsev/vue-yandex-metrika
48 | Vue.use(VueYandexMetrika, {
49 | id: process.env.VUE_APP_YANDEX_METRIKA,
50 | env: process.env.NODE_ENV,
51 | router
52 | })
53 | {% endif %}
54 |
55 | new Vue({
56 | router,
57 | store,
58 | {% if cookiecutter.api == "GraphQL" %}provide: createProvider().provide(), {% endif %}
59 | render: h => h(App)
60 | }).$mount('#app')
61 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import { register } from 'register-service-worker'
3 |
4 | if (process.env.NODE_ENV === 'production') {
5 | register(`${process.env.BASE_URL}service-worker.js`, {
6 | ready () {
7 | console.log(
8 | 'App is being served from cache by a service worker.\n' +
9 | 'For more details, visit https://goo.gl/AFskqB'
10 | )
11 | },
12 | cached () {
13 | console.log('Content has been cached for offline use.')
14 | },
15 | updated () {
16 | console.log('New content is available; please refresh.')
17 | },
18 | offline () {
19 | console.log('No internet connection found. App is running in offline mode.')
20 | },
21 | error (error) {
22 | console.error('Error during service worker registration:', error)
23 | }
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/router.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueRouter from 'vue-router'
3 |
4 | import ExampleComponent from '@/components/ExampleComponent.vue'
5 |
6 | const routes = [
7 | {path: '*', component: ExampleComponent}
8 | ]
9 |
10 | Vue.use(VueRouter)
11 | const router = new VueRouter({
12 | scrollBehavior (to, from, savedPosition) { return {x: 0, y: 0} },
13 | mode: 'history',
14 | routes
15 | })
16 |
17 | export default router
18 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/store.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 |
4 | Vue.use(Vuex)
5 |
6 | const store = new Vuex.Store({
7 | strict: true,
8 | state: {},
9 | mutations: {},
10 | actions: {}
11 | })
12 |
13 | export default store
14 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 |
4 | import users from '@/store/services/users'
5 | import auth from '@/store/modules/auth'
6 |
7 | Vue.use(Vuex)
8 |
9 | const store = new Vuex.Store({
10 | modules: {
11 | users,
12 | auth
13 | }
14 | })
15 |
16 | export default store
17 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/store/modules/auth.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | const state = {
4 | loggedIn: false,
5 | profile: {},
6 | validation: {email: true},
7 | authError: false
8 | }
9 |
10 | const getters = {}
11 |
12 | const mutations = {
13 | login (state) {
14 | state.loggedIn = true
15 | },
16 | logout (state) {
17 | state.loggedIn = false
18 | },
19 | setProfile (state, payload) {
20 | state.profile = payload
21 | },
22 | setValidationEmail (state, bool) {
23 | state.validation.email = bool
24 | },
25 | setAuthError (state, bool) {
26 | state.authError = bool
27 | }
28 | }
29 |
30 | const actions = {
31 | postLogin (context, payload) {
32 | return axios.post('/api/users/login/', payload)
33 | .then(response => {})
34 | .catch(e => {
35 | context.commit('setAuthError', true)
36 | console.log(e)
37 | })
38 | },
39 | postRegister (context, payload) {
40 | return axios.post('/api/users/register/', payload)
41 | .then(response => {
42 | if (response.data.status === 210) {
43 | context.commit('setValidationEmail', false)
44 | } else {
45 | context.commit('setValidationEmail', true)
46 | context.commit('login')
47 | context.commit('setProfile', response.data)
48 | }
49 | })
50 | .catch(e => { console.log(e) })
51 | },
52 | getProfile (context) {
53 | return axios.get('/api/users/profile')
54 | .then(response => {
55 | context.commit('login')
56 | context.commit('setProfile', response.data)
57 | })
58 | .catch(e => {
59 | context.commit('logout')
60 | console.log(e)
61 | })
62 | }
63 | }
64 |
65 | export default {
66 | state,
67 | getters,
68 | mutations,
69 | actions
70 | }
71 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/store/services/users.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | const state = {
4 | users: [],
5 | emailFail: false,
6 | tokenFail: false
7 | }
8 |
9 | const getters = {}
10 |
11 | const mutations = {
12 | setUsers (state, users) {
13 | state.users = users
14 | },
15 | setUser (state, user) {
16 | state.user = user
17 | },
18 | setEmailFail (state, bool) {
19 | state.emailFail = bool
20 | },
21 | setTokenFail (state, bool) {
22 | state.tokenFail = bool
23 | }
24 | }
25 |
26 | const actions = {
27 | getUsersList (context) {
28 | return axios.get('/api/users')
29 | .then(response => { context.commit('setUsers', response.data) })
30 | .catch(e => { console.log(e) })
31 | },
32 | getUser (context, userId) {
33 | return axios.get('/api/users/' + userId)
34 | .then(response => { context.commit('setUser', response.data) })
35 | .catch(e => { console.log(e) })
36 | },
37 | createUser (context, payload) {
38 | var avatar = payload.avatar
39 | delete payload.avatar
40 |
41 | return axios.post('/api/users/', payload)
42 | .then(response => {
43 | // Image upload
44 | if (typeof avatar === 'object') {
45 | let data = new FormData()
46 | data.append('avatar', avatar)
47 | return axios.patch('/api/users/' + response.data.id, data)
48 | }
49 | })
50 | .catch(e => { console.log(e) })
51 | },
52 | editUser (context, payload) {
53 | var avatar = payload.avatar
54 | delete payload.avatar
55 |
56 | return axios.patch('/api/users/' + payload.id, payload)
57 | .then(response => {
58 | // Image upload
59 | if (typeof avatar === 'object') {
60 | let data = new FormData()
61 | data.append('avatar', avatar)
62 | return axios.patch('/api/users/' + payload.id, data)
63 | }
64 | })
65 | .catch(e => { console.log(e) })
66 | },
67 | deleteUser (context, userId) {
68 | return axios.delete('/api/users/' + userId)
69 | .then(response => {})
70 | .catch(e => { console.log(e) })
71 | },
72 | passwordReset (context, user) {
73 | return axios.post('/api/users/password_reset/', user)
74 | .then(response => { context.commit('setEmailFail', false) })
75 | .catch(e => { context.commit('setEmailFail', true) })
76 | },
77 | passwordChange (context, payload) {
78 | return axios.post('/api/users/password_change/', payload)
79 | .then(response => { context.commit('setTokenFail', false) })
80 | .catch(e => { context.commit('setTokenFail', true) })
81 | }
82 | }
83 |
84 | export default {
85 | state,
86 | getters,
87 | mutations,
88 | actions
89 | }
90 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/tests/unit/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | jest: true
4 | },
5 | rules: {
6 | 'import/no-extraneous-dependencies': 'off'
7 | }
8 | }
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/vue.config.js:
--------------------------------------------------------------------------------
1 | // vue.config.js
2 | module.exports = {
3 | lintOnSave: false,
4 | devServer: {
5 | hot: true,
6 | hotOnly: true,
7 | disableHostCheck: true,
8 | historyApiFallback: true,
9 | public: '0.0.0.0:8000',
10 | headers: {
11 | 'Access-Control-Allow-Origin': '*',
12 | 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
13 | 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization'
14 | },
15 | watchOptions: {
16 | poll: 1000,
17 | ignored: '/app/node_modules/'
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/jsconfig.json:
--------------------------------------------------------------------------------
1 | // jsconfig.json
2 | {
3 | "compilerOptions": {
4 | "target": "ES6",
5 | "module": "commonjs",
6 | "allowSyntheticDefaultImports": true,
7 | "baseUrl": "./",
8 | "paths": {
9 | "@/*": ["frontend/src/*"],
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/nginx/Dockerfile:
--------------------------------------------------------------------------------
1 | # Stage 1 - build frontend app
2 | FROM node:10-alpine as build-deps
3 |
4 | WORKDIR /app/
5 |
6 | COPY frontend/package.json frontend/package-lock.json /app/
7 | RUN npm install
8 |
9 | COPY frontend /app/
10 | COPY .env /app/.env
11 | RUN npm run build
12 |
13 | # Stage 2 - nginx & frontend dist
14 | FROM nginx:alpine
15 |
16 | COPY nginx/prod.conf /etc/nginx/nginx.conf
17 | COPY --from=build-deps /app/dist/ /dist/
18 |
19 | CMD ["nginx", "-g", "daemon off;"]
20 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/nginx/dev.conf:
--------------------------------------------------------------------------------
1 | user nginx;
2 | worker_processes 1;
3 |
4 | events {
5 | worker_connections 1024;
6 | }
7 |
8 | http {
9 | include /etc/nginx/mime.types;
10 | client_max_body_size 100m;
11 |
12 | upstream backend {
13 | server backend:8000;
14 | }
15 |
16 | upstream frontend {
17 | server frontend:8080;
18 | }
19 |
20 | server {
21 | listen 80;
22 | charset utf-8;
23 |
24 | # frontend urls
25 | location / {
26 | proxy_redirect off;
27 | proxy_pass http://frontend;
28 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
29 | proxy_set_header Host $http_host;
30 | }
31 |
32 | # frontend dev-server
33 | location /sockjs-node {
34 | proxy_redirect off;
35 | proxy_pass http://frontend;
36 | proxy_set_header X-Real-IP $remote_addr;
37 | proxy_set_header X-Forwarded-For $remote_addr;
38 | proxy_set_header Host $host;
39 | proxy_set_header Upgrade $http_upgrade;
40 | proxy_set_header Connection "upgrade";
41 | }
42 |
43 | # backend urls
44 | location ~ ^/(admin|{% if cookiecutter.api == "REST" %}api{% elif cookiecutter.api == "GraphQL" %}graphql{% endif %}) {
45 | proxy_redirect off;
46 | proxy_pass http://backend;
47 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
48 | proxy_set_header Host $http_host;
49 | }
50 |
51 | # backend static
52 | location ~ ^/(staticfiles|media)/(.*)$ {
53 | alias /$1/$2;
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/nginx/prod.conf:
--------------------------------------------------------------------------------
1 | user nginx;
2 | worker_processes 1;
3 |
4 | events {
5 | worker_connections 1024;
6 | }
7 |
8 | http {
9 | include /etc/nginx/mime.types;
10 | client_max_body_size 100m;
11 |
12 | upstream backend {
13 | server backend:8000;
14 | }
15 |
16 | server {
17 | listen 80;
18 | charset utf-8;
19 |
20 | root /dist/;
21 | index index.html;
22 |
23 | # frontend
24 | location / {
25 | try_files $uri $uri/ @rewrites;
26 | }
27 |
28 | location @rewrites {
29 | rewrite ^(.+)$ /index.html last;
30 | }
31 |
32 | # backend urls
33 | location ~ ^/(admin|{% if cookiecutter.api == "REST" %}api{% elif cookiecutter.api == "GraphQL" %}graphql{% endif %}) {
34 | proxy_redirect off;
35 | proxy_pass http://backend;
36 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
37 | proxy_set_header Host $http_host;
38 | }
39 |
40 | # backend static
41 | location ~ ^/(staticfiles|media)/(.*)$ {
42 | alias /$1/$2;
43 | }
44 |
45 | # Some basic cache-control for static files to be sent to the browser
46 | location ~* \.(?:ico|css|js|gif|jpe?g|png)$ {
47 | expires max;
48 | add_header Pragma public;
49 | add_header Cache-Control "public, must-revalidate, proxy-revalidate";
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------