From 2119c30a0a12875453a8c70f0941ec10721e65c3 Mon Sep 17 00:00:00 2001 From: adm Date: Thu, 22 Feb 2024 16:15:54 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D1=82=D0=B0=D1=80=D1=82=D0=B0=D0=B2?= =?UTF-8?q?=D0=B0=D1=8F=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D1=8C=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=B4=D0=B0=D1=87=D0=BD=D0=B8=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/asgi.py | 4 + config/local_settings.py_orig | 48 +++++++ config/logging.py | 49 +++++++ config/settings.py | 184 ++++++++++++++++++++++++ config/urls.py | 35 +++++ config/wsgi.py | 4 + manage.py | 19 +++ requirements.txt | 31 +++++ system/accounts/admin.py | 62 +++++++++ system/accounts/apps.py | 7 + system/accounts/models.py | 68 +++++++++ system/accounts/serializers.py | 40 ++++++ system/accounts/tests.py | 2 + system/accounts/urls.py | 18 +++ system/accounts/views.py | 135 ++++++++++++++++++ system/main/admin.py | 10 ++ system/main/apps.py | 6 + system/main/models.py | 2 + system/main/tests.py | 2 + system/main/urls.py | 0 system/main/views.py | 8 ++ todos/admin.py | 2 + todos/apps.py | 7 + todos/models.py | 111 +++++++++++++++ todos/serializers.py | 105 ++++++++++++++ todos/tests.py | 2 + todos/urls.py | 19 +++ todos/views.py | 247 +++++++++++++++++++++++++++++++++ 28 files changed, 1227 insertions(+) create mode 100644 config/asgi.py create mode 100644 config/local_settings.py_orig create mode 100644 config/logging.py create mode 100644 config/settings.py create mode 100644 config/urls.py create mode 100644 config/wsgi.py create mode 100644 manage.py create mode 100644 requirements.txt create mode 100644 system/accounts/admin.py create mode 100644 system/accounts/apps.py create mode 100644 system/accounts/models.py create mode 100644 system/accounts/serializers.py create mode 100644 system/accounts/tests.py create mode 100644 system/accounts/urls.py create mode 100644 system/accounts/views.py create mode 100644 system/main/admin.py create mode 100644 system/main/apps.py create mode 100644 system/main/models.py create mode 100644 system/main/tests.py create mode 100644 system/main/urls.py create mode 100644 system/main/views.py create mode 100644 todos/admin.py create mode 100644 todos/apps.py create mode 100644 todos/models.py create mode 100644 todos/serializers.py create mode 100644 todos/tests.py create mode 100644 todos/urls.py create mode 100644 todos/views.py 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..ce4358b --- /dev/null +++ b/config/settings.py @@ -0,0 +1,184 @@ +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 = [ + 'todos.apps.TodosConfig', +] +# Сборщик всех приложений в один список +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' +# 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..47cc473 --- /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('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-redoc'), # API сборник адресов + 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'), # Верифекация токена + path(f'{api_prefix}/', include('todos.urls')), + path(f'{api_prefix}/todo/', TodoAPIList.as_view()), +] +# Обслуживание файлов, загруженных пользователем во время разработки +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 diff --git a/todos/admin.py b/todos/admin.py new file mode 100644 index 0000000..b213ae8 --- /dev/null +++ b/todos/admin.py @@ -0,0 +1,2 @@ +from django.contrib import admin +# \ No newline at end of file diff --git a/todos/apps.py b/todos/apps.py new file mode 100644 index 0000000..dafee7c --- /dev/null +++ b/todos/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig +# Настрока аппаса +class TodosConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'todos' + verbose_name = 'Ежедневник' + \ No newline at end of file diff --git a/todos/models.py b/todos/models.py new file mode 100644 index 0000000..2a43b2c --- /dev/null +++ b/todos/models.py @@ -0,0 +1,111 @@ +import markdown +from django.db import models +from django.conf import settings +from taggit.managers import TaggableManager +from django.contrib.auth import get_user_model +from django.contrib.auth.models import AnonymousUser +from django.utils.text import slugify +from django.urls import reverse + +User = get_user_model() + +TASK_STATUS = ( + ('N', 'Новая'), + ('D', 'Удалено'), + ('S', 'Ожидание'), + ('Y', 'Выполнена'), +) + +PRIORITY = ( + ('L', 'Низкий'), + ('W', 'Средний'), + ('H', 'Высокий'), +) + +class TodoQuerySet(models.QuerySet): + def with_favorites(self, user: AnonymousUser | User) -> models.QuerySet: + return self.annotate( + num_favorites=models.Count("favorites"), + # true if user is authenticated + is_favorite=models.Exists( + get_user_model().objects.filter( + pk=user.id, favorites=models.OuterRef("pk") + ), + ) + if user.is_authenticated + else models.Value(False, output_field=models.BooleanField()), + ) + + +TodoManager = models.Manager.from_queryset(TodoQuerySet) + + +class Todo(models.Model): + author = models.ForeignKey(settings.User, on_delete=models.CASCADE) + title = models.CharField(max_length=150, unique=True, verbose_name='Задача') + summary = models.TextField(blank=True, verbose_name='Краткое описание') + content = models.TextField(blank=True, verbose_name='Описание') + # + is_active = models.BooleanField(default=True, verbose_name='Активная') + is_deleted = models.BooleanField(default=False, verbose_name='Удаление') + priority = models.CharField(max_length=1, choices=PRIORITY, default='W', verbose_name='Приоритет') + status = models.CharField(max_length=1, choices=TASK_STATUS, default='N', verbose_name='Статус') + is_complete = models.BooleanField(default=False) + # + start_time = models.DateTimeField(verbose_name='Дата начала') + end_time = models.DateTimeField(verbose_name='Крайний срок') + created = models.DateTimeField(auto_now_add=True, verbose_name='Время создания') + updated = models.DateTimeField(auto_now_add=True, verbose_name='Время обновления') + # + substacles = models.ManyToManyField("Todo", blank=True, verbose_name='Подзадачи') + # + favorites = models.ManyToManyField( + settings.AUTH_USER_MODEL, blank=True, related_name="favorites", + verbose_name = 'Ответственный' + ) + suppliers = models.ManyToManyField( + settings.AUTH_USER_MODEL, blank=True, related_name="suppliers", + verbose_name='Постановщик' + ) + supporters = models.ManyToManyField( + settings.AUTH_USER_MODEL, blank=True, related_name="supporters", + verbose_name='Соисполнители' + ) + observers = models.ManyToManyField( + settings.AUTH_USER_MODEL, blank=True, related_name="observers", + verbose_name='Наблюдатели' + ) + + # + tags = TaggableManager(blank=True) + slug = models.SlugField(unique=True, max_length=255) + + objects = TodoManager() + + class Meta: + #unique_together = ["event", "user"] + verbose_name='Задача' + verbose_name_plural='Задачи' + + def __str__(self): + #return f"{self.author.first_name} - {self.title}" + return f"{self.title}" + + # @property + # def slug(self) -> str: + # return slugify(self.title) + def save(self, *args, **kwargs): + self.slug = slugify(self.title) + super().save(*args, **kwargs) + + def get_absolute_url(self) -> str: + return reverse( + "todo_detail", + kwargs={ + "todo_id": self.id, + "slug": self.slug, + } + ) + + def as_markdown(self) -> str: + return markdown.markdown(self.content, safe_mode="escape", extensions=["extra"]) \ No newline at end of file diff --git a/todos/serializers.py b/todos/serializers.py new file mode 100644 index 0000000..baed0cf --- /dev/null +++ b/todos/serializers.py @@ -0,0 +1,105 @@ +from rest_framework import serializers +from django.contrib.auth import get_user_model +from django.contrib.auth import get_user_model +from taggit.models import Tag +from taggit.serializers import (TagListSerializerField, + TaggitSerializer) + +from todos.models import Todo + +User = get_user_model() + + +class AuthorSerializer(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 + +class TodosSerializer(serializers.ModelSerializer): + slug = serializers.SlugField(required=False) + #titles = serializers.CharField(source='title') + + class Meta: + model = Todo + fields = "__all__" + +class TodoSerializer(TaggitSerializer, serializers.ModelSerializer): + slug = serializers.SlugField(required=False) + description = serializers.CharField(source='summary') + body = serializers.CharField(source='content') + tagList = TagListSerializerField(source='tags', required=False) + createdAt = serializers.DateTimeField(source='created', format='%Y-%m-%dT%H:%M:%S.%fZ', required=False) + updatedAt = serializers.DateTimeField(source='updated', format='%Y-%m-%dT%H:%M:%S.%fZ', required=False) + startTd = serializers.DateTimeField(source='start_time', format='%Y-%m-%d %H:%M', required=False) + endTd = serializers.DateTimeField(source='end_time', format='%Y-%m-%d %H:%M', required=False) + + favorited = serializers.SerializerMethodField() + favoritesCount = serializers.SerializerMethodField() + #supplierd = serializers.SerializerMethodField() + #suppliersCount = serializers.SerializerMethodField() + #supporterd = serializers.SerializerMethodField() + #supportersCount = serializers.SerializerMethodField() + #observerd = serializers.SerializerMethodField() + #observersCount = serializers.SerializerMethodField() + author = serializers.SerializerMethodField(read_only=True) + + class Meta: + model = Todo + fields = ['id', 'slug', 'title', 'description', 'body', 'tagList', + 'is_active', 'is_deleted', 'priority', 'status', 'is_complete', + 'startTd', 'endTd', 'createdAt', 'updatedAt', + #'favorited', 'favoritesCount', 'supplierd', 'suppliersCount', 'supporterd', 'supportersCount', 'observerd', 'observersCount', + 'favorited', 'favoritesCount', + 'author'] + read_only_fields = ['slug', 'createdAt', 'updatedAt', 'author'] + + def get_author(self, obj): + request = self.context.get('request') + # serializer = AuthorSerializer(request.user, context={'request': request}) + serializer = AuthorSerializer(obj.author, context={'request': request}) + return serializer.data + + def get_favorited(self, instance): + request = self.context.get('request') + if request and request.user.is_authenticated: + return instance.favorites.filter(pk=request.user.pk).exists() + return False + + def get_favoritesCount(self, instance): + return instance.favorites.count() + + def create(self, validated_data): + tags = validated_data.pop('tags') + todo = Todo( + author=self.context['request'].user, + **validated_data + ) + todo.save() + todo.tags.add(*tags) + return todo + + def update(self, instance, validated_data): + tags = validated_data.pop('tags') + for key, value in validated_data.items(): + setattr(instance, key, value) + instance.save() + + instance.tags.clear() + instance.tags.add(*tags) + + return instance + + +class TagSerializer(serializers.Serializer): + tags = serializers.ListField( + child=serializers.CharField() + ) + \ No newline at end of file diff --git a/todos/tests.py b/todos/tests.py new file mode 100644 index 0000000..43cac8b --- /dev/null +++ b/todos/tests.py @@ -0,0 +1,2 @@ +from django.test import TestCase +# Создайте первуый тест \ No newline at end of file diff --git a/todos/urls.py b/todos/urls.py new file mode 100644 index 0000000..8ea491a --- /dev/null +++ b/todos/urls.py @@ -0,0 +1,19 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter + +#from rest_framework.urlpatterns import format_suffix_patterns + +from todos import views + +todo_router = DefaultRouter(trailing_slash=False) +todo_router.register('todos', views.TodoView, basename='todos') +todo_router.register('tags', views.TagView) + +urlpatterns = [ + path('', include(todo_router.urls)), + path('todo/json/', views.TodoListView), + #path('todo/', views.TodoDetailView) + path('todo/', views.TodoDetailView) +] + +#urlpatterns = format_suffix_patterns(urlpatterns) \ No newline at end of file diff --git a/todos/views.py b/todos/views.py new file mode 100644 index 0000000..144fa28 --- /dev/null +++ b/todos/views.py @@ -0,0 +1,247 @@ + +from rest_framework import viewsets, status, mixins, generics +from rest_framework.decorators import action +from rest_framework.authentication import SessionAuthentication, BasicAuthentication +from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly +from rest_framework.response import Response +from rest_framework.decorators import api_view +from taggit.models import Tag +from django.http import JsonResponse +from rest_framework.pagination import PageNumberPagination + +from system.accounts.models import User +from todos.models import Todo +from todos.serializers import TodoSerializer, TagSerializer, TodosSerializer +from todos.filters import TodoFilter + +@api_view(['GET', 'POST']) +#@authentication_classes([SessionAuthentication, BasicAuthentication]) +#@permission_classes([IsAuthenticated]) +def TodoListView(request, format=None): + + if request.method == 'GET': + todos = Todo.objects.all() + serializer = TodosSerializer(todos, many=True) + return JsonResponse({'todos': serializer.data}) + + if request.method == 'POST': + serializer = TodosSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + +@api_view(['GET', 'PUT', 'DELETE']) +def TodoDetailView(request, id, format=None): + + try: + todo = Todo.objects.get(pk=id) + except Todo.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + + if request.method == 'GET': + serializer = TodosSerializer(todo)#, many=False) + return Response(serializer.data) + + elif request.method == 'PUT': + serializer = TodosSerializer(todo, data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + elif request.method == 'DELETE': + todo.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + +class TodoAPIListPagination(PageNumberPagination): + page_size = 3 + page_size_query_param = 'page_size' + max_page_size = 2 + +class TodoAPIList(generics.ListCreateAPIView): + queryset = Todo.objects.all() + serializer_class = TodosSerializer + #authentication_classes = [SessionAuthentication, BasicAuthentication] + permission_classes = (IsAuthenticatedOrReadOnly, IsAuthenticated) + pagination_class = TodoAPIListPagination + + +class TodoView(viewsets.ModelViewSet): + queryset = Todo.objects.all() + serializer_class = TodoSerializer + #authentication_classes = [SessionAuthentication, BasicAuthentication] + permission_classes = [IsAuthenticated] + lookup_field = 'slug' + filterset_class = TodoFilter + http_method_names = ['get', 'post', 'put', 'delete'] + + def get_permissions(self): + if self.action == 'retrieve' or self.action == 'list': + return [IsAuthenticatedOrReadOnly()] + + return super().get_permissions() + + def create(self, request, *args, **kwargs): + try: + todo_data = request.data.get('todo') + serializer = self.get_serializer(data=todo_data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + return Response({"todo": serializer.data}, status=status.HTTP_201_CREATED) + + except Exception: + return Response({"errors": { + "body": [ + "Bad Request" + ] + }}, status=status.HTTP_404_NOT_FOUND) + + @action(detail=True, methods=['post', 'delete']) + def favorite(self, request, slug, *args, **kwargs): + if request.method == 'POST': + try: + todo = Todo.objects.get(slug=slug) + + if todo.favorites.filter(id=request.user.id).exists(): + return Response({"errors": { + "body": [ + "Already Favourited Todo" + ] + }}) + + todo.favorites.add(request.user) + serializer = self.get_serializer(todo) + return Response({"todo": serializer.data}) + + except Exception: + return Response({"errors": { + "body": [ + "Bad Request" + ] + }}, status=status.HTTP_404_NOT_FOUND) + else: + try: + + todo = Todo.objects.get(slug=slug) + if todo.favorites.get(id=request.user.id): + todo.favorites.remove(request.user.id) + serializer = self.get_serializer(todo) + return Response({"todo": serializer.data}) + + else: + raise Exception + + except Exception: + return Response({"errors": { + "body": [ + "Bad Request" + ] + }}, status=status.HTTP_404_NOT_FOUND) + + @action(detail=False) + def feed(self, request, *args, **kwargs): + try: + followed_authors = User.objects.filter(followers=request.user) + queryset = self.get_queryset() + todos = queryset.filter( + author__in=followed_authors).order_by('-created') + queryset = self.filter_queryset(todos) + + serializer = self.get_serializer(queryset, many=True) + response = { + 'comments': serializer.data, + 'todoCount': len(serializer.data) + } + return Response(response) + + except Exception: + return Response({"errors": { + "body": [ + "Bad Request" + ] + }}, status=status.HTTP_404_NOT_FOUND) + + def retrieve(self, request, slug, *args, **kwargs): + try: + queryset = self.get_queryset() + todo = queryset.get(slug=slug) + serializer = self.get_serializer(todo) + + return Response({"todo": serializer.data}) + + except Exception: + return Response({"errors": { + "body": [ + "Bad Request" + ] + }}, status=status.HTTP_404_NOT_FOUND) + + def update(self, request, slug, *args, **kwargs): + + try: + queryset = self.get_queryset() + todo = queryset.get(slug=slug) + + if request.user != todo.author: + return Response({"errors": { + "body": [ + "UnAuthorized Action" + ] + }}, status=status.HTTP_401_UNAUTHORIZED) + + request_data = request.data.get('todo') + serializer = self.get_serializer(todo, data=request_data) + serializer.is_valid(raise_exception=True) + self.perform_update(serializer) + + return Response({"todo": serializer.data}) + + except Exception: + return Response({"errors": { + "body": [ + "Bad Request" + ] + }}, status=status.HTTP_404_NOT_FOUND) + + def destroy(self, request, slug, *args, **kwargs): + try: + queryset = self.get_queryset() + todo = queryset.get(slug=slug) + + if request.user != todo.author: + return Response({"errors": { + "body": [ + "UnAuthorized Action" + ] + }}, status=status.HTTP_401_UNAUTHORIZED) + + todo.delete() + return Response(status=status.HTTP_200_OK) + + except Exception: + return Response({"errors": { + "body": [ + "Bad Request" + ] + }}, status=status.HTTP_404_NOT_FOUND) + + +class TagView(viewsets.GenericViewSet, mixins.ListModelMixin): + queryset = Tag.objects.all() + serializer_class = TagSerializer + http_method_names = ['get', ] + + def list(self, request, *args, **kwargs): + try: + queryset = self.get_queryset() + tags = [element.name for element in queryset] + serializer = self.get_serializer({'tags': tags}) + return Response(serializer.data) + + except Exception: + return Response({"errors": { + "body": [ + "Bad Request" + ] + }}, status=status.HTTP_404_NOT_FOUND) \ No newline at end of file