Django: 'unique_together' and 'blank=True' - python

I have a Django model which looks like this:
class MyModel(models.Model):
parent = models.ForeignKey(ParentModel)
name = models.CharField(blank=True, max_length=200)
... other fields ...
class Meta:
unique_together = ("name", "parent")
This works as expected; If there is the same name more than once in the same parent then I get an error: "MyModel with this Name and Parent already exists."
However, I also get an error when I save more than one MyModel with the same parent but with the name field blank, but this should be allowed. So basically I don't want to get the above error when the name field is blank. Is that possible somehow?

Firstly, blank (empty string) IS NOT same as null ('' != None).
Secondly, Django CharField when used through forms will be storing empty string when you leave field empty.
So if your field was something else than CharField you should just add null=True to it. But in this case you need to do more than that. You need to create subclass of forms.CharField and override it's clean method to return None on empty string, something like this:
class NullCharField(forms.CharField):
def clean(self, value):
value = super(NullCharField, self).clean(value)
if value in forms.fields.EMPTY_VALUES:
return None
return value
and then use it in form for your ModelForm:
class MyModelForm(forms.ModelForm):
name = NullCharField(required=False, ...)
this way if you leave it blank it will store null in database instead of empty string ('')

Using unique_together, you're telling Django that you don't want any two MyModel instances with the same parent and name attributes -- which applies even when name is an empty string.
This is enforced at the database level using the unique attribute on the appropriate database columns. So to make any exceptions to this behavior, you'll have to avoid using unique_together in your model.
Instead, you can get what you want by overriding the save method on the model and enforcing the unique restraint there. When you try to save an instance of your model, your code can check to see if there are any existing instances that have the same parent and name combination, and refuse to save the instance if there are. But you can also allow the instance to be saved if the name is an empty string. A basic version of this might look like this:
class MyModel(models.Model):
...
def save(self, *args, **kwargs):
if self.name != '':
conflicting_instance = MyModel.objects.filter(parent=self.parent, \
name=self.name)
if self.id:
# This instance has already been saved. So we need to filter out
# this instance from our results.
conflicting_instance = conflicting_instance.exclude(pk=self.id)
if conflicting_instance.exists():
raise Exception('MyModel with this name and parent already exists.')
super(MyModel, self).save(*args, **kwargs)
Hope that helps.

This solution is very similar to the one given by #bigmattyh, however, i found the below page which describes where the validation should be done:
http://docs.djangoproject.com/en/1.3/ref/models/instances/#validating-objects
The solution i ended up using is the following:
from django import forms
class MyModel(models.Model):
...
def clean(self):
if self.name != '':
instance_exists = MyModel.objects.filter(parent=self.parent,
name=self.name).exists()
if instance_exists:
raise forms.ValidationError('MyModel with this name and parent already exists.')
Notice that a ValidationError is raised instead of a generic exception. This solution has the benefit that when validating a ModelForm, using .is_valid(), the models .clean() method above is automatically called, and will save the ValidationError string in .errors, so that it can be displayed in the html template.
Let me know if you do not agree with this solution.

You can use constraints to set up a partial index like so:
class MyModel(models.Model):
parent = models.ForeignKey(ParentModel)
name = models.CharField(blank=True, max_length=200)
... other fields ...
class Meta:
constraints = [
models.UniqueConstraint(
fields=['name', 'parent'],
condition=~Q(name='')
name='unique_name_for_parent'
)
]
This allow constraints like UniqueTogether to only apply to certain rows (based on conditions you can define using Q).
Incidentally, this happens to be the Django recommended path forward as well: https://docs.djangoproject.com/en/3.2/ref/models/options/#unique-together
Some more documentation: https://docs.djangoproject.com/en/3.2/ref/models/constraints/#django.db.models.UniqueConstraint

bigmattyh gives a good explanation as to what is happening. I'll just add a possible save method.
def save(self, *args, **kwargs):
if self.parent != None and MyModels.objects.filter(parent=self.parent, name=self.name).exists():
raise Exception('MyModel with this name and parent exists.')
super(MyModel, self).save(*args, **kwargs)
I think I chose to do something similar by overriding my model's clean method and it looked something like this:
from django.core.exceptions import ValidationError
def clean(self):
if self.parent != None and MyModels.objects.filter(parent=self.parent, name=self.name).exists():
raise ValidationError('MyModel with this name and parent exists.')

Related

How can I ensure uniqueness on a django model depending on the value?

The title may be slightly misleading, I essentially want to know how to ensure uniqueness on 1 type of attribute, but not on others. If I have a model 'MyModel' that has a related model called 'Settings', which sets certain attributes for MyModel, how do I ensure that there is only one default settings object?
What is the best way to make sure that only one settings object is the default? I can put that 'default' and 'related_model' are unique_together, but this only allows for one non-default settings object as well. I'd like to get to the point where I can only have one default settings object, but no limit on non-default ones.
MyModel
attribute1 = x
...
Settings
related_model = MyModel
default = boolean
Um, I don't think you can set this up in DB level. But you can do it in Settings Model. Lets say your MyModel Model has relation in Settings Model. Like this:
class Settings(models.Model):
mymodel = models.ForeignKey(Settings, on_delete=models.DO_NOTHING, related_name="attributes")
default = models.BooleanField(default=False)
Then in your Settings model, you can add a constrain like this in save method:
from django.core.exceptions import ValidationError
class Settings(...):
def save(self, *args, **kwargs):
query = self.mymodel.attributes.all()
if self.pk:
query = query.exclude(pk=self.pk)
if self.default == True:
if query.filter(default=True).exists():
raise ValidationError("Already a default value exists")
return super(Attribute, self).save(*args, **kwargs)

Django displaying error messages from two separated methods?

I have a Django model defined as below:
class Category(models.Model):
name = models.CharField(max_length=100, unique=True)
slug = models.SlugField(unique=True)
Although both defined as uniqe, django admin allows me to add categories like "python", "Python", "PYTHON". I know this is the default behavior.
To prevent this i have created a clean() method in Category models as follows:
def clean(self, *args, **kwargs):
from django.core.exceptions import ValidationError
slug = slugify(self.name.lower())
r = Category.objects.filter(slug=slug)
print("size")
print(r.count())
if r:
raise ValidationError("Category with this name already exists. Try again with a new name.")
self.slug = slug
super(Category, self).clean(*args, **kwargs)
It works for most of the cases. But lets say database already has Python category and if i try to add Python again, it will show me two errors one from clean() method and one from validate_unique() method. Here is how it looks.
I want to display only one message is there a way to prevent it. Is there any way to override this behavior or something. Thanks in advance.
From docs:
To assign exceptions to a specific field, instantiate the ValidationError with a dictionary, where the keys are the field names.
if r:
raise ValidationError({'name': ["Category with this name already exists.",]})

How to use TextInput widget with ModelChoiceField in Django forms?

In Django, the ChoiceField or ModelChoiceField represent the data in a Select widget. This is helpful when the list of objects is small. However it is extremely difficult to manage (for the end-user) if the number of objects are in thousands.
To eliminate the said problem I'd like the end-users to manually enter the field value in an input box of type text (i.e, via TextInput widget).
So far, I have created the below code. As ModelChoiceField has Select widget by default; It behaves in similar manner as before even after changing the widget to TextInput. It expects a pk or id value of the model object and thus raising an error :
Select a valid choice. That choice is not one of the available choices.
However, I'd want the end-user to enter sku_number field in the input box rather than the pk or id of the object. What is the correct way to solve this problem?
models.py
class Product(models.Model):
sku_number = models.CharField(null=False, unique=True)
product_name = models.CharField(null=Flase)
def __str__(self):
return self.sku_number
forms.py
class SkuForm(forms.Form):
sku_number = forms.ModelChoiceField(queryset=Product.objects.all(),
widget=forms.TextInput())
extra_field = forms.CharField(required=True)
Note : I did try another approach to solve this problem. By displaying only last 10 objects by slicing the number of objects; this ensures that the select box is not flooded with thousands of items.
queryset=Product.objects.all().order_by('-id')[:10]
The latter methodology if correctly implemented would work with my particular use-case, however others might be interested in the former method. The above statement further raised errors because of Django's limitation with generating SQL statements.
Also note that even though slicing an unevaluated QuerySet returns another unevaluated QuerySet, modifying it further (e.g., adding more filters, or modifying ordering) is not allowed, since that does not translate well into SQL and it would not have a clear meaning either.
Source : Django Docs
You can easily do that in the form clean() method.
I.e.
from django.shortcuts import get_object_or_404
class SkuForm(forms.Form):
sku = forms.CharField(required=True)
extra_field = forms.CharField(required=True)
def clean(self):
# If you're on Python 2.x, change super() to super(SkuForm, self)
cleaned_data = super().clean()
sku = cleaned_data['sku']
obj = get_object_or_404(Product, sku_number=sku)
# do sth with the Product
In my case, I had to change the sku_number field to Product object's id. This had to be done before clean(). As seen in this answer, __init__() should be used to modify data before it reaches clean().
class SkuForm(forms.Form):
sku_number = forms.ModelChoiceField(queryset=Product.objects.all(),
widget=forms.TextInput())
extra_field = forms.CharField(required=True)
def __init__(self, data=None, *args, **kwargs):
if data is not None:
data = data.copy() # make it mutable
if data['sku_number']:
obj = get_object_or_404(Product, batch_name=data['sku_number'])
data['sku_number'] = obj.id
super(SkuForm, self).__init__(data=data, *args, **kwargs)

Django save method called twice?

I'm trying to override a save method so that on creation of one model, an instance of the second model is created. However, it looks like the secondary model that I'm trying to create (Restaurant in this example) is being created twice. Why is that?
models.py
class Place(models.Model):
name = models.CharField(max_length=50)
address = models.CharField(max_length=80)
def __str__(self):
return "%s the place" % self.name
def save(self, *args, **kwargs):
super(Place, self).save(*args, **kwargs)
if Restaurant.objects.filter(place=self).count() == 0:
restaurant = Restaurant.objects.create(place=self)
class Restaurant(models.Model):
place = models.OneToOneField(
Place,
on_delete=models.CASCADE,
primary_key=True,
)
Your save method does not have proper indentation. I assume this was an error in cut and paste. With in that method.
if Restaurant.objects.filter(place=self).count() == 0:
restaurant = Restaurant.objects.create(restaurant=self)
This is essentially what get_or_create does but does atomically.
This method is atomic assuming correct usage, correct database
configuration, and correct behavior of the underlying database.
However, if uniqueness is not enforced at the database level for the
kwargs used in a get_or_create call (see unique or unique_together),
this method is prone to a race-condition which can result in multiple
rows with the same parameters being inserted simultaneously.
You can do the same in your own code of course with an atomic block but why bother. Just do
Restaurent.objects.get_or_create(place=self)
and isn't that place=self instead of restaurent=self as in your save method?
You can try:
obj.save(commit=False)
#change fields
obj.save()
First you will create save 'instance', do what you have to do, and then call the right save() method.

Django REST Framework - Serializing optional fields

I have an object that has optional fields. I have defined my serializer this way:
class ProductSerializer(serializers.Serializer):
code = serializers.Field(source="Code")
classification = serializers.CharField(source="Classification", required=False)
I thought required=False would do the job of bypassing the field if it doesn't exist. However, it is mentioned in the documentation that this affects deserialization rather than serialization.
I'm getting the following error:
'Product' object has no attribute 'Classification'
Which is happening when I try to access .data of the serialized instance. (Doesn't this mean it's deserialization that's raising this?)
This happens for instances that do not have Classification. If I omit Classification from the serializer class it works just fine.
How do I correctly do this? Serialize an object with optional fields, that is.
Django REST Framework 3.0+
Dynamic fields now supported, see http://www.django-rest-framework.org/api-guide/serializers/#dynamically-modifying-fields -- this approach defines all of the fields in the serializer, and then allows you to selectively remove the ones you don't want.
Or you could also do something like this for a Model Serializer, where you mess around with Meta.fields in the serializer init:
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ('code',)
def __init__(self, *args, **kwargs):
if SHOW_CLASSIFICATION: # add logic here for optional viewing
self.Meta.fields = list(self.Meta.fields)
self.Meta.fields.append('classification')
super(ProductSerializer, self).__init__(*args, **kwargs)
You'd have to ask Tom though if this is the "correct way" since it may not fit in with the long term plan.
Django REST Framework < 3.0
Try something like this:
class ProductSerializer(serializers.Serializer):
...
classification = serializers.SerializerMethodField('get_classification')
def get_classification(self, obj):
return getattr(obj, 'classification', None)
Multiple Serializers
Another approach would be to create multiple serializers with different sets of fields. One serializer inherits from another and adds additional fields. Then you can choose the appropriate serializer in the view with the get_serializer_class method. Here's an actual example of how I use this approach to call different serializers to present different user data if the user object is the same as the request user.
def get_serializer_class(self):
""" An authenticated user looking at their own user object gets more data """
if self.get_object() == self.request.user:
return SelfUserSerializer
return UserSerializer
Removing fields from representation
Another approach that I've used in security contexts is to remove fields in the to_representation method. Define a method like
def remove_fields_from_representation(self, representation, remove_fields):
""" Removes fields from representation of instance. Call from
.to_representation() to apply field-level security.
* remove_fields: a list of fields to remove
"""
for remove_field in remove_fields:
try:
representation.pop(remove_field)
except KeyError:
# Ignore missing key -- a child serializer could inherit a "to_representation" method
# from its parent serializer that applies security to a field not present on
# the child serializer.
pass
and then in your serializer, call that method like
def to_representation(self, instance):
""" Apply field level security by removing fields for unauthorized users"""
representation = super(ProductSerializer, self).to_representation(instance)
if not permission_granted: # REPLACE WITH PERMISSION LOGIC
remove_fields = ('classification', )
self.remove_fields_from_representation(representation, remove_fields)
return representation
This approach is straightforward and flexible, but it comes at the cost of serializing fields that are sometimes not displayed. But that's probably okay.
The method describe below did the work for me.
Pretty simple,easy and worked for me.
DRF version used = djangorestframework (3.1.0)
class test(serializers.Serializer):
id= serializers.IntegerField()
name=serializers.CharField(required=False,default='some_default_value')
The serializers are deliberately designed to use a fixed set of fields so you wouldn't easily be able to optionally drop out one of the keys.
You could use a SerializerMethodField to either return the field value or None if the field doesn't exist, or you could not use serializers at all and simply write a view that returns the response directly.
Update for REST framework 3.0 serializer.fields can be modified on an instantiated serializer. When dynamic serializer classes are required I'd probably suggest altering the fields in a custom Serializer.__init__() method.
The serializers Charfield method has a property allow_blank
By default it is set to False.
Setting it to True will allow you to mark the field as optional during "serialization".
This is the code that you should write
classification = serializers.CharField(source="Classification", allow_blank=True)
Note: required property is used for deserialization.
DynamicSerializer for DRF 3, which allows dynamicly specifying which fields will be used in serializer, which will be excluded, and optionally which will become required!
Create Mixin
class DynamicSerializerMixin:
"""
A Serializer that takes an additional `fields` argument that
controls which fields should be used.
"""
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
fields = kwargs.pop("fields", None)
excluded_fields = kwargs.pop("excluded_fields", None)
required_fields = kwargs.pop("required_fields", None)
# Instantiate the superclass normally
super().__init__(*args, **kwargs)
if fields is not None:
# Drop any fields that are not specified in the `fields` argument.
allowed = set(fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
if isinstance(fields, dict):
for field, config in fields.items():
set_attrs(self.fields[field], config)
if excluded_fields is not None:
# Drop any fields that are not specified in the `fields` argument.
for field_name in excluded_fields:
self.fields.pop(field_name)
if required_fields is not None:
for field_name in required_fields:
self.fields[field_name].required = True
Initialize/adjust your serializer by adding DynamicSerializerMixin to inheritence
class UserProfileSerializer(DynamicSerializerMixin, serializers.ModelSerializer):
class Meta:
model = User
fields = (
"id",
'first_name', 'last_name'
"email",
"is_staff",
)
Use it :)
class RoleInvitationSerializer(serializers.ModelSerializer):
invited_by = UserProfileSerializer(fields=['id', 'first_name', 'last_name'])
or in action apis
#action(detail=True, serializer_class=YourSerialzierClass)
def teams_roles(self, request, pk=None):
user = self.get_object()
queryset = user.roles.all()
serializer = self.get_serializer(queryset, many=True, excluded_fields=['user'])
return Response(data=serializer.data)
For this purpose the serializers have the partial argument. If when the serializer is initialized you can pass partial=True. If you are using generics or mixins you can overrider the get_serializer function as follows:
def get_serializer(self, *args, **kwargs):
kwargs['partial'] = True
return super(YOUR_CLASS, self).get_serializer(*args, **kwargs)
And that will do the trick.
Note: This allows all fields to be optional and not only a specific one. If you want only specifics, you can override the method (i.e. update) and add validations of existence for various fields.
What has worked well for me is to set the serializer like so:
classification = serializers.CharField(max_length=20, allow_blank=True, default=None)
From the "it's a terrible hack relying on specific implementation details of both DRF and Django, but it works (at least for now)" files, here's the approach I used to include some additional debugging data in the response from a "create" method implementation on a serializer:
def create(self, validated_data)
# Actual model instance creation happens here...
self.fields["debug_info"] = serializers.DictField(read_only=True)
my_model.debug_info = extra_data
return my_model
This is a temporary approach that lets me use the browsable API to display some of the raw response data received from a particular remote service during the creation process. In the future, I'm inclined to keep this capability, but hide it behind a "report debugging info" flag in the creation request rather than returning the lower level info by default.

Categories

Resources