parent
74a982a7d6
commit
932c6d36be
@ -0,0 +1,31 @@
|
||||
version: '3.3'
|
||||
|
||||
services:
|
||||
web:
|
||||
restart: on-failure
|
||||
build:
|
||||
context: ./src
|
||||
dockerfile: Dockerfile
|
||||
command: gunicorn conf.wsgi:application --bind 0.0.0.0:8000
|
||||
volumes:
|
||||
- static_volume:/home/app/web/static
|
||||
- media_volume:/home/app/web/media
|
||||
ports:
|
||||
- 8000:8000
|
||||
depends_on:
|
||||
- db
|
||||
db:
|
||||
restart: always
|
||||
image: kartoza/postgis:13.0
|
||||
environment:
|
||||
- POSTGRES_USER=my_user
|
||||
- POSTGRES_PASSWORD=mysecretpassword
|
||||
- POSTGRES_DB=db
|
||||
ports:
|
||||
- 5432:5432
|
||||
redis:
|
||||
image: redis:5-alpine
|
||||
volumes:
|
||||
postgres_data:
|
||||
static_volume:
|
||||
media_volume:
|
@ -0,0 +1,69 @@
|
||||
###########
|
||||
# BUILDER #
|
||||
###########
|
||||
|
||||
# pull official base image
|
||||
FROM python:3.10-slim as builder
|
||||
|
||||
# set work directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
RUN apt-get update -y \
|
||||
&& apt-get install gcc python3-dev musl-dev binutils libproj-dev gdal-bin g++ -y
|
||||
|
||||
# lint
|
||||
RUN pip install --upgrade pip
|
||||
COPY . .
|
||||
|
||||
# install dependencies
|
||||
COPY ./requirements.txt .
|
||||
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt
|
||||
|
||||
|
||||
#########
|
||||
# FINAL #
|
||||
#########
|
||||
|
||||
# pull official base image
|
||||
FROM python:3.10-slim
|
||||
|
||||
# create directory for the app user
|
||||
RUN mkdir -p /home/app
|
||||
|
||||
# create the app user
|
||||
RUN addgroup --system app && adduser --system app --ingroup app
|
||||
|
||||
# create the appropriate directories
|
||||
ENV HOME=/home/app
|
||||
ENV APP_HOME=/home/app/web
|
||||
RUN mkdir $APP_HOME
|
||||
RUN mkdir $APP_HOME/static
|
||||
RUN mkdir $APP_HOME/media
|
||||
WORKDIR $APP_HOME
|
||||
|
||||
# install dependencies
|
||||
RUN apt-get update -y && apt-get install gcc python3-dev musl-dev binutils libproj-dev gdal-bin g++ libpq-dev -y
|
||||
COPY --from=builder /usr/src/app/wheels /wheels
|
||||
COPY --from=builder /usr/src/app/requirements.txt .
|
||||
RUN pip install --no-cache /wheels/*
|
||||
|
||||
# copy entrypoint.prod.sh
|
||||
COPY ./entrypoint.prod.sh .
|
||||
RUN sed -i 's/\r$//g' $APP_HOME/entrypoint.prod.sh
|
||||
RUN chmod +x $APP_HOME/entrypoint.prod.sh
|
||||
|
||||
# copy project
|
||||
COPY . $APP_HOME
|
||||
|
||||
# chown all the files to the app user
|
||||
RUN chown -R app:app $APP_HOME
|
||||
|
||||
# change to the app user
|
||||
USER app
|
||||
|
||||
# run entrypoint.prod.sh
|
||||
ENTRYPOINT ["/home/app/web/entrypoint.prod.sh"]
|
@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for core project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'conf.settings')
|
||||
|
||||
application = get_asgi_application()
|
@ -0,0 +1,28 @@
|
||||
from rest_framework.views import exception_handler
|
||||
|
||||
|
||||
def core_exception_handler(exc, context):
|
||||
response = exception_handler(exc, context)
|
||||
handlers = {
|
||||
'ValidationError': _handle_generic_error
|
||||
}
|
||||
exception_class = exc.__class__.__name__
|
||||
|
||||
if exception_class in handlers:
|
||||
return handlers[exception_class](exc, context, response)
|
||||
|
||||
if response:
|
||||
if response.data.get('detail') == 'Токен испорчен' or response.data.get('detail') == (
|
||||
'Пользователь соответствующий данному токену не найден.') or response.data.get('detail') == (
|
||||
'Учетные данные не были предоставлены.'
|
||||
):
|
||||
response.status_code = 401
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def _handle_generic_error(exc, context, response):
|
||||
response.data = {
|
||||
'errors': response.data
|
||||
}
|
||||
return response
|
@ -0,0 +1,170 @@
|
||||
"""
|
||||
Django settings for core project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 5.0.4.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/5.0/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import dj_database_url
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-*48y!hsr!tup+5z2r64o6c6mw%axkh@#bx62pes8=r7@_+7qtv'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'jwtauth',
|
||||
'organizations',
|
||||
'jwt',
|
||||
'corsheaders',
|
||||
'rest_framework',
|
||||
'drf_yasg',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
CORS_ALLOW_ALL_ORIGINS = True
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
|
||||
ROOT_URLCONF = 'conf.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 = 'conf.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.contrib.gis.db.backends.postgis',
|
||||
'NAME': 'db',
|
||||
'USER': 'my_user',
|
||||
'PASSWORD': 'mysecretpassword',
|
||||
'HOST': 'db',
|
||||
'PORT': '5432'
|
||||
}
|
||||
}
|
||||
|
||||
AUTH_USER_MODEL = 'jwtauth.CustomUser'
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/5.0/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',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/5.0/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'ru-ru'
|
||||
|
||||
TIME_ZONE = 'Europe/Moscow'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/5.0/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
STATIC_ROOT = BASE_DIR / 'static/'
|
||||
|
||||
MEDIA_URL = 'media/'
|
||||
MEDIA_ROOT = BASE_DIR / 'media'
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
|
||||
'EXCEPTION_HANDLER': 'conf.exceptions.core_exception_handler',
|
||||
# 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
||||
'PAGE_SIZE': 30,
|
||||
'NON_FIELD_ERRORS_KEY': 'error',
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'jwtauth.backends.JWTAuthentication',
|
||||
),
|
||||
}
|
||||
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": "redis://redis:6379/0",
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
|
||||
SESSION_CACHE_ALIAS = "default"
|
||||
|
||||
CACHE_TTL = 60 * 1
|
@ -0,0 +1,35 @@
|
||||
"""
|
||||
URL configuration for core project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/5.0/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 core.openapi import schema_view
|
||||
|
||||
api_urls = [
|
||||
path('organizations/', include('organizations.urls')),
|
||||
path('user/', include('jwtauth.urls'))
|
||||
]
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r'^docs/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
|
||||
re_path(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
|
||||
path('admin/', admin.site.urls),
|
||||
path('api/', include(api_urls))
|
||||
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for core 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/5.0/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'conf.settings')
|
||||
|
||||
application = get_wsgi_application()
|
@ -0,0 +1,7 @@
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
|
||||
class CodeExpired(APIException):
|
||||
status_code = 400
|
||||
default_detail = 'Email code has been expired'
|
||||
default_code = 'code_expired'
|
@ -0,0 +1,19 @@
|
||||
from django.contrib.gis.geos.point import Point
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class LocationField(serializers.Field):
|
||||
default_error_messages = {
|
||||
"invalid": "Value must be valid list with lang and lat values."
|
||||
}
|
||||
|
||||
def to_representation(self, value):
|
||||
return value.coords
|
||||
|
||||
def to_internal_value(self, data):
|
||||
try:
|
||||
point = Point((data[0], data[1]))
|
||||
except Exception:
|
||||
self.fail("invalid")
|
||||
|
||||
return point
|
@ -0,0 +1,24 @@
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.generators import OpenAPISchemaGenerator
|
||||
from drf_yasg.views import get_schema_view
|
||||
from rest_framework import permissions
|
||||
|
||||
|
||||
class BothHttpAndHttpsSchemaGenerator(OpenAPISchemaGenerator):
|
||||
def get_schema(self, request=None, public=False):
|
||||
schema = super().get_schema(request, public)
|
||||
schema.schemes = ["http", "https"]
|
||||
return schema
|
||||
|
||||
|
||||
schema_view = get_schema_view(
|
||||
openapi.Info(
|
||||
title="",
|
||||
default_version='v1',
|
||||
description=" API",
|
||||
license=openapi.License(name="BSD License"),
|
||||
),
|
||||
public=True,
|
||||
generator_class=BothHttpAndHttpsSchemaGenerator,
|
||||
permission_classes=[permissions.AllowAny],
|
||||
)
|
@ -0,0 +1,10 @@
|
||||
from rest_framework.permissions import BasePermission, SAFE_METHODS
|
||||
|
||||
|
||||
class IsAuthorOrReadOnly(BasePermission):
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
if request.method in SAFE_METHODS:
|
||||
return True
|
||||
|
||||
return obj.user == request.user
|
@ -0,0 +1,31 @@
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
|
||||
class BaseRepository:
|
||||
model = None
|
||||
|
||||
@classmethod
|
||||
def get(cls, pk):
|
||||
try:
|
||||
return cls.model.objects.get(pk=pk)
|
||||
except ObjectDoesNotExist:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def create(cls, **kwargs):
|
||||
return cls.model.objects.create(**kwargs)
|
||||
|
||||
@classmethod
|
||||
def update(cls, instance, **kwargs):
|
||||
for key, value in kwargs.items():
|
||||
setattr(instance, key, value)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def delete(cls, instance):
|
||||
instance.delete()
|
||||
|
||||
@classmethod
|
||||
def all(cls):
|
||||
return cls.model.objects.all()
|
@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ "$DATABASE" = "postgres" ]
|
||||
then
|
||||
echo "Waiting for postgres..."
|
||||
|
||||
while ! nc -z $SQL_HOST $SQL_PORT; do
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
echo "PostgreSQL started"
|
||||
fi
|
||||
|
||||
exec "$@"
|
@ -0,0 +1,42 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
from .forms import UserCreationForm, UserChangeForm
|
||||
|
||||
from .models import CustomUser, Profile, RefreshToken
|
||||
|
||||
|
||||
class UserAdmin(BaseUserAdmin):
|
||||
# The forms to add and change user instances
|
||||
form = UserChangeForm
|
||||
add_form = UserCreationForm
|
||||
|
||||
# The fields to be used in displaying the User model.
|
||||
# These override the definitions on the base UserAdmin
|
||||
# that reference specific fields on custom_auth.User.
|
||||
list_display = ('email', 'is_staff')
|
||||
list_filter = ('is_staff',)
|
||||
fieldsets = (
|
||||
(None, {'fields': ('email', 'password', 'is_staff')}),
|
||||
)
|
||||
# add_fieldsets is not a standard ModelAdmin attribute. UserAdmin
|
||||
# overrides get_fieldsets to use this attribute when creating a user.
|
||||
add_fieldsets = (
|
||||
(None, {
|
||||
'classes': ('wide',),
|
||||
'fields': ('email', 'password1', 'password2', 'is_staff', 'is_superuser'),
|
||||
}),
|
||||
)
|
||||
search_fields = ('email', 'is_staff')
|
||||
ordering = ('email',)
|
||||
filter_horizontal = ()
|
||||
|
||||
|
||||
# Now register the new UserAdmin...
|
||||
admin.site.register(CustomUser, UserAdmin)
|
||||
# ... and, since we're not using Django's built-in permissions,
|
||||
# unregister the Group model from admin.
|
||||
admin.site.unregister(Group)
|
||||
|
||||
admin.site.register(RefreshToken)
|
||||
admin.site.register(Profile)
|
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AuthConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'jwtauth'
|
@ -0,0 +1,48 @@
|
||||
import datetime
|
||||
|
||||
from django.conf import settings
|
||||
import jwt
|
||||
from rest_framework import authentication, exceptions
|
||||
|
||||
from .models import CustomUser
|
||||
|
||||
|
||||
class JWTAuthentication(authentication.BaseAuthentication):
|
||||
authentication_header_prefix = 'Token'
|
||||
|
||||
def authenticate(self, request):
|
||||
request.user = None
|
||||
|
||||
auth_header = authentication.get_authorization_header(request).split()
|
||||
auth_header_prefix = self.authentication_header_prefix.lower()
|
||||
|
||||
if not auth_header:
|
||||
return
|
||||
|
||||
if len(auth_header) == 1:
|
||||
return
|
||||
|
||||
elif len(auth_header) > 2:
|
||||
return
|
||||
|
||||
prefix = auth_header[0].decode('utf-8')
|
||||
token = auth_header[1].decode('utf-8')
|
||||
|
||||
if prefix.lower() != auth_header_prefix:
|
||||
return
|
||||
|
||||
return self.authenticate_credentials(token)
|
||||
|
||||
def authenticate_credentials(self, token):
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256'])
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
try:
|
||||
user = CustomUser.objects.get(pk=payload['id'])
|
||||
except CustomUser.DoesNotExist:
|
||||
msg = 'Пользователь соответствующий данному токену не найден.'
|
||||
raise exceptions.AuthenticationFailed(msg)
|
||||
|
||||
return user, token
|
@ -0,0 +1,50 @@
|
||||
from django import forms
|
||||
from django.forms.models import ModelForm
|
||||
from django.contrib.auth.forms import ReadOnlyPasswordHashField
|
||||
from django.core.exceptions import ValidationError
|
||||
from.models import CustomUser
|
||||
|
||||
|
||||
class UserCreationForm(ModelForm):
|
||||
"""A form for creating new users. Includes all the required
|
||||
fields, plus a repeated password."""
|
||||
password1 = forms.CharField(label='Пароль', widget=forms.PasswordInput)
|
||||
password2 = forms.CharField(label='Подтверждение пароля', widget=forms.PasswordInput)
|
||||
|
||||
class Meta:
|
||||
model = CustomUser
|
||||
fields = ('email', 'is_staff', 'is_superuser')
|
||||
|
||||
def clean_password2(self):
|
||||
# Check that the two password entries match
|
||||
password1 = self.cleaned_data.get("password1")
|
||||
password2 = self.cleaned_data.get("password2")
|
||||
if password1 and password2 and password1 != password2:
|
||||
raise ValidationError("Пароли не совпадают")
|
||||
return password2
|
||||
|
||||
def save(self, commit=True):
|
||||
# Save the provided password in hashed format
|
||||
user = super().save(commit=False)
|
||||
user.set_password(self.cleaned_data["password1"])
|
||||
if commit:
|
||||
user.save()
|
||||
return user
|
||||
|
||||
|
||||
class UserChangeForm(ModelForm):
|
||||
"""A form for updating users. Includes all the fields on
|
||||
the user, but replaces the password field with admin's
|
||||
password hash display field.
|
||||
"""
|
||||
password = ReadOnlyPasswordHashField()
|
||||
|
||||
class Meta:
|
||||
model = CustomUser
|
||||
fields = ('email', 'password', 'is_staff')
|
||||
|
||||
def clean_password(self):
|
||||
# Regardless of what the user provides, return the initial value.
|
||||
# This is done here, rather than on the field, because the
|
||||
# field does not have access to the initial value
|
||||
return self.initial["password"]
|
@ -0,0 +1,28 @@
|
||||
from django.contrib.auth.base_user import BaseUserManager
|
||||
|
||||
|
||||
class CustomUserManager(BaseUserManager):
|
||||
use_in_migrations = True
|
||||
|
||||
def create_user(self, email, password=None):
|
||||
|
||||
if not email:
|
||||
raise ValueError(_('The email must be set'))
|
||||
email = self.normalize_email(email)
|
||||
user = self.model(
|
||||
email=email,
|
||||
)
|
||||
user.set_password(password)
|
||||
user.save(using=self._db)
|
||||
return user
|
||||
|
||||
def create_superuser(self, email, password=None):
|
||||
|
||||
user = self.create_user(
|
||||
email,
|
||||
password=password
|
||||
)
|
||||
user.is_staff = True
|
||||
user.is_superuser = True
|
||||
user.save(using=self._db)
|
||||
return user
|
@ -0,0 +1,65 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.base_user import AbstractBaseUser
|
||||
from django.contrib.auth.models import PermissionsMixin
|
||||
from django.db import models
|
||||
from pytz import timezone
|
||||
|
||||
from .managers import CustomUserManager
|
||||
from .token_generators import generate_jwt
|
||||
|
||||
|
||||
# Create your models here.
|
||||
|
||||
|
||||
class CustomUser(AbstractBaseUser, PermissionsMixin):
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'User'
|
||||
verbose_name_plural = 'Users'
|
||||
|
||||
email = models.EmailField(verbose_name='email', unique=True)
|
||||
is_staff = models.BooleanField(default=False, verbose_name='Administrator')
|
||||
|
||||
USERNAME_FIELD = 'email'
|
||||
|
||||
objects = CustomUserManager()
|
||||
|
||||
def __str__(self):
|
||||
return self.email
|
||||
|
||||
@property
|
||||
def access_token(self):
|
||||
return generate_jwt(self.pk)
|
||||
|
||||
|
||||
class RefreshToken(models.Model):
|
||||
token = models.CharField(max_length=255)
|
||||
expires = models.DateTimeField(default=datetime.now)
|
||||
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
|
||||
used = models.BooleanField(default=False)
|
||||
|
||||
def save(
|
||||
self, force_insert=False, force_update=False, using=None, update_fields=None
|
||||
):
|
||||
self.expires = datetime.now(timezone(settings.TIME_ZONE)) + timedelta(**settings.REFRESH_TOKEN_LIFETIME)
|
||||
super().save(force_insert, force_update, using, update_fields)
|
||||
|
||||
def __str__(self):
|
||||
return self.token
|
||||
|
||||
|
||||
class Profile(models.Model):
|
||||
user = models.OneToOneField(CustomUser, on_delete=models.CASCADE)
|
||||
first_name = models.CharField(max_length=32, default='', blank=True)
|
||||
last_name = models.CharField(max_length=32, default='', blank=True)
|
||||
title = models.CharField(max_length=32, default='', blank=True)
|
||||
avatar = models.ImageField(upload_to='avatar/', null=True, blank=True, default=None)
|
||||
friends = models.ManyToManyField('self', blank=True, through='Friend')
|
||||
|
||||
|
||||
class Friend(models.Model):
|
||||
first = models.ForeignKey(Profile, on_delete=models.CASCADE)
|
||||
second = models.ForeignKey(Profile, on_delete=models.CASCADE)
|
||||
approved = models.BooleanField(default=False)
|
@ -0,0 +1,24 @@
|
||||
import json
|
||||
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
|
||||
|
||||
class CustomUserJSONRenderer(JSONRenderer):
|
||||
charset = 'utf-8'
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
try:
|
||||
errors = data.get('errors', None)
|
||||
access_token = data.get('access_token', None)
|
||||
|
||||
if errors is not None:
|
||||
return super(CustomUserJSONRenderer, self).render(data)
|
||||
|
||||
if access_token is not None and isinstance(access_token, bytes):
|
||||
data['access_token'] = access_token.decode('utf-8')
|
||||
|
||||
return json.dumps(
|
||||
data
|
||||
)
|
||||
except:
|
||||
return None
|
@ -0,0 +1,39 @@
|
||||
from core.repositories import BaseRepository, ObjectDoesNotExist
|
||||
from .models import CustomUser, Profile, RefreshToken
|
||||
|
||||
|
||||
class UserRepository(BaseRepository):
|
||||
model = CustomUser
|
||||
|
||||
@classmethod
|
||||
def get(cls, email):
|
||||
try:
|
||||
return cls.model.objects.get(email=email)
|
||||
except ObjectDoesNotExist:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def update(cls, instance, **kwargs):
|
||||
password = kwargs.pop('password', None)
|
||||
for key, value in kwargs.items():
|
||||
setattr(instance, key, value)
|
||||
if password:
|
||||
instance.set_password(password)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
class ProfileRepository(BaseRepository):
|
||||
model = Profile
|
||||
|
||||
@classmethod
|
||||
def get_by_user(cls, user):
|
||||
return cls.model.objects.get(user=user)
|
||||
|
||||
|
||||
class RefreshTokenRepository(BaseRepository):
|
||||
model = RefreshToken
|
||||
|
||||
@classmethod
|
||||
def get(cls, token):
|
||||
return cls.model.objects.get(token=token)
|
@ -0,0 +1,118 @@
|
||||
from django.contrib.auth import authenticate
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import CustomUser, Profile, RefreshToken
|
||||
from .repositories import UserRepository, ProfileRepository, RefreshTokenRepository
|
||||
from .token_generators import generate_rt
|
||||
|
||||
|
||||
class RegistrationSerializer(serializers.ModelSerializer):
|
||||
password = serializers.CharField(
|
||||
max_length=128,
|
||||
min_length=8,
|
||||
write_only=True
|
||||
)
|
||||
|
||||
access_token = serializers.CharField(max_length=255, read_only=True)
|
||||
refresh_token = serializers.CharField(max_length=255, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = CustomUser
|
||||
fields = ['email', 'password', 'access_token', 'refresh_token']
|
||||
|
||||
def create(self, validated_data):
|
||||
user = UserRepository.create(**validated_data)
|
||||
token = RefreshTokenRepository.create(
|
||||
token=generate_rt(),
|
||||
user=user
|
||||
)
|
||||
return {
|
||||
'username': user.username,
|
||||
'access_token': user.access_token,
|
||||
'refresh_token': token.token
|
||||
}
|
||||
|
||||
|
||||
class LoginSerializer(serializers.Serializer):
|
||||
email = serializers.CharField(max_length=255)
|
||||
password = serializers.CharField(max_length=128, write_only=True)
|
||||
access_token = serializers.CharField(max_length=255, read_only=True)
|
||||
refresh_token = serializers.StringRelatedField(read_only=True)
|
||||
|
||||
def validate(self, attrs):
|
||||
|
||||
email = attrs.get('email', None)
|
||||
password = attrs.get('password', None)
|
||||
|
||||
if email is None:
|
||||
raise serializers.ValidationError(
|
||||
str(attrs)
|
||||
)
|
||||
|
||||
if password is None:
|
||||
raise serializers.ValidationError(
|
||||
'A password is required to log in'
|
||||
)
|
||||
|
||||
user = authenticate(username=email, password=password)
|
||||
|
||||
if user is None:
|
||||
raise serializers.ValidationError(
|
||||
'A user with this username and password was not found'
|
||||
)
|
||||
|
||||
token = RefreshTokenRepository.create(
|
||||
token=generate_rt(),
|
||||
user=user
|
||||
)
|
||||
|
||||
return {
|
||||
"email": email,
|
||||
'access_token': user.access_token,
|
||||
'refresh_token': token.token
|
||||
}
|
||||
|
||||
|
||||
class CustomUserSerializer(serializers.ModelSerializer):
|
||||
|
||||
password = serializers.CharField(
|
||||
max_length=128,
|
||||
min_length=8,
|
||||
write_only=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CustomUser
|
||||
fields = ('email', 'is_staff', 'password')
|
||||
read_only_fields = ('is_staff',)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
|
||||
password = validated_data.pop('password', None)
|
||||
|
||||
for key, value in validated_data.items():
|
||||
setattr(instance, key, value)
|
||||
|
||||
if password is not None:
|
||||
instance.set_password(password)
|
||||
|
||||
instance.save()
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class ProfileSerializer(serializers.ModelSerializer):
|
||||
user = serializers.StringRelatedField()
|
||||
friends = serializers.SerializerMethodField(read_only=True)
|
||||
subscribers = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def get_friends(self, obj):
|
||||
return obj.friends.filter(approved=True).count()
|
||||
|
||||
def get_subscribers(self, obj):
|
||||
return obj.friends.filter(approved=False).count()
|
||||
|
||||
class Meta:
|
||||
model = Profile
|
||||
fields = ('user', 'first_name', 'last_name', 'title', 'avatar', 'friends', 'subscribers')
|
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
@ -0,0 +1,23 @@
|
||||
import datetime, random, string
|
||||
import pytz
|
||||
|
||||
from django.conf import settings
|
||||
import jwt
|
||||
|
||||
|
||||
def generate_jwt(pk):
|
||||
dt = datetime.datetime.now(tz=pytz.timezone('Europe/Moscow')) + datetime.timedelta(
|
||||
seconds=settings.TOKEN_LIFETIME['seconds'],
|
||||
minutes=settings.TOKEN_LIFETIME['minutes'],
|
||||
hours=settings.TOKEN_LIFETIME['hours'],
|
||||
days=settings.TOKEN_LIFETIME['days']
|
||||
)
|
||||
token = jwt.api_jwt.encode({
|
||||
'id': pk,
|
||||
'exp': dt,
|
||||
}, settings.SECRET_KEY, algorithm='HS256')
|
||||
return token
|
||||
|
||||
|
||||
def generate_rt():
|
||||
return ''.join([random.choice(string.ascii_letters) for i in range(128)])
|
@ -0,0 +1,16 @@
|
||||
from django.urls import path
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from .views import RegistrationAPIView, LoginAPIView, CustomUserRetrieveUpdateAPIView, RefreshAPIView, ProfileRetrieveUpdateDestroyAPIView, ProfileRetrieveAPIView, ProfileListAPIView
|
||||
|
||||
app_name = 'jwtauth'
|
||||
|
||||
urlpatterns = [
|
||||
path('registration/', RegistrationAPIView.as_view()),
|
||||
path('login/', LoginAPIView.as_view()),
|
||||
path('refresh/', RefreshAPIView.as_view()),
|
||||
path('profile/<int:id>', ProfileRetrieveAPIView.as_view()),
|
||||
path('profiles/', ProfileListAPIView.as_view()),
|
||||
path('profile/', ProfileRetrieveUpdateDestroyAPIView.as_view()),
|
||||
path('', CustomUserRetrieveUpdateAPIView.as_view()),
|
||||
]
|
@ -0,0 +1,217 @@
|
||||
from datetime import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django.shortcuts import get_object_or_404
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from drf_yasg import openapi
|
||||
from pytz import timezone
|
||||
from rest_framework import status
|
||||
from rest_framework.generics import RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView, RetrieveAPIView, CreateAPIView, ListAPIView
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated, IsAdminUser
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from core.permissions import IsAuthorOrReadOnly
|
||||
from .models import CustomUser, RefreshToken
|
||||
from .repositories import RefreshTokenRepository, ProfileRepository
|
||||
from .serializers import RegistrationSerializer, LoginSerializer, CustomUserSerializer, ProfileSerializer
|
||||
from .renderers import CustomUserJSONRenderer
|
||||
from .token_generators import generate_rt, generate_jwt
|
||||
|
||||
|
||||
# Create your views here.
|
||||
|
||||
|
||||
class RefreshAPIView(APIView):
|
||||
"""
|
||||
Refreshing old access token
|
||||
"""
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
@swagger_auto_schema(request_body=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
'refresh_token': openapi.Schema(type=openapi.TYPE_STRING, description='refresh_token'),
|
||||
},
|
||||
required=['refresh_token']
|
||||
),
|
||||
responses={
|
||||
status.HTTP_200_OK: openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
'access_token': openapi.Schema(type=openapi.TYPE_STRING),
|
||||
'refresh_token': openapi.Schema(type=openapi.TYPE_STRING)
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
def post(self, request):
|
||||
try:
|
||||
refresh_token = request.data.get('refresh_token')
|
||||
old_token = RefreshTokenRepository.get(token=refresh_token)
|
||||
user = old_token.user
|
||||
except RefreshToken.DoesNotExist:
|
||||
return Response({
|
||||
'error': 'Token is not valid'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if not old_token.used and old_token.expires > datetime.now(tz=timezone(settings.TIME_ZONE)):
|
||||
token = RefreshToken.objects.create(
|
||||
token=generate_rt(),
|
||||
user=user
|
||||
)
|
||||
old_token.delete()
|
||||
data = {
|
||||
'access_token': generate_jwt(user.pk),
|
||||
'refresh_token': token.token
|
||||
}
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response(
|
||||
data={
|
||||
'error': 'Token expired',
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
class RegistrationAPIView(APIView):
|
||||
permission_classes = (AllowAny,)
|
||||
serializer_class = RegistrationSerializer
|
||||
renderer_classes = (CustomUserJSONRenderer,)
|
||||
|
||||
@swagger_auto_schema(request_body=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
'email': openapi.Schema(type=openapi.TYPE_STRING, description='email'),
|
||||
'password': openapi.Schema(type=openapi.TYPE_STRING, description='user password')
|
||||
}
|
||||
),
|
||||
responses={
|
||||
status.HTTP_200_OK: openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
'email': openapi.Schema(type=openapi.TYPE_STRING),
|
||||
'access_token': openapi.Schema(type=openapi.TYPE_STRING),
|
||||
'refresh_token': openapi.Schema(type=openapi.TYPE_STRING)
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
def post(self, request):
|
||||
user = request.data
|
||||
serializer = self.serializer_class(data=user)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class LoginAPIView(APIView):
|
||||
permission_classes = (AllowAny,)
|
||||
serializer_class = LoginSerializer
|
||||
renderer_classes = (CustomUserJSONRenderer,)
|
||||
|
||||
@swagger_auto_schema(request_body=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
'email': openapi.Schema(type=openapi.TYPE_STRING, description='email'),
|
||||
'password': openapi.Schema(type=openapi.TYPE_STRING, description='user password')
|
||||
},
|
||||
),
|
||||
responses={
|
||||
status.HTTP_200_OK: openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
'email': openapi.Schema(type=openapi.TYPE_STRING),
|
||||
'access_token': openapi.Schema(type=openapi.TYPE_STRING),
|
||||
'refresh_token': openapi.Schema(type=openapi.TYPE_STRING)
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
def post(self, request):
|
||||
user = request.data
|
||||
|
||||
serializer = self.serializer_class(data=user)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class CustomUserRetrieveUpdateAPIView(RetrieveUpdateAPIView):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
renderer_classes = (CustomUserJSONRenderer,)
|
||||
serializer_class = CustomUserSerializer
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
serializer = self.serializer_class(request.user)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
serializer_data = request.data
|
||||
serializer = self.serializer_class(
|
||||
request.user, data=serializer_data, partial=True
|
||||
)
|
||||
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
# class ChangePasswordAPIView(APIView):
|
||||
#
|
||||
# permission_classes = (IsAuthenticated,)
|
||||
#
|
||||
# @swagger_auto_schema(request_body=openapi.Schema(
|
||||
# type=openapi.TYPE_OBJECT,
|
||||
# properties={
|
||||
# 'new_password': openapi.Schema(type=openapi.TYPE_STRING, description='refresh_token'),
|
||||
# },
|
||||
# required=['new_password']
|
||||
# ),
|
||||
# responses={
|
||||
# status.HTTP_200_OK: openapi.Schema(
|
||||
# type=openapi.TYPE_OBJECT,
|
||||
# properties={
|
||||
# 'message': 'password changed',
|
||||
# }
|
||||
# ),
|
||||
# status.HTTP_401_UN
|
||||
# }
|
||||
# )
|
||||
# def post(self, request):
|
||||
# pass
|
||||
|
||||
|
||||
class ProfileRetrieveUpdateDestroyAPIView(CreateAPIView, RetrieveUpdateDestroyAPIView):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
serializer_class = ProfileSerializer
|
||||
queryset = ProfileRepository.all()
|
||||
|
||||
def get_queryset(self):
|
||||
return self.queryset
|
||||
|
||||
def get_object(self):
|
||||
queryset = self.get_queryset()
|
||||
obj = get_object_or_404(queryset, user=self.request.user)
|
||||
return obj
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(user=self.request.user)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(user=self.request.user)
|
||||
|
||||
|
||||
class ProfileRetrieveAPIView(RetrieveAPIView):
|
||||
permission_classes = (IsAuthorOrReadOnly,)
|
||||
serializer_class = ProfileSerializer
|
||||
queryset = ProfileRepository.all()
|
||||
|
||||
|
||||
class ProfileListAPIView(ListAPIView):
|
||||
queryset = ProfileRepository.all()
|
||||
serializer_class = ProfileSerializer
|
||||
permission_classes = [IsAdminUser]
|
||||
pagination_class = PageNumberPagination
|
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'conf.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()
|
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class OrganizationConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'organizations'
|
@ -0,0 +1,34 @@
|
||||
from django.db import models
|
||||
from django.contrib.gis.db.models import PointField
|
||||
|
||||
# Create your models here.
|
||||
|
||||
|
||||
class Region(models.Model):
|
||||
code = models.PositiveIntegerField()
|
||||
name = models.CharField(max_length=64)
|
||||
|
||||
|
||||
class Location(models.Model):
|
||||
coords = PointField(db_index=True)
|
||||
region = models.ForeignKey(Region, on_delete=models.SET_NULL, null=True)
|
||||
address = models.CharField(max_length=512, blank=True, null=True)
|
||||
|
||||
|
||||
class Category(models.Model):
|
||||
name = models.CharField(max_length=64)
|
||||
icon = models.ImageField(upload_to='icons/category/')
|
||||
|
||||
|
||||
class Organization(models.Model):
|
||||
location = models.ForeignKey(Location, on_delete=models.SET_NULL, null=True)
|
||||
name = models.CharField(max_length=256)
|
||||
phone = models.CharField(max_length=20)
|
||||
website = models.URLField()
|
||||
description = models.TextField()
|
||||
owner = models.ForeignKey('jwtauth.CustomUser', on_delete=models.SET_NULL, null=True)
|
||||
|
||||
|
||||
class OrganizationImage(models.Model):
|
||||
image = models.ImageField(upload_to='images/organizations')
|
||||
organization = models.ForeignKey(Organization, on_delete=models.CASCADE, related_name='organization_images')
|
@ -0,0 +1,30 @@
|
||||
from .models import Category, Location, Organization, OrganizationImage, Region
|
||||
from core.repositories import BaseRepository
|
||||
|
||||
|
||||
class CategoryRepository(BaseRepository):
|
||||
model = Category
|
||||
|
||||
|
||||
class LocationRepository(BaseRepository):
|
||||
model = Location
|
||||
|
||||
|
||||
class OrganizationRepository(BaseRepository):
|
||||
model = Organization
|
||||
|
||||
@classmethod
|
||||
def search(cls, name):
|
||||
return cls.model.objects.filter(name__icontains=name)
|
||||
|
||||
@classmethod
|
||||
def get_by_category(cls, category_id):
|
||||
return cls.model.objects.filter(category__pk=category_id)
|
||||
|
||||
|
||||
class OrganizationImagesRepository(BaseRepository):
|
||||
model = OrganizationImage
|
||||
|
||||
|
||||
class RegionRepository(BaseRepository):
|
||||
model = Region
|
@ -0,0 +1,41 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from core.fields import LocationField
|
||||
from jwtauth.serializers import CustomUserSerializer
|
||||
from .models import Category, Location, Organization, OrganizationImage
|
||||
from .repositories import OrganizationImagesRepository
|
||||
|
||||
|
||||
class CategorySerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Category
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class LocationSerializer(serializers.ModelSerializer):
|
||||
coords = LocationField()
|
||||
region = serializers.StringRelatedField()
|
||||
|
||||
class Meta:
|
||||
model = Location
|
||||
fields = ('coords', 'region', 'address')
|
||||
|
||||
|
||||
class OrganizationListSerializer(serializers.ModelSerializer):
|
||||
location = LocationSerializer()
|
||||
images = serializers.StringRelatedField(many=True)
|
||||
|
||||
class Meta:
|
||||
model = Organization
|
||||
fields = ('location', 'images', 'name', 'phone', 'description', 'website')
|
||||
|
||||
|
||||
class OrganizationCreateSerializer(serializers.ModelSerializer):
|
||||
location = LocationSerializer()
|
||||
images = serializers.PrimaryKeyRelatedField(many=True, queryset=OrganizationImagesRepository)
|
||||
owner = serializers.StringRelatedField()
|
||||
|
||||
class Meta:
|
||||
model = Organization
|
||||
fields = ('location', 'images', 'name', 'phone', 'website', 'description', 'owner')
|
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
@ -0,0 +1,13 @@
|
||||
from django.urls import path
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from .views import OrganizationViewSet, OrganizationSearchListAPIView, OrganizationByCategoryListAPIView
|
||||
|
||||
app_name = "organizations"
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register('', OrganizationViewSet, 'organizations')
|
||||
urlpatterns = [
|
||||
path('category/<int:id>/organizations/', OrganizationByCategoryListAPIView.as_view()),
|
||||
path('search/<str:name>/', OrganizationSearchListAPIView.as_view()),
|
||||
] + router.urls
|
@ -0,0 +1,79 @@
|
||||
from django.conf import settings
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_page
|
||||
from django.views.decorators.vary import vary_on_cookie
|
||||
from rest_framework.generics import ListAPIView
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from core.permissions import IsAuthorOrReadOnly
|
||||
from .repositories import CategoryRepository, OrganizationRepository
|
||||
from .serializers import CategorySerializer, OrganizationListSerializer, OrganizationCreateSerializer
|
||||
|
||||
|
||||
# Create your views here.
|
||||
|
||||
|
||||
class OrganizationViewSet(ModelViewSet):
|
||||
pagination_class = PageNumberPagination
|
||||
permission_classes = [IsAuthorOrReadOnly]
|
||||
|
||||
@method_decorator(vary_on_cookie)
|
||||
@method_decorator(cache_page(settings.CACHE_TTL))
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
return super(OrganizationViewSet, self).dispatch(*args, **kwargs)
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'list' or self.action == 'retrieve':
|
||||
return OrganizationListSerializer
|
||||
else:
|
||||
return OrganizationCreateSerializer
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(owner=self.request.user)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(owner=self.request.user)
|
||||
|
||||
|
||||
class OrganizationSearchListAPIView(ListAPIView):
|
||||
pagination_class = PageNumberPagination
|
||||
serializer_class = OrganizationListSerializer
|
||||
|
||||
@method_decorator(vary_on_cookie)
|
||||
@method_decorator(cache_page(settings.CACHE_TTL))
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
return super(OrganizationSearchListAPIView, self).dispatch(*args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
name = self.kwargs.get('name')
|
||||
if name:
|
||||
return OrganizationRepository.search(name)
|
||||
else:
|
||||
return OrganizationRepository.all()
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
return super().list(request, args, kwargs)
|
||||
|
||||
|
||||
class CategoryListAPIView(ListAPIView):
|
||||
pagination_class = PageNumberPagination
|
||||
queryset = CategoryRepository.all()
|
||||
serializer_class = CategorySerializer
|
||||
|
||||
|
||||
class OrganizationByCategoryListAPIView(ListAPIView):
|
||||
pagination_class = PageNumberPagination
|
||||
serializer_class = OrganizationListSerializer
|
||||
|
||||
@method_decorator(vary_on_cookie)
|
||||
@method_decorator(cache_page(settings.CACHE_TTL))
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
return super(OrganizationByCategoryListAPIView, self).dispatch(*args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
category = self.kwargs.get('id')
|
||||
if category:
|
||||
return OrganizationRepository.get_by_category(int(category))
|
||||
else:
|
||||
return OrganizationRepository.all()
|
Binary file not shown.
Loading…
Reference in new issue