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