version 0.0.1: add project, model user and api

This commit is contained in:
Dmitriy Kuzmenko 2019-08-07 16:10:23 +03:00
parent 0960ca9401
commit fc2a689b6d
44 changed files with 12241 additions and 0 deletions

19
.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# IDE stuff:
.idea/
.project/
.tags*
# Django stuff:
project/settings/custom.py
media/
logs/
#virtualenv
.env/
/datadir/

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "services/smsc"]
path = services/smsc
url = ssh://git@gitlab.spider.ru:32/in-house/smsc-connector.git

8
Dockerfile Normal file
View File

@ -0,0 +1,8 @@
FROM python:3.7
ENV PYTHONUNBUFFERED 1
RUN apt-get update; apt-get --assume-yes install binutils libproj-dev gdal-bin gettext
RUN mkdir /code
WORKDIR /code
ADD . /code/
RUN pip install --no-cache-dir -r /code/requirements/base.txt && \
pip install --no-cache-dir -r /code/requirements/development.txt

View File

@ -0,0 +1,3 @@
FROM mdillon/postgis:9.5
RUN localedef -i ru_RU -c -f UTF-8 -A /usr/share/locale/locale.alias ru_RU.UTF-8
ENV LANG ru_RU.utf8

0
apps/__init__.py Normal file
View File

0
apps/account/__init__.py Normal file
View File

42
apps/account/admin.py Normal file
View File

@ -0,0 +1,42 @@
"""Account app admin settings."""
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.utils.translation import ugettext_lazy as _
from account import models
@admin.register(models.User)
class UserAdmin(BaseUserAdmin):
"""User model admin settings."""
list_display = ('id', 'username', 'short_name', 'date_joined', 'is_active',
'is_staff', 'is_superuser',)
list_filter = ('is_active', 'is_staff', 'is_superuser', 'groups',)
search_fields = ('email', 'first_name', 'last_name')
readonly_fields = ('last_login', 'date_joined',)
fieldsets = (
(None, {'fields': ('email', 'password',)}),
(_('Personal info'), {
'fields': ('username', 'first_name', 'last_name', )}),
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
(_('Permissions'), {
'fields': (
'is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions'),
'classes': ('collapse', )
}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('password1', 'password2'),
}),
)
ordering = ('-date_joined', 'id',)
def short_name(self, obj):
"""Get user's short name."""
return obj.get_short_name()
short_name.short_description = _('Name')

7
apps/account/apps.py Normal file
View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class AccountConfig(AppConfig):
name = 'account'
verbose_name = _('Account')

View File

View File

@ -0,0 +1,42 @@
# Generated by Django 2.2.4 on 2019-08-07 12:42
import django.contrib.auth.validators
from django.db import migrations, models
import django.utils.timezone
import easy_thumbnails.fields
import utils.models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0011_update_proxy_permissions'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('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')),
('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('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')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('image', easy_thumbnails.fields.ThumbnailerImageField(blank=True, default=None, null=True, upload_to=utils.models.image_path, verbose_name='Image')),
('email', models.EmailField(blank=True, default=None, max_length=254, null=True, verbose_name='email address')),
('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')),
('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')),
],
options={
'verbose_name': 'User',
'verbose_name_plural': 'Users',
},
),
]

View File

43
apps/account/models.py Normal file
View File

@ -0,0 +1,43 @@
from django.contrib.auth.models import AbstractUser, UserManager as BaseUserManager
from django.db import models
from django.utils.translation import ugettext_lazy as _
from rest_framework.authtoken.models import Token
from utils.models import ImageMixin
class UserManager(BaseUserManager):
"""Extended manager for User model."""
use_in_migrations = False
class UserQuerySet(models.QuerySet):
"""Extended queryset for User model."""
def active(self, switcher=True):
"""Filter only active users."""
return self.filter(is_active=switcher)
class User(ImageMixin, AbstractUser):
"""Base user model."""
email = models.EmailField(_('email address'), blank=True,
null=True, default=None)
EMAIL_FIELD = 'email'
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = ['email']
objects = UserManager.from_queryset(UserQuerySet)()
class Meta:
"""Meta class."""
verbose_name = _('User')
verbose_name_plural = _('Users')
def __str__(self):
"""String method."""
return "%s:%s" % (self.email, self.get_short_name())
def remove_token(self):
Token.objects.filter(user=self).delete()

View File

@ -0,0 +1,57 @@
from fcm_django.models import FCMDevice
from rest_framework import serializers, exceptions
from account import models
class UserSerializer(serializers.ModelSerializer):
"""User serializer."""
class Meta:
model = models.User
fields = [
'first_name',
'last_name',
]
class FCMDeviceSerializer(serializers.ModelSerializer):
"""FCM Device model serializer"""
class Meta:
model = FCMDevice
fields = ('id', 'name', 'registration_id', 'device_id',
'active', 'date_created', 'type')
read_only_fields = ('id', 'date_created',)
extra_kwargs = {'active': {'default': True}}
def validate(self, attrs):
regid = attrs.get('registration_id')
dtype = attrs.get('type')
if regid and dtype and self.Meta.model.objects.filter(
registration_id=regid).exclude(type=dtype).count():
raise exceptions.ValidationError(
{'registration_id': 'This field must be unique.'})
return attrs
def __init__(self, *args, **kwargs):
super(FCMDeviceSerializer, self).__init__(*args, **kwargs)
self.fields['type'].help_text = (
'Should be one of ["%s"]' %
'", "'.join([i for i in self.fields['type'].choices]))
def create(self, validated_data):
user = self.context['request'].user
if not user.is_anonymous:
validated_data['user'] = user
device = FCMDevice.objects.create(**validated_data)
return device
def update(self, instance, validated_data):
user = self.context['request'].user
if not user.is_anonymous:
instance.user = user
instance.save()
else:
instance.user = None
instance.save()
return instance

3
apps/account/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

11
apps/account/urls.py Normal file
View File

@ -0,0 +1,11 @@
"""Account app urlconf."""
from django.urls import path
from account import views
app_name = 'account'
urlpatterns = [
path('user/', views.UserView.as_view(), name='user_get_update'),
path('device/', views.FCMDeviceViewSet.as_view(), name='fcm_device_create'),
]

53
apps/account/views.py Normal file
View File

@ -0,0 +1,53 @@
from fcm_django.models import FCMDevice
from rest_framework import generics, status
from rest_framework import permissions
from rest_framework.response import Response
from account import models
from account import serializers
class UserView(generics.RetrieveUpdateAPIView):
"""### User update view."""
permission_classes = (permissions.IsAuthenticated,)
serializer_class = serializers.UserSerializer
queryset = models.User.objects.active()
def get_object(self):
return self.request.user
class FCMDeviceViewSet(generics.GenericAPIView):
"""FCMDevice registration view.
* Pair of fields **registration_id** and **type** should be unique.
* In case of requested device existance, existing device will be returned
instead of creating new one.
"""
serializer_class = serializers.FCMDeviceSerializer
lookup_fields = ('registration_id', 'type',)
queryset = FCMDevice.objects.all()
permission_classes = (permissions.AllowAny, )
def post(self, request, *args, **kwargs):
"""Override post method."""
instance = self.get_object_or_none()
serializer = self.get_serializer(instance, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(
serializer.data, status=status.HTTP_200_OK if instance else status.HTTP_201_CREATED)
def get_object_or_none(self):
"""Object as resylt and the view is displaying or None."""
queryset = self.get_queryset() # get the base queryset
queryset = self.filter_queryset(queryset) # apply any filter backends
# generate filter
filter = {f: self.request.data.get(f) for f in self.lookup_fields
if self.request.data.get(f)}
# get object and check permissions or return None
obj = queryset.filter(**filter).first()
obj and self.check_object_permissions(self.request, obj)
return obj

0
apps/utils/__init__.py Normal file
View File

30
apps/utils/filters.py Normal file
View File

@ -0,0 +1,30 @@
"""Marketplace filters."""
from django.db.models import F
from django_filters import rest_framework as filters
EMPTY_VALUES = ([], (), {}, '', None)
class NullsAlwaysLastOrderingFilter(filters.OrderingFilter):
""" Use Django 1.11 nulls_last feature to force nulls to bottom in all orderings. """
def filter(self, qs, value):
if value in EMPTY_VALUES:
return qs
ordering = [self.get_ordering_value(param) for param in value]
if ordering:
f_ordering = []
for o in ordering:
if not o:
continue
if o[0] == '-':
f_ordering.append(F(o[1:]).desc(nulls_last=True))
else:
f_ordering.append(F(o).asc(nulls_last=True))
return qs.order_by(*f_ordering)
return qs

10
apps/utils/methods.py Normal file
View File

@ -0,0 +1,10 @@
"""Utils app method."""
import random
def generate_code(digits=6, string_output=True):
"""Generate random int."""
max_value = 10 ** digits - 1
min_value = 10 ** (digits - 1)
value = random.randint(min_value, max_value)
return str(value) if string_output else value

75
apps/utils/models.py Normal file
View File

@ -0,0 +1,75 @@
"""Utils app models."""
import random
from datetime import datetime
from django.conf import settings
from django.contrib.gis.db import models
from django.utils import timezone
from django.utils.html import mark_safe
from django.utils.translation import ugettext_lazy as _
from easy_thumbnails.fields import ThumbnailerImageField
class ProjectBaseMixin(models.Model):
"""Base mixin model."""
created = models.DateTimeField(default=timezone.now, editable=False,
verbose_name=_('Date created'))
modified = models.DateTimeField(auto_now=True,
verbose_name=_('Date updated'))
class Meta:
"""Meta class."""
abstract = True
basemixin_fields = ['created', 'modified']
def generate_code():
"""Generate code method."""
return '%06d' % random.randint(0, 999999)
def image_path(instance, filename):
"""Determine avatar path method."""
filename = '%s.jpeg' % generate_code()
return 'image/%s/%s/%s' % (
instance._meta.model_name,
datetime.now().strftime(settings.REST_DATE_FORMAT),
filename)
class ImageMixin(models.Model):
"""Avatar model."""
THUMBNAIL_KEY = 'default'
image = ThumbnailerImageField(upload_to=image_path,
blank=True, null=True, default=None,
verbose_name=_('Image'))
class Meta:
"""Meta class."""
abstract = True
def get_image(self, key=None):
"""Get thumbnailed image file."""
return self.image[key or self.THUMBNAIL_KEY] if self.image else None
def get_image_url(self, key=None):
"""Get image thumbnail url."""
return self.get_image(key).url if self.image else None
def image_tag(self):
"""Admin preview tag."""
if self.image:
return mark_safe('<img src="%s" />' % self.get_image_url())
else:
return None
image_tag.short_description = _('Image')
image_tag.allow_tags = True

46
apps/utils/pagination.py Normal file
View File

@ -0,0 +1,46 @@
"""Pagination settings."""
from base64 import b64encode
from urllib import parse as urlparse
from rest_framework.pagination import PageNumberPagination, CursorPagination
class ProjectPageNumberPagination(PageNumberPagination):
"""Customized pagination class."""
page_size_query_param = 'page_size'
class ProjectCursorPagination(CursorPagination):
"""Customized cursor pagination class."""
def encode_cursor(self, cursor):
"""
Given a Cursor instance, return an url with encoded cursor.
"""
tokens = {}
if cursor.offset != 0:
tokens['o'] = str(cursor.offset)
if cursor.reverse:
tokens['r'] = '1'
if cursor.position is not None:
tokens['p'] = cursor.position
querystring = urlparse.urlencode(tokens, doseq=True)
encoded = b64encode(querystring.encode('ascii')).decode('ascii')
return encoded
class ProjectMobilePagination(ProjectPageNumberPagination):
"""Pagination settings for mobile API."""
def get_next_link(self):
"""Get next link method."""
if not self.page.has_next():
return None
return self.page.next_page_number()
def get_previous_link(self):
"""Get previous link method."""
if not self.page.has_previous():
return None
return self.page.previous_page_number()

3
apps/utils/views.py Normal file
View File

@ -0,0 +1,3 @@
from rest_framework import generics

BIN
celerybeat-schedule Normal file

Binary file not shown.

89
docker-compose.yml Normal file
View File

@ -0,0 +1,89 @@
version: '3.5'
services:
# PostgreSQL database
db:
build:
context: ./_dockerfiles/db
dockerfile: Dockerfile
hostname: db
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=postgres
ports:
- "5436:5432"
networks:
- db-net
volumes:
- gm-db:/var/lib/postgresql/data/
# RabbitMQ
rabbitmq:
image: rabbitmq:latest
ports:
- "5672:5672"
# Celery
worker:
build: .
command: ./run_celery.sh
environment:
- SETTINGS_CONFIGURATION=local
- DB_NAME=postgres
- DB_USERNAME=postgres
- DB_HOSTNAME=db
- DB_PORT=5432
- DB_PASSWORD=postgres
volumes:
- .:/code
links:
- db
- rabbitmq
worker_beat:
build: .
command: ./run_celery_beat.sh
environment:
- SETTINGS_CONFIGURATION=local
- DB_NAME=postgres
- DB_USERNAME=postgres
- DB_HOSTNAME=db
- DB_PORT=5432
- DB_PASSWORD=postgres
volumes:
- .:/code
links:
- db
- rabbitmq
# App: G&M
gm_app:
build: .
command: python manage.py runserver 0.0.0.0:8000
environment:
- SETTINGS_CONFIGURATION=local
- DB_HOSTNAME=db
- DB_PORT=5432
- DB_NAME=postgres
- DB_USERNAME=postgres
- DB_PASSWORD=postgres
depends_on:
- db
- rabbitmq
- worker
- worker_beat
networks:
- app-net
- db-net
volumes:
- .:/code
- gm-media:/media-data
ports:
- "8000:8000"
networks:
app-net:
db-net:
volumes:
gm-db:
name: gm-db
gm-media:
name: gm-media

21
manage.py Executable file
View File

@ -0,0 +1,21 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

3
project/__init__.py Normal file
View File

@ -0,0 +1,3 @@
from .celery import app as celery_app
__all__ = ['celery_app']

15
project/celery.py Normal file
View File

@ -0,0 +1,15 @@
import os
from celery import Celery
# set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')
app = Celery('project')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()
@app.task(bind=True)
def debug_task(self):
print('Request: {0!r}'.format(self.request))

View File

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
"""Initial for settings app."""
import os
configuration = os.environ.get('SETTINGS_CONFIGURATION', None)
if configuration == 'local':
# local machine server settings
from .local import *
elif configuration == 'development':
# development server settings
from .development import *
elif configuration == 'production':
# production server settings
from .production import *
else:
from .base import *

273
project/settings/base.py Normal file
View File

@ -0,0 +1,273 @@
"""
Django settings for project project.
Generated by 'django-admin startproject' using Django 2.2.2.
For more information on this file, see
https://docs.djangoproject.com/en/2.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.2/ref/settings/
"""
import os
import sys
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_ROOT = os.path.abspath(os.path.join(BASE_DIR, '..'))
PUBLIC_ROOT = os.path.abspath(os.path.join(BASE_DIR, '..', 'media'))
# # insert apps and libs dirs to sys.path
for path in ('apps', 'services',):
path = os.path.abspath(os.path.join(PROJECT_ROOT, '%s' % path))
path in sys.path or sys.path.insert(0, path)
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'ta$edl0av6n#-xphew5922npa4f*h8mfv#$@4tids*gbrp17&b'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
CONTRIB_APPS = [
'bootstrap_admin',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.gis',
]
PROJECT_APPS = [
'account.apps.AccountConfig',
]
EXTERNAL_APPS = [
'django_filters',
'drf_yasg',
'fcm_django',
'easy_thumbnails',
'rest_framework',
'rest_framework.authtoken',
'easy_select2',
]
INSTALLED_APPS = CONTRIB_APPS + PROJECT_APPS + EXTERNAL_APPS
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'project.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'project.wsgi.application'
# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.contrib.gis.db.backends.postgis',
'NAME': os.environ.get('DB_NAME'),
'USER': os.environ.get('DB_USERNAME'),
'PASSWORD': os.environ.get('DB_PASSWORD'),
'HOST': os.environ.get('DB_HOSTNAME'),
'PORT': os.environ.get('DB_PORT'),
},
}
# Password validation
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Account settings
AUTH_USER_MODEL = 'account.User'
LOGIN_URL = 'admin:login'
LOGOUT_URL = 'admin:logout'
# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/
LANGUAGE_CODE = 'ru'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
LOCALE_PATHS = (
os.path.abspath(os.path.join(BASE_DIR, 'locale')),
)
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/
STATIC_ROOT = os.path.join(PUBLIC_ROOT, 'static')
STATIC_URL = '/static/'
MEDIA_ROOT = os.path.join(PUBLIC_ROOT, 'media')
MEDIA_URL = '/media/'
STATICFILES_DIRS = (
os.path.join(PROJECT_ROOT, 'static'),
)
FILE_UPLOAD_PERMISSIONS = 0o644
AVAILABLE_VERSIONS = {
# 'future': '1.0.1',
'current': '1.0.0',
# 'temp': '1.0.0'
}
# Django Rest Framework
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': (
'django_filters.rest_framework.DjangoFilterBackend',
),
'PAGE_SIZE': 10,
'DEFAULT_PAGINATION_CLASS': 'utils.pagination.ProjectMobilePagination',
'COERCE_DECIMAL_TO_STRING': False,
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
),
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning',
'DEFAULT_VERSION': (AVAILABLE_VERSIONS['current'],),
'ALLOWED_VERSIONS': AVAILABLE_VERSIONS.values(),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
# 'DATETIME_FORMAT': '%m-%d-%Y %H:%M:%S', # experiment
# 'DATE_FORMAT': '%s.%f', # experiment
}
REST_DATE_FORMAT = '%m-%d-%Y'
REST_DATETIME_FORMAT = '%m-%d-%Y %H:%M:%S'
# SMS Settings
SMS_EXPIRATION = 5
SMS_SEND_DELAY = 30
SMS_ATTEMPT_LIMIT = 3
# Following items used in several models
# make auto migrations if change
SMS_CODE_LENGTH = 6
SEND_SMS = True
SMS_CODE_SHOW = False
# SMSC Settings
SMS_SERVICE = 'http://smsc.ru/sys/send.php'
SMS_LOGIN = 'GM2019'
SMS_PASSWORD = '}#6%Qe7CYG7n'
SMS_SENDER = 'GM'
# Django Rest Swagger
SWAGGER_SETTINGS = {
# "DEFAULT_GENERATOR_CLASS": "rest_framework.schemas.generators.BaseSchemaGenerator",
'JSON_EDITOR': False,
'SHOW_REQUEST_HEADERS': True,
'SECURITY_DEFINITIONS': {
'Basic': {
'type': 'basic'
},
'Token': {
'type': 'apiKey',
'name': 'Authorization',
'in': 'header'
}
},
}
REDOC_SETTINGS = {
'LAZY_RENDERING': False,
}
# CELERY
BROKER_URL = 'amqp://rabbitmq:5672'
CELERY_RESULT_BACKEND = BROKER_URL
CELERY_BROKER_URL = BROKER_URL
CELERY_ACCEPT_CONTENT = ['application/json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = TIME_ZONE
USE_CELERY = False
# Django FCM (Firebase push notificatoins)
FCM_DJANGO_SETTINGS = {
'FCM_SERVER_KEY': (
"AAAAJcC4Vbc:APA91bGovq7233-RHu2MbZTsuMU4jNf3obOue8s"
"7qVznx-Xwr_h_R8Nz2Wfhf6hWIXdt99hr6xOH9WRVhxnC5eH96s"
"6rYnpKEY6OrcVoLtceIR1yRrBZJtklDiSkP7daoeCqsnQmtB2m"
),
}
# Thumbnail settings
THUMBNAIL_ALIASES = {
'': {
'tiny': {'size': (100, 0), },
'small': {'size': (480, 0), },
'middle': {'size': (700, 0), },
'large': {'size': (1500, 0), },
'default': {'size': (300, 200), 'crop': True},
'gallery': {'size': (240, 160), 'crop': True},
}
}

View File

@ -0,0 +1,7 @@
"""Development settings."""
from .base import *
ALLOWED_HOSTS = ['api.gm.id-east.ru', ]
SEND_SMS = False
SMS_CODE_SHOW = True

View File

@ -0,0 +1,7 @@
"""Local settings."""
from .base import *
ALLOWED_HOSTS = ['*', ]
SEND_SMS = False
SMS_CODE_SHOW = True

View File

@ -0,0 +1,2 @@
"""Production settings."""
from .base import *

63
project/urls.py Normal file
View File

@ -0,0 +1,63 @@
"""project URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/2.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include, re_path
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework import permissions
schema_view = get_schema_view(
openapi.Info(
title="G&M API",
default_version='v1',
description="Test description",
terms_of_service="https://www.google.com/policies/terms/",
contact=openapi.Contact(email="contact@snippets.local"),
license=openapi.License(name="BSD License"),
),
public=True,
permission_classes=(permissions.AllowAny,),
)
urlpatterns_doc = [
re_path(r'^docs(?P<format>\.json|\.yaml)$',
schema_view.without_ui(cache_timeout=0),
name='schema-json'),
re_path(r'^docs/$',
schema_view.with_ui('swagger', cache_timeout=0),
name='schema-swagger-ui'),
re_path(r'^redocs/$',
schema_view.with_ui('redoc', cache_timeout=0),
name='schema-redoc'),
]
urlpatterns_api = [
path('account/', include('account.urls'), name='account'),
]
urlpatterns = [
path('admin/', admin.site.urls),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
path('api/', include(urlpatterns_api)),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if settings.DEBUG:
urlpatterns.extend(urlpatterns_doc)

16
project/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
WSGI config for project project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')
application = get_wsgi_application()

20
requirements/base.txt Normal file
View File

@ -0,0 +1,20 @@
Django==2.2.4
psycopg2-binary==2.8.3
pytz==2019.1
sqlparse==0.3.0
requests
django-solo
easy-thumbnails
fcm-django
django-easy-select2
bootstrap-admin
drf-yasg==1.16.0
djangorestframework==3.9.4
markdown
django-filter==2.1.0
djangorestframework-xml
celery
amqp>=2.4.0
cloudpayments

View File

4
run_celery.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
sleep 5
celery -A project worker -l info -c 2

4
run_celery_beat.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
sleep 5
celery -A project worker -B -l info

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

272
static/css/_bootswatch.scss Normal file
View File

@ -0,0 +1,272 @@
// Superhero 4.3.1
// Bootswatch
// Variables ===================================================================
$web-font-path: "https://fonts.googleapis.com/css?family=Lato:300,400,700" !default;
@import url($web-font-path);
// Navbar ======================================================================
.navbar {
font-size: $font-size-sm;
}
// Buttons =====================================================================
.btn {
@each $color, $value in $theme-colors {
&-#{$color} {
@if $enable-gradients {
background: $value linear-gradient(180deg, mix($white, $value, 15%), $value) repeat-x;
} @else {
background-color: $value;
}
}
}
}
// Typography ==================================================================
.dropdown-menu {
font-size: $font-size-sm;
}
.dropdown-header {
font-size: $font-size-sm;
}
.blockquote-footer {
color: $body-color;
}
// Tables ======================================================================
.table {
font-size: $font-size-sm;
.thead-dark th {
color: $white;
}
a:not(.btn) {
color: #fff;
text-decoration: underline;
}
.dropdown-menu a {
text-decoration: none;
}
.text-muted {
color: $text-muted;
}
&-dark {
color: #fff;
}
&-primary {
&, > th, > td {
background-color: $primary;
}
}
&-secondary {
&, > th, > td {
background-color: $secondary;
}
}
&-light {
&, > th, > td {
background-color: $light;
}
}
&-dark {
&, > th, > td {
background-color: $dark;
}
}
&-success {
&, > th, > td {
background-color: $success;
}
}
&-info {
&, > th, > td {
background-color: $info;
}
}
&-danger {
&, > th, > td {
background-color: $danger;
}
}
&-warning {
&, > th, > td {
background-color: $warning;
}
}
&-active {
&, > th, > td {
background-color: $table-active-bg;
}
}
&-hover {
.table-primary:hover {
&, > th, > td {
background-color: darken($primary, 5%);
}
}
.table-secondary:hover {
&, > th, > td {
background-color: darken($secondary, 5%);
}
}
.table-light:hover {
&, > th, > td {
background-color: darken($light, 5%);
}
}
.table-dark:hover {
&, > th, > td {
background-color: darken($dark, 5%);
}
}
.table-success:hover {
&, > th, > td {
background-color: darken($success, 5%);
}
}
.table-info:hover {
&, > th, > td {
background-color: darken($info, 5%);
}
}
.table-danger:hover {
&, > th, > td {
background-color: darken($danger, 5%);
}
}
.table-warning:hover {
&, > th, > td {
background-color: darken($warning, 5%);
}
}
.table-active:hover {
&, > th, > td {
background-color: $table-active-bg;
}
}
}
}
// Forms =======================================================================
label,
.radio label,
.checkbox label,
.help-block {
font-size: $font-size-sm;
}
// Navs ========================================================================
.nav-tabs,
.nav-pills {
.nav-link,
.nav-link:hover {
color: $body-color;
}
.nav-link.disabled {
color: $nav-link-disabled-color;
}
}
.page-link:hover,
.page-link:focus {
color: #fff;
text-decoration: none;
}
// Indicators ==================================================================
.alert {
border: none;
color: $white;
a,
.alert-link {
color: #fff;
text-decoration: underline;
}
@each $color, $value in $theme-colors {
&-#{$color} {
@if $enable-gradients {
background: $value linear-gradient(180deg, mix($white, $value, 15%), $value) repeat-x;
} @else {
background-color: $value;
}
}
}
}
.badge {
&-warning,
&-info {
color: $white;
}
}
.close {
opacity: 0.5;
&:hover,
&:focus {
opacity: 1;
}
}
// Progress bars ===============================================================
// Containers ==================================================================
.modal {
&-header,
&-footer {
background-color: $table-hover-bg;
.close {
color: #fff;
text-shadow: none;
opacity: 0.5;
&:hover,
&:focus {
opacity: 1;
}
}
}
}

154
static/css/_variables.scss Normal file
View File

@ -0,0 +1,154 @@
// Superhero 4.3.1
// Bootswatch
//
// Color system
//
$white: #fff !default;
$gray-100: #EBEBEB !default;
$gray-200: #4E5D6C !default;
$gray-300: #dee2e6 !default;
$gray-400: #ced4da !default;
$gray-500: #adb5bd !default;
$gray-600: #868e96 !default;
$gray-700: #495057 !default;
$gray-800: #343a40 !default;
$gray-900: #212529 !default;
$black: #000 !default;
$blue: #DF691A !default;
$indigo: #6610f2 !default;
$purple: #6f42c1 !default;
$pink: #e83e8c !default;
$red: #d9534f !default;
$orange: #f0ad4e !default;
$yellow: #f0ad4e !default;
$green: #5cb85c !default;
$teal: #20c997 !default;
$cyan: #5bc0de !default;
$primary: $blue !default;
$secondary: $gray-200 !default;
$success: $green !default;
$info: $cyan !default;
$warning: $yellow !default;
$danger: $red !default;
$light: lighten($gray-200, 35%) !default;
$dark: $gray-200 !default;
$yiq-contrasted-threshold: 185 !default;
// Body
$body-bg: #2B3E50 !default;
$body-color: $gray-100 !default;
// Components
$border-radius: 0 !default;
$border-radius-lg: 0 !default;
$border-radius-sm: 0 !default;
// Fonts
$font-family-sans-serif: "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !default;
$text-muted: rgba(255,255,255,.4) !default;
// Tables
$table-accent-bg: rgba($white,.05) !default;
$table-hover-bg: rgba($white,.075) !default;
$table-border-color: rgba($black,.15) !default;
$table-head-bg: $light !default;
$table-dark-bg: $light !default;
$table-dark-border-color: $gray-200 !default;
$table-dark-color: $body-bg !default;
// Forms
$input-border-color: transparent !default;
$input-group-addon-color: $body-color !default;
$custom-file-button-color: $white !default;
$custom-file-border-color: $gray-200 !default;
// Dropdowns
$dropdown-bg: $gray-200 !default;
$dropdown-divider-bg: rgba($black,.15) !default;
$dropdown-link-color: $body-color !default;
$dropdown-link-hover-color: $dropdown-link-color !default;
$dropdown-link-hover-bg: $table-hover-bg !default;
// Navs
$nav-link-disabled-color: rgba(255,255,255,.4) !default;
$nav-tabs-border-color: $gray-200 !default;
$nav-tabs-link-active-color: $body-color !default;
$nav-tabs-link-active-border-color: $gray-200 !default;
// Navbar
$navbar-padding-y: 0.25rem !default;
$navbar-dark-color: rgba($white,.75) !default;
$navbar-dark-hover-color: $white !default;
// Pagination
$pagination-color: $white !default;
$pagination-bg: $gray-200 !default;
$pagination-border-color: transparent !default;
$pagination-hover-color: $white !default;
$pagination-hover-bg: $nav-link-disabled-color !default;
$pagination-hover-border-color: $pagination-border-color !default;
$pagination-disabled-color: $nav-link-disabled-color !default;
$pagination-disabled-bg: $pagination-bg !default;
$pagination-disabled-border-color: $pagination-border-color !default;
// Modals
$modal-content-bg: $gray-200 !default;
$modal-header-border-color: rgba(0,0,0,.2) !default;
// Cards
$card-cap-bg: $table-hover-bg !default;
$card-bg: $gray-200 !default;
// Popovers
$popover-bg: $gray-200 !default;
$popover-header-bg: $table-hover-bg !default;
// List group
$list-group-bg: $gray-200 !default;
$list-group-border-color: transparent !default;
$list-group-hover-bg: $nav-link-disabled-color !default;
$list-group-disabled-color: $nav-link-disabled-color !default;
$list-group-action-color: $white !default;
$list-group-action-hover-color: $white !default;
// Breadcrumbs
$breadcrumb-divider-color: $body-color !default;
$breadcrumb-active-color: $body-color !default;
// Code
$pre-color: inherit !default;

10807
static/css/bootstrap.css vendored Normal file

File diff suppressed because it is too large Load Diff

12
static/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long