Testing delete_selected action in Django Admin with pytest - django

I'm using Django 3.2b1 and pytest 6.2.2.
I'm trying to use pytest to write a test to make sure admins are able to delete objects using the delete_selected action. My test looks like this:
def test_delete_mymodel_action(admin_client):
objs_to_delete = [
MyModel.objects.create(),
MyModel.objects.create(),
]
MyModel.objects.create() # Don't delete this obj
data = {
"action": "delete_selected",
"_selected_action": [str(f.pk) for f in objs_to_delete],
"post": "yes", # Skip the confirmation page
}
change_url = reverse("admin:myapp_mymodel_changelist")
admin_client.post(change_url, data)
assert MyModel.objects.count() == 1
The code works and ends in a 302 redirect back to the changelist, but the objects don't get deleted. The response is:
test_delete_mymodel_action - assert 3 == 1
The reason I'm testing this is that certain code can cause the delete_selected action to fail. For example, if you override get_queryset() in the ModelAdmin and return a queryset that uses distinct(), the delete_selected action will fail.
Here the code from a delete confirmation page in Django Admin:
<form method="post">
<input type="hidden" name="csrfmiddlewaretoken" value="VCR7vjVYcb2xuMdPUknotrealViwj92wgZrT21k6RbqGxXNlQnCORU1Fp6NzKhn64">
<div>
<input type="hidden" name="_selected_action" value="31418">
<input type="hidden" name="_selected_action" value="31412">
<input type="hidden" name="action" value="delete_selected">
<input type="hidden" name="post" value="yes">
<input type="submit" value="Yes, I’m sure">
No, take me back
</div>
</form>
Some helpful references:
Django's delete_selected() method.
Testing custom admin actions in django SO Answer

I just run into the same problem. I noticed I was logged in with the wrong user.
My thought process:
302 doesn't indicate there's anything wrong, there was no content in the response either (b'')
I added follow=True to self.client.post. The response was 200 and there were no objects, so I assumed it worked correctly but it failed on assertion
I put breakpoint in delete_selected of django.contrib.admin.actions and n = queryset.count() was 0.
If it's not listed after but there's nothing to delete (n = 0), let's see if there was something to be deleted before.
response = self.client.get(reverse("admin:myapp_mymodel_changelist"))
self.assertContains(response, obj.id)
nope!
So the problem with your test is that these objects can't be deleted because they can't be retrieved, probably due to some filtering.
Note, Django admin won't raise 404 if the object has not been found.

Related

How to pass data from one view to the next

Summary: I am trying to build a job site. On index.html the user enters a zip code into a form to see jobs in that zip code, this form is handled with the job_query view. This brings them to another page(search.html) where at first you only see jobs in that specific zip code but I am trying to add a filter that lets the user see jobs within X miles. How can I pass the zip code value entered in the from on index.html to the next page?
index.html:
<h2>Find a Job</h2>
<!--Search Bar-->
<form method = "GET" action = "{% url 'search' %}" >
<div id = "form_grid">
<input name="query" type="text" placeholder="Zip Code">
<button type="submit">Search</button>
</div>
</form>
search.html:
<form method = "GET" action = "{% url 'search' %}" >
<input class="search_bar" name="query" type="text" placeholder="Zip Code">
<button class="search_btn btn btn-outline-success " type="submit">Find Jobs</button>
</form>
<form id="within_miles_form" method = "GET" action = "{% url 'within_miles' %}" >
<input class="search_bar" name="miles" type="text" placeholder="Within X miles of Zip Code">
<button type="submit">Filter</button>
</form>
<!--code to display jobs-->
views.py:
def job_query(request):
if request.method == "GET":
query = request.GET.get('query')
jobs_matching_query = Job.objects.filter(zip_code__iexact = query) | Job.objects.filter(city__iexact=query) | Job.objects.filter(state__iexact=query)
number_of_results = 0
for job in jobs_matching_query:
number_of_results = number_of_results + 1
return render(request, 'core/search.html', {'query': query ,'jobs_matching_query': jobs_matching_query, 'number_of_results': number_of_results})
def within_miles(request):
miles = request.GET['miles']
#how can i use value of the zip code entered?
urls.py:
url(r'^search$', views.job_query, name="search"),
url(r'within_miles', views.within_miles, name="within_miles"),
I think I included all the relevant info but if I am missing something please let me know, thanks in advance for any help.
You can encode the entered ZIP in a URL, pass it through cookies, store it in the session variables, or use a (hidden) input element that forces the browser to pass it through a GET and POST request.
Encode it in the URL
In that case we can rewrite the URL to:
url(r'^within_miles/(?P<zip>[0-9]{5})/$', views.within_miles, name="within_miles"),
So now one can no longer fetch your.domain.com/within_miles, but your.domain.com/within_miles/12345. It makes it easy for a user to "manipulate" the URL, but since the user can probably provide any ZIP, there is probably not much gain to protect that.
In the form, the URL that is generated is thus:
{% url 'within_miles' zip=query %}
(you can use another variable that is more strictly a ZIP code)
You should thus ensure that query is here a five digit string (or otherwise change the expression in the url(..) part such that it allows all possible queries).
Using hidden form elements
We can also encode content in hidden form elements, for example here we can create an element in the form:
<form id="within_miles_form" method = "GET" action = "{% url 'within_miles' %}" >
<input class="search_bar" name="miles" type="text" placeholder="Within X miles of Zip Code">
<input type="hidden" name="zip_code" value="{{ query }}">
<button type="submit">Filter</button>
</form>
We thus add a form element, fill it with some data, and let the browser submit the value again to the next view. Note that again it is the browser that does this, so a user can inspect the DOM (most browsers allow that, and subsequently edit it).
Using session variables and/or cookies
You can also decide to use session variables (stored at server side, so "secure") or cookies (stored at client side, can be tampered with). A potential problem however is that these are stored in the browser, and changes to the cookies in one tab page, thus can have effect in the other tab page. Furthermore cookies and sessions will "die" after the request, and thus can create a lot of trouble in future views.
You can set a session variable in the view with:
request.session['zip_code'] = query
This will thus store an entry at the server side such that another call can retrieve that value again. The request.session acts like a dictionary that keeps some sort of state per session.
setting and obtaining session variables
In another view, you can thus query the request.session, like:
zip_code = request.session.get('zip_code')
setting and obtaining cookies
We can use a similar approach with cookies. A browser however might reject cookies, or manipulate them, so there are not that much guarantees that there is no tampering with the data (in fact there are none). You can set a cookie with:
response = render(request, 'core/search.html', {'query': query ,'jobs_matching_query': jobs_matching_query, 'number_of_results': number_of_results})
response.set_cookie('zip_code', query)
return response
Before we thus return the result of render(..), we call .set_cookie(..) on the result.
We can - for example in a later view - retrieve the content with:
zip_code = request.COOKIES.get('zip_code')
Improving the job_query view
The job_query view however looks a bit strange: it uses all kinds of "uncommon" code practices. For example the number of elements is calculated by iterating over it, instead of taking the len(..). This also looks basically like a ListView [Django-doc] and we can make the query more elengant by using Q-objects [Django-doc]. The listview then looks like:
def JobListView(ListView):
model = Job
context_object_name = 'jobs_matching_query'
template_name = 'core/search.html'
def get_context_data(self, **kwargs):
kwargs = super(JobListView, self).get_context_data(**kwargs)
kwargs.update(
number_of_results=len(kwargs['object_list'],
query = self.request.GET.get('query')
)
return kwargs
In the view, you then not pass the JobListView, but JobListView.as_view() result as a reference.

Scrapy FormRequest How to check if URL is needed

I am new to scrapy and in general web tech.
While working on a scrapy example to perform auto login. I came across 1 field , referrer url . I am wondering when do i need to this.
return scrapy.FormRequest.from_response(
response,
url='www.myreferrer.com', #when do i need this ???
formnumber=1,
formdata=self.data['formdata'],
callback=self.after_login
)
I tested with and without it and it works in both instances.
I understand that referrer url is for security but how do i determine from html code that i need or dont need this ?
ADDON
The following html form required the url to be defined :
<form id="login" enctype="multipart/form-data" method="post" action="https:///myshop.com/login/index.php?route=account/login">
I am a returning customer.<br>
<br>
<b>E-Mail Address:</b><br>
<input type="text" name="email">
<br>
<br>
<b>Password:</b><br>
<input type="password" name="password">
<br>
Forgotten Password<br>
<div style="text-align: right;"><a class="button" onclick="$('#login').submit();"><span>Login</span></a></div>
</form>`
class FormRequest(Request):
# delete some code here
#classmethod
def from_response(cls, response, formname=None, formid=None, formnumber=0, formdata=None,
clickdata=None, dont_click=False, formxpath=None, formcss=None, **kwargs):
url = _get_form_url(form, kwargs.pop('url', None))
def _get_form_url(form, url):
if url is None:
return urljoin(form.base_url, form.action)
return urljoin(form.base_url, url)
if the url is empty, it uses form tag's action attribute to get the URL.
if the url is not empty, then it use the URL you give to it.
the base_url comes from the response.
def _get_form(response, formname, formid, formnumber, formxpath):
"""Find the form element """
root = create_root_node(response.text, lxml.html.HTMLParser,
base_url=get_base_url(response))
so, when the action attribute does not exist or the login requests is not sent to the action URL, you need to pass the argument.

How to specify name of a Model Form's submit button name via django-webtest

I'm using django-webtest to automate functional tests for a Django application. One of my ModelForms has multiple submit buttons. The template, using django-crispy-forms, looks like this:
<form action="" method="post">
{% csrf_token %}
<p>
{{ person_form|crispy }}
<br><br>
{{ admin_form|crispy }}
</p>
<button id="SaveButton" type="submit" name="save_data" class="btn btn-lg btn-primary">Save</button>
<button id="VerifyButton" type="submit" name="verify_data" class="btn btn-lg btn-primary">Verify</button>
</form>
When I submit the form manually from the webpage by clicking on the Save button, the request.POST that is passed into the corresponding view method contains the 'save_data' tag that I use to decide what to do in the code.
However, when I create a django-webtest testcase to do the same, the 'save_data' tag is absent, even if I specify it in form.submit() as follows:
def test_schools_app_access_school_admin_record(self):
school_index = self.app.get(reverse('schools:school_index'),
user=self.school_admin)
assert self.school_name in school_index
school_page = school_index.click(self.school_name)
assert 'View School Administrator' in school_page
school_admin_page = school_page.click('View School Administrator')
person_form = school_admin_page.forms[1]
assert person_form['person-name'].value == self.school_admin_name
# TODO: Figure out how to pass name='save_data' while submitting
person_form['person-email'] = self.school_admin_email
response = person_form.submit(name='save_data', value='save')
# Verify that the field has been updated in the database
person = Person.objects.get(name=self.school_admin_name)
assert self.school_admin_email in person.email
How do I get django-webtest to include the name of the submit button in request.POST ?
Also, since I have multiple forms on the same page, I'm currently using response.forms[1] to select the form of interest. But I would like to use the form id instead. I couldn't locate in the Django documentation how to assign the form id (not field id) to a ModelForm. Could someone help me with this?
I'm using Django 1.7, django-webtest 1.7.8, WebTest 2.0.18 and django-crispy-forms 1.4.0.
I figured out that I'd made a typo in my code snippet because of which my code was not working.
In this fragment
person_form['person-email'] = self.school_admin_email
response = person_form.submit(name='save_data', value='save')
I should have value='Save' instead of 'save'.
With this corrected, the response does contain the name 'save_data'.

Django Rest Framework returns 200 OK even though login request has incorrect credentials

This is my URLS.py:
from django.conf.urls import include, url
from django.contrib import admin
urlpatterns = [
url(r'^CMS/', include('CMSApp.urls')),
url(r'^admin/', include(admin.site.urls)),
]
and this is CMSApp/urls.py:
from django.conf.urls import url
from django.conf.urls import include
from CMSApp import views
urlpatterns = [
url(r'^$', views.HomePageView.as_view()),
url(r'^users$', views.user_list.as_view()),
url(r'^users/(?P<pk>[0-9]+)$', views.user_detail.as_view()),
url(r'^api-auth/', include('rest_framework.urls',
namespace='rest_framework')),
]
My HomePageView serves this home.html page:
<h3>Register</h3>
<form ng-submit="ctrl.add()" name="myForm">
<label>Username</label>
<input type="text" name="uname" ng-model="ctrl.user.username" required>
<label>Password</label>
<input type="password" name="pwd" ng-model="ctrl.user.password" required>
<label>Email</label>
<input type="email" name="mail" ng-model="ctrl.user.email" required>
<input type="submit" value="Register">
</form>
<h3>Login</h3>
<form ng-submit="ctrl.loginUser()" name="myLoginForm">
<label>Username</label>
<input type="text" name="uname" ng-model="ctrl.user.username" required>
<label>Password</label>
<input type="password" name="pwd" ng-model="ctrl.user.password" required>
<input type="submit" value="Login">
</form>
<script>
angular.module("notesApp", [])
.config(['$httpProvider', function($httpProvider) {
$httpProvider.defaults.xsrfCookieName = 'csrftoken';
$httpProvider.defaults.xsrfHeaderName = 'X-CSRFToken';
}])
.controller("MainCtrl", ["$http", function($http) {
var self = this;
self.users = {};
var fetchUsers = function() {
return $http.get("/CMS/users").then(function(response) { // get list of existing users
self.users = response.data;
}, function(errResponse) {
console.error("Error while fetching users.");
});
};
self.loginUser = function() {
$http.post("/CMS/api-auth/login/", self.user)
.error(function(data, status, headers, config) {
console.log(data);
})
.then(fetchUsers);
console.log("Success Login with ", self.user);
};
}]);
</script>
When I register a user which already exists, then Django returns a 400 Bad Request error, which is good because I use AngularJS on the frontend to handle the 400 status code.
The issue is, when I try to log in with an incorrect username and password (a username which does not exist or a username and password which do not match), Django returns a 200 OK status code so I can't track the issue using AngularJS on the frontend. Any idea why Django returns a 200 OK when I try to log in with an incorrect username and password? How do I get Django to return a 400 when an incorrect username / password is submitted?
Edit: This was my original post: Django Rest Framework + AngularJS not logging user in or throwing errors and charlietfl commented on it saying "as for the ajax error handling. If login fails .... using true REST methodology would send back a 401 status which would then fire your ajax error handler", which is why I do not want a 200 OK when login fails.
The first thing to understand is that Django does not by default enforce authentication.. you can use the auth module to enroll users and to authenticate them, but you have to protect your views explicitly. The authentication app only provides API's for you to use, it doesn't actually protect anything unless you use those API's.
Any view that isn't explicitly checked for authentication will be open to anyone.
Sure, admin requires you to log in, but that's because the authors of the admin app included checks in their code...
The Django REST Framework has it's own checks for this, so very little coding needed, you just have to configure each view (see docs):
http://www.django-rest-framework.org/api-guide/authentication/
For any other view you might want to protect, you need to add some checks. The #login_required decorator on your view is one way to do that, for regular function type views. Since you are dealing with Class-Based-Views, look at the Django docs here:
https://docs.djangoproject.com/en/1.8/topics/class-based-views/intro/#mixins-that-wrap-as-view
Another option for checking login status is to use a middleware class. That's what I'm doing in my current system since almost everything on our site requires that the user be logged in. So, in the middleware class, we check to see if request.user.is_anonymous. If they are, then there's just a small subset of pages they can access, and if they aren't accessing those pages, we redirect them to login. The middleware runs before any view so that covers the whole site.
Ok, so now that I understand you want to actually log the user in via an ajax request, and not just check their access.. and you want control over what response comes back, then what I suggest is implementing your own view for the login service. Something like:
class LoginView(View):
def get(self, request, *args, **kwargs):
user = auth.authenticate(
username=request.GET.get('username'),
password=request.GET.get('password'))
# return whatever you want on failure
if not user or not user.is_active:
return HttpResponse(status=500)
auth.login(request, user)
return HttpResponse('OK')
I did not test this code but it should be pretty close.

Django form "takes exactly 1 argument (2 given) " error - possibly related to CSRF?

I am attempting a fairly simple form on Django 1.3, and am trying to understand how CSRF works.
I think I've followed the three steps detailed on the Django site, but am still unable to make the form work.
The form displays when the URL is loaded, however upon hitting the submit button, I get the following error:
TypeError at /txt/ txt() takes exactly
1 argument (2 given)
Here's the actual code:
views.py:
from django.http import HttpResponse
from django.shortcuts import render_to_response, redirect
from django.template import RequestContext
def txt(request):
if request.method == 'POST':
msg="txt received"
else:
msg="nothing in POST"
return render_to_response('base.html', locals(), context_instance=RequestContext(request))
The HTML:
<body>
<form action="txt/" method="POST">{% csrf_token %}
From <input type="text" name="From"><br>
To <input type="text" name="To"><br>
Body <input type="text" name="Body"><br>
<input type="submit" value="Search">
</form>
{{ msg }}
</body>
I know I haven't done a forms.py etc. but I was just trying to get the basic functionality up and going. I think this code would have worked in previous versions of Django, and am unsure why its not working this time.
The error looks like your view function is getting more arguments than it is setup to accept. As you have it shown above, the view only accepts a single argument: the request.
If your URL pattern for this view is configured with a capturing group, or you are adding extra arguments via the optional dictionary or the kwargs parameter to url(), then those extra arguments will be given to the view function and could cause the error you're seeing.