Reworks this to be a little cleaner, adding some testing
This commit is contained in:
parent
63d6213272
commit
e418e0db88
@ -1,241 +0,0 @@
|
|||||||
# Generated by Django 4.2.5 on 2023-10-31 17:28
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
import django.utils.timezone
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations
|
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
("documents", "1039_consumptiontemplate"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="CustomField",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.AutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"created",
|
|
||||||
models.DateTimeField(
|
|
||||||
db_index=True,
|
|
||||||
default=django.utils.timezone.now,
|
|
||||||
verbose_name="created",
|
|
||||||
editable=False,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("name", models.CharField(max_length=128)),
|
|
||||||
(
|
|
||||||
"data_type",
|
|
||||||
models.CharField(
|
|
||||||
choices=[
|
|
||||||
("string", "String"),
|
|
||||||
("url", "URL"),
|
|
||||||
("date", "Date"),
|
|
||||||
("boolean", "Boolean"),
|
|
||||||
("integer", "Integer"),
|
|
||||||
],
|
|
||||||
max_length=50,
|
|
||||||
verbose_name="data type",
|
|
||||||
editable=False,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "custom field",
|
|
||||||
"verbose_name_plural": "custom fields",
|
|
||||||
"ordering": ("created",),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="CustomFieldInstance",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.AutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"created",
|
|
||||||
models.DateTimeField(
|
|
||||||
editable=False,
|
|
||||||
db_index=True,
|
|
||||||
default=django.utils.timezone.now,
|
|
||||||
verbose_name="created",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"document",
|
|
||||||
models.ForeignKey(
|
|
||||||
editable=False,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="custom_fields",
|
|
||||||
to="documents.document",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"field",
|
|
||||||
models.ForeignKey(
|
|
||||||
editable=False,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="fields",
|
|
||||||
to="documents.customfield",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "custom field instance",
|
|
||||||
"verbose_name_plural": "custom field instances",
|
|
||||||
"ordering": ("created",),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="CustomFieldBoolean",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.AutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("value", models.BooleanField()),
|
|
||||||
(
|
|
||||||
"parent",
|
|
||||||
models.OneToOneField(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
parent_link=True,
|
|
||||||
related_name="boolean",
|
|
||||||
to="documents.customfieldinstance",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="CustomFieldDate",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.AutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("value", models.DateField()),
|
|
||||||
(
|
|
||||||
"parent",
|
|
||||||
models.OneToOneField(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
parent_link=True,
|
|
||||||
related_name="date",
|
|
||||||
to="documents.customfieldinstance",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="CustomFieldInteger",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.AutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("value", models.IntegerField()),
|
|
||||||
(
|
|
||||||
"parent",
|
|
||||||
models.OneToOneField(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
parent_link=True,
|
|
||||||
related_name="integer",
|
|
||||||
to="documents.customfieldinstance",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="CustomFieldShortText",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.AutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("value", models.CharField(max_length=128)),
|
|
||||||
(
|
|
||||||
"parent",
|
|
||||||
models.OneToOneField(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
parent_link=True,
|
|
||||||
related_name="short_text",
|
|
||||||
to="documents.customfieldinstance",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="CustomFieldUrl",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.AutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("value", models.URLField()),
|
|
||||||
(
|
|
||||||
"parent",
|
|
||||||
models.OneToOneField(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
parent_link=True,
|
|
||||||
related_name="url",
|
|
||||||
to="documents.customfieldinstance",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.AddConstraint(
|
|
||||||
model_name="customfield",
|
|
||||||
constraint=models.UniqueConstraint(
|
|
||||||
fields=("name",),
|
|
||||||
name="documents_customfield_unique_name",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddConstraint(
|
|
||||||
model_name="customfieldinstance",
|
|
||||||
constraint=models.UniqueConstraint(
|
|
||||||
fields=("document", "field"),
|
|
||||||
name="documents_customfieldinstance_unique_document_field",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -0,0 +1,124 @@
|
|||||||
|
# Generated by Django 4.2.6 on 2023-11-02 17:38
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("documents", "1039_consumptiontemplate"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="CustomField",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created",
|
||||||
|
models.DateTimeField(
|
||||||
|
db_index=True,
|
||||||
|
default=django.utils.timezone.now,
|
||||||
|
editable=False,
|
||||||
|
verbose_name="created",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=128)),
|
||||||
|
(
|
||||||
|
"data_type",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("string", "String"),
|
||||||
|
("url", "URL"),
|
||||||
|
("date", "Date"),
|
||||||
|
("boolean", "Boolean"),
|
||||||
|
("integer", "Integer"),
|
||||||
|
],
|
||||||
|
editable=False,
|
||||||
|
max_length=50,
|
||||||
|
verbose_name="data type",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "custom field",
|
||||||
|
"verbose_name_plural": "custom fields",
|
||||||
|
"ordering": ("created",),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="CustomFieldInstance",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created",
|
||||||
|
models.DateTimeField(
|
||||||
|
db_index=True,
|
||||||
|
default=django.utils.timezone.now,
|
||||||
|
editable=False,
|
||||||
|
verbose_name="created",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("value_text", models.CharField(max_length=128, null=True)),
|
||||||
|
("value_bool", models.BooleanField(null=True)),
|
||||||
|
("value_url", models.URLField(null=True)),
|
||||||
|
("value_date", models.DateField(null=True)),
|
||||||
|
("value_int", models.IntegerField(null=True)),
|
||||||
|
(
|
||||||
|
"document",
|
||||||
|
models.ForeignKey(
|
||||||
|
editable=False,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="custom_fields",
|
||||||
|
to="documents.document",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"field",
|
||||||
|
models.ForeignKey(
|
||||||
|
editable=False,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="fields",
|
||||||
|
to="documents.customfield",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "custom field instance",
|
||||||
|
"verbose_name_plural": "custom field instances",
|
||||||
|
"ordering": ("created",),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="customfield",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=("name",),
|
||||||
|
name="documents_customfield_unique_name",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="customfieldinstance",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=("document", "field"),
|
||||||
|
name="documents_customfieldinstance_unique_document_field",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -4,7 +4,6 @@ import os
|
|||||||
import re
|
import re
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
|
||||||
from typing import Final
|
from typing import Final
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@ -960,6 +959,17 @@ class CustomFieldInstance(models.Model):
|
|||||||
editable=False,
|
editable=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Actual data storage
|
||||||
|
value_text = models.CharField(max_length=128, null=True)
|
||||||
|
|
||||||
|
value_bool = models.BooleanField(null=True)
|
||||||
|
|
||||||
|
value_url = models.URLField(null=True)
|
||||||
|
|
||||||
|
value_date = models.DateField(null=True)
|
||||||
|
|
||||||
|
value_int = models.IntegerField(null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ("created",)
|
ordering = ("created",)
|
||||||
verbose_name = _("custom field instance")
|
verbose_name = _("custom field instance")
|
||||||
@ -978,134 +988,16 @@ class CustomFieldInstance(models.Model):
|
|||||||
def value(self):
|
def value(self):
|
||||||
"""
|
"""
|
||||||
Based on the data type, access the actual value the instance stores
|
Based on the data type, access the actual value the instance stores
|
||||||
|
A little shorthand/quick way to get what is actually here
|
||||||
"""
|
"""
|
||||||
if self.field.data_type == CustomField.FieldDataType.STRING:
|
if self.field.data_type == CustomField.FieldDataType.STRING:
|
||||||
return self.short_text.value
|
return self.value_text
|
||||||
elif self.field.data_type == CustomField.FieldDataType.URL:
|
elif self.field.data_type == CustomField.FieldDataType.URL:
|
||||||
return self.url.value
|
return self.value_url
|
||||||
elif self.field.data_type == CustomField.FieldDataType.DATE:
|
elif self.field.data_type == CustomField.FieldDataType.DATE:
|
||||||
return self.date.value
|
return self.value_date
|
||||||
elif self.field.data_type == CustomField.FieldDataType.BOOL:
|
elif self.field.data_type == CustomField.FieldDataType.BOOL:
|
||||||
return self.boolean.value
|
return self.value_bool
|
||||||
elif self.field.data_type == CustomField.FieldDataType.INT:
|
elif self.field.data_type == CustomField.FieldDataType.INT:
|
||||||
return self.integer.value
|
return self.value_int
|
||||||
raise NotImplementedError(self.field.data_type)
|
raise NotImplementedError(self.field.data_type)
|
||||||
|
|
||||||
@property
|
|
||||||
def field_type(self):
|
|
||||||
"""
|
|
||||||
Based on the data type, quick access to class for storing that value
|
|
||||||
"""
|
|
||||||
if self.field.data_type == CustomField.FieldDataType.STRING:
|
|
||||||
return CustomFieldShortText
|
|
||||||
elif self.field.data_type == CustomField.FieldDataType.URL:
|
|
||||||
return CustomFieldUrl
|
|
||||||
elif self.field.data_type == CustomField.FieldDataType.DATE:
|
|
||||||
return CustomFieldDate
|
|
||||||
elif self.field.data_type == CustomField.FieldDataType.BOOL:
|
|
||||||
return CustomFieldBoolean
|
|
||||||
elif self.field.data_type == CustomField.FieldDataType.INT:
|
|
||||||
return CustomFieldInteger
|
|
||||||
raise NotImplementedError(self.field.data_type)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_json(
|
|
||||||
document: Document,
|
|
||||||
field: OrderedDict,
|
|
||||||
value: Any,
|
|
||||||
) -> "CustomFieldInstance":
|
|
||||||
instance, _ = CustomFieldInstance.objects.get_or_create(
|
|
||||||
document=document,
|
|
||||||
field=CustomField.objects.get(id=field["id"]),
|
|
||||||
)
|
|
||||||
instance.field_type.objects.update_or_create(
|
|
||||||
parent=instance,
|
|
||||||
defaults={"value": value},
|
|
||||||
)
|
|
||||||
|
|
||||||
return instance
|
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldShortText(models.Model):
|
|
||||||
"""
|
|
||||||
Data storage for a short text custom field
|
|
||||||
"""
|
|
||||||
|
|
||||||
value = models.CharField(max_length=128)
|
|
||||||
parent = models.OneToOneField(
|
|
||||||
CustomFieldInstance,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name="short_text",
|
|
||||||
parent_link=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"{self.value}"
|
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldBoolean(models.Model):
|
|
||||||
"""
|
|
||||||
Data storage for a boolean custom field
|
|
||||||
"""
|
|
||||||
|
|
||||||
value = models.BooleanField()
|
|
||||||
parent = models.OneToOneField(
|
|
||||||
CustomFieldInstance,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name="boolean",
|
|
||||||
parent_link=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"{self.value}"
|
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldUrl(models.Model):
|
|
||||||
"""
|
|
||||||
Data storage for a URL custom field
|
|
||||||
"""
|
|
||||||
|
|
||||||
value = models.URLField()
|
|
||||||
parent = models.OneToOneField(
|
|
||||||
CustomFieldInstance,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name="url",
|
|
||||||
parent_link=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"{self.value}"
|
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldDate(models.Model):
|
|
||||||
"""
|
|
||||||
Data storage for a date custom field
|
|
||||||
"""
|
|
||||||
|
|
||||||
value = models.DateField()
|
|
||||||
parent = models.OneToOneField(
|
|
||||||
CustomFieldInstance,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name="date",
|
|
||||||
parent_link=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"{self.value}"
|
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldInteger(models.Model):
|
|
||||||
"""
|
|
||||||
Data storage for a date custom field
|
|
||||||
"""
|
|
||||||
|
|
||||||
value = models.IntegerField()
|
|
||||||
parent = models.OneToOneField(
|
|
||||||
CustomFieldInstance,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name="integer",
|
|
||||||
parent_link=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"{self.value}"
|
|
||||||
|
@ -11,6 +11,7 @@ from django.contrib.auth.models import User
|
|||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
from drf_writable_nested.serializers import NestedUpdateMixin
|
||||||
from guardian.core import ObjectPermissionChecker
|
from guardian.core import ObjectPermissionChecker
|
||||||
from guardian.shortcuts import get_users_with_perms
|
from guardian.shortcuts import get_users_with_perms
|
||||||
from rest_framework import fields
|
from rest_framework import fields
|
||||||
@ -396,9 +397,6 @@ class StoragePathField(serializers.PrimaryKeyRelatedField):
|
|||||||
return StoragePath.objects.all()
|
return StoragePath.objects.all()
|
||||||
|
|
||||||
|
|
||||||
from drf_writable_nested.serializers import NestedUpdateMixin
|
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldSerializer(serializers.ModelSerializer):
|
class CustomFieldSerializer(serializers.ModelSerializer):
|
||||||
data_type = serializers.ChoiceField(
|
data_type = serializers.ChoiceField(
|
||||||
choices=CustomField.FieldDataType,
|
choices=CustomField.FieldDataType,
|
||||||
@ -426,41 +424,82 @@ class CustomFieldOnUpdateSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class CustomFieldInstanceSerializer(serializers.ModelSerializer):
|
class CustomFieldInstanceSerializer(serializers.ModelSerializer):
|
||||||
field = serializers.PrimaryKeyRelatedField(queryset=CustomField.objects.all())
|
field = serializers.PrimaryKeyRelatedField(queryset=CustomField.objects.all())
|
||||||
value = serializers.JSONField()
|
value = serializers.SerializerMethodField(read_only=True)
|
||||||
|
value_text = serializers.CharField(required=False, write_only=True)
|
||||||
|
value_bool = serializers.BooleanField(required=False, write_only=True)
|
||||||
|
value_url = serializers.URLField(required=False, write_only=True)
|
||||||
|
value_date = serializers.DateField(required=False, write_only=True)
|
||||||
|
value_int = serializers.IntegerField(required=False, write_only=True)
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
"""
|
||||||
|
Check that start is before finish.
|
||||||
|
"""
|
||||||
|
# Let the normal validation run first
|
||||||
|
data = super().validate(data)
|
||||||
|
# This field must exist, as it is validated
|
||||||
|
parent_field = data["field"]
|
||||||
|
type_to_key_map = {
|
||||||
|
CustomField.FieldDataType.STRING: "value_text",
|
||||||
|
CustomField.FieldDataType.URL: "value_url",
|
||||||
|
CustomField.FieldDataType.DATE: "value_date",
|
||||||
|
CustomField.FieldDataType.BOOL: "value_bool",
|
||||||
|
CustomField.FieldDataType.INT: "value_int",
|
||||||
|
}
|
||||||
|
# For the given data type, a certain key must exist
|
||||||
|
expected_key = type_to_key_map[parent_field.data_type]
|
||||||
|
if expected_key not in data:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
(
|
||||||
|
f"Field of type {parent_field.data_type} must"
|
||||||
|
f' contain a "{expected_key}" key'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
print("hello from create")
|
type_to_key_map = {
|
||||||
from pprint import pprint
|
CustomField.FieldDataType.STRING: "value_text",
|
||||||
|
CustomField.FieldDataType.URL: "value_url",
|
||||||
pprint(dict(validated_data))
|
CustomField.FieldDataType.DATE: "value_date",
|
||||||
|
CustomField.FieldDataType.BOOL: "value_bool",
|
||||||
|
CustomField.FieldDataType.INT: "value_int",
|
||||||
|
}
|
||||||
|
# An instance is attached to a document
|
||||||
document: Document = validated_data["document"]
|
document: Document = validated_data["document"]
|
||||||
|
# And to a CustomField
|
||||||
custom_field: CustomField = validated_data["field"]
|
custom_field: CustomField = validated_data["field"]
|
||||||
instance, _ = CustomFieldInstance.objects.get_or_create(
|
# This key must exist, as it is validated
|
||||||
document=document, field=custom_field
|
expected_key = type_to_key_map[custom_field.data_type]
|
||||||
)
|
|
||||||
instance_data_class = instance.field_type
|
# Actually update or create the instance, providing the value
|
||||||
_, _ = instance_data_class.objects.update_or_create(
|
instance, _ = CustomFieldInstance.objects.update_or_create(
|
||||||
parent=instance, defaults={"value": validated_data["value"]}
|
document=document,
|
||||||
|
field=custom_field,
|
||||||
|
defaults={expected_key: validated_data[expected_key]},
|
||||||
)
|
)
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
def update(self, instance: CustomFieldInstance, validated_data):
|
|
||||||
print("hello from update")
|
|
||||||
from pprint import pprint
|
|
||||||
|
|
||||||
pprint(validated_data)
|
|
||||||
pprint(instance.id)
|
|
||||||
|
|
||||||
def get_value(self, obj: CustomFieldInstance):
|
def get_value(self, obj: CustomFieldInstance):
|
||||||
return obj.value
|
return obj.value
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CustomFieldInstance
|
model = CustomFieldInstance
|
||||||
fields = ["value", "field"]
|
fields = [
|
||||||
|
"value",
|
||||||
|
"value_text",
|
||||||
|
"value_bool",
|
||||||
|
"value_url",
|
||||||
|
"value_date",
|
||||||
|
"value_int",
|
||||||
|
"field",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class DocumentSerializer(
|
class DocumentSerializer(
|
||||||
OwnedObjectSerializer, NestedUpdateMixin, DynamicFieldsModelSerializer
|
OwnedObjectSerializer,
|
||||||
|
NestedUpdateMixin,
|
||||||
|
DynamicFieldsModelSerializer,
|
||||||
):
|
):
|
||||||
correspondent = CorrespondentField(allow_null=True)
|
correspondent = CorrespondentField(allow_null=True)
|
||||||
tags = TagsField(many=True)
|
tags = TagsField(many=True)
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
|
from datetime import date
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from documents.models import CustomField, CustomFieldInstance, CustomFieldShortText
|
from documents.models import CustomField
|
||||||
|
from documents.models import CustomFieldInstance
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.tests.utils import DirectoriesMixin
|
from documents.tests.utils import DirectoriesMixin
|
||||||
from pprint import pprint
|
|
||||||
|
|
||||||
|
|
||||||
class TestCustomField(DirectoriesMixin, APITestCase):
|
class TestCustomField(DirectoriesMixin, APITestCase):
|
||||||
@ -76,38 +79,37 @@ class TestCustomField(DirectoriesMixin, APITestCase):
|
|||||||
data_type=CustomField.FieldDataType.URL,
|
data_type=CustomField.FieldDataType.URL,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
date_value = date.today()
|
||||||
|
|
||||||
resp = self.client.patch(
|
resp = self.client.patch(
|
||||||
f"/api/documents/{doc.id}/",
|
f"/api/documents/{doc.id}/",
|
||||||
data={
|
data={
|
||||||
"custom_fields": [
|
"custom_fields": [
|
||||||
{
|
{
|
||||||
"field": custom_field_string.id,
|
"field": custom_field_string.id,
|
||||||
"value": "test value",
|
"value_text": "test value",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": custom_field_date.id,
|
"field": custom_field_date.id,
|
||||||
"value": "2023-10-31",
|
"value_date": date_value.isoformat(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": custom_field_int.id,
|
"field": custom_field_int.id,
|
||||||
"value": "3",
|
"value_int": 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": custom_field_boolean.id,
|
"field": custom_field_boolean.id,
|
||||||
"value": "True",
|
"value_bool": True,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": custom_field_url.id,
|
"field": custom_field_url.id,
|
||||||
"value": "https://example.com",
|
"value_url": "https://example.com",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
format="json",
|
format="json",
|
||||||
)
|
)
|
||||||
from pprint import pprint
|
|
||||||
|
|
||||||
print("Response data")
|
|
||||||
pprint(resp.json())
|
|
||||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
resp_data = resp.json()["custom_fields"]
|
resp_data = resp.json()["custom_fields"]
|
||||||
@ -116,7 +118,7 @@ class TestCustomField(DirectoriesMixin, APITestCase):
|
|||||||
resp_data,
|
resp_data,
|
||||||
[
|
[
|
||||||
{"field": custom_field_string.id, "value": "test value"},
|
{"field": custom_field_string.id, "value": "test value"},
|
||||||
{"field": custom_field_date.id, "value": "2023-10-31"},
|
{"field": custom_field_date.id, "value": date_value.isoformat()},
|
||||||
{"field": custom_field_int.id, "value": 3},
|
{"field": custom_field_int.id, "value": 3},
|
||||||
{"field": custom_field_boolean.id, "value": True},
|
{"field": custom_field_boolean.id, "value": True},
|
||||||
{"field": custom_field_url.id, "value": "https://example.com"},
|
{"field": custom_field_url.id, "value": "https://example.com"},
|
||||||
@ -141,7 +143,6 @@ class TestCustomField(DirectoriesMixin, APITestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(CustomFieldInstance.objects.count(), 0)
|
self.assertEqual(CustomFieldInstance.objects.count(), 0)
|
||||||
self.assertEqual(CustomFieldShortText.objects.count(), 0)
|
|
||||||
|
|
||||||
resp = self.client.patch(
|
resp = self.client.patch(
|
||||||
f"/api/documents/{doc.id}/",
|
f"/api/documents/{doc.id}/",
|
||||||
@ -149,7 +150,7 @@ class TestCustomField(DirectoriesMixin, APITestCase):
|
|||||||
"custom_fields": [
|
"custom_fields": [
|
||||||
{
|
{
|
||||||
"field": custom_field_string.id,
|
"field": custom_field_string.id,
|
||||||
"value": "test value",
|
"value_text": "test value",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -162,7 +163,6 @@ class TestCustomField(DirectoriesMixin, APITestCase):
|
|||||||
|
|
||||||
self.assertEqual(doc.custom_fields.first().value, "test value")
|
self.assertEqual(doc.custom_fields.first().value, "test value")
|
||||||
self.assertEqual(CustomFieldInstance.objects.count(), 1)
|
self.assertEqual(CustomFieldInstance.objects.count(), 1)
|
||||||
self.assertEqual(CustomFieldShortText.objects.count(), 1)
|
|
||||||
|
|
||||||
resp = self.client.patch(
|
resp = self.client.patch(
|
||||||
f"/api/documents/{doc.id}/",
|
f"/api/documents/{doc.id}/",
|
||||||
@ -170,7 +170,7 @@ class TestCustomField(DirectoriesMixin, APITestCase):
|
|||||||
"custom_fields": [
|
"custom_fields": [
|
||||||
{
|
{
|
||||||
"field": custom_field_string.id,
|
"field": custom_field_string.id,
|
||||||
"value": "a new test value",
|
"value_text": "a new test value",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -182,4 +182,91 @@ class TestCustomField(DirectoriesMixin, APITestCase):
|
|||||||
|
|
||||||
self.assertEqual(doc.custom_fields.first().value, "a new test value")
|
self.assertEqual(doc.custom_fields.first().value, "a new test value")
|
||||||
self.assertEqual(CustomFieldInstance.objects.count(), 1)
|
self.assertEqual(CustomFieldInstance.objects.count(), 1)
|
||||||
self.assertEqual(CustomFieldShortText.objects.count(), 1)
|
|
||||||
|
def test_delete_custom_field_instance(self):
|
||||||
|
doc = Document.objects.create(
|
||||||
|
title="WOW",
|
||||||
|
content="the content",
|
||||||
|
checksum="123",
|
||||||
|
mime_type="application/pdf",
|
||||||
|
)
|
||||||
|
custom_field_string = CustomField.objects.create(
|
||||||
|
name="Test Custom Field String",
|
||||||
|
data_type=CustomField.FieldDataType.STRING,
|
||||||
|
)
|
||||||
|
custom_field_date = CustomField.objects.create(
|
||||||
|
name="Test Custom Field Date",
|
||||||
|
data_type=CustomField.FieldDataType.DATE,
|
||||||
|
)
|
||||||
|
|
||||||
|
date_value = date.today()
|
||||||
|
|
||||||
|
resp = self.client.patch(
|
||||||
|
f"/api/documents/{doc.id}/",
|
||||||
|
data={
|
||||||
|
"custom_fields": [
|
||||||
|
{
|
||||||
|
"field": custom_field_string.id,
|
||||||
|
"value_text": "a new test value",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": custom_field_date.id,
|
||||||
|
"value_date": date_value.isoformat(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(CustomFieldInstance.objects.count(), 2)
|
||||||
|
self.assertEqual(len(doc.custom_fields.all()), 2)
|
||||||
|
|
||||||
|
resp = self.client.patch(
|
||||||
|
f"/api/documents/{doc.id}/",
|
||||||
|
data={
|
||||||
|
"custom_fields": [
|
||||||
|
{
|
||||||
|
"field": custom_field_date.id,
|
||||||
|
"value_date": date_value.isoformat(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(CustomFieldInstance.objects.count(), 1)
|
||||||
|
self.assertEqual(Document.objects.count(), 1)
|
||||||
|
self.assertEqual(len(doc.custom_fields.all()), 1)
|
||||||
|
self.assertEqual(doc.custom_fields.first().value, date_value)
|
||||||
|
|
||||||
|
def test_custom_field_validation(self):
|
||||||
|
doc = Document.objects.create(
|
||||||
|
title="WOW",
|
||||||
|
content="the content",
|
||||||
|
checksum="123",
|
||||||
|
mime_type="application/pdf",
|
||||||
|
)
|
||||||
|
custom_field_string = CustomField.objects.create(
|
||||||
|
name="Test Custom Field String",
|
||||||
|
data_type=CustomField.FieldDataType.STRING,
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = self.client.patch(
|
||||||
|
f"/api/documents/{doc.id}/",
|
||||||
|
data={
|
||||||
|
"custom_fields": [
|
||||||
|
{
|
||||||
|
"field": custom_field_string.id,
|
||||||
|
# Whoops, spelling
|
||||||
|
"value_test": "a new test value",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
|
pprint(resp.json())
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertEqual(CustomFieldInstance.objects.count(), 0)
|
||||||
|
self.assertEqual(len(doc.custom_fields.all()), 0)
|
||||||
|
@ -153,7 +153,7 @@ class TestExportImport(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
|
|
||||||
manifest = self._do_export(use_filename_format=use_filename_format)
|
manifest = self._do_export(use_filename_format=use_filename_format)
|
||||||
|
|
||||||
self.assertEqual(len(manifest), 194)
|
self.assertEqual(len(manifest), 169)
|
||||||
|
|
||||||
# dont include consumer or AnonymousUser users
|
# dont include consumer or AnonymousUser users
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@ -247,7 +247,7 @@ class TestExportImport(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
self.assertEqual(Document.objects.get(id=self.d4.id).title, "wow_dec")
|
self.assertEqual(Document.objects.get(id=self.d4.id).title, "wow_dec")
|
||||||
self.assertEqual(GroupObjectPermission.objects.count(), 1)
|
self.assertEqual(GroupObjectPermission.objects.count(), 1)
|
||||||
self.assertEqual(UserObjectPermission.objects.count(), 1)
|
self.assertEqual(UserObjectPermission.objects.count(), 1)
|
||||||
self.assertEqual(Permission.objects.count(), 144)
|
self.assertEqual(Permission.objects.count(), 124)
|
||||||
messages = check_sanity()
|
messages = check_sanity()
|
||||||
# everything is alright after the test
|
# everything is alright after the test
|
||||||
self.assertEqual(len(messages), 0)
|
self.assertEqual(len(messages), 0)
|
||||||
@ -676,15 +676,15 @@ class TestExportImport(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
os.path.join(self.dirs.media_dir, "documents"),
|
os.path.join(self.dirs.media_dir, "documents"),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(ContentType.objects.count(), 36)
|
self.assertEqual(ContentType.objects.count(), 31)
|
||||||
self.assertEqual(Permission.objects.count(), 144)
|
self.assertEqual(Permission.objects.count(), 124)
|
||||||
|
|
||||||
manifest = self._do_export()
|
manifest = self._do_export()
|
||||||
|
|
||||||
with paperless_environment():
|
with paperless_environment():
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
len(list(filter(lambda e: e["model"] == "auth.permission", manifest))),
|
len(list(filter(lambda e: e["model"] == "auth.permission", manifest))),
|
||||||
144,
|
124,
|
||||||
)
|
)
|
||||||
# add 1 more to db to show objects are not re-created by import
|
# add 1 more to db to show objects are not re-created by import
|
||||||
Permission.objects.create(
|
Permission.objects.create(
|
||||||
@ -692,7 +692,7 @@ class TestExportImport(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
codename="test_perm",
|
codename="test_perm",
|
||||||
content_type_id=1,
|
content_type_id=1,
|
||||||
)
|
)
|
||||||
self.assertEqual(Permission.objects.count(), 145)
|
self.assertEqual(Permission.objects.count(), 125)
|
||||||
|
|
||||||
# will cause an import error
|
# will cause an import error
|
||||||
self.user.delete()
|
self.user.delete()
|
||||||
@ -701,5 +701,5 @@ class TestExportImport(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
with self.assertRaises(IntegrityError):
|
with self.assertRaises(IntegrityError):
|
||||||
call_command("document_importer", "--no-progress-bar", self.target)
|
call_command("document_importer", "--no-progress-bar", self.target)
|
||||||
|
|
||||||
self.assertEqual(ContentType.objects.count(), 36)
|
self.assertEqual(ContentType.objects.count(), 31)
|
||||||
self.assertEqual(Permission.objects.count(), 145)
|
self.assertEqual(Permission.objects.count(), 125)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user