├── .dockerignore ├── .env ├── .gitlab-ci.yml ├── .pep8 ├── Dockerfile ├── LICENSE ├── README.md ├── apps ├── __init__.py ├── core │ ├── __init__.py │ ├── asgi.py │ ├── consumers.py │ ├── fixtures │ │ └── initial_data.json │ ├── routing.py │ ├── settings │ │ ├── __init__.py │ │ ├── base.py │ │ ├── dev.py │ │ ├── docker.py │ │ ├── local.py.dist │ │ ├── production.py │ │ └── test.py │ ├── urls.py │ └── wsgi.py ├── frontend │ ├── __init__.py │ ├── apps.py │ └── static │ │ └── css │ │ └── admin-extra.css ├── user │ ├── __init__.py │ ├── apps.py │ ├── fixtures │ │ └── auth_data.json │ └── management │ │ ├── __init__.py │ │ └── commands │ │ ├── __init__.py │ │ └── verify_users.py └── utils │ ├── __init__.py │ ├── apps.py │ └── management │ ├── __init__.py │ └── commands │ ├── __init__.py │ ├── recreate_database.py │ ├── update_fixtures.py │ └── wait_for_database.py ├── bin └── entrypoint.sh ├── docker-compose.yml ├── libs ├── __init__.py └── tests.py ├── manage.py ├── pytest.ini ├── requirements-devel.txt ├── requirements.txt ├── setup.sh ├── tasks.py ├── templates ├── admin │ └── base_site.html └── base.html └── tests ├── __init__.py ├── admin.py ├── models.py ├── recipes.py └── views.py /.dockerignore: -------------------------------------------------------------------------------- 1 | # These files will not be copied to the Docker app container 2 | .git/ 3 | .cache/ 4 | .idea/ 5 | env/ 6 | .coverage 7 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Default environment variables for executing docker-compose 2 | # These are overwritten on staging in .gitlab-ci.yml 3 | MAIN_BRANCH=master 4 | CONTAINER_NAME=django-api 5 | IMAGE_NAME=django-api 6 | DB_USER=postgres 7 | DB_PW=postgres 8 | DB_NAME=postgres 9 | DJANGO_SETTINGS_MODULE=apps.core.settings.docker 10 | USE_VENV=false 11 | 12 | # Just to disable warnings dont run sonarqube local 13 | CI_PROJECT_NAME=empty 14 | CI_BUILD_REF=0 15 | CI_BUILD_REF_NAME=empty 16 | CI_PROJECT_ID=empty 17 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # Stages are run in sequence, jobs in the same stage are run in parallel 2 | # See https://docs.gitlab.com/ce/ci/yaml/ 3 | # TODO: Correct the tags to fit your gitlab 4 | stages: 5 | - prepare 6 | - install 7 | - push 8 | - deploy 9 | 10 | # Specify custom variables 11 | variables: 12 | # Enables multibranch build 13 | COMPOSE_PROJECT_NAME: $CI_PROJECT_NAME-$CI_BUILD_REF_NAME 14 | # How to store the image 15 | IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_BUILD_REF_NAME 16 | 17 | # Prepare 18 | # ------- 19 | # Removes old images and containers, if they exist 20 | prepare:clean: 21 | stage: prepare 22 | script: 23 | # Stop all containers 24 | - docker-compose stop 25 | # Remove all containers and volumes 26 | - docker-compose rm -v 27 | tags: 28 | - meetup 29 | 30 | # Install 31 | # ------- 32 | # Build all images 33 | install:containers: 34 | stage: install 35 | script: 36 | # Build the Containers without caching and newest source image 37 | - docker-compose build --pull --no-cache 38 | tags: 39 | - meetup 40 | 41 | # Push 42 | # ---- 43 | # Pushes the Docker image to th registry for selected tags 44 | push:image: 45 | stage: push 46 | script: 47 | # Login to the registry 48 | - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY 49 | # Push images to the gitlab remote 50 | - docker-compose push 51 | # Only these branches are getting pushed 52 | only: 53 | - develop 54 | - master 55 | - tags 56 | tags: 57 | - meetup 58 | 59 | # Deploy 60 | # ------ 61 | # Starts the containers 62 | deploy:http: 63 | stage: deploy 64 | script: 65 | # Bring up the app 66 | - docker-compose up -d app 67 | tags: 68 | - meetup 69 | -------------------------------------------------------------------------------- /.pep8: -------------------------------------------------------------------------------- 1 | [pep8] 2 | max-line-length = 120 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6-alpine 2 | 3 | WORKDIR /build 4 | 5 | # ENV defaults 6 | ENV INVOKE_SHELL /bin/ash 7 | ENV USE_VENV false 8 | ENV DJANGO_SETTINGS_MODULE apps.core.settings.docker 9 | 10 | # Add required alpine packages 11 | RUN echo http://nl.alpinelinux.org/alpine/edge/testing >> /etc/apk/repositories \ 12 | && apk add --update git postgresql-dev python-dev gcc musl-dev jpeg-dev zlib-dev gosu && rm -rf /var/cache/apk/* \ 13 | && adduser -D -u 1002 worker 14 | 15 | # Install required python packages 16 | COPY requirements.txt requirements-devel.txt /build/ 17 | RUN pip install -r 'requirements-devel.txt' 18 | 19 | # Copy app source to docker image 20 | COPY . /build/ 21 | 22 | RUN invoke static \ 23 | && chown -R worker /build 24 | 25 | # Execute entrypoint script when the container is started 26 | ENTRYPOINT ["/usr/bin/gosu", "worker"] 27 | CMD ["/build/bin/entrypoint.sh"] 28 | EXPOSE 8000 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Motius GmbH 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Django Meetup 2 | ========== 3 | 4 | Install and run locally 5 | ----------------------- 6 | 7 | In preparation you will need Python >=3.4, virtualenv, and pip installed. You can then launch the build process: 8 | 9 | sudo pip install invoke 10 | invoke main --env=dev 11 | 12 | After the build succeeds, you should be able to activate the virtual environment and launch the development server: 13 | 14 | source ./env/bin/activate 15 | python manage.py runserver 16 | 17 | You can run tests with pytest after activating the virtualenv: 18 | 19 | pytest 20 | 21 | 22 | Install and run in Docker 23 | ------------------------- 24 | 25 | Install docker-engine and docker-compose then login with your Gitlab credentials and run compose: 26 | 27 | docker-compose build --no-cache 28 | docker-compose up -d 29 | 30 | To run tests: 31 | 32 | docker-compose run app invoke test 33 | 34 | To launch a shell in your app: 35 | 36 | docker-compose run app /bin/ash 37 | 38 | Use .gitlab-ci.yml 39 | ------------------ 40 | 41 | Install a runner: https://docs.gitlab.com/runner/install/ 42 | 43 | **.gitlab-ci.yml** 44 | - Define stages (prepare, install ..) 45 | - Define jobs (clean, dependencies ..) 46 | 47 | **docker-compose.yml** 48 | - Using variables defined in .env 49 | - Defining the services need to run the django app 50 | 51 | **.dockerignore** 52 | - Dont copy folder like .git to the containers 53 | 54 | **.env** 55 | - Define all the need environment variables for local builds 56 | 57 | Log in 58 | ------ 59 | 60 | After launching the server go to http://127.0.0.1:8000/admin/ and log in with user `admin`, password `admin`. 61 | -------------------------------------------------------------------------------- /apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motius/django-gitlab-boilerplate/b10e0096b8760d4d001a3ba9c19dd909267a4334/apps/__init__.py -------------------------------------------------------------------------------- /apps/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motius/django-gitlab-boilerplate/b10e0096b8760d4d001a3ba9c19dd909267a4334/apps/core/__init__.py -------------------------------------------------------------------------------- /apps/core/asgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | from channels.asgi import get_channel_layer 3 | 4 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "") 5 | 6 | channel_layer = get_channel_layer() 7 | -------------------------------------------------------------------------------- /apps/core/consumers.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from channels.handler import AsgiHandler 3 | 4 | def ws_message(message): 5 | # ASGI WebSocket packet-received and send-packet message types 6 | # both have a "text" key for their textual data. 7 | message.reply_channel.send({ 8 | "text": message.content['text'], 9 | }) 10 | -------------------------------------------------------------------------------- /apps/core/fixtures/initial_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "fields": { 4 | "domain": "example.com", 5 | "name": "example.com" 6 | }, 7 | "model": "sites.site", 8 | "pk": 1 9 | } 10 | ] -------------------------------------------------------------------------------- /apps/core/routing.py: -------------------------------------------------------------------------------- 1 | from channels.routing import route 2 | from apps.core.consumers import ws_message 3 | channel_routing = [ 4 | route("websocket.receive", ws_message, path="^/"), 5 | ] 6 | -------------------------------------------------------------------------------- /apps/core/settings/__init__.py: -------------------------------------------------------------------------------- 1 | from .dev import * 2 | -------------------------------------------------------------------------------- /apps/core/settings/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for core project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.7/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.7/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | import os 13 | from datetime import timedelta 14 | 15 | from django.utils.translation import ugettext_lazy as _ 16 | 17 | 18 | PROJECT_DIR = os.path.dirname(os.path.dirname(__file__)) 19 | BASE_DIR = os.path.dirname(PROJECT_DIR) 20 | 21 | AUTHENTICATION_BACKENDS = ( 22 | 'django.contrib.auth.backends.RemoteUserBackend', 23 | 'django.contrib.auth.backends.ModelBackend', 24 | 'allauth.account.auth_backends.AuthenticationBackend', 25 | ) 26 | 27 | LOCALE_PATHS = ( 28 | os.path.join(BASE_DIR, 'locale'), 29 | ) 30 | 31 | SECRET_KEY = 'cpj4dqe2!3osv66zpui1*sne$)j!z8a=dwr8@i$3j2(+af69mz' 32 | 33 | DEBUG = False 34 | 35 | ALLOWED_HOSTS = [] 36 | 37 | # Application definition 38 | 39 | INSTALLED_APPS = ( 40 | 'django.contrib.auth', 41 | 'django.contrib.contenttypes', 42 | 'django.contrib.sessions', 43 | 'django.contrib.messages', 44 | 'django.contrib.staticfiles', 45 | 'django.contrib.humanize', 46 | 'django.contrib.sites', 47 | 'django.contrib.admin', 48 | 'channels', 49 | 'allauth', 50 | 'allauth.account', 51 | 'rest_framework', 52 | 'rest_auth', 53 | 'rest_auth.registration', 54 | 'rest_framework.authtoken', 55 | 'rest_framework_swagger', 56 | 'django_filters', 57 | 58 | 'apps.utils', 59 | 'apps.frontend', 60 | 'apps.user', 61 | ) 62 | 63 | MIDDLEWARE_CLASSES = ( 64 | 'django.contrib.sessions.middleware.SessionMiddleware', 65 | 'django.middleware.common.CommonMiddleware', 66 | 'django.middleware.csrf.CsrfViewMiddleware', 67 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 68 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 69 | 'django.contrib.messages.middleware.MessageMiddleware', 70 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 71 | ) 72 | 73 | PASSWORD_HASHERS = ( 74 | 'django.contrib.auth.hashers.PBKDF2PasswordHasher', 75 | 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', 76 | 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', 77 | 'django.contrib.auth.hashers.BCryptPasswordHasher', 78 | 'django.contrib.auth.hashers.SHA1PasswordHasher', 79 | 'django.contrib.auth.hashers.MD5PasswordHasher', 80 | 'django.contrib.auth.hashers.CryptPasswordHasher' 81 | ) 82 | 83 | TEMPLATES = [ 84 | { 85 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 86 | 'DIRS': [ 87 | os.path.join(os.path.dirname(BASE_DIR), 'templates'), 88 | ], 89 | 'OPTIONS': { 90 | 'context_processors': [ 91 | 'django.contrib.auth.context_processors.auth', 92 | 'django.template.context_processors.debug', 93 | 'django.template.context_processors.i18n', 94 | 'django.template.context_processors.media', 95 | 'django.template.context_processors.static', 96 | 'django.template.context_processors.tz', 97 | 'django.contrib.messages.context_processors.messages', 98 | 'django.template.context_processors.request', 99 | 'django.contrib.messages.context_processors.messages' 100 | ], 101 | 'loaders': [ 102 | 'django.template.loaders.filesystem.Loader', 103 | 'django.template.loaders.app_directories.Loader', 104 | 'django.template.loaders.eggs.Loader', 105 | ], 106 | }, 107 | }, 108 | ] 109 | 110 | SITE_ID = 1 111 | 112 | ROOT_URLCONF = 'apps.core.urls' 113 | 114 | WSGI_APPLICATION = 'apps.core.wsgi.application' 115 | 116 | 117 | # Database 118 | # https://docs.djangoproject.com/en/1.7/ref/settings/#databases 119 | 120 | DATABASES = { 121 | 'default': { 122 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 123 | 'NAME': 'postgres', 124 | 'USER': 'postgres', 125 | 'PASSWORD': 'postgres', 126 | 'HOST': 'localhost' 127 | }, 128 | } 129 | 130 | # Internationalization 131 | # https://docs.djangoproject.com/en/1.7/topics/i18n/ 132 | 133 | LANGUAGE_CODE = 'en-us' 134 | TIME_ZONE = 'UTC' 135 | USE_I18N = True 136 | USE_L10N = True 137 | USE_TZ = True 138 | LANGUAGES = ( 139 | ('de', _('German')), 140 | ('en', _('English')), 141 | ) 142 | 143 | 144 | # Static files (CSS, JavaScript, Images) 145 | # https://docs.djangoproject.com/en/1.7/howto/static-files/ 146 | 147 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 148 | MEDIA_URL = '/media/' 149 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 150 | STATIC_URL = '/static/' 151 | 152 | STATICFILES_FINDERS = ( 153 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 154 | 'django.contrib.staticfiles.finders.FileSystemFinder', 155 | ) 156 | 157 | # Third party 158 | REST_FRAMEWORK = { 159 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 160 | 'rest_framework.authentication.SessionAuthentication', 161 | 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', 162 | ), 163 | } 164 | 165 | REST_USE_JWT = True 166 | REST_SESSION_LOGIN = False 167 | 168 | CORS_ALLOW_CREDENTIALS = True 169 | CORS_ORIGIN_WHITELIST = ( 170 | 'https://django-api.staging.motius.de', 171 | 'http://django-api.staging.motius.de', 172 | 'django-api.staging.motius.de', 173 | '0.0.0.0:8000', 174 | 'localhost:8000', 175 | '127.0.0.1:8000', 176 | 'localhost', 177 | ) 178 | 179 | JWT_AUTH = { 180 | 'JWT_VERIFY': True, 181 | 'JWT_EXPIRATION_DELTA': timedelta(days=7), 182 | 'JWT_ALLOW_REFRESH': True 183 | } 184 | -------------------------------------------------------------------------------- /apps/core/settings/dev.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | DEBUG = True 4 | 5 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 6 | 7 | SITE_URL = 'http://127.0.0.1:8000' 8 | 9 | try: 10 | from .local import * 11 | except ImportError: 12 | pass 13 | -------------------------------------------------------------------------------- /apps/core/settings/docker.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | DEBUG = True 4 | 5 | LOGFILE_ROOT = BASE_DIR 6 | LOGGING = { 7 | 'version': 1, 8 | 'disable_existing_loggers': False, 9 | 'filters': { 10 | 'require_debug_false': { 11 | '()': 'django.utils.log.RequireDebugFalse' 12 | } 13 | }, 14 | 'formatters': { 15 | 'standard': { 16 | 'format': "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s", 17 | 'datefmt': "%d/%b/%Y %H:%M:%S" 18 | }, 19 | }, 20 | 'handlers': { 21 | 'mail_admins': { 22 | 'level': 'ERROR', 23 | 'filters': ['require_debug_false'], 24 | 'class': 'django.utils.log.AdminEmailHandler' 25 | }, 26 | 'logfile': { 27 | 'level': 'DEBUG', 28 | 'class': 'logging.handlers.RotatingFileHandler', 29 | 'filename': os.path.join(LOGFILE_ROOT, 'error_log'), 30 | 'filters': ['require_debug_false'], 31 | 'maxBytes': 50000, 32 | 'backupCount': 2, 33 | 'formatter': 'standard', 34 | }, 35 | 'console': { 36 | 'level': 'DEBUG', 37 | 'class': 'logging.StreamHandler' 38 | }, 39 | }, 40 | 'loggers': { 41 | 'django.request': { 42 | 'handlers': ['mail_admins', 'logfile'], 43 | 'level': 'ERROR', 44 | 'propagate': True, 45 | }, 46 | }, 47 | } 48 | 49 | DATABASES = { 50 | 'default': { 51 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 52 | 'NAME': os.environ.get('DB_NAME', ''), 53 | 'USER': os.environ.get('DB_USER', ''), 54 | 'PASSWORD': os.environ.get('DB_PW', ''), 55 | 'HOST': os.environ.get('DB_HOST', ''), 56 | 'PORT': os.environ.get('DB_PORT', '5432'), 57 | } 58 | } 59 | 60 | CHANNEL_LAYERS = { 61 | "default": { 62 | "BACKEND": "asgi_redis.RedisChannelLayer", 63 | "CONFIG": { 64 | "hosts": [("redis", 6379)], 65 | }, 66 | "ROUTING": "apps.core.routing.channel_routing", 67 | }, 68 | } 69 | CORS_ORIGIN_ALLOW_ALL = True 70 | 71 | ALLOWED_HOSTS = ['*'] 72 | 73 | try: 74 | from .local import * 75 | except ImportError: 76 | pass 77 | -------------------------------------------------------------------------------- /apps/core/settings/local.py.dist: -------------------------------------------------------------------------------- 1 | # Put any machine-specific settings in local.py 2 | DATABASES = { 3 | 'default': { 4 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 5 | 'NAME': 'postgres', 6 | 'USER': 'postgres', 7 | 'PASSWORD': 'postgres', 8 | 'HOST': '127.0.0.1', 9 | }, 10 | } -------------------------------------------------------------------------------- /apps/core/settings/production.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | DEBUG = False 4 | 5 | ALLOWED_HOSTS = ["django-api.staging.motius.de"] 6 | 7 | SITE_URL = 'http://django-api.staging.motius.de' 8 | STATIC_URL = 'http://django-api.staging.motius.de/static/' 9 | 10 | try: 11 | from .local import * 12 | except ImportError: 13 | pass 14 | -------------------------------------------------------------------------------- /apps/core/settings/test.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | DEBUG = False 4 | 5 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 6 | 7 | SITE_URL = 'http://127.0.0.1:8000' 8 | 9 | PASSWORD_HASHERS = ( 10 | 'django.contrib.auth.hashers.MD5PasswordHasher', 11 | ) 12 | 13 | 14 | # Disables migrations, 15 | class DisableMigrations(object): 16 | def __contains__(self, *args): 17 | return True 18 | 19 | def __getitem__(self, *args): 20 | return "notmigrations" 21 | 22 | 23 | MIGRATION_MODULES = DisableMigrations() 24 | 25 | -------------------------------------------------------------------------------- /apps/core/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | from django.contrib import admin 3 | from django.conf.urls.static import static 4 | from django.conf import settings 5 | 6 | from rest_framework.routers import DefaultRouter 7 | from rest_framework_swagger.views import get_swagger_view 8 | 9 | # API Routes 10 | router = DefaultRouter() 11 | 12 | # API Docs 13 | schema_view = get_swagger_view(title='Django API') 14 | 15 | urlpatterns = [ 16 | url(r'^admin/', include(admin.site.urls)), 17 | url(r'^accounts/', include('allauth.urls')), 18 | url(r'^api/docs/$', schema_view), 19 | url(r'^api/', include(router.urls, namespace='api')), 20 | url(r'^api/auth/', include('rest_auth.urls')), 21 | url(r'^api/auth/registration/', include('rest_auth.registration.urls')), 22 | ] 23 | 24 | # Static routes 25 | if settings.DEBUG: 26 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 27 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 28 | -------------------------------------------------------------------------------- /apps/core/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for core 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.7/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | 13 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "apps.core.settings") 14 | 15 | from django.core.wsgi import get_wsgi_application 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /apps/frontend/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'apps.frontend.apps.FrontendConfig' -------------------------------------------------------------------------------- /apps/frontend/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | 5 | class FrontendConfig(AppConfig): 6 | name = 'apps.frontend' 7 | verbose_name = _('Frontend') 8 | -------------------------------------------------------------------------------- /apps/frontend/static/css/admin-extra.css: -------------------------------------------------------------------------------- 1 | #header { 2 | background: #004860; 3 | } 4 | 5 | .module h2, .module caption, .inline-group h2 { 6 | background: #004860; 7 | } 8 | -------------------------------------------------------------------------------- /apps/user/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'apps.user.apps.UserConfig' 2 | -------------------------------------------------------------------------------- /apps/user/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | 5 | class UserConfig(AppConfig): 6 | name = 'apps.user' 7 | verbose_name = _('User') 8 | -------------------------------------------------------------------------------- /apps/user/fixtures/auth_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "fields": { 4 | "date_joined": "2015-03-08T18:39:45Z", 5 | "email": "admin@example.com", 6 | "first_name": "", 7 | "groups": [], 8 | "is_active": true, 9 | "is_staff": true, 10 | "is_superuser": true, 11 | "last_login": "2015-05-26T00:20:07Z", 12 | "last_name": "", 13 | "password": "pbkdf2_sha256$15000$UuMQEFlR48wg$soY4ftW6o8pegbKlei95G+deK57Ch/P+hReChrYdLVk=", 14 | "user_permissions": [], 15 | "username": "admin" 16 | }, 17 | "model": "auth.user", 18 | "pk": 1 19 | }, 20 | { 21 | "fields": { 22 | "email": "admin@example.com", 23 | "primary": true, 24 | "user": 1, 25 | "verified": true 26 | }, 27 | "model": "account.emailaddress", 28 | "pk": 2 29 | } 30 | ] -------------------------------------------------------------------------------- /apps/user/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motius/django-gitlab-boilerplate/b10e0096b8760d4d001a3ba9c19dd909267a4334/apps/user/management/__init__.py -------------------------------------------------------------------------------- /apps/user/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motius/django-gitlab-boilerplate/b10e0096b8760d4d001a3ba9c19dd909267a4334/apps/user/management/commands/__init__.py -------------------------------------------------------------------------------- /apps/user/management/commands/verify_users.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.core.management.base import BaseCommand 3 | 4 | from allauth.account.models import EmailAddress 5 | 6 | 7 | class Command(BaseCommand): 8 | help = 'Verify all users\'s email addresses' 9 | 10 | def handle(self, *args, **options): 11 | EmailAddress.objects.all().delete() 12 | users = get_user_model().objects.exclude(email__isnull=True, emailaddress__isnull=False) 13 | emails = [] 14 | for user in users: 15 | emails.append(EmailAddress(user=user, email=user.email, verified=True, primary=True)) 16 | EmailAddress.objects.bulk_create(emails, batch_size=200) 17 | self.stdout.write("Verified %i email addresses.\n" % len(emails)) 18 | -------------------------------------------------------------------------------- /apps/utils/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'apps.utils.apps.UtilsConfig' -------------------------------------------------------------------------------- /apps/utils/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | 5 | class UtilsConfig(AppConfig): 6 | name = 'apps.utils' 7 | verbose_name = _('Utilities') 8 | -------------------------------------------------------------------------------- /apps/utils/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motius/django-gitlab-boilerplate/b10e0096b8760d4d001a3ba9c19dd909267a4334/apps/utils/management/__init__.py -------------------------------------------------------------------------------- /apps/utils/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motius/django-gitlab-boilerplate/b10e0096b8760d4d001a3ba9c19dd909267a4334/apps/utils/management/commands/__init__.py -------------------------------------------------------------------------------- /apps/utils/management/commands/recreate_database.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.conf import settings 4 | from django.core.management.base import BaseCommand, CommandError 5 | from django.db import connections 6 | from django.db.utils import ConnectionDoesNotExist 7 | 8 | 9 | class Command(BaseCommand): 10 | help = 'Recreates the database' 11 | 12 | def add_arguments(self, parser): 13 | parser.add_argument('--database', 14 | action='store', 15 | dest='database', 16 | default='default', 17 | help='Database connection name') 18 | 19 | def handle(self, *args, **options): 20 | try: 21 | connection = connections[options['database']] 22 | cursor = connection.cursor() 23 | database_settings = settings.DATABASES[options['database']] 24 | except ConnectionDoesNotExist: 25 | raise CommandError('Database "%s" does not exist in settings' % options['database']) 26 | 27 | if connection.vendor == 'sqlite': 28 | print("Deleting database %s" % database_settings['NAME']) 29 | os.remove(database_settings['NAME']) 30 | elif connection.vendor == 'mysql': 31 | print("Dropping database %s" % database_settings['NAME']) 32 | cursor.execute("DROP DATABASE `%s`;" % database_settings['NAME']) 33 | 34 | print("Creating database %s" % database_settings['NAME']) 35 | cursor.execute("CREATE DATABASE `%s` CHARACTER SET utf8;" % database_settings['NAME']) 36 | # Should fix some "MySQL has gone away issues" 37 | cursor.execute("SET GLOBAL max_allowed_packet=32*1024*1024;") 38 | elif connection.vendor == 'postgresql': 39 | print("Dropping and recreating schema public") 40 | cursor.execute("DROP schema public CASCADE; CREATE schema public") 41 | else: 42 | raise CommandError('Database vendor not supported') 43 | -------------------------------------------------------------------------------- /apps/utils/management/commands/update_fixtures.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from io import StringIO 4 | 5 | from django.apps.registry import apps 6 | from django.conf import settings 7 | from django.core.management import call_command 8 | from django.core.management.base import BaseCommand 9 | 10 | 11 | class Command(BaseCommand): 12 | help = 'Updates fixtures from the database' 13 | 14 | def handle(self, *args, **options): 15 | app_configs = [a for a in apps.get_app_configs() if a.name.startswith('apps.')] 16 | for app_config in app_configs: 17 | dirname = os.path.join(os.path.dirname(app_config.module.__file__), 'fixtures') 18 | filename = os.path.join(dirname, '{name}_data.json'.format(name=app_config.label)) 19 | if not list(app_config.get_models()): 20 | continue 21 | models = ['{}.{}'.format(app_config.label, m._meta.object_name.lower()) for m in app_config.get_models() if m._meta.proxy is False] 22 | self.write_fixtures(filename, *models) 23 | 24 | self.write_fixtures( 25 | os.path.join(settings.BASE_DIR, 'user/fixtures/auth_data.json'), 26 | *['auth.user', 'auth.group', 'account'] 27 | ) 28 | 29 | self.write_fixtures( 30 | os.path.join(settings.BASE_DIR, 'core/fixtures/initial_data.json'), 31 | *['sites'] 32 | ) 33 | 34 | def write_fixtures(self, filename, *args): 35 | out = StringIO() 36 | print('Updating {filename}'.format(filename=filename)) 37 | call_command('dumpdata', *args, stdout=out, natural_keys=True) 38 | parsed = json.loads(out.getvalue()) 39 | 40 | if not os.path.exists(os.path.dirname(filename)): 41 | os.makedirs(os.path.dirname(filename)) 42 | 43 | with open(filename, 'w') as fh: 44 | fh.write(json.dumps(parsed, indent=4, sort_keys=True)) 45 | -------------------------------------------------------------------------------- /apps/utils/management/commands/wait_for_database.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | from django.core.management.base import BaseCommand, CommandError 4 | from django.db import connections 5 | from django.db.utils import ConnectionDoesNotExist, OperationalError 6 | 7 | 8 | class Command(BaseCommand): 9 | help = 'Returns when database is ready' 10 | 11 | def add_arguments(self, parser): 12 | parser.add_argument('--database', 13 | action='store', 14 | dest='database', 15 | default='default', 16 | help='Database connection name') 17 | parser.add_argument('--retries', 18 | action='store', 19 | dest='retries', 20 | type=int, 21 | default=20, 22 | help='Number of retries') 23 | parser.add_argument('--sleep-time', 24 | action='store', 25 | dest='sleep_time', 26 | type=int, 27 | default=5, 28 | help='Seconds between retries') 29 | 30 | def handle(self, *args, **options): 31 | try: 32 | connection = connections[options['database']] 33 | except ConnectionDoesNotExist: 34 | raise CommandError('Database "%s" does not exist in settings' % options['database']) 35 | 36 | for i in range(0, options['retries']): 37 | try: 38 | connection.cursor() 39 | except OperationalError: 40 | i += 1 41 | self.stdout.write('{} / {}: Waiting for database...'.format(i, options['retries'])) 42 | sleep(options['sleep_time']) 43 | else: 44 | self.stdout.write(self.style.SUCCESS('Successfully connected to database')) 45 | return 46 | 47 | raise CommandError('Number of retries reached, exiting') 48 | -------------------------------------------------------------------------------- /bin/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/ash 2 | # Make sure we are in build 3 | cd /build/ 4 | 5 | echo "Wait until database is ready..." 6 | while ! nc -z $DB_HOST 5432 7 | do 8 | echo "Retrying" 9 | sleep 5 10 | done 11 | 12 | # Prepare the database 13 | invoke db --fixtures --recreate --wait 14 | 15 | # Perform test 16 | pytest --cov-report xml:cov.xml --cov-report term --junitxml=xunit.xml --cov=apps --migration 17 | touch /build/up 18 | 19 | # Start worker 20 | python3 manage.py runworker -v 2 & 21 | 22 | # Start daphne 23 | daphne -b 0.0.0.0 -p 8000 apps.core.asgi:channel_layer -v 2 --proxy-headers 24 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | # Variables are set in .env and .gitlab-ci.yml (for staging build) 3 | # Make sure to add cleanup of new services for your app in the .gitlab-ci.yml build process (in prepare stage) 4 | 5 | services: 6 | redis: 7 | image: redis:alpine 8 | 9 | db: 10 | image: postgres:alpine 11 | container_name: ${CONTAINER_NAME}-db 12 | environment: 13 | - POSTGRES_USER=${DB_USER} 14 | - POSTGRES_PASSWORD=${DB_PW} 15 | 16 | app: 17 | build: 18 | context: . 19 | image: ${IMAGE_NAME} 20 | environment: 21 | DJANGO_SETTINGS_MODULE: ${DJANGO_SETTINGS_MODULE} 22 | DB_USER: ${DB_USER} 23 | DB_NAME: ${DB_NAME} 24 | DB_PW: ${DB_PW} 25 | DB_HOST: "db" 26 | container_name: ${CONTAINER_NAME} 27 | ports: 28 | - 8000:8000 29 | volumes: 30 | - /build/ 31 | depends_on: 32 | - db 33 | - redis 34 | -------------------------------------------------------------------------------- /libs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motius/django-gitlab-boilerplate/b10e0096b8760d4d001a3ba9c19dd909267a4334/libs/__init__.py -------------------------------------------------------------------------------- /libs/tests.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from collections import OrderedDict 3 | 4 | from django.contrib.auth import get_user_model 5 | from django.core.urlresolvers import reverse 6 | from rest_framework.test import APITestCase 7 | from rest_framework.test import APIRequestFactory 8 | from rest_framework.utils.serializer_helpers import ReturnList 9 | from hamcrest import (assert_that, greater_than, instance_of, any_of, 10 | is_, is_in, has_key, has_length) 11 | 12 | from tests.recipes import EmailAddressRecipe 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | 17 | class LoginAPITestCase(APITestCase): 18 | def login(self): 19 | data = dict(email="foo@bar.com", password="foo") 20 | user = get_user_model().objects.create_user(username="foo", is_staff=True, is_superuser=True, **data) 21 | EmailAddressRecipe.make(user=user, verified=True) 22 | self.client.login(**data) 23 | 24 | 25 | class BaseAPITestCase(LoginAPITestCase): 26 | fixtures = [ 27 | 'apps/user/fixtures/auth_data.json', 28 | ] 29 | 30 | def setUp(self): 31 | EmailAddressRecipe.make(user=get_user_model().objects.get(pk=1), verified=True) # Verify admin 32 | # Create user to log in as 33 | self.login() 34 | self.api_factory = APIRequestFactory(format='json') 35 | 36 | def assert_status_code(self, response, status_code): 37 | self.assertEqual(response.status_code, status_code, 'Wrong status code on {url}: {data}'.format( 38 | url=response.request.get('PATH_INFO'), 39 | data=response.data, 40 | )) 41 | 42 | def assert_url_list_length(self, url, length): 43 | response = self.get_200_response(url) 44 | assert_that(response.data, has_length(length), url) 45 | 46 | def assert_url_list_not_empty(self, url): 47 | """ 48 | Ensure we have at least one record as empty lists would not engage 49 | serializer. 50 | """ 51 | response = self.get_200_response(url) 52 | try: 53 | assert_that(response.data, any_of(instance_of(OrderedDict), 54 | instance_of(ReturnList), 55 | instance_of(list))) 56 | assert_that(len(response.data), greater_than(0)) 57 | except: 58 | log.info("Expected non-empty list as result from {}".format(url)) 59 | raise 60 | 61 | def assert_400_with_validation_error(self, response, msg, key="non_field_errors"): 62 | self.assert_status_code(response, 400) 63 | data = response.data 64 | assert_that(data, has_key(key)) 65 | assert_that(data[key], has_length(1)) 66 | self.assertEqual(data[key][0], msg) 67 | 68 | def get_200_response(self, url): 69 | """Grab url, assert 200 status code and return response.""" 70 | try: 71 | response = self.client.get(url) 72 | except: 73 | log.info("{} failed.".format(url)) 74 | raise 75 | self.assert_status_code(response, 200) 76 | log.info("{} 200".format(url)) 77 | return response 78 | 79 | 80 | class SerializerSmokeTestCaseMixin(LoginAPITestCase): 81 | """ 82 | Tests modelviewset with given sample data: 83 | - creating new record via api 84 | - updating record via api 85 | - serialize/deserialize record 86 | Making this a mixin as we don't want it to run by itself 87 | Introduced after an innocent looking fields definition on a serializer worked 88 | well for reading but not for writing. 89 | """ 90 | 91 | def setUp(self): 92 | super().setUp() 93 | self.login() 94 | self.model = self.ViewsetClass.queryset.model 95 | self.model.objects.all().delete() # get rid of dummy_data.json 96 | self.list_url = reverse("api:{}-list".format(self.model.__name__.lower())) 97 | self.detail_url = "api:{}-detail".format(self.model.__name__.lower()) 98 | self.serializer_class = self.get_serializer_class() 99 | 100 | def get_serializer_class(self): 101 | request = self.api_factory.post(self.list_url) 102 | viewset = self.ViewsetClass() 103 | viewset.request = request 104 | viewset.format_kwarg = "json" 105 | return viewset.get_serializer_class() 106 | 107 | def create_from_sample_data_via_api(self): 108 | return self.client.post(self.list_url, self.sample_data) 109 | 110 | def create_empty_via_api(self): 111 | return self.client.post(self.list_url, {}) 112 | 113 | def test_empty_instance_does_not_fail_serializer(self): 114 | print(self.serializer_class(self.model()).data) 115 | 116 | def test_empty_input_does_not_raise_500(self): 117 | """ 118 | Ensure empty/unsaved data does not raise 500s in serializer. 119 | """ 120 | response = self.create_empty_via_api() 121 | assert_that(response.status_code, is_in([201, 400])) 122 | 123 | def test_can_create_record_via_viewset_api_endpoint(self): 124 | response = self.create_from_sample_data_via_api() 125 | if not response.status_code == 201: 126 | log.error(response.content) 127 | self.assert_status_code(response, 201) 128 | 129 | def test_can_update_record_via_viewset_api_endpoint(self): 130 | self.create_from_sample_data_via_api() 131 | pk = self.ViewsetClass.queryset.model.objects.get().pk 132 | response = self.client.patch(reverse(self.detail_url, args=[pk]), 133 | self.update_data) 134 | if not response.status_code == 200: 135 | log.error(response.content) 136 | self.assert_status_code(response, 200) 137 | key, value = self.update_data.popitem() 138 | data_value = response.data.get(key) 139 | # API might respond with a dict containing the sent ID 140 | if data_value != value and isinstance(data_value, dict) and 'id' in data_value: 141 | assert_that(data_value['id'], is_(value)) 142 | else: 143 | assert_that(data_value, is_(value)) 144 | 145 | def test_viewset_serializer_validates_does_not_fail_on_optional_fields(self): 146 | """ 147 | Regression test for serializer classes 148 | """ 149 | serializer = self.serializer_class(data=self.sample_data) 150 | serializer.is_valid(raise_exception=True) 151 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | if len(sys.argv) > 1 and sys.argv[1] == "test": 7 | print("Try running py.test") 8 | sys.exit(0) 9 | 10 | settings_module = "apps.core.settings" 11 | 12 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings_module) 13 | 14 | from django.core.management import execute_from_command_line 15 | 16 | execute_from_command_line(sys.argv) 17 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE=apps.core.settings.test 3 | testpaths = apps tests 4 | python_files = tests/* 5 | addopts = -s --cov=apps 6 | -------------------------------------------------------------------------------- /requirements-devel.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | 3 | django-debug-toolbar==1.6 # Updated from 1.3.0 4 | django-extensions==1.7.5 # Updated from 1.5.5 5 | django-webtest==1.8.0 # Updated from 1.7.8 6 | django-dynamic-fixture==1.9.0 # Updated from 1.8.4 7 | django-admin-smoke-tests==0.3.0 8 | model_mommy==1.3.1 9 | WebTest==2.0.23 # Updated from 2.0.18 10 | pep8==1.7.0 # Updated from 1.6.2 11 | pytest-django==3.1.2 12 | pytest-cov==2.4.0 13 | PyHamcrest==1.9.0 14 | termcolor==1.1.0 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.10.4 # Updated from 1.9.7 2 | django-allauth==0.29.0 # Updated from 0.25.2 3 | djangorestframework==3.5.3 # Updated from 3.3.3 4 | djangorestframework-jwt==1.9.0 # Updated from 1.7.2 5 | django-rest-auth==0.8.2 6 | django-rest-swagger==2.1.0 7 | dry-rest-permissions==0.1.8 8 | django-cors-middleware==1.3.1 9 | django-import-export==0.5.1 10 | django-filter==1.0.1 # Updated from 0.13.0 11 | gunicorn==19.6.0 # Updated from 19.4.5 12 | invoke==0.14.0 # Updated from 0.13.0 13 | psycopg2==2.6.2 # Updated from 2.6.1 14 | daphne==1.0.3 15 | asgi-redis==1.0.0 16 | channels==1.0.2 17 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Please enter the project name in lowercase and with dashes (i.e. django-api): " 4 | read project 5 | find . -type f -print0 | xargs -0 sed -i "s/django\-api/$project/g" 6 | 7 | echo "Please enter the project name in title case (i.e. Django API): " 8 | read name 9 | find . -type f -print0 | xargs -0 sed -i "s/Django API/$name/g" -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from invoke import task 4 | 5 | 6 | def success(string): 7 | return '\033[92m' + string + '\033[0m' 8 | 9 | 10 | def prefix_venv(cmd): 11 | """ 12 | Prefixes the cmd with the virtualenv's bin/ folder if the cmd is found there 13 | """ 14 | if os.environ.get('USE_VENV') == 'true': 15 | venv_cmd = os.path.join(VENV, 'bin', cmd) 16 | if os.path.exists(venv_cmd): 17 | return venv_cmd 18 | return cmd 19 | 20 | # Default ENV variables 21 | os.environ.setdefault('PIP', 'pip') 22 | os.environ.setdefault('PYTHON', 'python') 23 | os.environ.setdefault('PYTHON_VERSION', '3') 24 | os.environ.setdefault('HIDE', 'out') # Hides stdout by default 25 | os.environ.setdefault('INVOKE_SHELL', '/bin/sh') # Replace default shell, i.e. /bin/ash for alpine 26 | os.environ.setdefault('USE_VENV', 'true') # Execute commands from virtualenv if true 27 | 28 | ENV_PRODUCTION = 'production' 29 | ENV_DOCKER = 'docker' 30 | ENV_DEV = 'dev' 31 | ENV_TEST = 'test' 32 | ENV_DEFAULT = ENV_PRODUCTION 33 | 34 | DIR = os.path.dirname(os.path.realpath(__file__)) 35 | VENV = os.path.join(DIR, 'env') 36 | MANAGE = os.path.join(DIR, 'manage.py') 37 | 38 | 39 | def python(ctx, args, **kwargs): 40 | """ 41 | Uses python from virtualenv if available, falls back to ENV variable PYTHON 42 | """ 43 | ctx.run('{python} {args}'.format(python=prefix_venv(os.environ.get('PYTHON')), args=args), 44 | shell=os.environ.get('INVOKE_SHELL'), **kwargs) 45 | 46 | 47 | def pip(ctx, args, **kwargs): 48 | """ 49 | Uses pip from virtualenv if available, falls back to ENV variable PIP 50 | """ 51 | ctx.run('{pip} {args}'.format(pip=prefix_venv(os.environ.get('PIP')), args=args), 52 | shell=os.environ.get('INVOKE_SHELL'), **kwargs) 53 | 54 | 55 | def manage(ctx, args, **kwargs): 56 | python(ctx, '{manage} {args}'.format(manage=MANAGE, args=args), **kwargs) 57 | 58 | 59 | @task 60 | def clean(ctx): 61 | """ 62 | Removes generate directories and compiled files 63 | """ 64 | print(success('Cleaning cached files...')) 65 | ctx.run('find %s | grep -E "(__pycache__|\.pyc|\.pyo$)" | xargs rm -rf' % DIR, shell=os.environ.get('INVOKE_SHELL')) 66 | 67 | 68 | @task 69 | def virtualenv(ctx): 70 | """ 71 | Creates a virtualenv with the provided Python version, falls back to python3 72 | :param ctx: 73 | """ 74 | if os.environ.get('USE_VENV') == 'true': 75 | print(success('Creating virtualenv...')) 76 | ctx.run('virtualenv -p python{version} env'.format(version=os.environ.get('PYTHON_VERSION')), 77 | hide=os.environ.get('HIDE'), 78 | shell=os.environ.get('INVOKE_SHELL')) 79 | 80 | 81 | @task 82 | def requirements(ctx, env=ENV_DEFAULT, **kwargs): 83 | """ 84 | Installs requirements, optionally with devel requirements 85 | :param ctx: 86 | :param env: 87 | """ 88 | if env == ENV_PRODUCTION: 89 | print(success('Installing requirements...')) 90 | pip(ctx, 'install -r requirements.txt', **kwargs) 91 | else: 92 | print(success('Installing development requirements...')) 93 | pip(ctx, 'install -r requirements-devel.txt', **kwargs) 94 | 95 | 96 | @task 97 | def setup(ctx, env=ENV_DEFAULT): 98 | """ 99 | Runs all setup tasks (no management commands) 100 | :param ctx: 101 | :param env: 102 | """ 103 | if env == ENV_DEV and not os.path.isdir(VENV): 104 | virtualenv(ctx) 105 | requirements(ctx, env=env, hide=os.environ.get('HIDE')) 106 | 107 | 108 | @task 109 | def db_recreate(ctx): 110 | """ 111 | Drops and recreates the database 112 | """ 113 | print(success('Recreating the database...')) 114 | manage(ctx, 'recreate_database', hide=os.environ.get('HIDE')) 115 | 116 | 117 | @task 118 | def db_migrate(ctx): 119 | """ 120 | Runs database migrations 121 | """ 122 | print(success('Running database migrations...')) 123 | manage(ctx, 'migrate --noinput', hide=os.environ.get('HIDE')) 124 | 125 | 126 | @task 127 | def db_fixtures(ctx, verify_users=True, initial_revisions=True): 128 | """ 129 | Loads fixtures (test data) into the database 130 | 131 | :param ctx: 132 | :param verify_users: 133 | :param initial_revisions: 134 | """ 135 | fixtures = [ 136 | 'apps/core/fixtures/initial_data.json', 137 | 'apps/user/fixtures/auth_data.json', 138 | ] 139 | print(success('Loading fixtures...')) 140 | manage(ctx, 'loaddata {fixtures}'.format(fixtures=' '.join(fixtures)), hide=os.environ.get('HIDE')) 141 | 142 | if verify_users: 143 | # Verify user emails, so no email confirmations are necessary on sign-in 144 | print(success('Verifying users...')) 145 | manage(ctx, 'verify_users', hide=os.environ.get('HIDE')) 146 | 147 | 148 | @task 149 | def db(ctx, recreate=False, fixtures=False, wait=False): 150 | """ 151 | Runs all database tasks 152 | :param ctx: 153 | :param recreate: 154 | :param fixtures: 155 | """ 156 | if wait: 157 | print(success('Waiting for database connection...')) 158 | manage(ctx, 'wait_for_database', hide=os.environ.get('HIDE')) 159 | 160 | if recreate: 161 | db_recreate(ctx) 162 | 163 | db_migrate(ctx) 164 | 165 | if fixtures: 166 | db_fixtures(ctx) 167 | 168 | 169 | @task 170 | def static(ctx): 171 | """ 172 | Collect static files 173 | """ 174 | print(success('Collecting static files...')) 175 | manage(ctx, 'collectstatic --noinput', hide=os.environ.get('HIDE')) 176 | 177 | 178 | @task 179 | def test(ctx): 180 | """ 181 | Runs tests 182 | """ 183 | print(success('Running tests...')) 184 | ctx.run(prefix_venv('pytest'), shell=os.environ.get('INVOKE_SHELL')) 185 | 186 | 187 | @task(default=True) 188 | def main(ctx, env=ENV_DEFAULT): 189 | """ 190 | Does a full build for a python version and environment 191 | :param ctx: 192 | :param env: 193 | """ 194 | setup(ctx, env=env) 195 | db(ctx, recreate=(env == ENV_DEV), fixtures=(env == ENV_DEV)) 196 | -------------------------------------------------------------------------------- /templates/admin/base_site.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n admin_static log %} 3 | 4 | {% block title %}{{ title }} | {{ site_title|default:_('Django API Admin') }}{% endblock %} 5 | 6 | {% block branding %} 7 |

{{ site_header|default:_('Django API') }}

8 | {% endblock %} 9 | 10 | {% block nav-global %}{% endblock %} 11 | 12 | {% block extrastyle %} 13 | 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | 3 | {% block body %} 4 | {% if messages %} 5 |
6 | Messages: 7 | 12 |
13 | {% endif %} 14 | 15 |
16 | Menu: 17 | 26 |
27 | {% block content %} 28 | {% endblock %} 29 | {% endblock %} 30 | {% block extra_body %} 31 | {% endblock %} -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motius/django-gitlab-boilerplate/b10e0096b8760d4d001a3ba9c19dd909267a4334/tests/__init__.py -------------------------------------------------------------------------------- /tests/admin.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from django_admin_smoke_tests.tests import AdminSiteSmokeTestMixin 4 | 5 | 6 | class AdminSiteSmokeTest(AdminSiteSmokeTestMixin, TestCase): 7 | fixtures = [] 8 | 9 | def setUp(self): 10 | super().setUp() 11 | -------------------------------------------------------------------------------- /tests/models.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from io import StringIO 3 | from unittest.case import skip 4 | 5 | from django.core.management import call_command 6 | from django.test.utils import override_settings 7 | from django.apps import apps 8 | from django.core.urlresolvers import reverse 9 | from django.test import TestCase 10 | 11 | from libs.tests import BaseAPITestCase 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | @skip 17 | class CreateModelSmokeTest(BaseAPITestCase): 18 | """ 19 | TODO: Implement a model create test case 20 | """ 21 | def test_create_with_api(self): 22 | pc = YourModel.make() 23 | url = reverse('yourmodel-list', kwargs=dict(photochallenge_pk=pc.pk)) 24 | response = self.client.post(url, dict(title='title')) 25 | self.assert_status_code(response, 201) 26 | 27 | 28 | class ModelStringifyTestCase(TestCase): 29 | """ 30 | Call str() of all our models. Should not fail for (un)saved records. 31 | """ 32 | def test_model_str_methods(self): 33 | models = [m for m in apps.get_models() 34 | if m._meta.model.__module__.startswith("apps.")] 35 | for M in models: 36 | # Unsaved record: 37 | str(M()) 38 | # Existing record: 39 | if M.objects.exists(): 40 | str(M.objects.all()[0]) 41 | 42 | 43 | class TestMissingMigrations(TestCase): 44 | @override_settings(MIGRATION_MODULES={}) 45 | def test_for_missing_migrations(self): 46 | output = StringIO() 47 | try: 48 | call_command('makemigrations', interactive=False, dry_run=True, exit_code=True, stdout=output) 49 | except SystemExit as e: 50 | # The exit code will be 1 when there are no missing migrations 51 | self.assertEqual(str(e), '1', 'makemigrations exit code indicates there are missing migrations') 52 | else: 53 | self.fail("There are missing migrations:\n %s" % output.getvalue()) 54 | -------------------------------------------------------------------------------- /tests/recipes.py: -------------------------------------------------------------------------------- 1 | import string 2 | import datetime 3 | import random 4 | 5 | from allauth.account.models import EmailAddress 6 | from django.contrib.auth import get_user_model 7 | from model_mommy.random_gen import gen_email 8 | from model_mommy.recipe import Recipe, seq, foreign_key, related 9 | 10 | 11 | def generate_id(size=6, chars=string.ascii_uppercase + string.digits): 12 | return ''.join(random.choice(chars) for _ in range(size)) 13 | 14 | 15 | def generate_short_id(): 16 | return generate_id(4) 17 | 18 | 19 | def generate_id_or_none(*args, **kwargs): 20 | if random.uniform(0, 1): 21 | return generate_id(*args, **kwargs) 22 | else: 23 | return None 24 | 25 | 26 | def generate_past_date(): 27 | random_date = datetime.date.today()\ 28 | - datetime.timedelta(days=6 * 30)\ 29 | + datetime.timedelta(days=random.uniform(-100, 100)) 30 | return random_date 31 | 32 | 33 | def generate_future_date(): 34 | random_date = datetime.date.today()\ 35 | + datetime.timedelta(days=6 * 30)\ 36 | + datetime.timedelta(days=random.uniform(-100, 100)) 37 | return random_date 38 | 39 | 40 | def generate_duration(): 41 | return random.randrange(1, 20) 42 | 43 | 44 | def generate_duration_or_zero(): 45 | if random.uniform(0, 10) > 8: 46 | return generate_duration() 47 | else: 48 | return 0 49 | 50 | 51 | def generate_bool(): 52 | return random.uniform(0, 1) > 0 53 | 54 | 55 | def generate_color_hex(): 56 | """ 57 | See http://stackoverflow.com/a/14019260 58 | """ 59 | def r(): 60 | return random.randint(0, 255) 61 | return '#%02X%02X%02X' % (r(), r(), r()) 62 | 63 | 64 | UserRecipe = Recipe(get_user_model(), email=gen_email()) 65 | EmailAddressRecipe = Recipe(EmailAddress, user=foreign_key(UserRecipe), verified=True, primary=True) 66 | -------------------------------------------------------------------------------- /tests/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | 4 | from django.contrib.auth import get_user_model 5 | from hamcrest import assert_that, has_key 6 | 7 | from django.core.urlresolvers import reverse 8 | 9 | from apps.core.urls import router 10 | from libs.tests import BaseAPITestCase 11 | from tests.recipes import UserRecipe 12 | from .recipes import EmailAddressRecipe 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | 17 | class TestAPIResponses(BaseAPITestCase): 18 | """ 19 | Smoke test to see if (almost) all api views work. 20 | """ 21 | # Overwrite args for detail routes that do not use an id 22 | detail_args = {} 23 | ignore_urls = ['api-root'] 24 | 25 | def setUp(self): 26 | super().setUp() 27 | urls = set(list([u.name for u in router.get_urls()])) 28 | self.default_viewset_urls = [] 29 | self.extra_urls = [] 30 | 31 | for u in urls: 32 | if u in self.ignore_urls: 33 | continue 34 | u = 'api:{}'.format(u) 35 | if u.endswith('-detail') or u.endswith('-list'): 36 | self.default_viewset_urls.append(u) 37 | else: 38 | self.extra_urls.append(u) 39 | 40 | def test_default_viewset_reponses(self): 41 | for name in self.default_viewset_urls: 42 | if name.endswith('-detail'): 43 | args = self.detail_args[name] if name in self.detail_args else [1] 44 | url = reverse(name, args=args) 45 | else: 46 | url = reverse(name) 47 | 48 | if name.endswith('-list'): 49 | self.assert_url_list_not_empty(url) 50 | else: 51 | self.get_200_response(url) 52 | 53 | def test_extra_viewset_reponses(self): 54 | no_list_urls = [] 55 | urls = self.extra_urls 56 | for name in urls: 57 | url = reverse(name) 58 | if name in no_list_urls: 59 | self.get_200_response(url) 60 | else: 61 | self.assert_url_list_not_empty(url) 62 | 63 | def test_standalone_responses(self): 64 | urls = [] 65 | 66 | for url in urls: 67 | self.get_200_response(url) 68 | 69 | def test_token_login(self): 70 | data = dict(username="bar", password="bar") 71 | user = get_user_model().objects.create_user(**data) 72 | EmailAddressRecipe.make(user=user, verified=True) 73 | url = reverse('rest_login') 74 | response = self.client.post(url, data) 75 | assert_that(json.loads(response.content.decode("utf8")), 76 | has_key("token")) 77 | --------------------------------------------------------------------------------