I'm trying to trigger a nice error that appears in the admin backend when you try to delete a model instance. If I use the following signal to do this, my problem is that it throws a 500 error instead of posting a nice error message inside the form:
#receiver(models.signals.pre_delete, sender=MyInvincibleModel)
def execute_before_delete(sender, instance, using, *args, **kwargs):
# some logic
raise ValidationError("Nooo, I don't want to die!!")
If, however, I raise a ValidationError in the model's clean method, it DOES appear as a nice error message in the form itself. The problem with doing it this way is that I don't know how to make the clean method check to see if an instance is being deleted. If I had a custom ModelForm setup, I could do something like this:
for cleaned_data in self.cleaned_data:
if cleaned_data.get('DELETE', True):
raise ValidationError("Nooo, I don't want to die!!")
But I want to rely on the standard admin forms and prefer to avoid overwriting every single one where deletion might occur.
My question is: how can I make the validation error thrown by the pre-delete signal render nicely in the admin form or, failing that, how can I make the model clean method detect when data is being deleted?
Django: 1.6.1
Python: 3.3
This proved to be quite a bit more difficult than it should have been, but I found a solution by overriding both ModelAdmin.delete_model (in case a user accessed a single instance of the object via the hyperlink) and the delete_selected action (in case the user tried deleting using the change_list) and putting my logic in there.
Related
I've got a Django Rest Framework endpoint to which an external party sends data in a fairly high volume. Because it was running about 200 inserts per second I checked the queries it was making. I found that it actually first did a SELECT query to check whether there was a duplicate id. Since I didn't think there would be any (or at least not many) duplicate keys I wanted to disable that check. So I added this line to my Serializer:
id = serializers.UUIDField(validators=[]) # Disable the validators for the id, which improves performance (rps) by over 200%
And as you can see in the comment, it greatly enhances the performance. However, one disadvantage of it is that it gives a 500 error if a duplicate key is in fact posted, saying
duplicate key value violates unique constraint
How would I be able to catch this Database error and returning a neat response instead of throwing this 500 error?
One way to do it would be to override the serializer's create method to catch the error.
from django.db import IntegrityError
from django.core.exceptions import ValidationError
from rest_framework import serializers
class MySerializer(serializers.ModelSerializer):
[ ... ]
def create(self, validated_data):
try:
return super().create(validated_data)
except IntegrityError as e:
raise serializers.ValidationError(str(e))
You have to override the serializer's update as well if you allow PUT on the object.
There might be better to place the hook, depending on your use case. It have to be somewhere on the serializer and the try-catch has to be around where the objects save() method is called. For you, it might work to override the serializer's save() method with roughly the same code as above.
Given the following code:
from django.db import transaction
#transaction.atomic
def viewfunc(request):
# This code executes inside a transaction.
do_stuff()
From my understanding of transactions in Django 1.6 if do_stuff throws an exception, say an IntegrityError, then the transaction will be rolled back right. But since Django itself is calling the view nothing will stop the IntegrityError from rising up the call stack and causing an HTTP 500 error yes? Let's assume that's not what we want, as we want to gracefully handle the error, but still get the rollback functionality.
So I guess the obvious thought is well, don't do that, use transaction.atomic as a context manager that is wrapped in a try except block like the example here:
try:
with transaction.atomic():
generate_relationships()
except IntegrityError:
handle_exception()
Fine. But then if you want to use the Transaction per HTTP Request functionality by setting ATOMIC_REQUEST = True in your db config, which means django will in effect just add the transaction.atomic decorate to your view, which won't catch any exceptions. How is ATOMIC_REQUEST even useful? Why would you ever want to let your Database errors propagate all the way up to the user?
So my question is.
What am I missing here or is my understanding correct?
If I'm correct what is a use case for using ATOMIC_REQUEST? Am I expected to write a urls.hadler500 or should I implement some middleware to catch the errors?
Your understanding is correct. What you're missing is that letting exceptions propagate from your view code (which is quite different from "propagate all the way up to the user") is a perfectly normal thing to do in Django.
You can customize the resulting behavior by making a 500.html template, by overriding handler500, or by making your own custom middleware. In all of those standard cases, using ATOMIC_REQUESTS will do what you want it to do.
If you want to catch exceptions in your view code and handle them specially, you can certainly do that, you'll just have to specify how to handle transactions manually. Using ATOMIC_REQUESTS is just a way to save some boilerplate for the common case, while allowing you to customize the behavior yourself in the uncommon case.
The built-in actions that come with the Django admin generally display a helpful message after they execute at the top, e.g. saying that a new object was added or what have you.
The docs show how to do it with simple actions that can be represented as methods of the custom ModelAdmin. However, with custom actions that need intermediate pages (covered further down on that same page), I am encouraged to pass the user onto another view. That's great, but it means that I no longer have access to the custom ModelAdmin instance in order to call its message_user() method... Or at least I'm not sure how to get it.
Can you tell me how to get a hold of the current ModelAdmin instance or, if there's a better way, how else to display one of those helpful little messages when I'm done in the other view?
To mimic the ModelAdmin.message_user method you only need to do the following:
from django.contrib import messages
messages.info(request, message)
Adding a message is documented here https://docs.djangoproject.com/en/dev/ref/contrib/messages/#adding-a-message and the way ModelAdmin uses it can be seen here: https://code.djangoproject.com/browser/django/trunk/django/contrib/admin/options.py#L691
Construct a LogEntry and write a custom templatetag to render messages on your intermediat page, for instance:
LogEntry.objects.log_action(
user_id=request.user.id,
content_type_id=ContentType.objects.get_for_model(yourmodel).pk,
object_id=case.id,
object_repr=force_unicode(yourmodel),
action_flag=ADDITION if created else CHANGE)
read more: Django docs (Message Framework)
Please help me understand if the following choices I have made are idiomatic/good and if not, how to improve.
1) Model Validation over Form Validation
I prefer to use the new model validation over form validation whenever possible as it seems a more DRY and fundamental way to create rules for the data. Two examples for a simple calendar-entry model:
"start must be before end"
"period must be less than (end-start)"
Is it idiomatic/good to put those at the model level so they don't have to be put into forms? If ModelForm is the best answer, then what about the case of using multiple models in the same form?
(edit: I didn't realize that multiple ModelForms can actually be used together)
2) Transferring Model Validation to a Form (not ModelForm)
(edit: It turns out reinventing the plumbing between model validation and form validation is not necessary in my situation and the solutions below show why)
Let's assume for a moment that any model validation errors I have can be directly transferred and displayed directly to the user (i.e. ignore translating model validation errors to user-friendly form validation errors).
This is one working way I came up with to do it (all in one place, no helper functions):
view_with_a_form:
...
if form.is_valid():
instance = ...
...
#save() is overridden to do full_clean with optional exclusions
try: instance.save()
except ValidationError e:
nfe= e.message_dict[NON_FIELD_ERRORS]
#this is one big question mark:
instance._errors['__all__'] = ErrorList(nfe)
#this is the other big question mark. seems unnatural
if form.is_valid()
return response...
... #other code in standard format
#send the user the form as 1)new 2)form errors 3)model errors
return form
As commented in the code:
a) Is this an idiomatic/good way to transfer model errors to a form?
b) Is this an idiomatic/good way to test for new "form" errors?
Note: This example uses non-field errors, but I think it could equally apply to field errors.
[Edited - hope this answers your comments]
I'll be brief and direct but do not want be impolite :)
1) Model Validation over Form Validation
I guess that the rule of thumb could be that as long as the rule is really associated with the model, yes, it's better to do validation at model level.
For multiple-model forms, please checkout this other SO question: Django: multiple models in one template using forms, not forgetting the attached link which is slightly old but still relevant on the dryest way to achieve it. It's too long to re-discuss it all here!
2)
a) No! You should derive your form from a ModelForm that will perform model validation for you -> you do not need to call model validation yourself
b) No! If you want to validate a model you should not try and save it; you should use full_clean -> so if your ModelForm is valid, then you know the model is valid too and you can save.
And yes, these answers still apply even with multi-model forms.
So what you should do is:
Still use ModelForm
do not worry about validating a model, because a ModelForm will do that for you.
In general if you find yourself doing some plumbing with django it's because there's another, simpler way to do it!
The 2nd pointer should get you started and actually simplify your code a lot...
1) Yes, it's perfectly valid to do validation on the model. That's why the Django folks added it. Forms aren't always used in the process of saving models, so if the validation is only done through forms, you'll have problems. Of course, people worked around this limitation in the past by overriding the save method and including validation that way. However, the new model validation is more semantic and gives you a hook into the validation process when actually using a form.
2) The docs pretty clearly say that model validation (full_clean) is run when ModelForm.is_valid is called. However, if you don't use a ModelForm or want to do additional processing, you need to call full_clean manually. You're doing that, but putting it in an overridden save is the wrong approach. Remember: "Explicit is better than implicit." Besides, save is called in many other places and ways, and in the case of a ModelForm, you'll actually end up running full_clean twice that way.
That said, since the docs say that ModelForm does full_clean automatically, I figured it would make sense to see how it handles errors. In the _post_clean method, starting on line 323 of django.forms.models:
# Clean the model instance's fields.
try:
self.instance.clean_fields(exclude=exclude)
except ValidationError, e:
self._update_errors(e.message_dict)
# Call the model instance's clean method.
try:
self.instance.clean()
except ValidationError, e:
self._update_errors({NON_FIELD_ERRORS: e.messages})
In turn, the code for _update_errors starts on line 248 of the same module:
def _update_errors(self, message_dict):
for k, v in message_dict.items():
if k != NON_FIELD_ERRORS:
self._errors.setdefault(k, self.error_class()).extend(v)
# Remove the data from the cleaned_data dict since it was invalid
if k in self.cleaned_data:
del self.cleaned_data[k]
if NON_FIELD_ERRORS in message_dict:
messages = message_dict[NON_FIELD_ERRORS]
self._errors.setdefault(NON_FIELD_ERRORS, self.error_class()).extend(messages)
You'll have to play with the code a bit, but that should give you a good starting point for combining the form and model validation errors.
I have a Django admin interface that is used almost solely as a gui form for making changes to a single postgresql table. There's also a Python script that's currently run manually from the command line whenever a change is made to the database, & I'd like to hook that up so it runs whenever someone hits "save" after making a change to a row of the table via the admin interface. If this was an entry in views.py, it looks like I'd import the script as a module and run its main function from the view (ie, Can Django use "external" python scripts linked to other libraries (NumPy, RPy2...)). I'm not sure, however, how to do this in the admin interface.
How is admin.py similar/different to a regular entry in views.py?
Where do I put the import/call to the external script - somewhere in the model, somewhere in admin.py?
I'm familiar with Python, but am fairly new to (& somewhat mystified by) "web stuff" (ie, frameworks like Django), & I'm not even sure if I'm asking this question very clearly, because I'm still a little fuzzy on the view/model concept ...
Edit: Turns out I had, in fact, found the solution by reading the documentation/tutorial, but assumed there was a difference with admin stuff. As Keith mentioned in the comments, I'm now running into permissions issues, but I guess that's a separate problem. So thanks, & maybe I'll stop second guessing myself ...
Generally, things you want to happen at 'save' time are either
Part of the model.
If so, you override the model's save method: http://docs.djangoproject.com/en/1.3/ref/models/instances/#saving-objects
You can do anything in that save method.
Part of the view function.
If so, you either extend the admin interface (not so easy), or you write your own.
One thing you might consider is defining the save_model method in your ModelAdmin. This will get executed when someone saves from the admin (but not when someone does a save outside of the admin). This approach might depend on what your requirements are, but should give you the necessary hook when doing the save from the admin.
In admin.py
class MyModelAdmin(admin.ModelAdmin):
model = models.MyModel
def save_model(self, request, obj, form, change):
# you can put custom code in here
obj.save()