version 0.0.1: add project, model user and api
This commit is contained in:
parent
0960ca9401
commit
fc2a689b6d
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal 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
3
.gitmodules
vendored
Normal 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
8
Dockerfile
Normal 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
|
||||
3
_dockerfiles/db/Dockerfile
Normal file
3
_dockerfiles/db/Dockerfile
Normal 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
0
apps/__init__.py
Normal file
0
apps/account/__init__.py
Normal file
0
apps/account/__init__.py
Normal file
42
apps/account/admin.py
Normal file
42
apps/account/admin.py
Normal 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
7
apps/account/apps.py
Normal 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')
|
||||
0
apps/account/management/__init__.py
Normal file
0
apps/account/management/__init__.py
Normal file
0
apps/account/management/commands/__init__.py
Normal file
0
apps/account/management/commands/__init__.py
Normal file
42
apps/account/migrations/0001_initial.py
Normal file
42
apps/account/migrations/0001_initial.py
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
||||
0
apps/account/migrations/__init__.py
Normal file
0
apps/account/migrations/__init__.py
Normal file
43
apps/account/models.py
Normal file
43
apps/account/models.py
Normal 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()
|
||||
57
apps/account/serializers.py
Normal file
57
apps/account/serializers.py
Normal 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
3
apps/account/tests.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
11
apps/account/urls.py
Normal file
11
apps/account/urls.py
Normal 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
53
apps/account/views.py
Normal 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
0
apps/utils/__init__.py
Normal file
30
apps/utils/filters.py
Normal file
30
apps/utils/filters.py
Normal 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
10
apps/utils/methods.py
Normal 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
75
apps/utils/models.py
Normal 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
46
apps/utils/pagination.py
Normal 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
3
apps/utils/views.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from rest_framework import generics
|
||||
|
||||
|
||||
BIN
celerybeat-schedule
Normal file
BIN
celerybeat-schedule
Normal file
Binary file not shown.
89
docker-compose.yml
Normal file
89
docker-compose.yml
Normal 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
21
manage.py
Executable 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
3
project/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from .celery import app as celery_app
|
||||
|
||||
__all__ = ['celery_app']
|
||||
15
project/celery.py
Normal file
15
project/celery.py
Normal 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))
|
||||
17
project/settings/__init__.py
Normal file
17
project/settings/__init__.py
Normal 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
273
project/settings/base.py
Normal 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},
|
||||
}
|
||||
}
|
||||
7
project/settings/development.py
Normal file
7
project/settings/development.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
"""Development settings."""
|
||||
from .base import *
|
||||
|
||||
ALLOWED_HOSTS = ['api.gm.id-east.ru', ]
|
||||
|
||||
SEND_SMS = False
|
||||
SMS_CODE_SHOW = True
|
||||
7
project/settings/local.py
Normal file
7
project/settings/local.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
"""Local settings."""
|
||||
from .base import *
|
||||
|
||||
ALLOWED_HOSTS = ['*', ]
|
||||
|
||||
SEND_SMS = False
|
||||
SMS_CODE_SHOW = True
|
||||
2
project/settings/production.py
Normal file
2
project/settings/production.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
"""Production settings."""
|
||||
from .base import *
|
||||
63
project/urls.py
Normal file
63
project/urls.py
Normal 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
16
project/wsgi.py
Normal 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
20
requirements/base.txt
Normal 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
|
||||
0
requirements/development.txt
Normal file
0
requirements/development.txt
Normal file
4
run_celery.sh
Executable file
4
run_celery.sh
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
sleep 5
|
||||
|
||||
celery -A project worker -l info -c 2
|
||||
4
run_celery_beat.sh
Executable file
4
run_celery_beat.sh
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
sleep 5
|
||||
|
||||
celery -A project worker -B -l info
|
||||
BIN
static/bootstrap_admin/img/logo-140x60.png
Normal file
BIN
static/bootstrap_admin/img/logo-140x60.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
272
static/css/_bootswatch.scss
Normal file
272
static/css/_bootswatch.scss
Normal 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
154
static/css/_variables.scss
Normal 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
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
12
static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user