I've noticed a strange behavior with how Django is processing my url patterns. A user should login and then be redirected to their profile page. I also have the ability for a user to edit their profile.
Here are my URL patterns for one of my apps:
urlpatterns=patterns('student.views',
(r'profile/$', login_required(profile,'student')),
(r'editprofile/$', login_required(editprofile,'student')),
)
This is for an app called student. If the user goes to /student/profile they should get the profile view. If they go to /student/editprofile they should get the editprofile view. I setup a function called login_required which does some checks on the user. It's a little more complicated than I could handle with just annotations.
Here's login_required:
def login_required(view,user_type='common'):
print 'Going to '+str(view)
def new_view(request,*args,**kwargs):
if(user_type == 'common'):
perm = ''
else:
perm = user_type+'.is_'+user_type
if not request.user.is_authenticated():
messages.error(request,'You must be logged in. Please log in.')
return HttpResponseRedirect('/')
elif request.user.is_authenticated() and user_type != 'common' and not request.user.has_perm(perm):
messages.error(request,'You must be an '+user_type+' to visit this page. Please log in.')
return HttpResponseRedirect('/')
return view(request,*args,**kwargs)
return new_view
Anyways, the weird thing is that, when I visit /student/profile, even though I get to the right page, login_required prints the following:
Going to <function profile at 0x03015DF0>
Going to <function editprofile at 0x03015BB0>
Why is it printing both? Why is it trying to visit both?
Even weirder, when I try to visit /student/editprofile, the profile page is what loads and this is what's printed:
Going to <function profile at 0x02FCA370>
Going to <function editprofile at 0x02FCA3F0>
Going to <function view_profile at 0x02FCA4F0>
view_profile is a function in a completely different app.
These two patterns:
(r'profile/$', login_required(profile,'student')),
(r'editprofile/$', login_required(editprofile,'student')),
Both match http://your-site/student/editprofile.
Try:
(r'^profile/$', login_required(profile,'student')),
(r'^editprofile/$', login_required(editprofile,'student')),
Django uses the view who's pattern matches first (see number 3 here).
Not sure why you can't use the standard #login_required decorator - it seems that your version actually provides less functionality, given that it always redirects to \, rather than the actual login view.
In any case, the reason why both are printed is because the print statement is in the top level of the decorator, and thus is executed when the urlconf is evaluated. If you put it in the inner new_view function, it will only be executed when it is actually called, and should print only the relevant view name.
Your login_required looks like it's a python decorator. Any reason you need to have it in your urls.py?
I think the print 'Going to '+str(view) line is getting evaluated when urlpatterns is read to determine which view to execute. It looks weird, but I don't think it'll hurt you.
The line print 'Going to '+str(view) will not be executed every time the view is hit, only when the url pattern is evaluated (I think). The code in new_view is the only code that will execute for certain as part of the view.
Related
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've burrowed through the mound of NoReverseMatch questions here on SO and elsewhere, but to no avail.
I have a view method, clean, and within in a redirect:
def clean(request, aid):
if request.method == 'POST':
return redirect(enrich,
permanent=True,
aid=account.id)
else:
return render(request, 'clean.html')
And a view method called enrich:
def enrich(request, aid):
return HttpResponse('this be the enrich page')
It has a path in urls.py:
path('<aid>/enrich/', views.enrich, name='enrich'),
And yet, when calling on the redirect in the clean method, I am lovingly told this by Python:
NoReverseMatch at /app2/<aid>/clean/
Reverse for 'app2.views.enrich' not found. 'app2.views.enrich' is not a valid view function or pattern name.
Which leaves me flummoxed, as app2.views.enrich does indeed exist. What am I to do? The path exists and operates correctly (if I visit /app2/<aid>/add/, I am welcomed with the proper HTTP response), but on redirect it doesn't actually seem to work, and neither do any of the other views.
Some context:
There are 3 apps in this Django project
All of these apps have proved functional along with their views
The versioning is Django 2.1 and Python 3.7.1
Disclaimer: the app is not actually called 'app2', that is simply a placeholder.
The wrong in this program is redirect(enrich, ...), the redirect first argument can't be a view function or view class.
ref from https://docs.djangoproject.com/en/2.1/_modules/django/shortcuts/#redirect:
The arguments could be:
A model: the model's get_absolute_url() function will be called.
A view name, possibly with arguments: urls.reverse() will be used
to reverse-resolve the name.
A URL, which will be used as-is for the redirect location.
The error message of Reverse for 'app2.views.enrich' not found, is because it print the view function's qualified name.
You should use str 'enrich' as first argument in redirect().
In the context of multiple apps, one has to specify the app namespace in the redirect.
return redirect('app2:enrich', ...
I will try to describe problem:
In HTML i have some AJAX call to, lets say, URL getMetaData (this is function in views.py). I that function I check POST dictionary to check is all values there. If not i want to redirect to main page ("main.html"). This main.html is rendered in function main(request) in same views.py file.
When i do this:
def main(request):
return render(request,'main.html')
def getMetaData(request):
if dictionary not valid:
return main(request)
This not working... main function is called but page stays the same.
A redirect in an ajax call will not necessary change the page, and in any case it's not a good idea to mix POST and GET like that. I suggest that you handle it all in the ajax call and redirect there instead, so your django view would be something like:
def getMetaData(request):
if is_invalid(request.POST):
return redirect_url
else:
return None
And the jquery:
$.post(post_url, data, function(new_url) {
if (new_url== null)
do_stuff();
else
window.location.replace(new_url);
});
The problem is that you do this with an AJAX call. Your Python code should work (at least, with this little example I don't see why it wouldn't), but the HTML of your home page will be the data returned to your AJAX call. There is no reason why your browser would then show it as the current page.
If you want to do something different depending on the result of the AJAX call, you should do it in Javascript. Can you show us that?
I'm having trouble with logout() while testing my project with the Django web server. This is my logout view:
def logout(request):
logout(request)
return render_to_response('main.html', {})
When I access /logout (which calls this view) I get a popup window that says Python crashed. It doesn't give me any trace in the console.
You have a slight problem of recursion there. logout is calling itself, and so on until you get a stack overflow.
Rename the view or the Django logout function when you import it.
The answer above says it all, but I find it helpful to rename external functions with some sort of unique prefix so you know where it's coming from, and because of this prefix, it will never conflict with your own functions. For example, if you're using django's logout function, you would have something like:
from django.contrib.auth import logout as auth_logout
def logout(request):
auth_logout(request)
return render_to_response('main.html', {})
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