Django displaying error messages from two separated methods? - python

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.",]})

Related

In Django, how do you enforce a relationship across tables without using constraint?

I'm creating a simple list app that has Groups and Items, each with an associated User. How can I enforce, in the model, that an item created by one user can never be linked to a group created by another user? Here's the model:
class Group(models.Model):
name = models.CharField(max_length=200)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
class Item(models.Model):
name = models.CharField(max_length=200)
group = models.ForeignKey(Group, on_delete=models.PROTECT)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
I've figured out this is impossible to do with a CheckConstraint in class Meta because constraints are apparently made in the database itself (I'm using postgres) and cross-table constraints are not allowed.
Coding without a framework, you would simply query the group before saving a link and throw an exception if the users didn't match. So how do you do that in django?
I figured this out moments later. From the docs:
https://docs.djangoproject.com/en/4.0/topics/db/models/#overriding-predefined-model-methods
You can override the save() method in class Item to do a check beforehand:
from django.db.utils import IntegrityError
.
.
def save(self, *args, **kwargs):
# if no user is given don't fail here; instead let normal integrity check catch it
if hasattr(self, 'user') and self.user != self.group.user:
raise IntegrityError("Item user must match group user")
super().save(*args, **kwargs)
But I'll leave this open in case there's a better way...

slugify() got an unexpected keyword argument 'allow_unicode'

When I want to create new object from product I got this error:
slugify() got an unexpected keyword argument 'allow_unicode'
This is my models:
class BaseModel(models.Model):
created_date = models.DateTimeField(auto_now_add=True)
modified_date = models.DateTimeField(auto_now=True,)
slug = models.SlugField(null=True, blank=True, unique=True, allow_unicode=True, max_length=255)
class Meta:
abstract = True
class Product(BaseModel):
author = models.ForeignKey(User)
title = models.CharField()
# overwrite your model save method
def save(self, *args, **kwargs):
title = self.title
# allow_unicode=True for support utf-8 languages
self.slug = slugify(title, allow_unicode=True)
super(Product, self).save(*args, **kwargs)
I also ran the same pattern for other app(blog) ,and there I didn't run into this problem.
What's wrong with this app?
Since the slugify function is working in the other apps, it means that you use a different function that, at least in that file is referenced through the slugify identifier. This can have several reasons:
you imported the wrong slugify function (for example the slugify template filter function [Django-doc];
you did import the correct one, but later in the file you imported another function with the name slugify (perhaps through an alias or through a wildcard import); or
you defined a class or function named slugify in your file (perhaps after importing slugify).
Regardless the reason, it is thus pointing to the "wrong" function, and therefore it can not handle the named argument allow_unicode.
You can resolve that by reorganizing your imports, or giving the function/class name a different name.
Upgrade Django, that argument allow_unicode introduced in the version 1.9, or call the function without that argument.

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)

Can't disable ForeignKey referential integrity check in Django 1.9

I have a model with two entities, Person and Code. Person is referenced by Code twice, a Person can be either the user of the code or the approver.
What I want to achieve is the following:
if the user provides an existing Person.cusman, no further action is needed.
if the user provides an unknown Person.cusman, a helper code looks up other attributes of the Person (from an external database), and creates a new Person entity.
I have implemented a function triggered by pre_save signal, which creates the missing Person on the fly. It works fine as long as I use python manage.py shell to create a Code with nonexistent Person.
However, when I try to add a new Code using the admin form or a CreateView descendant I always get the following validation error on the HTML form:
Select a valid choice. That choice is not one of the available choices.
Obviously there's a validation happening between clicking on the Save button and the Code.save() method, but I can't figure out which is it. Can you help me which method should I override to accept invalid foreign keys until pre_save creates the referenced entity?
models.py
class Person(models.Model):
cusman = models.CharField(
max_length=10,
primary_key=True)
name = models.CharField(max_length=30)
email = models.EmailField()
def __unicode__(self):
return u'{0} ({1})'.format(self.name, self.cusman)
class Code(models.Model):
user = models.ForeignKey(
Person,
on_delete=models.PROTECT,
db_constraint=False)
approver = models.ForeignKey(
Person,
on_delete=models.PROTECT,
related_name='approves',
db_constraint=False)
signals.py
#receiver(pre_save, sender=Code)
def create_referenced_person(sender, instance, **kwargs):
def create_person_if_doesnt_exist(cusman):
try:
Person = Person.objects.get(pk=cusman)
except Person.DoesNotExist:
Person = Person()
cr = CusmanResolver()
Person_details = cr.get_person_details(cusman)
Person.cusman = Person_details['cusman']
Person.name = Person_details['name']
Person.email = Person_details['email']
Person.save()
create_Person_if_doesnt_exist(instance.user_id)
create_Person_if_doesnt_exist(instance.approver_id)
views.py
class CodeAddForm(ModelForm):
class Meta:
model = Code
fields = [
'user',
'approver',
]
widgets = {
'user': TextInput,
'approver': TextInput
}
class CodeAddView(generic.CreateView):
template_name = 'teladm/code_add.html'
form_class = CodeAddForm
You misunderstood one thing: You shouldn't use TextField to populate ForeignKey, because django foreign keys are populated using dropdown/radio button to refer to the id of the object in another model. The error you got means you provided wrong information that doesn't match any id in another model(Person in your case).
What you can do is: not using ModelForm but Form. You might have some extra work to do after you call form.is_valid(), but at least you could code up your logic however you want.

Django: 'unique_together' and 'blank=True'

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.')

Categories

Resources