diff --git a/README.md b/README.md
index 35eadc7..07aa208 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,7 @@
+
+
+
+
Cookiecutter Django-Vue
=======================
@@ -8,17 +12,16 @@ inspired by [Cookiecutter Django](https://github.com/pydanny/cookiecutter-django
-
Features
--------
- [Docker](https://www.docker.com/)
- [12 Factor](http://12factor.net/)
-
-- Frontend: [Vue](https://vuejs.org/) + vue-cli
+- Server: [Nginx](https://nginx.org/)
+- Frontend: [Vue](https://vuejs.org/) + [vue-cli](https://cli.vuejs.org/)
- Backend: [Django](https://www.djangoproject.com/)
- Database: [PostgreSQL](https://www.postgresql.org/)
-
+- API: REST or GraphQL
Optional Integrations
---------------------
@@ -27,7 +30,9 @@ Optional Integrations
- Integration with [MailHog](https://github.com/mailhog/MailHog) for local email testing
- Integration with [Sentry](https://sentry.io/welcome/) for frontend and backend errors logging
+- Integration with [Portainer](https://portainer.io/) (management UI for docker)
- Integration with [Google Analytics](https://www.google.com/analytics/) or [Yandex Metrika](https://tech.yandex.ru/metrika/) for web-analytics
+- Automatic database backups
Usage
-----
@@ -45,24 +50,33 @@ will be created for you.
Answer the prompts with your own desired options. For example:
- ======================= GENERAL ====================== [ ]:
+ ======================== INFO ======================= [ ]:
project_name [Project Name]: Website
project_slug [website]: website
description [Short description]: My awesome website
author [Your Name]: Your Name
email []:
- ======================= DEVOPS ======================= [ ]:
+ ====================== GENERAL ====================== [ ]:
+ Select api:
+ 1 - REST
+ 2 - GraphQL
+ Choose from 1, 2 [1]: 2
+ backups [y]: y
+ ==================== INTEGRATIONS =================== [ ]:
use_sentry [y]: y
- ======================= BACKEND ====================== [ ]:
+ use_portainer [y]: y
use_mailhog [y]: y
- custom_user [n]: n
- ======================= FRONTEND ===================== [ ]:
Select analytics:
1 - Google Analytics
2 - Yandex Metrika
3 - None
Choose from 1, 2, 3 [1]: 2
+Project creation will cause some odd newlines and linter errors, so I'd recommend:
+
+ $ autopep8 -r --in-place --aggressive --aggressive .
+ $ npm run lint --fix
+
Now you can start project with
[docker-compose](https://docs.docker.com/compose/):
@@ -72,3 +86,9 @@ For production you'll need to fill out `.env` file and use
`docker-compose-prod.yml` file:
$ docker-compose -f docker-compose-prod.yml up --build -d
+
+
+Contributing
+------------
+
+Help and feedback are welcome :)
\ No newline at end of file
diff --git a/cookiecutter.json b/cookiecutter.json
index 7cc4ef9..f73254d 100644
--- a/cookiecutter.json
+++ b/cookiecutter.json
@@ -1,5 +1,5 @@
{
- "======================= GENERAL ======================": " ",
+ "======================== INFO =======================": " ",
"project_name": "Project Name",
"project_slug": "{{ cookiecutter.project_name.lower()|replace(' ', '_')|replace('-', '_') }}",
"domain": "{{ cookiecutter.project_slug }}.com",
@@ -8,14 +8,13 @@
"author": "Your Name",
"email": "admin@{{ cookiecutter.domain }}",
- "======================= DEVOPS =======================": " ",
- "use_travis": "y",
+ "====================== GENERAL ======================": " ",
+ "api": ["REST", "GraphQL"],
+ "backups": "y",
+
+ "==================== INTEGRATIONS ===================": " ",
"use_sentry": "y",
-
- "======================= BACKEND ======================": " ",
+ "use_portainer": "y",
"use_mailhog": "y",
- "custom_user": "n",
-
- "======================= FRONTEND =====================": " ",
- "analytics": ["Google Analytics", "Yandex Metrika", "None"],
+ "analytics": ["Google Analytics", "Yandex Metrika", "None"]
}
diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py
index 262f0b0..fcc9c03 100644
--- a/hooks/post_gen_project.py
+++ b/hooks/post_gen_project.py
@@ -1,7 +1,7 @@
"""
1. Generates and saves random secret key
2. Renames env.example to .env
-3. Removes users app if it isn't going to be used
+3. Deletes unused API files
"""
import os
import random
@@ -12,9 +12,9 @@
PROJECT_DIRECTORY = os.path.realpath(os.path.curdir)
-def set_secret_key(file_location):
+def set_secret_key():
""" Generates and saves random secret key """
- with open(file_location) as f:
+ with open(os.path.join(PROJECT_DIRECTORY, 'env.example')) as f:
file_ = f.read()
punctuation = string.punctuation.replace('"', '').replace("'", '').replace('\\', '')
@@ -22,7 +22,7 @@ def set_secret_key(file_location):
file_ = file_.replace('CHANGEME!!!', secret, 1)
# Write the results
- with open(file_location, 'w') as f:
+ with open(os.path.join(PROJECT_DIRECTORY, 'env.example'), 'w') as f:
f.write(file_)
@@ -30,20 +30,29 @@ def rename_env_file():
""" Renames env file """
os.rename(os.path.join(PROJECT_DIRECTORY, 'env.example'), os.path.join(PROJECT_DIRECTORY, '.env'))
-def remove_users_app():
- """ Removes users app if it isn't going to be used """
- users_app = os.path.join(PROJECT_DIRECTORY, '{{ cookiecutter.project_slug }}/users')
- shutil.rmtree(users_app)
- for filename in ['modules/auth.js', 'services/users.js']:
- os.remove(os.path.join(PROJECT_DIRECTORY, '{{ cookiecutter.project_slug }}/static/store/' + filename))
-
-# Removes users app if it isn't going to be used
-if '{{ cookiecutter.custom_user }}' == 'n':
- remove_users_app()
-
-# Generates and saves random secret key
-set_secret_key(os.path.join(PROJECT_DIRECTORY, 'env.example')) # env file
-
-# Renames env file
+def delete_api_files():
+ """ Deletes unused API files """
+ if '{{ cookiecutter.api }}' == 'REST':
+ files = [
+ '.graphqlrc',
+ 'backend/config/schema.py',
+ 'backend/apps/users/schema.py',
+ 'frontend/src/apollo.js',
+ ]
+ shutil.rmtree(os.path.join(PROJECT_DIRECTORY, 'frontend/src/graphql'))
+ else:
+ files = [
+ 'backend/config/api.py',
+ 'backend/apps/users/views.py',
+ 'backend/apps/users/serializers.py',
+ ]
+ shutil.rmtree(os.path.join(PROJECT_DIRECTORY, 'frontend/src/store'))
+
+ for filename in files:
+ os.remove(os.path.join(PROJECT_DIRECTORY, filename))
+
+
+set_secret_key()
rename_env_file()
+delete_api_files()
\ No newline at end of file
diff --git a/tests/test_docker.sh b/tests/test_docker.sh
index 4e41686..ed8c24c 100644
--- a/tests/test_docker.sh
+++ b/tests/test_docker.sh
@@ -1,8 +1,7 @@
#!/bin/sh
# install test requirements
-pip install pipenv
-pipenv install --system
+pip install -r requirements.txt
# create a cache directory
mkdir -p .cache/docker && cd .cache/docker
@@ -11,5 +10,4 @@ mkdir -p .cache/docker && cd .cache/docker
# DEFAULT SETTINGS
cookiecutter ../../ --no-input --overwrite-if-exists && cd project_name
-# run the project's tests
-docker-compose run backend py.test
+docker-compose run backend python manage.py check
diff --git a/{{cookiecutter.project_slug}}/.editorconfig b/{{cookiecutter.project_slug}}/.editorconfig
new file mode 100644
index 0000000..3876b05
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/.editorconfig
@@ -0,0 +1,13 @@
+root = true
+
+[*]
+indent_size = 2
+charset = utf-8
+end_of_line = lf
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.py]
+indent_size = 4
+max_line_length = 120
diff --git a/{{cookiecutter.project_slug}}/.gitignore b/{{cookiecutter.project_slug}}/.gitignore
new file mode 100644
index 0000000..c14f408
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/.gitignore
@@ -0,0 +1,78 @@
+### OSX ###
+.DS_Store
+.AppleDouble
+.LSOverride
+
+### SublimeText ###
+# cache files for sublime text
+*.tmlanguage.cache
+*.tmPreferences.cache
+*.stTheme.cache
+
+*.sublime-project
+*.sublime-workspace
+
+# sftp configuration file
+sftp-config.json
+
+# Basics
+*.py[cod]
+__pycache__
+
+# Logs
+logs
+*.log
+pip-log.txt
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Unit test / coverage reports
+.coverage
+.tox
+nosetests.xml
+htmlcov
+
+# Translations
+*.mo
+*.pot
+
+# Webpack
+webpack-stats.json
+dist/
+
+# Vim
+*~
+*.swp
+*.swo
+
+# npm
+node_modules
+
+# Compass
+.sass-cache
+
+# User-uploaded media
+media/
+
+# Collected staticfiles
+*/staticfiles/
+
+.cache/
+**/certs
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw*
+
+# VS code
+.vscode
+.pythonconfig
+
+# Venv
+venv
diff --git a/{{cookiecutter.project_slug}}/.graphqlrc b/{{cookiecutter.project_slug}}/.graphqlrc
new file mode 100644
index 0000000..cdc6b31
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/.graphqlrc
@@ -0,0 +1,5 @@
+{
+ "request": {
+ "url": "http://localhost:8000/graphql"
+ }
+}
diff --git a/{{cookiecutter.project_slug}}/README.md b/{{cookiecutter.project_slug}}/README.md
index b98e387..76f70ab 100644
--- a/{{cookiecutter.project_slug}}/README.md
+++ b/{{cookiecutter.project_slug}}/README.md
@@ -3,8 +3,10 @@
{{cookiecutter.description}}
-
-
-
+
+
+
- {% endif %}
+
+## Development
++ run `docker-compose up --build`
diff --git a/{{cookiecutter.project_slug}}/backend/Dockerfile b/{{cookiecutter.project_slug}}/backend/Dockerfile
new file mode 100644
index 0000000..da07082
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/backend/Dockerfile
@@ -0,0 +1,18 @@
+FROM python:3.6
+
+# python envs
+ENV PYTHONFAULTHANDLER=1 \
+ PYTHONUNBUFFERED=1 \
+ PYTHONHASHSEED=random \
+ PIP_NO_CACHE_DIR=off \
+ PIP_DISABLE_PIP_VERSION_CHECK=on \
+ PIP_DEFAULT_TIMEOUT=100
+
+# python dependencies
+COPY ./requirements.txt /
+RUN pip install -r ./requirements.txt
+
+# upload scripts
+COPY ./scripts/entrypoint.sh ./scripts/start.sh ./scripts/gunicorn.sh /
+
+WORKDIR /app
diff --git a/{{cookiecutter.project_slug}}/backend/__init__.py b/{{cookiecutter.project_slug}}/backend/__init__.py
index 76f97b1..e69de29 100644
--- a/{{cookiecutter.project_slug}}/backend/__init__.py
+++ b/{{cookiecutter.project_slug}}/backend/__init__.py
@@ -1,2 +0,0 @@
-__version__ = '0.1.0'
-__version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')])
diff --git a/{{cookiecutter.project_slug}}/backend/config/settings/__init__.py b/{{cookiecutter.project_slug}}/backend/apps/__init__.py
similarity index 100%
rename from {{cookiecutter.project_slug}}/backend/config/settings/__init__.py
rename to {{cookiecutter.project_slug}}/backend/apps/__init__.py
diff --git a/{{cookiecutter.project_slug}}/backend/users/__init__.py b/{{cookiecutter.project_slug}}/backend/apps/users/__init__.py
similarity index 100%
rename from {{cookiecutter.project_slug}}/backend/users/__init__.py
rename to {{cookiecutter.project_slug}}/backend/apps/users/__init__.py
diff --git a/{{cookiecutter.project_slug}}/backend/users/admin.py b/{{cookiecutter.project_slug}}/backend/apps/users/admin.py
similarity index 90%
rename from {{cookiecutter.project_slug}}/backend/users/admin.py
rename to {{cookiecutter.project_slug}}/backend/apps/users/admin.py
index 3a9c7db..7850a9d 100644
--- a/{{cookiecutter.project_slug}}/backend/users/admin.py
+++ b/{{cookiecutter.project_slug}}/backend/apps/users/admin.py
@@ -2,8 +2,8 @@
from django.contrib.auth.models import Group
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
-from .models import User
-from .forms import UserChangeForm, UserCreationForm
+from apps.users.models import User
+from apps.users.forms import UserChangeForm, UserCreationForm
class UserAdmin(BaseUserAdmin):
@@ -18,13 +18,13 @@ class UserAdmin(BaseUserAdmin):
['Auth', {'fields': ['email', 'password']}],
['Personal info', {'fields': ['last_name', 'first_name', 'avatar']}],
['Settings', {'fields': ['groups', 'is_admin', 'is_active', 'is_staff', 'is_superuser']}],
- ['Important dates', {'fields': ['last_login', 'registered_at']}]
+ ['Important dates', {'fields': ['last_login', 'registered_at']}],
]
# add_fieldsets is not a standard ModelAdmin attribute. UserAdmin
# overrides get_fieldsets to use this attribute when creating a user.
add_fieldsets = [
[None, {'classes': ['wide'],
- 'fields': ['email', 'first_name', 'last_name', 'password1', 'password2']}]
+ 'fields': ['email', 'first_name', 'last_name', 'password1', 'password2']}],
]
search_fields = ['email']
ordering = ['email']
diff --git a/{{cookiecutter.project_slug}}/backend/users/apps.py b/{{cookiecutter.project_slug}}/backend/apps/users/apps.py
similarity index 65%
rename from {{cookiecutter.project_slug}}/backend/users/apps.py
rename to {{cookiecutter.project_slug}}/backend/apps/users/apps.py
index 2389055..22fd981 100644
--- a/{{cookiecutter.project_slug}}/backend/users/apps.py
+++ b/{{cookiecutter.project_slug}}/backend/apps/users/apps.py
@@ -2,5 +2,5 @@
class UsersConfig(AppConfig):
- name = '{{cookiecutter.project_slug}}.users'
+ name = 'apps.users'
verbose_name = 'Users'
diff --git a/{{cookiecutter.project_slug}}/backend/users/forms.py b/{{cookiecutter.project_slug}}/backend/apps/users/forms.py
similarity index 91%
rename from {{cookiecutter.project_slug}}/backend/users/forms.py
rename to {{cookiecutter.project_slug}}/backend/apps/users/forms.py
index 3b9feee..ca6028d 100644
--- a/{{cookiecutter.project_slug}}/backend/users/forms.py
+++ b/{{cookiecutter.project_slug}}/backend/apps/users/forms.py
@@ -1,14 +1,14 @@
from django import forms
from django.contrib.auth.forms import ReadOnlyPasswordHashField
-from .models import User
+from apps.users.models import User
class UserCreationForm(forms.ModelForm):
- '''
+ """
A form for creating new users.
Includes all the required fields, plus a repeated password
- '''
+ """
password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput)
@@ -34,13 +34,13 @@ def save(self, commit=True):
class UserChangeForm(forms.ModelForm):
- '''
+ """
A form for updating users. Includes all the fields on
the user, but replaces the password field with admin's
password hash display field.
- '''
- help_text = '''Raw passwords are not stored, so there is no way to see this user's password,
- but you can change the password using this form.'''
+ """
+ help_text = """Raw passwords are not stored, so there is no way to see this user's password,
+ but you can change the password using this form."""
password = ReadOnlyPasswordHashField(label='Password', help_text=help_text)
class Meta:
diff --git a/{{cookiecutter.project_slug}}/backend/users/migrations/0001_initial.py b/{{cookiecutter.project_slug}}/backend/apps/users/migrations/0001_initial.py
similarity index 100%
rename from {{cookiecutter.project_slug}}/backend/users/migrations/0001_initial.py
rename to {{cookiecutter.project_slug}}/backend/apps/users/migrations/0001_initial.py
diff --git a/{{cookiecutter.project_slug}}/backend/users/migrations/__init__.py b/{{cookiecutter.project_slug}}/backend/apps/users/migrations/__init__.py
similarity index 100%
rename from {{cookiecutter.project_slug}}/backend/users/migrations/__init__.py
rename to {{cookiecutter.project_slug}}/backend/apps/users/migrations/__init__.py
diff --git a/{{cookiecutter.project_slug}}/backend/users/models.py b/{{cookiecutter.project_slug}}/backend/apps/users/models.py
similarity index 100%
rename from {{cookiecutter.project_slug}}/backend/users/models.py
rename to {{cookiecutter.project_slug}}/backend/apps/users/models.py
diff --git a/{{cookiecutter.project_slug}}/backend/apps/users/schema.py b/{{cookiecutter.project_slug}}/backend/apps/users/schema.py
new file mode 100644
index 0000000..78089b6
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/backend/apps/users/schema.py
@@ -0,0 +1,139 @@
+import graphene
+import graphql_jwt
+from graphene_django import DjangoObjectType
+from uuid import uuid4
+
+from django.contrib.auth import logout
+from django.conf import settings
+from django.core.mail import send_mail
+from django.template.loader import render_to_string
+
+from apps.users.models import User
+
+
+class UserType(DjangoObjectType):
+ """ User type object """
+
+ class Meta:
+ model = User
+ only_fields = [
+ 'id',
+ 'email',
+ 'first_name',
+ 'last_name',
+ 'registered_at',
+ ]
+
+
+class Query(object):
+ user = graphene.Field(UserType, id=graphene.Int(required=True))
+ users = graphene.List(UserType)
+ profile = graphene.Field(UserType)
+
+ @staticmethod
+ def resolve_user(cls, info, **kwargs):
+ return User.objects.get(id=kwargs.get('id'))
+
+ @staticmethod
+ def resolve_users(cls, info, **kwargs):
+ return User.objects.all()
+
+ @staticmethod
+ def resolve_profile(cls, info, **kwargs):
+ if info.context.user.is_authenticated:
+ return info.context.user
+
+
+class Register(graphene.Mutation):
+ """ Mutation to register a user """
+ success = graphene.Boolean()
+ errors = graphene.List(graphene.String)
+
+ class Arguments:
+ email = graphene.String(required=True)
+ password = graphene.String(required=True)
+ first_name = graphene.String(required=True)
+ last_name = graphene.String(required=True)
+
+ def mutate(self, info, email, password, first_name, last_name):
+ if User.objects.filter(email__iexact=email).exists():
+ errors = ['emailAlreadyExists']
+ return Register(success=False, errors=errors)
+
+ # create user
+ user = User.objects.create(
+ email=email,
+ last_name=last_name,
+ first_name=first_name,
+ )
+ user.set_password(password)
+ user.save()
+ return Register(success=True)
+
+
+class Logout(graphene.Mutation):
+ """ Mutation to logout a user """
+ success = graphene.Boolean()
+
+ def mutate(self, info):
+ logout(info.context)
+ return Logout(success=True)
+
+
+class ResetPassword(graphene.Mutation):
+ """ Mutation for requesting a password reset email """
+ success = graphene.Boolean()
+
+ class Arguments:
+ email = graphene.String(required=True)
+
+ def mutate(self, info, email):
+ try:
+ user = User.objects.get(email=email)
+ except User.DoesNotExist:
+ errors = ['emailDoesNotExists']
+ return ResetPassword(success=False, errors=errors)
+
+ params = {
+ 'user': user,
+ 'DOMAIN': settings.DOMAIN,
+ }
+ send_mail(
+ subject='Password reset',
+ message=render_to_string('mail/password_reset.txt', params),
+ from_email=settings.DEFAULT_FROM_EMAIL,
+ recipient_list=[email],
+ )
+ return ResetPassword(success=True)
+
+
+class ResetPasswordConfirm(graphene.Mutation):
+ """ Mutation for requesting a password reset email """
+ success = graphene.Boolean()
+ errors = graphene.List(graphene.String)
+
+ class Arguments:
+ token = graphene.String(required=True)
+ password = graphene.String(required=True)
+
+ def mutate(self, info, token, password):
+ try:
+ user = User.objects.get(token=token)
+ except User.DoesNotExist:
+ errors = ['wrongToken']
+ return ResetPasswordConfirm(success=False, errors=errors)
+
+ user.set_password(password)
+ user.token = uuid4()
+ user.save()
+ return ResetPasswordConfirm(success=True)
+
+
+class Mutation(object):
+ login = graphql_jwt.ObtainJSONWebToken.Field()
+ verify_token = graphql_jwt.Verify.Field()
+ refresh_token = graphql_jwt.Refresh.Field()
+ register = Register.Field()
+ logout = Logout.Field()
+ reset_password = ResetPassword.Field()
+ reset_password_confirm = ResetPasswordConfirm.Field()
diff --git a/{{cookiecutter.project_slug}}/backend/users/serializers.py b/{{cookiecutter.project_slug}}/backend/apps/users/serializers.py
similarity index 96%
rename from {{cookiecutter.project_slug}}/backend/users/serializers.py
rename to {{cookiecutter.project_slug}}/backend/apps/users/serializers.py
index d277cf8..caab8cc 100644
--- a/{{cookiecutter.project_slug}}/backend/users/serializers.py
+++ b/{{cookiecutter.project_slug}}/backend/apps/users/serializers.py
@@ -1,7 +1,8 @@
from rest_framework import serializers
+
from django.conf import settings
-from .models import User
+from apps.users.models import User
class UserSerializer(serializers.ModelSerializer):
diff --git a/{{cookiecutter.project_slug}}/backend/users/templates/mail/password_reset.txt b/{{cookiecutter.project_slug}}/backend/apps/users/templates/mail/password_reset.txt
similarity index 100%
rename from {{cookiecutter.project_slug}}/backend/users/templates/mail/password_reset.txt
rename to {{cookiecutter.project_slug}}/backend/apps/users/templates/mail/password_reset.txt
diff --git a/{{cookiecutter.project_slug}}/backend/users/views.py b/{{cookiecutter.project_slug}}/backend/apps/users/views.py
similarity index 80%
rename from {{cookiecutter.project_slug}}/backend/users/views.py
rename to {{cookiecutter.project_slug}}/backend/apps/users/views.py
index 238ad61..6e5984a 100644
--- a/{{cookiecutter.project_slug}}/backend/users/views.py
+++ b/{{cookiecutter.project_slug}}/backend/apps/users/views.py
@@ -1,99 +1,104 @@
-import requests
-from uuid import uuid4
-
-from django.contrib.auth import authenticate, login
-from django.conf import settings
-from django.core.mail import send_mail
-from django.template.loader import render_to_string
-
-from rest_framework import viewsets, status
-from rest_framework.decorators import list_route
-from rest_framework.response import Response
-
-from .models import User
-from .serializers import UserSerializer, UserWriteSerializer
-
-
-class UserViewSet(viewsets.ModelViewSet):
- queryset = User.objects.all()
- serializer_class = UserSerializer
- permission_classes = []
-
- def get_serializer_class(self):
- if self.action in ['list', 'retrieve']:
- return UserSerializer
- return UserWriteSerializer
-
- def perform_create(self, serializer):
- user = serializer.save()
- user.set_password(self.request.data.get('password'))
- user.save()
-
- def perform_update(self, serializer):
- user = serializer.save()
- if 'password' in self.request.data:
- user.set_password(self.request.data.get('password'))
- user.save()
-
- def perform_destroy(self, instance):
- instance.is_active = False
- instance.save()
-
- @list_route(methods=['GET'])
- def profile(self, request):
- if request.user.is_authenticated:
- serializer = self.serializer_class(request.user)
- return Response(status=status.HTTP_200_OK, data=serializer.data)
- return Response(status=status.HTTP_401_UNAUTHORIZED)
-
- @list_route(methods=['POST'])
- def login(self, request, format=None):
- email = request.data.get('email', None)
- password = request.data.get('password', None)
- user = authenticate(username=email, password=password)
-
- if user:
- login(request, user)
- return Response(status=status.HTTP_200_OK)
- return Response(status=status.HTTP_404_NOT_FOUND)
-
- @list_route(methods=['POST'])
- def register(self, request):
- last_name = request.data.get('last_name', None)
- first_name = request.data.get('first_name', None)
- email = request.data.get('email', None)
- password = request.data.get('password', None)
-
- if User.objects.filter(email__iexact=email).exists():
- return Response({'status': 210})
-
- # user creation
- user = User.objects.create(email=email,
- password=password,
- last_name=last_name,
- first_name=first_name,
- is_admin=True)
- return Response(UserSerializer(user).data, status=status.HTTP_201_CREATED)
-
- @list_route(methods=['POST'])
- def password_reset(self, request, format=None):
- if User.objects.filter(email=request.data['email']).exists():
- user = User.objects.get(email=request.data['email'])
- send_mail(subject='Password reset',
- message=render_to_string('mail/password_reset.txt', {'user': user, 'DOMAIN': settings.DOMAIN}),
- from_email=settings.DEFAULT_FROM_EMAIL,
- recipient_list=[request.data['email']])
- return Response(status=status.HTTP_200_OK)
- else:
- return Response(status=status.HTTP_404_NOT_FOUND)
-
- @list_route(methods=['POST'])
- def password_change(self, request, format=None):
- if User.objects.filter(token=request.data['token']).exists():
- user = User.objects.get(token=request.data['token'])
- user.set_password(request.data['password'])
- user.token = uuid4()
- user.save()
- return Response(status=status.HTTP_200_OK)
- else:
- return Response(status=status.HTTP_404_NOT_FOUND)
+import requests
+from uuid import uuid4
+
+from django.contrib.auth import authenticate, login
+from django.conf import settings
+from django.core.mail import send_mail
+from django.template.loader import render_to_string
+
+from rest_framework import viewsets, status
+from rest_framework.decorators import list_route
+from rest_framework.response import Response
+
+from apps.users.models import User
+from apps.users.serializers import UserSerializer, UserWriteSerializer
+
+
+class UserViewSet(viewsets.ModelViewSet):
+ queryset = User.objects.all()
+ serializer_class = UserSerializer
+ permission_classes = []
+
+ def get_serializer_class(self):
+ if self.action in ['list', 'retrieve']:
+ return UserSerializer
+ return UserWriteSerializer
+
+ def perform_create(self, serializer):
+ user = serializer.save()
+ user.set_password(self.request.data.get('password'))
+ user.save()
+
+ def perform_update(self, serializer):
+ user = serializer.save()
+ if 'password' in self.request.data:
+ user.set_password(self.request.data.get('password'))
+ user.save()
+
+ def perform_destroy(self, instance):
+ instance.is_active = False
+ instance.save()
+
+ @list_route(methods=['GET'])
+ def profile(self, request):
+ if request.user.is_authenticated:
+ serializer = self.serializer_class(request.user)
+ return Response(status=status.HTTP_200_OK, data=serializer.data)
+ return Response(status=status.HTTP_401_UNAUTHORIZED)
+
+ @list_route(methods=['POST'])
+ def login(self, request, format=None):
+ email = request.data.get('email', None)
+ password = request.data.get('password', None)
+ user = authenticate(username=email, password=password)
+
+ if user:
+ login(request, user)
+ return Response(status=status.HTTP_200_OK)
+ return Response(status=status.HTTP_404_NOT_FOUND)
+
+ @list_route(methods=['POST'])
+ def register(self, request):
+ last_name = request.data.get('last_name', None)
+ first_name = request.data.get('first_name', None)
+ email = request.data.get('email', None)
+ password = request.data.get('password', None)
+
+ if User.objects.filter(email__iexact=email).exists():
+ return Response({'status': 210})
+
+ # user creation
+ user = User.objects.create(
+ email=email,
+ password=password,
+ last_name=last_name,
+ first_name=first_name,
+ is_admin=False,
+ )
+ return Response(UserSerializer(user).data, status=status.HTTP_201_CREATED)
+
+ @list_route(methods=['POST'])
+ def password_reset(self, request, format=None):
+ if User.objects.filter(email=request.data['email']).exists():
+ user = User.objects.get(email=request.data['email'])
+ params = {'user': user, 'DOMAIN': settings.DOMAIN}
+ send_mail(
+ subject='Password reset',
+ message=render_to_string('mail/password_reset.txt', params),
+ from_email=settings.DEFAULT_FROM_EMAIL,
+ recipient_list=[request.data['email']],
+ )
+ return Response(status=status.HTTP_200_OK)
+ else:
+ return Response(status=status.HTTP_404_NOT_FOUND)
+
+ @list_route(methods=['POST'])
+ def password_change(self, request, format=None):
+ if User.objects.filter(token=request.data['token']).exists():
+ user = User.objects.get(token=request.data['token'])
+ user.set_password(request.data['password'])
+ user.token = uuid4()
+ user.save()
+ return Response(status=status.HTTP_200_OK)
+ else:
+ return Response(status=status.HTTP_404_NOT_FOUND)
diff --git a/{{cookiecutter.project_slug}}/backend/config/api.py b/{{cookiecutter.project_slug}}/backend/config/api.py
index ffba87d..4e43765 100644
--- a/{{cookiecutter.project_slug}}/backend/config/api.py
+++ b/{{cookiecutter.project_slug}}/backend/config/api.py
@@ -1,13 +1,9 @@
from rest_framework import routers
-{% if cookiecutter.custom_user == 'y' %}
-from {{cookiecutter.project_slug}}.users.views import UserViewSet
-{% endif %}
+from apps.users.views import UserViewSet
# Settings
api = routers.DefaultRouter()
api.trailing_slash = '/?'
-{% if cookiecutter.custom_user == 'y' %}
# Users API
api.register(r'users', UserViewSet)
-{% endif %}
diff --git a/{{cookiecutter.project_slug}}/backend/config/schema.py b/{{cookiecutter.project_slug}}/backend/config/schema.py
new file mode 100644
index 0000000..17b1f83
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/backend/config/schema.py
@@ -0,0 +1,15 @@
+import graphene
+from graphene_django.debug import DjangoDebug
+
+import apps.users.schema
+
+
+class Query(apps.users.schema.Query, graphene.ObjectType):
+ debug = graphene.Field(DjangoDebug, name='__debug')
+
+
+class Mutation(apps.users.schema.Mutation, graphene.ObjectType):
+ ...
+
+
+schema = graphene.Schema(query=Query, mutation=Mutation)
diff --git a/{{cookiecutter.project_slug}}/backend/config/settings/base.py b/{{cookiecutter.project_slug}}/backend/config/settings.py
similarity index 66%
rename from {{cookiecutter.project_slug}}/backend/config/settings/base.py
rename to {{cookiecutter.project_slug}}/backend/config/settings.py
index 33e853f..0447fb0 100644
--- a/{{cookiecutter.project_slug}}/backend/config/settings/base.py
+++ b/{{cookiecutter.project_slug}}/backend/config/settings.py
@@ -1,16 +1,16 @@
-'''
-Django settings for {{cookiecutter.project_name}} project.
+"""
+Django settings for {{ cookiecutter.project_name }} project.
For more information on this file, see
https://docs.djangoproject.com/en/dev/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/dev/ref/settings/
-'''
+"""
import environ
+from datetime import timedelta
-ROOT_DIR = environ.Path(__file__) - 3 # ({{cookiecutter.project_slug}}/config/settings/base.py - 3 = {{cookiecutter.project_slug}}/)
-APPS_DIR = ROOT_DIR.path('{{cookiecutter.project_slug}}')
+ROOT_DIR = environ.Path(__file__) - 2
# Load operating system environment variables and then prepare to use them
env = environ.Env()
@@ -18,27 +18,25 @@
# APP CONFIGURATION
# ------------------------------------------------------------------------------
DJANGO_APPS = [
- # Default Django apps:
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
-
- # Admin
- 'django.contrib.admin'
+ 'django.contrib.admin',
]
THIRD_PARTY_APPS = [
+ {% if cookiecutter.api == 'REST' %}
'rest_framework',
- 'django_extensions'
+ {% elif cookiecutter.api == 'GraphQL' %}
+ 'graphene_django',
+ {% endif %}
+ 'django_extensions',
]
-# Apps specific for this project go here.
LOCAL_APPS = [
- # Your stuff: custom apps go here
- {% if cookiecutter.custom_user == 'y' %}
- '{{ cookiecutter.project_slug }}.users.apps.UsersConfig'{% endif %}
+ 'apps.users',
]
# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
@@ -52,32 +50,31 @@
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
+ {% if cookiecutter.api == 'GraphQL' %}'graphql_jwt.middleware.JSONWebTokenMiddleware',{% endif %}
'django.contrib.messages.middleware.MessageMiddleware',
- 'django.middleware.clickjacking.XFrameOptionsMiddleware'
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# DEBUG
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#debug
-DEBUG = False
-DOMAIN = 'localhost:8000'
+DEBUG = env.bool('DEBUG')
+SECRET_KEY = env.str('SECRET_KEY')
-# FIXTURE CONFIGURATION
-# ------------------------------------------------------------------------------
-# See: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FIXTURE_DIRS
-FIXTURE_DIRS = [
- str(APPS_DIR.path('fixtures'))
-]
+# DOMAINS
+ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=['*'])
+DOMAIN = env.str('DOMAIN')
# EMAIL CONFIGURATION
# ------------------------------------------------------------------------------
-EMAIL_BACKEND = env('DJANGO_EMAIL_BACKEND', default='django.core.mail.backends.smtp.EmailBackend')
+EMAIL_PORT = env.int('EMAIL_PORT', default='1025')
+EMAIL_HOST = env.str('EMAIL_HOST', default='mailhog')
# MANAGER CONFIGURATION
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#admins
ADMINS = [
- ('''{{cookiecutter.author}}''', '{{cookiecutter.email}}')
+ ('{{ cookiecutter.author }}', '{{ cookiecutter.email }}'),
]
# See: https://docs.djangoproject.com/en/dev/ref/settings/#managers
@@ -87,10 +84,15 @@
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#databases
DATABASES = {
- 'default': env.db('DATABASE_URL', default='postgres:///{{cookiecutter.project_slug}}')
+ 'default': {
+ 'ENGINE': 'django.db.backends.postgresql_psycopg2',
+ 'NAME': env.str('POSTGRES_DB'),
+ 'USER': env.str('POSTGRES_USER'),
+ 'PASSWORD': env.str('POSTGRES_PASSWORD'),
+ 'HOST': 'postgres',
+ 'PORT': 5432,
+ },
}
-DATABASES['default']['ATOMIC_REQUESTS'] = True
-
# GENERAL CONFIGURATION
# ------------------------------------------------------------------------------
@@ -122,7 +124,7 @@
# See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS
STATICFILES_DIRS = [
- str(APPS_DIR.path('static')),
+ str(ROOT_DIR('static')),
]
# See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders
@@ -152,9 +154,7 @@
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
- 'DIRS': [
- str(APPS_DIR.path('static')),
- ],
+ 'DIRS': STATICFILES_DIRS,
'OPTIONS': {
'debug': DEBUG,
'loaders': [
@@ -184,7 +184,7 @@
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
- 'django.contrib.auth.hashers.BCryptPasswordHasher'
+ 'django.contrib.auth.hashers.BCryptPasswordHasher',
]
# PASSWORD VALIDATION
@@ -194,9 +194,22 @@
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
- {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}
+ {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]
+# AUTHENTICATION CONFIGURATION
+# ------------------------------------------------------------------------------
+AUTHENTICATION_BACKENDS = [
+ {% if cookiecutter.api == 'GraphQL' %}'graphql_jwt.backends.JSONWebTokenBackend',{% endif %}
+ 'django.contrib.auth.backends.ModelBackend',
+]
+
+# Custom user app defaults
+# Select the correct user model
+AUTH_USER_MODEL = 'users.User'
+
+
+{% if cookiecutter.api == 'REST' %}
# DJANGO REST FRAMEWORK
# ------------------------------------------------------------------------------
REST_FRAMEWORK = {
@@ -213,33 +226,82 @@
'rest_framework.parsers.FileUploadParser'
]
}
+{% elif cookiecutter.api == 'GraphQL' %}
+# Graphene
+GRAPHENE = {
+ 'SCHEMA': 'config.schema.schema',
+}
+if DEBUG:
+ GRAPHENE['MIDDLEWARE'] = ['graphene_django.debug.DjangoDebugMiddleware']
-# AUTHENTICATION CONFIGURATION
-# ------------------------------------------------------------------------------
-AUTHENTICATION_BACKENDS = [
- 'django.contrib.auth.backends.ModelBackend'
-]
-
-# Custom user app defaults
-# Select the correct user model
-{% if cookiecutter.custom_user == 'y' %}
-AUTH_USER_MODEL = 'users.User'
+GRAPHQL_JWT = {
+ 'JWT_EXPIRATION_DELTA': timedelta(days=30),
+ 'JWT_AUTH_HEADER': 'authorization',
+ 'JWT_AUTH_HEADER_PREFIX': 'Bearer',
+}
{% endif %}
-LOGIN_REDIRECT_URL = '/login'
-LOGIN_URL = '/login'
-
-# Location of root django.contrib.admin URL
-ADMIN_URL = r'^admin/'
-{% if cookiecutter.analytics == 'Google Analytics' -%}
-# Google Analytics
-GOOGLE_ANALYTICS = env('GOOGLE_ANALYTICS', default=None){% endif %}
-
-{% if cookiecutter.analytics == 'Yandex Metrika' -%}
-# Yandex Metrika
-YANDEX_METRIKA = env('YANDEX_METRIKA', default=1){% endif %}
{% if cookiecutter.use_sentry == 'y' %}
-# Raven for frontend errors logging
-SENTRY_PUBLIC_DSN = env('SENTRY_PUBLIC_DSN', default=None){% endif %}
+# raven sentry client
+# See https://docs.sentry.io/clients/python/integrations/django/
+INSTALLED_APPS += ['raven.contrib.django.raven_compat']
+RAVEN_MIDDLEWARE = ['raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware']
+MIDDLEWARE = RAVEN_MIDDLEWARE + MIDDLEWARE
+
+# Sentry Configuration
+SENTRY_DSN = env.str('SENTRY_DSN')
+SENTRY_CLIENT = 'raven.contrib.django.raven_compat.DjangoClient'
+LOGGING = {
+ 'version': 1,
+ 'disable_existing_loggers': True,
+ 'root': {
+ 'level': 'WARNING',
+ 'handlers': ['sentry'],
+ },
+ 'formatters': {
+ 'verbose': {
+ 'format': '%(levelname)s %(asctime)s %(module)s '
+ '%(process)d %(thread)d %(message)s'
+ },
+ },
+ 'handlers': {
+ 'sentry': {
+ 'level': 'ERROR',
+ 'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler',
+ },
+ 'console': {
+ 'level': 'DEBUG',
+ 'class': 'logging.StreamHandler',
+ 'formatter': 'verbose'
+ }
+ },
+ 'loggers': {
+ 'django.db.backends': {
+ 'level': 'ERROR',
+ 'handlers': ['console'],
+ 'propagate': False,
+ },
+ 'raven': {
+ 'level': 'DEBUG',
+ 'handlers': ['console'],
+ 'propagate': False,
+ },
+ 'sentry.errors': {
+ 'level': 'DEBUG',
+ 'handlers': ['console'],
+ 'propagate': False,
+ },
+ 'django.security.DisallowedHost': {
+ 'level': 'ERROR',
+ 'handlers': ['console', 'sentry'],
+ 'propagate': False,
+ },
+ },
+}
+
+RAVEN_CONFIG = {
+ 'DSN': SENTRY_DSN
+}
+{% endif %}
diff --git a/{{cookiecutter.project_slug}}/backend/config/settings/production.py b/{{cookiecutter.project_slug}}/backend/config/settings/production.py
deleted file mode 100644
index c3400de..0000000
--- a/{{cookiecutter.project_slug}}/backend/config/settings/production.py
+++ /dev/null
@@ -1,130 +0,0 @@
-"""
-Production Configurations
-
-- Use Redis for cache
-{% if cookiecutter.use_sentry == 'y' -%}
-- Use Sentry for error logging
-{% endif %}
-"""
-
-import logging
-from datetime import datetime, timedelta
-
-from .base import * # noqa
-
-# SECRET CONFIGURATION
-# ------------------------------------------------------------------------------
-# See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
-# Raises ImproperlyConfigured exception if DJANGO_SECRET_KEY not in os.environ
-SECRET_KEY = env.str('DJANGO_SECRET_KEY')
-
-SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
-
-{% if cookiecutter.use_sentry == 'y' -%}
-# raven sentry client
-# See https://docs.sentry.io/clients/python/integrations/django/
-INSTALLED_APPS += ['raven.contrib.django.raven_compat']
-RAVEN_MIDDLEWARE = ['raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware']
-MIDDLEWARE = RAVEN_MIDDLEWARE + MIDDLEWARE
-{% endif %}
-
-
-# SITE CONFIGURATION
-ALLOWED_HOSTS = env.list('ALLOWED_HOSTS')
-DOMAIN = env.str.str('DOMAIN')
-
-# Gunicorn
-INSTALLED_APPS += ['gunicorn']
-
-# TEMPLATE CONFIGURATION
-# ------------------------------------------------------------------------------
-# See:
-# https://docs.djangoproject.com/en/dev/ref/templates/api/#django.template.loaders.cached.Loader
-TEMPLATES[0]['OPTIONS']['loaders'] = [
- ('django.template.loaders.cached.Loader', [
- 'django.template.loaders.filesystem.Loader', 'django.template.loaders.app_directories.Loader', ]),
-]
-
-# DATABASE CONFIGURATION
-# ------------------------------------------------------------------------------
-
-# Use the Heroku-style specification
-# Raises ImproperlyConfigured exception if DATABASE_URL not in os.environ
-DATABASES['default'] = env.db('DATABASE_URL')
-
-# CACHING
-# ------------------------------------------------------------------------------
-
-REDIS_URL = env('REDIS_URL', default='redis://127.0.0.1:6379')
-REDIS_LOCATION = f'{REDIS_URL}/0'
-# Heroku URL does not pass the DB number, so we parse it in
-CACHES = {
- 'default': {
- 'BACKEND': 'django_redis.cache.RedisCache',
- 'LOCATION': REDIS_LOCATION,
- 'OPTIONS': {
- 'CLIENT_CLASS': 'django_redis.client.DefaultClient',
- 'IGNORE_EXCEPTIONS': True, # mimics memcache behavior.
- # http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior
- }
- }
-}
-
-{% if cookiecutter.use_sentry == 'y' -%}
-# Sentry Configuration
-SENTRY_DSN = env.str('SENTRY_DSN')
-SENTRY_CLIENT = env.str('DJANGO_SENTRY_CLIENT', default='raven.contrib.django.raven_compat.DjangoClient')
-LOGGING = {
- 'version': 1,
- 'disable_existing_loggers': True,
- 'root': {
- 'level': 'WARNING',
- 'handlers': ['sentry', ],
- },
- 'formatters': {
- 'verbose': {
- 'format': '%(levelname)s %(asctime)s %(module)s '
- '%(process)d %(thread)d %(message)s'
- },
- },
- 'handlers': {
- 'sentry': {
- 'level': 'ERROR',
- 'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler',
- },
- 'console': {
- 'level': 'DEBUG',
- 'class': 'logging.StreamHandler',
- 'formatter': 'verbose'
- }
- },
- 'loggers': {
- 'django.db.backends': {
- 'level': 'ERROR',
- 'handlers': ['console', ],
- 'propagate': False,
- },
- 'raven': {
- 'level': 'DEBUG',
- 'handlers': ['console', ],
- 'propagate': False,
- },
- 'sentry.errors': {
- 'level': 'DEBUG',
- 'handlers': ['console', ],
- 'propagate': False,
- },
- 'django.security.DisallowedHost': {
- 'level': 'ERROR',
- 'handlers': ['console', 'sentry', ],
- 'propagate': False,
- },
- },
-}
-
-SENTRY_CELERY_LOGLEVEL = env.int('DJANGO_SENTRY_LOG_LEVEL', logging.INFO)
-RAVEN_CONFIG = {
- 'CELERY_LOGLEVEL': env.int('DJANGO_SENTRY_LOG_LEVEL', logging.INFO),
- 'DSN': SENTRY_DSN
-}
-{% endif %}
diff --git a/{{cookiecutter.project_slug}}/backend/config/urls.py b/{{cookiecutter.project_slug}}/backend/config/urls.py
index dc8e872..589732b 100644
--- a/{{cookiecutter.project_slug}}/backend/config/urls.py
+++ b/{{cookiecutter.project_slug}}/backend/config/urls.py
@@ -1,36 +1,25 @@
-from django.conf import settings
-from django.conf.urls import include, url
-from django.conf.urls.static import static
+from django.urls import path
from django.contrib import admin
-from django.views.generic import TemplateView
-from django.contrib.auth.views import logout
+from django.contrib.auth import logout
+{% if cookiecutter.api == 'REST' %}
+from django.conf.urls import include
from config.api import api
+{% elif cookiecutter.api == 'GraphQL' %}
+from django.conf import settings
+from django.views.decorators.csrf import csrf_exempt
-urlpatterns = [
- url(r'^admin/', admin.site.urls),
- url(r'^api/', include(api.urls)),
- url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
- url(r'^logout/$', logout, {'next_page': '/'}, name='logout')
-]
-
-if settings.DEBUG:
- # Media urls for development
- urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
-
-
-class ExtraContextTemplateView(TemplateView):
- """
- Hack to pass some content to Template View
- """
- def get_context_data(self, **kwargs):
- context = super(ExtraContextTemplateView, self).get_context_data(**kwargs)
- context['DEBUG'] = settings.DEBUG
- {% if cookiecutter.use_sentry == 'y' %}context['SENTRY_PUBLIC_DSN'] = settings.SENTRY_PUBLIC_DSN{% endif %}
- {% if cookiecutter.analytics == 'Yandex Metrika' -%}context['YANDEX_METRIKA'] = settings.YANDEX_METRIKA{% endif %}
- {% if cookiecutter.analytics == 'Google Analytics' -%}context['GOOGLE_ANALYTICS'] = settings.GOOGLE_ANALYTICS{% endif %}
- return context
+from graphene_django.views import GraphQLView
+{% endif %}
-# App: Vue routing
-urlpatterns += [url(r'^', ExtraContextTemplateView.as_view(template_name='main.html'))]
+urlpatterns = [
+ path('admin/', admin.site.urls, name='admin'),
+ path('logout/', logout, {'next_page': '/'}, name='logout'),
+ {% if cookiecutter.api == 'REST' %}
+ path('api/', include(api.urls)),
+ path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
+ {% elif cookiecutter.api == 'GraphQL' %}
+ path('graphql', csrf_exempt(GraphQLView.as_view(graphiql=settings.DEBUG))),
+ {% endif %}
+]
diff --git a/{{cookiecutter.project_slug}}/backend/config/wsgi.py b/{{cookiecutter.project_slug}}/backend/config/wsgi.py
index 91b384c..c0da8d8 100644
--- a/{{cookiecutter.project_slug}}/backend/config/wsgi.py
+++ b/{{cookiecutter.project_slug}}/backend/config/wsgi.py
@@ -1,37 +1,29 @@
"""
-WSGI config for {{cookiecutter.project_name}} project.
+WSGI config for serenity-escrow project.
This module contains the WSGI application used by Django's development server
and any production WSGI deployments. It should expose a module-level variable
named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
this application via the ``WSGI_APPLICATION`` setting.
-
"""
+
import os
import sys
from django.core.wsgi import get_wsgi_application
-# This allows easy placement of apps within the interior
-# {{ cookiecutter.project_slug }} directory.
-app_path = os.path.dirname(os.path.abspath(__file__)).replace('/config', '')
-sys.path.append(os.path.join(app_path, '{{ cookiecutter.project_slug }}'))
-
-{% if cookiecutter.use_sentry == 'y' -%}
-if os.environ.get('DJANGO_SETTINGS_MODULE') == 'config.settings.production':
- from raven.contrib.django.raven_compat.middleware.wsgi import Sentry
-{% endif %}
-
-# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
-# if running multiple sites in the same mod_wsgi process. To fix this, use
-# mod_wsgi daemon mode with each site in its own daemon process, or use
-# os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.production"
-os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
+# This allows easy placement of apps within the interior serenity directory.
+current_path = os.path.dirname(os.path.abspath(__file__)).replace('/config', '')
+sys.path.append(current_path)
+sys.path.append(os.path.join(current_path, 'apps'))
# This application object is used by any WSGI server configured to use this
# file. This includes Django's development server, if the WSGI_APPLICATION
# setting points here.
application = get_wsgi_application()
+
{% if cookiecutter.use_sentry == 'y' -%}
-if os.environ.get('DJANGO_SETTINGS_MODULE') == 'config.settings.production':
- application = Sentry(application){% endif %}
+from raven.contrib.django.raven_compat.middleware.wsgi import Sentry
+application = Sentry(application)
+{% endif %}
diff --git a/{{cookiecutter.project_slug}}/backend/manage.py b/{{cookiecutter.project_slug}}/backend/manage.py
old mode 100755
new mode 100644
diff --git a/{{cookiecutter.project_slug}}/backend/requirements.txt b/{{cookiecutter.project_slug}}/backend/requirements.txt
new file mode 100644
index 0000000..1ac28c9
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/backend/requirements.txt
@@ -0,0 +1,53 @@
+autopep8==1.4
+attrs==18.2.0
+backcall==0.1.0
+certifi==2018.8.24
+chardet==3.0.4
+decorator==4.3.0
+Django==2.1.1
+django-environ==0.4.5
+django-extensions==2.1.2
+django-redis==4.9.0
+flake8==3.5.0
+flake8-commas==2.0.0
+flake8-mypy==17.8.0
+gevent==1.3.6
+greenlet==0.4.14
+gunicorn==19.9.0
+idna==2.7
+ipython==6.5.0
+ipython-genutils==0.2.0
+jedi==0.12.1
+mccabe==0.6.1
+mypy==0.620
+parso==0.3.1
+pexpect==4.6.0
+pickleshare==0.7.4
+Pillow==5.0.0
+prompt-toolkit==1.0.15
+psycopg2-binary==2.7.5
+ptyprocess==0.6.0
+pycodestyle==2.3.1
+pyflakes==1.6.0
+Pygments==2.2.0
+pytz==2018.5
+redis==2.10.6
+requests==2.19.1
+rope==0.11.0
+simplegeneric==0.8.1
+six==1.11.0
+traitlets==4.3.2
+typed-ast==1.1.0
+urllib3==1.23
+wcwidth==0.1.7
+Werkzeug==0.14.1
+{% if cookiecutter.use_sentry == 'y' %}raven==6.9.0{% endif %}
+{% if cookiecutter.api == 'REST' %}
+djangorestframework==3.8.2
+{% elif cookiecutter.api == 'GraphQL' %}
+django-graphql-jwt==0.1.10
+graphene==2.1.3
+graphene-django==2.1.0
+{% endif %}
+
+
diff --git a/{{cookiecutter.project_slug}}/backend/scripts/entrypoint.sh b/{{cookiecutter.project_slug}}/backend/scripts/entrypoint.sh
new file mode 100755
index 0000000..27a0888
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/backend/scripts/entrypoint.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+
+set -o errexit
+set -o pipefail
+cmd="$@"
+
+function postgres_ready(){
+python << END
+import sys
+import psycopg2
+import environ
+
+try:
+ env = environ.Env()
+ dbname = env.str('POSTGRES_DB')
+ user = env.str('POSTGRES_USER')
+ password = env.str('POSTGRES_PASSWORD')
+ conn = psycopg2.connect(dbname=dbname, user=user, password=password, host='postgres', port=5432)
+except psycopg2.OperationalError:
+ sys.exit(-1)
+sys.exit(0)
+END
+}
+
+until postgres_ready; do
+ >&2 echo "Postgres is unavailable - sleeping"
+ sleep 1
+done
+
+>&2 echo "Postgres is up - continuing..."
+exec $cmd
diff --git a/{{cookiecutter.project_slug}}/backend/scripts/gunicorn.sh b/{{cookiecutter.project_slug}}/backend/scripts/gunicorn.sh
new file mode 100755
index 0000000..8ced569
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/backend/scripts/gunicorn.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+set -o errexit
+set -o pipefail
+set -o nounset
+
+python manage.py migrate
+python manage.py collectstatic --noinput --verbosity 0
+gunicorn config.wsgi -w 4 --worker-class gevent -b 0.0.0.0:8000 --chdir=/app
diff --git a/{{cookiecutter.project_slug}}/backend/scripts/start.sh b/{{cookiecutter.project_slug}}/backend/scripts/start.sh
new file mode 100755
index 0000000..743f6ca
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/backend/scripts/start.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+set -o errexit
+set -o pipefail
+set -o nounset
+set -o xtrace
+
+python manage.py migrate
+python manage.py collectstatic --noinput --verbosity 0
+python manage.py runserver_plus 0.0.0.0:8000
diff --git a/{{cookiecutter.project_slug}}/backend/users/tests/__init__.py b/{{cookiecutter.project_slug}}/backend/static/.gitkeep
similarity index 100%
rename from {{cookiecutter.project_slug}}/backend/users/tests/__init__.py
rename to {{cookiecutter.project_slug}}/backend/static/.gitkeep
diff --git a/{{cookiecutter.project_slug}}/backend/users/tests/factories.py b/{{cookiecutter.project_slug}}/backend/users/tests/factories.py
deleted file mode 100644
index 145867e..0000000
--- a/{{cookiecutter.project_slug}}/backend/users/tests/factories.py
+++ /dev/null
@@ -1,14 +0,0 @@
-import factory
-from uuid import uuid4
-
-
-class UserFactory(factory.django.DjangoModelFactory):
- email = factory.Sequence(lambda n: f'user-{n}@example.com')
- password = factory.PostGenerationMethodCall('set_password', 'password')
- first_name = 'First'
- last_name = 'Last'
- token = uuid4()
-
- class Meta:
- model = 'users.User'
- django_get_or_create = ['email']
diff --git a/{{cookiecutter.project_slug}}/backend/users/tests/test_api.py b/{{cookiecutter.project_slug}}/backend/users/tests/test_api.py
deleted file mode 100644
index af98740..0000000
--- a/{{cookiecutter.project_slug}}/backend/users/tests/test_api.py
+++ /dev/null
@@ -1,121 +0,0 @@
-from uuid import uuid4
-
-from test_plus.test import TestCase
-
-from ..models import User
-from .factories import UserFactory
-
-
-class UserTests(TestCase):
-
- def setUp(self):
- self.user = UserFactory()
- self.another_user = UserFactory()
-
- def test_profile(self):
- # wihout login
- response = self.get('user-profile')
- self.response_401(response)
-
- # with login
- with self.login(username=self.user.email):
- response = self.get('user-profile')
- self.response_200(response)
-
- def test_login(self):
- # success
- data = {'email': self.user.email, 'password': 'password'}
- response = self.post('user-login', data=data)
- self.response_200(response)
-
- # fail
- data = {'email': self.user.email, 'password': 'bad_password'}
- response = self.post('user-login', data=data)
- self.response_404(response)
-
- def test_list(self):
- with self.login(username=self.user.email):
- response = self.get('user-list')
- self.response_200(response)
- self.assertEqual(len(response.json()), 2)
-
- def test_detail(self):
- with self.login(username=self.user.email):
- response = self.get('user-detail', pk=self.user.id)
- self.response_200(response)
-
- def test_delete(self):
- with self.login(username=self.user.email):
- response = self.delete('user-detail', pk=self.user.id)
- self.assertEqual(response.status_code, 204)
-
- def test_update(self):
- with self.login(username=self.user.email):
- response = self.get('user-detail', pk=self.user.id, data={'password': 'new_password'})
- self.response_200(response)
- self.assertEqual(self.user.password == User.objects.last().password, False)
-
- def test_create(self):
- with self.login(username=self.user.email):
- data = {
- 'email': 'test_create@gmail.com',
- 'password': 'password',
- 'first_name': 'first_name',
- 'last_name': 'last_name'
- }
- response = self.post('user-list', data=data)
- self.response_201(response)
- self.assertEqual(response.json()['email'], 'test_create@gmail.com')
-
- def test_register(self):
- with self.login(username=self.user.email):
- UserFactory(email='test_reg@gmail.com')
-
- # user with email already exists
- data = {
- 'email': 'test_reg@gmail.com',
- 'password': 'password',
- 'first_name': 'first',
- 'last_name': 'last',
- 'avatar': ''
- }
- response = self.post('user-register', data=data)
- self.response_200(response)
- self.assertEqual(response.json()['status'], 210)
-
- # successful registration
- data = {
- 'email': 'test_reg_new@gmail.com',
- 'password': 'password',
- 'first_name': 'first',
- 'last_name': 'last',
- 'avatar': ''
- }
- response = self.post('user-register', data=data)
- self.response_201(response)
-
- def test_password_reset(self):
- with self.login(username=self.user.email):
- # successful password reset
- response = self.post('user-password-reset', data={'email': self.user.email})
- self.response_200(response)
-
- # email doesn't exist
- response = self.post('user-password-reset', data={'email': 'fake-email@gmail.com'})
- self.response_404(response)
-
- def test_password_change(self):
- with self.login(username=self.user.email):
- current_password = self.user.password
-
- # token doesn't exist
- response = self.post('user-password-change', data={'token': uuid4(), 'password': 'new_password'})
- self.response_404(response)
- self.assertEqual(current_password, self.user.password)
-
- # successful password change
- self.user.token = uuid4()
- self.user.save()
- response = self.post('user-password-change', data={'token': self.user.token, 'password': 'new_password'})
- self.response_200(response)
- self.assertEqual(current_password == User.objects.last().password, False)
diff --git a/{{cookiecutter.project_slug}}/backend/users/tests/test_models.py b/{{cookiecutter.project_slug}}/backend/users/tests/test_models.py
deleted file mode 100644
index e272298..0000000
--- a/{{cookiecutter.project_slug}}/backend/users/tests/test_models.py
+++ /dev/null
@@ -1,34 +0,0 @@
-from test_plus.test import TestCase
-
-from ..models import User
-from .factories import UserFactory
-
-
-class TestUser(TestCase):
- user_factory = UserFactory
-
- def setUp(self):
- self.user = self.make_user()
-
- def test_full_name(self):
- self.assertEqual(self.user.get_full_name(), 'First Last')
-
- def test_short_name(self):
- self.assertEqual(self.user.get_short_name(), 'Last F.')
-
- def test_string_representation(self):
- self.assertEqual(str(self.user), 'First Last')
-
- def test_create(self):
- extra_fields = {'email': 'test@email.com', 'password': 'password', 'first_name': 'test'}
- user = User.objects.create(**extra_fields)
- self.assertEqual(user.email, 'test@email.com')
- self.assertEqual(user.first_name, 'test')
-
- def test_create_user(self):
- user = User.objects.create_user(email='user@email.com', password='password')
- self.assertEqual(user.email, 'user@email.com')
-
- def test_create_superuser(self):
- user = User.objects.create_superuser(email='user@email.com', password='password')
- self.assertEqual(user.email, 'user@email.com')
diff --git a/{{cookiecutter.project_slug}}/docker-compose-prod.yml b/{{cookiecutter.project_slug}}/docker-compose-prod.yml
new file mode 100644
index 0000000..801bf46
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/docker-compose-prod.yml
@@ -0,0 +1,59 @@
+version: '3.3'
+
+volumes:
+ postgres_data: {}
+ portainer_data: {}
+
+services:
+ backend:
+ build:
+ context: ./backend
+ depends_on:
+ - postgres
+ volumes:
+ - ./backend:/app
+ command: /gunicorn.sh
+ entrypoint: /entrypoint.sh
+ restart: on-failure
+ env_file: .env
+
+ postgres:
+ image: postgres:10-alpine
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ env_file: .env
+
+ nginx:
+ build:
+ context: .
+ dockerfile: nginx/Dockerfile
+ ports:
+ - "8000:80"
+ depends_on:
+ - backend
+ volumes:
+ - ./backend/media/:/media/
+ - ./backend/staticfiles/:/staticfiles/
+ - ./nginx/prod.conf:/etc/nginx/nginx.conf:ro
+
+
+{% if cookiecutter.backups == 'y' %}
+ backups:
+ image: prodrigestivill/postgres-backup-local
+ restart: on-failure
+ depends_on:
+ - postgres
+ volumes:
+ - /tmp/backups/:/backups/
+{% endif %}
+
+{% if cookiecutter.use_portainer == 'y' %}
+ portainer:
+ image: portainer/portainer
+ ports:
+ - "9000:9000"
+ command: -H unix:///var/run/docker.sock
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock
+ - portainer_data:/data
+{% endif %}
diff --git a/{{cookiecutter.project_slug}}/docker-compose.yml b/{{cookiecutter.project_slug}}/docker-compose.yml
new file mode 100644
index 0000000..b9af33a
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/docker-compose.yml
@@ -0,0 +1,54 @@
+version: '3.3'
+
+volumes:
+ postgres_data: {}
+
+services:
+ backend:
+ build:
+ context: ./backend
+ depends_on:
+ - postgres
+ volumes:
+ - ./backend:/app
+ command: /start.sh
+ entrypoint: /entrypoint.sh
+ restart: on-failure
+ env_file: .env
+
+ frontend:
+ image: node:10-alpine
+ command: npm run serve
+ volumes:
+ - ./.env:/app/.env:ro
+ - ./frontend:/app
+ working_dir: /app
+ restart: on-failure
+
+ postgres:
+ image: postgres:10-alpine
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ env_file: .env
+
+{% if cookiecutter.use_mailhog == 'y' %}
+ mailhog:
+ image: mailhog/mailhog
+ ports:
+ - "8025:8025"
+ logging:
+ driver: none
+{% endif %}
+
+ nginx:
+ image: nginx:alpine
+ ports:
+ - "8000:80"
+ depends_on:
+ - backend
+ volumes:
+ - ./backend/media/:/media/
+ - ./backend/staticfiles/:/staticfiles/
+ - ./nginx/dev.conf:/etc/nginx/nginx.conf:ro
+ logging:
+ driver: none
diff --git a/{{cookiecutter.project_slug}}/env.example b/{{cookiecutter.project_slug}}/env.example
index 623c31e..6aefed2 100644
--- a/{{cookiecutter.project_slug}}/env.example
+++ b/{{cookiecutter.project_slug}}/env.example
@@ -1,12 +1,16 @@
+# NOTICE:
+# Vue app will only detect VUE_APP_* envs
+
# Django
-DEBUG=True # change for production
+DEBUG=True
SECRET_KEY=CHANGEME!!!
-DOMAIN=http://localhost:8000 # change for production
-ALLOWED_HOSTS=* # change for production
+DOMAIN=http://localhost:8000
+ALLOWED_HOSTS=*
-EMAIL_PORT=1025 # change for production
-EMAIL_HOST=mailhog # change for production
+# Email settings, defaults to 1025 and mailhog
+#EMAIL_PORT=25
+#EMAIL_HOST=localhost
# PostgreSQL
POSTGRES_DB={{cookiecutter.project_slug}}
@@ -17,14 +21,15 @@ POSTGRES_USER=postgresuser
# Sentry
SENTRY_DSN=
SENTRY_PUBLIC_DSN=
+VUE_APP_SENTRY_PUBLIC_DSN=
{% endif %}
{% if cookiecutter.analytics == 'Google Analytics' -%}
# Google Analytics
-GOOGLE_ANALYTICS=UA-XXXXXXXXX-X
+VUE_APP_GOOGLE_ANALYTICS=UA-XXXXXXXXX-X
{% endif %}
{% if cookiecutter.analytics == 'Yandex Metrika' -%}
# Yandex Metrika
-YANDEX_METRIKA=
+VUE_APP_YANDEX_METRIKA=
{% endif %}
diff --git a/{{cookiecutter.project_slug}}/frontend/babel.config.js b/{{cookiecutter.project_slug}}/frontend/babel.config.js
new file mode 100644
index 0000000..ba17966
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/frontend/babel.config.js
@@ -0,0 +1,5 @@
+module.exports = {
+ presets: [
+ '@vue/app'
+ ]
+}
diff --git a/{{cookiecutter.project_slug}}/frontend/images/default_avatar.png b/{{cookiecutter.project_slug}}/frontend/images/default_avatar.png
deleted file mode 100644
index e397a40..0000000
Binary files a/{{cookiecutter.project_slug}}/frontend/images/default_avatar.png and /dev/null differ
diff --git a/{{cookiecutter.project_slug}}/frontend/images/favicons/android-chrome-192x192.png b/{{cookiecutter.project_slug}}/frontend/images/favicons/android-chrome-192x192.png
deleted file mode 100644
index 3516c02..0000000
Binary files a/{{cookiecutter.project_slug}}/frontend/images/favicons/android-chrome-192x192.png and /dev/null differ
diff --git a/{{cookiecutter.project_slug}}/frontend/images/favicons/android-chrome-384x384.png b/{{cookiecutter.project_slug}}/frontend/images/favicons/android-chrome-384x384.png
deleted file mode 100644
index 45cbc0a..0000000
Binary files a/{{cookiecutter.project_slug}}/frontend/images/favicons/android-chrome-384x384.png and /dev/null differ
diff --git a/{{cookiecutter.project_slug}}/frontend/images/favicons/apple-touch-icon.png b/{{cookiecutter.project_slug}}/frontend/images/favicons/apple-touch-icon.png
deleted file mode 100644
index 7b3239f..0000000
Binary files a/{{cookiecutter.project_slug}}/frontend/images/favicons/apple-touch-icon.png and /dev/null differ
diff --git a/{{cookiecutter.project_slug}}/frontend/images/favicons/browserconfig.xml b/{{cookiecutter.project_slug}}/frontend/images/favicons/browserconfig.xml
deleted file mode 100644
index 5cd27e3..0000000
--- a/{{cookiecutter.project_slug}}/frontend/images/favicons/browserconfig.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
- #603cba
-
-
-
diff --git a/{{cookiecutter.project_slug}}/frontend/images/favicons/favicon-16x16.png b/{{cookiecutter.project_slug}}/frontend/images/favicons/favicon-16x16.png
deleted file mode 100644
index 68eadbd..0000000
Binary files a/{{cookiecutter.project_slug}}/frontend/images/favicons/favicon-16x16.png and /dev/null differ
diff --git a/{{cookiecutter.project_slug}}/frontend/images/favicons/favicon-32x32.png b/{{cookiecutter.project_slug}}/frontend/images/favicons/favicon-32x32.png
deleted file mode 100644
index dde01f3..0000000
Binary files a/{{cookiecutter.project_slug}}/frontend/images/favicons/favicon-32x32.png and /dev/null differ
diff --git a/{{cookiecutter.project_slug}}/frontend/images/favicons/favicon.ico b/{{cookiecutter.project_slug}}/frontend/images/favicons/favicon.ico
deleted file mode 100644
index e481e5d..0000000
Binary files a/{{cookiecutter.project_slug}}/frontend/images/favicons/favicon.ico and /dev/null differ
diff --git a/{{cookiecutter.project_slug}}/frontend/images/favicons/manifest.json b/{{cookiecutter.project_slug}}/frontend/images/favicons/manifest.json
deleted file mode 100644
index f784358..0000000
--- a/{{cookiecutter.project_slug}}/frontend/images/favicons/manifest.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "name": "",
- "icons": [
- {
- "src": "/android-chrome-192x192.png",
- "sizes": "192x192",
- "type": "image/png"
- },
- {
- "src": "/android-chrome-384x384.png",
- "sizes": "384x384",
- "type": "image/png"
- }
- ],
- "theme_color": "#ffffff",
- "background_color": "#ffffff",
- "display": "standalone"
-}
\ No newline at end of file
diff --git a/{{cookiecutter.project_slug}}/frontend/images/favicons/mstile-150x150.png b/{{cookiecutter.project_slug}}/frontend/images/favicons/mstile-150x150.png
deleted file mode 100644
index ab41d50..0000000
Binary files a/{{cookiecutter.project_slug}}/frontend/images/favicons/mstile-150x150.png and /dev/null differ
diff --git a/{{cookiecutter.project_slug}}/frontend/images/favicons/safari-pinned-tab.svg b/{{cookiecutter.project_slug}}/frontend/images/favicons/safari-pinned-tab.svg
deleted file mode 100644
index dc0b992..0000000
--- a/{{cookiecutter.project_slug}}/frontend/images/favicons/safari-pinned-tab.svg
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
diff --git a/{{cookiecutter.project_slug}}/frontend/main.html b/{{cookiecutter.project_slug}}/frontend/main.html
deleted file mode 100644
index b50fd37..0000000
--- a/{{cookiecutter.project_slug}}/frontend/main.html
+++ /dev/null
@@ -1,48 +0,0 @@
-
-{% raw %}
-{% load static %}
-{% endraw %}
-
-
-
- {{cookiecutter.project_name}}
-
-
-
-
-
-
-
-
-
- {% raw -%}
-
-
-
-
-
-
-
- {% endraw -%}
-
-
-
-
-
-
-
-
-
-
- {% if cookiecutter.analytics == 'Google Analytics' -%}{% raw %}{% if GOOGLE_ANALYTICS %}{% endif %}{% endraw %}{% endif %}
- {% if cookiecutter.analytics == 'Yandex Metrika' -%}{% raw %}{% endraw %}{% endif %}
- {% if cookiecutter.use_sentry == 'y' %}{% raw %}{% endraw %}{% endif %}
-
- {% raw %}
- {% if DEBUG %}
-
- {% else %}
-
- {% endif %}
- {% endraw %}
-
diff --git a/{{cookiecutter.project_slug}}/frontend/package-lock.json b/{{cookiecutter.project_slug}}/frontend/package-lock.json
new file mode 100644
index 0000000..e69de29
diff --git a/{{cookiecutter.project_slug}}/frontend/package.json b/{{cookiecutter.project_slug}}/frontend/package.json
index 0826654..4a82066 100644
--- a/{{cookiecutter.project_slug}}/frontend/package.json
+++ b/{{cookiecutter.project_slug}}/frontend/package.json
@@ -1,48 +1,86 @@
{
- "name": "{{cookiecutter.project_slug}}",
- "description": "{{cookiecutter.description}}",
+ "name": "frontend",
"version": "0.1.0",
- "author": "{{cookiecutter.author}}",
- {% if cookiecutter.license != 'Not open source' %}"license": "{{cookiecutter.license}}",{% endif %}
- {% if cookiecutter.license == 'Not open source' %}"private": true,{% endif %}
+ "private": true,
"scripts": {
- "dev": "cross-env NODE_ENV=development webpack-dev-server",
- "build": "cross-env NODE_ENV=production webpack --progress --hide-modules",
- "lint": "eslint --ext .js,.vue {{cookiecutter.project_slug}}/static"
+ "serve": "npm i && vue-cli-service serve",
+ "build": "vue-cli-service build",
+ "lint": "vue-cli-service lint",
+ "test:unit": "vue-cli-service test:unit"
},
"dependencies": {
- "axios": "^0.16.2",
- "babel-core": "^6.25.0",
- "babel-eslint": "^8.2.1",
- "babel-loader": "^7.0.0",
- "babel-polyfill": "^6.26.0",
- "babel-preset-env": "^1.5.2",
- "cross-env": "^5.0.1",
- "css-loader": "^0.28.4",
- "eslint": "^4.15.0",
- "eslint-config-standard": "^10.2.1",
- "eslint-friendly-formatter": "^3.0.0",
- "eslint-loader": "^1.7.1",
- "eslint-plugin-import": "^2.7.0",
- "eslint-plugin-node": "^5.2.0",
- "eslint-plugin-promise": "^3.4.0",
- "eslint-plugin-standard": "^3.0.1",
- "eslint-plugin-vue": "^4.0.0",
- "file-loader": "^1.1.4",
- "foundation-sites": "^6.4.1",
- "node-sass": "^4.5.3",
- "sass-loader": "^6.0.6",
- "url-loader": "^0.5.8",
- "vue": "^2.3.4",
- {% if cookiecutter.analytics == "Google Analytics" %}"vue-analytics": "^5.3.2",{% endif %}
- "vue-loader": "^13.3.0",
- "vue-meta": "^1.2.0",
- {% if cookiecutter.use_sentry == 'y' %}"vue-raven": "^0.1.0",{% endif %}
- "vue-router": "^2.7.0",
- "vue-template-compiler": "^2.3.4",
- {% if cookiecutter.analytics == "Yandex Metrika" %}"vue-yandex-metrika": "^1.4.0",{% endif %}
- "vuex": "^2.3.1",
- "webpack": "^2.6.1",
- "webpack-dev-server": "^2.4.5"
+ "axios": "^0.18.0",
+ "register-service-worker": "^1.5.2",
+ "vue": "^2.5.17",
+ {% if cookiecutter.api == "GraphQL" %}"vue-apollo": "^3.0.0-beta.20",{% endif %}
+ "vue-router": "^3.0.1",
+ {% if cookiecutter.use_sentry == 'y' %}"vue-raven": "^1.0.0",{% endif %}
+ {% if cookiecutter.analytics == "Google Analytics" %}"vue-analytics": "^5.16.0",{% endif %}
+ {% if cookiecutter.analytics == "Yandex Metrika" %}{% endif %}
+ "vuex": "^3.0.1"
+ },
+ "devDependencies": {
+ "@vue/cli-plugin-babel": "^3.0.1",
+ "@vue/cli-plugin-eslint": "^3.0.1",
+ "@vue/cli-plugin-pwa": "^3.0.1",
+ "@vue/cli-plugin-unit-jest": "^3.0.1",
+ "@vue/cli-service": "^3.0.1",
+ "@vue/eslint-config-standard": "^3.0.1",
+ "@vue/test-utils": "^1.0.0-beta.24",
+ "babel-core": "^6.26.3",
+ "babel-jest": "^23.4.2",
+ "node-sass": "^4.9.3",
+ {% if cookiecutter.api == "GraphQL" %}
+ "graphql-tag": "^2.9.2",
+ "vue-cli-plugin-apollo": "^0.16.4",
+ {% endif %}
+ "sass-loader": "^7.1.0",
+ "vue-template-compiler": "^2.5.17"
+ },
+ "eslintConfig": {
+ "root": true,
+ "env": {
+ "node": true
+ },
+ "extends": [
+ "plugin:vue/essential",
+ "@vue/standard"
+ ],
+ "rules": {},
+ "parserOptions": {
+ "parser": "babel-eslint"
+ }
+ },
+ "postcss": {
+ "plugins": {
+ "autoprefixer": {}
+ }
+ },
+ "browserslist": [
+ "> 1%",
+ "last 2 versions",
+ "not ie <= 8"
+ ],
+ "jest": {
+ "moduleFileExtensions": [
+ "js",
+ "jsx",
+ "json",
+ "vue"
+ ],
+ "transform": {
+ "^.+\\.vue$": "vue-jest",
+ ".+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$": "jest-transform-stub",
+ "^.+\\.jsx?$": "babel-jest"
+ },
+ "moduleNameMapper": {
+ "^@/(.*)$": "/src/$1"
+ },
+ "snapshotSerializers": [
+ "jest-serializer-vue"
+ ],
+ "testMatch": [
+ "/(tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx))"
+ ]
}
}
diff --git a/{{cookiecutter.project_slug}}/frontend/public/img/icons/android-chrome-192x192.png b/{{cookiecutter.project_slug}}/frontend/public/img/icons/android-chrome-192x192.png
new file mode 100644
index 0000000..b02aa64
Binary files /dev/null and b/{{cookiecutter.project_slug}}/frontend/public/img/icons/android-chrome-192x192.png differ
diff --git a/{{cookiecutter.project_slug}}/frontend/public/img/icons/android-chrome-512x512.png b/{{cookiecutter.project_slug}}/frontend/public/img/icons/android-chrome-512x512.png
new file mode 100644
index 0000000..06088b0
Binary files /dev/null and b/{{cookiecutter.project_slug}}/frontend/public/img/icons/android-chrome-512x512.png differ
diff --git a/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-120x120.png b/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-120x120.png
new file mode 100644
index 0000000..1427cf6
Binary files /dev/null and b/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-120x120.png differ
diff --git a/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-152x152.png b/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-152x152.png
new file mode 100644
index 0000000..f24d454
Binary files /dev/null and b/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-152x152.png differ
diff --git a/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-180x180.png b/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-180x180.png
new file mode 100644
index 0000000..404e192
Binary files /dev/null and b/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-180x180.png differ
diff --git a/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-60x60.png b/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-60x60.png
new file mode 100644
index 0000000..cf10a56
Binary files /dev/null and b/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-60x60.png differ
diff --git a/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-76x76.png b/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-76x76.png
new file mode 100644
index 0000000..c500769
Binary files /dev/null and b/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-76x76.png differ
diff --git a/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon.png b/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon.png
new file mode 100644
index 0000000..03c0c5d
Binary files /dev/null and b/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon.png differ
diff --git a/{{cookiecutter.project_slug}}/frontend/public/img/icons/favicon-16x16.png b/{{cookiecutter.project_slug}}/frontend/public/img/icons/favicon-16x16.png
new file mode 100644
index 0000000..42af009
Binary files /dev/null and b/{{cookiecutter.project_slug}}/frontend/public/img/icons/favicon-16x16.png differ
diff --git a/{{cookiecutter.project_slug}}/frontend/public/img/icons/favicon-32x32.png b/{{cookiecutter.project_slug}}/frontend/public/img/icons/favicon-32x32.png
new file mode 100644
index 0000000..46ca04d
Binary files /dev/null and b/{{cookiecutter.project_slug}}/frontend/public/img/icons/favicon-32x32.png differ
diff --git a/{{cookiecutter.project_slug}}/frontend/public/img/icons/msapplication-icon-144x144.png b/{{cookiecutter.project_slug}}/frontend/public/img/icons/msapplication-icon-144x144.png
new file mode 100644
index 0000000..7808237
Binary files /dev/null and b/{{cookiecutter.project_slug}}/frontend/public/img/icons/msapplication-icon-144x144.png differ
diff --git a/{{cookiecutter.project_slug}}/frontend/public/img/icons/mstile-150x150.png b/{{cookiecutter.project_slug}}/frontend/public/img/icons/mstile-150x150.png
new file mode 100644
index 0000000..3b37a43
Binary files /dev/null and b/{{cookiecutter.project_slug}}/frontend/public/img/icons/mstile-150x150.png differ
diff --git a/{{cookiecutter.project_slug}}/frontend/public/img/icons/safari-pinned-tab.svg b/{{cookiecutter.project_slug}}/frontend/public/img/icons/safari-pinned-tab.svg
new file mode 100644
index 0000000..732afd8
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/frontend/public/img/icons/safari-pinned-tab.svg
@@ -0,0 +1,149 @@
+
+
+
diff --git a/{{cookiecutter.project_slug}}/frontend/public/index.html b/{{cookiecutter.project_slug}}/frontend/public/index.html
new file mode 100644
index 0000000..9c47558
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/frontend/public/index.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ cookiecutter.project_name }}
+
+
+
+
+
+
+
diff --git a/{{cookiecutter.project_slug}}/frontend/public/manifest.json b/{{cookiecutter.project_slug}}/frontend/public/manifest.json
new file mode 100644
index 0000000..c4cb1e1
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/frontend/public/manifest.json
@@ -0,0 +1,20 @@
+{
+ "name": "{{ cookiecutter.project_slug }}",
+ "short_name": "{{ cookiecutter.project_slug }}",
+ "icons": [
+ {
+ "src": "/img/icons/android-chrome-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/img/icons/android-chrome-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ],
+ "start_url": "",
+ "display": "standalone",
+ "background_color": "#000000",
+ "theme_color": "#4DBA87"
+}
diff --git a/{{cookiecutter.project_slug}}/frontend/public/robots.txt b/{{cookiecutter.project_slug}}/frontend/public/robots.txt
new file mode 100644
index 0000000..eb05362
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/frontend/public/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow:
diff --git a/{{cookiecutter.project_slug}}/frontend/scss/_settings.scss b/{{cookiecutter.project_slug}}/frontend/scss/_settings.scss
deleted file mode 100644
index 0ea8dac..0000000
--- a/{{cookiecutter.project_slug}}/frontend/scss/_settings.scss
+++ /dev/null
@@ -1,163 +0,0 @@
-// Foundation for Sites Settings
-// -----------------------------
-
-@import '~foundation-sites/scss/util/util';
-
-// 1. Global
-// ---------
-
-$global-font-size: 100%;
-$global-width: rem-calc(1345);
-$global-lineheight: 1.2;
-$foundation-palette: (
- main: #424770,
- hover: #32325d,
- link: #0f92df,
- primary: #009fe6,
- secondary: #a6b3b6,
- header: #ececec,
- button-main: #009ddb,
- secondary2: #1bbe00,
- success: #3adb76,
- warning: #e8980f,
- alert: #ff7249,
- sidebar-bg: #80888a,
- free: #33b600
-);
-
-$gray: (
- gray1: #e8e8e8,
- gray2: #d8d8d8,
- gray3: #c8c8c8
-);
-
-$black: #000000;
-$white: #ffffff;
-$light-gray: #e6e6e6;
-$medium-gray: #dadada;
-$dark-gray: #141414;
-$red: #e14252;
-$yellow: #e0994c;
-$green: #24b47e;
-$select-color: #525f7f;
-$btn-color: #555abf;
-$background-color: #f1f5f9;
-
-$body-background: $white;
-$body-font-color: #424770;
-$body-font-family: 'Open Sans', sans-serif;
-$body-font-family-bold: 'Open Sans', sans-serif;
-$body-font-family-semibold: 'Open Sans', sans-serif;
-$body-antialiased: true;
-$global-margin: 1rem;
-$global-padding: 1rem;
-$global-weight-normal: normal;
-$global-weight-bold: bold;
-$global-radius: 0;
-$btn-radius: 4px;
-$global-text-direction: ltr;
-$global-flexbox: false;
-$print-transparent-backgrounds: true;
-
-@include add-foundation-colors;
-
-// 2. Breakpoints
-// --------------
-
-$breakpoints: (
- small: 0,
- medium: 640px,
- large: 1024px,
- xlarge: 1440px,
- xxlarge: 1920px
-);
-$print-breakpoint: large;
-$breakpoint-classes: (small medium large);
-
-// 3. The Grid
-// -----------
-
-$grid-row-width: $global-width;
-$grid-column-count: 12;
-$grid-column-gutter: (
- small: 20px,
- medium: 30px
-);
-$grid-column-align-edge: true;
-$block-grid-max: 8;
-
-// 4. Base Typography
-// ------------------
-
-$header-font-family: $body-font-family-bold;
-$header-font-weight: $global-weight-normal;
-$header-font-style: normal;
-$font-family-monospace: Consolas, 'Liberation Mono', Courier, monospace;
-$header-color: inherit;
-$header-lineheight: 1.2;
-$header-margin-bottom: 1rem;
-$header-styles: (
- small: (
- 'h1': ('font-size': 24),
- 'h2': ('font-size': 20),
- 'h3': ('font-size': 19),
- 'h4': ('font-size': 18),
- 'h5': ('font-size': 17),
- 'h6': ('font-size': 16)
- ),
- medium: (
- 'h1': ('font-size': 44),
- 'h2': ('font-size': 30),
- 'h3': ('font-size': 24),
- 'h4': ('font-size': 20),
- 'h5': ('font-size': 18),
- 'h6': ('font-size': 16)
- ),
-);
-
-$header-text-rendering: optimizeLegibility;
-$small-font-size: 80%;
-$header-small-font-color: $medium-gray;
-$paragraph-lineheight: 1.5;
-$paragraph-margin-bottom: rem-calc(20);
-$paragraph-text-rendering: optimizeLegibility;
-$code-color: $black;
-$code-font-family: $font-family-monospace;
-$code-font-weight: $global-weight-normal;
-$code-background: $light-gray;
-$code-border: 1px solid $medium-gray;
-$code-padding: rem-calc(2 5 1);
-$anchor-color: #0f92df;
-$anchor-color-hover: #424770;
-$anchor-text-decoration: none;
-$anchor-text-decoration-hover: underline;
-$hr-width: 100%;
-$hr-border: 1px solid map-get($gray, gray2);
-$hr-margin: rem-calc(20) auto;
-$list-lineheight: $paragraph-lineheight;
-$list-margin-bottom: $paragraph-margin-bottom;
-$list-style-type: disc;
-$list-style-position: outside;
-$list-side-margin: 1.25rem;
-$list-nested-side-margin: 1.25rem;
-$defnlist-margin-bottom: 1rem;
-$defnlist-term-weight: $global-weight-bold;
-$defnlist-term-margin-bottom: 0.3rem;
-$blockquote-color: $dark-gray;
-$blockquote-padding: rem-calc(9 20 0 19);
-$blockquote-border: 1px solid $medium-gray;
-$cite-font-size: rem-calc(13);
-$cite-color: $dark-gray;
-$cite-pseudo-content: '\2014 \0020';
-$keystroke-font: $font-family-monospace;
-$keystroke-color: $black;
-$keystroke-background: $light-gray;
-$keystroke-padding: rem-calc(2 4 0);
-$keystroke-radius: $global-radius;
-$abbr-underline: 1px dotted $black;
-
-// 5. Forms
-// ---------
-
-$input-border: 1px solid $medium-gray;
-$input-border-focus: 1px solid $primary-color;
diff --git a/{{cookiecutter.project_slug}}/frontend/Main.vue b/{{cookiecutter.project_slug}}/frontend/src/App.vue
similarity index 68%
rename from {{cookiecutter.project_slug}}/frontend/Main.vue
rename to {{cookiecutter.project_slug}}/frontend/src/App.vue
index 27d761a..f63fb4d 100644
--- a/{{cookiecutter.project_slug}}/frontend/Main.vue
+++ b/{{cookiecutter.project_slug}}/frontend/src/App.vue
@@ -6,8 +6,6 @@
-
-
diff --git a/{{cookiecutter.project_slug}}/frontend/src/apollo.js b/{{cookiecutter.project_slug}}/frontend/src/apollo.js
new file mode 100644
index 0000000..031f4f6
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/frontend/src/apollo.js
@@ -0,0 +1,70 @@
+import Vue from 'vue'
+import VueApollo from 'vue-apollo'
+import { createApolloClient, restartWebsockets } from 'vue-cli-plugin-apollo/graphql-client'
+
+// Install the vue plugin
+Vue.use(VueApollo)
+
+// Name of the localStorage item
+const AUTH_TOKEN = 'jwt-token'
+
+// Config
+const defaultOptions = {
+ httpEndpoint: '/graphql',
+ wsEndpoint: null,
+ tokenName: AUTH_TOKEN,
+ persisting: false,
+ websocketsOnly: false,
+ ssr: false
+}
+
+// Call this in the Vue app file
+export function createProvider (options = {}) {
+ // Create apollo client
+ const { apolloClient, wsClient } = createApolloClient({
+ ...defaultOptions,
+ ...options
+ })
+ apolloClient.wsClient = wsClient
+
+ // Create vue apollo provider
+ const apolloProvider = new VueApollo({
+ defaultClient: apolloClient,
+ defaultOptions: {
+ $query: {
+ loadingKey: 'loading',
+ fetchPolicy: 'cache-and-network'
+ }
+ },
+ errorHandler (error) {
+ // eslint-disable-next-line no-console
+ console.log('%cError', 'background: red; color: white; padding: 2px 4px; border-radius: 3px; font-weight: bold;', error.message)
+ }
+ })
+
+ return apolloProvider
+}
+
+// Manually call this when user log in
+export async function onLogin (apolloClient, token) {
+ localStorage.setItem(AUTH_TOKEN, token)
+ if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient)
+ try {
+ await apolloClient.resetStore()
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.log('%cError on cache reset (login)', 'color: orange;', e.message)
+ }
+}
+
+// Manually call this when user log out
+export async function onLogout (apolloClient) {
+ localStorage.removeItem(AUTH_TOKEN)
+ if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient)
+ try {
+ await apolloClient.resetStore()
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.log('%cError on cache reset (logout)', 'color: orange;', e.message)
+ }
+}
diff --git a/{{cookiecutter.project_slug}}/frontend/components/FirstComponent.vue b/{{cookiecutter.project_slug}}/frontend/src/components/ExampleComponent.vue
similarity index 100%
rename from {{cookiecutter.project_slug}}/frontend/components/FirstComponent.vue
rename to {{cookiecutter.project_slug}}/frontend/src/components/ExampleComponent.vue
diff --git a/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/login.gql b/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/login.gql
new file mode 100644
index 0000000..c1d5107
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/login.gql
@@ -0,0 +1,5 @@
+mutation login ($email: String!, $password: String!) {
+ login (email: $email, password: $password) {
+ token
+ }
+}
diff --git a/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/logout.gql b/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/logout.gql
new file mode 100644
index 0000000..9440265
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/logout.gql
@@ -0,0 +1,5 @@
+mutation logout {
+ logout {
+ success
+ }
+}
diff --git a/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/refreshToken.gql b/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/refreshToken.gql
new file mode 100644
index 0000000..abd715f
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/refreshToken.gql
@@ -0,0 +1,6 @@
+mutation refreshToken ($token: String!) {
+ refreshToken (token: $token) {
+ token
+ payload
+ }
+}
diff --git a/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/register.gql b/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/register.gql
new file mode 100644
index 0000000..b35c59a
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/register.gql
@@ -0,0 +1,6 @@
+mutation register ($email: String!, $password: String!, $firstName: String!, $lastName: String!) {
+ register (email: $email, password: $password, firstName: $firstName, lastName: $lastName) {
+ success
+ errors
+ }
+}
diff --git a/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/resetPassword.gql b/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/resetPassword.gql
new file mode 100644
index 0000000..4bf6987
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/resetPassword.gql
@@ -0,0 +1,5 @@
+mutation resetPassword ($email: String!) {
+ resetPassword (email: $email) {
+ success
+ }
+}
diff --git a/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/resetPasswordConfirm.gql b/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/resetPasswordConfirm.gql
new file mode 100644
index 0000000..97219e0
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/resetPasswordConfirm.gql
@@ -0,0 +1,6 @@
+mutation resetPasswordConfirm ($token: String!, $password: String!) {
+ resetPasswordConfirm (token: $token, password: $password) {
+ success
+ errors
+ }
+}
diff --git a/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/verifyToken.gql b/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/verifyToken.gql
new file mode 100644
index 0000000..6c0c0d6
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/frontend/src/graphql/mutations/verifyToken.gql
@@ -0,0 +1,5 @@
+mutation verifyToken ($token: String!) {
+ verifyToken (token: $token) {
+ payload
+ }
+}
diff --git a/{{cookiecutter.project_slug}}/frontend/src/graphql/queries/profile.gql b/{{cookiecutter.project_slug}}/frontend/src/graphql/queries/profile.gql
new file mode 100644
index 0000000..c19c804
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/frontend/src/graphql/queries/profile.gql
@@ -0,0 +1,8 @@
+query profile {
+ profile {
+ id
+ email
+ firstName
+ lastName
+ }
+}
diff --git a/{{cookiecutter.project_slug}}/frontend/main.js b/{{cookiecutter.project_slug}}/frontend/src/main.js
similarity index 53%
rename from {{cookiecutter.project_slug}}/frontend/main.js
rename to {{cookiecutter.project_slug}}/frontend/src/main.js
index 0ea0214..c7d330f 100644
--- a/{{cookiecutter.project_slug}}/frontend/main.js
+++ b/{{cookiecutter.project_slug}}/frontend/src/main.js
@@ -1,39 +1,51 @@
import Vue from 'vue'
+import store from '@/store'
+import router from '@/router'
+{% if cookiecutter.api == "REST" %}
import axios from 'axios'
-import router from './router'
-import {store} from './store'
-import Meta from 'vue-meta'
+axios.defaults.xsrfCookieName = 'csrftoken'
+axios.defaults.xsrfHeaderName = 'X-CSRFToken'
+{% elif cookiecutter.api == "GraphQL" %}
+import { createProvider } from '@/apollo'
+{% endif %}
+
{% if cookiecutter.analytics == 'Google Analytics' %}import VueAnalytics from 'vue-analytics'{% endif %}
{% if cookiecutter.analytics == 'Yandex Metrika' %}import VueYandexMetrika from 'vue-yandex-metrika'{% endif %}
{% if cookiecutter.use_sentry == 'y' %}import VueRaven from 'vue-raven'{% endif %}
-import Main from './Main.vue'
+import App from '@/App.vue'
+import './registerServiceWorker'
-// Axios csrf settings
-axios.defaults.xsrfCookieName = 'csrftoken'
-axios.defaults.xsrfHeaderName = 'X-CSRFToken'
+Vue.config.productionTip = false
{% if cookiecutter.use_sentry == 'y' %}
// Sentry for logging frontend errors
-if (process.env.NODE_ENV === 'production') {Vue.use(VueRaven, {dsn: SENTRY_PUBLIC_DSN})}
+if (process.env.NODE_ENV === 'production') {
+ Vue.use(VueRaven, {dsn: process.env.VUE_APP_SENTRY_PUBLIC_DSN})
+}
{% endif %}
{% if cookiecutter.analytics == 'Google Analytics' %}
// more info: https://github.com/MatteoGabriele/vue-analytics
-Vue.use(VueAnalytics, {id: GOOGLE_ANALYTICS, router})
+Vue.use(VueAnalytics, {
+ id: process.env.VUE_APP_GOOGLE_ANALYTICS,
+ router
+})
{% endif %}
+
{% if cookiecutter.analytics == 'Yandex Metrika' %}
// more info: https://github.com/vchaptsev/vue-yandex-metrika
-Vue.use(VueYandexMetrika, {id: YANDEX_METRIKA, env: process.env.NODE_ENV, router})
+Vue.use(VueYandexMetrika, {
+ id: process.env.VUE_APP_YANDEX_METRIKA,
+ env: process.env.NODE_ENV,
+ router
+})
{% endif %}
-Vue.use(Meta)
-
-/* eslint-disable no-new */
new Vue({
- el: '#main',
router,
store,
- render: h => h(Main)
-})
+ {% if cookiecutter.api == "GraphQL" %}provide: createProvider().provide(),{% endif %}
+ render: h => h(App)
+}).$mount('#app')
diff --git a/{{cookiecutter.project_slug}}/frontend/src/registerServiceWorker.js b/{{cookiecutter.project_slug}}/frontend/src/registerServiceWorker.js
new file mode 100644
index 0000000..fc23027
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/frontend/src/registerServiceWorker.js
@@ -0,0 +1,25 @@
+/* eslint-disable no-console */
+import { register } from 'register-service-worker'
+
+if (process.env.NODE_ENV === 'production') {
+ register(`${process.env.BASE_URL}service-worker.js`, {
+ ready () {
+ console.log(
+ 'App is being served from cache by a service worker.\n' +
+ 'For more details, visit https://goo.gl/AFskqB'
+ )
+ },
+ cached () {
+ console.log('Content has been cached for offline use.')
+ },
+ updated () {
+ console.log('New content is available; please refresh.')
+ },
+ offline () {
+ console.log('No internet connection found. App is running in offline mode.')
+ },
+ error (error) {
+ console.error('Error during service worker registration:', error)
+ }
+ })
+}
diff --git a/{{cookiecutter.project_slug}}/frontend/router.js b/{{cookiecutter.project_slug}}/frontend/src/router.js
similarity index 68%
rename from {{cookiecutter.project_slug}}/frontend/router.js
rename to {{cookiecutter.project_slug}}/frontend/src/router.js
index 0fa6570..61c002f 100644
--- a/{{cookiecutter.project_slug}}/frontend/router.js
+++ b/{{cookiecutter.project_slug}}/frontend/src/router.js
@@ -1,17 +1,17 @@
-import Vue from 'vue'
-import VueRouter from 'vue-router'
-
-import FirstComponent from './components/FirstComponent.vue'
-
+import Vue from 'vue'
+import VueRouter from 'vue-router'
+
+import ExampleComponent from '@/components/ExampleComponent.vue'
+
const routes = [
- {path: '*', component: FirstComponent}
-]
-
-Vue.use(VueRouter)
-const router = new VueRouter({
- scrollBehavior (to, from, savedPosition) { return {x: 0, y: 0} },
- mode: 'history',
- routes
-})
-
-export default router
+ {path: '*', component: ExampleComponent}
+]
+
+Vue.use(VueRouter)
+const router = new VueRouter({
+ scrollBehavior (to, from, savedPosition) { return {x: 0, y: 0} },
+ mode: 'history',
+ routes
+})
+
+export default router
diff --git a/{{cookiecutter.project_slug}}/frontend/src/store.js b/{{cookiecutter.project_slug}}/frontend/src/store.js
new file mode 100644
index 0000000..8ba0dc1
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/frontend/src/store.js
@@ -0,0 +1,13 @@
+import Vue from 'vue'
+import Vuex from 'vuex'
+
+Vue.use(Vuex)
+
+const store = new Vuex.Store({
+ strict: true,
+ state: {},
+ mutations: {},
+ actions: {}
+})
+
+export default store
diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/index.js b/{{cookiecutter.project_slug}}/frontend/src/store/index.js
new file mode 100644
index 0000000..78d435a
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/frontend/src/store/index.js
@@ -0,0 +1,16 @@
+import Vue from 'vue'
+import Vuex from 'vuex'
+
+import users from '@/store/services/users'
+import auth from '@/store/modules/auth'
+
+Vue.use(Vuex)
+
+const store = new Vuex.Store({
+ modules: {
+ users,
+ auth
+ }
+})
+
+export default store
diff --git a/{{cookiecutter.project_slug}}/frontend/store/modules/auth.js b/{{cookiecutter.project_slug}}/frontend/src/store/modules/auth.js
similarity index 100%
rename from {{cookiecutter.project_slug}}/frontend/store/modules/auth.js
rename to {{cookiecutter.project_slug}}/frontend/src/store/modules/auth.js
diff --git a/{{cookiecutter.project_slug}}/frontend/store/services/users.js b/{{cookiecutter.project_slug}}/frontend/src/store/services/users.js
similarity index 100%
rename from {{cookiecutter.project_slug}}/frontend/store/services/users.js
rename to {{cookiecutter.project_slug}}/frontend/src/store/services/users.js
diff --git a/{{cookiecutter.project_slug}}/frontend/store/index.js b/{{cookiecutter.project_slug}}/frontend/store/index.js
deleted file mode 100644
index 7b18da3..0000000
--- a/{{cookiecutter.project_slug}}/frontend/store/index.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import Vue from 'vue'
-import Vuex from 'vuex'
-
-{% if cookiecutter.custom_user == 'y' %}
-import users from './services/users'
-import auth from './modules/auth'{% endif %}
-
-Vue.use(Vuex)
-
-export const store = new Vuex.Store({
- modules: {
- {% if cookiecutter.custom_user == 'y' %}
- users,
- auth{% endif %}
- }
-})
diff --git a/{{cookiecutter.project_slug}}/frontend/tests/unit/.eslintrc.js b/{{cookiecutter.project_slug}}/frontend/tests/unit/.eslintrc.js
new file mode 100644
index 0000000..4e51c63
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/frontend/tests/unit/.eslintrc.js
@@ -0,0 +1,8 @@
+module.exports = {
+ env: {
+ jest: true
+ },
+ rules: {
+ 'import/no-extraneous-dependencies': 'off'
+ }
+}
\ No newline at end of file
diff --git a/{{cookiecutter.project_slug}}/frontend/vue.config.js b/{{cookiecutter.project_slug}}/frontend/vue.config.js
new file mode 100644
index 0000000..4146417
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/frontend/vue.config.js
@@ -0,0 +1,20 @@
+// vue.config.js
+module.exports = {
+ lintOnSave: false,
+ devServer: {
+ hot: true,
+ hotOnly: true,
+ disableHostCheck: true,
+ historyApiFallback: true,
+ public: '0.0.0.0:8000',
+ headers: {
+ 'Access-Control-Allow-Origin': '*',
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
+ 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization'
+ },
+ watchOptions: {
+ poll: 1000,
+ ignored: '/app/node_modules/'
+ }
+ }
+}
diff --git a/{{cookiecutter.project_slug}}/jsconfig.json b/{{cookiecutter.project_slug}}/jsconfig.json
new file mode 100644
index 0000000..d614f94
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/jsconfig.json
@@ -0,0 +1,12 @@
+// jsconfig.json
+{
+ "compilerOptions": {
+ "target": "ES6",
+ "module": "commonjs",
+ "allowSyntheticDefaultImports": true,
+ "baseUrl": "./",
+ "paths": {
+ "@/*": ["frontend/src/*"],
+ }
+ }
+}
diff --git a/{{cookiecutter.project_slug}}/nginx/Dockerfile b/{{cookiecutter.project_slug}}/nginx/Dockerfile
new file mode 100644
index 0000000..f2aae08
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/nginx/Dockerfile
@@ -0,0 +1,19 @@
+# Stage 1 - build frontend app
+FROM node:10-alpine as build-deps
+
+WORKDIR /app/
+
+COPY frontend/package.json frontend/package-lock.json /app/
+RUN npm install
+
+COPY frontend /app/
+COPY .env /app/.env
+RUN npm run build
+
+# Stage 2 - nginx & frontend dist
+FROM nginx:alpine
+
+COPY nginx/prod.conf /etc/nginx/nginx.conf
+COPY --from=build-deps /app/dist/ /dist/
+
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/{{cookiecutter.project_slug}}/nginx/dev.conf b/{{cookiecutter.project_slug}}/nginx/dev.conf
new file mode 100644
index 0000000..a659a77
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/nginx/dev.conf
@@ -0,0 +1,56 @@
+user nginx;
+worker_processes 1;
+
+events {
+ worker_connections 1024;
+}
+
+http {
+ include /etc/nginx/mime.types;
+ client_max_body_size 100m;
+
+ upstream backend {
+ server backend:8000;
+ }
+
+ upstream frontend {
+ server frontend:8080;
+ }
+
+ server {
+ listen 80;
+ charset utf-8;
+
+ # frontend urls
+ location / {
+ proxy_redirect off;
+ proxy_pass http://frontend;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header Host $http_host;
+ }
+
+ # frontend dev-server
+ location /sockjs-node {
+ proxy_redirect off;
+ proxy_pass http://frontend;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header Host $host;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ }
+
+ # backend urls
+ location ~ ^/(admin|{% if cookiecutter.api == "REST" %}api{% elif cookiecutter.api == "GraphQL" %}graphql{% endif %}) {) {
+ proxy_redirect off;
+ proxy_pass http://backend;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header Host $http_host;
+ }
+
+ # backend static
+ location ~ ^/(staticfiles|media)/(.*)$ {
+ alias /$1/$2;
+ }
+ }
+}
diff --git a/{{cookiecutter.project_slug}}/nginx/prod.conf b/{{cookiecutter.project_slug}}/nginx/prod.conf
new file mode 100644
index 0000000..1fc6d59
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/nginx/prod.conf
@@ -0,0 +1,52 @@
+user nginx;
+worker_processes 1;
+
+events {
+ worker_connections 1024;
+}
+
+http {
+ include /etc/nginx/mime.types;
+ client_max_body_size 100m;
+
+ upstream backend {
+ server backend:8000;
+ }
+
+ server {
+ listen 80;
+ charset utf-8;
+
+ root /dist/;
+ index index.html;
+
+ # frontend
+ location / {
+ try_files $uri $uri/ @rewrites;
+ }
+
+ location @rewrites {
+ rewrite ^(.+)$ /index.html last;
+ }
+
+ # backend urls
+ location ~ ^/(admin|{% if cookiecutter.api == "REST" %}api{% elif cookiecutter.api == "GraphQL" %}graphql{% endif %}) {
+ proxy_redirect off;
+ proxy_pass http://backend;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header Host $http_host;
+ }
+
+ # backend static
+ location ~ ^/(staticfiles|media)/(.*)$ {
+ alias /$1/$2;
+ }
+
+ # Some basic cache-control for static files to be sent to the browser
+ location ~* \.(?:ico|css|js|gif|jpe?g|png)$ {
+ expires max;
+ add_header Pragma public;
+ add_header Cache-Control "public, must-revalidate, proxy-revalidate";
+ }
+ }
+}