handling unique constraint for custom field in django admin - python

Hi I'm new to django and just took my first foray into using a custom field for my model. I have a Char field that I want to always be saved as lower case so I implemented it as a custom field as follows (learned from another Stack Overflow post):
from django.db import models
from django.db.models.fields import CharField
class LowercaseCharField(CharField):
def pre_save(self, model_instance, add):
current_value = getattr(model_instance, self.attname)
setattr(model_instance, self.attname, current_value.lower())
return getattr(model_instance, self.attname)
class Item(models.Model):
name = LowercaseCharField(max_length=50, unique=True)
def __str__(self):
return self.name
I've tested this out in admin and indeed a field entry gets correctly converted to lowercase before it's saved. Unfortunately, when I tested the uniqueness constraint, admin isn't handling the Integrity Error gracefully. Instead of getting the clean error like I do if it's an exact case match from the get go:
I get the ugly error page:
How do I go about setting the custom field in such a way that the unique constraint is caught "early" enough to trigger the graceful error, or otherwise modify the admin so that this "later" error is handled more gracefully?
(Note: I am just using sqlite3 for my db at the moment)
UPDATE:
In case any one is interested, here's the modified code that worked for me:
class LowercaseCharField(CharField):
def get_db_prep_value(self, value, connection, prepared=False):
return value.lower()

I don't think you'll make it by overriding pre_save, because pre_save gets called after uniqueness validation has occurred.
Try with the other methods, such as get_db_prep_save or get_db_prep_value.

Related

Django rest Framework error messages. Does not work

I am new to Django and I am working on a small project, I want an error message to be shown if the user let the field empty. the code that I wrote is not working. Can anyone help me ?
def validate_name(school: School):
if school.name is None:
raise APIException(detail='Name is mandatory.')
class SchoolService(object):
#staticmethod
def validate_create(school: School):
validate_name(school)
Django Rest Framework provides default messages for such common wanted behaviours.
You do not even need to add anything to your field as fields are required by default, unless you explicitly specify required=False
If the user does not fill that field, DRF will automatically return a json object mentioning the field is required and should be filled.
see docs
In your serializer class try adding the validation this way
from django.core.exceptions import ValidationError
def validate_name(self, value):
if value is None:
raise serializers.ValidationError('Name is mandatory')
return value

Indirect way of usine Model.objects.all() in a formset

I'm using something like this to populate inlineformsets for an update view:
formset = inline_formsetfactory(Client, Groupe_esc, form=GroupEscForm, formset=BaseGroupEscInlineFormset, extra=len(Groupe.objects.all()))
(Basically I need as many extra form as there are entries in that table, for some special processing I'm doing in class BaseGroupEscInlineFormset(BaseInlineFormset)).
That all works fine, BUT if I pull my code & try to makemigrations in order to establish a brand new DB, that line apparently fails some django checks and throws up a "no such table (Groupe)" error and I cannot makemigrations. Commenting that line solves the issues (then I can uncomment it after making migration). But that's exactly best programming practices.
So I would need a way to achieve the same result (determine the extra number of forms based on the content of Groupe table)... but without triggering that django check that fails. I'm unsure if the answer is django-ic or pythonic.
E.g. perhaps I could so some python hack that allows me to specific the classname without actually importing Groupe, so I can do my_hacky_groupe_import.Objects.all(), and maybe that wouldn't trigger the error?
EDIT:
In forms.py:
from .models import Client, Groupe
class BaseGroupEscInlineFormset(BaseInlineFormSet):
def get_form_kwargs(self, index):
""" this BaseInlineFormset method returns kwargs provided to the form.
in this case the kwargs are provided to the GroupEsForm constructor
"""
kwargs = super().get_form_kwargs(index)
try:
group_details = kwargs['group_details'][index]
except Exception as ex: # likely this is a POST, but the data is already in the form
group_details = []
return {'group_details':group_details}
GroupeEscFormset = inlineformset_factory(Client, Groupe_esc,
form=GroupeEscForm,
formset=BaseGroupEscInlineFormset,
extra=len(Groupe.objects.all()),
can_delete=False)
The issue as already outlined is that your code is written at the module level and it executes a query when the migrations are not yet done, giving you an error.
One solution as I already pointed in the comment would be to write the line to create the formset class in a view, example:
def some_view(request):
GroupeEscFormset = inlineformset_factory(
Client,
Groupe_esc,
form=GroupeEscForm,
formset=BaseGroupEscInlineFormset,
extra=len(Groupe.objects.all()),
can_delete=False
)
Or if you want some optimization and want to keep this line at the module level to not keep recreating this formset class, you can override the __init__ method and accept extra as an argument (basically your indirect way to call Model.objects.all()):
class BaseGroupEscInlineFormset(BaseInlineFormSet):
def __init__(self, *args, extra=3, **kwargs):
self.extra = extra
super().__init__(*args, **kwargs)
...
GroupeEscFormset = inlineformset_factory(Client, Groupe_esc,
form=GroupeEscForm,
formset=BaseGroupEscInlineFormset,
can_delete=False)
# In your views:
def some_view(request):
formset = GroupeEscFormset(..., extra=Groupe.objects.count()) # count is better if the queryset is needed only to get a count

Errors including the custom model method in Django admin

What I tried to do
I tried to make custom admin method to include product thumbnail preview in my admin panel. product_thumbnail is a ImageField inside the Product Model which has general information of each products that I upload on the admin panel.
I wanted to make product_thumbnail as not required. Therefore, I set null=True and blank=True in my models.py also.
class Product(TimeStampModel):
...
product_thumbnail = models.ImageField(verbose_name='상품사진', null=True, blank=True, upload_to='product_thumbnail')
I created the custom function as below and included it in list_display admin option inside of ProductAdmin model.
def thumbnail_preview(self, obj):
return mark_safe(f'<img src="{obj.product_thumbnail.url}" width="40%" />')
thumbnail_preview.short_description = '상품사진'
Which error did I get?
I got the error as below:
ValueError: The 'product_thumbnail' attribute has no file associated with it.
What I have tried to solve issue
This might be because some Product may not have product_thumbnail so that Django admin failed to load its url due to its null value.
I used try-except so that if Django fails to find product_thumbnail associated with that product, then it can throw error text.
def thumbnail_preview(self, obj):
try:
return mark_safe(f'<img src="{obj.product_thumbnail.url}" width="40%" />')
except obj.product_thumbnail is None:
return HttpResponse('No images')
thumbnail_preview.short_description = '상품사진'
Then I got another error as below:
TypeError: catching classes that do not inherit from BaseException is not allowed
I already applied migrations. How can I solve this problem?
You write except obj.product_thumbnail is None:, but this is very incorrect! After the except keyword you are supposed to specify an exception class, not write some condition. Better yet why even use exceptions here? Generally one should avoid exceptions when one can do so, considering that if a FileField / ImageField has no file then they are considered falsy you can write the below code to solve your problem:
def thumbnail_preview(self, obj):
if obj.product_thumbnail:
return mark_safe(f'<img src="{obj.product_thumbnail.url}" width="40%" />')
return HttpResponse('No images')
thumbnail_preview.short_description = '상품사진'
This means that file is not uploaded in field at all, but you are trying to get its URL which is impossible of course, just wrap in condition. See another way to fix with using templates:
https://fixexception.com/django/the-s-attribute-has-no-file-associated-with-it/

What's the standard way of saving something only if its foreign key exists?

I'm using Python 3.7 and Django . I have the following model, with a foreign key to another model ...
class ArticleStat(models.Model):
objects = ArticleStatManager()
article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='articlestats')
...
def save(self, *args, **kwargs):
if self.article.exists():
try:
article_stat = ArticleStat.objects.get(article=self.article, elapsed_time_in_seconds=self.elapsed_time_in_seconds)
self.id = article_stat.id
super().save(*args, **kwargs, update_fields=["hits"])
except ObjectDoesNotExist:
super().save(*args, **kwargs)
I only want to save this if the related foreign key exists, otherwise, I've noticed errors result. What's the standard Django/Python way of doing something like this? I thought I read I could use ".exists()" (Check if an object exists), but instead I get an error
AttributeError: 'Article' object has no attribute 'exists'
Edit: This is the unit test I have to check this ...
id = 1
article = Article.objects.get(pk=id)
self.assertTrue(article, "A pre-condition of this test is that an article exist with id=" + str(id))
articlestat = ArticleStat(article=article, elapsed_time_in_seconds=250, hits=25)
# Delete the article
article.delete()
# Attempt to save ArticleStat
articlestat.save()
If you want to be sure Article exists in ArticleStat's save method you can try to get it from your database and not just test self.article.
Quoting Alex Martelli:
" ... Grace Murray Hopper's famous motto, "It's easier to ask forgiveness than permission", has many useful applications -- in Python, ... "
I think using try .. except .. else is more pythonic and I will do something like that:
from django.db import models
class ArticleStat(models.Model):
...
article = models.ForeignKey(
Article, on_delete=models.CASCADE, related_name='articlestats'
)
def save(self, *args, **kwargs):
try:
article = Article.objects.get(pk=self.article_id)
except Article.DoesNotExist:
pass
else:
try:
article_stat = ArticleStat.objects.get(
article=article,
elapsed_time_in_seconds=self.elapsed_time_in_seconds
)
self.id = article_stat.id
super().save(*args, **kwargs, update_fields=["hits"])
except ArticleStat.DoesNotExist:
super().save(*args, **kwargs)
If you are using a relational database, foreign key constraints will be added automatically post-migration. save method may not need any customization.
class ArticleStat(models.Model):
objects = ArticleStatManager()
article = models.ForeignKey(
Article, on_delete=models.CASCADE, related_name='articlestats'
)
Use the following code to create ArticleStats
from django.db import IntegrityError
try:
ArticleStats.objects.create(article=article, ...)
except IntegrityError:
pass
If article_id is valid, ArticleStats objects get created else IntegrityError is raised.
article = Article.objects.get(id=1)
article.delete()
try:
ArticleStats.objects.create(article=article, ...)
print("article stats is created")
except IntegrityError:
print("article stats is not created")
# Output
article stats is not created
Note: Tested on MySQL v5.7, Django 1.11
article field on your ArticleStat model is not optional. You can't save your ArticleStat object without the ForeignKey to Article
Here is a similar code, item is a ForeignKey to the Item model, and it is required.
class Interaction(TimeStampedModel, models.Model):
...
item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name='interactions')
type = models.IntegerField('Type', choices=TYPE_CHOICES)
...
If I try to save an object of Interaction from the shell without selecting a ForeignKey to the item, I receive an IntegrityError.
~ interaction = Interaction()
~ interaction.save()
~ IntegrityError: null value in column "item_id" violates not-null constraint
You don't need a check self.article.exists(). Django and Database will require that field and will not let you save the object without it.
You should read about ForeignKey field in Django Docs
You can just test the value of the article field. If it's not set, I believe it defaults to None.
if self.article: # Value is set
If you want this ForeignKey field to be optional (which it sounds like you do), you need to set blank=True and null=True on that field. This will allow the field to be blank (in validation) and will set null on the field when it's not there.
As mentioned in the comments below, your database is likely enforcing the fact that the field is required, and refuses to remove the article instance.
As other answers have pointed out, the Article ForeignKey is required on your ArticleStat model and saving will automatically fail without a valid Article instance. The best way to fail gracefully with non-valid input is using Form validation with Django's Forms API. Or if handling serialized data with Django Rest Framework, using a Serializer, which is the Form-like equivalent for JSON data. That way you don't need to overwrite the save method unless you have a specific requirement.
So far nobody has mentioned the correct usage of .exists(). It is a method of a queryset not a model instance, which is why you get the error you mentioned above when trying to apply it to an individual model instance with self.article.exists(). To check the existence of an object, simply use .filter instead of .get. If your Article (pk=1) exists then:
Article.objects.filter(pk=1)
Will return a queryset with one Article in it:
<Queryset: [Article: 1]>
and
Article.objects.filter(pk=1).exists()
Will return True. Whereas if the item does not exist the query will return an empty queryset and .exists() will return False, rather than raising an exception (as attempting to .get() a non-existent object does). This still applies if the pk previously existed and has been deleted.
EDIT: Just noticed that your ArticleStat's on_delete behaviour is currently set to CASCADE. This means that when an Article is deleted, the related ArticleStat is also deleted. So I think you must have been misinterpreting the errors/difficulties you mentioned in reply to #jonah-bishop's answer when trying if self.article:. For a minimal fix, if you still want to keep the ArticleStat after an Article is deleted, change the on_delete keyword to models.SET_NULL, and as per Jonah's answer, add the extra keywords null=True, blank=True:
article = models.ForeignKey(Article, on_delete=models.SET_NULL, related_name='articlestats', null=True, blank=True)
Then there should be no problem simply doing if self.article: to check for a valid ForeignKey object relation. But using Forms/Serializers is still better practice though.
The code of last line articlestat.save() will fail if the article instance has been deleted. Django and database will check the article automatically for you if you are using relation database like mysql or sqlite3.
During the migrations, a constraint will be created. For example:
shell>>> python manage.py sqlmigrate <appname> 0001
CREATE TABLE impress_impress ...
...
ALTER TABLE `impress_impress` ADD CONSTRAINT
`impress_impress_target_id_73acd523_fk_account_myuser_id` FOREIGN KEY (`target_id`)
REFERENCES `account_myuser` (`id`);
...
So if you want to save the articlestat without article, an error will be raised.
You can call .full_clean() before .save()
from django.core.exceptions import ValidationError
class ArticleStat(models.Model):
#...
def save(self, *args, **kwargs):
try:
self.full_clean()
except ValidationError as e:
# dont save
# Do something based on the errors contained in e.message_dict.
# Display them to a user, or handle them programmatically.
pass
else:
super().save(*args, **kwargs)

What is the difference between a Model field type and a similar validator in Django?

What is the difference between, say, a EmailField and a validator_email? And is it a bad idea to use both?
Or for those who perfer code
import django.db import models
email = models.EmailField()
vs
import django.db import models
email = models.CharField( max_length=75, validators = validate_email )
From the doc it seems like you could also use validators inside forms but if you already specify a validation restriction inside models.py, then you don't need specify again in the forms, right? So it seems better to me to take care of all of the restriction inside models.py.
I suppose the difference is very little, but then you would be violating the DRY principal, which you probably shouldn't do, unless you have a good reason to do it.
If you go to the code base:
#django.db.fields.__init__.py
class EmailField(CharField):
default_validators = [validators.validate_email]
description = _("E-mail address")
def __init__(self, *args, **kwargs):
kwargs['max_length'] = kwargs.get('max_length', 75)
CharField.__init__(self, *args, **kwargs)
def formfield(self, **kwargs):
# As with CharField, this will cause email validation to be performed
# twice.
defaults = {
'form_class': forms.EmailField,
}
defaults.update(kwargs)
return super(EmailField, self).formfield(**defaults)
As you can see, the model inherits from Charfield, so you lose nothing by using emailfield, where appropriate. Furthermore, the default validator is validate_email. Additionally you get the description variable already defined for you. Lastly, on the backend it is already setting max_length for you at '75'. You could of course override this easily enough by defining a max_length in the same way you would when creating a CharField.
You can see formfields() is returning forms.EmailField from django.forms.
Looking at that, you can see:
#django.forms.fields.py
class EmailField(CharField):
default_error_messages = {
'invalid': _(u'Enter a valid e-mail address.'),
}
default_validators = [validators.validate_email]
def clean(self, value):
value = self.to_python(value).strip()
return super(EmailField, self).clean(value)
However, you would lose any default values that using the EmailField might provide, such as the "correct" error message and the custom clean() method.
In the end, while it looks small, actually a good bit of work has already been done for you. So, in general, you shouldn't violate the DRY principal unless you have a good reason to do so.
Edit:
Regarding the second question, you want the form to validate against whatever criteria you are concerned about, so when you call form.is_valid() it returns True / False when it should and generates the appropriate failure message. Otherwise, is_valid() would validate True, and when you model goes to save, it would fail silently, which would be very hard to track down.

Categories