I've got a django app that has some middleware (written in the new style) that checks to see if something a user can register for has become 'full' before the user has finished the process to register for it.
If it has become full - the middleware kicks off an error message letting the user know that it's become full and links them to their registration so they can change it.
The middleware looks like this:
def __call__(self, request):
# Code to be executed for each request before
# the view (and later middleware) are called.
# ....extra logic (working without issue)....
full_problem_registrations = Registration.objects.filter(
id__in=full_problem_registration_ids
)
request.full_problem_registrations = full_problem_registrations
request.session['registration_now_full'] = False
if full_problem_registrations:
request.session['registration_now_full'] = True
for problem_reg in full_problem_registrations:
reg_url = reverse(
"camp_registrations:edit_registration", kwargs={
'person_id': problem_reg.person.id,
'registration_id': problem_reg.id,
}
)
url_string = '<a href="%s">' % reg_url
error_message = format_html(
"The %s %s registration for %s %s at %s</a> has become\
full and is no longer available. Please either remove\
or change this registration." % (
url_string,
problem_reg.course_detail.course.camp.name,
problem_reg.person.first_name,
problem_reg.person.last_name,
problem_reg.course_detail.location.name,
)
)
existing_messages = get_messages(request)
if existing_messages:
for message in get_messages(request):
# check for duplicates
if message.message == error_message:
pass
else:
messages.error(
request,
error_message,
)
else:
messages.error(
request,
error_message,
)
else:
pass
response = self.get_response(request)
# Code to be executed for each request/response after
# the view is called.
return response
This works great - the message is displayed letting everyone know the user has a problem with their registration.
However - when the user goes in to edit their registration and changes to a non-full course and saves it - when they save it redirects them to the next page.
On the next page it shows the success message - but also continues to show the error message. If the user refreshes or goes to any other page on the site, the error message goes away.
This is because the middleware is processing before the view is processed - and at that time the error is still true.
What is the best way to fix that and keep it from showing?
I figured in the middleware portion after the view is processed (which I believe would include the processing of the POST data) then we could run the check again and remove the error if it exists; but I can't figure out how to nicely remove an error from the messages.
response = self.get_response(request)
# Code to be executed for each request/response after
# the view is called.
# should I be checking here to see if the problem still exists and
# removing the message here?
I've tried that - but struggle with figuring out how to remove just that specific message from the FallbackStorage object.
Forms are processed using Class Based Views (in particular this one is an UpdateView)
As you seem to keep running that check on every request and as far I understand the "problem" messages keep interferring with others (like the "success" message) and it's difficult to tell them apart I would recommend to probably not use the messages framework for that, you could eg. just create a simple context processor that provides the data for the full registrations to every template on every request. So these messages won't get persisted into the session but freshly generated on every request.
Something like:
# context processor
def registrations(request):
# .....
full_problem_registrations = Registration.objects.filter(
id__in=full_problem_registration_ids
)
return full_problem_registrations
# add a snippet to eg. your base template
{% if full_problem_registrations.exists %}
Generate problem messages here
{% endif %}
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 use Django's built-in feature for sending me error reports every time there's a 500 on the server. I use the include_html feature to get the richer version.
I'd really like the logged-in user, if one is logged in, to be sent as part of the report. I know I can deduce it myself using the session id, but I'd really prefer it be sent so I won't have to look it up every time. How can I do that?
You need to make use of Custom Error Reports. settings.py You'll need to make your own exception reporter class. I haven't used it with the include_html feature though.
This is an example of what you'll need to do.
from django.shortcuts import render
from django.views.debug import SafeExceptionReporterFilter
class CustomExceptionReporterFilter(SafeExceptionReporterFilter):
def get_request_repr(self, request):
result = u"User Info: \n"
if request.user.is_authenticated():
user = request.user
result += u"\nUser Id: {}".format(user.id)
else:
result += u"\nUser Id: Not logged in"
result += super(CustomExceptionReporterFilter, self).get_request_repr(request)
return result
Question Clarification:
I'm trying to test if the user is authenticated or not for each page request.
I'm trying to use Authentication for the first time in Django and I am not grasping how the login view is supposed to handle authentications.
When I use #login_required, I'm redirecting to "/login" to check if the user is logged in and if not, display the login page. However, trying to redirect back to the original page is causing an infinite loop because it's sending me back to the login page over and over again.
I'm clearly not grasping how #login_required is supposed to work but I'm not sure what I'm missing. I've been searching around for awhile for an example, but everyone uses the default #login_required without the 'login_url' parameter.
So for example.. the page I'm trying to access would be...
#login_required(login_url='/login')
def index(request):
And then my login would be.. (obviously incomplete)..
Edit: just to note.. the session variables are set in another view
def login(request):
if '_auth_user_id' in request.session:
# just for testing purposes.. to make sure the id is being set
print "id:",request.session['_auth_user_id']
try:
user = Users.objects.get(id=request.session['_auth_user_id'])
except:
raise Exception("Invalid UserID")
# TODO: Check the backend session key
# this is what I'm having trouble with.. I'm not sure how
# to redirect back to the proper view
return redirect('/')
else:
form = LoginForm()
return render_to_response('login.html',
{'form':form},
context_instance=RequestContext(request)
)
Well, as you say, obviously that's not going to work, because it's incomplete. So, until you complete it, you're going to get an infinite loop - because you haven't written the code that puts _auth_user_id into request.session.
But I don't really know why you're making that test in the first place. The auth documentation has a perfectly good example of how to write a login view: get the username and password from the form, send them to authenticate to get the user object, then pass that to login... done.
Edit I think I might see where your confusion is. The login_required decorator itself does the check for whether the user is logged in - that's exactly what it's for. There's no need for you to write any code to do that. Your job is to write the code that actually logs the user in, by calling authenticate and login.
Try to call login(), see the next please:
https://docs.djangoproject.com/en/dev/topics/auth/#django.contrib.auth.login
When processing a POST request in the Django views.py file, I sometimes need to redirect it to another url. This url I'm redirecting to is handled by another function in the same Django views.py file. Is there a way of doing this and maintaining the original POST data?
UPDATE: More explanation of why I want to do this.
I have two web apps (let's call them AppA and AppB) which accept data entered into a text field by the user. When the the user clicks submit, the data is processed and detailed results are displayed. AppA and AppB expect different types of data. Sometimes a user mistakenly posts AppB type data to AppA. When this happens I want to redirect them to AppB and show the AppB results or at least have it populated with the data they entered into AppA.
Also:
The client wants two separate apps rather than combining them into just one.
I can't show the code as it belongs to a client.
UPDATE 2:
I've decided that KISS is the best principle here. I have combined the two apps into one which makes things simpler and more robust; I should be able to convince the client it's the best way to go too. Thanks for all the great feedback. If I was going to maintain two apps as described then I think sessions would be the way to do this - thanks to Matthew J Morrison for suggesting that. Thanks to Dzida as his comments got me thinking about the design and simplification.
If you faced such problem there's a slight chance that you might need to revise your designs.
This is a restriction of HTTP that POST data cannot go with redirects.
Can you describe what are you trying to accomplish and maybe then we can think about some neat solution.
If you do not want use sessions as Matthew suggested you can pass POST params in GET to the new page (consider some limitations such as security and max length of GET params in query string).
UPDATE to your update:)
It sounds strange to me that you have 2 web apps and those apps use one views.py (am I right?). Anyway consider passing your data from POST in GET to the proper view (in case data is not sensitive of course).
I think how I would probably handle this situation would be to save the post data in session, then remove it when I no longer need it. That way I can access the original post data after a redirect even though that post is gone.
Will that work for what you're trying to do?
Here is a code sample of what I'm suggesting: (keep in mind this is untested code)
def some_view(request):
#do some stuff
request.session['_old_post'] = request.POST
return HttpResponseRedirect('next_view')
def next_view(request):
old_post = request.session.get('_old_post')
#do some stuff using old_post
One other thing to keep in mind... if you're doing this and also uploading files, i would not do it this way.
You need to use a HTTP 1.1 Temporary Redirect (307).
Unfortunately, Django redirect() and HTTPResponseRedirect
(permanent) return only a 301 or 302. You have to implement it yourself:
from django.http import HttpResponse, iri_to_uri
class HttpResponseTemporaryRedirect(HttpResponse):
status_code = 307
def __init__(self, redirect_to):
HttpResponse.__init__(self)
self['Location'] = iri_to_uri(redirect_to)
See also the django.http module.
Edit:
on recent Django versions, change iri_to_uri import to:
from django.utils.encoding import iri_to_uri
use requests package.Its very easy to implement
pip install requests
then you can call any urls with any method and transfer data
in your views import requests
import requests
to post data, follow the format
r = requests.post('http://yourdomain/path/', data = {'key':'value'})
to get the absolute url in django view, use
request.build_absolute_uri(reverse('view_name'))
Thus the django view code looks like
r = requests.post(
request.build_absolute_uri(reverse('view_name')),
data = {'key':'value'}
)
where r is the response object with status_code and content attribute.
r.status_code gives the status code(on success it will be 200) and r.content gives the body of response. There is a json method(r.json()) that will convert response to json format
requests
requests.post
Just call your new view from your old view using the same request object.
Of course it won't result in a redirect as such, but if all you care about is 'transferring' data from one view to the other, then it should work.
I tested the following snippet and it works.
from django.views.generic import View
class MyOldView(View):
def post(self, request):
return MyNewView().post(request)
class MyNewView(View):
def post(self, request):
my_data = request.body
print "look Ma; my data made it over here:", my_data
You can use render and context with with it:
Render(request,"your template path", {'vad name' : var value}
You can recive vars in template :
{% If var name %}
{{ var name }}
{% endif %}
I faced a similar issue recently.
Basically I had a form A, upon submitting it another form B would show up, which contains some results + a form. Upon submitting B, i wanted to display some alert to user and keep user on B only.
The way I solved this, is by displaying the results in a <output> field, in B.
<output name="xyz" value="xyz">{{xyz}}</output>
And I used the same view for A->B and B->B. Now I just had to distinguish if the request is coming from A or B and render accordingly.
def view1(request):
if "xyz" in request.POST:
# request from B
# do some processing
return render(request, 'page.html', {"xyz":request.POST["xyz"]})
else:
# request from A
res = foo() # some random function
return render(request, 'page.html', {"xyz":res})
But this only works if form B is small and not that dynamic.
If you are using a redirect after processing the POST to AppB, you can actually get away with calling the AppB method from the AppA method.
An Example:
def is_appa_request(request):
## do some magic.
return False or True
is_appb_request = is_appa_request
def AppA(request):
if is_appb_request(request):
return AppB(request)
## Process AppA.
return HttpResponseRedirect('/appa/thank_you/')
def AppB(request):
if is_appa_request(request):
return AppA(request)
## Process AppB.
return HttpResponseRedirect('/appb/thank_you/')
This should yield a transparent experience for the end-user, and the client who hired you will likely never know the difference.
If you're not redirecting after the POST, aren't you worried about duplicate data due to the user refreshing the page?
You can redirect with session using request.session["key"] as shown below:
# "views.py"
from django.shortcuts import redirect
def my_view(request):
# Here
request.session["message"] = "success"
return redirect("https://example.com")
# "index.html"
{{ request.session.message }} {# success #}