diff --git a/README.md b/README.md index 53d4100..a4002e4 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,18 @@ # accounts +Пример авторизации и управление пользователями. -Пример авторизации и управление пользователями. \ No newline at end of file +Для локаольного развертывания требуется сделать следующие шаги. +Переименовать local_settings.py_orig в local_settings.py +Исправьте следующие строки + SECRET_KEY Укажите свой ключ безопасности + DEBUG Выберите включен или выключен дебаг + ALLOWED_HOSTS Укажите свои сетевые адреса + CORS_ORIGIN_WHITELIST Добавьте свой список + CSRF_TRUSTED_ORIGINS Добавьте свой список + DB_CONFIG_SQLL или DB_CONFIG_PSQL выбирете один из доступных способов хранения данных + DB_CONFIG_PSQL Укажите актуальные учётные данные + +В проекте в основу взять Django 4.2 Это стабильная ветка на данный момент, в версии 5.0 есть ряд изменений которые требую существенных изменений в проекте. + +pip install -r requirements.txt +python manage.py runserver 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_orig b/config/local_settings.py_orig new file mode 100644 index 0000000..02e67c3 --- /dev/null +++ b/config/local_settings.py_orig @@ -0,0 +1,48 @@ +import os +from pathlib import Path +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, "media") +LOGS_DIR = os.path.join(BASE_DIR, "logs") +SECRET_KEY = '***********************' +DEBUG = True +#DEBUG = False +ALLOWED_HOSTS = [] +# 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', '**********') +} + +CORS_ORIGIN_WHITELIST = [ + 'http://localhost:8000', + 'https://localhost:8000', + 'http://localhost:8001', + 'https://localhost:8001', + 'http://127.0.0.1:8000', + 'https://127.0.0.1:8000', + 'http://127.0.0.1:8001', + 'https://127.0.0.1:8001', + 'https://google.com', + 'http://google.com', +] +CSRF_TRUSTED_ORIGINS = [ + 'http://127.0.0.1:8001', + 'https://127.0.0.1:8001', + 'http://127.0.0.1:8000', + 'https://127.0.0.1:8000', +] \ 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..99b3f4e --- /dev/null +++ b/config/settings.py @@ -0,0 +1,178 @@ +import os +from pathlib import Path +from datetime import timedelta +# Вставка из файла конфика, нужно данный файл вынесити за пределы проекта в ENV часть. +from .local_settings import ( + SECRET_KEY, DEBUG, ALLOWED_HOSTS, DB_CONFIG, + TEMPLATES_DIR, STATICFILES_DIR, STATIC_DIR, MEDIA_DIR, LOGS_DIR, CORS_ORIGIN_WHITELIST, CSRF_TRUSTED_ORIGINS +) +from .logging import 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', +] +# Внетренние приложения +LOCAL_APPS = [ + 'system.accounts.apps.AccountsConfig', + 'system.main.apps.MainConfig', +] +# Сборщик всех приложений в один список +INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_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', +] +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 +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' +# Logging --------------------------------------------------------------------- +# https://docs.djangoproject.com/en/5.0/topics/logging/ +if os.getenv('DISABLE_LOGGING', False): # только для celery в jenkins ci + LOGGING_CONFIG = None +LOGGING = LOGGING +# REST API +REST_FRAMEWORK = { + 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], + #'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', + #'PAGE_SIZE': 2, + #ХЗ почему с этими параметрами не работает. Нужно более подробно изучить проблему. + 'DEFAULT_RENDERER_CLASSES': [ + 'rest_framework.renderers.JSONRenderer', + 'rest_framework.renderers.BrowsableAPIRenderer', + ], + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.AllowAny', + 'rest_framework.permissions.IsAuthenticated', + ], + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework_simplejwt.authentication.JWTAuthentication', + 'rest_framework.authentication.BasicAuthentication', + 'rest_framework.authentication.SessionAuthentication', + ] +} +""" +# 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',) +} +""" +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), + 'ROTATE_REFRESH_TOKENS': False, + 'BLACKLIST_AFTER_ROTATION': False, + 'UPDATE_LAST_LOGIN': False, + + 'ALGORITHM': 'HS256', + 'SIGNING_KEY': SECRET_KEY, + 'VERIFYING_KEY': None, + 'AUDIENCE': None, + 'ISSUER': None, + 'JWK_URL': None, + 'LEEWAY': 0, + + 'AUTH_HEADER_TYPES': ('JWT',), + 'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION', + 'USER_ID_FIELD': 'id', + 'USER_ID_CLAIM': 'user_id', + 'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule', + + 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), + 'TOKEN_TYPE_CLAIM': 'token_type', + + 'JTI_CLAIM': 'jti', + + 'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp', + 'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5), + 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1), +} +# https://whitenoise.readthedocs.io/en/stable/ +# Радикально упрощенное обслуживание статических файлов для веб-приложений Python. +STORAGES = { + 'staticfiles': { + 'BACKEND': 'whitenoise.storage.CompressedManifestStaticFilesStorage', + }, +} +# приложение для обработки заголовков сервера, необходимых для совместного использования ресурсов между источниками (CORS) +CORS_ORIGIN_ALLOW_ALL=True +CORS_ORIGIN_WHITELIST = CORS_ORIGIN_WHITELIST +CSRF_TRUSTED_ORIGINS = CSRF_TRUSTED_ORIGINS +# При выходе из учётной записи вас направит на данный 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..16240ff --- /dev/null +++ b/config/urls.py @@ -0,0 +1,35 @@ +from django.contrib import admin +from django.urls import path, include +from django.conf import settings +from django.conf.urls.static import static +from drf_yasg import openapi +from drf_yasg.views import get_schema_view +from rest_framework import permissions +from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView, TokenVerifyView +# Машиночитаемая [схема] описывает, какие ресурсы доступны через API +schema_view = get_schema_view( + openapi.Info( + title="??? API", + default_version='v1', + description="??? API Documentation", + ), + public=True, + permission_classes=(permissions.AllowAny,), +) +# Префикс для понтов, можно без него +api_prefix = 'api' +# Кортеж адресов +urlpatterns = [ + path('admin/', admin.site.urls),# Адрес админ панели + path('', include('system.main.urls')), # Адрес главной странице (заглушка) + path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-redoc'), # API сборник адресов + path(f'{api_prefix}/', include('system.accounts.urls')), # Адрес управление учётными данными + path(f'{api_prefix}/api-auth/', 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'), # Верифекация токена +] +# Обслуживание файлов, загруженных пользователем во время разработки +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..4ba2659 --- /dev/null +++ b/manage.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +import os +import sys + +def main(): + 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() + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..49878ca --- /dev/null +++ b/requirements.txt @@ -0,0 +1,31 @@ +asgiref==3.7.2 +certifi==2023.11.17 +charset-normalizer==3.3.2 +coreapi==2.3.3 +coreschema==0.0.4 +Django==4.2.7 +django-cors-headers==4.3.1 +django-filter==23.5 +django-taggit==5.0.1 +djangorestframework==3.14.0 +djangorestframework-simplejwt==5.3.1 +drf-yasg==1.21.7 +idna==3.6 +inflection==0.5.1 +itypes==1.2.0 +Jinja2==3.1.3 +Markdown==3.5.2 +MarkupSafe==2.1.4 +packaging==23.2 +pillow==10.2.0 +psycopg2==2.9.9 +PyJWT==2.8.0 +pytz==2023.3.post1 +PyYAML==6.0.1 +requests==2.31.0 +sqlparse==0.4.4 +typing_extensions==4.9.0 +tzdata==2023.4 +uritemplate==4.1.1 +urllib3==2.1.0 +whitenoise==6.6.0 \ No newline at end of file diff --git a/system/accounts/admin.py b/system/accounts/admin.py new file mode 100644 index 0000000..fa565aa --- /dev/null +++ b/system/accounts/admin.py @@ -0,0 +1,62 @@ +# Системные приложения +import logging +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin +from django.contrib.admin.models import LogEntry, DELETION +from django.utils.html import escape +from django.utils.safestring import mark_safe +from django.urls import reverse +# Внутриние приложения +from system.accounts.models import User + +logger = logging.getLogger(__name__) + +class ProfileInline(admin.StackedInline): + model = User + can_delete = False + max_num = 1 + verbose_name = 'Profile' + verbose_name_plural = 'Profile' + fk_name = 'user' + +@admin.register(User) +class UserAdmin(UserAdmin): + ordering = ('email', ) + list_display = ( + 'id', 'email', 'is_staff', 'is_superuser', 'is_active', 'date_joined', + 'last_updated', 'last_login' + #'verified_email', 'accepted_terms', 'read_terms', + ) + search_fields = ('email',) + list_filter = ( + 'is_staff', 'is_superuser', 'is_active', + #'verified_email' + ) + readonly_fields = ( + 'last_login', 'last_updated', 'date_joined' + #'email_token', + ) + list_display_links = ('id', 'email') + fieldsets = ( + (None, {'fields': ('email', 'password')}), + ('Permissions', {'fields': ('groups', 'user_permissions')}), + ('Roles', {'fields': ('is_staff', 'is_superuser', 'is_active')}), + #('Additional', {'fields': ( + #'verified_email', 'email_token', 'accepted_terms', 'read_terms' + #)}), + ('Profile', {'fields': ( + 'first_name','last_name', 'gender', 'bio', 'image', + 'address', 'followers' + )}), + ('Dates', {'fields': ('last_login', 'last_updated', 'date_joined')}) + ) + add_fieldsets = ( + (None, { + 'classes': ('wide', ), + 'fields': ( + 'email', 'password1', 'password2' + ) + }), + ) + + #inlines = (ProfileInline, ) \ No newline at end of file diff --git a/system/accounts/apps.py b/system/accounts/apps.py new file mode 100644 index 0000000..9393881 --- /dev/null +++ b/system/accounts/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + +class AccountsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'system.accounts' + verbose_name = 'Управление пользователями' + \ No newline at end of file diff --git a/system/accounts/models.py b/system/accounts/models.py new file mode 100644 index 0000000..3b1f707 --- /dev/null +++ b/system/accounts/models.py @@ -0,0 +1,68 @@ +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', 'Женщина'), + ) + first_name: str = models.CharField(max_length=60, null=True, blank=True, verbose_name='Имя') + last_name: str = models.CharField(max_length=60, null=True, blank=True, verbose_name='Фамилия') + gender: str = models.CharField(verbose_name='Пол', max_length=10, choices=GENDER_CHOICES, default='n') + email: str = models.EmailField(verbose_name='Email Address', unique=True) + username: str = models.CharField(max_length=60, verbose_name='Ник') + bio: str = models.TextField(blank=True, verbose_name='О себе') + image: str | None = models.URLField(null=True, blank=True, verbose_name='Аватар') + + followers = models.ManyToManyField("self", blank=True, symmetrical=False, verbose_name='Подписчики') + + address: str = models.CharField(verbose_name='Адрес', max_length=255, blank=True, null=True) + #avatar = models.ImageField(upload_to=avatar_upload_path, null=True, blank=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) + + EMAIL_FIELD = "email" + USERNAME_FIELD = "email" + REQUIRED_FIELDS: list[str] = [] + + 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 \ No newline at end of file diff --git a/system/accounts/serializers.py b/system/accounts/serializers.py new file mode 100644 index 0000000..2584681 --- /dev/null +++ b/system/accounts/serializers.py @@ -0,0 +1,40 @@ +from rest_framework import serializers +from system.accounts.models import User + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ('username', 'email', 'first_name', 'last_name', 'password', 'bio', 'image') + 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', 'first_name', 'last_name', '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/system/accounts/tests.py b/system/accounts/tests.py new file mode 100644 index 0000000..43cac8b --- /dev/null +++ b/system/accounts/tests.py @@ -0,0 +1,2 @@ +from django.test import TestCase +# Создайте первуый тест \ No newline at end of file diff --git a/system/accounts/urls.py b/system/accounts/urls.py new file mode 100644 index 0000000..264c49d --- /dev/null +++ b/system/accounts/urls.py @@ -0,0 +1,18 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from django.contrib.auth.views import LogoutView +from django.contrib.auth import views +from django.urls import path +from system.accounts import views + +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('signout/', views.signout, name='signout'), + path('logout/', views.signout, name='logout'), + path('', include(profile_router.urls)) +] \ No newline at end of file diff --git a/system/accounts/views.py b/system/accounts/views.py new file mode 100644 index 0000000..5748e77 --- /dev/null +++ b/system/accounts/views.py @@ -0,0 +1,135 @@ +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 django.shortcuts import redirect +from django.contrib.auth import logout +from rest_framework_simplejwt.tokens import RefreshToken +from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly + +from system.accounts.models import User +from system.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', ]) +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) + + +class UserView(views.APIView): + permission_classes = [IsAuthenticated] + + def get(self, request, format=None): + user = self.request.user + serializer = UserSerializer(user) + return Response(serializer.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/system/main/admin.py b/system/main/admin.py new file mode 100644 index 0000000..15add8c --- /dev/null +++ b/system/main/admin.py @@ -0,0 +1,10 @@ +from django.apps import apps +from django.contrib import admin +# +models = apps.get_models() +# код что отображает все модели всех apps +for model in models: + try: + admin.site.register(model) + except admin.sites.AlreadyRegistered: + pass \ No newline at end of file diff --git a/system/main/apps.py b/system/main/apps.py new file mode 100644 index 0000000..eb47c5d --- /dev/null +++ b/system/main/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + +class MainConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'system.main' + verbose_name = 'Главное приложение' \ No newline at end of file diff --git a/system/main/models.py b/system/main/models.py new file mode 100644 index 0000000..451b642 --- /dev/null +++ b/system/main/models.py @@ -0,0 +1,2 @@ +from django.db import models +# Создайте первую модель \ No newline at end of file diff --git a/system/main/tests.py b/system/main/tests.py new file mode 100644 index 0000000..43cac8b --- /dev/null +++ b/system/main/tests.py @@ -0,0 +1,2 @@ +from django.test import TestCase +# Создайте первуый тест \ No newline at end of file diff --git a/system/main/urls.py b/system/main/urls.py new file mode 100644 index 0000000..e69de29 diff --git a/system/main/views.py b/system/main/views.py new file mode 100644 index 0000000..60611ae --- /dev/null +++ b/system/main/views.py @@ -0,0 +1,8 @@ +from django.shortcuts import render +from django.views.generic import View +# Простое отображение страницы заглушки. +class MainView(View): + template_name = 'index.html' + + def get(self, request): + return render(request, self.template_name) \ No newline at end of file