From ed213f56b64acfee94c4710706a5d654c5117891 Mon Sep 17 00:00:00 2001 From: Lymar Volodymyr Date: Sat, 29 Jun 2024 13:30:59 +0200 Subject: [PATCH 1/8] docs: update schema.json --- docs/schema.json | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/docs/schema.json b/docs/schema.json index b616a11..8fd9014 100644 --- a/docs/schema.json +++ b/docs/schema.json @@ -162,6 +162,8 @@ paths: /api/v1/doc/data/: post: operationId: v1_doc_data_create + description: Responsible for receiving data, validating it and storing it in + cache. tags: - v1 requestBody: @@ -185,9 +187,26 @@ paths: schema: $ref: '#/components/schemas/DataDoc' description: '' + /api/v1/doc/downloadfile/: + post: + operationId: v1_doc_downloadfile_create + description: |- + To download a file: + + - No parameters: get the last created file. + + - With the parameter {"file_name": "name your file"} - load a specific file from the list. + tags: + - v1 + security: + - jwtAuth: [] + responses: + '200': + description: No response body /api/v1/doc/filemake/: get: operationId: v1_doc_filemake_retrieve + description: Responsible for creating documents based on data from the cache. tags: - v1 security: @@ -1059,6 +1078,18 @@ paths: schema: $ref: '#/components/schemas/UserUpdatePassword' description: '' + /api/v1/users/list_user_files/: + get: + operationId: v1_users_list_user_files_retrieve + description: Responsible for displaying a list of documents of an authorized + user. + tags: + - v1 + security: + - jwtAuth: [] + responses: + '200': + description: No response body /api/v1/users/login/: post: operationId: v1_users_login_create @@ -1396,8 +1427,7 @@ components: type: object properties: delivery_type: - type: array - items: {} + type: string client_id: type: integer items: @@ -1566,7 +1596,9 @@ components: product_id: type: integer quantity: - type: integer + type: number + format: double + minimum: 0.001 discount: type: integer maximum: 100 From 43f612e7d3dd4b4572bb310e6c353e32e72c4c81 Mon Sep 17 00:00:00 2001 From: Lymar Volodymyr Date: Sat, 29 Jun 2024 13:52:28 +0200 Subject: [PATCH 2/8] docs: updated docstrings in users app --- users/managers.py | 35 ------------------- users/models.py | 29 +++++++++++----- users/serializers.py | 39 +++++++++++++++++++-- users/services.py | 9 ++++- users/urls.py | 4 +-- users/views.py | 80 +++++++++++++++++++++++++++++++++++++------- 6 files changed, 135 insertions(+), 61 deletions(-) delete mode 100644 users/managers.py diff --git a/users/managers.py b/users/managers.py deleted file mode 100644 index b1b15b5..0000000 --- a/users/managers.py +++ /dev/null @@ -1,35 +0,0 @@ -from django.contrib.auth.models import BaseUserManager -from django.db import IntegrityError, transaction -from django.utils.translation import gettext_lazy as _ - -from users.models import CustomUser - - -# Менеджер пользователей -class CustomUserManager(BaseUserManager[CustomUser]): - def create_user(self, email, full_name, password=None, **extra_fields): - """ - Создает и сохраняет пользователя с указанным email, ФИО и другими полями - """ - if not email: - raise ValueError(_("Email должен быть указан")) - try: - with transaction.atomic(): - email = self.normalize_email(email) - user = self.model(email=email, full_name=full_name, **extra_fields) - user.set_password(password) - user.save(using=self.db) - return user - except IntegrityError: - raise ValueError("Пользователь с таким email уже существует") - except (TypeError, ValueError): - raise ValueError("Неправильные данные для создания пользователя") - - def create_superuser(self, email, full_name, password=None, **extra_fields): - """ - Создает и сохраняет суперпользователя с указанным email, ФИО и другими полями - Проверяет действительно ли суперпользователь является им - """ - extra_fields.setdefault("is_staff", True) - extra_fields.setdefault("is_superuser", True) - return self.create_user(email, full_name, password=password, **extra_fields) diff --git a/users/models.py b/users/models.py index 8a09758..465d95d 100644 --- a/users/models.py +++ b/users/models.py @@ -4,9 +4,12 @@ from phonenumber_field.modelfields import PhoneNumberField -# Класс позиции пользователя class Position(models.Model): - position = models.CharField(max_length=30) + """ + A model for storing information about user positions. + """ + + position = models.CharField(max_length=30, verbose_name="Название позиции") class Meta: verbose_name = "Позиция" @@ -16,9 +19,12 @@ def __str__(self) -> str: return self.position -# Класс департамента пользователя class Department(models.Model): - department = models.CharField(max_length=50) + """ + A model for storing information about user departments. + """ + + department = models.CharField(max_length=50, verbose_name="Название департамента") class Meta: verbose_name = "Департамент" @@ -28,11 +34,14 @@ def __str__(self) -> str: return self.department -# Менеджер пользователей class CustomUserManager(BaseUserManager["CustomUser"]): + """ + Custom user manager implementing user and superuser creation. + """ + def create_user(self, email, full_name, password=None, **extra_fields): """ - Создает и сохраняет пользователя с указанным email, ФИО и другими полями + Creates and saves a regular user with the given email and full name. """ if not email: raise ValueError(_("Email должен быть указан")) @@ -50,16 +59,18 @@ def create_user(self, email, full_name, password=None, **extra_fields): def create_superuser(self, email, full_name, password=None, **extra_fields): """ - Создает и сохраняет суперпользователя с указанным email, ФИО и другими полями - Проверяет действительно ли суперпользователь является им + Creates and saves a superuser with the given email and full name. """ extra_fields.setdefault("is_staff", True) extra_fields.setdefault("is_superuser", True) return self.create_user(email, full_name, password=password, **extra_fields) -# Информация о пользователе приложения class CustomUser(AbstractBaseUser, PermissionsMixin): + """ + Custom user model using email as the unique identifier. + """ + email = models.EmailField(max_length=50, unique=True, db_index=True, verbose_name="E-mail") full_name = models.CharField(max_length=75, verbose_name="ФИО") position = models.ForeignKey( diff --git a/users/serializers.py b/users/serializers.py index 2412b58..1122817 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -4,8 +4,11 @@ from .models import CustomUser, Department, Position -# Сериализатор для создания новой записи class CustomUserSerializer(serializers.ModelSerializer[CustomUser]): + """ + Serializer for creating a new user record. + """ + password = serializers.CharField(write_only=True, required=True) password_confirm = serializers.CharField(write_only=True, required=True) @@ -24,6 +27,9 @@ class Meta: ] def validate(self, data): + """ + Validate password complexity and match between password and confirmation. + """ password = data.get("password") if len(password) < 8: raise serializers.ValidationError("Пароль должен содержать не менее 8 символов") @@ -36,6 +42,9 @@ def validate(self, data): return data def create(self, validated_data): + """ + Create a new user instance after validating data. + """ validated_data.pop("password_confirm") user = CustomUser.objects.create(**validated_data) user.set_password(validated_data["password"]) @@ -43,8 +52,11 @@ def create(self, validated_data): return user -# Сериализатор для редактирования существующей записи + изменения пароля class UserUpdateSerializer(serializers.ModelSerializer[CustomUser]): + """ + Serializer for updating existing user records. + """ + class Meta: model = CustomUser fields = [ @@ -59,6 +71,10 @@ class Meta: class UserUpdatePasswordSerializer(serializers.ModelSerializer[CustomUser]): + """ + Serializer for updating user password. + """ + old_password = serializers.CharField(write_only=True, required=True) new_password = serializers.CharField(write_only=True, required=True) new_password_confirm = serializers.CharField(write_only=True, required=True) @@ -72,6 +88,10 @@ class Meta: ] def validate(self, data): + """ + Validate old password, new password complexity, + and match between new password and confirmation. + """ # Проверяем, что старый пароль верный old_password = data.get("old_password") user = self.context["request"].user @@ -95,6 +115,9 @@ def validate(self, data): return super().validate(data) def update(self, instance, validated_data): + """ + Update user instance with new password if provided. + """ # Если предоставлены новые пароли, устанавливаем новый пароль new_password = validated_data.pop("new_password", None) if new_password is not None: @@ -104,16 +127,28 @@ def update(self, instance, validated_data): class LogoutSerializer(serializers.Serializer): # type: ignore + """ + Serializer for logging out a user. + """ + refresh_token = serializers.CharField() class DepartmentSerializer(serializers.ModelSerializer[Department]): + """ + Serializer for Department model. + """ + class Meta: model = Department fields = ["id", "department"] class PositionSerializer(serializers.ModelSerializer[Position]): + """ + Serializer for Position model. + """ + class Meta: model = Position fields = ["id", "position"] diff --git a/users/services.py b/users/services.py index 0d4d1c1..11225e0 100644 --- a/users/services.py +++ b/users/services.py @@ -3,9 +3,16 @@ from users.models import CustomUser -# Базовый класс get_object для действий с пользвателем class UserRelatedView(generics.RetrieveUpdateAPIView[CustomUser]): + """ + Base class for operations related to the current user. + Used for retrieving and updating data of the current authenticated user. + """ + permission_classes = [permissions.IsAuthenticated] def get_object(self): + """ + Retrieves the current authenticated user object. + """ return self.request.user diff --git a/users/urls.py b/users/urls.py index 7d645bc..292cd05 100644 --- a/users/urls.py +++ b/users/urls.py @@ -9,7 +9,7 @@ UserCreateView, UserUpdatePasswordView, UserUpdateView, - ListUserFilesAPIView + ListUserFilesAPIView, ) urlpatterns = [ @@ -18,7 +18,7 @@ path("logout/", LogoutView.as_view(), name="logout"), path("edit/", UserUpdateView.as_view(), name="edit"), path("edit_password/", UserUpdatePasswordView.as_view(), name="edit_password"), - path("listuserfiles/", ListUserFilesAPIView.as_view(), name="list_user_files"), + path("list_user_files/", ListUserFilesAPIView.as_view(), name="list_user_files"), path("departments/", DepartmentListView.as_view(), name="departments"), path("positions/", PositionListView.as_view(), name="positions"), path("registration/", UserCreateView.as_view(), name="registration"), diff --git a/users/views.py b/users/views.py index 43b65ee..4cc16c1 100644 --- a/users/views.py +++ b/users/views.py @@ -23,28 +23,55 @@ class LoginView(TokenObtainPairView): + """ + View to obtain JSON Web Token (JWT) by posting username and password. + """ + def post(self, request, *args, **kwargs): + """ + Handle POST requests to obtain JWT. + + Params: + - email: User's email address. + - password: User's password. + + Returns: + - JSON response with JWT or error message. + """ email = request.data.get("email") password = request.data.get("password") if email is None or password is None: return Response( - {"detail": "Пожалуйста, укажите email и пароль."}, + {"detail": "Please provide both email and password."}, status=status.HTTP_400_BAD_REQUEST, ) try: return super().post(request, *args, **kwargs) except Exception: return Response( - {"detail": "Неверные учетные данные."}, + {"detail": "Invalid credentials."}, status=status.HTTP_401_UNAUTHORIZED, ) class LogoutView(APIView): + """ + View to blacklist JWT refresh token and logout user. + """ + permission_classes = (IsAuthenticated,) serializer_class = LogoutSerializer def post(self, request): + """ + Handle POST requests to logout user. + + Params: + - refresh_token: JWT refresh token. + + Returns: + - HTTP 205 Reset Content on success, or HTTP 400 Bad Request on failure. + """ try: serializer = self.serializer_class(data=request.data) serializer.is_valid(raise_exception=True) @@ -56,29 +83,45 @@ def post(self, request): return Response(status=status.HTTP_400_BAD_REQUEST) -# Класс регистрации пользователя -class UserCreateView(generics.CreateAPIView[CustomUser]): +class UserCreateView(generics.CreateAPIView): + """ + View to handle user registration. + """ + queryset = CustomUser.objects.all() serializer_class = CustomUserSerializer -# Изменение данных пользователя class UserUpdateView(UserRelatedView): + """ + View to handle updating user data. + """ + serializer_class = UserUpdateSerializer -# Изменение пароля пользователя class UserUpdatePasswordView(UserRelatedView): + """ + View to handle changing user password. + """ + serializer_class = UserUpdatePasswordSerializer class ListUserFilesAPIView(APIView): """ - Responsible for displaying a list of documents of an authorized user. + View to list files of an authorized user. """ + permission_classes = (IsAuthenticated,) def get(self, request, *args, **kwargs): + """ + Handle GET requests to list user's files. + + Returns: + - List of user's files or error message. + """ user_folder = os.path.join("makedoc", "tempdoc", str(request.user.id)) if not os.path.exists(user_folder): @@ -98,19 +141,32 @@ def get(self, request, *args, **kwargs): return Response({"files": files_list}, status=status.HTTP_200_OK) -# Сброс пароля @receiver(reset_password_token_created) def password_reset_token_created(sender, instance, reset_password_token, *args, **kwargs): + """ + Signal receiver function to handle password reset token creation. + + Args: + - sender: Sender of the signal. + - instance: Instance related to the signal. + - reset_password_token: Password reset token object. + """ send_reset_email.delay(reset_password_token.user.email, reset_password_token.key) -# Передача списка департаментов для фронтенда -class DepartmentListView(generics.ListAPIView[Department]): +class DepartmentListView(generics.ListAPIView): + """ + View to list departments. + """ + queryset = Department.objects.all() serializer_class = DepartmentSerializer -# Передача списка позиций для фронтенда -class PositionListView(generics.ListAPIView[Position]): +class PositionListView(generics.ListAPIView): + """ + View to list positions. + """ + queryset = Position.objects.all() serializer_class = PositionSerializer From 13ddaa201af45a03e9c11f87bc303b91e417361b Mon Sep 17 00:00:00 2001 From: Lymar Volodymyr Date: Sat, 29 Jun 2024 13:56:12 +0200 Subject: [PATCH 3/8] docs: fix mypy --- users/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/users/views.py b/users/views.py index 4cc16c1..748e2c3 100644 --- a/users/views.py +++ b/users/views.py @@ -83,7 +83,7 @@ def post(self, request): return Response(status=status.HTTP_400_BAD_REQUEST) -class UserCreateView(generics.CreateAPIView): +class UserCreateView(generics.CreateAPIView[CustomUser]): """ View to handle user registration. """ @@ -154,7 +154,7 @@ def password_reset_token_created(sender, instance, reset_password_token, *args, send_reset_email.delay(reset_password_token.user.email, reset_password_token.key) -class DepartmentListView(generics.ListAPIView): +class DepartmentListView(generics.ListAPIView[Department]): """ View to list departments. """ @@ -163,7 +163,7 @@ class DepartmentListView(generics.ListAPIView): serializer_class = DepartmentSerializer -class PositionListView(generics.ListAPIView): +class PositionListView(generics.ListAPIView[Position]): """ View to list positions. """ From b7548c61bd1f12a4453ed3fd632e8ca70ad81549 Mon Sep 17 00:00:00 2001 From: Lymar Volodymyr Date: Sat, 29 Jun 2024 14:04:08 +0200 Subject: [PATCH 4/8] docs: updated docstrings in clients app --- clients/models.py | 18 ++++++++++++------ clients/permissions.py | 9 +++++---- clients/serializers.py | 22 ++++++++++++++++++++++ clients/views.py | 32 ++++++++++++++++++++++++++++---- 4 files changed, 67 insertions(+), 14 deletions(-) diff --git a/clients/models.py b/clients/models.py index 7eaed9c..2296809 100644 --- a/clients/models.py +++ b/clients/models.py @@ -5,7 +5,10 @@ class DirectorPosition(models.Model): - director_position = models.CharField(max_length=40) + """ + Model representing a director's position. + """ + director_position = models.CharField(max_length=40, verbose_name="Должность директора") class Meta: verbose_name = "Должность директора" @@ -16,7 +19,10 @@ def __str__(self) -> str: class Client(models.Model): - # Основная информация + """ + Model representing a client organization. + """ + # Main Information client_name = models.CharField( max_length=100, verbose_name="Наименование организации", db_index=True ) @@ -31,7 +37,7 @@ class Client(models.Model): destination_city = models.ForeignKey( City, on_delete=models.PROTECT, verbose_name="Город доставки", db_index=True ) - # ЖД реквизиты + # Railway Details railway_station = models.ForeignKey( RailwayStation, on_delete=models.PROTECT, @@ -40,7 +46,7 @@ class Client(models.Model): blank=True, null=True, ) - # Остальные данные + # Other Data receiver_name = models.CharField(max_length=100, blank=True, verbose_name="Имя получателя") receiver_id = models.PositiveIntegerField( blank=True, null=True, verbose_name="Номер получателя" @@ -48,11 +54,11 @@ class Client(models.Model): receiver_okpo = models.PositiveIntegerField(blank=True, null=True, verbose_name="ОКПО") receiver_adress = models.CharField(max_length=200, blank=True, verbose_name="Адрес получателя") special_marks = models.CharField(max_length=200, blank=True, verbose_name="Особые отметки") - # Номер приложения + # Application Number last_application_number = models.CharField( max_length=50, blank=True, verbose_name="Номер приложения" ) - # Пользователь который создал запись + # User who created the record user = models.ForeignKey( CustomUser, verbose_name="Пользователь", diff --git a/clients/permissions.py b/clients/permissions.py index fcee28b..9ff1022 100644 --- a/clients/permissions.py +++ b/clients/permissions.py @@ -3,10 +3,11 @@ class ClientAccessPermission(permissions.BasePermission): """ - Класс разрешений для доступа к записям клиентов. - Проверяет, что пользователь аутентифицирован для просмотра записей. - Также проверяет, что пользователь является автором записи - или администратором для выполнения изменений или удаления записей. + Permission class for accessing client records. + + Checks that the user is authenticated to view records. + Additionally, verifies that the user is either the author of the record + or an administrator to perform changes or deletions on records. """ def has_permission(self, request, view): diff --git a/clients/serializers.py b/clients/serializers.py index 461b37e..62966ba 100644 --- a/clients/serializers.py +++ b/clients/serializers.py @@ -4,12 +4,34 @@ class DirectorPositionSerializer(serializers.ModelSerializer[DirectorPosition]): + """ + Serializer for the DirectorPosition model. + + Serializes the 'id' and 'director_position' fields of DirectorPosition. + """ + class Meta: model = DirectorPosition fields = ["id", "director_position"] class ClientSerializer(serializers.ModelSerializer[Client]): + """ + Serializer for the Client model. + + Serializes all fields of the Client model including nested serialization of 'director_position'. + Adds 'destination_city' and 'railway_station' as CharField serializers. + Sets the current authenticated user as the value for the 'user' field using HiddenField. + + Fields: + - id: IntegerField + - director_position: Nested serialization using DirectorPositionSerializer + - destination_city: CharField for destination city name + - railway_station: CharField for railway station name + - user: HiddenField that defaults to the current authenticated user + + Note: 'user' field is automatically populated with the current user making the request. + """ director_position = DirectorPositionSerializer() destination_city: serializers.CharField = serializers.CharField() railway_station: serializers.CharField = serializers.CharField() diff --git a/clients/views.py b/clients/views.py index ba40cd6..50e0015 100644 --- a/clients/views.py +++ b/clients/views.py @@ -7,8 +7,13 @@ from .serializers import ClientSerializer, DirectorPositionSerializer -# Базовый класс для получения данных по записям клиентов class ClientAPIView(generics.ListCreateAPIView[Client]): + """ + API view for retrieving a list of clients and creating a new client record. + + Retrieves a list of clients with related director position, destination city, + and railway station. Caches the client list for 15 minutes if not already cached. + """ queryset = Client.objects.select_related( "director_position", "destination_city", "railway_station" ).all() @@ -16,6 +21,10 @@ class ClientAPIView(generics.ListCreateAPIView[Client]): permission_classes = (IsAuthenticated,) def get_queryset(self): + """ + Get the queryset of clients. If cached, return cached data; otherwise, fetch from database + and cache for 15 minutes. + """ cached_clients = cache.get("clients_list") if cached_clients: return cached_clients @@ -25,22 +34,37 @@ def get_queryset(self): return clients -# Изменение данных записи клиента class ClientAPIUpdateView(generics.RetrieveUpdateAPIView[Client]): + """ + API view for updating a client record. + + Retrieves and updates a specific client record based on its primary key. + Requires ClientAccessPermission for authorization. + """ queryset = Client.objects.all() serializer_class = ClientSerializer permission_classes = (ClientAccessPermission,) -# Удаление данных записи клиента class ClientAPIDeleteView(generics.DestroyAPIView[Client]): + """ + API view for deleting a client record. + + Deletes a specific client record based on its primary key. + Requires ClientAccessPermission for authorization. + """ queryset = Client.objects.all() serializer_class = ClientSerializer permission_classes = (ClientAccessPermission,) -# Передача списка позиций директора для фронтенда class DirectorPositionListView(generics.ListAPIView[DirectorPosition]): + """ + API view for retrieving a list of director positions. + + Retrieves a list of all available director positions. + Requires authentication (IsAuthenticated). + """ queryset = DirectorPosition.objects.all() serializer_class = DirectorPositionSerializer permission_classes = (IsAuthenticated,) From 844589ba99a7aca625c3d89746e105796314e57c Mon Sep 17 00:00:00 2001 From: Lymar Volodymyr Date: Sat, 29 Jun 2024 14:12:15 +0200 Subject: [PATCH 5/8] docs: updated docstrings in goods app --- clients/models.py | 2 ++ clients/serializers.py | 4 +++- clients/views.py | 4 ++++ goods/models.py | 17 +++++++++++++++++ goods/serializers.py | 4 ++++ goods/views.py | 8 ++++++++ 6 files changed, 38 insertions(+), 1 deletion(-) diff --git a/clients/models.py b/clients/models.py index 2296809..4f61b8b 100644 --- a/clients/models.py +++ b/clients/models.py @@ -8,6 +8,7 @@ class DirectorPosition(models.Model): """ Model representing a director's position. """ + director_position = models.CharField(max_length=40, verbose_name="Должность директора") class Meta: @@ -22,6 +23,7 @@ class Client(models.Model): """ Model representing a client organization. """ + # Main Information client_name = models.CharField( max_length=100, verbose_name="Наименование организации", db_index=True diff --git a/clients/serializers.py b/clients/serializers.py index 62966ba..20653f2 100644 --- a/clients/serializers.py +++ b/clients/serializers.py @@ -19,7 +19,8 @@ class ClientSerializer(serializers.ModelSerializer[Client]): """ Serializer for the Client model. - Serializes all fields of the Client model including nested serialization of 'director_position'. + Serializes all fields of the Client model + including nested serialization of 'director_position'. Adds 'destination_city' and 'railway_station' as CharField serializers. Sets the current authenticated user as the value for the 'user' field using HiddenField. @@ -32,6 +33,7 @@ class ClientSerializer(serializers.ModelSerializer[Client]): Note: 'user' field is automatically populated with the current user making the request. """ + director_position = DirectorPositionSerializer() destination_city: serializers.CharField = serializers.CharField() railway_station: serializers.CharField = serializers.CharField() diff --git a/clients/views.py b/clients/views.py index 50e0015..b9495de 100644 --- a/clients/views.py +++ b/clients/views.py @@ -14,6 +14,7 @@ class ClientAPIView(generics.ListCreateAPIView[Client]): Retrieves a list of clients with related director position, destination city, and railway station. Caches the client list for 15 minutes if not already cached. """ + queryset = Client.objects.select_related( "director_position", "destination_city", "railway_station" ).all() @@ -41,6 +42,7 @@ class ClientAPIUpdateView(generics.RetrieveUpdateAPIView[Client]): Retrieves and updates a specific client record based on its primary key. Requires ClientAccessPermission for authorization. """ + queryset = Client.objects.all() serializer_class = ClientSerializer permission_classes = (ClientAccessPermission,) @@ -53,6 +55,7 @@ class ClientAPIDeleteView(generics.DestroyAPIView[Client]): Deletes a specific client record based on its primary key. Requires ClientAccessPermission for authorization. """ + queryset = Client.objects.all() serializer_class = ClientSerializer permission_classes = (ClientAccessPermission,) @@ -65,6 +68,7 @@ class DirectorPositionListView(generics.ListAPIView[DirectorPosition]): Retrieves a list of all available director positions. Requires authentication (IsAuthenticated). """ + queryset = DirectorPosition.objects.all() serializer_class = DirectorPositionSerializer permission_classes = (IsAuthenticated,) diff --git a/goods/models.py b/goods/models.py index b3e1a58..20f85e0 100644 --- a/goods/models.py +++ b/goods/models.py @@ -4,6 +4,11 @@ class Product(models.Model): + """ + A model representing a product with specific + attributes like flour type, brand, package, and price. + """ + flour_name = models.ForeignKey( "Flour", on_delete=models.PROTECT, related_name="flour_goods", db_index=True ) @@ -28,6 +33,10 @@ def __str__(self) -> str: class Flour(models.Model): + """ + A model representing the type of flour used in products. + """ + flour_name = models.CharField(max_length=255, blank=False) class Meta: @@ -40,6 +49,10 @@ def __str__(self) -> str: class Brand(models.Model): + """ + A model representing a brand associated with products. + """ + brand = models.CharField(max_length=100, verbose_name="Брэнд", blank=True) class Meta: @@ -52,6 +65,10 @@ def __str__(self) -> str: class Package(models.Model): + """ + A model representing packaging details associated with products. + """ + package = models.IntegerField(verbose_name="Тара") factory = models.ForeignKey(Factory, on_delete=models.PROTECT) pallet_weight = models.PositiveIntegerField(verbose_name="Вес на паллете") diff --git a/goods/serializers.py b/goods/serializers.py index cecf80f..34686c8 100644 --- a/goods/serializers.py +++ b/goods/serializers.py @@ -4,6 +4,10 @@ class GoodsSerializer(serializers.ModelSerializer[Product]): + """ + Serializer for serializing/deserializing Product objects. + """ + flour_name: serializers.CharField = serializers.CharField() brand: serializers.CharField = serializers.CharField() package: serializers.CharField = serializers.CharField() diff --git a/goods/views.py b/goods/views.py index 5deec59..51dca05 100644 --- a/goods/views.py +++ b/goods/views.py @@ -7,11 +7,19 @@ class GoodsViewSet(viewsets.ModelViewSet[Product]): + """ + A viewset for managing CRUD operations for products. + Retrieves a list of products with caching enabled for 30 minutes. + """ + queryset = Product.objects.select_related("flour_name", "brand", "package").all() serializer_class = GoodsSerializer permission_classes = (IsAuthenticated,) def get_queryset(self): + """ + Override the default queryset to utilize caching for better performance. + """ cached_goods = cache.get("goods_list") if cached_goods: return cached_goods From 3ffcbe571bfe4e271f12ebce856e75b6339f6318 Mon Sep 17 00:00:00 2001 From: Lymar Volodymyr Date: Sun, 30 Jun 2024 11:54:48 +0200 Subject: [PATCH 6/8] docs: updated docstrings in logistics app --- logistics/models.py | 23 ++++++++++++++++++++--- logistics/serializers.py | 25 +++++++++++++++++++++++++ logistics/views.py | 25 ++++++++++++++++++++++++- 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/logistics/models.py b/logistics/models.py index b49911d..0a74ebb 100644 --- a/logistics/models.py +++ b/logistics/models.py @@ -4,6 +4,10 @@ class City(models.Model): + """ + Model representing a city. + """ + city = models.CharField( max_length=100, blank=False, @@ -25,8 +29,11 @@ def __str__(self) -> str: return f"{self.city}, {self.region}" -# Данные по логистике авто class TripAuto(models.Model): + """ + Model representing auto logistics data. + """ + departure_city = models.ForeignKey( "City", db_column="departure_city", @@ -53,8 +60,11 @@ def __str__(self) -> str: ) -# Таблица ж/д станций class RailwayStation(models.Model): + """ + Model representing a railway station. + """ + station_name = models.CharField( max_length=100, blank=False, @@ -77,8 +87,11 @@ def __str__(self) -> str: return f"{self.station_name}, {self.station_branch}, {self.station_id}" -# Данные по логистике жд class TripRailway(models.Model): + """ + Model representing railway logistics data. + """ + departure_station_name = models.ForeignKey( "RailwayStation", db_column="departure_station_name", @@ -105,6 +118,10 @@ def __str__(self) -> str: class Factory(models.Model): + """ + Model representing a factory. + """ + full_name = models.CharField( max_length=100, blank=False, verbose_name="Полное название предприятия" ) diff --git a/logistics/serializers.py b/logistics/serializers.py index aa215e3..1b2025a 100644 --- a/logistics/serializers.py +++ b/logistics/serializers.py @@ -4,12 +4,25 @@ class CitySerializer(serializers.ModelSerializer[City]): + """ + Serializer for City model. + + Serializes all fields of the City model. + """ + class Meta: model = City fields = "__all__" class TripAutoSerializer(serializers.ModelSerializer[TripAuto]): + """ + Serializer for TripAuto model. + + Serializes all fields of the TripAuto model, and includes + additional fields for departure_city and destination_city. + """ + departure_city: serializers.CharField = serializers.CharField() destination_city: serializers.CharField = serializers.CharField() @@ -19,12 +32,20 @@ class Meta: class RailwayStationSerializer(serializers.ModelSerializer[RailwayStation]): + """ + Serializer for RailwayStation model. + """ + class Meta: model = RailwayStation fields = "__all__" class TripRailwaySerializer(serializers.ModelSerializer[TripRailway]): + """ + Serializer for TripRailway model. + """ + departure_station_name: serializers.CharField = serializers.CharField() destination_station_name: serializers.CharField = serializers.CharField() @@ -34,6 +55,10 @@ class Meta: class FactorySerializer(serializers.ModelSerializer[Factory]): + """ + Serializer for Factory model. + """ + class Meta: model = Factory fields = "__all__" diff --git a/logistics/views.py b/logistics/views.py index aa5ba48..92b075c 100644 --- a/logistics/views.py +++ b/logistics/views.py @@ -14,39 +14,62 @@ class CityViewSet(viewsets.ModelViewSet[City]): + """ + A viewset for handling CRUD operations on City objects. + """ + queryset = City.objects.all() serializer_class = CitySerializer permission_classes = (IsAuthenticated,) class TripAutoViewSet(viewsets.ModelViewSet[TripAuto]): + """ + A viewset for handling CRUD operations on TripAuto objects. + """ + queryset = TripAuto.objects.all() serializer_class = TripAutoSerializer permission_classes = (IsAuthenticated,) class RailwayStationViewSet(viewsets.ModelViewSet[RailwayStation]): + """ + A viewset for handling CRUD operations on RailwayStation objects. + """ + queryset = RailwayStation.objects.all() serializer_class = RailwayStationSerializer permission_classes = (IsAuthenticated,) class TripRailwayViewSet(viewsets.ModelViewSet[TripRailway]): + """ + A viewset for handling CRUD operations on TripRailway objects. + """ + queryset = TripRailway.objects.all() serializer_class = TripRailwaySerializer permission_classes = (IsAuthenticated,) class FactoryListAPIView(generics.ListAPIView[Factory]): + """ + A view for listing factories, with caching implemented. + """ + queryset = Factory.objects.all() serializer_class = FactorySerializer permission_classes = (IsAuthenticated,) def get_queryset(self): + """ + Overrides the queryset retrieval to implement caching. + """ cached_factories = cache.get("factories_list") if cached_factories: return cached_factories else: - factories = list(super().get_queryset()) + factories = super().get_queryset() cache.set("factories_list", factories, 3600) return factories From 1857ffaebfda63f476a0f7ff9b885fae3e0623f6 Mon Sep 17 00:00:00 2001 From: Lymar Volodymyr Date: Sun, 30 Jun 2024 12:15:27 +0200 Subject: [PATCH 7/8] docs: updated docstrings in makedoc app --- makedoc/data_service.py | 27 ++++++++++++ makedoc/serializers.py | 12 ++++++ makedoc/services.py | 66 ++++++++++++++++++++++++++--- makedoc/tasks.py | 7 +++ makedoc/tests/conftest.py | 16 ++++--- makedoc/tests/test_views_makedoc.py | 5 ++- makedoc/utils.py | 13 ++++++ makedoc/views.py | 4 +- 8 files changed, 136 insertions(+), 14 deletions(-) diff --git a/makedoc/data_service.py b/makedoc/data_service.py index e6f5026..a30c158 100644 --- a/makedoc/data_service.py +++ b/makedoc/data_service.py @@ -6,8 +6,16 @@ class DataService: + """ + Service class providing methods to retrieve and validate data related to delivery, + clients, products, factories, delivery costs, destination cities, and authenticated users. + """ + @staticmethod def get_delivery_type(validated_data): + """ + Retrieves the delivery type from validated data. + """ delivery_type = validated_data.get("delivery_type") if delivery_type is None: raise Exception("Delivery type not found") @@ -15,6 +23,9 @@ def get_delivery_type(validated_data): @staticmethod def get_client(validated_data): + """ + Retrieves the client object based on the client ID from validated data. + """ try: client_id = validated_data.get("client_id") client = Client.objects.get(id=client_id) @@ -24,6 +35,10 @@ def get_client(validated_data): @staticmethod def get_products(validated_data): + """ + Retrieves a list of products with quantities, discounts, + and prices based on product IDs from validated data. + """ products_data = validated_data.get("items") results = [] cached_goods = cache.get("goods_list") @@ -48,6 +63,9 @@ def get_products(validated_data): @staticmethod def get_factory(validated_data): + """ + Retrieves the factory object based on the factory ID from validated data. + """ try: factory_id = validated_data.get("factory_id") cached_factories = cache.get("factories_list") @@ -62,6 +80,9 @@ def get_factory(validated_data): @staticmethod def get_delivery_cost(validated_data): + """ + Retrieves the delivery cost from validated data. + """ delivery_cost = validated_data.get("delivery_cost") if delivery_cost is None: raise Exception("Delivery cost not found") @@ -69,6 +90,9 @@ def get_delivery_cost(validated_data): @staticmethod def get_destination(validated_data): + """ + Retrieves the destination city from validated data. + """ destination = validated_data.get("destination") if destination is None: raise Exception("City not found") @@ -76,6 +100,9 @@ def get_destination(validated_data): @staticmethod def get_user(request): + """ + Retrieves the authenticated user from the request object. + """ user = request.user if user: return user diff --git a/makedoc/serializers.py b/makedoc/serializers.py index 6236825..96bcd34 100644 --- a/makedoc/serializers.py +++ b/makedoc/serializers.py @@ -4,16 +4,25 @@ class DocumentsSimpleSerializer(serializers.Serializer[Any]): + """ + A simple serializer for document data. + """ pass class OrderItemSerializer(serializers.Serializer[Any]): + """ + A serializer for order items. + """ product_id = serializers.IntegerField() quantity = serializers.FloatField(min_value=0.001) discount = serializers.IntegerField(required=False, default=0, max_value=100) class DataDocSerializer(serializers.Serializer[Any]): + """ + A serializer for document data. + """ delivery_type = serializers.CharField() client_id = serializers.IntegerField() items = serializers.ListField(child=OrderItemSerializer()) @@ -23,4 +32,7 @@ class DataDocSerializer(serializers.Serializer[Any]): class FileNameSerializer(serializers.Serializer[Any]): + """ + A serializer for file names. + """ file_name = serializers.CharField(required=False, allow_blank=True) diff --git a/makedoc/services.py b/makedoc/services.py index c4b372a..b89daea 100644 --- a/makedoc/services.py +++ b/makedoc/services.py @@ -14,6 +14,10 @@ class Documents: + """ + A class to handle the creation and management of various shipment-related documents. + """ + def __init__(self, validated_data): self.validated_data = validated_data self.auto = 0 @@ -23,6 +27,9 @@ def __init__(self, validated_data): self.archive_name = "" def update_documents(self): + """ + Updates the document indicators based on the validated data. + """ delivery_type = DataService.get_delivery_type(self.validated_data) if delivery_type in ("auto", "self-delivery"): self.auto = 1 @@ -40,6 +47,9 @@ def update_documents(self): self.service_note = 1 def form_auto_document(self, request): + """ + Creates an auto document based on the validated data and user request. + """ self.docname = "Авто" try: user = DataService.get_user(request) @@ -68,6 +78,9 @@ def form_auto_document(self, request): self.add_workbook_to_archive_xlsx(wb, user, client) def fill_contract_info(self, ws, client): + """ + Fills the contract information into the worksheet. + """ formatted_contract_date = client.contract_date.strftime("%d.%m.%Y") title = ws.cell(row=1, column=1, value=f"Приложение № {client.last_application_number}") title.font = Font(bold=True) @@ -80,6 +93,9 @@ def fill_contract_info(self, ws, client): ws.cell(row=6, column=6, value=get_formatted_date_agreement()) def fill_product_info(self, ws, products, logistics, caret): + """ + Fills the product information into the worksheet. + """ goods_quantity = len(products) thin_border = Border( left=Side(style="thin"), @@ -121,6 +137,9 @@ def fill_product_info(self, ws, products, logistics, caret): self.caret_product = caret + 1 def fill_factory_info(self, ws, factory): + """ + Fills the factory information into the worksheet. + """ caret = self.caret_product ws.merge_cells(start_row=caret, start_column=1, end_row=caret, end_column=2) ws.cell(row=caret, column=1, value="Грузоотправитель:") @@ -128,6 +147,9 @@ def fill_factory_info(self, ws, factory): self.caret_factory = caret + 1 def fill_auto_services(self, ws, logistics): + """ + Fills the auto services information into the worksheet. + """ caret = self.caret_factory ws.merge_cells(start_row=caret, start_column=1, end_row=caret, end_column=2) ws.cell(row=caret, column=1, value="Автотранспортные услуги:") @@ -139,6 +161,9 @@ def fill_auto_services(self, ws, logistics): self.caret_services = caret + 1 def fill_debt_info(self, ws): + """ + Fills the debt information into the worksheet. + """ caret = self.caret_services ws.merge_cells(start_row=caret, start_column=1, end_row=caret, end_column=2) ws.cell(row=caret, column=1, value="Срок отгрузки:") @@ -152,6 +177,9 @@ def fill_debt_info(self, ws): self.caret_debt = caret + 2 def fill_legal_info(self, ws, client): + """ + Fills the legal information into the worksheet. + """ caret = self.caret_debt ws.merge_cells(start_row=caret, start_column=1, end_row=caret, end_column=6) formatted_contract_date = client.contract_date.strftime("%d.%m.%Y") @@ -165,6 +193,9 @@ def fill_legal_info(self, ws, client): self.caret_legal = caret + 4 def fill_signatures(self, ws, client): + """ + Fills the signature information into the worksheet. + """ caret = self.caret_legal ws.cell(row=caret, column=1, value="Генеральный директор") ws.cell(row=caret + 1, column=1, value="ООО «Торговый дом «Оскольская мука»") @@ -175,6 +206,9 @@ def fill_signatures(self, ws, client): self.caret_signatures = caret + 11 def fill_manager_contact(self, ws, user): + """ + Fills the manager contact information into the worksheet. + """ caret = 59 ws.merge_cells(start_row=caret, start_column=1, end_row=caret, end_column=6) manager = ws.cell( @@ -190,14 +224,15 @@ def fill_manager_contact(self, ws, user): phone.alignment = Alignment(horizontal="center", vertical="center") def add_workbook_to_archive_xlsx(self, wb, user, client): + """ + Adds the generated workbook to a zip archive. + """ tempdir = os.path.join("makedoc", "tempdoc", str(user.id)) os.makedirs(tempdir, exist_ok=True) - client_name = client.client_name.replace(' ', '_') - date_today = datetime.today().strftime('%d.%m.%Y_%H:%M:%S') - self.archive_name = ( - f"{client_name}_{date_today}.zip" - ) + client_name = client.client_name.replace(" ", "_") + date_today = datetime.today().strftime("%d.%m.%Y_%H:%M:%S") + self.archive_name = f"{client_name}_{date_today}.zip" archive_path = f"{tempdir}/{self.archive_name}" xlsx_filename = f"{client.last_application_number} {self.docname} {client.client_name} \ @@ -211,6 +246,9 @@ def add_workbook_to_archive_xlsx(self, wb, user, client): archive.writestr(xlsx_filename, xlsx_io.getvalue()) def apply_styles(self, ws): + """ + Applies styles to the cells in the worksheet. + """ for row in ws.iter_rows(): for cell in row: if cell.value is not None: @@ -222,6 +260,9 @@ def apply_styles(self, ws): cell.font = Font(bold=True, size=12) def form_rw_document(self, request): + """ + Creates a railways document based on the validated data and user request. + """ self.docname = "Жд" try: user = DataService.get_user(request) @@ -251,6 +292,9 @@ def form_rw_document(self, request): self.add_workbook_to_archive_xlsx(wb, user, client) def fill_rw_services(self, ws, factory, client, logistics): + """ + Fills the railways services information into the worksheet. + """ caret = self.caret_factory ws.merge_cells(start_row=caret, start_column=1, end_row=caret, end_column=2) ws.cell(row=caret, column=1, value="Поставка осуществляется:") @@ -312,6 +356,9 @@ def fill_rw_services(self, ws, factory, client, logistics): self.caret_services = caret + 1 def form_service_note(self, request): + """ + Creates a service note based on the validated data and user request. + """ self.docname = "Служебная записка" try: client = DataService.get_client(self.validated_data) @@ -336,6 +383,9 @@ def form_service_note(self, request): self.add_workbook_to_archive_xlsx(wb, user, client) def fill_text_note(self, ws, client, discount, destination): + """ + Fills the text note section in the service note Excel sheet. + """ blank = "________" if destination != "0": region = destination.split(",")[1].strip() @@ -349,6 +399,9 @@ def fill_text_note(self, ws, client, discount, destination): ws.cell(row=19, column=1, value=text) def form_transport_sheet(self, request): + """ + Creates a transport sheet document based on the validated data and user request. + """ self.docname = "Сопроводительный лист" try: user = DataService.get_user(request) @@ -379,6 +432,9 @@ def form_transport_sheet(self, request): self.add_workbook_to_archive_xlsx(wb, user, client) def fill_contract_info_transport_sheet(self, ws, client): + """ + Fills the contract information section in the transport sheet Excel sheet. + """ formatted_contract_date = client.contract_date.strftime("%d.%m.%Y") ws.cell(row=1, column=1, value="СОПРОВОДИТЕЛЬНЫЙ ЛИСТ к") title = ws.cell(row=2, column=1, value=f"Приложению № {client.last_application_number}") diff --git a/makedoc/tasks.py b/makedoc/tasks.py index 44ef499..4baeaf9 100644 --- a/makedoc/tasks.py +++ b/makedoc/tasks.py @@ -7,6 +7,13 @@ @shared_task def delete_files(): + """ + Deletes all user folders from the specified directory. + + Checks if the directory exists, and if it does, deletes all subdirectories + within it. If the directory does not exist, it returns an appropriate message. + If an error occurs while deleting a subdirectory, it returns the error message. + """ directory = settings.BASE_DIR / "makedoc" / "tempdoc" if not os.path.exists(directory): diff --git a/makedoc/tests/conftest.py b/makedoc/tests/conftest.py index ff0c90d..296f1ba 100644 --- a/makedoc/tests/conftest.py +++ b/makedoc/tests/conftest.py @@ -27,12 +27,16 @@ def make_test_data(faker): "delivery_type": faker.random_element(elements=("auto", "rw", "self-delivery")), "client_id": faker.random_int(min=1, max=100), "items": [ - {"product_id": faker.random_int(min=1, max=100), - "quantity": faker.random_int(min=1, max=10), - "discount": faker.random_int(min=0, max=100)}, - {"product_id": faker.random_int(min=1, max=100), - "quantity": faker.random_int(min=1, max=10), - "discount": faker.random_int(min=0, max=100)}, + { + "product_id": faker.random_int(min=1, max=100), + "quantity": faker.random_int(min=1, max=10), + "discount": faker.random_int(min=0, max=100), + }, + { + "product_id": faker.random_int(min=1, max=100), + "quantity": faker.random_int(min=1, max=10), + "discount": faker.random_int(min=0, max=100), + }, ], "factory_id": faker.random_int(min=1, max=100), "destination": faker.city(), diff --git a/makedoc/tests/test_views_makedoc.py b/makedoc/tests/test_views_makedoc.py index 0d9669b..257ccaf 100644 --- a/makedoc/tests/test_views_makedoc.py +++ b/makedoc/tests/test_views_makedoc.py @@ -22,8 +22,9 @@ def test__data_doc_view__authorized_user_can_post_data(authorized_client, make_t @pytest.mark.django_db -def test__data_doc_view__authorized_user_post_data_response_is_correct(authorized_client, - make_test_data): +def test__data_doc_view__authorized_user_post_data_response_is_correct( + authorized_client, make_test_data +): url = reverse("data") response = authorized_client.post(url, make_test_data, format="json") assert response.status_code == 200 diff --git a/makedoc/utils.py b/makedoc/utils.py index 7044448..ab02c82 100644 --- a/makedoc/utils.py +++ b/makedoc/utils.py @@ -7,19 +7,32 @@ def format_date_case(date_object, case): + """ + Formats the given date object to the specified grammatical case. + """ # case - падеж, https://pymorphy2.readthedocs.io/en/stable/user/grammemes.html#russian-cases return morph.parse(str(date_object))[0].inflect({case}).word def format_month_ru_locale(date_object): + """ + Formats the month of the given date object. + """ return bd.format_date(date_object, "MMMM", locale="ru_RU") def get_formatted_date_agreement(): + """ + Gets the current date formatted for agreement documents. + """ return bd.format_date(date.today(), "«d» MMMM y г.", locale="ru_RU") def get_formatted_date_shipment(case): + """ + Gets the formatted shipment date range from the current month to the next month + in the specified grammatical case. + """ current_date = date.today() next_month_date = (current_date.replace(day=1) + timedelta(days=31)).replace(day=1) raw_current_month = format_month_ru_locale(current_date) diff --git a/makedoc/views.py b/makedoc/views.py index e70d900..4fc41b1 100644 --- a/makedoc/views.py +++ b/makedoc/views.py @@ -18,6 +18,7 @@ class CreateDocsAPIView(APIView): """ Responsible for creating documents based on data from the cache. """ + serializer_class = DocumentsSimpleSerializer permission_classes = [IsAuthenticated] @@ -52,7 +53,6 @@ def get(self, request, *args, **kwargs): return Response({"message": "Documents saved"}, status=status.HTTP_200_OK) -# Загрузка документа class DownloadDocAPIView(APIView): """ To download a file: @@ -61,6 +61,7 @@ class DownloadDocAPIView(APIView): - With the parameter {"file_name": "name your file"} - load a specific file from the list. """ + permission_classes = [IsAuthenticated] def post(self, request, *args, **kwargs): @@ -96,6 +97,7 @@ class DataDocAPIView(generics.GenericAPIView[Any]): """ Responsible for receiving data, validating it and storing it in cache. """ + serializer_class = DataDocSerializer permission_classes = (IsAuthenticated,) From 90442b236cfbf52ac6360cd88c86ffc5717aff8b Mon Sep 17 00:00:00 2001 From: Lymar Volodymyr Date: Sun, 30 Jun 2024 12:16:50 +0200 Subject: [PATCH 8/8] docs: update README, schema.json --- README.md | 77 +++++++++++++++----- docs/schema.json | 186 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 239 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index a4e4d0e..0bdc07e 100644 --- a/README.md +++ b/README.md @@ -11,19 +11,58 @@ [![pytest](https://img.shields.io/badge/pytest-8.0.2-0A9EDC?style=flat&logo=pytest&logoColor=white)](https://docs.pytest.org/) [![Ruff](https://img.shields.io/badge/Ruff-0.3-FCC21B?style=flat&logo=ruff&logoColor=white"/)](https://github.com/astral-sh/ruff) [![SimpleJWT](https://img.shields.io/badge/SimpleJWT-5.3.1-orange?style=flat&logo=jwt&logoColor=white)](https://github.com/jazzband/djangorestframework-simplejwt) +[![pre-commit](https://img.shields.io/badge/precommit-0.2-FAB040?style=flat&logo=precommit&logoColor=white)](https://pre-commit.com/) +[![Flake8](https://img.shields.io/badge/flake8-checked-blueviolet?style=flat)](https://flake8.pycqa.org/en/latest/) +[![mypy](https://img.shields.io/badge/mypy-checked-blue?style=flat)](https://mypy-lang.org/) # Melnichanka ## Table of Contents -- [How to run the project](#how-to-run-the-project) - [Project Description](#project-description) +- [How to run the project](#how-to-run-the-project) - [Usage](#usage) - [Documentation](#documentation) - [Testing](#testing) - [Contributing](#contributing) - [License](#license) +## Project Description + +***Melnichanka: Simplifying Shipment Application Submissions*** + +Melnichanka is a web application designed to streamline the process of submitting shipment +applications. It automates the generation of necessary documents based on user input, covering +details such as goods, brands, factories, and packages. + +***Key Benefits:*** + +- Efficiency: Automates document generation to save time and resources. +- Accuracy: Reduces errors associated with manual data entry. +- Accessibility: User-friendly interface ensures ease of use, suitable for all levels of technical + proficiency. +- Convenience: Simplifies the submission process, even for users with minimal technical experience. + +***Technological Foundation:*** + +- Built with Django: Utilizes the robust Django framework for Python, ensuring reliability and + flexibility. +- Containerized with Docker: Deployed using Docker and Docker Compose, enabling easy scalability + and + deployment. + +***Target Audience:*** + +Designed for companies needing to submit shipment applications regularly. +Ideal for organizations seeking to enhance efficiency, minimize errors, and optimize resource +allocation. + +***Conclusion:*** + +Melnichanka empowers companies by automating the creation of shipment documents, enabling them to +focus on core business activities. It stands as a powerful tool for improving operational +efficiency and streamlining document submission processes. + ## How to run the project 1. Install [`Docker`](https://www.docker.com/) @@ -43,6 +82,10 @@ git clone https://github.com/KroshkaByte/Melnichanka.git cd Melnichanka ``` +- [Configure .env](#environment-configuration) + +

+ - Start the project from root directory: ```sh @@ -51,27 +94,21 @@ docker-compose up -d --build - Open your web browser and navigate to http://localhost:80 to access the application. -## Project Description - -Melnichanka is a web application designed to facilitate the process of submitting shipment -applications to consignees. The application generates a package of documents required for shipment -based on user input, including information about goods, brands, factories, and packages. +#### Environment Configuration -The application is intended to be used by companies that need to submit shipment applications on a -regular basis. By using Melnichanka, companies can streamline the process of generating the -necessary documents, reduce errors, and save time and resources. +You need to manually update this secret in your `.env` file each time it changes. -The application includes a user-friendly interface that allows users to easily enter data and -generate documents. The interface is designed to be intuitive and easy to use, even for users with -little or no technical experience. +Additionally, create a `.env` file in the root directory based on the provided `.env.example`. Fill +in your own data and rename the file to `.env`. -Melnichanka is built using modern web technologies, including `Django`, a popular web framework for -Python. The application is containerized using Docker and Docker Compose, making it easy to deploy -and scale. +Please note that the Django secret key used in the `.env.example` is just an example. You can +generate a new key using the following command: -Overall, Melnichanka is a powerful and flexible tool that can help companies save time and -resources when submitting shipment applications. By automating the process of generating documents, -Melnichanka can help companies reduce errors, improve efficiency, and focus on their core business. + ```sh + from django.core.management.utils import get_random_secret_key + + print(get_random_secret_key()) + ``` ## Usage @@ -79,8 +116,8 @@ To use Melnichanka, follow these steps: - Enter the required information about the goods, brands, factories, and packages. - Click the `Generate Documents` button to generate the package of documents required for shipment. -- Review the generated documents and make any necessary edits. -- Download the documents in the desired format (e.g., PDF, Word, Excel). +- Verify all the data and create an archive with documents. +- Download the archive of documents in Excel format. - Submit the documents to the consignee as required. ## Database Pre-population diff --git a/docs/schema.json b/docs/schema.json index 8fd9014..7712032 100644 --- a/docs/schema.json +++ b/docs/schema.json @@ -8,6 +8,11 @@ paths: /api/v1/clients/: get: operationId: v1_clients_list + description: |- + API view for retrieving a list of clients and creating a new client record. + + Retrieves a list of clients with related director position, destination city, + and railway station. Caches the client list for 15 minutes if not already cached. tags: - v1 security: @@ -23,6 +28,11 @@ paths: description: '' post: operationId: v1_clients_create + description: |- + API view for retrieving a list of clients and creating a new client record. + + Retrieves a list of clients with related director position, destination city, + and railway station. Caches the client list for 15 minutes if not already cached. tags: - v1 requestBody: @@ -49,6 +59,11 @@ paths: /api/v1/clients/{id}/: get: operationId: v1_clients_retrieve + description: |- + API view for updating a client record. + + Retrieves and updates a specific client record based on its primary key. + Requires ClientAccessPermission for authorization. parameters: - in: path name: id @@ -68,6 +83,11 @@ paths: description: '' put: operationId: v1_clients_update + description: |- + API view for updating a client record. + + Retrieves and updates a specific client record based on its primary key. + Requires ClientAccessPermission for authorization. parameters: - in: path name: id @@ -99,6 +119,11 @@ paths: description: '' patch: operationId: v1_clients_partial_update + description: |- + API view for updating a client record. + + Retrieves and updates a specific client record based on its primary key. + Requires ClientAccessPermission for authorization. parameters: - in: path name: id @@ -130,6 +155,11 @@ paths: /api/v1/clients/delete/{id}/: delete: operationId: v1_clients_delete_destroy + description: |- + API view for deleting a client record. + + Deletes a specific client record based on its primary key. + Requires ClientAccessPermission for authorization. parameters: - in: path name: id @@ -146,6 +176,11 @@ paths: /api/v1/clients/directorposition/: get: operationId: v1_clients_directorposition_list + description: |- + API view for retrieving a list of director positions. + + Retrieves a list of all available director positions. + Requires authentication (IsAuthenticated). tags: - v1 security: @@ -217,6 +252,9 @@ paths: /api/v1/goods/: get: operationId: v1_goods_list + description: |- + A viewset for managing CRUD operations for products. + Retrieves a list of products with caching enabled for 30 minutes. tags: - v1 security: @@ -232,6 +270,9 @@ paths: description: '' post: operationId: v1_goods_create + description: |- + A viewset for managing CRUD operations for products. + Retrieves a list of products with caching enabled for 30 minutes. tags: - v1 requestBody: @@ -258,6 +299,9 @@ paths: /api/v1/goods/{id}/: get: operationId: v1_goods_retrieve + description: |- + A viewset for managing CRUD operations for products. + Retrieves a list of products with caching enabled for 30 minutes. parameters: - in: path name: id @@ -278,6 +322,9 @@ paths: description: '' put: operationId: v1_goods_update + description: |- + A viewset for managing CRUD operations for products. + Retrieves a list of products with caching enabled for 30 minutes. parameters: - in: path name: id @@ -310,6 +357,9 @@ paths: description: '' patch: operationId: v1_goods_partial_update + description: |- + A viewset for managing CRUD operations for products. + Retrieves a list of products with caching enabled for 30 minutes. parameters: - in: path name: id @@ -341,6 +391,9 @@ paths: description: '' delete: operationId: v1_goods_destroy + description: |- + A viewset for managing CRUD operations for products. + Retrieves a list of products with caching enabled for 30 minutes. parameters: - in: path name: id @@ -358,6 +411,7 @@ paths: /api/v1/logistics/auto/: get: operationId: v1_logistics_auto_list + description: A viewset for handling CRUD operations on TripAuto objects. tags: - v1 security: @@ -373,6 +427,7 @@ paths: description: '' post: operationId: v1_logistics_auto_create + description: A viewset for handling CRUD operations on TripAuto objects. tags: - v1 requestBody: @@ -399,6 +454,7 @@ paths: /api/v1/logistics/auto/{id}/: get: operationId: v1_logistics_auto_retrieve + description: A viewset for handling CRUD operations on TripAuto objects. parameters: - in: path name: id @@ -419,6 +475,7 @@ paths: description: '' put: operationId: v1_logistics_auto_update + description: A viewset for handling CRUD operations on TripAuto objects. parameters: - in: path name: id @@ -451,6 +508,7 @@ paths: description: '' patch: operationId: v1_logistics_auto_partial_update + description: A viewset for handling CRUD operations on TripAuto objects. parameters: - in: path name: id @@ -482,6 +540,7 @@ paths: description: '' delete: operationId: v1_logistics_auto_destroy + description: A viewset for handling CRUD operations on TripAuto objects. parameters: - in: path name: id @@ -499,6 +558,7 @@ paths: /api/v1/logistics/city/: get: operationId: v1_logistics_city_list + description: A viewset for handling CRUD operations on City objects. tags: - v1 security: @@ -514,6 +574,7 @@ paths: description: '' post: operationId: v1_logistics_city_create + description: A viewset for handling CRUD operations on City objects. tags: - v1 requestBody: @@ -540,6 +601,7 @@ paths: /api/v1/logistics/city/{id}/: get: operationId: v1_logistics_city_retrieve + description: A viewset for handling CRUD operations on City objects. parameters: - in: path name: id @@ -560,6 +622,7 @@ paths: description: '' put: operationId: v1_logistics_city_update + description: A viewset for handling CRUD operations on City objects. parameters: - in: path name: id @@ -592,6 +655,7 @@ paths: description: '' patch: operationId: v1_logistics_city_partial_update + description: A viewset for handling CRUD operations on City objects. parameters: - in: path name: id @@ -623,6 +687,7 @@ paths: description: '' delete: operationId: v1_logistics_city_destroy + description: A viewset for handling CRUD operations on City objects. parameters: - in: path name: id @@ -640,6 +705,7 @@ paths: /api/v1/logistics/factories/: get: operationId: v1_logistics_factories_list + description: A view for listing factories, with caching implemented. tags: - v1 security: @@ -656,6 +722,7 @@ paths: /api/v1/logistics/rw/: get: operationId: v1_logistics_rw_list + description: A viewset for handling CRUD operations on TripRailway objects. tags: - v1 security: @@ -671,6 +738,7 @@ paths: description: '' post: operationId: v1_logistics_rw_create + description: A viewset for handling CRUD operations on TripRailway objects. tags: - v1 requestBody: @@ -697,6 +765,7 @@ paths: /api/v1/logistics/rw/{id}/: get: operationId: v1_logistics_rw_retrieve + description: A viewset for handling CRUD operations on TripRailway objects. parameters: - in: path name: id @@ -717,6 +786,7 @@ paths: description: '' put: operationId: v1_logistics_rw_update + description: A viewset for handling CRUD operations on TripRailway objects. parameters: - in: path name: id @@ -749,6 +819,7 @@ paths: description: '' patch: operationId: v1_logistics_rw_partial_update + description: A viewset for handling CRUD operations on TripRailway objects. parameters: - in: path name: id @@ -780,6 +851,7 @@ paths: description: '' delete: operationId: v1_logistics_rw_destroy + description: A viewset for handling CRUD operations on TripRailway objects. parameters: - in: path name: id @@ -797,6 +869,7 @@ paths: /api/v1/logistics/stations/: get: operationId: v1_logistics_stations_list + description: A viewset for handling CRUD operations on RailwayStation objects. tags: - v1 security: @@ -812,6 +885,7 @@ paths: description: '' post: operationId: v1_logistics_stations_create + description: A viewset for handling CRUD operations on RailwayStation objects. tags: - v1 requestBody: @@ -838,6 +912,7 @@ paths: /api/v1/logistics/stations/{id}/: get: operationId: v1_logistics_stations_retrieve + description: A viewset for handling CRUD operations on RailwayStation objects. parameters: - in: path name: id @@ -858,6 +933,7 @@ paths: description: '' put: operationId: v1_logistics_stations_update + description: A viewset for handling CRUD operations on RailwayStation objects. parameters: - in: path name: id @@ -890,6 +966,7 @@ paths: description: '' patch: operationId: v1_logistics_stations_partial_update + description: A viewset for handling CRUD operations on RailwayStation objects. parameters: - in: path name: id @@ -921,6 +998,7 @@ paths: description: '' delete: operationId: v1_logistics_stations_destroy + description: A viewset for handling CRUD operations on RailwayStation objects. parameters: - in: path name: id @@ -938,6 +1016,7 @@ paths: /api/v1/users/departments/: get: operationId: v1_users_departments_list + description: View to list departments. tags: - v1 security: @@ -955,6 +1034,7 @@ paths: /api/v1/users/edit/: get: operationId: v1_users_edit_retrieve + description: View to handle updating user data. tags: - v1 security: @@ -968,6 +1048,7 @@ paths: description: '' put: operationId: v1_users_edit_update + description: View to handle updating user data. tags: - v1 requestBody: @@ -993,6 +1074,7 @@ paths: description: '' patch: operationId: v1_users_edit_partial_update + description: View to handle updating user data. tags: - v1 requestBody: @@ -1018,6 +1100,7 @@ paths: /api/v1/users/edit_password/: get: operationId: v1_users_edit_password_retrieve + description: View to handle changing user password. tags: - v1 security: @@ -1031,6 +1114,7 @@ paths: description: '' put: operationId: v1_users_edit_password_update + description: View to handle changing user password. tags: - v1 requestBody: @@ -1056,6 +1140,7 @@ paths: description: '' patch: operationId: v1_users_edit_password_partial_update + description: View to handle changing user password. tags: - v1 requestBody: @@ -1081,8 +1166,11 @@ paths: /api/v1/users/list_user_files/: get: operationId: v1_users_list_user_files_retrieve - description: Responsible for displaying a list of documents of an authorized - user. + description: |- + Handle GET requests to list user's files. + + Returns: + - List of user's files or error message. tags: - v1 security: @@ -1094,8 +1182,14 @@ paths: post: operationId: v1_users_login_create description: |- - Takes a set of user credentials and returns an access and refresh JSON web - token pair to prove the authentication of those credentials. + Handle POST requests to obtain JWT. + + Params: + - email: User's email address. + - password: User's password. + + Returns: + - JSON response with JWT or error message. tags: - v1 requestBody: @@ -1120,6 +1214,14 @@ paths: /api/v1/users/logout/: post: operationId: v1_users_logout_create + description: |- + Handle POST requests to logout user. + + Params: + - refresh_token: JWT refresh token. + + Returns: + - HTTP 205 Reset Content on success, or HTTP 400 Bad Request on failure. tags: - v1 requestBody: @@ -1225,6 +1327,7 @@ paths: /api/v1/users/positions/: get: operationId: v1_users_positions_list + description: View to list positions. tags: - v1 security: @@ -1242,6 +1345,7 @@ paths: /api/v1/users/registration/: post: operationId: v1_users_registration_create + description: View to handle user registration. tags: - v1 requestBody: @@ -1297,6 +1401,10 @@ components: schemas: City: type: object + description: |- + Serializer for City model. + + Serializes all fields of the City model. properties: id: type: integer @@ -1315,6 +1423,22 @@ components: - region Client: type: object + description: |- + Serializer for the Client model. + + Serializes all fields of the Client model + including nested serialization of 'director_position'. + Adds 'destination_city' and 'railway_station' as CharField serializers. + Sets the current authenticated user as the value for the 'user' field using HiddenField. + + Fields: + - id: IntegerField + - director_position: Nested serialization using DirectorPositionSerializer + - destination_city: CharField for destination city name + - railway_station: CharField for railway station name + - user: HiddenField that defaults to the current authenticated user + + Note: 'user' field is automatically populated with the current user making the request. properties: id: type: integer @@ -1380,6 +1504,7 @@ components: - railway_station CustomUser: type: object + description: Serializer for creating a new user record. properties: id: type: integer @@ -1425,6 +1550,7 @@ components: - phone_number_work DataDoc: type: object + description: A serializer for document data. properties: delivery_type: type: string @@ -1450,12 +1576,14 @@ components: - items Department: type: object + description: Serializer for Department model. properties: id: type: integer readOnly: true department: type: string + title: Название департамента maxLength: 50 required: - department @@ -1498,12 +1626,17 @@ components: * `ДВЖД` - Дальневосточная ж/д DirectorPosition: type: object + description: |- + Serializer for the DirectorPosition model. + + Serializes the 'id' and 'director_position' fields of DirectorPosition. properties: id: type: integer readOnly: true director_position: type: string + title: Должность директора maxLength: 40 required: - director_position @@ -1518,6 +1651,7 @@ components: - email Factory: type: object + description: Serializer for Factory model. properties: id: type: integer @@ -1562,6 +1696,7 @@ components: - short_name Goods: type: object + description: Serializer for serializing/deserializing Product objects. properties: id: type: integer @@ -1585,6 +1720,7 @@ components: - price Logout: type: object + description: Serializer for logging out a user. properties: refresh_token: type: string @@ -1592,6 +1728,7 @@ components: - refresh_token OrderItem: type: object + description: A serializer for order items. properties: product_id: type: integer @@ -1619,6 +1756,10 @@ components: - token PatchedCity: type: object + description: |- + Serializer for City model. + + Serializes all fields of the City model. properties: id: type: integer @@ -1633,6 +1774,22 @@ components: maxLength: 100 PatchedClient: type: object + description: |- + Serializer for the Client model. + + Serializes all fields of the Client model + including nested serialization of 'director_position'. + Adds 'destination_city' and 'railway_station' as CharField serializers. + Sets the current authenticated user as the value for the 'user' field using HiddenField. + + Fields: + - id: IntegerField + - director_position: Nested serialization using DirectorPositionSerializer + - destination_city: CharField for destination city name + - railway_station: CharField for railway station name + - user: HiddenField that defaults to the current authenticated user + + Note: 'user' field is automatically populated with the current user making the request. properties: id: type: integer @@ -1689,6 +1846,7 @@ components: maxLength: 50 PatchedGoods: type: object + description: Serializer for serializing/deserializing Product objects. properties: id: type: integer @@ -1706,6 +1864,7 @@ components: title: Цена, руб./тн PatchedRailwayStation: type: object + description: Serializer for RailwayStation model. properties: id: type: integer @@ -1722,6 +1881,11 @@ components: $ref: '#/components/schemas/StationBranchEnum' PatchedTripAuto: type: object + description: |- + Serializer for TripAuto model. + + Serializes all fields of the TripAuto model, and includes + additional fields for departure_city and destination_city. properties: id: type: integer @@ -1737,6 +1901,7 @@ components: title: Цена за рейс, руб./тн PatchedTripRailway: type: object + description: Serializer for TripRailway model. properties: id: type: integer @@ -1751,6 +1916,7 @@ components: minimum: 0 PatchedUserUpdate: type: object + description: Serializer for updating existing user records. properties: id: type: integer @@ -1782,6 +1948,7 @@ components: maxLength: 128 PatchedUserUpdatePassword: type: object + description: Serializer for updating user password. properties: old_password: type: string @@ -1794,18 +1961,21 @@ components: writeOnly: true Position: type: object + description: Serializer for Position model. properties: id: type: integer readOnly: true position: type: string + title: Название позиции maxLength: 30 required: - id - position RailwayStation: type: object + description: Serializer for RailwayStation model. properties: id: type: integer @@ -1901,6 +2071,11 @@ components: - refresh TripAuto: type: object + description: |- + Serializer for TripAuto model. + + Serializes all fields of the TripAuto model, and includes + additional fields for departure_city and destination_city. properties: id: type: integer @@ -1921,6 +2096,7 @@ components: - id TripRailway: type: object + description: Serializer for TripRailway model. properties: id: type: integer @@ -1940,6 +2116,7 @@ components: - id UserUpdate: type: object + description: Serializer for updating existing user records. properties: id: type: integer @@ -1977,6 +2154,7 @@ components: - phone_number_work UserUpdatePassword: type: object + description: Serializer for updating user password. properties: old_password: type: string