Django signup/login not directing users to ?next=/my/url - django

Setup
I use [django-allauth][1] for user accounts.
# urls.py
url(r'^login$', allauth.account.views.login, name="account_login"),
url(r'^join$', allauth.account.views.signup, name="account_signup"),
.
# settings.py
LOGIN_REDIRECT_URL = '/me'
LOGIN_URL = '/join' # users sent here if they run into #login_required decorator
# To collect additional info if user signs up by email:
ACCOUNT_SIGNUP_FORM_CLASS = 'allauth.account.forms.WinnerSignupForm'
.
That custom signup form:
# account/forms.py
from .models import Winner, FriendCode
class WinnerSignupForm(forms.ModelForm):
"""
This is the additional custom form to accompany the default fields email/password (and maybe username)
"""
class Meta:
model = Winner
fields = ('author_display_name','signupcode',)
widgets = {'author_display_name':forms.TextInput(attrs={
'placeholder': _('Display Name'), # 'Display Name',
'autofocus': 'autofocus',
})
,
'signupcode': forms.TextInput(attrs={
'placeholder': _('Invite code (optional)'),
'autofocus': 'autofocus'
})
}
def signup(self, request, user):
# custom code that performs some account setup for the user
# just runs a procedure; there's no "return" at end of this block
I don't think my custom WinnerSignupForm is causing the issue, because the problem persists even if I disable it (i.e., I comment out this line from settings.py: ACCOUNT_SIGNUP_FORM_CLASS = 'allauth.account.forms.WinnerSignupForm')
Behaviour
0. Without ?next=/some/url parameter:
Thanks to LOGIN_REDIRECT_URL in settings.py, if I visit example.com/join or example.com/login, I'll wind up on example.com/me
That is fine.
1. If I am already logged in, everything works as expected:
A) If I visit https://example.com/login?next=/some/url,
I'm immediately forwarded to https://example.com/some/url (without being asked to log in, since I am already logged in).
I conclude the /login view is correctly reading the next=/some/url argument.
B) Similarly, if I visit https://example.com/join?next=/some/url, I'm immediately forwarded to https://example.com/some/url.
I conclude the /join view is also correctly reading the next=/some/url argument.
2. If I log in or sign up by social account, everything works as expected
This uses allauth/socialaccount
After I sign up or log in, I'm forwarded to https://example.com/some/url
However, here's the problem:
3. But! If I log in by email, ?next=/some/url is being ignored:
A) If I visit https://example.com/login?next=/some/url, I'm brought first to the /login page.
If I log in by email, I'm then forwarded to https://example.com/me
For some reason now, the ?next= is not over-riding the default LOGIN_REDIRECT_URL in settings.
(If I log in via Twitter, the ?next= paramter is correctly read, and I'm brought to https://example.com/some/url.)
B) Similarly, if I visit https://example.com/join?next=/some/url, I'm brought first to the /join (signup) page, and after successful login by email, I'm brought to /me, i.e., the fallback LOGIN_REDIRECT_URL defined in settings.py.
Inspecting the POST data in the signup/login form, the "next" parameter is there alright: {"next": "/some/url", "username": "myusername", "password": "..."}
More context
Extracts from django-allauth:
# allauth/account/views.py
from .utils import (get_next_redirect_url, complete_signup,
get_login_redirect_url, perform_login,
passthrough_next_redirect_url)
...
class SignupView(RedirectAuthenticatedUserMixin, CloseableSignupMixin,
AjaxCapableProcessFormViewMixin, FormView):
template_name = "account/signup.html"
form_class = SignupForm
redirect_field_name = "next"
success_url = None
def get_form_class(self):
return get_form_class(app_settings.FORMS, 'signup', self.form_class)
def get_success_url(self):
# Explicitly passed ?next= URL takes precedence
ret = (get_next_redirect_url(self.request,
self.redirect_field_name)
or self.success_url)
return ret
...
.
# allauth/account/utils.py
def get_next_redirect_url(request, redirect_field_name="next"):
"""
Returns the next URL to redirect to, if it was explicitly passed
via the request.
"""
redirect_to = request.GET.get(redirect_field_name)
if not is_safe_url(redirect_to):
redirect_to = None
return redirect_to
def get_login_redirect_url(request, url=None, redirect_field_name="next"):
redirect_url \
= (url
or get_next_redirect_url(request,
redirect_field_name=redirect_field_name)
or get_adapter().get_login_redirect_url(request))
return redirect_url
_user_display_callable = None
...
I'm pretty sure it was originally working when I installed [django-allauth][1] out of the box. I must have somehow interfered to break this ?next=/some/url functionality, though I can't remember the last time it was working or find out what I've done to mess things up.
Any tips on troubleshooting would be greatly appreciated.
(In case relevant -- perhaps settings are not being read correctly;
ACCOUNT_LOGIN_ON_PASSWORD_RESET = True in settings.py seems to be ignored, users have to log in after resetting their password.)

#Akshay, the following work-around worked for me.
I added the following lines to allauth/account/adapter.py, within the get_login_redirect_url sub-function.
goto = request.POST.get('next', '')
if goto:
return goto
To clarify, the result looks like this:
class DefaultAccountAdapter(object):
# no change to stash_verified_email, unstash_verified_email, etc.
# edit only get_login_redirect_url as follows
def get_login_redirect_url(self, request):
"""
Returns the default URL to redirect to after logging in. Note
that URLs passed explicitly (e.g. by passing along a `next`
GET parameter) take precedence over the value returned here.
"""
assert request.user.is_authenticated()
url = getattr(settings, "LOGIN_REDIRECT_URLNAME", None)
if url:
warnings.warn("LOGIN_REDIRECT_URLNAME is deprecated, simply"
" use LOGIN_REDIRECT_URL with a URL name",
DeprecationWarning)
else:
url = settings.LOGIN_REDIRECT_URL
# Added 20170301 - look again for ?next parameter, as work-around fallback
goto = request.POST.get('next', '')
if goto:
return goto
print "found next url in adapter.py"
else:
print "no sign of next in adapter.py"
# end of work-around manually added bit
return resolve_url(url)
# leave remainder of fn untouched
# get_logout_redirect_url, get_email_confirmation_redirect_url, etc.
I still don't know how I broke this functionality in the first place, so I won't mark my answer as accepted/best answer. It does, however, resolve the issue I had, so I am happy. Hope this is useful to others.

Related

django - No User matches the given query. Page Not found 404: User post list view breaks post detail view

I'm fairly new to django and trying to build a message board app. Everything's been running smoothly up until I tried to add functionality to display a list of posts by author. I got it working but then my post detail view started throwing a 'No User matches the given query. Page Not found 404' error and I can't seem to figure out why. Can anyone help?
views.py
class UserPostList(generic.ListView):
model = Post
# queryset = Post.objects.filter(status=1).order_by('-created_on')
template_name = 'user_posts.html'
paginate_by = 6
def get_queryset(self):
"""
Method to return posts restricted to 'published' status AND to
authorship by the user whose username is the parameter in the
url.
"""
user = get_object_or_404(User, username=self.kwargs.get('username'))
return Post.objects.filter(
status=1, author=user
).order_by('-created_on')
class FullPost(View):
def get(self, request, slug, *args, **kwargs):
"""
Method to get post object.
"""
queryset = Post.objects.filter(status=1)
post = get_object_or_404(queryset, slug=slug)
comments = post.comments.order_by('created_on')
liked = False
if post.likes.filter(id=self.request.user.id).exists():
liked = True
return render(
request,
"full_post.html",
{
"post": post,
"comments": comments,
"liked": liked
},
)
# I'll be adding a comment form in here too
urls.py
urlpatterns = [
...
path('<slug:slug>/', views.FullPost.as_view(), name='boards_post'),
...
path('<str:username>/', views.UserPostList.as_view(), name='user_posts'),
...
]
Error message
(When trying to view a single post (previously working) after adding the UserPostList view and route)
Using the URLconf defined in mhcmsgboard.urls, Django tried these URL patterns, in this order:
1. admin/
2. summernote/
3. register/ [name='register']
4. profile/ [name='profile']
5. login/ [name='login']
6. logout/ [name='logout']
7. new/ [name='create_post']
8. <slug:slug>/update/ [name='update_post']
9. <slug:slug>/delete/ [name='delete_post']
10. <str:username>/ [name='user_posts']
The current path, test-post/, matched the last one.
for <str:name> in path "-" not allowed to use in name,
when you use Slug both paths are equal.
urlpatterns = [
...
path('<slug:slug>/', views.FullPost.as_view(), name='boards_post'),
...
path('<slug:username>/', views.UserPostList.as_view(), name='user_posts'),
]
there are 2 simple ways
use sub path for one or both paths : user_post/<slug:username>/
Use re_path and define the regex to change path
exp:
re_path(r'^(?P\<username>\w+)/$', views.UserPostList.as_view()),
The problem is that you match update_post, delete_post and user_posts URL's to the root. As the error message explains, the current request is made against user_posts. And it seems that you don't have a user called test-post.
You could solve it e.g. with the following URL conf:
urlpatterns = [
...
path('board/<slug:slug>/', views.FullPost.as_view(), name='boards_post'),
...
path('posts/<str:username>/', views.UserPostList.as_view(), name='user_posts'),
...
]
That way, each path is unique and Django knows which View it has to call.

Page view refers to id, whil path is not asking for one

I want to load a default django page. Nothing fancy. However, the error I get, hints at an id that is incorrectly set.
"Field 'id' expected a number but got 'zoekboek'."
The confusing things here (I am a django beginner, so I wouldn't be surprised if this is not confusing at all for you):
the path for this page in the urls.py is not asking for an id.
the view is not querying anything yet (I found some posts that had similar errors,
but related to a filter).
the debug info points to another view that indeed is requesting an id.
when I add a slash at the beginning of the path, the error is gone!
The code
urls.py
urlpatterns = [
path('', views.scholen, name='scholen'),
path('<school_id>', views.school_detail, name='school_detail'),
path('<school_id>/<groep_id>', views.school_groep, name='school_groep'),
path('<school_id>/<groep_id>/<UserProfile_id>', views.leerling_page, name='leerling_page'),
path('zoekboek', views.zoekboek, name='zoekboek'),
]
views.py
from django.shortcuts import render, redirect, reverse, get_object_or_404
from books.models import Book, Rating
from .models import School, Groep
from profiles.models import UserProfile, Hobby, Sport
from django.contrib.auth.models import User
# Create your views here.
def scholen(request):
"""
Homepage for participating
schools.
"""
scholen = School.objects.all()
context = {
'scholen': scholen,
}
return render(request, 'schools/school_landing.html', context)
def school_detail(request, school_id):
"""
Details of individual schools.
"""
school = get_object_or_404(School, pk=school_id)
groep = Groep.objects.filter(school=school)
context = {
'school': school,
'groep': groep,
}
return render(request, 'schools/school_detail.html', context)
def school_groep(request, school_id, groep_id):
"""
Details of groep.
"""
school = get_object_or_404(School, pk=school_id)
groep = get_object_or_404(Groep, pk=groep_id)
a = groep.naam
kinderen = UserProfile.objects.filter(groep=a)
context = {
'school': school,
'groep': groep,
'kinderen': kinderen,
}
return render(request, 'schools/school_groep.html', context)
def leerling_page(request, school_id, groep_id, UserProfile_id):
"""
Personal page of school kids.
"""
profile = get_object_or_404(UserProfile, pk=UserProfile_id)
# If viewer is owner of page, viewer can edit
owner = False
if request.user == profile.user:
owner = True
context = {
'profile': profile,
'owner': owner,
}
return render(request, 'schools/leerling_page.html', context)
def zoekboek(request):
"""
Page for kids to search their favorite book
"""
context = {
}
return render(request, 'schools/zoek_boek.html', context)
Is this enough information?
Simple fix: move path('zoekboek', views.zoekboek, name='zoekboek'), from the last place to the second place in your urls.
Why?
Because Django URLs are resolved using regular expressions; the docs say here in point 3:
Django runs through each URL pattern, in order, and stops at the first one that matches the requested URL, matching against path_info.
Since your URL path path('<school_id>', views.school_detail, name='school_detail'), is very generic, it matches any string including the string zoekboek; so the request to zoekboek falls into the second line in your URL conf and gets routed to the view school_detail() and a school_id is expected for that view.
Suggestion: to make the URL handling easier and so you can order the URL paths however you like, you could change the URL a bit and add a prefix (for example school/) so that not any string matches the URL paths. For example, this schould work:
urlpatterns = [
path('', ...),
path('school/<school_id>', ...),
path('school/<school_id>/<groep_id>', ...),
path('school/<school_id>/<groep_id>/<UserProfile_id>', ...),
path('zoekboek', ...),
]

Django 2 Test response for valid and invalid form

I made a TestCase to check if I would get the proper response and page redirection, but It's not working as I thought it would. When I tried a valid form I got the response I expected, but when I made it invalid, I still got the same response.
views.py (I left off the 'GET' 'else:')
def create_employee_profile(request):
if request.POST:
name_form = EmployeeNameForm(request.POST)
if name_form.is_valid():
new_name_form = name_form.save()
return redirect(new_name_form)
else:
return render(request,
'service/create_or_update_profile.html',
{'name_form': name_form}
)
Test.py
class TestCreateEmployeeProfileView(TestCase):
def test_redirect_on_success(self):
response = self.client.post('/service/', {
'first_name': 'Test', # Required
'middile_name': 'Testy', # Optional
'last_name': '', # Required
})
self.assertEqual(response.status_code, 200)
I guess while I am question, I might as well ask how to access the redirect to test that as well.
On success, the new path should be /service/name/1/, the '1' being the 'pk' of the created object.
I know I've seen SimpleTestCase, but I haven't found a good example or tutorial on how to use it.
If you always get a 200, that is because your form is always invalid. Your view redirects on successful save, which is a 302.
The way to test that the form has saved is to check that the new item is indeed in the database:
self.assertTrue(Employee.objects.filter(first_name='Testy').exists())
or whatever.
Here are two scenarios:
Your form is valid, it will be saved and will be redirected to new_name_form that means a successful redirection. Since it is a successful redirection, you will get status code 200.
The same thing will happen when your form is invalid, i.e it will start rendering the create_or_update_profile page. Hence successful rendering and 200 status code.
So in either way, you will get successful redirection.
If you want to check the form, this is the better approach to do:
from form import EmployeeNameForm
class TestCreateEmployeeProfileView(TestCase):
def test_redirect_on_success(self):
form = UserForm(data='first_name': 'Test', # Required
'middile_name': 'Testy', # Optional
'last_name': '',)
self.assertTrue(form.is_valid())
def test_redirect_on_failure(self):
form = UserForm(data='first_name': 'Test', # Required
'middile_name': 'Testy', # Optional
'last_name': '',)
self.assertFalse(form.is_valid())
There will be no need to test the redirection. It surely will work fine, if the form is valid.
Hope that helped.

Most appropriate way to redirect page after successful POST request in Django

I have build a view and a form in Django1.5. If the POST request is successful (based on some values I set) then I need the page to redirect to another URL which is created simultaneously.
Otherwise, if the POST was not successful I need to stay on the same page. Right now I have solved the problem as following but I am quite sure this is not the best way to do it:
This is a part of my view:
def layer_create(request, template='layers/layer_create.html'):
if request.method == 'GET':
....
elif request.method == 'POST':
out = {}
...
new_table = 'something that comes from the form'
if form.is_valid():
...
try:
...
out['success'] = True
except:
...
out['success'] = False
finally:
if out['success']:
status_code = 200
# THIS IS THE PART WHICH I THINK I CAN IMPROVE
template = '/something/workspace:' + new_table + '/metadata'
else: # if form not valid
out['success'] = False
return render_to_response(template, RequestContext(request, {'form': form}))
This part of the code:
template = '/something/workspace:' + new_table + '/metadata'
seems very ugly to me. But as I am quite new in Django I am not sure how to approach this matter.
A side note first about Django 1.5 - you're highly advised to upgrade to a supported version like 1.8.
Redirecting
For redirecting you can use the redirect shortcut. (Or HttpResponseRedirect)
from django.shortcuts import redirect
# out of a view context
return redirect('/url/to/redirect/to/')
Building URLs
Indeed - as you did mention, your attempt with template = '/something/workspace:' + new_table + '/metadata' is not the cleanest way :)
Django provides a really nice way with the URL dispatcher.
A complete solution here would go too far (or definitely would require more detailed information about your project structure) - I would recommend you to dive into the Django URL dispatcher.
In short you would do something like:
# app/urls.py
urlpatterns = [
#...
url(r'^workspace/(?P<id>[0-9]+)/metadata/$', views.workspace_detail, name='workspace-detail-metadata'),
#...
]
Then you are able to reverse your URL patterns:
from django.core.urlresolvers import reverse
url = reverse('workspace-detail-metadata', kwargs={'id': 123})
# would result in:
# .../workspace/123/metadata/
After all, I have used the "reverse" method as follows:
layer = 'geonode:' + new_table
return HttpResponseRedirect(
reverse(
'layer_metadata',
args=(
layer,
)))
Where my urls.py file includes:
url(r'^(?P<layername>[^/]*)/metadata$', 'layer_metadata', name="layer_metadata"),
As described here this is the most appropriate way to do it.

How can I access URL parameters from within a BasePermission?

I'm trying to write a custom rest_framework Permission to prevent users from querying information that's not of the same company as them. Unfortunately, I can't seem to access any of the URL's parameters from within has_permission() or has_object_permissions().
Here's the beginning of my router:
# Create a basic router
router = routers.SimpleRouter()
# Establish some variables to assist with nested routes
root_elem = 'companies'
root_elem_id = '/(?P<company_id>[0-9]+)'
loca_elem = '/locations'
loca_elem_id = '/(?P<location_id>[0-9]+)'
# Companies will be the root from which all other relations branch
router.register(r'' + root_elem, views.CompanyViewSet)
router.register(r'' + root_elem + root_elem_id + loca_elem,
views.LocationViewSet)
Here's my custom permission:
# Only permit actions originating from location managers or company admins
class IsLocationManagerOrHigher(BasePermission):
# Checked when displaying lists of records
def has_permission(self, request, *args, **kwargs):
is_correct_level = False
# Admins can see every location if their location_id
# matches a location that's a child of the company
# specified in the URL
if request.employee.is_admin:
is_correct_level = True
return request.user and is_correct_level
# Checked when viewing specific records
def has_object_permission(self, request, view, obj):
is_correct_level = False
# Admins can see location details if their location's company_id
# matches a Location's company_id
if request.employee.is_admin:
is_correct_level = True
# Managers can see location details if it's their location
elif obj.id == request.employee.location_id and request.employee.is_manager:
is_correct_level = True
return request.user and is_correct_level
Right now checking request.employee.is_admin is only half of what I need - I also need to access the company_id from the URL and make sure it matches the admin's location's company_id:
# Pseudocode
try:
user_location = Location.objects.get(id=request.employee.location_id)
return user_location.company_id == kwargs['company_id']
except ObjectDoesNotExist:
pass
I've yet to figure out how to pass these parameters into the Permission so that it can perform this extra step. Or perhaps there's a better way of accomplishing what I'm trying to do?
If you can't pass them in directly (which would be preferable), they are available on the request object:
company_id = request.resolver_match.kwargs.get('company_id')
request.resolver_match.args and request.resolver_match.kwargs contain the positional/keyword arguments captured in your url.
As an alternative to the correct response posted by knbk, you can also get the URL parameters using the view object passed to has_permission method. Like this:
company_id = view.kwargs.get('company_id')