initial commit
This commit is contained in:
0
core/apps/contracts/__init__.py
Normal file
0
core/apps/contracts/__init__.py
Normal file
4
core/apps/contracts/admin/__init__.py
Normal file
4
core/apps/contracts/admin/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .attached_files import * # noqa
|
||||
from .contracts import * # noqa
|
||||
from .file_contents import * # noqa
|
||||
from .owners import * # noqa
|
||||
24
core/apps/contracts/admin/attached_files.py
Normal file
24
core/apps/contracts/admin/attached_files.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from django.contrib import admin
|
||||
from unfold.admin import ModelAdmin # type: ignore
|
||||
|
||||
from core.apps.contracts.models import ContractAttachedFileModel
|
||||
|
||||
|
||||
@admin.register(ContractAttachedFileModel)
|
||||
class FileAdmin(ModelAdmin):
|
||||
list_display = (
|
||||
"name",
|
||||
"contract",
|
||||
"allowed_types",
|
||||
"created_at",
|
||||
)
|
||||
search_fields = (
|
||||
"name",
|
||||
"contract",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
list_display_links = (
|
||||
"name",
|
||||
)
|
||||
|
||||
26
core/apps/contracts/admin/contracts.py
Normal file
26
core/apps/contracts/admin/contracts.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from django.contrib import admin
|
||||
from unfold.admin import ModelAdmin # type: ignore
|
||||
|
||||
from core.apps.contracts.models import ContractModel
|
||||
|
||||
|
||||
@admin.register(ContractModel)
|
||||
class ContractAdmin(ModelAdmin):
|
||||
list_display = (
|
||||
"name",
|
||||
"identifier",
|
||||
"file_permissions",
|
||||
"created_at",
|
||||
)
|
||||
|
||||
search_fields = (
|
||||
"name",
|
||||
"identifier",
|
||||
"owners",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
|
||||
list_display_links = (
|
||||
"name",
|
||||
)
|
||||
26
core/apps/contracts/admin/file_contents.py
Normal file
26
core/apps/contracts/admin/file_contents.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from django.contrib import admin
|
||||
from unfold.admin import ModelAdmin # type: ignore
|
||||
|
||||
from core.apps.contracts.models import ContractFileContentModel
|
||||
|
||||
|
||||
@admin.register(ContractFileContentModel)
|
||||
class FilecontentAdmin(ModelAdmin):
|
||||
list_display = (
|
||||
"owner_name",
|
||||
"contract_owner",
|
||||
"file",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
search_fields = (
|
||||
"contract_owner"
|
||||
"file",
|
||||
"content",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"document_url",
|
||||
)
|
||||
list_display_links = (
|
||||
"owner_name",
|
||||
)
|
||||
78
core/apps/contracts/admin/owners.py
Normal file
78
core/apps/contracts/admin/owners.py
Normal file
@@ -0,0 +1,78 @@
|
||||
from django.contrib import admin
|
||||
from unfold.admin import ModelAdmin # type: ignore
|
||||
|
||||
from core.apps.contracts.models import (
|
||||
ContractOwnerModel,
|
||||
LegalEntityModel,
|
||||
IndividualModel,
|
||||
)
|
||||
|
||||
|
||||
@admin.register(IndividualModel)
|
||||
class IndividualAdmin(ModelAdmin):
|
||||
list_display = (
|
||||
"full_name",
|
||||
"individual_code",
|
||||
"phone",
|
||||
"use_face_id",
|
||||
"created_at",
|
||||
)
|
||||
|
||||
search_fields = (
|
||||
"full_name",
|
||||
"iin_code",
|
||||
"person_code",
|
||||
"phone",
|
||||
"use_face_id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
|
||||
list_display_links = (
|
||||
"full_name",
|
||||
)
|
||||
|
||||
|
||||
@admin.register(LegalEntityModel)
|
||||
class LegalentityAdmin(ModelAdmin):
|
||||
list_display = (
|
||||
"name",
|
||||
"role",
|
||||
"entity_code",
|
||||
"phone",
|
||||
"created_at",
|
||||
)
|
||||
search_fields = (
|
||||
"name",
|
||||
"role",
|
||||
"bin_code",
|
||||
"identifier",
|
||||
"phone",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
|
||||
list_display_links = (
|
||||
"name",
|
||||
)
|
||||
|
||||
|
||||
@admin.register(ContractOwnerModel)
|
||||
class OwnerAdmin(ModelAdmin):
|
||||
list_display = (
|
||||
"owner_identity",
|
||||
"owner_name",
|
||||
"status",
|
||||
"created_at"
|
||||
)
|
||||
|
||||
search_fields = (
|
||||
"owner_name",
|
||||
"status",
|
||||
"created_at",
|
||||
"updated_at"
|
||||
)
|
||||
|
||||
list_display_links = (
|
||||
"owner_identity",
|
||||
)
|
||||
6
core/apps/contracts/apps.py
Normal file
6
core/apps/contracts/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ModuleConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "core.apps.contracts"
|
||||
0
core/apps/contracts/choices/__init__.py
Normal file
0
core/apps/contracts/choices/__init__.py
Normal file
12
core/apps/contracts/choices/contracts.py
Normal file
12
core/apps/contracts/choices/contracts.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class ContractOwnerStatus(models.TextChoices):
|
||||
"""
|
||||
Owner status choices.
|
||||
"""
|
||||
|
||||
ACCEPTED = "accepted", _("Accepted")
|
||||
REJECTED = "rejected", _("Rejected")
|
||||
PENDING = "pending", _("Pending")
|
||||
4
core/apps/contracts/filters/__init__.py
Normal file
4
core/apps/contracts/filters/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .attached_files import * # noqa
|
||||
from .contracts import * # noqa
|
||||
from .file_contents import * # noqa
|
||||
from .owners import * # noqa
|
||||
13
core/apps/contracts/filters/attached_files.py
Normal file
13
core/apps/contracts/filters/attached_files.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from django_filters import rest_framework as filters
|
||||
|
||||
from core.apps.contracts.models import ContractattachedfileModel
|
||||
|
||||
|
||||
class ContractattachedfileFilter(filters.FilterSet):
|
||||
# name = filters.CharFilter(field_name="name", lookup_expr="icontains")
|
||||
|
||||
class Meta:
|
||||
model = ContractattachedfileModel
|
||||
fields = [
|
||||
"name",
|
||||
]
|
||||
13
core/apps/contracts/filters/contracts.py
Normal file
13
core/apps/contracts/filters/contracts.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from django_filters import rest_framework as filters
|
||||
|
||||
from core.apps.contracts.models import ContractModel
|
||||
|
||||
|
||||
class ContractFilter(filters.FilterSet):
|
||||
# name = filters.CharFilter(field_name="name", lookup_expr="icontains")
|
||||
|
||||
class Meta:
|
||||
model = ContractModel
|
||||
fields = [
|
||||
"name",
|
||||
]
|
||||
13
core/apps/contracts/filters/file_contents.py
Normal file
13
core/apps/contracts/filters/file_contents.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from django_filters import rest_framework as filters
|
||||
|
||||
from core.apps.contracts.models import ContractfilecontentModel
|
||||
|
||||
|
||||
class ContractfilecontentFilter(filters.FilterSet):
|
||||
# name = filters.CharFilter(field_name="name", lookup_expr="icontains")
|
||||
|
||||
class Meta:
|
||||
model = ContractfilecontentModel
|
||||
fields = [
|
||||
"name",
|
||||
]
|
||||
13
core/apps/contracts/filters/owners.py
Normal file
13
core/apps/contracts/filters/owners.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from django_filters import rest_framework as filters
|
||||
|
||||
from core.apps.contracts.models import ContractownerModel
|
||||
|
||||
|
||||
class ContractownerFilter(filters.FilterSet):
|
||||
# name = filters.CharFilter(field_name="name", lookup_expr="icontains")
|
||||
|
||||
class Meta:
|
||||
model = ContractownerModel
|
||||
fields = [
|
||||
"name",
|
||||
]
|
||||
4
core/apps/contracts/forms/__init__.py
Normal file
4
core/apps/contracts/forms/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .attached_files import * # noqa
|
||||
from .contracts import * # noqa
|
||||
from .file_contents import * # noqa
|
||||
from .owners import * # noqa
|
||||
10
core/apps/contracts/forms/attached_files.py
Normal file
10
core/apps/contracts/forms/attached_files.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django import forms
|
||||
|
||||
from core.apps.contracts.models import ContractattachedfileModel
|
||||
|
||||
|
||||
class ContractattachedfileForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = ContractattachedfileModel
|
||||
fields = "__all__"
|
||||
10
core/apps/contracts/forms/contracts.py
Normal file
10
core/apps/contracts/forms/contracts.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django import forms
|
||||
|
||||
from core.apps.contracts.models import ContractModel
|
||||
|
||||
|
||||
class ContractForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = ContractModel
|
||||
fields = "__all__"
|
||||
10
core/apps/contracts/forms/file_contents.py
Normal file
10
core/apps/contracts/forms/file_contents.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django import forms
|
||||
|
||||
from core.apps.contracts.models import ContractfilecontentModel
|
||||
|
||||
|
||||
class ContractfilecontentForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = ContractfilecontentModel
|
||||
fields = "__all__"
|
||||
10
core/apps/contracts/forms/owners.py
Normal file
10
core/apps/contracts/forms/owners.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django import forms
|
||||
|
||||
from core.apps.contracts.models import ContractownerModel
|
||||
|
||||
|
||||
class ContractownerForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = ContractownerModel
|
||||
fields = "__all__"
|
||||
347
core/apps/contracts/migrations/0001_initial.py
Normal file
347
core/apps/contracts/migrations/0001_initial.py
Normal file
@@ -0,0 +1,347 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-01 09:53
|
||||
|
||||
import core.apps.contracts.validators.attached_files
|
||||
import core.apps.contracts.validators.owners
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="LegalEntityModel",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True, verbose_name="Created At")),
|
||||
("updated_at", models.DateTimeField(auto_now=True, verbose_name="Updated At")),
|
||||
(
|
||||
"name",
|
||||
models.CharField(
|
||||
max_length=255,
|
||||
validators=[core.apps.contracts.validators.owners.name_validator],
|
||||
verbose_name="Name",
|
||||
),
|
||||
),
|
||||
("role", models.CharField(verbose_name="Role")),
|
||||
(
|
||||
"bin_code",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
max_length=14,
|
||||
null=True,
|
||||
validators=[core.apps.contracts.validators.owners.bin_code_validator],
|
||||
verbose_name="BIN code",
|
||||
),
|
||||
),
|
||||
("identifier", models.CharField(blank=True, max_length=255, null=True, verbose_name="Identifier")),
|
||||
(
|
||||
"phone",
|
||||
models.CharField(
|
||||
max_length=25,
|
||||
validators=[
|
||||
django.core.validators.RegexValidator(
|
||||
message="Enter a valid international phone number (E.164 format, e.g., +14155552671).",
|
||||
regex="^\\+?[1-9]\\d{1,14}$",
|
||||
)
|
||||
],
|
||||
verbose_name="Phone",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Legal Entity",
|
||||
"verbose_name_plural": "Legal Entities",
|
||||
"db_table": "legal_entities",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="ContractModel",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True, verbose_name="Created At")),
|
||||
("updated_at", models.DateTimeField(auto_now=True, verbose_name="Updated At")),
|
||||
(
|
||||
"name",
|
||||
models.CharField(
|
||||
max_length=255,
|
||||
validators=[
|
||||
django.core.validators.RegexValidator(
|
||||
message="Enter a valid contract name. Use letters, numbers, spaces, and limited punctuation (e.g., &, -, (), ., ', \"). Length must be 3 to 100 characters.",
|
||||
regex="^[A-Za-z0-9À-ÿ&()\\-.,'\\\" ]{3,100}$",
|
||||
)
|
||||
],
|
||||
verbose_name="name",
|
||||
),
|
||||
),
|
||||
("identifier", models.CharField(verbose_name="Identifier")),
|
||||
("allow_add_files", models.BooleanField(default=False)),
|
||||
("allow_delete_files", models.BooleanField(default=False)),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Contract",
|
||||
"verbose_name_plural": "Contracts",
|
||||
"db_table": "contracts",
|
||||
"indexes": [models.Index(fields=["name"], name="contracts_name_inx")],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="ContractAttachedFileModel",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True, verbose_name="Created At")),
|
||||
("updated_at", models.DateTimeField(auto_now=True, verbose_name="Updated At")),
|
||||
(
|
||||
"name",
|
||||
models.CharField(
|
||||
max_length=150,
|
||||
validators=[
|
||||
django.core.validators.MinLengthValidator(
|
||||
3, message="File name must be at least 3 characters long."
|
||||
),
|
||||
django.core.validators.MaxLengthValidator(
|
||||
150, message="File name must be at most 150 characters long."
|
||||
),
|
||||
core.apps.contracts.validators.attached_files.starts_with_letter_validator,
|
||||
django.core.validators.RegexValidator(
|
||||
code="invalid_characters",
|
||||
message="File name can only contain letters, digits, spaces, dots, underscores, or hyphens.",
|
||||
regex="^[A-Za-z0-9\\s._-]+$",
|
||||
),
|
||||
],
|
||||
verbose_name="name",
|
||||
),
|
||||
),
|
||||
("allow_pdf", models.BooleanField(default=True)),
|
||||
("allow_word", models.BooleanField(default=True)),
|
||||
("allow_image", models.BooleanField(default=True)),
|
||||
(
|
||||
"contract",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="attached_files",
|
||||
to="contracts.contractmodel",
|
||||
verbose_name="Contract",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Contract Attached File",
|
||||
"verbose_name_plural": "Contract Attached Files",
|
||||
"db_table": "contract_attached_files",
|
||||
"unique_together": {("name", "contract")},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="ContractOwnerModel",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True, verbose_name="Created At")),
|
||||
("updated_at", models.DateTimeField(auto_now=True, verbose_name="Updated At")),
|
||||
(
|
||||
"status",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
choices=[("accepted", "Accepted"), ("rejected", "Rejected"), ("pending", "Pending")],
|
||||
default="pending",
|
||||
max_length=255,
|
||||
verbose_name="Owner Status",
|
||||
),
|
||||
),
|
||||
(
|
||||
"contract",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="owners",
|
||||
to="contracts.contractmodel",
|
||||
verbose_name="Contract",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Contract Owner",
|
||||
"verbose_name_plural": "Contract Owners",
|
||||
"db_table": "contract_owners",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="ContractFileContentModel",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True, verbose_name="Created At")),
|
||||
("updated_at", models.DateTimeField(auto_now=True, verbose_name="Updated At")),
|
||||
("document", models.FileField(upload_to="", verbose_name="Document")),
|
||||
(
|
||||
"file",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="contents",
|
||||
to="contracts.contractattachedfilemodel",
|
||||
verbose_name="File",
|
||||
),
|
||||
),
|
||||
(
|
||||
"contract_owner",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="contracts.contractownermodel",
|
||||
verbose_name="Contract Owner",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Contract File Content",
|
||||
"verbose_name_plural": "Contract File Contents",
|
||||
"db_table": "contract_file_contents",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="IndividualModel",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True, verbose_name="Created At")),
|
||||
("updated_at", models.DateTimeField(auto_now=True, verbose_name="Updated At")),
|
||||
(
|
||||
"full_name",
|
||||
models.CharField(
|
||||
max_length=512,
|
||||
validators=[
|
||||
django.core.validators.RegexValidator(
|
||||
message="Invalid Full Name, Please enter Full Name in the format: <first name> <last name> <father name>",
|
||||
regex="^[A-Z][a-z]+ [A-Z][a-z]+ [A-Z][a-z]+$",
|
||||
)
|
||||
],
|
||||
verbose_name="name",
|
||||
),
|
||||
),
|
||||
(
|
||||
"iin_code",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
max_length=14,
|
||||
null=True,
|
||||
unique=True,
|
||||
validators=[
|
||||
django.core.validators.RegexValidator(
|
||||
code="invalid_iin",
|
||||
message="IIN code must consist of exactly 14 digits.",
|
||||
regex="^\\d{14}$",
|
||||
)
|
||||
],
|
||||
verbose_name="IIN code",
|
||||
),
|
||||
),
|
||||
(
|
||||
"person_code",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
max_length=64,
|
||||
null=True,
|
||||
unique=True,
|
||||
validators=[
|
||||
django.core.validators.RegexValidator(
|
||||
code="invalid_person_code",
|
||||
message="Person Code must be 3 to 64 characters long and contain only letters, digits, dashes, or underscores.",
|
||||
regex="^[A-Za-z0-9_-]{3,64}$",
|
||||
)
|
||||
],
|
||||
verbose_name="Person Code (if no IIN code)",
|
||||
),
|
||||
),
|
||||
(
|
||||
"phone",
|
||||
models.CharField(
|
||||
validators=[
|
||||
django.core.validators.RegexValidator(
|
||||
message="Enter a valid international phone number (E.164 format, e.g., +14155552671).",
|
||||
regex="^\\+?[1-9]\\d{1,14}$",
|
||||
)
|
||||
],
|
||||
verbose_name="Phone",
|
||||
),
|
||||
),
|
||||
("use_face_id", models.BooleanField(default=False, verbose_name="Use FaceID")),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Individual",
|
||||
"verbose_name_plural": "Individuals",
|
||||
"db_table": "individuals",
|
||||
"indexes": [
|
||||
models.Index(fields=["full_name"], name="individuals_fullname_inx"),
|
||||
models.Index(fields=["iin_code"], name="individuals_iin_inx"),
|
||||
models.Index(fields=["person_code"], name="individuals_code_inx"),
|
||||
models.Index(fields=["phone"], name="individuals_phone_inx"),
|
||||
],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="contractownermodel",
|
||||
name="individual",
|
||||
field=models.OneToOneField(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="owner",
|
||||
to="contracts.individualmodel",
|
||||
verbose_name="Individual",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="contractownermodel",
|
||||
name="legal_entity",
|
||||
field=models.OneToOneField(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="owner",
|
||||
to="contracts.legalentitymodel",
|
||||
verbose_name="Legal Entity",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="contractownermodel",
|
||||
constraint=models.UniqueConstraint(fields=("individual", "contract"), name="unique_individual_contract"),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="contractownermodel",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("legal_entity", "contract"), name="unique_legal_entity_contract"
|
||||
),
|
||||
),
|
||||
]
|
||||
0
core/apps/contracts/migrations/__init__.py
Normal file
0
core/apps/contracts/migrations/__init__.py
Normal file
4
core/apps/contracts/models/__init__.py
Normal file
4
core/apps/contracts/models/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .attached_files import * # noqa
|
||||
from .contracts import * # noqa
|
||||
from .file_contents import * # noqa
|
||||
from .owners import * # noqa
|
||||
110
core/apps/contracts/models/attached_files.py
Normal file
110
core/apps/contracts/models/attached_files.py
Normal file
@@ -0,0 +1,110 @@
|
||||
from django.db import models
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from functools import cache
|
||||
|
||||
from django.core.validators import (
|
||||
MinLengthValidator,
|
||||
MaxLengthValidator,
|
||||
)
|
||||
|
||||
from core.utils.base_model import UUIDPrimaryKeyBaseModel
|
||||
from .contracts import ContractModel
|
||||
from core.apps.contracts.validators.attached_files import (
|
||||
ContractAttachedFileValidator,
|
||||
allowed_chars_validator,
|
||||
starts_with_letter_validator,
|
||||
)
|
||||
|
||||
|
||||
ALLOWED_FILE_EXTENTIONS: dict[str, list[str]] = {
|
||||
'pdf': ['.pdf'],
|
||||
'word': ['.doc', '.docx'],
|
||||
'image': ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'],
|
||||
}
|
||||
|
||||
class ContractAttachedFileModel(UUIDPrimaryKeyBaseModel):
|
||||
name = models.CharField(
|
||||
_("name"),
|
||||
max_length=150,
|
||||
validators=[
|
||||
MinLengthValidator(
|
||||
3,
|
||||
message=_(
|
||||
"File name must be at " \
|
||||
"least 3 characters long."
|
||||
)
|
||||
),
|
||||
MaxLengthValidator(
|
||||
150,
|
||||
message=_(
|
||||
"File name must be at " \
|
||||
"most 150 characters long."
|
||||
)
|
||||
),
|
||||
starts_with_letter_validator,
|
||||
allowed_chars_validator,
|
||||
],
|
||||
)
|
||||
|
||||
contract = models.ForeignKey(
|
||||
ContractModel,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_("Contract"),
|
||||
related_name="attached_files",
|
||||
)
|
||||
|
||||
allow_pdf = models.BooleanField(default=True)
|
||||
allow_word = models.BooleanField(default=True)
|
||||
allow_image = models.BooleanField(default=True)
|
||||
|
||||
@property
|
||||
def allowed_types(self) -> str:
|
||||
allowed_types: list[str] = []
|
||||
|
||||
if self.allow_pdf:
|
||||
allowed_types.append("pdf")
|
||||
if self.allow_word:
|
||||
allowed_types.append("word")
|
||||
if self.allow_image:
|
||||
allowed_types.append("image")
|
||||
|
||||
if len(allowed_types) == 3:
|
||||
return "All"
|
||||
return ", ".join(allowed_type.upper() for allowed_type in allowed_types)
|
||||
|
||||
@property
|
||||
@cache
|
||||
def allowed_extensions(self) -> list[str]:
|
||||
extensions: list[str] = []
|
||||
|
||||
if self.allow_pdf:
|
||||
extensions += ALLOWED_FILE_EXTENTIONS['pdf']
|
||||
if self.allow_word:
|
||||
extensions += ALLOWED_FILE_EXTENTIONS['word']
|
||||
if self.allow_image:
|
||||
extensions += ALLOWED_FILE_EXTENTIONS['image']
|
||||
|
||||
return extensions
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def _create_fake(cls):
|
||||
return cls.objects.create(
|
||||
name="mock",
|
||||
)
|
||||
|
||||
def clean(self) -> None:
|
||||
super().clean()
|
||||
validator = ContractAttachedFileValidator()
|
||||
validator()
|
||||
|
||||
class Meta: # type: ignore
|
||||
db_table = "contract_attached_files"
|
||||
|
||||
verbose_name = _("Contract Attached File")
|
||||
verbose_name_plural = _("Contract Attached Files")
|
||||
|
||||
unique_together = ("name", "contract")
|
||||
70
core/apps/contracts/models/contracts.py
Normal file
70
core/apps/contracts/models/contracts.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.utils.base_model import UUIDPrimaryKeyBaseModel
|
||||
from core.apps.contracts.validators.contracts import (
|
||||
ContractValidator,
|
||||
name_validator
|
||||
)
|
||||
|
||||
|
||||
class ContractModel(UUIDPrimaryKeyBaseModel):
|
||||
|
||||
name = models.CharField(
|
||||
_("name"),
|
||||
validators=[
|
||||
name_validator,
|
||||
],
|
||||
max_length=255
|
||||
)
|
||||
|
||||
identifier = models.CharField(
|
||||
_("Identifier"),
|
||||
null=False,
|
||||
blank=False
|
||||
)
|
||||
|
||||
allow_add_files = models.BooleanField(default=False)
|
||||
allow_delete_files = models.BooleanField(default=False)
|
||||
|
||||
@property
|
||||
def file_permissions(self) -> str:
|
||||
permissions: list[str] = []
|
||||
|
||||
if self.allow_add_files:
|
||||
permissions.append("add")
|
||||
if self.allow_delete_files:
|
||||
permissions.append("delete")
|
||||
|
||||
if len(permissions) == 2:
|
||||
return "All"
|
||||
|
||||
return ", ".join(permission.capitalize() for permission in permissions)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def _create_fake(cls):
|
||||
return cls.objects.create(
|
||||
name="mock",
|
||||
)
|
||||
|
||||
def clean(self) -> None:
|
||||
super().clean()
|
||||
|
||||
validator = ContractValidator(self)
|
||||
validator()
|
||||
|
||||
class Meta: # type: ignore
|
||||
db_table = "contracts"
|
||||
|
||||
verbose_name = _("Contract")
|
||||
verbose_name_plural = _("Contracts")
|
||||
|
||||
indexes = [
|
||||
models.Index(
|
||||
fields=["name"],
|
||||
name="contracts_name_inx"
|
||||
)
|
||||
]
|
||||
70
core/apps/contracts/models/file_contents.py
Normal file
70
core/apps/contracts/models/file_contents.py
Normal file
@@ -0,0 +1,70 @@
|
||||
import os
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.db.models.fields.files import FieldFile
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from core.utils.base_model import UUIDPrimaryKeyBaseModel
|
||||
from core.apps.contracts.models.attached_files import ContractAttachedFileModel
|
||||
from core.apps.contracts.models.owners import ContractOwnerModel
|
||||
|
||||
|
||||
class ContractFileContentModel(UUIDPrimaryKeyBaseModel):
|
||||
file = models.ForeignKey(
|
||||
ContractAttachedFileModel,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_("File"),
|
||||
related_name="contents",
|
||||
null=False,
|
||||
blank=False,
|
||||
)
|
||||
|
||||
contract_owner = models.ForeignKey(
|
||||
ContractOwnerModel,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_("Contract Owner"),
|
||||
null=False,
|
||||
blank=False,
|
||||
)
|
||||
|
||||
document = models.FileField(
|
||||
_("Document"),
|
||||
null=False,
|
||||
blank=False,
|
||||
)
|
||||
|
||||
@property
|
||||
def owner_name(self) -> str:
|
||||
return self.contract_owner.owner_name
|
||||
|
||||
def __str__(self):
|
||||
return self.file.name
|
||||
|
||||
def validate_file(self, document: FieldFile):
|
||||
try:
|
||||
ext = os.path.splitext(document.name)[1].lower()
|
||||
except IndexError:
|
||||
raise ValidationError(f"Unsupported document name: {document.name}")
|
||||
|
||||
if ext.lower() not in self.file.allowed_extensions:
|
||||
raise ValidationError(f"Unsupported document type: {ext.upper()}")
|
||||
|
||||
return document
|
||||
|
||||
@classmethod
|
||||
def _create_fake(cls):
|
||||
return cls.objects.create(
|
||||
file=ContractAttachedFileModel._create_fake(), # type: ignore
|
||||
owner=ContractOwnerModel._create_fake(), # type: ignore
|
||||
document_url=(
|
||||
"https://img.freepik.com/free-photo/closeup-scarlet-macaw-from-side"
|
||||
"-view-scarlet-macaw-closeup-head_488145-3540.jpg?semt=ais_hybrid&w=740"
|
||||
)
|
||||
)
|
||||
|
||||
class Meta: # type: ignore
|
||||
db_table = "contract_file_contents"
|
||||
verbose_name = _("Contract File Content")
|
||||
verbose_name_plural = _("Contract File Contents")
|
||||
279
core/apps/contracts/models/owners.py
Normal file
279
core/apps/contracts/models/owners.py
Normal file
@@ -0,0 +1,279 @@
|
||||
from typing import Self
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.apps.contracts.models.contracts import ContractModel
|
||||
from core.apps.contracts.choices.contracts import ContractOwnerStatus
|
||||
from core.utils.base_model import UUIDPrimaryKeyBaseModel
|
||||
from core.apps.contracts.validators.owners import (
|
||||
ContractOwnerValidator,
|
||||
LegalEntityValidator,
|
||||
IndividualValidator,
|
||||
name_validator,
|
||||
bin_code_validator,
|
||||
phone_validator,
|
||||
person_code_validator,
|
||||
full_name_validator,
|
||||
iin_code_validator,
|
||||
)
|
||||
|
||||
|
||||
class LegalEntityModel(UUIDPrimaryKeyBaseModel):
|
||||
name = models.CharField(
|
||||
_("Name"),
|
||||
validators=[
|
||||
name_validator,
|
||||
],
|
||||
max_length=255,
|
||||
null=False,
|
||||
blank=False,
|
||||
)
|
||||
|
||||
role = models.CharField(
|
||||
_("Role"),
|
||||
null=False,
|
||||
blank=False
|
||||
)
|
||||
|
||||
bin_code = models.CharField(
|
||||
_("BIN code"),
|
||||
validators=[
|
||||
bin_code_validator,
|
||||
],
|
||||
max_length=14,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
identifier = models.CharField(
|
||||
_("Identifier"),
|
||||
max_length=255,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
|
||||
phone = models.CharField(
|
||||
_("Phone"),
|
||||
validators=[
|
||||
phone_validator
|
||||
],
|
||||
max_length=25,
|
||||
null=False,
|
||||
blank=False
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def _create_fake(cls):
|
||||
return cls.objects.create(
|
||||
name="mock",
|
||||
)
|
||||
|
||||
@property
|
||||
def entity_code(self) -> str:
|
||||
if self.bin_code is not None:
|
||||
return f"BIN code: {self.bin_code}"
|
||||
else:
|
||||
return f"Identifier: {self.identifier}"
|
||||
|
||||
def clean(self) -> None:
|
||||
super().clean()
|
||||
validator = LegalEntityValidator(self)
|
||||
validator()
|
||||
|
||||
class Meta: # type: ignore
|
||||
db_table = "legal_entities"
|
||||
|
||||
verbose_name = _("Legal Entity")
|
||||
verbose_name_plural = _("Legal Entities")
|
||||
|
||||
|
||||
class IndividualModel(UUIDPrimaryKeyBaseModel):
|
||||
|
||||
full_name = models.CharField(
|
||||
_("name"),
|
||||
validators=[
|
||||
full_name_validator,
|
||||
],
|
||||
max_length=512,
|
||||
null=False,
|
||||
blank=False
|
||||
)
|
||||
|
||||
iin_code = models.CharField(
|
||||
_("IIN code"),
|
||||
max_length=14,
|
||||
validators=[
|
||||
iin_code_validator,
|
||||
],
|
||||
null=True,
|
||||
blank=True,
|
||||
unique=True
|
||||
)
|
||||
|
||||
person_code = models.CharField(
|
||||
_("Person Code (if no IIN code)"),
|
||||
validators=[
|
||||
person_code_validator,
|
||||
],
|
||||
max_length=64,
|
||||
null=True,
|
||||
blank=True,
|
||||
unique=True
|
||||
)
|
||||
|
||||
phone = models.CharField(
|
||||
_("Phone"),
|
||||
validators=[
|
||||
phone_validator,
|
||||
],
|
||||
null=False,
|
||||
blank=False
|
||||
)
|
||||
|
||||
use_face_id = models.BooleanField(
|
||||
_("Use FaceID"),
|
||||
null=False,
|
||||
blank=False,
|
||||
default=False
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.full_name
|
||||
|
||||
@classmethod
|
||||
def _create_fake(cls):
|
||||
return cls.objects.create(
|
||||
name="mock",
|
||||
)
|
||||
|
||||
@property
|
||||
def individual_code(self) -> str:
|
||||
if self.iin_code is not None:
|
||||
return f"IIN Code: {self.iin_code}"
|
||||
else:
|
||||
return f"Person Code: {self.person_code}"
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
validator = IndividualValidator(self)
|
||||
validator()
|
||||
|
||||
class Meta: # type: ignore
|
||||
db_table = "individuals"
|
||||
|
||||
verbose_name = _("Individual")
|
||||
verbose_name_plural = _("Individuals")
|
||||
|
||||
indexes = [
|
||||
models.Index(
|
||||
fields=["full_name"],
|
||||
name="individuals_fullname_inx"
|
||||
),
|
||||
models.Index(
|
||||
fields=["iin_code"],
|
||||
name="individuals_iin_inx"
|
||||
),
|
||||
models.Index(
|
||||
fields=["person_code"],
|
||||
name="individuals_code_inx"
|
||||
),
|
||||
models.Index(
|
||||
fields=["phone"],
|
||||
name="individuals_phone_inx"
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class ContractOwnerModel(UUIDPrimaryKeyBaseModel):
|
||||
|
||||
legal_entity = models.OneToOneField(
|
||||
LegalEntityModel,
|
||||
related_name="owner",
|
||||
verbose_name=_("Legal Entity"),
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
|
||||
individual = models.OneToOneField(
|
||||
IndividualModel,
|
||||
related_name="owner",
|
||||
verbose_name=_("Individual"),
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
status = models.CharField(
|
||||
_("Owner Status"),
|
||||
max_length=255,
|
||||
choices=ContractOwnerStatus,
|
||||
default=ContractOwnerStatus.PENDING,
|
||||
null=False,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
contract = models.ForeignKey(
|
||||
ContractModel,
|
||||
verbose_name=_("Contract"),
|
||||
related_name="owners",
|
||||
on_delete=models.PROTECT,
|
||||
null=False,
|
||||
blank=False
|
||||
)
|
||||
|
||||
def clean(self) -> None:
|
||||
super().clean()
|
||||
validator = ContractOwnerValidator(self)
|
||||
validator()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.legal_entity or self.individual)
|
||||
|
||||
@property
|
||||
def owner_name(self) -> str:
|
||||
if self.legal_entity is not None:
|
||||
return str(self.legal_entity)
|
||||
else:
|
||||
return str(self.individual)
|
||||
|
||||
@property
|
||||
def owner_identity(self) -> str:
|
||||
if self.legal_entity is not None:
|
||||
return _("Legal Entity")
|
||||
else:
|
||||
return _("Individual")
|
||||
|
||||
@classmethod
|
||||
def _create_fake(cls, mock_individual: bool = True) -> Self:
|
||||
kwargs: dict[str, IndividualModel | LegalEntityModel] = dict()
|
||||
|
||||
if mock_individual:
|
||||
kwargs["individual"] = IndividualModel._create_fake() # type: ignore
|
||||
else:
|
||||
kwargs["legal_entity"] = LegalEntityModel._create_fake() # type: ignore
|
||||
|
||||
return cls.objects.create(
|
||||
**kwargs
|
||||
)
|
||||
|
||||
class Meta: # type: ignore
|
||||
db_table = "contract_owners"
|
||||
|
||||
verbose_name = _("Contract Owner")
|
||||
verbose_name_plural = _("Contract Owners")
|
||||
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["individual", "contract"],
|
||||
name="unique_individual_contract"
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=["legal_entity", "contract"],
|
||||
name="unique_legal_entity_contract"
|
||||
),
|
||||
]
|
||||
4
core/apps/contracts/permissions/__init__.py
Normal file
4
core/apps/contracts/permissions/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .attached_files import * # noqa
|
||||
from .contracts import * # noqa
|
||||
from .file_contents import * # noqa
|
||||
from .owners import * # noqa
|
||||
12
core/apps/contracts/permissions/attached_files.py
Normal file
12
core/apps/contracts/permissions/attached_files.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
|
||||
class ContractattachedfilePermission(permissions.BasePermission):
|
||||
|
||||
def __init__(self) -> None: ...
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return self
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return True
|
||||
12
core/apps/contracts/permissions/contracts.py
Normal file
12
core/apps/contracts/permissions/contracts.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
|
||||
class ContractPermission(permissions.BasePermission):
|
||||
|
||||
def __init__(self) -> None: ...
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return self
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return True
|
||||
12
core/apps/contracts/permissions/file_contents.py
Normal file
12
core/apps/contracts/permissions/file_contents.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
|
||||
class ContractfilecontentPermission(permissions.BasePermission):
|
||||
|
||||
def __init__(self) -> None: ...
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return self
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return True
|
||||
12
core/apps/contracts/permissions/owners.py
Normal file
12
core/apps/contracts/permissions/owners.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
|
||||
class ContractownerPermission(permissions.BasePermission):
|
||||
|
||||
def __init__(self) -> None: ...
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return self
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return True
|
||||
4
core/apps/contracts/serializers/__init__.py
Normal file
4
core/apps/contracts/serializers/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .attached_files import * # noqa
|
||||
from .contracts import * # noqa
|
||||
from .file_contents import * # noqa
|
||||
from .owners import * # noqa
|
||||
@@ -0,0 +1 @@
|
||||
from .attached_files import * # noqa
|
||||
@@ -0,0 +1,47 @@
|
||||
from rest_framework import serializers # type: ignore
|
||||
|
||||
from core.apps.contracts.models import ContractAttachedFileModel
|
||||
from core.apps.contracts.serializers.file_contents import (
|
||||
RetrieveContractFileContentSerializer
|
||||
)
|
||||
|
||||
|
||||
class BaseContractAttachedFileSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ContractAttachedFileModel
|
||||
fields = "__all__"
|
||||
read_only_fields = (
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
|
||||
|
||||
class ListContractAttachedFileSerializer(BaseContractAttachedFileSerializer):
|
||||
class Meta(BaseContractAttachedFileSerializer.Meta):
|
||||
fields = (
|
||||
"id",
|
||||
"name",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
|
||||
|
||||
class RetrieveContractAttachedFileSerializer(BaseContractAttachedFileSerializer):
|
||||
contents = RetrieveContractFileContentSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta(BaseContractAttachedFileSerializer.Meta):
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class CreateContractAttachedFileSerializer(BaseContractAttachedFileSerializer):
|
||||
class Meta(BaseContractAttachedFileSerializer.Meta): ...
|
||||
|
||||
|
||||
class UpdateContractAttachedFileSerializer(BaseContractAttachedFileSerializer):
|
||||
class Meta(BaseContractAttachedFileSerializer.Meta): ...
|
||||
|
||||
|
||||
class DestroyContractAttachedFileSerializer(BaseContractAttachedFileSerializer):
|
||||
class Meta(BaseContractAttachedFileSerializer.Meta):
|
||||
fields = ["id"]
|
||||
1
core/apps/contracts/serializers/contracts/__init__.py
Normal file
1
core/apps/contracts/serializers/contracts/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .contracts import * # noqa
|
||||
42
core/apps/contracts/serializers/contracts/contracts.py
Normal file
42
core/apps/contracts/serializers/contracts/contracts.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from core.apps.contracts.models import ContractModel
|
||||
|
||||
|
||||
class BaseContractSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ContractModel
|
||||
fields = "__all__"
|
||||
read_only_fields = (
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at"
|
||||
)
|
||||
|
||||
|
||||
class ListContractSerializer(BaseContractSerializer):
|
||||
class Meta(BaseContractSerializer.Meta):
|
||||
fields = (
|
||||
"id",
|
||||
"name",
|
||||
"identifier",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
|
||||
|
||||
class RetrieveContractSerializer(BaseContractSerializer):
|
||||
class Meta(BaseContractSerializer.Meta): ...
|
||||
|
||||
|
||||
class CreateContractSerializer(BaseContractSerializer):
|
||||
class Meta(BaseContractSerializer.Meta): ...
|
||||
|
||||
|
||||
class UpdateContractSerializer(BaseContractSerializer):
|
||||
class Meta(BaseContractSerializer.Meta): ...
|
||||
|
||||
|
||||
class DestroyContractSerializer(BaseContractSerializer):
|
||||
class Meta(BaseContractSerializer.Meta):
|
||||
fields = ["id"]
|
||||
@@ -0,0 +1 @@
|
||||
from .file_contents import * # noqa
|
||||
@@ -0,0 +1,51 @@
|
||||
from rest_framework import serializers # type: ignore
|
||||
from core.apps.contracts.models import ContractFileContentModel
|
||||
|
||||
|
||||
class BaseContractFileContentSerializer(serializers.ModelSerializer):
|
||||
document = serializers.FileField(required=True)
|
||||
|
||||
class Meta:
|
||||
model = ContractFileContentModel
|
||||
fields = "__all__"
|
||||
read_only_fields = (
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
|
||||
|
||||
class ListContractFileContentSerializer(BaseContractFileContentSerializer):
|
||||
class Meta(BaseContractFileContentSerializer.Meta): ...
|
||||
|
||||
|
||||
class RetrieveContractFileContentSerializer(BaseContractFileContentSerializer):
|
||||
class Meta(BaseContractFileContentSerializer.Meta): ...
|
||||
|
||||
|
||||
class CreateContractFileContentSerializer(BaseContractFileContentSerializer):
|
||||
class Meta(BaseContractFileContentSerializer.Meta): ...
|
||||
|
||||
|
||||
class UpdateContractFileContentSerializer(BaseContractFileContentSerializer):
|
||||
class Meta(BaseContractFileContentSerializer.Meta): ...
|
||||
|
||||
|
||||
class DestroyContractFileContentSerializer(BaseContractFileContentSerializer):
|
||||
class Meta(BaseContractFileContentSerializer.Meta):
|
||||
fields = ["id"]
|
||||
|
||||
|
||||
class CreateContractFileContentFromOwnerSerializer(BaseContractFileContentSerializer):
|
||||
class Meta(BaseContractFileContentSerializer.Meta):
|
||||
fields = "__all__"
|
||||
read_only_fields = (
|
||||
*BaseContractFileContentSerializer.Meta.read_only_fields,
|
||||
"file",
|
||||
"contract_owner",
|
||||
)
|
||||
def create(self, validated_data): # type: ignore
|
||||
validated_data["contract_owner_id"] = self.context["owner_id"]
|
||||
validated_data["file_id"] = self.context["file_id"]
|
||||
return super().create(validated_data) # type: ignore
|
||||
|
||||
1
core/apps/contracts/serializers/owners/__init__.py
Normal file
1
core/apps/contracts/serializers/owners/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .owner import * # noqa
|
||||
174
core/apps/contracts/serializers/owners/owner.py
Normal file
174
core/apps/contracts/serializers/owners/owner.py
Normal file
@@ -0,0 +1,174 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from core.apps.contracts.models import (
|
||||
ContractOwnerModel,
|
||||
LegalEntityModel,
|
||||
IndividualModel,
|
||||
)
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
# from core.apps.contracts.serializers.attached_files import (
|
||||
# BaseContractAttachedFileSerializer,
|
||||
# )
|
||||
# from core.apps.contracts.serializers.file_contents import (
|
||||
# BaseContractFileContentSerializer
|
||||
# )
|
||||
|
||||
|
||||
#! TODO fix: BaseContractOwnerSerializer (.create/.update) fix
|
||||
class BaseIndividualSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = IndividualModel
|
||||
fields = "__all__"
|
||||
read_only_fields = (
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
extra_kwargs = {
|
||||
"iin_code": {
|
||||
"required": False
|
||||
},
|
||||
"person_code": {
|
||||
"required": False
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class ListIndividualSerializer(BaseIndividualSerializer):
|
||||
class Meta(BaseIndividualSerializer.Meta):
|
||||
fields = (
|
||||
"id",
|
||||
"full_name",
|
||||
"phone",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
|
||||
|
||||
class BaseLegalEntitySerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = LegalEntityModel
|
||||
fields = "__all__"
|
||||
read_only_fields = (
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
extra_kwargs = {
|
||||
"bin_code": {
|
||||
"required": False
|
||||
},
|
||||
"identifier": {
|
||||
"required": False
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class ListLegalEntitySerializer(BaseLegalEntitySerializer):
|
||||
class Meta(BaseLegalEntitySerializer.Meta):
|
||||
fields = (
|
||||
"id",
|
||||
"name",
|
||||
"phone",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
|
||||
|
||||
class BaseContractOwnerSerializer(serializers.ModelSerializer):
|
||||
legal_entity = BaseLegalEntitySerializer(required=False)
|
||||
individual = BaseIndividualSerializer(required=False)
|
||||
|
||||
class Meta:
|
||||
model = ContractOwnerModel
|
||||
fields = "__all__"
|
||||
read_only_fields = (
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
extra_kwargs = {
|
||||
"legal_entity": {
|
||||
"required": False
|
||||
},
|
||||
"individual": {
|
||||
"required": False
|
||||
}
|
||||
}
|
||||
|
||||
def create(self, validated_data):
|
||||
legal_entity_data = validated_data.pop("legal_entity", None)
|
||||
individual_data = validated_data.pop("individual", None)
|
||||
|
||||
with transaction.atomic():
|
||||
if legal_entity_data is not None:
|
||||
legal_entity_serializer = BaseLegalEntitySerializer(data=legal_entity_data)
|
||||
legal_entity_serializer.is_valid(raise_exception=True)
|
||||
validated_data["legal_entity"] = legal_entity_serializer.save()
|
||||
|
||||
if individual_data is not None:
|
||||
individual_serializer = BaseIndividualSerializer(data=individual_data)
|
||||
individual_serializer.is_valid(raise_exception=True)
|
||||
validated_data["individual"] = individual_serializer.save()
|
||||
|
||||
contract_owner = ContractOwnerModel.objects.create(**validated_data)
|
||||
return contract_owner
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
legal_entity_data = validated_data.pop("legal_entity", None)
|
||||
individual_data = validated_data.pop("individual", None)
|
||||
|
||||
with transaction.atomic():
|
||||
if legal_entity_data is not None:
|
||||
if instance.legal_entity:
|
||||
for attr, value in legal_entity_data.items():
|
||||
setattr(instance.legal_entity, attr, value)
|
||||
instance.legal_entity.save()
|
||||
else:
|
||||
legal_entity = LegalEntityModel.objects.create(**legal_entity_data)
|
||||
instance.legal_entity = legal_entity
|
||||
|
||||
if individual_data is not None:
|
||||
if instance.individual:
|
||||
for attr, value in individual_data.items():
|
||||
setattr(instance.individual, attr, value)
|
||||
instance.individual.save()
|
||||
else:
|
||||
individual = IndividualModel.objects.create(**individual_data)
|
||||
instance.individual = individual
|
||||
|
||||
# Update ContractOwnerModel fields
|
||||
for attr, value in validated_data.items():
|
||||
setattr(instance, attr, value)
|
||||
instance.save()
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class ListContractOwnerSerializer(BaseContractOwnerSerializer):
|
||||
legal_entity = ListLegalEntitySerializer(read_only=True)
|
||||
individual = ListIndividualSerializer(read_only=True)
|
||||
|
||||
class Meta(BaseContractOwnerSerializer.Meta): ...
|
||||
|
||||
|
||||
class RetrieveContractOwnerSerializer(BaseContractOwnerSerializer):
|
||||
class Meta(BaseContractOwnerSerializer.Meta): ...
|
||||
|
||||
|
||||
class CreateContractOwnerSerializer(BaseContractOwnerSerializer):
|
||||
class Meta(BaseContractOwnerSerializer.Meta): ...
|
||||
|
||||
|
||||
class UpdateContractOwnerSerializer(BaseContractOwnerSerializer):
|
||||
class Meta(BaseContractOwnerSerializer.Meta): ...
|
||||
|
||||
|
||||
class DestroyContractOwnerSerializer(BaseContractOwnerSerializer):
|
||||
class Meta(BaseContractOwnerSerializer.Meta):
|
||||
fields = ["id"]
|
||||
|
||||
4
core/apps/contracts/signals/__init__.py
Normal file
4
core/apps/contracts/signals/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .attached_files import * # noqa
|
||||
from .contracts import * # noqa
|
||||
from .file_contents import * # noqa
|
||||
from .owners import * # noqa
|
||||
8
core/apps/contracts/signals/attached_files.py
Normal file
8
core/apps/contracts/signals/attached_files.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from core.apps.contracts.models import ContractattachedfileModel
|
||||
|
||||
|
||||
@receiver(post_save, sender=ContractattachedfileModel)
|
||||
def ContractattachedfileSignal(sender, instance, created, **kwargs): ...
|
||||
8
core/apps/contracts/signals/contracts.py
Normal file
8
core/apps/contracts/signals/contracts.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from core.apps.contracts.models import ContractModel
|
||||
|
||||
|
||||
@receiver(post_save, sender=ContractModel)
|
||||
def ContractSignal(sender, instance, created, **kwargs): ...
|
||||
8
core/apps/contracts/signals/file_contents.py
Normal file
8
core/apps/contracts/signals/file_contents.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from core.apps.contracts.models import ContractfilecontentModel
|
||||
|
||||
|
||||
@receiver(post_save, sender=ContractfilecontentModel)
|
||||
def ContractfilecontentSignal(sender, instance, created, **kwargs): ...
|
||||
8
core/apps/contracts/signals/owners.py
Normal file
8
core/apps/contracts/signals/owners.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from core.apps.contracts.models import ContractownerModel
|
||||
|
||||
|
||||
@receiver(post_save, sender=ContractownerModel)
|
||||
def ContractownerSignal(sender, instance, created, **kwargs): ...
|
||||
4
core/apps/contracts/tests/__init__.py
Normal file
4
core/apps/contracts/tests/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .test_attached_files import * # noqa
|
||||
from .test_contracts import * # noqa
|
||||
from .test_file_contents import * # noqa
|
||||
from .test_owners import * # noqa
|
||||
47
core/apps/contracts/tests/test_attached_files.py
Normal file
47
core/apps/contracts/tests/test_attached_files.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core.apps.contracts.models import ContractattachedfileModel
|
||||
|
||||
|
||||
class ContractattachedfileTest(TestCase):
|
||||
|
||||
def _create_data(self):
|
||||
return ContractattachedfileModel._create_fake()
|
||||
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
self.instance = self._create_data()
|
||||
self.urls = {
|
||||
"list": reverse("ContractAttachedFile-list"),
|
||||
"retrieve": reverse("ContractAttachedFile-detail", kwargs={"pk": self.instance.pk}),
|
||||
"retrieve-not-found": reverse("ContractAttachedFile-detail", kwargs={"pk": 1000}),
|
||||
}
|
||||
|
||||
def test_create(self):
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_update(self):
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_partial_update(self):
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_destroy(self):
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_list(self):
|
||||
response = self.client.get(self.urls["list"])
|
||||
self.assertTrue(response.json()["status"])
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_retrieve(self):
|
||||
response = self.client.get(self.urls["retrieve"])
|
||||
self.assertTrue(response.json()["status"])
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_retrieve_not_found(self):
|
||||
response = self.client.get(self.urls["retrieve-not-found"])
|
||||
self.assertFalse(response.json()["status"])
|
||||
self.assertEqual(response.status_code, 404)
|
||||
47
core/apps/contracts/tests/test_contracts.py
Normal file
47
core/apps/contracts/tests/test_contracts.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core.apps.contracts.models import ContractModel
|
||||
|
||||
|
||||
class ContractTest(TestCase):
|
||||
|
||||
def _create_data(self):
|
||||
return ContractModel._create_fake()
|
||||
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
self.instance = self._create_data()
|
||||
self.urls = {
|
||||
"list": reverse("Contract-list"),
|
||||
"retrieve": reverse("Contract-detail", kwargs={"pk": self.instance.pk}),
|
||||
"retrieve-not-found": reverse("Contract-detail", kwargs={"pk": 1000}),
|
||||
}
|
||||
|
||||
def test_create(self):
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_update(self):
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_partial_update(self):
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_destroy(self):
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_list(self):
|
||||
response = self.client.get(self.urls["list"])
|
||||
self.assertTrue(response.json()["status"])
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_retrieve(self):
|
||||
response = self.client.get(self.urls["retrieve"])
|
||||
self.assertTrue(response.json()["status"])
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_retrieve_not_found(self):
|
||||
response = self.client.get(self.urls["retrieve-not-found"])
|
||||
self.assertFalse(response.json()["status"])
|
||||
self.assertEqual(response.status_code, 404)
|
||||
47
core/apps/contracts/tests/test_file_contents.py
Normal file
47
core/apps/contracts/tests/test_file_contents.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core.apps.contracts.models import ContractfilecontentModel
|
||||
|
||||
|
||||
class ContractfilecontentTest(TestCase):
|
||||
|
||||
def _create_data(self):
|
||||
return ContractfilecontentModel._create_fake()
|
||||
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
self.instance = self._create_data()
|
||||
self.urls = {
|
||||
"list": reverse("ContractFileContent-list"),
|
||||
"retrieve": reverse("ContractFileContent-detail", kwargs={"pk": self.instance.pk}),
|
||||
"retrieve-not-found": reverse("ContractFileContent-detail", kwargs={"pk": 1000}),
|
||||
}
|
||||
|
||||
def test_create(self):
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_update(self):
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_partial_update(self):
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_destroy(self):
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_list(self):
|
||||
response = self.client.get(self.urls["list"])
|
||||
self.assertTrue(response.json()["status"])
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_retrieve(self):
|
||||
response = self.client.get(self.urls["retrieve"])
|
||||
self.assertTrue(response.json()["status"])
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_retrieve_not_found(self):
|
||||
response = self.client.get(self.urls["retrieve-not-found"])
|
||||
self.assertFalse(response.json()["status"])
|
||||
self.assertEqual(response.status_code, 404)
|
||||
47
core/apps/contracts/tests/test_owners.py
Normal file
47
core/apps/contracts/tests/test_owners.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core.apps.contracts.models import ContractownerModel
|
||||
|
||||
|
||||
class ContractownerTest(TestCase):
|
||||
|
||||
def _create_data(self):
|
||||
return ContractownerModel._create_fake()
|
||||
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
self.instance = self._create_data()
|
||||
self.urls = {
|
||||
"list": reverse("ContractOwner-list"),
|
||||
"retrieve": reverse("ContractOwner-detail", kwargs={"pk": self.instance.pk}),
|
||||
"retrieve-not-found": reverse("ContractOwner-detail", kwargs={"pk": 1000}),
|
||||
}
|
||||
|
||||
def test_create(self):
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_update(self):
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_partial_update(self):
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_destroy(self):
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_list(self):
|
||||
response = self.client.get(self.urls["list"])
|
||||
self.assertTrue(response.json()["status"])
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_retrieve(self):
|
||||
response = self.client.get(self.urls["retrieve"])
|
||||
self.assertTrue(response.json()["status"])
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_retrieve_not_found(self):
|
||||
response = self.client.get(self.urls["retrieve-not-found"])
|
||||
self.assertFalse(response.json()["status"])
|
||||
self.assertEqual(response.status_code, 404)
|
||||
4
core/apps/contracts/translation/__init__.py
Normal file
4
core/apps/contracts/translation/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .attached_files import * # noqa
|
||||
from .contracts import * # noqa
|
||||
from .file_contents import * # noqa
|
||||
from .owners import * # noqa
|
||||
8
core/apps/contracts/translation/attached_files.py
Normal file
8
core/apps/contracts/translation/attached_files.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from modeltranslation.translator import TranslationOptions, register
|
||||
|
||||
from core.apps.contracts.models import ContractattachedfileModel
|
||||
|
||||
|
||||
@register(ContractattachedfileModel)
|
||||
class ContractattachedfileTranslation(TranslationOptions):
|
||||
fields = []
|
||||
8
core/apps/contracts/translation/contracts.py
Normal file
8
core/apps/contracts/translation/contracts.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from modeltranslation.translator import TranslationOptions, register
|
||||
|
||||
from core.apps.contracts.models import ContractModel
|
||||
|
||||
|
||||
@register(ContractModel)
|
||||
class ContractTranslation(TranslationOptions):
|
||||
fields = []
|
||||
8
core/apps/contracts/translation/file_contents.py
Normal file
8
core/apps/contracts/translation/file_contents.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from modeltranslation.translator import TranslationOptions, register
|
||||
|
||||
from core.apps.contracts.models import ContractfilecontentModel
|
||||
|
||||
|
||||
@register(ContractfilecontentModel)
|
||||
class ContractfilecontentTranslation(TranslationOptions):
|
||||
fields = []
|
||||
8
core/apps/contracts/translation/owners.py
Normal file
8
core/apps/contracts/translation/owners.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from modeltranslation.translator import TranslationOptions, register
|
||||
|
||||
from core.apps.contracts.models import ContractownerModel
|
||||
|
||||
|
||||
@register(ContractownerModel)
|
||||
class ContractownerTranslation(TranslationOptions):
|
||||
fields = []
|
||||
37
core/apps/contracts/urls.py
Normal file
37
core/apps/contracts/urls.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter # type: ignore
|
||||
|
||||
from . import views
|
||||
|
||||
router = DefaultRouter()
|
||||
|
||||
router.register(r"contract-attached-files", views.ContractAttachedFileView, "contract-attached-files") # type: ignore
|
||||
router.register(r"contracts", views.ContractView, "contracts") # type: ignore
|
||||
router.register(r"contract-file-contents", views.ContractFileContentView, "contract-file-contents") # type: ignore
|
||||
router.register(r"contract-owners", views.ContractOwnerView, "contract-owners") # type: ignore
|
||||
router.register(r"contract-owners", views.ContractOwnerFileViewSet, "contract-owner-files") # type: ignore
|
||||
|
||||
|
||||
urlpatterns = [ # type: ignore
|
||||
path("", include(router.urls)), # type: ignore
|
||||
path(
|
||||
r"contract-owners/<uuid:owner_id>/contract",
|
||||
views.ContractDetailView.as_view(),
|
||||
name="contract-detail"
|
||||
),
|
||||
path(
|
||||
"contract-owners/<uuid:owner_id>/files/<uuid:file_id>",
|
||||
views.ContractAttachedFileDeleteView.as_view(),
|
||||
name="contract-file-delete"
|
||||
),
|
||||
path(
|
||||
r"contract-owners/<uuid:owner_id>/files/<uuid:file_id>/upload",
|
||||
views.UploadFileContentView.as_view(),
|
||||
name="upload-file-content"
|
||||
),
|
||||
path(
|
||||
r"folders/<uuid:pk>/contracts",
|
||||
views.ListFolderContractsView.as_view(),
|
||||
name="list-folder-contracts"
|
||||
)
|
||||
]
|
||||
4
core/apps/contracts/validators/__init__.py
Normal file
4
core/apps/contracts/validators/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .attached_files import * # noqa
|
||||
from .contracts import * # noqa
|
||||
from .file_contents import * # noqa
|
||||
from .owners import * # noqa
|
||||
34
core/apps/contracts/validators/attached_files.py
Normal file
34
core/apps/contracts/validators/attached_files.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_core.models.base import AbstractBaseModel # type: ignore
|
||||
|
||||
|
||||
def starts_with_letter_validator(value: str) -> None:
|
||||
"""
|
||||
Checks if the value starts with a letter.
|
||||
"""
|
||||
if value[0].isalpha():
|
||||
return
|
||||
|
||||
raise ValidationError(
|
||||
_("File name must start with a letter."),
|
||||
code="invalid_start",
|
||||
)
|
||||
|
||||
|
||||
allowed_chars_validator = RegexValidator(
|
||||
regex=r"^[A-Za-z0-9\s._-]+$",
|
||||
message=_(
|
||||
"File name can only contain letters, " \
|
||||
"digits, spaces, dots, underscores, or hyphens."
|
||||
),
|
||||
code="invalid_characters",
|
||||
)
|
||||
|
||||
|
||||
class ContractAttachedFileValidator:
|
||||
def __init__(self): ...
|
||||
|
||||
def __call__(self):
|
||||
return True
|
||||
23
core/apps/contracts/validators/contracts.py
Normal file
23
core/apps/contracts/validators/contracts.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_core.models.base import AbstractBaseModel # type: ignore
|
||||
|
||||
|
||||
name_validator = RegexValidator(
|
||||
regex=r"^[A-Za-z0-9À-ÿ&()\-.,'\" ]{3,100}$",
|
||||
message=_(
|
||||
"Enter a valid contract name. "
|
||||
"Use letters, numbers, spaces, and "
|
||||
"limited punctuation (e.g., &, -, (), ., ', \"). "
|
||||
"Length must be 3 to 100 characters."
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ContractValidator:
|
||||
def __init__(self, instance: AbstractBaseModel):
|
||||
self.instance = instance
|
||||
|
||||
def __call__(self):
|
||||
return True
|
||||
8
core/apps/contracts/validators/file_contents.py
Normal file
8
core/apps/contracts/validators/file_contents.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# from django.core.exceptions import ValidationError
|
||||
|
||||
|
||||
class ContractfilecontentValidator:
|
||||
def __init__(self): ...
|
||||
|
||||
def __call__(self):
|
||||
return True
|
||||
146
core/apps/contracts/validators/owners.py
Normal file
146
core/apps/contracts/validators/owners.py
Normal file
@@ -0,0 +1,146 @@
|
||||
import re
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_core.models.base import AbstractBaseModel # type: ignore
|
||||
from django.core.validators import RegexValidator
|
||||
|
||||
|
||||
full_name_validator = RegexValidator(
|
||||
regex=r"^[A-Z][a-z]+ [A-Z][a-z]+ [A-Z][a-z]+$",
|
||||
message=_(
|
||||
"Invalid Full Name, Please enter Full Name in the "
|
||||
"format: <first name> <last name> <father name>"
|
||||
)
|
||||
)
|
||||
|
||||
iin_code_validator = RegexValidator(
|
||||
regex=r'^\d{14}$',
|
||||
message=_("IIN code must consist of exactly 14 digits."),
|
||||
code='invalid_iin'
|
||||
)
|
||||
|
||||
|
||||
person_code_validator = RegexValidator(
|
||||
regex=r'^[A-Za-z0-9_-]{3,64}$',
|
||||
message=_(
|
||||
"Person Code must be 3 to 64 characters long and contain"
|
||||
" only letters, digits, dashes, or underscores."
|
||||
),
|
||||
code='invalid_person_code'
|
||||
)
|
||||
|
||||
|
||||
phone_validator = RegexValidator(
|
||||
regex=r'^\+?[1-9]\d{1,14}$',
|
||||
message=_(
|
||||
"Enter a valid international phone number "
|
||||
"(E.164 format, e.g., +14155552671)."
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def bin_code_validator(value: str):
|
||||
if not value.isdigit():
|
||||
raise ValidationError(_("BIN code must contain only digits."))
|
||||
|
||||
if len(value) != 14:
|
||||
raise ValidationError(_("BIN code must be exactly 14 digits long."))
|
||||
|
||||
if not re.match(r"^\d{14}$", value):
|
||||
raise ValidationError(_("Invalid BIN code format."))
|
||||
|
||||
# Optional: check if first 6 digits represent a valid date (YYMMDD)
|
||||
try:
|
||||
from datetime import datetime
|
||||
datetime.strptime(value[:6], "%y%m%d")
|
||||
except ValueError:
|
||||
raise ValidationError(_("BIN code starts with an invalid date."))
|
||||
|
||||
|
||||
def name_validator(value: str):
|
||||
if len(value.strip()) < 3:
|
||||
raise ValidationError(_("Name is too short."))
|
||||
|
||||
if re.fullmatch(r"(.)\1{2,}", value): # e.g., "aaa", "!!!"
|
||||
raise ValidationError(_("Name cannot contain excessive repeated characters."))
|
||||
|
||||
if not re.match(r"^[A-Za-z0-9&\-\.\,\(\)\s]+$", value):
|
||||
raise ValidationError(_("Name contains invalid characters. Only letters, numbers, spaces, and symbols like . , & - ( ) are allowed."))
|
||||
|
||||
if value.lower() in {"test", "company", "name", "example", "sample"}:
|
||||
raise ValidationError(_("This name is too generic. Please choose a more specific name."))
|
||||
|
||||
if value.isupper() or value.islower():
|
||||
raise ValidationError(_("Name must have both uppercase and lowercase letters."))
|
||||
|
||||
if not value[0].isupper():
|
||||
raise ValidationError(_("Name should start with a capital letter."))
|
||||
|
||||
if len(set(value.lower())) < 4:
|
||||
raise ValidationError(_("Name is too repetitive or lacks meaningful structure."))
|
||||
|
||||
|
||||
class IndividualValidator:
|
||||
def __init__(self, instance: AbstractBaseModel):
|
||||
self.instance = instance
|
||||
|
||||
def __call__(self):
|
||||
if (
|
||||
self.instance.iin_code is None # type: ignore
|
||||
and self.instance.person_code is None # type: ignore
|
||||
):
|
||||
raise ValidationError(_(
|
||||
"Either IIN code or Person Code must be provided."
|
||||
))
|
||||
|
||||
if (
|
||||
self.instance.iin_code is not None # type: ignore
|
||||
and self.instance.person_code is not None # type: ignore
|
||||
):
|
||||
raise ValidationError(_(
|
||||
"One of IIN code or Person code must be empty"
|
||||
))
|
||||
|
||||
|
||||
class LegalEntityValidator:
|
||||
def __init__(self, instance: AbstractBaseModel):
|
||||
self.instance = instance
|
||||
|
||||
def __call__(self):
|
||||
|
||||
if (
|
||||
self.instance.identifier is None # type: ignore
|
||||
and self.instance.bin_code is None # type: ignore
|
||||
):
|
||||
raise ValidationError(_(
|
||||
"Either Identifier or BIN code "
|
||||
"must contain a value"
|
||||
))
|
||||
|
||||
if (
|
||||
self.instance.identifier is not None # type: ignore
|
||||
and self.instance.bin_code is not None # type: ignore
|
||||
):
|
||||
raise ValidationError(_(
|
||||
"One of Indentifier or BIN code "
|
||||
"must be empty"
|
||||
))
|
||||
|
||||
|
||||
class ContractOwnerValidator:
|
||||
def __init__(self, instance: AbstractBaseModel):
|
||||
self.instance = instance
|
||||
|
||||
def __call__(self):
|
||||
if (
|
||||
self.instance.legal_entity is None # type: ignore
|
||||
and self.instance.individual is None # type: ignore
|
||||
):
|
||||
raise ValidationError("Either entity or individual should be not None")
|
||||
|
||||
if (
|
||||
self.instance.legal_entity is not None # type: ignore
|
||||
and self.instance.individual is not None # type: ignore
|
||||
):
|
||||
raise ValidationError("One of Individual or Legal entity should be given")
|
||||
4
core/apps/contracts/views/__init__.py
Normal file
4
core/apps/contracts/views/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .attached_files import * # noqa
|
||||
from .contracts import * # noqa
|
||||
from .file_contents import * # noqa
|
||||
from .owners import * # noqa
|
||||
35
core/apps/contracts/views/attached_files.py
Normal file
35
core/apps/contracts/views/attached_files.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from django_core.mixins import BaseViewSetMixin
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework.permissions import AllowAny, IsAdminUser
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from core.apps.contracts.models import ContractAttachedFileModel
|
||||
from core.apps.contracts.serializers.attached_files import (
|
||||
CreateContractAttachedFileSerializer,
|
||||
ListContractAttachedFileSerializer,
|
||||
RetrieveContractAttachedFileSerializer,
|
||||
UpdateContractAttachedFileSerializer,
|
||||
DestroyContractAttachedFileSerializer,
|
||||
)
|
||||
|
||||
|
||||
@extend_schema(tags=["ContractAttachedFile"])
|
||||
class ContractAttachedFileView(BaseViewSetMixin, ModelViewSet):
|
||||
queryset = ContractAttachedFileModel.objects.all()
|
||||
serializer_class = ListContractAttachedFileSerializer
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
action_permission_classes = {
|
||||
"list": [IsAdminUser],
|
||||
"retrieve": [IsAdminUser],
|
||||
"create": [IsAdminUser],
|
||||
"update": [IsAdminUser],
|
||||
"destroy": [IsAdminUser],
|
||||
}
|
||||
action_serializer_class = {
|
||||
"list": ListContractAttachedFileSerializer,
|
||||
"retrieve": RetrieveContractAttachedFileSerializer,
|
||||
"create": CreateContractAttachedFileSerializer,
|
||||
"update": UpdateContractAttachedFileSerializer,
|
||||
"destroy": DestroyContractAttachedFileSerializer,
|
||||
}
|
||||
123
core/apps/contracts/views/contracts.py
Normal file
123
core/apps/contracts/views/contracts.py
Normal file
@@ -0,0 +1,123 @@
|
||||
import uuid
|
||||
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework.permissions import AllowAny, IsAdminUser # type: ignore
|
||||
from rest_framework.viewsets import ModelViewSet # type: ignore
|
||||
from rest_framework.views import APIView # type: ignore
|
||||
|
||||
from rest_framework.request import HttpRequest # type: ignore
|
||||
from rest_framework.response import Response # type: ignore
|
||||
from rest_framework.decorators import action # type: ignore
|
||||
from rest_framework.generics import get_object_or_404 # type: ignore
|
||||
from rest_framework import status # type: ignore
|
||||
|
||||
from django_core.mixins import BaseViewSetMixin # type: ignore
|
||||
from core.apps.contracts.models import (
|
||||
ContractModel,
|
||||
ContractAttachedFileModel,
|
||||
ContractOwnerModel
|
||||
)
|
||||
from core.apps.contracts.serializers import (
|
||||
CreateContractSerializer,
|
||||
ListContractSerializer,
|
||||
RetrieveContractSerializer,
|
||||
UpdateContractSerializer,
|
||||
DestroyContractSerializer,
|
||||
|
||||
ListContractAttachedFileSerializer,
|
||||
RetrieveContractOwnerSerializer
|
||||
)
|
||||
|
||||
|
||||
@extend_schema(tags=["Contract"])
|
||||
class ContractView(BaseViewSetMixin, ModelViewSet):
|
||||
queryset = ContractModel.objects.all()
|
||||
serializer_class = ListContractSerializer
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
action_permission_classes = { # type: ignore
|
||||
"list": [IsAdminUser],
|
||||
"retrieve": [IsAdminUser],
|
||||
"create": [IsAdminUser],
|
||||
"update": [IsAdminUser],
|
||||
"destroy": [IsAdminUser],
|
||||
"list_file": [AllowAny],
|
||||
"list_owner": [AllowAny],
|
||||
}
|
||||
action_serializer_class = { # type: ignore
|
||||
"list": ListContractSerializer,
|
||||
"retrieve": RetrieveContractSerializer,
|
||||
"create": CreateContractSerializer,
|
||||
"update": UpdateContractSerializer,
|
||||
"destroy": DestroyContractSerializer,
|
||||
"list_file": ListContractAttachedFileSerializer,
|
||||
"list_owner": RetrieveContractOwnerSerializer,
|
||||
}
|
||||
|
||||
@extend_schema(
|
||||
summary="Get List Of Files",
|
||||
description="Get List Of Files"
|
||||
)
|
||||
@action(url_path="files", detail=True, methods=["GET"])
|
||||
def list_file(
|
||||
self,
|
||||
request: HttpRequest,
|
||||
pk: uuid.UUID,
|
||||
*args: object,
|
||||
**kwargs: object
|
||||
) -> Response:
|
||||
contract = get_object_or_404(ContractModel, pk=pk)
|
||||
files = ContractAttachedFileModel.objects.filter(contract=contract)
|
||||
ser = self.get_serializer(instance=files, many=True) # type: ignore
|
||||
return Response(data=ser.data, status=status.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
summary="Get List Of Owners",
|
||||
description="Get list of owners"
|
||||
)
|
||||
@action(url_path="owners", detail=True, methods=["GET"])
|
||||
def list_owner(
|
||||
self,
|
||||
request: HttpRequest,
|
||||
pk: uuid.UUID,
|
||||
*args: object,
|
||||
**kwargs: object
|
||||
) -> Response:
|
||||
contract = get_object_or_404(ContractModel, pk=pk)
|
||||
owners = ContractOwnerModel.objects.filter(contract=contract)
|
||||
ser = self.get_serializer(instance=owners, many=True) # type: ignore
|
||||
return Response(ser.data, status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ContractDetailView(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@extend_schema(
|
||||
summary="Uploads a file for contract attached files",
|
||||
description="Creates a file for contract attached files.",
|
||||
)
|
||||
def get(
|
||||
self,
|
||||
request: HttpRequest,
|
||||
owner_id: uuid.UUID,
|
||||
*args: object,
|
||||
**kwargs: object
|
||||
) -> Response:
|
||||
contract = ContractModel.objects.filter(owners__id=owner_id)[0]
|
||||
ser = RetrieveContractSerializer(instance=contract)
|
||||
return Response(ser.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ListFolderContractsView(APIView):
|
||||
permission_classes = [IsAdminUser]
|
||||
|
||||
def get(
|
||||
self,
|
||||
request: HttpRequest,
|
||||
pk: uuid.UUID,
|
||||
*args: object,
|
||||
**kwargs: object
|
||||
) -> Response:
|
||||
contracts = ContractModel.objects.filter(folders__id=pk)
|
||||
ser = ListContractSerializer(instance=contracts, many=True)
|
||||
return Response(ser.data, status.HTTP_200_OK)
|
||||
35
core/apps/contracts/views/file_contents.py
Normal file
35
core/apps/contracts/views/file_contents.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from django_core.mixins import BaseViewSetMixin
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework.permissions import AllowAny, IsAdminUser
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from core.apps.contracts.models import ContractFileContentModel
|
||||
from core.apps.contracts.serializers.file_contents import (
|
||||
CreateContractFileContentSerializer,
|
||||
ListContractFileContentSerializer,
|
||||
RetrieveContractFileContentSerializer,
|
||||
UpdateContractFileContentSerializer,
|
||||
DestroyContractFileContentSerializer
|
||||
)
|
||||
|
||||
|
||||
@extend_schema(tags=["ContractFileContent"])
|
||||
class ContractFileContentView(BaseViewSetMixin, ModelViewSet):
|
||||
queryset = ContractFileContentModel.objects.all()
|
||||
serializer_class = ListContractFileContentSerializer
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
action_permission_classes = {
|
||||
"list": [IsAdminUser],
|
||||
"retrieve": [IsAdminUser],
|
||||
"create": [IsAdminUser],
|
||||
"update": [IsAdminUser],
|
||||
"destroy": [IsAdminUser],
|
||||
}
|
||||
action_serializer_class = {
|
||||
"list": ListContractFileContentSerializer,
|
||||
"retrieve": RetrieveContractFileContentSerializer,
|
||||
"create": CreateContractFileContentSerializer,
|
||||
"update": UpdateContractFileContentSerializer,
|
||||
"destroy": DestroyContractFileContentSerializer,
|
||||
}
|
||||
171
core/apps/contracts/views/owners.py
Normal file
171
core/apps/contracts/views/owners.py
Normal file
@@ -0,0 +1,171 @@
|
||||
import uuid
|
||||
from typing import cast
|
||||
|
||||
from django_core.mixins import BaseViewSetMixin # type: ignore
|
||||
from django.utils.translation import gettext as _
|
||||
from drf_spectacular.utils import extend_schema, OpenApiResponse
|
||||
from rest_framework.exceptions import PermissionDenied # type: ignore
|
||||
from rest_framework.decorators import action # type: ignore
|
||||
from rest_framework.permissions import AllowAny, IsAdminUser # type: ignore
|
||||
from rest_framework.viewsets import ModelViewSet # type: ignore
|
||||
from rest_framework.request import HttpRequest # type: ignore
|
||||
from rest_framework.response import Response # type: ignore
|
||||
from rest_framework import status # type: ignore
|
||||
from rest_framework.views import APIView # type: ignore
|
||||
from rest_framework.parsers import MultiPartParser, FormParser # type: ignore
|
||||
from rest_framework.generics import get_object_or_404 # type: ignore
|
||||
|
||||
from core.apps.contracts.models import (
|
||||
ContractOwnerModel,
|
||||
ContractAttachedFileModel
|
||||
)
|
||||
from core.apps.contracts.serializers import (
|
||||
CreateContractOwnerSerializer,
|
||||
ListContractOwnerSerializer,
|
||||
RetrieveContractOwnerSerializer,
|
||||
UpdateContractOwnerSerializer,
|
||||
DestroyContractOwnerSerializer,
|
||||
|
||||
RetrieveContractAttachedFileSerializer,
|
||||
CreateContractAttachedFileSerializer,
|
||||
CreateContractFileContentFromOwnerSerializer,
|
||||
)
|
||||
|
||||
|
||||
@extend_schema(tags=["ContractOwner"])
|
||||
class ContractOwnerView(BaseViewSetMixin, ModelViewSet):
|
||||
queryset = ContractOwnerModel.objects.all()
|
||||
serializer_class = ListContractOwnerSerializer
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
action_permission_classes = {
|
||||
"list": [IsAdminUser],
|
||||
"retrieve": [IsAdminUser],
|
||||
"create": [IsAdminUser],
|
||||
"update": [IsAdminUser],
|
||||
"destroy": [IsAdminUser],
|
||||
}
|
||||
action_serializer_class = { # type: ignore
|
||||
"list": ListContractOwnerSerializer,
|
||||
"retrieve": RetrieveContractOwnerSerializer,
|
||||
"create": CreateContractOwnerSerializer,
|
||||
"update": UpdateContractOwnerSerializer,
|
||||
"destroy": DestroyContractOwnerSerializer,
|
||||
}
|
||||
|
||||
|
||||
class ContractOwnerFileViewSet(BaseViewSetMixin, ModelViewSet):
|
||||
queryset = ContractOwnerModel.objects.all()
|
||||
serializer_class = RetrieveContractAttachedFileSerializer
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
# action_permission_classes = {
|
||||
# "create_file": [AllowAny],
|
||||
# "list_file": [AllowAny]
|
||||
# }
|
||||
action_serializer_class = { # type: ignore
|
||||
"list_file": RetrieveContractAttachedFileSerializer,
|
||||
"create_file": CreateContractAttachedFileSerializer,
|
||||
}
|
||||
|
||||
@extend_schema(
|
||||
summary="Contract Files Related to Owner",
|
||||
description="Contract Files Related to Owner",
|
||||
)
|
||||
@action(url_path="files", methods=["GET"], detail=True)
|
||||
def list_file(
|
||||
self,
|
||||
request: HttpRequest,
|
||||
*args: object,
|
||||
**kwargs: object,
|
||||
) -> Response:
|
||||
owner = cast(ContractOwnerModel, self.get_object())
|
||||
files = ContractAttachedFileModel.objects.filter(
|
||||
contract__owners=owner, contents__owner=owner
|
||||
).select_related("contents")
|
||||
serializer = self.get_serializer(instance=files, many=True)
|
||||
return Response(serializer.data, status.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
summary="Create Contract Files Related to Owner",
|
||||
description="Create Contract Files Related to Owner"
|
||||
)
|
||||
@action(url_path="files", methods=["GET"], detail=True)
|
||||
def create_file(
|
||||
self,
|
||||
request: HttpRequest,
|
||||
*args: object,
|
||||
**kwargs: object,
|
||||
) -> Response:
|
||||
owner = cast(
|
||||
ContractOwnerModel,
|
||||
self.get_queryset().select_related("contract")
|
||||
)
|
||||
if not owner.contract.allow_add_files:
|
||||
raise PermissionDenied(_("Attaching new files was restricted for this contract."))
|
||||
ser = self.get_serializer(data=request.data)
|
||||
ser.is_valid(raise_exception=True)
|
||||
ser.save() # type: ignore
|
||||
return Response(ser.data, status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class ContractAttachedFileDeleteView(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@extend_schema(
|
||||
# request=ContractFileDeleteRequestSerializer,
|
||||
responses={204: OpenApiResponse(description="File successfully deleted.")},
|
||||
summary="Delete a file from contract",
|
||||
description="Deletes a contract-attached file if contract allows file deletion.",
|
||||
)
|
||||
def delete(
|
||||
self,
|
||||
request: HttpRequest,
|
||||
owner_id: uuid.UUID,
|
||||
file_id: uuid.UUID,
|
||||
*args: object,
|
||||
**kwargs: object
|
||||
) -> Response:
|
||||
owner = get_object_or_404(
|
||||
ContractOwnerModel.objects.all().select_related("contract"), pk=owner_id
|
||||
)
|
||||
if not owner.contract.allow_delete_files:
|
||||
raise PermissionDenied(_("Deleting attached files was restricted for this contract"))
|
||||
file = get_object_or_404(
|
||||
ContractAttachedFileModel.objects.all().select_related("contract"),
|
||||
pk=file_id
|
||||
)
|
||||
if owner.contract.pk != file.contract.pk:
|
||||
raise PermissionDenied(_("Contract have no such file attached"))
|
||||
file.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class UploadFileContentView(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
parser_classes = [MultiPartParser, FormParser] # type: ignore
|
||||
|
||||
@extend_schema(
|
||||
summary="Uploads a file for contract attached files",
|
||||
description="Creates a file for contract attached files.",
|
||||
request=CreateContractFileContentFromOwnerSerializer,
|
||||
responses={201: CreateContractFileContentFromOwnerSerializer},
|
||||
)
|
||||
def post(
|
||||
self,
|
||||
request: HttpRequest,
|
||||
owner_id: uuid.UUID,
|
||||
file_id: uuid.UUID,
|
||||
*args: object,
|
||||
**kwargs: object
|
||||
) -> Response:
|
||||
serializer = CreateContractFileContentFromOwnerSerializer(
|
||||
data=request.data, # type: ignore
|
||||
context={
|
||||
"file_id": file_id,
|
||||
"contract_owner_id": owner_id,
|
||||
}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save() # type: ignore
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
Reference in New Issue
Block a user