├── core ├── __init__.py └── mobile_devices │ ├── __init__.py │ ├── migrations │ └── __init__.py │ ├── admin.py │ ├── tests.py │ ├── views.py │ ├── apps.py │ ├── tasks.py │ └── models.py ├── templates └── .gitkeep ├── users ├── __init__.py ├── api │ ├── __init__.py │ ├── urls.py │ ├── auth_adapters.py │ ├── serializers.py │ └── views.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── views.py ├── apps.py ├── forms.py ├── admin.py ├── models.py └── tests.py ├── staticfiles └── .gitkeep ├── django_mobile_app ├── __init__.py ├── settings │ ├── __init__.py │ ├── dev.py │ ├── production.py │ └── base.py ├── wsgi_production.py ├── wsgi.py ├── celery.py └── urls.py ├── Procfile ├── docker └── web │ ├── Caddyfile │ └── Dockerfile ├── .travis.yml ├── manage.py ├── requirements.txt ├── docker-compose.yaml ├── LICENSE ├── .gitignore └── README.md /core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /staticfiles/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /users/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /users/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/mobile_devices/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_mobile_app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_mobile_app/settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/mobile_devices/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn django_mobile_app.wsgi -------------------------------------------------------------------------------- /users/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /core/mobile_devices/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /core/mobile_devices/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /core/mobile_devices/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | name = 'users' 6 | -------------------------------------------------------------------------------- /docker/web/Caddyfile: -------------------------------------------------------------------------------- 1 | domain.tld { 2 | root /var/www/project/folder 3 | proxy / localhost:8000 { 4 | transparent 5 | } 6 | } -------------------------------------------------------------------------------- /core/mobile_devices/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MobileDevicesConfig(AppConfig): 5 | name = 'core.mobile_devices' 6 | -------------------------------------------------------------------------------- /users/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from .views import MeApiHandler 4 | 5 | urlpatterns = [ 6 | url(r"^me$", MeApiHandler.as_view(), name='api_accounts_me'), 7 | ] 8 | -------------------------------------------------------------------------------- /users/forms.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.forms import UserChangeForm 2 | 3 | from .models import Account 4 | 5 | 6 | class AccountChangeForm(UserChangeForm): 7 | class Meta(UserChangeForm.Meta): 8 | model = Account 9 | -------------------------------------------------------------------------------- /django_mobile_app/wsgi_production.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.wsgi import get_wsgi_application 4 | 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_mobile_app.settings.production") 6 | 7 | application = get_wsgi_application() 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | services: 4 | - postgresql 5 | 6 | addons: 7 | postgresql: "9.6" 8 | 9 | python: 10 | - "3.6" 11 | 12 | install: 13 | - "pip install -r requirements.txt" 14 | 15 | before_script: 16 | - psql -c 'create database test;' -U postgres 17 | - python manage.py migrate 18 | 19 | script: python manage.py test -------------------------------------------------------------------------------- /users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | 4 | from .forms import AccountChangeForm 5 | from .models import Account 6 | 7 | 8 | class AccountsAdmin(UserAdmin): 9 | form = AccountChangeForm 10 | list_display = ('username', 'first_name', 'last_name', 'last_login') 11 | 12 | 13 | admin.site.register(Account, AccountsAdmin) 14 | -------------------------------------------------------------------------------- /django_mobile_app/settings/dev.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | env = environ.Env() 4 | environ.Env.read_env() # the .env file should be in the settings path, not the manage.py path 5 | 6 | SECRET_KEY = env('SECRET_KEY') 7 | 8 | DEBUG = env('DEBUG', default=True) 9 | 10 | DATABASES = { 11 | 'default': env.db(), # Raises ImproperlyConfigured exception if DATABASE_URL not in os.environ 12 | } 13 | -------------------------------------------------------------------------------- /users/api/auth_adapters.py: -------------------------------------------------------------------------------- 1 | from rest_auth.registration.views import SocialLoginView 2 | from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter 3 | from allauth.socialaccount.providers.instagram.views import InstagramOAuth2Adapter 4 | 5 | 6 | class FacebookLogin(SocialLoginView): 7 | adapter_class = FacebookOAuth2Adapter 8 | 9 | 10 | class InstagramLogin(SocialLoginView): 11 | adapter_class = InstagramOAuth2Adapter 12 | -------------------------------------------------------------------------------- /docker/web/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6 2 | MAINTAINER Konstantinos Livieratos 3 | 4 | # Make sure all logs are correctly printed 5 | ENV PYTHONUNBUFFERED=1 6 | 7 | # Prepare source code directory 8 | RUN mkdir -p /usr/src/app 9 | WORKDIR /usr/src/app 10 | 11 | # Update setuptools 12 | RUN pip install -U setuptools 13 | 14 | # Install dependencies first, for better caching 15 | COPY requirements.txt /usr/src/app/ 16 | RUN pip install -r requirements.txt 17 | COPY ./ /usr/src/app/ -------------------------------------------------------------------------------- /django_mobile_app/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_mobile_app 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/1.11/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", "django_mobile_app.settings.dev") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /users/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractUser 2 | 3 | from django.conf import settings 4 | from django.db.models.signals import post_save 5 | from django.dispatch import receiver 6 | from rest_framework.authtoken.models import Token 7 | 8 | 9 | class Account(AbstractUser): 10 | """ 11 | This has been intentionally left empty, so that you can fill it in as you'd like. 12 | """ 13 | pass 14 | 15 | 16 | @receiver(post_save, sender=settings.AUTH_USER_MODEL) 17 | def create_auth_token(sender, instance=None, created=False, **kwargs): 18 | if created: 19 | Token.objects.create(user=instance) 20 | -------------------------------------------------------------------------------- /users/api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from ..models import Account 4 | 5 | 6 | class AccountSerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = Account 9 | fields = ( 10 | 'id', 'username', 'first_name', 'last_name', 'email', 11 | 'date_joined', 'last_login' 12 | ) 13 | read_only_fields = ( 14 | 'id', 'username', 'date_joined', 'last_login', 15 | ) 16 | 17 | 18 | class PublicScopeAccountSerializer(serializers.ModelSerializer): 19 | class Meta: 20 | model = Account 21 | fields = ( 22 | 'id', 'username', 'first_name', 'last_name', 'email' 23 | ) 24 | read_only_fields = fields 25 | -------------------------------------------------------------------------------- /users/api/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.decorators import permission_classes 2 | from rest_framework.generics import RetrieveUpdateAPIView 3 | from rest_framework.parsers import FormParser, MultiPartParser, JSONParser 4 | from rest_framework.permissions import IsAuthenticated 5 | 6 | from .serializers import AccountSerializer 7 | 8 | 9 | class MeApiHandler(RetrieveUpdateAPIView): 10 | """ 11 | API Endpoint that returns currently logged-in account's user information. 12 | This includes information that is not publicly-available. 13 | """ 14 | permission_classes(IsAuthenticated) 15 | parser_classes = [FormParser, MultiPartParser, JSONParser] 16 | serializer_class = AccountSerializer 17 | 18 | def get_object(self): 19 | return self.request.user 20 | -------------------------------------------------------------------------------- /django_mobile_app/celery.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | import os 3 | from celery import Celery 4 | 5 | # set the default Django settings module for the 'celery' program. 6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_mobile_app.settings') 7 | 8 | app = Celery('django_mobile_app') 9 | 10 | # Using a string here means the worker doesn't have to serialize 11 | # the configuration object to child processes. 12 | # - namespace='CELERY' means all celery-related configuration keys 13 | # should have a `CELERY_` prefix. 14 | app.config_from_object('django.conf:settings', namespace='CELERY') 15 | 16 | # Load task modules from all registered Django app configs. 17 | app.autodiscover_tasks() 18 | 19 | 20 | @app.task(bind=True) 21 | def debug_task(self): 22 | print('Request: {0!r}'.format(self.request)) -------------------------------------------------------------------------------- /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", "django_mobile_app.settings.dev") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /django_mobile_app/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url, include 2 | from django.contrib import admin 3 | from rest_framework.routers import SimpleRouter 4 | from rest_framework_swagger.views import get_swagger_view 5 | 6 | from users.api.auth_adapters import FacebookLogin, InstagramLogin 7 | 8 | schema_view = get_swagger_view(title='Mobile App API') 9 | 10 | router = SimpleRouter() 11 | # register api routes using `router.register()` 12 | 13 | urlpatterns = [ 14 | url(r'^admin/', admin.site.urls), 15 | url(r'^$', schema_view), 16 | 17 | url(r'^api/v1/rest-auth/', include('rest_auth.urls')), 18 | url(r'^api/v1/rest-auth/registration/', include('rest_auth.registration.urls')), 19 | url(r'^api/v1/rest-auth/facebook/$', FacebookLogin.as_view(), name='fb_login'), 20 | url(r'^api/v1/rest-auth/instagram/$', InstagramLogin.as_view(), name='instagram_login'), 21 | url(r'^api/v1/accounts/', include('users.api.urls')), 22 | ] 23 | -------------------------------------------------------------------------------- /core/mobile_devices/tasks.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import logging 3 | from django.conf import settings 4 | from celery import shared_task 5 | 6 | 7 | @shared_task 8 | def register_device_on_sns(device): 9 | """ 10 | Registers your device on AWS SNS and attaches the ARN endpoint on the device object. 11 | The ARN endpoint is used when publishing push notifications. 12 | :param device: your device object, extending the AbstractMobileDevice. 13 | :return: - 14 | """ 15 | try: 16 | client = boto3.client('sns', region_name=settings.AWS_REGION) 17 | platform_arn = settings.AWS_IOS_APPLICATION_ARN if device.is_ios else settings.AWS_ANDROID_APPLICATION_ARN 18 | response = client.create_platform_endpoint( 19 | PlatformApplicationArn=platform_arn, 20 | Token=device.push_token, 21 | ) 22 | endpoint_arn = response.get('EndpointArn') 23 | device.arn_endpoint = endpoint_arn 24 | device.save() 25 | except Exception as e: 26 | logging.error(e) 27 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | amqp==2.2.2 2 | billiard==3.5.0.3 3 | boto3==1.4.7 4 | botocore==1.7.24 5 | celery==4.1.0 6 | certifi==2017.7.27.1 7 | chardet==3.0.4 8 | coreapi==2.3.3 9 | coreschema==0.0.4 10 | defusedxml==0.5.0 11 | dj-database-url==0.4.2 12 | Django==1.11.28 13 | django-allauth==0.33.0 14 | django-anymail==1.2.1 15 | django-environ==0.4.4 16 | django-extensions==1.9.6 17 | django-filter==1.0.4 18 | django-guardian==1.4.9 19 | django-rest-auth==0.9.2 20 | django-rest-swagger==2.1.2 21 | django-storages==1.6.5 22 | djangorestframework==3.9.1 23 | docutils==0.14 24 | gunicorn==19.7.1 25 | idna==2.6 26 | itypes==1.1.0 27 | Jinja2==2.9.6 28 | jmespath==0.9.3 29 | kombu==4.1.0 30 | MarkupSafe==1.0 31 | oauthlib==2.0.4 32 | openapi-codec==1.3.2 33 | psycopg2==2.7.3.1 34 | python-dateutil==2.6.1 35 | python3-openid==3.1.0 36 | pytz==2017.2 37 | raven==6.5.0 38 | redis==2.10.6 39 | requests==2.20.0 40 | requests-oauthlib==0.8.0 41 | s3transfer==0.1.11 42 | simplejson==3.11.1 43 | six==1.11.0 44 | uritemplate==3.0.0 45 | urllib3==1.24.2 46 | vine==1.1.4 47 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.0' 2 | 3 | services: 4 | postgres: 5 | image: postgres:latest 6 | volumes: 7 | - postgres_data:/var/lib/postgresql/data 8 | environment: 9 | POSTGRES_PASSWORD: pass 10 | POSTGRES_USER: user 11 | POSTGRES_DB: db 12 | 13 | redis: 14 | image: redis:alpine 15 | ports: 16 | - 6379 17 | volumes: 18 | - redisdata:/data 19 | 20 | web: 21 | build: ./docker/web 22 | command: python manage.py runserver 0.0.0.0:8000 23 | env_file: '.env' 24 | 25 | ports: 26 | - 8000 27 | volumes: 28 | - mediadata:/media 29 | 30 | worker: 31 | build: ./ 32 | command: celery -A django_mobile_app worker -l info --app=django_mobile_app.celery:app 33 | environment: 34 | DATABASE_URL: postgres://user:pass@postgres/db 35 | REDIS_URL: redis://redis:6379 36 | volumes_from: 37 | - web 38 | links: 39 | - redis 40 | - postgres 41 | depends_on: 42 | - redis 43 | - postgres 44 | 45 | volumes: 46 | postgres_data: 47 | redisdata: 48 | mediadata: -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2018 Intelligems Technologies OU 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /core/mobile_devices/models.py: -------------------------------------------------------------------------------- 1 | from uuid import uuid4 2 | 3 | from django.db import models 4 | from django.utils.translation import ugettext_lazy as _ 5 | 6 | 7 | class AbstractMobileDevice(models.Model): 8 | """ 9 | Abstract model to extend while creating your custom Devices model for your 10 | project. By default, this offers the bare-minimum fields to register push-enabled devices, 11 | register them on AWS SNS and store this information in the database. 12 | """ 13 | 14 | APN = 'apn' 15 | GCM = 'gcm' 16 | DEVICE_TYPES = ( 17 | (APN, 'APN'), 18 | (GCM, 'GCM') 19 | ) 20 | 21 | id = models.UUIDField( 22 | primary_key=True, 23 | unique=True, 24 | default=uuid4 25 | ) 26 | push_device_type = models.CharField( 27 | verbose_name='Push Device Type', 28 | max_length=5, choices=DEVICE_TYPES, 29 | default=APN, help_text=_('APN or GCM') 30 | ) 31 | push_token = models.CharField( 32 | verbose_name=_('Device Push Token'), 33 | max_length=250, default='', 34 | help_text=_('APN or GCM Token') 35 | ) 36 | arn_endpoint = models.CharField( 37 | verbose_name=_('Device ARN Endpoint'), 38 | max_length=250, default='', 39 | help_text=_('ARN endpoint provided by AWS') 40 | ) 41 | 42 | class Meta: 43 | abstract = True 44 | 45 | @property 46 | def is_ios(self): 47 | return True if self.push_device_type == self.APN else False 48 | 49 | @property 50 | def is_android(self): 51 | return True if self.push_device_type == self.GCM else False 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # Environments 84 | .env 85 | .venv 86 | env/ 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | # sqlite db 104 | db.sqlite3 105 | 106 | # PyCharm 107 | .idea 108 | 109 | # data folders 110 | media 111 | data 112 | -------------------------------------------------------------------------------- /users/tests.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from django.test import TestCase 3 | 4 | from rest_framework.authtoken.models import Token 5 | from rest_framework.test import APIClient 6 | 7 | from .models import Account 8 | 9 | 10 | class APITests(TestCase): 11 | def setUp(self): 12 | self.account = Account.objects.create_user( 13 | username="test", 14 | password="test1234" 15 | ) 16 | 17 | def test_auth(self): 18 | """ 19 | Ensure we can successfully get authorized via the API 20 | """ 21 | request = self.client.post( 22 | reverse('rest_login'), 23 | {'username': 'test', 'password': 'test1234'} 24 | ) 25 | self.assertEqual(request.status_code, 200) 26 | 27 | def test_account_me_unauthorized(self): 28 | """ 29 | Ensure the `me` accounts endpoint does to return data to unauthorized requests 30 | """ 31 | client = APIClient() 32 | # client.login(username='test', password='test1234') 33 | request = client.get(reverse('api_accounts_me')) 34 | self.assertEqual(request.status_code, 401) 35 | 36 | def test_account_me_authorized(self): 37 | """ 38 | Ensure the `me` accounts endpoint returns the correct data to authorized requests 39 | """ 40 | token = Token.objects.get(user=self.account) 41 | client = APIClient() 42 | client.credentials(HTTP_AUTHORIZATION='Token ' + token.key) 43 | request = client.get(reverse('api_accounts_me')) 44 | self.assertEqual(request.status_code, 200) 45 | self.assertEqual(request.data.get('id'), self.account.id) 46 | 47 | def test_account_update(self): 48 | """ 49 | Ensure our update requests get executed correctly 50 | """ 51 | token = Token.objects.get(user=self.account) 52 | client = APIClient() 53 | client.credentials(HTTP_AUTHORIZATION='Token ' + token.key) 54 | request = client.patch(reverse('api_accounts_me'), data={'first_name': 'FirstName'}) 55 | self.assertEqual(request.status_code, 200) 56 | request2 = client.get(reverse('api_accounts_me')) 57 | self.assertEqual(request2.data.get('first_name'), 'FirstName') 58 | -------------------------------------------------------------------------------- /django_mobile_app/settings/production.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | env = environ.Env() 4 | environ.Env.read_env() # the .env file should be in the settings path, not the manage.py path 5 | 6 | SECRET_KEY = env('SECRET_KEY') 7 | 8 | DEBUG = env('DEBUG', default=False) 9 | 10 | ALLOWED_HOSTS.extend(env('ALLOWED_HOSTS').split(',')) 11 | 12 | INSTALLED_APPS.extend( 13 | [ 14 | 'storages', 15 | ] 16 | ) 17 | 18 | DATABASES = { 19 | 'default': env.db(), # Raises ImproperlyConfigured exception if DATABASE_URL not in os.environ 20 | } 21 | 22 | DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' 23 | STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' 24 | 25 | # Logging settings 26 | LOGGING = { 27 | 'version': 1, 28 | 'disable_existing_loggers': False, 29 | 'formatters': { 30 | 'verbose': { 31 | 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' 32 | }, 33 | 'simple': { 34 | 'format': '%(levelname)s %(message)s' 35 | }, 36 | }, 37 | 'filters': { 38 | 'require_debug_true': { 39 | '()': 'django.utils.log.RequireDebugTrue', 40 | }, 41 | }, 42 | 'handlers': { 43 | 'sentry': { 44 | 'level': env('LOGGING_LEVEL'), # To capture more than ERROR, change to WARNING, INFO, etc. 45 | 'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler', 46 | }, 47 | 'console': { 48 | 'level': env('LOGGING_LEVEL', default='INFO'), 49 | 'filters': ['require_debug_true'], 50 | 'class': 'logging.StreamHandler', 51 | 'formatter': 'simple' 52 | }, 53 | 'mail_admins': { 54 | 'level': env('MAIL_ADMINS_LOGGING_LEVEL', default='ERROR'), 55 | 'class': 'django.utils.log.AdminEmailHandler', 56 | } 57 | }, 58 | 'loggers': { 59 | 'django': { 60 | 'handlers': ['console', 'sentry'], 61 | 'propagate': True, 62 | }, 63 | 'django.db.backends': { 64 | 'level': env('LOGGING_LEVEL'), 65 | 'handlers': ['console', 'sentry'], 66 | 'propagate': False, 67 | }, 68 | 'django.request': { 69 | 'handlers': ['mail_admins', 'sentry'], 70 | 'level': env('LOGGING_LEVEL', default='WARNING'), 71 | 'propagate': False, 72 | }, 73 | 'raven': { 74 | 'level': env('LOGGING_LEVEL'), 75 | 'handlers': ['console'], 76 | 'propagate': False, 77 | }, 78 | 'sentry.errors': { 79 | 'level': env('LOGGING_LEVEL'), 80 | 'handlers': ['console'], 81 | 'propagate': False, 82 | }, 83 | } 84 | } 85 | 86 | CACHES = { 87 | 'default': env.cache(), 88 | 'redis': env.cache('REDIS_URL') 89 | } 90 | -------------------------------------------------------------------------------- /users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.6 on 2017-10-06 20:51 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 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | initial = True 14 | 15 | dependencies = [ 16 | ('auth', '0008_alter_user_username_max_length'), 17 | ] 18 | 19 | operations = [ 20 | migrations.CreateModel( 21 | name='Account', 22 | fields=[ 23 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 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 | ('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')), 35 | ('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')), 36 | ], 37 | options={ 38 | 'verbose_name': 'user', 39 | 'verbose_name_plural': 'users', 40 | 'abstract': False, 41 | }, 42 | managers=[ 43 | ('objects', django.contrib.auth.models.UserManager()), 44 | ], 45 | ), 46 | ] 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Mobile App 2 | 3 | An easy to use project template in Django 1.11, focused on a custom backend for a mobile app. [![Build Status](https://travis-ci.org/intelligems/django-mobile-app.svg?branch=master)](https://travis-ci.org/intelligems/django-mobile-app) 4 | 5 | # Repository unmaintained 6 | As Intelligems has stopped operations since Aug19, this repository remains unmaintained. Whoever may be interested to keep it up-to-date or extend it, DM [koslib](https://twitter.com/koslib) to arrange project transfer. 7 | 8 | # General 9 | This repo acts as a decent starting point for those who are looking for a custom backend deployment for their mobile app. 10 | It includes a full-serving django project which exposes a RESTful API, manages user instances and is highly configurable. 11 | 12 | In fact, this project is not a package that you can include in your project and use right-away, but it's a project template that you can download, 13 | extend and keep working on it as a base for your new project. 14 | 15 | 3rd-party apps it includes: 16 | - `django-storages`, to store files in AWS S3 (the most commonly used object storage) 17 | - `django-allauth`, for social media authentication 18 | - `django-anymail[mailgun]`, to send transactional emails using Mailgun (first 10k messages/month are free) 19 | - `djangorestframework`, for the RESTful API 20 | - `django-rest-swagger`, to automatically generate documentation for your RESTful API endpoints 21 | - `django-rest-auth`, to provide social media authentication over the API 22 | - `django-filters`, which provides filtering capabilities in the DRF API views 23 | - `django-guardian`, for custom object or model level permissions 24 | - `celery`, for background tasks handling. By default, it's expected to be used for device registration on the AWS SNS service. 25 | - `django-extensions`, offering a collection of custom extensions for Django 26 | - `django-environ`, following the 12-factor methodology 27 | 28 | # Prerequisites 29 | - Python3 30 | - Git 31 | - pip 32 | - virtualenv (recommended) 33 | 34 | # How to use 35 | 1. Clone this repo on your local machine: 36 | ```bash 37 | git clone https://github.com/intelligems/django-mobile-app 38 | ``` 39 | 2. We strongly advise to create a Python virtual environment and install the project requirements in there: 40 | ```bash 41 | mkvirtualenv --python=`which python3` 42 | ``` 43 | 3. Install project requirements inside your newly created local virtual environment: 44 | ```bash 45 | pip install -r requirements.txt 46 | ``` 47 | 4. Inside the `settings` path, create an `.env` file. Add in there all the environment variables that should be included 48 | in the project runtime. 49 | 5. It's time to perform your first database migrations - no worries, we have included them too: 50 | ```bash 51 | python manage.py migrate 52 | ``` 53 | 6. Run the server! 54 | ```bash 55 | python manage.py runserver 0.0.0.0:80 56 | ``` 57 | 58 | # Registering Push Devices 59 | For the `push_devices` app usage, you are expected to use the `AbstractMobileDevice` abstract model. 60 | You can extend it and add any fields you wish, but you are not allowed (by Django) to override the same fields that the `AbstractMobileDevice` model uses. 61 | 62 | In order to create a push device, inside the create view of your devices' API, import the sns registration method 63 | ```python 64 | from core.mobile_devices.tasks import register_device_on_sns 65 | ``` 66 | and use the `delay` method to register the newly created device on SNS. This will assign the ARN endpoint on the device model, so that you will be able to publish push notifications to your registered push device. 67 | 68 | For example: 69 | ```python 70 | device = Device.objects.create(**data) 71 | register_device_on_sns.delay(device) 72 | ``` 73 | -------------------------------------------------------------------------------- /django_mobile_app/settings/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | import environ 3 | import raven 4 | 5 | env = environ.Env() 6 | 7 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 8 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 9 | 10 | ALLOWED_HOSTS = [] 11 | 12 | # Application definition 13 | INSTALLED_APPS = [ 14 | 'django.contrib.admin', 15 | 'django.contrib.auth', 16 | 'django.contrib.contenttypes', 17 | 'django.contrib.sessions', 18 | 'django.contrib.sites', 19 | 'django.contrib.messages', 20 | 'django.contrib.staticfiles', 21 | 22 | 'users.apps.UsersConfig', 23 | 24 | 'allauth', 25 | 'allauth.account', 26 | 'allauth.socialaccount', 27 | 'allauth.socialaccount.providers.facebook', 28 | 'allauth.socialaccount.providers.instagram', 29 | 30 | 'anymail', 31 | 'django_extensions', 32 | 'guardian', 33 | 'raven.contrib.django.raven_compat', 34 | 'rest_framework', 35 | 'rest_framework.authtoken', 36 | 'rest_auth', 37 | 'rest_auth.registration', 38 | 'rest_framework_swagger', 39 | 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 = 'django_mobile_app.urls' 53 | 54 | TEMPLATES = [ 55 | { 56 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 57 | 'DIRS': ['templates'], 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 = 'django_mobile_app.wsgi.application' 71 | 72 | # Password validation 73 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 74 | 75 | AUTH_PASSWORD_VALIDATORS = [ 76 | { 77 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 78 | }, 79 | { 80 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 81 | }, 82 | { 83 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 84 | }, 85 | { 86 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 87 | }, 88 | ] 89 | 90 | # Internationalization 91 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 92 | 93 | LANGUAGE_CODE = 'en-us' 94 | 95 | TIME_ZONE = 'UTC' 96 | 97 | USE_I18N = True 98 | 99 | USE_L10N = True 100 | 101 | USE_TZ = True 102 | 103 | # Static files (CSS, JavaScript, Images) 104 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 105 | 106 | STATIC_ROOT = '{}/staticfiles'.format(BASE_DIR) 107 | STATIC_URL = '/static/' 108 | 109 | # Custom user model config 110 | AUTH_USER_MODEL = 'users.Account' 111 | 112 | AUTHENTICATION_BACKENDS = ( 113 | 'django.contrib.auth.backends.ModelBackend', 114 | 'allauth.account.auth_backends.AuthenticationBackend', 115 | 'guardian.backends.ObjectPermissionBackend' 116 | ) 117 | 118 | # Rest API config 119 | REST_SESSION_LOGIN = False 120 | 121 | REST_FRAMEWORK = { 122 | 'DEFAULT_RENDERER_CLASSES': ( 123 | 'rest_framework.renderers.JSONRenderer', 124 | 'rest_framework.renderers.BrowsableAPIRenderer', 125 | ), 126 | 'DEFAULT_PARSER_CLASSES': ( 127 | ( 128 | 'rest_framework.parsers.JSONParser', 129 | 'rest_framework.parsers.FormParser', 130 | 'rest_framework.parsers.MultiPartParser' 131 | ) 132 | ), 133 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 134 | 'rest_framework.authentication.TokenAuthentication', 135 | 'rest_framework.authentication.BasicAuthentication' 136 | ), 137 | 'DEFAULT_PERMISSION_CLASSES': ( 138 | 'rest_framework.permissions.IsAuthenticated', 139 | ), 140 | 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 141 | 'PAGE_SIZE': 10, 142 | 'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',) 143 | } 144 | 145 | SITE_ID = 1 146 | 147 | SOCIALACCOUNT_PROVIDERS = { 148 | 'facebook': { 149 | 'METHOD': 'oauth2', 150 | 'SCOPE': ['email', 'public_profile'], 151 | 'AUTH_PARAMS': {'auth_type': 'reauthenticate'}, 152 | 'INIT_PARAMS': {'cookie': True}, 153 | 'FIELDS': [ 154 | 'id', 155 | 'email', 156 | 'name', 157 | 'first_name', 158 | 'last_name', 159 | 'verified', 160 | 'locale', 161 | 'timezone', 162 | 'link', 163 | 'gender', 164 | 'updated_time', 165 | ], 166 | 'EXCHANGE_TOKEN': True, 167 | # 'LOCALE_FUNC': 'path.to.callable', 168 | 'VERIFIED_EMAIL': False, 169 | 'VERSION': 'v2.4', 170 | }, 171 | 'google': { 172 | 'SCOPE': [ 173 | 'profile', 174 | 'email', 175 | ], 176 | 'AUTH_PARAMS': { 177 | 'access_type': 'online', 178 | } 179 | } 180 | } 181 | 182 | # AWS config 183 | AWS_REGION = env('AWS_REGION', default=None) 184 | AWS_LOCATION = env('AWS_LOCATION', default=None) 185 | AWS_ACCESS_KEY_ID = env('AWS_ACCESS_KEY_ID', default=None) 186 | AWS_SECRET_ACCESS_KEY = env('AWS_SECRET_ACCESS_KEY', default=None) 187 | AWS_STORAGE_BUCKET_NAME = env('AWS_STORAGE_BUCKET_NAME', default=None) 188 | AWS_HEADERS = { 189 | 'Cache-Control': 'max-age=86400', 190 | } 191 | AWS_S3_OBJECT_PARAMETERS = { 192 | 'CacheControl': 'max-age=86400', 193 | } 194 | AWS_QUERYSTRING_AUTH = False # or set it to True, depending on your needs 195 | 196 | # SNS Config 197 | AWS_IOS_APPLICATION_ARN = env('AWS_IOS_APPLICATION_ARN', default=None) 198 | AWS_ANDROID_APPLICATION_ARN = env('AWS_ANDROID_APPLICATION_ARN', default=None) 199 | 200 | # Mail config 201 | ANYMAIL = { 202 | 'MAILGUN_API_KEY': env('MAILGUN_API_KEY', default=None), 203 | 'MAILGUN_SENDER_DOMAIN': env('MAILGUN_SENDER_DOMAIN', default=None) 204 | } 205 | EMAIL_BACKEND = 'anymail.backends.mailgun.EmailBackend' 206 | DEFAULT_FROM_EMAIL = env('DEFAULT_FROM_EMAIL', default=None) 207 | 208 | # Celery config 209 | REDIS_URL = env('REDIS_URL', default=None) 210 | CELERY_BROKER_URL = f'{REDIS_URL}/0' 211 | CELERY_RESULT_BACKEND = f'{REDIS_URL}/1' 212 | 213 | # Sentry config (optional) 214 | if 'SENTRY_DSN' in os.environ: 215 | RAVEN_CONFIG = { 216 | 'dsn': env('SENTRY_DSN', default=None), 217 | # If you are using git, you can also automatically configure the 218 | # release based on the git info. 219 | 'release': raven.fetch_git_sha(os.path.abspath(os.pardir)), 220 | } 221 | --------------------------------------------------------------------------------