├── .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 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 148 | 149 | 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 | 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 | 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 | --------------------------------------------------------------------------------