090500
This commit is contained in:
3
.idea/.gitignore
generated
vendored
Normal file
3
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="PyInterpreterInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
10
.idea/misc.xml
generated
Normal file
10
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Black">
|
||||||
|
<option name="sdkName" value="Python 3.10 (vikileo-shop)" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (vikileo-shop)" project-jdk-type="Python SDK" />
|
||||||
|
<component name="PyCharmProfessionalAdvertiser">
|
||||||
|
<option name="shown" value="true" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/vikileo-shop.iml" filepath="$PROJECT_DIR$/.idea/vikileo-shop.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/vikileo-shop.iml
generated
Normal file
8
.idea/vikileo-shop.iml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
0
config/__init__.py
Normal file
0
config/__init__.py
Normal file
0
config/accounts/__init__.py
Normal file
0
config/accounts/__init__.py
Normal file
3
config/accounts/admin.py
Normal file
3
config/accounts/admin.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
6
config/accounts/apps.py
Normal file
6
config/accounts/apps.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AccountsConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'accounts'
|
||||||
0
config/accounts/management/__init__.py
Normal file
0
config/accounts/management/__init__.py
Normal file
0
config/accounts/management/commands/__init__.py
Normal file
0
config/accounts/management/commands/__init__.py
Normal file
26
config/accounts/management/commands/createUser.py
Normal file
26
config/accounts/management/commands/createUser.py
Normal file
@@ -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.'))
|
||||||
0
config/accounts/migrations/__init__.py
Normal file
0
config/accounts/migrations/__init__.py
Normal file
80
config/accounts/models.py
Normal file
80
config/accounts/models.py
Normal file
@@ -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
|
||||||
41
config/accounts/serializers.py
Normal file
41
config/accounts/serializers.py
Normal file
@@ -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
|
||||||
155
config/accounts/tests.py
Normal file
155
config/accounts/tests.py
Normal file
@@ -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)
|
||||||
18
config/accounts/urls.py
Normal file
18
config/accounts/urls.py
Normal file
@@ -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))
|
||||||
|
]
|
||||||
161
config/accounts/views.py
Normal file
161
config/accounts/views.py
Normal file
@@ -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")
|
||||||
4
config/asgi.py
Normal file
4
config/asgi.py
Normal file
@@ -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()
|
||||||
71
config/local_settings.py
Normal file
71
config/local_settings.py
Normal file
@@ -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
|
||||||
49
config/logging.py
Normal file
49
config/logging.py
Normal file
@@ -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
|
||||||
170
config/settings.py
Normal file
170
config/settings.py
Normal file
@@ -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"
|
||||||
51
config/urls.py
Normal file
51
config/urls.py
Normal file
@@ -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)
|
||||||
4
config/wsgi.py
Normal file
4
config/wsgi.py
Normal file
@@ -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()
|
||||||
22
manage.py
Normal file
22
manage.py
Normal file
@@ -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()
|
||||||
Reference in New Issue
Block a user