I have a model whose pre_save() signal is connected to a remove service (json, REST, etc.) in the following way:
before saving locally, query the remote service, asking for remote insertion
remote service does its things, the main being checking that a relevant entry does not already exist.
On success (HTTP 201), all is good, the local model uses the remote service response to populate the db.
on failure, the service returns an HTTP 400 (the status code is debatable but that is for a different question on SO :-) )
The error response is in the following form:
{'local_model_field': [u'This element already exists']}
The local model pre_save signal then raises a ValidationError:
raise ValidationError(json_response['local_model_field'][0])
This works nicely.
Now, on the django admin, when I try to simulate the remote insertion of an already-existing object, I get a 500 page, which is fine but not ideal.
Is there any way to have the pre_save() error bubble all the way up to the ModelAdmin and be displayed as a standard error message, populated with the relevant content?
I have tried the following but to no avail:
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
"""
trying to display the right message when pre_save() fails on model save() method (after asking CC)
"""
try:
return super(ObjectAdmin, self).changeform_view(request, object_id, form_url, extra_context)
except IntegrityError as e:
self.message_user(request, e, level=messages.ERROR)
return HttpResponseRedirect(form_url)
Is a ValidationError the right thing to do? Knowing that the pre_save() must lock out any chance of ending with with duplicates both locally and remotely. The main reason is that the local/remote object creation can be made form the admin but also from other website instances/types (front-end, end-user facing, for example).
Thanks
Not sure if this is still relevant.
But the way I solved this is by creating a ModelForm:
class AuthorAdminForm(forms.ModelForm):
def clean(self):
# or some api calls here
if self.instance.id > 4:
self.instance.name = "4+"
else:
ValidationError("Id is bigger that 4")
return super().clean()
And then by adding the form to the admin model:
class AuthorAdmin(admin.ModelAdmin):
form = AuthorAdminForm
This clean() method will ensure that you could add/modify fields before you hit the model save() method and still throw a ValidationError if something goes south like a 404 error when hitting a url
Related
Error page
The website gets a 'Session matching query does not exist' error when users auto login via chrome. The error happens in the middleware that makes sure users can't login in different browsers.
from django.contrib.sessions.models import Session
class OneSessionPerUser:
def __init__(self, get_response):
self.get_response = get_response
# One-time configuration and initialization.
def __call__(self, request):
# Code to be executed for each request before
# the view (and later middleware) are called.
if request.user.is_authenticated:
current_session_key = request.user.logged_in_user.session_key
if current_session_key and current_session_key != request.session.session_key:
Session.objects.get(session_key=current_session_key).delete()
request.user.logged_in_user.session_key = request.session.session_key
request.user.logged_in_user.save()
response = self.get_response(request)
# Code to be executed for each request/response after
# the view is called.
return response
Does anybody know what might be the problem here or does anybody know how to disable the chrome auto login?
The problem is here:
if current_session_key and current_session_key != request.session.session_key:
Session.objects.get(session_key=current_session_key).delete()
I had the same problem, and as I understand it, you should change the get for filter. Because if get doesn't get an answer, he throws this error. With filter, a filter can be an empty query. So I did:
if current_session_key and current_session_key != request.session.session_key:
Session.objects.filter(session_key=current_session_key).delete()
after this, it was all good. No more strange bugs.
Edit:
After some good points made by my partner, what happened is that the session key got deleted or destroyed, so, when you apply the get, it doesn't get an answer because there is no empty value to get, that's why it spits the error. With filter, you are still searching a empty value, but in this case you can get empty queries, and because the values are unique, you won't take out two sessions at once.
Sorry if I have bad English, this is not my first language.
I'm using Django Rest Framework to serve an API. I've got a couple tests which work great. To do a post the user needs to be logged in and I also do some checks for the detail view for a logged in user. I do this as follows:
class DeviceTestCase(APITestCase):
USERNAME = "username"
EMAIL = 'a#b.com'
PASSWORD = "password"
def setUp(self):
self.sa_group, _ = Group.objects.get_or_create(name=settings.KEYCLOAK_SA_WRITE_PERMISSION_NAME)
self.authorized_user = User.objects.create_user(self.USERNAME, self.EMAIL, self.PASSWORD)
self.sa_group.user_set.add(self.authorized_user)
def test_post(self):
device = DeviceFactory.build()
url = reverse('device-list')
self.client.force_login(self.authorized_user)
response = self.client.post(url, data={'some': 'test', 'data': 'here'}, format='json')
self.client.logout()
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
# And some more tests here
def test_detail_logged_in(self):
device = DeviceFactory.create()
url = reverse('device-detail', kwargs={'pk': device.pk})
self.client.force_login(self.authorized_user)
response = self.client.get(url)
self.client.logout()
self.assertEqual(status.HTTP_200_OK, response.status_code, 'Wrong response code for {}'.format(url))
# And some more tests here
The first test works great. It posts the new record and all checks pass. The second test fails though. It gives an error saying
AssertionError: 200 != 302 : Wrong response code for /sa/devices/1/
It turns out the list view redirects the user to the login screen. Why does the first test log the user in perfectly, but does the second test redirect the user to the login screen? Am I missing something?
Here is the view:
class APIAuthGroup(InAuthGroup):
"""
A permission to allow all GETS, but only allow a POST if a user is logged in,
and is a member of the slimme apparaten role inside keycloak.
"""
allowed_group_names = [settings.KEYCLOAK_SA_WRITE_PERMISSION_NAME]
def has_permission(self, request, view):
return request.method in SAFE_METHODS \
or super(APIAuthGroup, self).has_permission(request, view)
class DevicesViewSet(DatapuntViewSetWritable):
"""
A view that will return the devices and makes it possible to post new ones
"""
queryset = Device.objects.all().order_by('id')
serializer_class = DeviceSerializer
serializer_detail_class = DeviceSerializer
http_method_names = ['post', 'list', 'get']
permission_classes = [APIAuthGroup]
Here is why you are getting this error.
Dependent Libraries
I did some searching by Class Names to find which libraries you were using so that I can re-create the problem on my machine. The library causing the problem is the one called keycloak_idc. This library installs another library mozilla_django_oidc which would turn out to be the reason you are getting this.
Why This Library Is Causing The Problem
Inside the README file of this library, it gives you instructions on how to set it up. These are found in this file. Inside these instructions, it instructed you to add the AUTHENTICATION_BACKENDS
AUTHENTICATION_BACKENDS = [
'keycloak_oidc.auth.OIDCAuthenticationBackend',
...
]
When you add this authentication backend, all your requests pass through a Middleware defined inside the SessionRefresh class defined inside mozilla_django_oidc/middleware.py. Inside this class, the method process_request() is always called.
The first thing this method does is call the is_refreshable_url() method which always returns False if the request method was POST. Otherwise (when the request method is GET), it will return True.
Now the body of this if condition was as follows.
if not self.is_refreshable_url(request):
LOGGER.debug('request is not refreshable')
return
# lots of stuff in here
return HttpResponseRedirect(redirect_url)
Since this is a middleware, if the request was POST and the return was None, Django would just proceed with actually doing your request. However when the request is GET and the line return HttpResponseRedirect(redirect_url) is triggered instead, Django will not even proceed with calling your view and will return the 302 response immediately.
The Solution
After a couple of hours debugging this, I do not the exact logic behind this middleware or what exactly are you trying to do to provide a concrete solution since this all started based off guess-work but a naive fix can be that you remove the AUTHENTICATION_BACKENDS from your settings file. While I feel that this is not acceptable, maybe you can try using another library that accomplishes what you're trying to do or find an alternative way to do it. Also, maybe you can contact the author and see what they think.
So i guess you have tested this and you get still the same result:
class APIAuthGroup(InAuthGroup):
def has_permission(self, request, view):
return True
Why do you use DeviceFactory.build() in the first test and DeviceFactory.create() in the second?
Maybe a merge of the two can help you:
def test_get(self):
device = DeviceFactory.build()
url = reverse('device-list')
response = self.client.get(url)
self.assertEqual(status.HTTP_200_OK, response.status_code)
Is this a problem with the setUp() method? From what I see, you may be setting self.authorize_user to a user that was already created on the first test.
Instead, I would create the user on each test, making sure that the user doesn't exist already, like so:
user_exists = User.objects.filter(username=self.USERNAME, email=self.EMAIL).exists()
if not user_exists:
self.authorize_user = User.objects.create_user....
That would explain why your first test did pass, why your second didn't, and why #anupam-chaplot's answer didn't reproduce the error.
Your reasoning and code looks ok.
However you are not giving the full code, there must be error you are not seeing.
Suspicious fact
It isn't be default 302 when you are not logged in.
(#login_required, etc redirects but your code doesn't have it)
Your APIAuthGroup permission does allow GET requests for non-logged-in user ( return request.method in SAFE_METHODS), and you are using GET requests (self.client.get(url))
So it means you are not hitting the endpoint that you think you are hitting (your get request is not hitting the DevicesViewSet method)
Or it could be the case you have some global permission / redirect related setting in your settings.py which could be DRF related..
eg :
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
]
}
Guess
url = reverse('device-detail', kwargs={'pk': device.pk})
might not point to the url you are thinking..
maybe there's another url (/sa/devices/1/) that overrides the viewset's url. (You might have a django view based url)
And I didn't address why you are getting redirected after force_login.
If it's indeed login related redirect, all I can think of is self.authorized_user.refresh_from_db() or refreshing the request ..
I guess some loggin related property (such as session, or request.user) might point to old instance .. (I have no evidence or fact this can happen, but just a hunch) and you better off not logging out/in for every test case)
You should make a seperate settings file for testing and add to the test command --settings=project_name.test_settings, that's how I was told to do.
I'm trying to perform a HTTP request in my post_save and in case of a failure to connect to the service I want to send a message back to the user via the response from the view. My post_save looks something like this:
def test_post_save(instance, created, **dummy):
if created:
try:
success = request_function(instance.item.id)
if success:
log.debug('Request succeeded')
except requests.exceptions.ConnectionError:
instance.stop = instance.start
instance.save()
log.debug('Request failed')
The above code works just fine, What I want to do is to add a field to the instance or in someway return a message back to the view's create function which utilizes serializer.data that the request failed.
I was hoping to achieve this without adding an extra column on the model's table. I've read about unmanaged models but I couldn't find any reference to an unmanaged field. Is there any other way for me to achieve this?
My end goal is to basically send back a message in the response to the client letting them know that the connection failed.
I have an API endpoint to trigger a payment request to a third party. The endpoint is called with some data in a POST request and goes something like the code below.
Somehow when the payment request failed (payment.is_successful = False), no data was ever saved to the database, although the debug log showed SQL INSERTs being made, and no error was thrown. The data was just dropped silently.
I've not set the database to use ATOMIC_REQUESTS (I'm using postgreSQL). And I'm using django 1.11.5 and DRF 3.6.3.
I figured that if I changed HTTP_400_BAD_REQUEST to HTTP_200_OK then my data is saved.
Why is this happening (and where is the code responsible for it)?
Is there a way to prevent it from happening (I want my data in the db for this view, no matter what) with some setting in Django/DRF?
I've temporarily set the return code to 200, but it feels wrong as the request actually failed. What code would make more sense, that doesn't cause the data to disappear?
view code
ser = MySerializer(data=request.data)
if ser.is_valid():
payment = ser.save()
else:
# do some non database stuff
return Response(result, status=status.HTTP_400_BAD_REQUEST)
if payment.is_successful:
# some more processing
return Response({'success': True}, status=status.HTTP_201_CREATED)
else:
return Response({'success': False}, status=status.HTTP_400_BAD_REQUEST) ## THIS LINE ##
serializer code
class MySerializer(serializers.ModelSerializer):
def create(...):
# call payment provider and process response
payment = MyPayment()
payment.save() # contains response from the provider that I always want to keep
return payment
DRF doesn't rollbacks transactions unless it's an exceptions and it meets some criteria.
This looks like the issue is somewhere outside Django REST framework.
Double check your middlewares for some may apply a rollback in case of a non 200 error.
One of the functionalities in a Django project I am writing is sending a newsletter. I have a model, Newsletter and a function, send_newsletter, which I have registered to listen to Newsletter's post_save signal. When the newsletter object is saved via the admin interface, send_newsletter checks if created is True, and if yes it actually sends the mail.
However, it doesn't make much sense to edit a newsletter that has already been sent, for the obvious reasons. Is there a way of making the Newsletter object read-only once it has been saved?
Edit:
I know I can override the save method of the object to raise an error or do nothin if the object existed. However, I don't see the point of doing that. As for the former, I don't know where to catch that error and how to communicate the user the fact that the object wasn't saved. As for the latter, giving the user false feedback (the admin interface saying that the save succeded) doesn't seem like a Good Thing.
What I really want is allow the user to use the Admin interface to write the newsletter and send it, and then browse the newsletters that have already been sent. I would like the admin interface to show the data for sent newsletters in an non-editable input box, without the "Save" button. Alternatively I would like the "Save" button to be inactive.
You can check if it is creation or update in the model's save method:
def save(self, *args, **kwargs):
if self.pk:
raise StandardError('Can\'t modify bla bla bla.')
super(Payment, self).save(*args, **kwargs)
Code above will raise an exception if you try to save an existing object. Objects not previously persisted don't have their primary keys set.
Suggested reading: The Zen of Admin in chapter 17 of the Django Book.
Summary: The admin is not designed for what you're trying to do :(
However, the 1.0 version of the book covers only Django 0.96, and good things have happened since.
In Django 1.0, the admin site is more customizable. Since I haven't customized admin myself, I'll have to guess based on the docs, but I'd say overriding the model form is your best bet.
use readonlyadmin in ur amdin.py.List all the fields which u want to make readonly.After creating the object u canot edit them then
use the link
http://www.djangosnippets.org/snippets/937/
copy the file and then import in ur admin.py and used it
What you can easily do, is making all fields readonly:
class MyModelAdmin(ModelAdmin):
form = ...
def get_readonly_fields(self, request, obj=None):
if obj:
return MyModelAdmin.form.Meta.fields
else: # This is an addition
return []
As for making the Save disappear, it would be much easier if
has_change_permission returning False wouldnt disable even displaying the form
the code snippet responsible for rendering the admin form controls (the admin_modify.submit_row templatetag), wouldnt use show_save=True unconditionally.
Anyways, one way of making that guy not to be rendered :
Create an alternate version of has_change_permission, with proper logic:
class NoSaveModelAdminMixin(object):
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
response = super(NoSaveModelAdmin, self).render_change_form(request, context, add, change,form_url, obj)
response.context_data["has_change_permission"] = self.has_real_change_permission(request, obj)
def has_real_change_permission(self, request, obj):
return obj==None
def change_view(self, request, object_id, extra_context=None):
obj = self.get_object(request, unquote(object_id))
if not self.has_real_change_permission(request, obj) and request.method == 'POST':
raise PermissionDenied
return super(NoSaveModelAdmin, self).change_view(request, object_id, extra_context=extra_context)
Override the submit_row templatetag similar to this:
#admin_modify.register.inclusion_tag('admin/submit_line.html', takes_context=True)
def submit_row(context):
...
'show_save': context['has_change_permission']
...
admin_modify.submit_row = submit_row