├── fooo ├── fooo │ ├── __init__.py │ ├── asgi.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py └── manage.py ├── piedpiper ├── __init__.py ├── users │ ├── __init__.py │ ├── test │ │ ├── __init__.py │ │ ├── factories.py │ │ ├── test_serializers.py │ │ └── test_views.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0002_auto_20171227_2246.py │ │ └── 0001_initial.py │ ├── admin.py │ ├── permissions.py │ ├── models.py │ ├── serializers.py │ └── views.py ├── config │ ├── __init__.py │ ├── local.py │ ├── production.py │ └── common.py ├── wsgi.py └── urls.py ├── .dockerignore ├── setup.cfg ├── .coveragerc ├── mkdocs.yml ├── .travis.yml ├── Dockerfile ├── docker-compose.yml ├── requirements.txt ├── README.md ├── manage.py ├── wait_for_postgres.py ├── docs ├── api │ ├── authentication.md │ └── users.md └── index.md └── .gitignore /fooo/fooo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piedpiper/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piedpiper/users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piedpiper/users/test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piedpiper/users/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .* 2 | !.coveragerc 3 | !.env 4 | -------------------------------------------------------------------------------- /piedpiper/config/__init__.py: -------------------------------------------------------------------------------- 1 | from .local import Local # noqa 2 | from .production import Production # noqa 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E226,E302,E41,E702,E731 3 | max-line-length = 110 4 | exclude = migrations 5 | -------------------------------------------------------------------------------- /piedpiper/users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | from .models import User 4 | 5 | 6 | @admin.register(User) 7 | class UserAdmin(UserAdmin): 8 | pass 9 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | omit = *migrations*, 4 | *urls*, 5 | *test*, 6 | *admin*, 7 | ./manage.py, 8 | ./piedpiper/config/*, 9 | ./piedpiper/wsgi.py, 10 | *__init__* 11 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: piedpiper 2 | site_description: Its all about a Weissman score > 5.0 3 | repo_url: https://github.com/agconti/piedpiper-web 4 | site_dir: site 5 | copyright: Copyright © 2019, agconti. 6 | dev_addr: 0.0.0.0:8001 7 | 8 | nav: 9 | - Home: 'index.md' 10 | - API: 11 | - Authentication: 'api/authentication.md' 12 | - Users: 'api/users.md' 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: python 3 | services: 4 | - docker 5 | python: 6 | - "3.6" 7 | before_script: 8 | - docker-compose build 9 | script: 10 | - docker-compose run --rm web bash -c "flake8 . && 11 | python wait_for_postgres.py && 12 | ./manage.py test" 13 | 14 | notifications: 15 | email: false 16 | 17 | cache: 18 | pip: true 19 | -------------------------------------------------------------------------------- /piedpiper/users/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | 3 | 4 | class IsUserOrReadOnly(permissions.BasePermission): 5 | """ 6 | Object-level permission to only allow owners of an object to edit it. 7 | """ 8 | 9 | def has_object_permission(self, request, view, obj): 10 | 11 | if request.method in permissions.SAFE_METHODS: 12 | return True 13 | 14 | return obj == request.user 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6 2 | ENV PYTHONUNBUFFERED 1 3 | 4 | # Allows docker to cache installed dependencies between builds 5 | COPY ./requirements.txt requirements.txt 6 | RUN pip install -r requirements.txt 7 | 8 | # Adds our application code to the image 9 | COPY . code 10 | WORKDIR code 11 | 12 | EXPOSE 8000 13 | 14 | # Run the production server 15 | CMD newrelic-admin run-program gunicorn --bind 0.0.0.0:$PORT --access-logfile - piedpiper.wsgi:application 16 | -------------------------------------------------------------------------------- /fooo/fooo/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for fooo project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'fooo.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /fooo/fooo/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for fooo project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'fooo.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /piedpiper/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for piedpiper project. 3 | It exposes the WSGI callable as a module-level variable named ``application``. 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/gunicorn/ 6 | """ 7 | import os 8 | 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "piedpiper.config") 10 | os.environ.setdefault("DJANGO_CONFIGURATION", "Production") 11 | 12 | from configurations.wsgi import get_wsgi_application # noqa 13 | application = get_wsgi_application() 14 | -------------------------------------------------------------------------------- /piedpiper/users/migrations/0002_auto_20171227_2246.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0 on 2017-12-27 22:46 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('users', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='user', 15 | name='last_name', 16 | field=models.CharField(blank=True, max_length=150, verbose_name='last name'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /piedpiper/users/test/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | 4 | class UserFactory(factory.django.DjangoModelFactory): 5 | 6 | class Meta: 7 | model = 'users.User' 8 | django_get_or_create = ('username',) 9 | 10 | id = factory.Faker('uuid4') 11 | username = factory.Sequence(lambda n: f'testuser{n}') 12 | password = factory.Faker('password', length=10, special_chars=True, digits=True, 13 | upper_case=True, lower_case=True) 14 | email = factory.Faker('email') 15 | first_name = factory.Faker('first_name') 16 | last_name = factory.Faker('last_name') 17 | is_active = True 18 | is_staff = False 19 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | postgres: 5 | image: postgres:9.6 6 | web: 7 | restart: always 8 | environment: 9 | - DJANGO_SECRET_KEY=local 10 | image: web 11 | build: ./ 12 | command: > 13 | bash -c "python wait_for_postgres.py && 14 | ./manage.py migrate && 15 | ./manage.py runserver 0.0.0.0:8000" 16 | volumes: 17 | - ./:/code 18 | ports: 19 | - "8000:8000" 20 | depends_on: 21 | - postgres 22 | documentation: 23 | restart: always 24 | build: ./ 25 | command: "mkdocs serve" 26 | volumes: 27 | - ./:/code 28 | ports: 29 | - "8001:8001" 30 | -------------------------------------------------------------------------------- /piedpiper/users/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from django.db import models 3 | from django.conf import settings 4 | from django.dispatch import receiver 5 | from django.contrib.auth.models import AbstractUser 6 | from django.db.models.signals import post_save 7 | from rest_framework.authtoken.models import Token 8 | 9 | 10 | class User(AbstractUser): 11 | id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) 12 | 13 | def __str__(self): 14 | return self.username 15 | 16 | 17 | @receiver(post_save, sender=settings.AUTH_USER_MODEL) 18 | def create_auth_token(sender, instance=None, created=False, **kwargs): 19 | if created: 20 | Token.objects.create(user=instance) 21 | -------------------------------------------------------------------------------- /fooo/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'fooo.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /piedpiper/config/local.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .common import Common 3 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 4 | 5 | 6 | class Local(Common): 7 | DEBUG = True 8 | 9 | # Testing 10 | INSTALLED_APPS = Common.INSTALLED_APPS 11 | INSTALLED_APPS += ('django_nose',) 12 | TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' 13 | NOSE_ARGS = [ 14 | BASE_DIR, 15 | '-s', 16 | '--nologcapture', 17 | '--with-coverage', 18 | '--with-progressive', 19 | '--cover-package=piedpiper' 20 | ] 21 | 22 | # Mail 23 | EMAIL_HOST = 'localhost' 24 | EMAIL_PORT = 1025 25 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 26 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Core 2 | pytz==2019.3 3 | Django==3.0.2 4 | django-configurations==2.2 5 | gunicorn==20.0.4 6 | newrelic==5.4.1.134 7 | 8 | # For the persistence stores 9 | psycopg2-binary==2.8.4 10 | dj-database-url==0.5.0 11 | 12 | # Model Tools 13 | django-model-utils==4.0.0 14 | django_unique_upload==0.2.1 15 | 16 | # Rest apis 17 | djangorestframework==3.11.0 18 | Markdown==3.1.1 19 | django-filter==2.2.0 20 | 21 | # Developer Tools 22 | ipdb==0.12.3 23 | ipython==7.11.1 24 | mkdocs==1.0.4 25 | flake8==3.7.9 26 | 27 | # Testing 28 | mock==3.0.5 29 | factory-boy==2.12.0 30 | django-nose==1.4.6 31 | nose-progressive==1.5.2 32 | coverage==5.0.3 33 | 34 | # Static and Media Storage 35 | django-storages==1.8.0 36 | boto3==1.18.50 37 | 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # piedpiper-web 2 | 3 | [![Build Status](https://travis-ci.org/agconti/piedpiper-web.svg?branch=master)](https://travis-ci.org/agconti/piedpiper-web) 4 | [![Built with](https://img.shields.io/badge/Built_with-Cookiecutter_Django_Rest-F7B633.svg)](https://github.com/agconti/cookiecutter-django-rest) 5 | 6 | Its all about a Weissman score > 5.0. Check out the project's [documentation](http://agconti.github.io/piedpiper-web/). 7 | 8 | # Prerequisites 9 | 10 | - [Docker](https://docs.docker.com/docker-for-mac/install/) 11 | 12 | # Local Development 13 | 14 | Start the dev server for local development: 15 | ```bash 16 | docker-compose up 17 | ``` 18 | 19 | Run a command inside the docker container: 20 | 21 | ```bash 22 | docker-compose run --rm web [command] 23 | ``` 24 | -------------------------------------------------------------------------------- /fooo/fooo/urls.py: -------------------------------------------------------------------------------- 1 | """fooo URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path 18 | 19 | urlpatterns = [ 20 | path('admin/', admin.site.urls), 21 | ] 22 | -------------------------------------------------------------------------------- /piedpiper/users/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import User 3 | 4 | 5 | class UserSerializer(serializers.ModelSerializer): 6 | 7 | class Meta: 8 | model = User 9 | fields = ('id', 'username', 'first_name', 'last_name',) 10 | read_only_fields = ('username', ) 11 | 12 | 13 | class CreateUserSerializer(serializers.ModelSerializer): 14 | 15 | def create(self, validated_data): 16 | # call create_user on user object. Without this 17 | # the password will be stored in plain text. 18 | user = User.objects.create_user(**validated_data) 19 | return user 20 | 21 | class Meta: 22 | model = User 23 | fields = ('id', 'username', 'password', 'first_name', 'last_name', 'email', 'auth_token',) 24 | read_only_fields = ('auth_token',) 25 | extra_kwargs = {'password': {'write_only': True}} 26 | -------------------------------------------------------------------------------- /piedpiper/users/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets, mixins 2 | from rest_framework.permissions import AllowAny 3 | from .models import User 4 | from .permissions import IsUserOrReadOnly 5 | from .serializers import CreateUserSerializer, UserSerializer 6 | 7 | 8 | class UserViewSet(mixins.RetrieveModelMixin, 9 | mixins.UpdateModelMixin, 10 | viewsets.GenericViewSet): 11 | """ 12 | Updates and retrieves user accounts 13 | """ 14 | queryset = User.objects.all() 15 | serializer_class = UserSerializer 16 | permission_classes = (IsUserOrReadOnly,) 17 | 18 | 19 | class UserCreateViewSet(mixins.CreateModelMixin, 20 | viewsets.GenericViewSet): 21 | """ 22 | Creates user accounts 23 | """ 24 | queryset = User.objects.all() 25 | serializer_class = CreateUserSerializer 26 | permission_classes = (AllowAny,) 27 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "piedpiper.config") 7 | os.environ.setdefault("DJANGO_CONFIGURATION", "Local") 8 | 9 | try: 10 | from configurations.management import execute_from_command_line 11 | except ImportError: 12 | # The above import may fail for some other reason. Ensure that the 13 | # issue is really that Django is missing to avoid masking other 14 | # exceptions on Python 2. 15 | try: 16 | import django # noqa 17 | except ImportError: 18 | raise ImportError( 19 | "Couldn't import Django. Are you sure it's installed and " 20 | "available on your PYTHONPATH environment variable? Did you " 21 | "forget to activate a virtual environment?" 22 | ) 23 | raise 24 | execute_from_command_line(sys.argv) 25 | -------------------------------------------------------------------------------- /piedpiper/users/test/test_serializers.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.forms.models import model_to_dict 3 | from django.contrib.auth.hashers import check_password 4 | from nose.tools import eq_, ok_ 5 | from .factories import UserFactory 6 | from ..serializers import CreateUserSerializer 7 | 8 | 9 | class TestCreateUserSerializer(TestCase): 10 | 11 | def setUp(self): 12 | self.user_data = model_to_dict(UserFactory.build()) 13 | 14 | def test_serializer_with_empty_data(self): 15 | serializer = CreateUserSerializer(data={}) 16 | eq_(serializer.is_valid(), False) 17 | 18 | def test_serializer_with_valid_data(self): 19 | serializer = CreateUserSerializer(data=self.user_data) 20 | ok_(serializer.is_valid()) 21 | 22 | def test_serializer_hashes_password(self): 23 | serializer = CreateUserSerializer(data=self.user_data) 24 | ok_(serializer.is_valid()) 25 | 26 | user = serializer.save() 27 | ok_(check_password(self.user_data.get('password'), user.password)) 28 | -------------------------------------------------------------------------------- /piedpiper/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.urls import path, re_path, include, reverse_lazy 3 | from django.conf.urls.static import static 4 | from django.contrib import admin 5 | from django.views.generic.base import RedirectView 6 | from rest_framework.routers import DefaultRouter 7 | from rest_framework.authtoken import views 8 | from .users.views import UserViewSet, UserCreateViewSet 9 | 10 | router = DefaultRouter() 11 | router.register(r'users', UserViewSet) 12 | router.register(r'users', UserCreateViewSet) 13 | 14 | urlpatterns = [ 15 | path('admin/', admin.site.urls), 16 | path('api/v1/', include(router.urls)), 17 | path('api-token-auth/', views.obtain_auth_token), 18 | path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), 19 | 20 | # the 'api-root' from django rest-frameworks default router 21 | # http://www.django-rest-framework.org/api-guide/routers/#defaultrouter 22 | re_path(r'^$', RedirectView.as_view(url=reverse_lazy('api-root'), permanent=False)), 23 | 24 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 25 | -------------------------------------------------------------------------------- /wait_for_postgres.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | from time import time, sleep 4 | import psycopg2 5 | check_timeout = os.getenv("POSTGRES_CHECK_TIMEOUT", 30) 6 | check_interval = os.getenv("POSTGRES_CHECK_INTERVAL", 1) 7 | interval_unit = "second" if check_interval == 1 else "seconds" 8 | config = { 9 | "dbname": os.getenv("POSTGRES_DB", "postgres"), 10 | "user": os.getenv("POSTGRES_USER", "postgres"), 11 | "password": os.getenv("POSTGRES_PASSWORD", ""), 12 | "host": os.getenv("DATABASE_URL", "postgres") 13 | } 14 | 15 | start_time = time() 16 | logger = logging.getLogger() 17 | logger.setLevel(logging.INFO) 18 | logger.addHandler(logging.StreamHandler()) 19 | 20 | 21 | def pg_isready(host, user, password, dbname): 22 | while time() - start_time < check_timeout: 23 | try: 24 | conn = psycopg2.connect(**vars()) 25 | logger.info("Postgres is ready! ✨ 💅") 26 | conn.close() 27 | return True 28 | except psycopg2.OperationalError: 29 | logger.info(f"Postgres isn't ready. Waiting for {check_interval} {interval_unit}...") 30 | sleep(check_interval) 31 | 32 | logger.error(f"We could not connect to Postgres within {check_timeout} seconds.") 33 | return False 34 | 35 | 36 | pg_isready(**config) 37 | -------------------------------------------------------------------------------- /docs/api/authentication.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | For clients to authenticate, the token key should be included in the Authorization HTTP header. The key should be prefixed by the string literal "Token", with whitespace separating the two strings. For example: 3 | 4 | ``` 5 | Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b 6 | ``` 7 | 8 | Unauthenticated responses that are denied permission will result in an HTTP `401 Unauthorized` response with an appropriate `WWW-Authenticate` header. For example: 9 | 10 | ``` 11 | WWW-Authenticate: Token 12 | ``` 13 | 14 | The curl command line tool may be useful for testing token authenticated APIs. For example: 15 | 16 | ```bash 17 | curl -X GET http://127.0.0.1:8000/api/v1/example/ -H 'Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b' 18 | ``` 19 | 20 | ## Retrieving Tokens 21 | Authorization tokens are issued and returned when a user registers. A registered user can also retrieve their token with the following request: 22 | 23 | **Request**: 24 | 25 | `POST` `api-token-auth/` 26 | 27 | Parameters: 28 | 29 | Name | Type | Description 30 | ---|---|--- 31 | username | string | The user's username 32 | password | string | The user's password 33 | 34 | **Response**: 35 | ```json 36 | { 37 | "token" : "9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b" 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /piedpiper/config/production.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .common import Common 3 | 4 | 5 | class Production(Common): 6 | INSTALLED_APPS = Common.INSTALLED_APPS 7 | SECRET_KEY = os.getenv('DJANGO_SECRET_KEY') 8 | # Site 9 | # https://docs.djangoproject.com/en/2.0/ref/settings/#allowed-hosts 10 | ALLOWED_HOSTS = ["*"] 11 | INSTALLED_APPS += ("gunicorn", ) 12 | 13 | # Static files (CSS, JavaScript, Images) 14 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 15 | # http://django-storages.readthedocs.org/en/latest/index.html 16 | INSTALLED_APPS += ('storages',) 17 | DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' 18 | STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' 19 | AWS_ACCESS_KEY_ID = os.getenv('DJANGO_AWS_ACCESS_KEY_ID') 20 | AWS_SECRET_ACCESS_KEY = os.getenv('DJANGO_AWS_SECRET_ACCESS_KEY') 21 | AWS_STORAGE_BUCKET_NAME = os.getenv('DJANGO_AWS_STORAGE_BUCKET_NAME') 22 | AWS_DEFAULT_ACL = 'public-read' 23 | AWS_AUTO_CREATE_BUCKET = True 24 | AWS_QUERYSTRING_AUTH = False 25 | MEDIA_URL = f'https://s3.amazonaws.com/{AWS_STORAGE_BUCKET_NAME}/' 26 | 27 | # https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#cache-control 28 | # Response can be cached by browser and any intermediary caches (i.e. it is "public") for up to 1 day 29 | # 86400 = (60 seconds x 60 minutes x 24 hours) 30 | AWS_HEADERS = { 31 | 'Cache-Control': 'max-age=86400, s-maxage=86400, must-revalidate', 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | .static_storage/ 56 | .media/ 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # django staticfiles 104 | /static 105 | 106 | # mypy 107 | .mypy_cache/ 108 | -------------------------------------------------------------------------------- /piedpiper/users/test/test_views.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from django.contrib.auth.hashers import check_password 3 | from nose.tools import ok_, eq_ 4 | from rest_framework.test import APITestCase 5 | from rest_framework import status 6 | from faker import Faker 7 | import factory 8 | from ..models import User 9 | from .factories import UserFactory 10 | 11 | fake = Faker() 12 | 13 | 14 | class TestUserListTestCase(APITestCase): 15 | """ 16 | Tests /users list operations. 17 | """ 18 | 19 | def setUp(self): 20 | self.url = reverse('user-list') 21 | self.user_data = factory.build(dict, FACTORY_CLASS=UserFactory) 22 | 23 | def test_post_request_with_no_data_fails(self): 24 | response = self.client.post(self.url, {}) 25 | eq_(response.status_code, status.HTTP_400_BAD_REQUEST) 26 | 27 | def test_post_request_with_valid_data_succeeds(self): 28 | response = self.client.post(self.url, self.user_data) 29 | eq_(response.status_code, status.HTTP_201_CREATED) 30 | 31 | user = User.objects.get(pk=response.data.get('id')) 32 | eq_(user.username, self.user_data.get('username')) 33 | ok_(check_password(self.user_data.get('password'), user.password)) 34 | 35 | 36 | class TestUserDetailTestCase(APITestCase): 37 | """ 38 | Tests /users detail operations. 39 | """ 40 | 41 | def setUp(self): 42 | self.user = UserFactory() 43 | self.url = reverse('user-detail', kwargs={'pk': self.user.pk}) 44 | self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.user.auth_token}') 45 | 46 | def test_get_request_returns_a_given_user(self): 47 | response = self.client.get(self.url) 48 | eq_(response.status_code, status.HTTP_200_OK) 49 | 50 | def test_put_request_updates_a_user(self): 51 | new_first_name = fake.first_name() 52 | payload = {'first_name': new_first_name} 53 | response = self.client.put(self.url, payload) 54 | eq_(response.status_code, status.HTTP_200_OK) 55 | 56 | user = User.objects.get(pk=self.user.id) 57 | eq_(user.first_name, new_first_name) 58 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # piedpiper-web 2 | 3 | [![Build Status](https://travis-ci.org/agconti/piedpiper-web.svg?branch=master)](https://travis-ci.org/agconti/piedpiper-web) 4 | [![Built with](https://img.shields.io/badge/Built_with-Cookiecutter_Django_Rest-F7B633.svg)](https://github.com/agconti/cookiecutter-django-rest) 5 | 6 | Its all about a Weissman score > 5.0. Check out the project's [documentation](http://agconti.github.io/piedpiper-web/). 7 | 8 | # Prerequisites 9 | 10 | - [Docker](https://docs.docker.com/docker-for-mac/install/) 11 | - [Travis CLI](http://blog.travis-ci.com/2013-01-14-new-client/) 12 | - [Heroku Toolbelt](https://toolbelt.heroku.com/) 13 | 14 | # Initialize the project 15 | 16 | Start the dev server for local development: 17 | 18 | ```bash 19 | docker-compose up 20 | ``` 21 | 22 | Create a superuser to login to the admin: 23 | 24 | ```bash 25 | docker-compose run --rm web ./manage.py createsuperuser 26 | ``` 27 | 28 | 29 | # Continuous Deployment 30 | 31 | Deployment automated via Travis. When builds pass on the master or qa branch, Travis will deploy that branch to Heroku. Enable this by: 32 | 33 | Creating the production sever: 34 | 35 | ``` 36 | heroku create piedpiper-prod --remote prod && \ 37 | heroku addons:create newrelic:wayne --app piedpiper-prod && \ 38 | heroku addons:create heroku-postgresql:hobby-dev --app piedpiper-prod && \ 39 | heroku config:set DJANGO_SECRET=`openssl rand -base64 32` \ 40 | DJANGO_AWS_ACCESS_KEY_ID="Add your id" \ 41 | DJANGO_AWS_SECRET_ACCESS_KEY="Add your key" \ 42 | DJANGO_AWS_STORAGE_BUCKET_NAME="piedpiper-prod" \ 43 | --app piedpiper-prod 44 | ``` 45 | 46 | Creating the qa sever: 47 | 48 | ``` 49 | heroku create `piedpiper-qa --remote qa && \ 50 | heroku addons:create newrelic:wayne && \ 51 | heroku addons:create heroku-postgresql:hobby-dev && \ 52 | heroku config:set DJANGO_SECRET=`openssl rand -base64 32` \ 53 | DJANGO_AWS_ACCESS_KEY_ID="Add your id" \ 54 | DJANGO_AWS_SECRET_ACCESS_KEY="Add your key" \ 55 | DJANGO_AWS_STORAGE_BUCKET_NAME="piedpiper-qa" \ 56 | ``` 57 | 58 | Securely add your heroku credentials to travis so it can automatically deploy your changes. 59 | 60 | ```bash 61 | travis encrypt HEROKU_AUTH_TOKEN="$(heroku auth:token)" --add 62 | ``` 63 | 64 | Commit your changes and push to master and qa to trigger your first deploys: 65 | 66 | ```bash 67 | git commit -m "ci(travis): added heroku credentials" && \ 68 | git push origin master && \ 69 | git checkout -b qa && \ 70 | git push -u origin qa 71 | ``` 72 | You're ready to continuously ship! ✨ 💅 🛳 73 | -------------------------------------------------------------------------------- /docs/api/users.md: -------------------------------------------------------------------------------- 1 | # Users 2 | Supports registering, viewing, and updating user accounts. 3 | 4 | ## Register a new user account 5 | 6 | **Request**: 7 | 8 | `POST` `/users/` 9 | 10 | Parameters: 11 | 12 | Name | Type | Required | Description 13 | -----------|--------|----------|------------ 14 | username | string | Yes | The username for the new user. 15 | password | string | Yes | The password for the new user account. 16 | first_name | string | No | The user's given name. 17 | last_name | string | No | The user's family name. 18 | email | string | No | The user's email address. 19 | 20 | *Note:* 21 | 22 | - Not Authorization Protected 23 | 24 | **Response**: 25 | 26 | ```json 27 | Content-Type application/json 28 | 201 Created 29 | 30 | { 31 | "id": "6d5f9bae-a31b-4b7b-82c4-3853eda2b011", 32 | "username": "richard", 33 | "first_name": "Richard", 34 | "last_name": "Hendriks", 35 | "email": "richard@piedpiper.com", 36 | "auth_token": "132cf952e0165a274bf99e115ab483671b3d9ff6" 37 | } 38 | ``` 39 | 40 | The `auth_token` returned with this response should be stored by the client for 41 | authenticating future requests to the API. See [Authentication](authentication.md). 42 | 43 | 44 | ## Get a user's profile information 45 | 46 | **Request**: 47 | 48 | `GET` `/users/:id` 49 | 50 | Parameters: 51 | 52 | *Note:* 53 | 54 | - **[Authorization Protected](authentication.md)** 55 | 56 | **Response**: 57 | 58 | ```json 59 | Content-Type application/json 60 | 200 OK 61 | 62 | { 63 | "id": "6d5f9bae-a31b-4b7b-82c4-3853eda2b011", 64 | "username": "richard", 65 | "first_name": "Richard", 66 | "last_name": "Hendriks", 67 | "email": "richard@piedpiper.com", 68 | } 69 | ``` 70 | 71 | 72 | ## Update your profile information 73 | 74 | **Request**: 75 | 76 | `PUT/PATCH` `/users/:id` 77 | 78 | Parameters: 79 | 80 | Name | Type | Description 81 | -----------|--------|--- 82 | first_name | string | The first_name of the user object. 83 | last_name | string | The last_name of the user object. 84 | email | string | The user's email address. 85 | 86 | 87 | 88 | *Note:* 89 | 90 | - All parameters are optional 91 | - **[Authorization Protected](authentication.md)** 92 | 93 | **Response**: 94 | 95 | ```json 96 | Content-Type application/json 97 | 200 OK 98 | 99 | { 100 | "id": "6d5f9bae-a31b-4b7b-82c4-3853eda2b011", 101 | "username": "richard", 102 | "first_name": "Richard", 103 | "last_name": "Hendriks", 104 | "email": "richard@piedpiper.com", 105 | } 106 | ``` 107 | -------------------------------------------------------------------------------- /piedpiper/users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.8 on 2017-12-21 03:04 3 | from __future__ import unicode_literals 4 | 5 | import django.contrib.auth.models 6 | import django.contrib.auth.validators 7 | from django.db import migrations, models 8 | import django.utils.timezone 9 | import uuid 10 | 11 | 12 | class Migration(migrations.Migration): 13 | 14 | initial = True 15 | 16 | dependencies = [ 17 | ('auth', '0008_alter_user_username_max_length'), 18 | ] 19 | 20 | operations = [ 21 | migrations.CreateModel( 22 | name='User', 23 | fields=[ 24 | ('password', models.CharField(max_length=128, verbose_name='password')), 25 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 26 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 27 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), 28 | ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), 29 | ('last_name', models.CharField(blank=True, max_length=30, verbose_name='last name')), 30 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), 31 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 32 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 33 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 34 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 35 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), 36 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), 37 | ], 38 | options={ 39 | 'verbose_name': 'user', 40 | 'verbose_name_plural': 'users', 41 | 'abstract': False, 42 | }, 43 | managers=[ 44 | ('objects', django.contrib.auth.models.UserManager()), 45 | ], 46 | ), 47 | ] 48 | -------------------------------------------------------------------------------- /fooo/fooo/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for fooo project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.0.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'mduf()jw70crqnlr9z%_5r)6m=6!o66qritr)ih6**tth5j)1j' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | ] 41 | 42 | MIDDLEWARE = [ 43 | 'django.middleware.security.SecurityMiddleware', 44 | 'django.contrib.sessions.middleware.SessionMiddleware', 45 | 'django.middleware.common.CommonMiddleware', 46 | 'django.middleware.csrf.CsrfViewMiddleware', 47 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 48 | 'django.contrib.messages.middleware.MessageMiddleware', 49 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 50 | ] 51 | 52 | ROOT_URLCONF = 'fooo.urls' 53 | 54 | TEMPLATES = [ 55 | { 56 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 57 | 'DIRS': [], 58 | 'APP_DIRS': True, 59 | 'OPTIONS': { 60 | 'context_processors': [ 61 | 'django.template.context_processors.debug', 62 | 'django.template.context_processors.request', 63 | 'django.contrib.auth.context_processors.auth', 64 | 'django.contrib.messages.context_processors.messages', 65 | ], 66 | }, 67 | }, 68 | ] 69 | 70 | WSGI_APPLICATION = 'fooo.wsgi.application' 71 | 72 | 73 | # Database 74 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 75 | 76 | DATABASES = { 77 | 'default': { 78 | 'ENGINE': 'django.db.backends.sqlite3', 79 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 80 | } 81 | } 82 | 83 | 84 | # Password validation 85 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators 86 | 87 | AUTH_PASSWORD_VALIDATORS = [ 88 | { 89 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 90 | }, 91 | { 92 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 93 | }, 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 99 | }, 100 | ] 101 | 102 | 103 | # Internationalization 104 | # https://docs.djangoproject.com/en/3.0/topics/i18n/ 105 | 106 | LANGUAGE_CODE = 'en-us' 107 | 108 | TIME_ZONE = 'UTC' 109 | 110 | USE_I18N = True 111 | 112 | USE_L10N = True 113 | 114 | USE_TZ = True 115 | 116 | 117 | # Static files (CSS, JavaScript, Images) 118 | # https://docs.djangoproject.com/en/3.0/howto/static-files/ 119 | 120 | STATIC_URL = '/static/' 121 | -------------------------------------------------------------------------------- /piedpiper/config/common.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os.path import join 3 | from distutils.util import strtobool 4 | import dj_database_url 5 | from configurations import Configuration 6 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 7 | 8 | 9 | class Common(Configuration): 10 | 11 | INSTALLED_APPS = ( 12 | 'django.contrib.admin', 13 | 'django.contrib.auth', 14 | 'django.contrib.contenttypes', 15 | 'django.contrib.sessions', 16 | 'django.contrib.messages', 17 | 'django.contrib.staticfiles', 18 | 19 | 20 | # Third party apps 21 | 'rest_framework', # utilities for rest apis 22 | 'rest_framework.authtoken', # token authentication 23 | 'django_filters', # for filtering rest endpoints 24 | 25 | # Your apps 26 | 'piedpiper.users', 27 | 28 | ) 29 | 30 | # https://docs.djangoproject.com/en/2.0/topics/http/middleware/ 31 | MIDDLEWARE = ( 32 | 'django.middleware.security.SecurityMiddleware', 33 | 'django.contrib.sessions.middleware.SessionMiddleware', 34 | 'django.middleware.common.CommonMiddleware', 35 | 'django.middleware.csrf.CsrfViewMiddleware', 36 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 37 | 'django.contrib.messages.middleware.MessageMiddleware', 38 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 39 | ) 40 | 41 | ALLOWED_HOSTS = ["*"] 42 | ROOT_URLCONF = 'piedpiper.urls' 43 | SECRET_KEY = os.getenv('DJANGO_SECRET_KEY') 44 | WSGI_APPLICATION = 'piedpiper.wsgi.application' 45 | 46 | # Email 47 | EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' 48 | 49 | ADMINS = ( 50 | ('Author', 'richard.hendriks@piedpiper.com'), 51 | ) 52 | 53 | # Postgres 54 | DATABASES = { 55 | 'default': dj_database_url.config( 56 | default='postgres://postgres:@postgres:5432/postgres', 57 | conn_max_age=int(os.getenv('POSTGRES_CONN_MAX_AGE', 600)) 58 | ) 59 | } 60 | 61 | # General 62 | APPEND_SLASH = False 63 | TIME_ZONE = 'UTC' 64 | LANGUAGE_CODE = 'en-us' 65 | # If you set this to False, Django will make some optimizations so as not 66 | # to load the internationalization machinery. 67 | USE_I18N = False 68 | USE_L10N = True 69 | USE_TZ = True 70 | LOGIN_REDIRECT_URL = '/' 71 | 72 | # Static files (CSS, JavaScript, Images) 73 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 74 | STATIC_ROOT = os.path.normpath(join(os.path.dirname(BASE_DIR), 'static')) 75 | STATICFILES_DIRS = [] 76 | STATIC_URL = '/static/' 77 | STATICFILES_FINDERS = ( 78 | 'django.contrib.staticfiles.finders.FileSystemFinder', 79 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 80 | ) 81 | 82 | # Media files 83 | MEDIA_ROOT = join(os.path.dirname(BASE_DIR), 'media') 84 | MEDIA_URL = '/media/' 85 | 86 | TEMPLATES = [ 87 | { 88 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 89 | 'DIRS': STATICFILES_DIRS, 90 | 'APP_DIRS': True, 91 | 'OPTIONS': { 92 | 'context_processors': [ 93 | 'django.template.context_processors.debug', 94 | 'django.template.context_processors.request', 95 | 'django.contrib.auth.context_processors.auth', 96 | 'django.contrib.messages.context_processors.messages', 97 | ], 98 | }, 99 | }, 100 | ] 101 | 102 | # Set DEBUG to False as a default for safety 103 | # https://docs.djangoproject.com/en/dev/ref/settings/#debug 104 | DEBUG = strtobool(os.getenv('DJANGO_DEBUG', 'no')) 105 | 106 | # Password Validation 107 | # https://docs.djangoproject.com/en/2.0/topics/auth/passwords/#module-django.contrib.auth.password_validation 108 | AUTH_PASSWORD_VALIDATORS = [ 109 | { 110 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 111 | }, 112 | { 113 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 114 | }, 115 | { 116 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 117 | }, 118 | { 119 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 120 | }, 121 | ] 122 | 123 | # Logging 124 | LOGGING = { 125 | 'version': 1, 126 | 'disable_existing_loggers': False, 127 | 'formatters': { 128 | 'django.server': { 129 | '()': 'django.utils.log.ServerFormatter', 130 | 'format': '[%(server_time)s] %(message)s', 131 | }, 132 | 'verbose': { 133 | 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' 134 | }, 135 | 'simple': { 136 | 'format': '%(levelname)s %(message)s' 137 | }, 138 | }, 139 | 'filters': { 140 | 'require_debug_true': { 141 | '()': 'django.utils.log.RequireDebugTrue', 142 | }, 143 | }, 144 | 'handlers': { 145 | 'django.server': { 146 | 'level': 'INFO', 147 | 'class': 'logging.StreamHandler', 148 | 'formatter': 'django.server', 149 | }, 150 | 'console': { 151 | 'level': 'DEBUG', 152 | 'class': 'logging.StreamHandler', 153 | 'formatter': 'simple' 154 | }, 155 | 'mail_admins': { 156 | 'level': 'ERROR', 157 | 'class': 'django.utils.log.AdminEmailHandler' 158 | } 159 | }, 160 | 'loggers': { 161 | 'django': { 162 | 'handlers': ['console'], 163 | 'propagate': True, 164 | }, 165 | 'django.server': { 166 | 'handlers': ['django.server'], 167 | 'level': 'INFO', 168 | 'propagate': False, 169 | }, 170 | 'django.request': { 171 | 'handlers': ['mail_admins', 'console'], 172 | 'level': 'ERROR', 173 | 'propagate': False, 174 | }, 175 | 'django.db.backends': { 176 | 'handlers': ['console'], 177 | 'level': 'INFO' 178 | }, 179 | } 180 | } 181 | 182 | # Custom user app 183 | AUTH_USER_MODEL = 'users.User' 184 | 185 | # Django Rest Framework 186 | REST_FRAMEWORK = { 187 | 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 188 | 'PAGE_SIZE': int(os.getenv('DJANGO_PAGINATION_LIMIT', 10)), 189 | 'DATETIME_FORMAT': '%Y-%m-%dT%H:%M:%S%z', 190 | 'DEFAULT_RENDERER_CLASSES': ( 191 | 'rest_framework.renderers.JSONRenderer', 192 | 'rest_framework.renderers.BrowsableAPIRenderer', 193 | ), 194 | 'DEFAULT_PERMISSION_CLASSES': [ 195 | 'rest_framework.permissions.IsAuthenticated', 196 | ], 197 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 198 | 'rest_framework.authentication.SessionAuthentication', 199 | 'rest_framework.authentication.TokenAuthentication', 200 | ) 201 | } 202 | --------------------------------------------------------------------------------