Running Django, I have a view to delete logged-in user on POST request.
#login_required
def delete_user(request):
"""Delete user from DB."""
if request.method == "POST":
get_object_or_404(User, pk=request.user.pk).delete()
messages.success(request, _("User deleted!"), extra_tags="success")
return redirect("home")
return render(request, "accounts/delete-user.html")
HTML form is:
<form method="POST" name="delete-user-form" id="delete-user-form">
{% csrf_token %}
<button class="mt-4 btn btn-danger" name="delete-user-btn" id="delete-user-btn" type="submit">{% translate "Delete my profile" %}</button>
</form>
My test class with user:
class ViewsWithLoggedInUserTest(TestCase):
#classmethod
def setUpClass(cls):
super().setUpClass()
User.objects.create_user(
email="test#test.com",
password="test",
first_name="TestFirstName",
last_name="TestLastName",
hometown="Kiev",
facebook_link="https://www.facebook.com/profile.php?id=1000",
contacts="+380991111111",
start_coordinates="50.45, 30.52",
avatar="avatar/default_avatar.jpg",
)
def setUp(self):
self.user = User.objects.get(email="test#test.com")
self.client.force_login(user=self.user)
print("setUp")
def test_delete_user_post(self):
"""Test delete user post."""
response = self.client.post(path=reverse("delete_user"))
self.assertFalse(self.user)
self.assertEqual(response.status_code, 302)
self.assertRedirects(response, expected_url=reverse("home"))
I can not undrstand how to test this function, my test_delete_user_post gives me
AssertionError: <User: test#test.com> is not false
It's not working because the variable self.user still holds the previously assigned value.
Use the refresh_from_db() method to update the variable with fresh value.
Also, to test deletion, you have to test if the DoesNotExist exception is raised or not. Testing using assertFalse will not work.
Code example:
def test_delete_user_post(self):
response = self.client.post(path=reverse("delete_user"))
with self.assertRaises(User.DoesNotExist):
# Django will try to fetch the new value from the database
# but since it's been deleted, DoesNotExist exception
# will be raised
self.user.refresh_from_db()
...
A (less complex/ more obvious?) alternative to xyres' answer
# setUp created self.user which this test should delete ...
def test_delete_user_post(self):
response = self.client.post(path=reverse("delete_user"))
self.assertFalse( User.objects.filter( pk=self.user.pk).exists() )
Related
I'm writing a custom login functionality in the Django rest framework. But I can't check if the password is correct or not.
class LoginView(APIView):
def post(self, request):
username=request.data["username"]
password=request.data["password"]
user=User.objects.filter(username=username)
if user is None:
return Response({"response":"No User exist"})
if user.check_password(password):
return Response({"response":"correct Password"})
return Response({"data":"done"})
the problem is check_password function is not working.Is there any right way to do that or do I miss something in between?
Check the documentation here https://docs.djangoproject.com/en/4.1/topics/auth/passwords/#django.contrib.auth.hashers.check_password
You need to compare the plain password in request.data["password"], with the password of the user in the DB.
from django.contrib.auth import authenticate
class LoginView(APIView):
def post(self, request):
username=request.data["username"]
password=request.data["password"]
user = authenticate(request, username=username, password=password)
if user is None:
return Response({"response":"No User exist"})
else:
return Response({"response":"correct Password"})
Take a look at this one:
def login(request):
if request.method == 'POST':
username = request.POST['username']
password = request.POST['password']
user = auth.authenticate(request, username=username, password=password)
if user is not None:
auth.login(request, user)
return redirect('Home')
else:
messages.info(request, 'Invalid Credential')
return redirect('login')
else:
return render(request, 'login.html')
pass this in your template:
<div class="text-center text-danger">
{% for message in messages %}
<h5>{{ message }}</h5>
{% endfor %}
<br>
</div>
Fixed this issue by making an updation, I changed the filter() to get(), as filter() will return a query set but get () will return an object.
Updated Code :
class LoginView(APIView):
def post(self, request):
username=request.data["username"]
password=request.data["password"]
user=User.objects.get(username=username)
if user is None:
return Response({"response":"No User exist"})
if not user.check_password(password):
return Response({"response":"incorrect Password"})
return Response({"data":"done"})
If anyone still couldn't understand the difference of both functions (get and filter). Please check out the link Difference between Django's filter() and get() methods.
Thanks everyone who helps for the solution.
I'm having an issue trying to get valid form in pytest. I am testing data changes through a Django Admin action. No matter what data I post to the form, form.is_valid() will always return False. Passing a dictionary to the form directly works, however I would like to be able to test through the action to make sure that the action filters out the locked records.
# test_admin.py
#pytest.mark.django_db
class BaseTestCase(TestCase):
"""Base TestCase with utilites to create user and login client."""
def setUp(self):
"""Class setup."""
self.index_url = '/'
self.login()
self.django_db_setup()
def create_user(self):
"""Create user and returns username, password tuple."""
username, password = 'testadmin', 'password123'
user = User.objects.create_superuser(
username,
'admin#test.com',
password,
first_name='Admin',
last_name='Account',
)
self.user = user
return (username, password)
def login(self):
"""Log in client session."""
username, password = self.create_user()
self.client.login(username=username, password=password)
#staticmethod
def django_db_setup():
call_command('loaddata', 'fixtures/fixture.json')
class AdminTestCase(BaseTestCase):
def test_responsible_list(self):
products = Product.objects.filter(pk__in=[230005, 229724])
form_data = {
'action': 'set_product_class',
'apply': 'Submit',
'product_class': '1',
ACTION_CHECKBOX_NAME: products.values_list('pk', flat=True),
}
self.client.post('/admin/store/product/', form_data, follow=True)
# Assert product_classes have changed
# actions.py
def set_product_class(modeladmin, request, queryset):
# Exclude products that are locked
queryset = queryset.exclude(is_locked=True)
form = None
if 'apply' in request.POST:
form = SetProductClassForm(data=request.POST)
if form.is_valid():
# action code
# forms.py
class SetProductClassForm(forms.Form):
_selected_action = forms.CharField(widget=forms.MultipleHiddenInput)
product_class = forms.ModelChoiceField(ProductClass.objects.all())
I put a pdb trace above the line in the action where it checks if form.is_valid():
When I run this in pytest:
(Pdb) form.is_valid()
False
(Pdb) request.POST
<QueryDict: {u'action': [u'set_product_class'], u'apply': [u'Submit'], u'product_class': [u'1'], u'_selected_action': [u'230005', u'229724']}>
However if I run the same code through the Django shell, the form is valid:
(Pdb) form.is_valid()
True
(Pdb) request.POST
<QueryDict: {u'action': [u'set_product_class'], u'apply': [u'Submit'], u'product_class': [u'1'], u'_selected_action': [u'230005', u'229724']}>
Why can I not get a valid form through pytest?
Checking form.errors gave the following:
{'product_class': [u'Select a valid choice. That choice is not one of the available choices.']}
The issue was being caused by the data for the ProductClass being selected in the form was missing in the fixture. Adding the data to the test db fixed the issue.
I have the following code:
user.py
class User(Base, UserMixin):
username = StringField(max_length=10, required=True, unique=True)
first_name = StringField(max_length=32, required=True)
last_name = StringField(max_length=32, required=True)
def get_id(self):
return self.username
app.py
app = Flask(
__name__,
static_folder='./static',
template_folder='./static/templates'
)
app.config.from_pyfile(CONFIG_FILE)
app.register_blueprint(LOGIN)
#app.before_request
def request_setup():
if current_user.is_authenticated:
g.user = current_user.get_id()
else:
g.user = None
lm = LoginManager()
lm.init_app(app)
lm.login_view = 'login.user_login'
#lm.user_loader
def load_user(username):
return LoginController.get_user(username)
login_controller.py
LOGIN = Blueprint('login', __name__)
class LoginController(object):
#staticmethod
def get_user(username):
return User.objects(username=username).first()
#staticmethod
#LOGIN.route('/login', methods=['GET', 'POST'])
def user_login():
if request.method == 'GET':
return render_template('login.html')
username = request.form.get('username')
user = User.objects(username=username).first()
if not user:
flash('User does not exist!', 'error')
return flask.redirect(url_for('home'))
else:
login_user(user)
return flask.redirect(url_for('home'))
index.html
{% if current_user.is_authenticated %}
<a class="item">My Profile</a>
<a class="item" href="{{ login.user_logout }}">Logout</a>
{% else %}
<a class="item" id="login_button">Register/Login</a>
{% endif %}
As far as I can see, I have everything set up right. However, logging-in does not seem to actually do anything.
The current_user is still the anonymous user.
The Register/Login link still shows up, instead of the My Profile link.
For other parts of the site, where I have set #login_required, it still redirects me to the login page.
Help?
def get_id(self) must be return id, not username. Try this:
def get_id(self):
return self.id
The following is written in the documentation:
get_id()
This method must return a unicode that uniquely identifies this user, and can be used to load the user from the user_loader callback.
Note that this must be a unicode - if the ID is natively an int or
some other type, you will need to convert it to unicode.
I have been trying to use the Google reCAPTCHA on a website that I've been making. The captcha loads on the webpage but I've been unable to validate it using several methods. I've tried the recaptcha validation using the method given at
How to use Python plugin reCaptcha client for validation? but I think it's outdated as it no longer works and it is referring to challenges whereas the one I'm trying to use is the new 'checkbox' reCAPTCHA v2 by Google or maybe I need to make changes in my settings after installing recaptcha-client or django-recaptcha.
Please help!
Here is a simple example to verify Google reCAPTCHA v2 within Django view using requests library (http://docs.python-requests.org/en/latest/):
import requests
from django.conf import settings
def get_client_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
def grecaptcha_verify(request):
if request.method == 'POST':
response = {}
data = request.POST
captcha_rs = data.get('g-recaptcha-response')
url = "https://www.google.com/recaptcha/api/siteverify"
params = {
'secret': settings.RECAPTCHA_SECRET_KEY,
'response': captcha_rs,
'remoteip': get_client_ip(request)
}
verify_rs = requests.get(url, params=params, verify=True)
verify_rs = verify_rs.json()
response["status"] = verify_rs.get("success", False)
response['message'] = verify_rs.get('error-codes', None) or "Unspecified error."
return HttpResponse(response)
There is a third-party Django app to implement the new reCAPTCHA v2 here:
https://github.com/ImaginaryLandscape/django-nocaptcha-recaptcha
After installing it, add the following lines to the following files:
# settings.py
NORECAPTCHA_SITE_KEY = <the Google provided site_key>
NORECAPTCHA_SECRET_KEY = <the Google provided secret_key>
INSTALLED_APPS = (
....
'nocaptcha_recaptcha'
)
#forms.py
from nocaptcha_recaptcha.fields import NoReCaptchaField
class YourForm(forms.Form):
.....
captcha = NoReCaptchaField()
# In your template, add the following script tag:
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
Google has changed the API around, we need to use a POST request now. Here a re-usable solution in case you need to do the validation in more than one django view:
utils.py
# django imports
from django.conf import settings
from django.views.generic.base import View
from django.http import HttpResponseForbidden
# 3rd-party imports
import requests
from ipware import get_client_ip
def is_recaptcha_valid(request):
"""
Verify if the response for the Google recaptcha is valid.
"""
return requests.post(
settings.GOOGLE_VERIFY_RECAPTCHA_URL,
data={
'secret': settings.RECAPTCHA_SECRET_KEY,
'response': request.POST.get('g-recaptcha-response'),
'remoteip': get_client_ip(request)
},
verify=True
).json().get("success", False)
def human_required(view_func):
"""
This decorator is aimed to verify Google recaptcha in the backend side.
"""
def wrapped(request, *args, **kwargs):
if is_recaptcha_valid(request):
return view_func(request, *args, **kwargs)
else:
return HttpResponseForbidden()
return wrapped
then:
views.py
from utils import human_required
class MyView(View):
#human_required
def post(request, *args, **args):
pass
Note we are using django-ipware in this solution to get the ip address, but this is up to you. Also, don't forget to add GOOGLE_VERIFY_RECAPTCHA_URL and RECAPTCHA_SECRET_KEY to the django settings file!
views.py
def login(request):
if request.method == 'POST':
username = request.POST['username']
password = request.POST['password']
user = auth.authenticate(request, username=username, password=password)
if user is not None:
if user.is_active:
auth.login(request, user)
''' Begin reCAPTCHA validation '''
recaptcha_response = request.POST.get('g-recaptcha-response')
url = 'https://www.google.com/recaptcha/api/siteverify'
values = {
'secret' : settings.GOOGLE_RECAPTCHA_SECRET_KEY,
'response' : recaptcha_response
}
data = urllib.parse.urlencode(values).encode("utf-8")
req = urllib2.Request(url, data)
response = urllib2.urlopen(req)
result = json.load(response)
''' End reCAPTCHA validation '''
if result['success']:
return redirect('index')
else:
messages.error(request, 'Invalid reCAPTCHA. Please try again.')
return redirect('login')
else:
messages.info(request, 'Wrong Credentials!!! enter right username or password')
return redirect('login')
else:
return render(request, 'login.html')
login.html
<form action="{% url 'login' %}" method="post">
{% csrf_token %}
<div class="body bg-gray">
<div class="form-group">
<input type="text" name="username" class="form-control" placeholder="Username"/>
</div>
<div class="form-group">
<input type="password" name="password" class="form-control" placeholder="Password"/>
</div>
<div class="form-group">
<input type="checkbox" name="remember_me"/> Remember me
</div>
</div>
<div class="footer">
<button type="submit" class="btn bg-olive btn-block">Sign me in</button>
<p>I forgot my password</p>
Register a new membership
</div>
<br><br>
<script src='https://www.google.com/recaptcha/api.js'></script>
<div class="g-recaptcha" data-sitekey="(enter your key here that is private or authenticated on google recapthcha)"></div>
</form>
settings.py
INSTALLED_APPS = [
....
....
'captcha'
]
GOOGLE_RECAPTCHA_SECRET_KEY ='6LdXBLAUAMlGYqqyDESeHKI7-'
RECAPTCHA_PUBLIC_KEY = '6LdXBLAUAAAAAP3oI1VPJgA-VHXoj'
RECAPTCHA_PRIVATE_KEY = '6LdXBLAUAAAAAGYqqyDESeHKI7-'
''' you have to register your domain to get the access of these keys. or you can
register your localhost also to test this after uploading on the server you can
register with real domain and change the keys.
don't forget to like if you find it helpful.'''
'https://www.google.com/recaptcha/intro/v3.html' -> 'admin console' where you can
register your domain or localhost and get your key.
Expanding on the answer given by #trinchet, here is a simple modification of the FormView Django class to automatically handle Google's reCAPTCHA v2.
class RecaptchaFormView(FormView):
""" This class handles Google's reCAPTCHA v2. """
recaptcha_ok = None
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['grecaptcha_site_key'] = settings.RECAPTCHA_SITE_KEY
return context
def get_form(self):
form = super().get_form()
if self.recaptcha_ok == False:
form.add_error(None, "Invalid reCAPTCHA, please try again.")
return form
def post(self, request, *args, **kwargs):
self.recaptcha_ok = is_recaptcha_valid(request)
return super().post(self, request, *args, **kwargs)
Don't forget to include the is_recaptcha_valid function provided by #trinchet (see his answer), reCAPTCHA keys in settings.py and the reCAPTCHA code in the template (use {{ grecaptcha_site_key }} as the site key).
This is how I handle the proposed question:
views.py
from django.contrib.auth.views import LoginView, LogoutView
from django.conf import settings
from authentication.forms import MyAuthenticationForm
class MyLoginView(LoginView):
template_name = 'authentication/login.html'
form_class = MyAuthenticationForm
def get_client_ip(self):
x_forwarded_for = self.request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = self.request.META.get('REMOTE_ADDR')
return ip
def get_form_kwargs(self):
kwargs = super(MyLoginView, self).get_form_kwargs()
if self.request.method in 'POST':
kwargs['g-recaptcha-response'] = self.request.POST.get('g-recaptcha-response')
kwargs['remote_ip'] = self.get_client_ip()
return kwargs
def get_context_data(self, **kwargs):
context = super(MyLoginView, self).get_context_data(**kwargs)
# To use in the template
context['recaptcha_challenge_secret'] = settings.G_RECAPTCHA_CHALLENGE_SECRET
return context
forms.py
import requests
from django.contrib.auth.forms import AuthenticationForm
from django.conf import settings
from django.forms import ValidationError
from django.utils.translation import ugettext_lazy as _
class MyAuthenticationForm(AuthenticationForm):
def __init__(self, *args, **kwargs):
self.g_recaptcha_response = kwargs.pop('g-recaptcha-response', None)
self.remote_ip = kwargs.pop('remote_ip', None)
super(MyAuthenticationForm, self).__init__(*args, **kwargs)
def clean(self):
cleaned_data = super(MyAuthenticationForm, self).clean()
self.verify_captcha()
return cleaned_data
def verify_captcha(self):
if self.g_recaptcha_response:
data = {
'secret': settings.G_RECAPTCHA_VERIFY_SECRET,
'response': self.g_recaptcha_response,
'remoteip': self.remote_ip
}
response = requests.post(settings.G_RECAPTCHA_VERIFICATION_URL, data=data)
result = response.json()
if result['success']:
return
raise ValidationError(
_('Invalid reCAPTCHA challenge.'),
code='invalid_recaptcha_challenge'
)
On many forms we have a cancel button. Trying to keep things DRY it seemed that a helper function was in order. The function if_form_cancel() would check to see if the cancel button had been clicked and then redirect accordingly (with destination passed to the function). However, the redirect is ignored when we use a helper function (it works just fine in-line).
view.py
from helper.py import if_form_cancel
...
def index():
return render_template('index.html')
def edit():
...
form = EditForm(obj=user, csrf_enabled=False)
if request.method == 'POST':
if_form_cancel(url_for('.index'))
if form.validate():
...
return redirect(url_for('.index'))
return render_template('edit.html')
helper.py
from flask import request, redirect, flash
def if_form_cancel(destination='/'):
try:
if request.form['cancel']:
flash(u'Operation canceled at user request')
return redirect(destination)
except KeyError:
pass
edit.html
....
<input type="submit" name="submit" value="Submit" />
<input type="submit" name="cancel" value="Cancel" />
....
Result if cancel button is clicked: helper function is called, correct message is flashed, but redirect does not occur. Is it possible to redirect from a helper function like this? If so how? What are we doing wrong?
You aren't returning the result of if_form_cancel in the edit function. But you only want to return if the user cancelled. An exception would work well here, and it looks like werkzeug has such an exception.
See if this will work (untested).
from werkzeug.routing import RequestRedirect
def if_form_cancel(destination='/'):
try:
if request.form['cancel']:
flash(u'Operation canceled at user request')
raise RequestRedirect(destination)
except KeyError:
pass
Alternatively, you could just have the form_cancel helper check the form and return True if the user cancelled, and do the redirect in the view itself.
def edit():
...
form = EditForm(obj=user, csrf_enabled=False)
if request.method == 'POST':
if form_cancel():
return redirect(url_for('.index'))
if form.validate():
...
return redirect(url_for('.index'))
return render_template('edit.html')
Also, consider using validate_on_submit instead of checking for POST manually.
https://flask-wtf.readthedocs.org/en/latest/quickstart.html#validating-forms