diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..ac21435 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..3386033 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..0dfa639 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vikileo-shop.iml b/.idea/vikileo-shop.iml new file mode 100644 index 0000000..d0876a7 --- /dev/null +++ b/.idea/vikileo-shop.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/accounts/__init__.py b/config/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/accounts/admin.py b/config/accounts/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/config/accounts/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/config/accounts/apps.py b/config/accounts/apps.py new file mode 100644 index 0000000..3e3c765 --- /dev/null +++ b/config/accounts/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'accounts' diff --git a/config/accounts/management/__init__.py b/config/accounts/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/accounts/management/commands/__init__.py b/config/accounts/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/accounts/management/commands/createUser.py b/config/accounts/management/commands/createUser.py new file mode 100644 index 0000000..f622a33 --- /dev/null +++ b/config/accounts/management/commands/createUser.py @@ -0,0 +1,26 @@ +from django.core.management.base import BaseCommand, CommandParser +from django.contrib.auth import get_user_model + + +class Command(BaseCommand): + help = 'Create Application User' + + def add_arguments(self, parser: CommandParser) -> None: + parser.add_argument('--email', type=str, help="User's email") + parser.add_argument('--name', type=str, help="User's name") + parser.add_argument('--password', type=str, help="User's password") + + def handle(self, *args, **options) -> None: + email: str = options['email'] + name: str = options['name'] + password: str = options['password'] + + User = get_user_model() + if email and name and password: + if not User.objects.filter(email=email).exists() and not User.objects.filter(name=name).exists(): + User.objects.create_user(email=email, password=password, name=name) + self.stdout.write(self.style.SUCCESS('Admin user created successfully.')) + else: + self.stdout.write(self.style.WARNING('Admin user already exists.')) + else: + self.stdout.write(self.style.ERROR('Please provide --email, --name, and --password arguments.')) \ No newline at end of file diff --git a/config/accounts/migrations/__init__.py b/config/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/accounts/models.py b/config/accounts/models.py new file mode 100644 index 0000000..637b299 --- /dev/null +++ b/config/accounts/models.py @@ -0,0 +1,80 @@ +from __future__ import annotations +from django.contrib.auth.models import AbstractUser, BaseUserManager +from django.db import models + + +# +# Управление учётными записями +class UserManager(BaseUserManager): + def create_user( + self, email: str, password: str | None = None, **other_fields + ) -> User: + user = User(email=email, **other_fields) + + if password: + user.set_password(password) + else: + user.set_unusable_password() + + user.save() + return user + + def create_superuser(self, email: str, password: str | None = None, **other_fields) -> User: + other_fields.setdefault("is_staff", True) + other_fields.setdefault("is_superuser", True) + other_fields.setdefault("is_active", True) + + if other_fields.get("is_staff") is not True: + raise ValueError("Superuser must be assigned to is_staff=True.") + if other_fields.get("is_superuser") is not True: + raise ValueError("Superuser must be assigned to is_superuser=True.") + + return self.create_user(email, password, **other_fields) + + +class User(AbstractUser): + GENDER_CHOICES = ( + ('n', 'Не указано'), + ('m', 'Мужчина'), + ('f', 'Женщина'), + ) + # remove default fields + # first_name = None + # last_name = None + + first_name = models.CharField(max_length=60, null=True, blank=True, verbose_name='Имя') + last_name = models.CharField(max_length=60, null=True, blank=True, verbose_name='Фамилия') + gender = models.CharField(verbose_name='Пол', max_length=10, choices=GENDER_CHOICES, default='n') + email = models.EmailField(verbose_name='Email Address', unique=True) + username = models.CharField(max_length=60, verbose_name='Ник') + bio = models.TextField(blank=True, verbose_name='О себе') + image = models.URLField(null=True, blank=True, verbose_name='Аватар') + # avatar = models.ImageField(upload_to='avatar_users/%Y/%m/%d/', null=True, blank=True) + + followers = models.ManyToManyField("self", blank=True, symmetrical=False, verbose_name='Подписчики') + + address = models.CharField(verbose_name='Адрес', max_length=255, blank=True, null=True) + # country = models.ForeignKey('chatroom.Country', on_delete=models.SET_NULL, null=True) + # state = models.ForeignKey('chatroom.State', on_delete=models.SET_NULL, null=True) + date_joined = models.DateTimeField(verbose_name='Дата регистрации', blank=True, null=True, auto_now_add=True) + last_updated = models.DateTimeField(verbose_name='Последнее обновление', blank=True, null=True, auto_now=True) + last_login = models.DateTimeField(verbose_name='Последняя авторизация', blank=True, null=True, auto_now=True) + status = models.BooleanField(default=False, verbose_name='Статус') + + EMAIL_FIELD = 'email' # Указываем поле для аутентификации по email + USERNAME_FIELD = 'email' # Указываем поле email как основной идентификатор пользователя + REQUIRED_FIELDS = [] # Поля, которые должны быть заполнены при создании пользователя + + objects = UserManager() + + def get_full_name(self) -> str: + if self.first_name and self.last_name: + return f"{self.first_name} {self.last_name}" + else: + return self.username + + def get_short_name(self) -> str: + if self.first_name and self.last_name: + return f"{self.first_name[0]}{self.last_name}" + else: + return self.username diff --git a/config/accounts/serializers.py b/config/accounts/serializers.py new file mode 100644 index 0000000..1f00918 --- /dev/null +++ b/config/accounts/serializers.py @@ -0,0 +1,41 @@ +from rest_framework import serializers +from config.accounts.models import User + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ('username', 'email', 'password', 'first_name', 'last_name', 'gender', 'bio', 'image', 'status') + extra_kwargs = {'password': {'write_only': True}} + + def create(self, validated_data): + password = validated_data.pop('password') + user = User( + **validated_data + ) + user.set_password(password) + user.save() + return user + + def update(self, instance, validated_data): + for key, value in validated_data.items(): + if key == 'password': + instance.set_password(value) + else: + setattr(instance, key, value) + instance.save() + return instance + + +class ProfileSerializer(serializers.ModelSerializer): + following = serializers.SerializerMethodField() + + class Meta: + model = User + fields = ('username', 'bio', 'image', 'following') + + def get_following(self, obj): + user = self.context.get('request').user + if user.is_authenticated: + return obj.followers.filter(pk=user.id).exists() + return False \ No newline at end of file diff --git a/config/accounts/tests.py b/config/accounts/tests.py new file mode 100644 index 0000000..ed483ab --- /dev/null +++ b/config/accounts/tests.py @@ -0,0 +1,155 @@ +from django.test import TestCase + +# Create your tests here. +from django.contrib.auth import get_user_model +from rest_framework.test import APITestCase +from django.urls import reverse +from rest_framework import status +from rest_framework_simplejwt.tokens import AccessToken + +# from config.accounts.models import User +User = get_user_model() + + +class AccountRegistrationTestCase(APITestCase): + def test_account_registration(self): + url = '/api/users' + user_data = { + 'user': { + 'email': 'test@example.com', + 'password': 'testpassword', + 'username': 'testuser', + } + } + + response = self.client.post(url, user_data, format='json') + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_account_registration_invalid_data(self): + url = '/api/users' + invalid_user_data = { + 'user': {} + } + + response = self.client.post(url, invalid_user_data, format='json') + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + +class AccountLoginTestCase(APITestCase): + def setUp(self): + self.email = 'test@example.com' + self.username = 'testuser' + self.password = 'testpassword' + self.user = User.objects.create_user( + email=self.email, + username=self.username, + password=self.password + ) + self.url = '/api/users/login' + + def tearDown(self): + self.user.delete + + def test_account_login(self): + user_data = { + 'user': { + 'email': self.email, + 'password': self.password, + } + } + + response = self.client.post(self.url, user_data, format='json') + + self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) + + def test_account_login_invalid_data(self): + invalid_user_data = { + 'user': {} + } + + response = self.client.post(self.url, invalid_user_data, format='json') + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + +class UserViewTestCase(APITestCase): + def setUp(self): + self.email = 'test@example.com' + self.username = 'testuser' + self.password = 'testpassword' + self.user = User.objects.create_user( + email=self.email, + username=self.username, + password=self.password + ) + self.access_token = str(AccessToken.for_user(self.user)) + self.client.credentials( + HTTP_AUTHORIZATION='Token ' + self.access_token + ) + self.url = reverse('user-account') + + def test_user_view_get(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_user_view_put(self): + updated_email = 'updated@example.com' + updated_bio = 'Updated bio' + updated_image = 'http://example.com/updated-image.jpg' + user_data = { + 'user': { + 'email': updated_email, + 'bio': updated_bio, + 'image': updated_image, + } + } + + response = self.client.put(self.url, user_data, format='json') + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class ProfileDetailViewTestCase(APITestCase): + def setUp(self): + self.user = User.objects.create_user( + email='test@example.com', + username='testuser', + password='testpassword' + ) + self.access_token = str(AccessToken.for_user(self.user)) + self.client.credentials( + HTTP_AUTHORIZATION='Token ' + self.access_token + ) + self.url = f'/api/profiles/{self.user.username}' + + def test_profile_detail_view_get(self): + response = self.client.get(self.url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_profile_detail_view_follow(self): + second_user = User.objects.create_user( + email='test2@gmail.com', + username='test2user', + password='password' + ) + follow_url = f'/api/profiles/{second_user.username}/follow' + + response = self.client.post(follow_url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_profile_detail_view_unfollow(self): + second_user = User.objects.create_user( + email='test2@gmail.com', + username='test2user', + password='password' + ) + second_user.followers.add(self.user) + unfollow_url = f'/api/profiles/{second_user.username}/follow' + + response = self.client.delete(unfollow_url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/config/accounts/urls.py b/config/accounts/urls.py new file mode 100644 index 0000000..4a8a8f1 --- /dev/null +++ b/config/accounts/urls.py @@ -0,0 +1,18 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter + +from config.accounts import views +from .views import ProfileDetailView + +profile_router = DefaultRouter(trailing_slash=False) +profile_router.register('profiles', views.ProfileDetailView) + + +urlpatterns = [ + path('users/login', views.account_login, name='account-login'), + path('users', views.account_registration, name="account-registration"), + path('user', views.UserView.as_view(), name='user-account'), + #path('users/profiles/', ProfileDetailView.as_view(), name='user-profiles'), + path('logout/', views.signout, name='logout'), + path('', include(profile_router.urls)) +] \ No newline at end of file diff --git a/config/accounts/views.py b/config/accounts/views.py new file mode 100644 index 0000000..a51c302 --- /dev/null +++ b/config/accounts/views.py @@ -0,0 +1,161 @@ +from rest_framework.decorators import api_view, action +from rest_framework.response import Response +from rest_framework import status, views, viewsets +from django.contrib.auth import authenticate +from rest_framework_simplejwt.tokens import RefreshToken +from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly +from django.views.decorators.csrf import csrf_exempt +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import AllowAny +from django.shortcuts import redirect +from django.contrib.auth import logout +from django.http import JsonResponse + +from config.accounts.models import User +from config.accounts.serializers import UserSerializer, ProfileSerializer + + +@api_view(['POST', ]) +def account_registration(request): + try: + user_data = request.data.get('user') + + serializer = UserSerializer(data=user_data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response({"user": serializer.data}, status=status.HTTP_201_CREATED) + + except Exception: + return Response(status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST', ]) +@permission_classes([AllowAny]) # Разрешение для всех источников +@csrf_exempt +def account_login(request): + try: + user_data = request.data.get('user') + user = authenticate(email=user_data['email'], password=user_data['password']) + serializer = UserSerializer(user) + jwt_token = RefreshToken.for_user(user) + serializer_data = serializer.data + serializer_data['token'] = str(jwt_token.access_token) + response_data = { + "user": serializer_data, + } + return Response(response_data, status=status.HTTP_202_ACCEPTED) + + # except Exception: + # return Response(status=status.HTTP_400_BAD_REQUEST) + + # Добавляем заголовок Access-Control-Allow-Origin + # ТЕСТ + # response = JsonResponse({"message": "Success!"}) + # response["Access-Control-Allow-Origin"] = "http://localhost:3000" # Разрешаем только локальный хост + # return response + + except Exception: + # return JsonResponse({"message": "Error!"}, status=400) + return Response(status=status.HTTP_400_BAD_REQUEST) + + +class UserView(views.APIView): + permission_classes = [IsAuthenticated] + + def get(self, request, format=None): + user = self.request.user + serializer = UserSerializer(user) + # Создание JWT-токена для пользователя + token = RefreshToken.for_user(user) + # Добавление ответа + response_data = { + "user": serializer.data, + "token": { + "access_token": str(token.access_token), + "refresh_token": str(token), + } + } + return Response(response_data, status=status.HTTP_200_OK) + + def put(self, request, format=None, pk=None): + user = self.request.user + user_data = request.data.get('user') + + user.email = user_data['email'] + user.bio = user_data['bio'] + user.image = user_data['image'] + user.save() + + serializer = UserSerializer(user) + + return Response(serializer.data, status=status.HTTP_200_OK) + + +class ProfileDetailView(viewsets.ModelViewSet): + queryset = User.objects.all() + serializer_class = ProfileSerializer + permission_classes = [IsAuthenticated] + lookup_field = 'username' + http_method_names = ['get', 'post', 'delete'] + + def get_permissions(self): + if self.action == 'list': + return [IsAuthenticatedOrReadOnly(), ] + return super().get_permissions() + + def list(self, request, username=None, *args, **kwargs): + try: + profile = User.objects.get(username=username) + serializer = self.get_serializer(profile) + return Response({"profile": serializer.data}) + + except Exception: + return Response({"errors": { + "body": [ + "Invalid User" + ] + }}) + + @action(detail=True, methods=['post', 'delete']) + def follow(self, request, username=None, *args, **kwargs): + if request.method == 'POST': + + profile = self.get_object() + follower = request.user + if profile == follower: + return Response({"errors": { + "body": [ + "Invalid follow Request" + ] + }}, status=status.HTTP_400_BAD_REQUEST) + + profile.followers.add(follower) + serializer = self.get_serializer(profile) + return Response({"profile": serializer.data}) + + elif request.method == 'DELETE': + + profile = self.get_object() + follower = request.user + if profile == follower: + return Response({"errors": { + "body": [ + "Invalid follow Request" + ] + }}, status=status.HTTP_400_BAD_REQUEST) + + if not profile.followers.filter(pk=follower.id).exists(): + return Response({"errors": { + "body": [ + "Invalid follow Request" + ] + }}, status=status.HTTP_400_BAD_REQUEST) + + profile.followers.remove(follower) + serializer = self.get_serializer(profile) + return Response({"profile": serializer.data}) + + +def signout(request): + logout(request) + return redirect("main") \ No newline at end of file diff --git a/config/asgi.py b/config/asgi.py new file mode 100644 index 0000000..4f703b7 --- /dev/null +++ b/config/asgi.py @@ -0,0 +1,4 @@ +import os +from django.core.asgi import get_asgi_application +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +application = get_asgi_application() \ No newline at end of file diff --git a/config/local_settings.py b/config/local_settings.py new file mode 100644 index 0000000..0179a8b --- /dev/null +++ b/config/local_settings.py @@ -0,0 +1,71 @@ +import os +from pathlib import Path +from datetime import timedelta +from django.core.management.utils import get_random_secret_key +BASE_DIR = Path(__file__).resolve().parent.parent +#BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +TEMPLATES_DIR = os.path.join(BASE_DIR, "templates") +STATICFILES_DIR = os.path.join(BASE_DIR, "staticfiles") +STATIC_DIR = os.path.join(BASE_DIR, "static") +MEDIA_DIR = os.path.join(BASE_DIR, "mediafiles") +LOGS_DIR = os.path.join(BASE_DIR, "logs") +SECRET_KEY = 'django-insecure-xvn9xig-%a4=4xt14#yo6on@g(l$tc!r^8i#ard1nio(4i_b+@' +DADATA_API_KEY = 'a6370792e9cfc9afdf5074c604eadf093ce9521f' +DEBUG = True +#DEBUG = False +ALLOWED_HOSTS = [] +# database +# For SQLlITE3 database +DB_CONFIG_SQLL = { + 'ENGINE': os.getenv('DB_ENGINE', 'django.db.backends.sqlite3'), + 'NAME': os.getenv('DB_NAME', 'db.sqlite3'), +} +# For PostgreSQL database (comment it if not needed) +DB_CONFIG_PSQL = { + 'ENGINE': os.getenv('DB_ENGINE', 'django.db.backends.postgresql'), + 'HOST': os.getenv('DB_HOST', '127.0.0.1'), + 'PORT': os.getenv('DB_PORT', 5432), + 'NAME': os.getenv('DB_NAME', '_db'), + 'USER': os.getenv('DB_USER', 'admdb'), + 'PASSWORD': os.getenv('DB_PASS', '**********') +} +DB_CONFIG_MYSQL = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'django_db', + 'USER': 'django_user', + 'PASSWORD': 'Ax123456', + 'HOST': '127.0.0.1', + 'PORT': '3306', + } +} +CORS_CSRF_WHITELIST = [ + 'http://localhost:3000', + 'https://localhost:3000', + 'http://localhost:8000', + 'https://localhost:8000', + 'http://localhost:8001', + 'https://localhost:8001', + 'http://localhost:2121', + 'https://localhost:2121', + 'http://localhost:5173', + 'https://localhost:5173', + 'http://127.0.0.1:8000', + 'https://127.0.0.1:8000', + 'http://127.0.0.1:3000', + 'https://127.0.0.1:3000', + 'http://127.0.0.1:8001', + 'https://127.0.0.1:8001', + 'http://127.0.0.1:8002', + 'https://127.0.0.1:8002', + 'http://127.0.0.1:2121', + 'https://127.0.0.1:2121', + 'http://127.0.0.1:5173', + 'https://127.0.0.1:5173', + 'https://google.com', + 'http://google.com', + 'http://fipi.pro', + 'https://fipi.pro', +] +CORS_ORIGIN_WHITELIST = CORS_CSRF_WHITELIST +CSRF_TRUSTED_ORIGINS = CORS_CSRF_WHITELIST \ No newline at end of file diff --git a/config/logging.py b/config/logging.py new file mode 100644 index 0000000..01607dc --- /dev/null +++ b/config/logging.py @@ -0,0 +1,49 @@ +import os +from config.local_settings import LOGS_DIR + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '{asctime} {levelname} {module} {process:d} {thread:d} ' + '{message}', + 'style': '{', + }, + 'simple': { + 'format': '{asctime} {levelname} {message}', + 'style': '{', + }, + }, # formatters + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'simple', + }, + 'file': { + 'level': 'DEBUG', + 'formatter': 'verbose', + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': os.path.join(LOGS_DIR, "debug.log"), + 'when': 'midnight', + 'backupCount': 30, + }, + }, # handlers + 'loggers': { + '': { # root logger + 'handlers': ['console', 'file'], + 'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO').upper(), + }, + 'customauth': { + 'handlers': ['console', 'file'], + 'level': os.getenv('DJANGO_LOG_LEVEL', 'DEBUG').upper(), + 'propagate': False, # required to eliminate duplication on root + }, + # 'app_name': { + # 'handlers': ['console', 'file'], + # 'level': os.getenv('DJANGO_LOG_LEVEL', 'DEBUG').upper(), + # 'propagate': False, # required to eliminate duplication on root + # }, + }, # loggers +} # logging \ No newline at end of file diff --git a/config/settings.py b/config/settings.py new file mode 100644 index 0000000..dd0ed10 --- /dev/null +++ b/config/settings.py @@ -0,0 +1,170 @@ +import os +#from pathlib import Path +from datetime import timedelta +# Вставка из файла конфика, нужно данный файл вынесити за пределы проекта в ENV часть. +from .local_settings import ( + SECRET_KEY, + DEBUG, + ALLOWED_HOSTS, + DB_CONFIG_SQLL, + DB_CONFIG_PSQL, + DB_CONFIG_MYSQL, + TEMPLATES_DIR, + STATICFILES_DIR, + STATIC_DIR, + MEDIA_DIR, + LOGS_DIR, + CORS_ORIGIN_WHITELIST, + CSRF_TRUSTED_ORIGINS +) +""" +# Logging --------------------------------------------------------------------- +from .logging import LOGGING +# https://docs.djangoproject.com/en/4.2/topics/logging/ +if os.getenv('DISABLE_LOGGING', False): # только для celery в jenkins ci +LOGGING_CONFIG = None +LOGGING = LOGGING +""" +# Списки сокращений +SETTINGS_DIR = os.path.dirname(os.path.abspath(__file__)) +BASE_DIR = os.path.dirname(SETTINGS_DIR) +TEMPLATES_DIR = os.getenv('TEMPLATES_DIR', TEMPLATES_DIR) +STATICFILES_DIR = os.getenv('STATICFILES_DIR', STATICFILES_DIR) +STATIC_DIR = os.getenv('STATIC_DIR', STATIC_DIR) +MEDIA_DIR = os.getenv('MEDIA_DIR', MEDIA_DIR) +LOGS_DIR = os.getenv('LOGS_DIR', LOGS_DIR) +SECRET_KEY = SECRET_KEY +DEBUG = DEBUG +ALLOWED_HOSTS = ALLOWED_HOSTS +# Определение приложения +# Встроенные в framework +DJANGO_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] +# Стронние приложения +THIRD_PARTY_APPS = [ + 'rest_framework', + 'rest_framework_simplejwt', + 'django_filters', + 'taggit', + 'drf_yasg', + 'corsheaders', + 'autoslug', + 'localflavor', + 'simple_history', +] +# Внутренние приложения +LOCAL_APPS = [ + 'config.accounts.apps.AccountsConfig', + #'system.media.apps.MediaConfig', + #'system.comments.apps.CommentsConfig', + #'main.apps.MainConfig', + #'articles.apps.ArticlesConfig', + #'todos.apps.TodosConfig', +] +# Сборщик всех приложений в один список +INSTALLED_APPS = LOCAL_APPS + THIRD_PARTY_APPS + DJANGO_APPS +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', + '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', + 'simple_history.middleware.HistoryRequestMiddleware', +] +ROOT_URLCONF = 'config.urls' +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [TEMPLATES_DIR, ], + '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 = 'config.wsgi.application' +# База данныех для изменеия используйте один теговое обозначение +# DB_CONFIG_SQLL = SQLlITE3 +# DB_CONFIG_PSQL = PostgreSQL +# DB_CONFIG_MYSQL = MySQL +DATABASES = { + 'default': os.getenv('DB_CONFIG_SQLL', DB_CONFIG_SQLL) +} +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', }, +] +LANGUAGE_CODE = 'ru-ru' +TIME_ZONE = 'Europe/Moscow' +USE_I18N = True +USE_TZ = True +# Static files (CSS, JavaScript, Images) +STATIC_URL = '/static/' +STATIC_ROOT = STATIC_DIR +STATICFILES_DIRS = [STATICFILES_DIR, ] +MEDIA_URL = '/media/' +MEDIA_ROOT = MEDIA_DIR +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +# Настройка аутентификации +AUTH_USER_MODEL = 'accounts.User' +EMAIL_FIELD = 'email' # Указываем поле для аутентификации по email +# REST API +REST_FRAMEWORK = { + 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework_simplejwt.authentication.JWTAuthentication', + 'rest_framework.authentication.BasicAuthentication', + ] +} +# JWT token settings +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(days=7), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), + 'ROTATE_REFRESH_TOKENS': True, + 'UPDATE_LAST_LOGIN': True, + 'AUTH_HEADER_TYPES': ('Token', 'JWT', 'Bearer') +} +# https://whitenoise.readthedocs.io/en/stable/ +# Радикально упрощенное обслуживание статических файлов для веб-приложений Python. +STORAGES = { + 'staticfiles': { + 'BACKEND': 'whitenoise.storage.CompressedManifestStaticFilesStorage', + }, + 'default': { + 'BACKEND': 'django.core.files.storage.FileSystemStorage', + 'OPTIONS': { + 'location': MEDIA_DIR, + }, + 'UPLOADCARE': { + 'PUB_KEY': 'your_uploadcare_public_key', + 'SECRET': 'your_uploadcare_secret_key', + }, + }, +} +# приложение для обработки заголовков сервера, +# необходимых для совместного использования ресурсов между источниками (CORS) +CORS_ORIGIN_ALLOW_ALL = True +CORS_ORIGIN_WHITELIST = CORS_ORIGIN_WHITELIST +CSRF_TRUSTED_ORIGINS = CSRF_TRUSTED_ORIGINS +# +CSRF_COOKIE_SECURE = True +CSRF_COOKIE_HTTPONLY = True +# При выходе из учётной записи вас направит на данный url +LOGOUT_REDIRECT_URL = "main" \ No newline at end of file diff --git a/config/urls.py b/config/urls.py new file mode 100644 index 0000000..2052ca9 --- /dev/null +++ b/config/urls.py @@ -0,0 +1,51 @@ +from django.contrib import admin +from django.urls import path, include +from django.conf import settings +from django.conf.urls.static import static +from rest_framework import permissions +from drf_yasg.views import get_schema_view +from drf_yasg import openapi +#from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView, TokenVerifyView +# Вставки из апсов приложения +# Машиночитаемая [схема] описывает, какие ресурсы доступны через API +schema_view = get_schema_view( + openapi.Info( + title="vikileo.shop API", + default_version='v1', + description="vikileo.shop API Documentation", + ), + public=True, + permission_classes=(permissions.AllowAny,), +) +# Префикс для понтов, можно без него но сним проще ориентироваться что должно иди на фронт, а что для тестов. +api_prefix = 'api' +# Кортежи адресов для системных приложений +urlpatterns_system = [ + path('admin/', admin.site.urls),# Адрес админ панели + path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-redoc'), # API сборник адресов +] +# Кортежи адресов для приложений +urlpatterns_apps = [ + path('', include('main.urls')), # Адрес главной странице (заглушка) + path(f'{api_prefix}/', include('config.accounts.urls')), # Адрес управление учётными данными + #path(f'{api_prefix}/', include('system.comments.urls')), # Адрес комментариев от пользователей + #path(f'{api_prefix}/', include('todos.urls')), # Адрес статей + #path(f'{api_prefix}/', include('articles.urls')), # Адрес стадей для пользователей +] +# Кортежи адресов для тестовых приложений +urlpatterns_test = [ + #path('/accounts/', include('system.accounts.urls')), # Адрес управление учётными данными + #path(f'{api_prefix}/token/', obtain_token, name='token_obtain_pair'), + #path(f'{api_prefix}/protected/', protected_view, name='protected_view'), + path('api-rest/', include('rest_framework.urls')), # API rest, допилить виюшку + #path(f'{api_prefix}/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), # Присвоение токена + #path(f'{api_prefix}/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), # Перевыдача токена + #path(f'{api_prefix}/token/verify/', TokenVerifyView.as_view(), name='token_verify'), # Верифекация токена + #path(f'{api_prefix}/', include('system.media.urls')), # Адрес файлаобменника +] +# Объединение кортежа адресов +urlpatterns = urlpatterns_system + urlpatterns_apps + urlpatterns_test +# Обслуживание файлов, загруженных пользователем во время разработки +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) \ No newline at end of file diff --git a/config/wsgi.py b/config/wsgi.py new file mode 100644 index 0000000..feac0f5 --- /dev/null +++ b/config/wsgi.py @@ -0,0 +1,4 @@ +import os +from django.core.wsgi import get_wsgi_application +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +application = get_wsgi_application() \ No newline at end of file diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..8e7ac79 --- /dev/null +++ b/manage.py @@ -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', 'config.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() diff --git a/req.pip b/req.pip new file mode 100644 index 0000000..0fd9860 Binary files /dev/null and b/req.pip differ