Class Based Views Form neither Valid nor Invalid (Django) - python

I'm new to Django Class Based Views and I can't get my form to pass through neither form_valid() nor form_invalid().
I have taken most of this code from the Django allauth module, so I extend some mixins (AjaxCapableProcessFormViewMixin & LogoutFunctionalityMixin) that I do not know well.
This form is meant to allow users to change their passwords upon receiving an email. As it is now, users are able to change their password but since the form_valid() function is never triggered, they do no get redirected to the success URL as is intended. Instead the password change is registered but the users stay on the same page.
The functions dispatch(), get_form_kwargs() & get_form_class() are all triggered and behave in the way that they should. Still, it's unclear to me why they execute in the order that they do (dispatch() is triggered first, then get_form_class() and finally get_form_kwargs(). I suppose they implicitely have an order as presented in this documentation: https://ccbv.co.uk/projects/Django/4.0/django.views.generic.edit/FormView/)
I am lacking some intuition about how this works, therefore I don't know if there is a way to redirect to the success URL without passing through form_valid() because that would also solve my problem.
As is mentionned in the title, neither form_valid() nor form_invalid() is triggered after submitting a new password. The last executed bit of code is the return kwargs from the get_form_kwargs() function.
Here is my code:
class PasswordResetFromKeyView(AjaxCapableProcessFormViewMixin, LogoutFunctionalityMixin, FormView):
template_name = "account/password_reset_from_key." + app_settings.TEMPLATE_EXTENSION
form_class = ResetPasswordKeyForm
success_url = '/'
reset_url_key = "set-password"
def get_form_class(self):
return get_form_class(
app_settings.FORMS, "reset_password_from_key", self.form_class
)
def dispatch(self, request, uuid, **kwargs):
self.request = request
token = get_object_or_404(ResetToken, token=uuid)
if token.redeemed == False:
self.reset_user = token.client
self.token = token
response = self.render_to_response(self.get_context_data(token_fail=False))
else:
return super(PasswordResetFromKeyView, self).dispatch(
request, uuid, **kwargs
)
return response
def get_form_kwargs(self, **kwargs):
kwargs = super(PasswordResetFromKeyView, self).get_form_kwargs(**kwargs)
kwargs["user"] = self.reset_user
if len(kwargs) > 3:
try:
if kwargs['data']['password1'] == kwargs['data']['password2']:
self.reset_user.set_password(kwargs['data']['password1'])
self.reset_user.save()
self.token.redeemed = True
self.token.date_redeemed = datetime.now()
self.token.save()
perform_login(
self.request,
self.reset_user,
email_verification=app_settings.EMAIL_VERIFICATION,
)
else:
pass
##passwords dont match
except:
##couldnt change the password
pass
return kwargs
def form_valid(self, form, **kwargs):
form.save()
return super(PasswordResetFromKeyView, self).form_valid(form)
def form_invalid(self, form):
response = super().form_invalid(form)
if self.request.accepts('text/html'):
return response
else:
return JsonResponse(form.errors, status=400)

If both methods are not triggered, it means - you requests.method is never is 'POST'.
The class FormView calls this two methods only if post.method == 'POST':
# code from django.views.generic.edit
...
def post(self, request, *args, **kwargs):
"""
Handle POST requests: instantiate a form instance with the passed
POST variables and then check if it's valid.
"""
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
By the way in dispatch, if token.redeemed == False you should return self.form_invalid().

Related

Django - adding another check to my login

I'm creating a single page application that uses Django's session authentication on the backend. Django is using django-allauth for everything authentication-related.
I would like to add an additional step to my login where the user inputs a code and Django must verify that code too, other than password and username. How can i do that?
Note that i'm using Django as an API, so i don't need to edit the form and add another field, i only need to add another check to the authentication backend, so something very easy: if the code is right, than proceed to check username and password too, else return an error.
The problem is that i don't know where to add this check. I think i need to work on the authentication backend, but i'm stuck here.
Here is an example of the login data that django receives:
{'login': 'test', 'password': 'testPass12', 'testToken': '123abc'}
So basically, other than checking login and password like it already does now, it should check if testToken is equal to a specific value.
Here is the allauth authentication backend:
class AuthenticationBackend(ModelBackend):
def authenticate(self, request, **credentials):
ret = None
if app_settings.AUTHENTICATION_METHOD == AuthenticationMethod.EMAIL:
ret = self._authenticate_by_email(**credentials)
elif app_settings.AUTHENTICATION_METHOD == AuthenticationMethod.USERNAME_EMAIL:
ret = self._authenticate_by_email(**credentials)
if not ret:
ret = self._authenticate_by_username(**credentials)
else:
ret = self._authenticate_by_username(**credentials)
return ret
def _authenticate_by_username(self, **credentials):
username_field = app_settings.USER_MODEL_USERNAME_FIELD
username = credentials.get("username")
password = credentials.get("password")
User = get_user_model()
if not username_field or username is None or password is None:
return None
try:
# Username query is case insensitive
user = filter_users_by_username(username).get()
if self._check_password(user, password):
return user
except User.DoesNotExist:
return None
def _authenticate_by_email(self, **credentials):
# Even though allauth will pass along `email`, other apps may
# not respect this setting. For example, when using
# django-tastypie basic authentication, the login is always
# passed as `username`. So let's play nice with other apps
# and use username as fallback
email = credentials.get("email", credentials.get("username"))
if email:
for user in filter_users_by_email(email):
if self._check_password(user, credentials["password"]):
return user
return None
def _check_password(self, user, password):
ret = user.check_password(password)
if ret:
ret = self.user_can_authenticate(user)
if not ret:
self._stash_user(user)
return ret
#classmethod
def _stash_user(cls, user):
"""Now, be aware, the following is quite ugly, let me explain:
Even if the user credentials match, the authentication can fail because
Django's default ModelBackend calls user_can_authenticate(), which
checks `is_active`. Now, earlier versions of allauth did not do this
and simply returned the user as authenticated, even in case of
`is_active=False`. For allauth scope, this does not pose a problem, as
these users are properly redirected to an account inactive page.
This does pose a problem when the allauth backend is used in a
different context where allauth is not responsible for the login. Then,
by not checking on `user_can_authenticate()` users will allow to become
authenticated whereas according to Django logic this should not be
allowed.
In order to preserve the allauth behavior while respecting Django's
logic, we stash a user for which the password check succeeded but
`user_can_authenticate()` failed. In the allauth authentication logic,
we can then unstash this user and proceed pointing the user to the
account inactive page.
"""
global _stash
ret = getattr(_stash, "user", None)
_stash.user = user
return ret
#classmethod
def unstash_authenticated_user(cls):
return cls._stash_user(None)
And here is the allauth login view:
class LoginView(
RedirectAuthenticatedUserMixin, AjaxCapableProcessFormViewMixin, FormView
):
form_class = LoginForm
template_name = "account/login." + app_settings.TEMPLATE_EXTENSION
success_url = None
redirect_field_name = "next"
#sensitive_post_parameters_m
def dispatch(self, request, *args, **kwargs):
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_form_kwargs(self):
kwargs = super(LoginView, self).get_form_kwargs()
kwargs["request"] = self.request
return kwargs
def get_form_class(self):
return get_form_class(app_settings.FORMS, "login", self.form_class)
def form_valid(self, form):
success_url = self.get_success_url()
try:
return form.login(self.request, redirect_url=success_url)
except ImmediateHttpResponse as e:
return e.response
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
def get_context_data(self, **kwargs):
ret = super(LoginView, self).get_context_data(**kwargs)
signup_url = passthrough_next_redirect_url(
self.request, reverse("account_signup"), self.redirect_field_name
)
redirect_field_value = get_request_param(self.request, self.redirect_field_name)
site = get_current_site(self.request)
ret.update(
{
"signup_url": signup_url,
"site": site,
"redirect_field_name": self.redirect_field_name,
"redirect_field_value": redirect_field_value,
}
)
return ret

How to return an empty form in ModelFormMixin

DetailStory subclasses DetailView and ModelFormMixin thus presenting the DetailView of a certain story and a form at the end. However, on filling the form and submitting the data, the data is saved in the databases but it is still shown on the form (in addition to the one now displayed on the DetailView). How do I present an empty form after submitting it? (Here is the code sample)
class DetailStory(DetailView, ModelFormMixin):
model = Story
template_name = 'stories/detail_story.html'
context_object_name = 'detail'
form_class = CommentForm
def get(self, request, *args, **kwargs):
self.object = None
self.form = self.get_form(self.form_class)
return DetailView.get(self, request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.object = None
self.form = self.get_form(self.form_class)
if self.form.is_valid():
obj = self.form.save(commit=False)
obj.user = self.request.user
obj.memoir = self.get_object()
self.object = obj.save()
return self.get(request, *args, **kwargs)
def get_object(self):
item_id = crypt.decode(self.kwargs['story_id'])[0]
obj = get_object_or_404(Story, Q(privacy='public') | Q(user_id=self.request.user.id), pk=item_id)
return obj
get_form uses the request data to construct the form as per the docs
If the request is a POST or PUT, the request data (request.POST and request.FILES) will also be provided.
So simply don't make your post function go back through the get, just have it redirect to your required place or do anything differently to pointing it at the get function.
return redirect('mynamespace:story_detail', story_id=self.object.pk)
You may wish to read this answer for a list of technical details you should consider whilst making your application. In particular,
Redirect after a POST if that POST was successful, to prevent a refresh from submitting again.

Difference between self.request and request in Django class-based view

In django, for class-based view like ListView and DetailView, methods like get() or post() or other functions defined by developer take parameters include self and request. I learnt that in self these is actually a self.request field, so wha's the difference between self.request and request?
Example, this is the function in a class based view and used to handle user's login requirement:
def login(self, request):
name = request.POST['name']
pwd = request.POST['password']
user = authenticate(username=name, password=pwd)
if user is not None:
request.session.set_expiry(0)
login(request, user)
log_message = 'Login successfully.'
else:
log_message = 'Fail to login.'
return HttpResponseRedirect(reverse('blog:testindex'))
This is the function used to handle user's register:
def register(self, request):
user_name = self.request.POST['username']
firstname = self.request.POST['firstname']
lastname = self.request.POST['lastname']
pwd = self.request.POST['password']
e_mail = self.request.POST['email']
user = User.objects.create(username=user_name, first_name=firstname, last_name=lastname, email=e_mail)
user.set_password(pwd)
try:
user.save()
user = authenticate(username=user_name, password=pwd)
login(self.request, user)
except Exception:
pass
else:
return HttpResponseRedirect(reverse('blog:testindex'))
In the first function, it used data stored in request and in the second one, it used self.request, both work functionally. What's the difference?
For a subclass of View, they're the same object. self.request = request is set in view function that as_view() returns. I looked into the history, but only found setting self.request and then immediately passing request into the view function.

Django: How can I make Form Wizard take in a request object when it creates the forms?

How can I make it so that when Django's Form Tool's Wizard creates my forms, it also gives request object as a parameter? The main reason why I'm using Form Tool is to have a multi-page form. Right now to "normally" create my AnswerForm without the Wizard, it looks like
form = AnswerForm(request=request)
I'm taking in request because inside the form, I want to keep track of a key called 'question' in request.session. (The purpose of this key is to make sure that I can keep track of the Question model instance that is associated with the Answer instance that I'm trying to create through this form. I'm actually not sure if this is a good way to do this. Tips?) Anyway, right now I'm getting an error that seems to mean that somewhere in my ReviewWizard, request is not a parameter when the AnswerForm is created. I've read about how instance_dict can be used in the Wizard's as_view() in urls.py, but I don't think it would help in this case since the request object isn't available in urls.py. Can anyone help me with this? Or do you know of a better approach to do what I'm trying to do in general? Thanks for your help!!
(Ultimately the reason why I'm trying to keep track of Questions with Answers through request.session is because I think that when the first form a person sees shows up, it's a different instance from the form that gets POSTed. Since my setup() gets a random Question, it will probably not match up with the Question of the POSTed form, but maybe there's a better approach to all of this?? )
Line from urls.py:
url(r'^review_form/', ReviewWizard.as_view([AnswerForm, AnswerForm]), name='review_form'),
This is my form:
class AnswerForm(forms.Form):
answer = forms.CharField(required=True)
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request')
self.question = None
self.setup()
super(AnswerForm, self).__init__(*args, **kwargs)
self.fields['answer'].label = self.question.question
def setup(self):
if "question" in self.request.session:
self.question = Question.objects.get(id=self.request.session["question"])
else:
self.question = get_random_question()
self.request.session["question"] = self.question.id
def save(self):
del self.request.session["question"]
Answer.objects.create(
question=self.question,
author=self.request.user,
answer=self.cleaned_data['answer']
)
This is my Wizard Class in views.py (I copied it from elsewhere):
def process_form_data(form_list):
form_data = [form.cleaned_data for form in form_list]
print form_data[0]
print form_data[1]
return form_data
class ReviewWizard(SessionWizardView):
template_name = "review_form.html"
def done(self, form_list, **kwargs):
form_data = process_form_data(form_list)
return render("done.html", {"form_data": form_data})
You can use get_form_kwargs method from SessionWizardView to add your request into form kwargs.
def process_form_data(form_list):
form_data = [form.cleaned_data for form in form_list]
print form_data[0]
print form_data[1]
return form_data
class ReviewWizard(SessionWizardView):
template_name = "review_form.html"
def done(self, form_list, **kwargs):
form_data = process_form_data(form_list)
return render("done.html", {"form_data": form_data})
def get_form_kwargs(self, step):
kwargs = super(ReviewWizard, self).get_form_kwargs(step)
kwargs['request'] = self.request
return kwargs

Django: How to provide context into a FormView get() method (also using request param)

I'm trying to provide some additional context into the get() method in my FormView. I need get() because I need to run some logic first, check for a potential redirect. I also need access to the request object (because I need to check session data). Can't figure out how to do it. Simplified code below..
Attempt 1:
class LoginView(FormView):
template_name = 'members/login.html'
form_class = LoginForm
def get(self, request):
# check if to redirect
if self.request.session.get('user'):
return redirect('/dashboard/')
# render page with extra context
else:
context = super(LoginView, self).get(request)
context['message'] = self.request.session['message']
return context
No errors, but context does not come through in the template.
Attempt 2:
class LoginView(FormView):
template_name = 'members/login.html'
form_class = LoginForm
def get_context_data(self, request, **kwargs):
# check if to redirect
if self.request.session.get('user'):
return redirect('/dashboard/')
# render page with extra context
else:
context = super(LoginView, self).get_context_data(**kwargs)
context['message'] = self.request.session['message']
return context
Getting TypeError: get_context_data() takes exactly 2 arguments (1 given)
P.S. This work relates to a workaround Django's buggy messages middleware which seems to be working locally flawlessly but on live (Heroku) is not 100% reliable, renders on some pages only. Ugh, frustration setting in...
Ditch the request argument to the get_context_data method. You should also use the dispatch method to check if the user is logged in.
class LoginView(FormView):
template_name = 'members/login.html'
form_class = LoginForm
def dispatch(self, *args, **kwargs):
"""Use this to check for 'user'."""
if request.session.get('user'):
return redirect('/dashboard/')
return super(LoginView, self).dispatch(*args, **kwargs)
def get_context_data(self, **kwargs):
"""Use this to add extra context."""
context = super(LoginView, self).get_context_data(**kwargs)
context['message'] = self.request.session['message']
return context

Categories

Resources