Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions drf_writable_nested/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework.validators import UniqueValidator
from rest_framework.validators import UniqueTogetherValidator, UniqueValidator


class BaseNestedModelSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -400,12 +400,14 @@ class Meta:
(`UniqueFieldsMixin` and `NestedCreateMixin` or `NestedUpdateMixin`)
you should put `UniqueFieldsMixin` ahead.
"""
_unique_fields = [] # type: List[Tuple[str,UniqueValidator]]
_unique_fields = [] # type: List[Tuple[str, UniqueValidator]]
_unique_together_validators = [] # type: List[UniqueTogetherValidator]

def get_fields(self):
self._unique_fields = []

fields = super(UniqueFieldsMixin, self).get_fields()

for field_name, field in fields.items():
unique_validators = [validator
for validator in field.validators
Expand All @@ -419,6 +421,10 @@ def get_fields(self):

return fields

def get_unique_together_validators(self):
self._unique_together_validators = super().get_unique_together_validators()
return []

def _validate_unique_fields(self, validated_data):
for unique_field in self._unique_fields:
field_name, unique_validator = unique_field
Expand All @@ -434,6 +440,8 @@ def _validate_unique_fields(self, validated_data):
self.fields[field_name])
except ValidationError as exc:
raise ValidationError({field_name: exc.detail})
for validator in self._unique_together_validators:
validator(validated_data, self)

def create(self, validated_data):
self._validate_unique_fields(validated_data)
Expand Down
32 changes: 32 additions & 0 deletions tests/migrations/0003_add_itemcategory_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 5.2.6 on 2025-09-14 14:12

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('tests', '0002_alter_profile_sites_setnullforeignkey_and_more'),
]

operations = [
migrations.CreateModel(
name='ItemCategory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('company', models.CharField(max_length=50)),
],
options={
'unique_together': {('name', 'company')},
},
),
migrations.CreateModel(
name='ItemParent',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('child', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.itemcategory')),
],
),
]
14 changes: 14 additions & 0 deletions tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,20 @@ class UFMParent(models.Model):
child = models.ForeignKey(UFMChild, on_delete=models.CASCADE)


class ItemCategory(models.Model):
name = models.CharField(max_length=50)
company = models.CharField(max_length=50)

class Meta:
unique_together = (
("name", "company"),
)


class ItemParent(models.Model):
child = models.ForeignKey(ItemCategory, on_delete=models.CASCADE)


# Models for different relations

class ForeignKeyChild(models.Model):
Expand Down
14 changes: 14 additions & 0 deletions tests/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,20 @@ class Meta:
model = models.UFMParent
fields = ('pk', 'child')

# UniqueFieldsMixin, unique_together validation serializers

class ItemCategorySerializer(UniqueFieldsMixin, serializers.ModelSerializer):
class Meta:
model = models.ItemCategory
fields = "__all__"


class ItemParentSerializer(WritableNestedModelSerializer):
child = ItemCategorySerializer()

class Meta:
model = models.ItemParent
fields = "__all__"

# Different relations

Expand Down
62 changes: 62 additions & 0 deletions tests/test_unique_fields_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,65 @@ def test_unique_field_not_required_for_partial_updates(self):
)
self.assertTrue(serializer.is_valid())
serializer.save()


class UniqueFieldsMixinUniqueTogetherTestCase(TestCase):
def test_create_update_success(self):
serializer = serializers.ItemParentSerializer(
data={'child': {'name': 'Video Cards', 'company': 'Example'}})
self.assertTrue(serializer.is_valid())
parent = serializer.save() # type: models.ItemParent

serializer = serializers.ItemParentSerializer(
instance=parent,
data={
'pk': parent.pk,
'child': {
'pk': parent.child.pk,
'name': 'value',
'company': 'value',
}
}
)
self.assertTrue(serializer.is_valid())
serializer.save()

def test_create_update_failed(self):
# In this case everything is valid on the validation stage, because
# UniqueTogetherValidator is skipped
# But `save` should raise an exception on create/update

child = models.ItemCategory.objects.create(name='value', company='value')
parent = models.ItemParent.objects.create(child=child)

default_error_detail = ErrorDetail(
string='The fields name, company must make a unique set.',
code='unique')
serializer = serializers.ItemParentSerializer(
data={
'child': {
'name': child.name,
'company': child.company,
}
}
)

self.assertTrue(serializer.is_valid())

with self.assertRaises(ValidationError) as ctx:
serializer.save()
self.assertEqual(
ctx.exception.detail,
{'child': [default_error_detail]}
)


def test_unique_field_not_required_for_partial_updates(self):
child = models.ItemCategory.objects.create(name='value', company='value')
serializer = serializers.ItemCategorySerializer(
instance=child,
data={},
partial=True
)
self.assertTrue(serializer.is_valid())
serializer.save()