I didn't understand the way we should write tests for Django views. Because, my case felt different than others.
I have a view, let's say MyView, inherits from CreateView of Django generic class based views. But, in the background, I mean in the form_valid function, this view tries to connect to a remote server with a Python library, then fetches data according the input of the user, then process other details and saves the object if everything is OK, if not, it returns custom errors.
What should be my way to handle this kind of view so I can write a test?
(I can use something like that but it's not good to handle possible errors:
from django.test import Client
c = Client()
c.login(mydetails..)
y = c.post('/url/', {'my': 'data'})
It works, yes, but I only check the status code in this case. y.status_code can't help, because Django returns 200 even if there is an error in the form.
To simplify testing framework in django, the test will simulate client request (like a user clicked on a link "url") this url is associated with a view which will handle the request and return a response to the user. for example in your test you will assert if the view renders the correct response and template. let suppose that we have a view for creating a blog, how might be tested? see the code below.
views.py
def createblog_view(request):
if request.method == 'POST':
form = BlogForm(request.POST)
if form.is_valid():
blog = form.save(commit=False)
blog.author = request.user # save the user who created the blog
blog.save()
return redirect(blog.get_absolute_url())
else:
form = BlogForm()
context = {'form': form}
return render(request, 'blog/createblog.html', context)
test.py
class BlogTest(TestCase):
def setUp(self):
# this user will be used to create the blog
self.user = User.objects.create_superuser(
'foo',
'foo#test.com',
'password'
)
def test_create_blog(self): # create update and delete a blog
# log user in and user
self.client.login(username='foo', password='password')
# create new blog
# expected date from the user, you can put invalid data to test from validation
form_data = {
'title': 'new test blog',
'body': 'blog body'
}
form = BlogForm(data=blogform_data) # create form indstance
"""
simulate post request with self.client.post
/blog/createblog/ is the url associated with create_blog view
"""
response = self.client.post('/blog/createblog/', form_data)
# get number of created blog to be tested later
num_of_blogs = Blog.objects.all().count()
# get created blog
blog = Blog.objects.get(title=form_data['title'])
# test form validation
self.assertTrue(blogform.is_valid())
# test slugify method, if any
self.assertEqual(blog.slug, 'new-test-blog')
# test if the blog auther is the same logged in user
self.assertEqual(blog.author, self.user1)
# one blog created, test if this is true
self.assertEqual(num_of_blogs, 1)
# test redirection after blog created
self.assertRedirects(
createblog_response,
'/blog/new-test-blog/',
status_code=302,
target_status_code=200
)
Related
This is my view
class UserRegistrationView(FormView):
template_name = "register/register_form.html"
form_class = None
extra_fields_form_page_slug = ""
email_template = "register/account_activation_email.html"
def get_form(self, form_class=None):
form = super().get_form(form_class)
add_extra_fields(
form.fields, form.helper.layout.fields, self.extra_fields_form_page_slug
)
return form
def get_extra_form(self):
return FormPage.objects.filter(slug=self.extra_fields_form_page_slug).first()
def form_valid(self, form):
user = form.save(commit=False)
user.is_active = False
user.save()
email = form.cleaned_data.get("email")
email_template = "register/account_activation_email.html"
send_confirmation_email(
self, "Activate Your Account", user, email, email_template
)
return render(self.request, "register/after_submission.html")
This is working fine (registration wise) but it's not from many other sides, because I have the registration form as a pop up window in the header, so it's available throughout the whole website, my problems are:
if the user had a successful registration they will be redirected to the specified template "after_submission" what I want is to stay on the same page and display a pop up with some message
if the user had an unsuccessful registration they will be redirected to the main template "register_form.html" with the errors displayed their, what I want is to stay on the same page and to display the error messages on the pop up form if the user opened it again
is this achievable using only Django?? or I must throw some JS in there, and if JS is the answer, can you give me a quick insight into how to do it?
You can redirect to the same page in your CBV :
from django.http import HttpResponseRedirect
return HttpResponseRedirect(self.request.path_info)
As stated in the comment your solution require Ajax and JS, you can however redirect to the same page but that will make a new request and refresh the whole page which might no be ideal for you.
There is a tutorial to work with Django and Ajax Ajax / Django Tutorial
I am new to django and writing authentication system for my application. Right now I have built the basic login/logout functionality.
What I want is that whenever a logged in user wants to access their profile page, they should be asked to confirm their password.
I have searched this on django docs but I was not able to find any resource on how to achieve. I know flask has a fresh_login_required decorator which does this, just wondering if it is possible to do the same thing with django as well.
I don't think there is any function for this in django. But you can write on your own with help of django session. For example:
First, we need to write a decorator:
# decorator
def require_fresh_login(function):
#wraps(function)
def wrap(request, *args, **kwargs):
has_verified_profile = request.session.pop('has_login_verified',None)
if has_verified_profile:
return function(request, *args, **kwargs)
else:
return redirect(reverse('fresh_password_view_url'))
return wrap
Then there should be a view for fresh_password_view_url, where you need to put a value against key has_login_verified in request.session. For example:
def verify_fresh_password(request):
form = SomeForm(request.POST)
if form.is_valid():
password = form.cleaned_data.get('password')
if request.user.check_password(password):
request.session['has_login_verified'] = True
return redirect('profile_view')
# else send error response
I have Django forms rendered using Django templates. Now I want to add a React component to one of the form fields (and perhaps to more than one field in the long term).
Based on what I have read so far, its seem best not to mix Django templating with React rendering and have Django serve only as backend API sending JSON data to React, while React takes over the entire form rendering. So I am now trying to re-render my forms entirely through React.
Instead of forms.py, I have now created serializers.py to define what data is to be sent to React and have Django Rest Framework setup in my environment. Now I am trying to figure out how to send this data across. There are some good online tutorials (and SO posts) that talk about integrating Django/DRF with React but havent found a single example of end-to-end form rendering through React and DRF.
Specifically, can anyone let me know what do I really write in my view that can then be useful for the GET request from React that tries to fetch the form data? A web reference or just the broad steps needed should be enough for me to get started (and to dig in more into the docs).
Update:
Also adding a simplified version of the serializers.py code here:
from .models import Entity
from rest_framework import serializers
class EntitySerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Entity
fields = ['name', 'role', 'location']
First, i think you need to check related React documentation about forms with multiple inputs. It gives you base idea about how things should be structured in React side.
About fetching data from server, you can try something like this in componentDidMount:
componentDidMount() {
// Assuming you are using jQuery,
// if not, try fetch().
// Note that 2 is hardcoded, get your user id from
// URL or session or somewhere else.
$.get('/api/profile/2/', (data) => {
this.setState({
formFields: data.fields // fields is an array
});
});
}
Then you can create your html input elements in render method with something like this:
render () {
let fields = this.state.formFields.map((field) => {
return (
<input type="text" value={field.value} onChange={(newValue) => {/* update your state here with new value */ }} name={field.name}/>
)
});
return (
<div className="container">
<form action="">
{fields}
<button onClick={this.submitForm.bind(this)}>Save</button>
</form>
</div>
)
}
And here is your submitForm method:
submitForm() {
$.post('/api/profile/2/', {/* put your updated fields' data here */}, (response) => {
// check if things done successfully.
});
}
Update:
Here is an untested-but-should-work example for your DRF view:
from rest_framework.decorators import api_view
from django.http import JsonResponse
from rest_framework.views import APIView
class ProfileFormView(APIView):
# Assume you have a model named UserProfile
# And a serializer for that model named UserSerializer
# This is the view to let users update their profile info.
# Like E-Mail, Birth Date etc.
def get_object(self, pk):
try:
return UserProfile.objects.get(pk=pk)
except:
return None
# this method will be called when your request is GET
# we will use this to fetch field names and values while creating our form on React side
def get(self, request, pk, format=None):
user = self.get_object(pk)
if not user:
return JsonResponse({'status': 0, 'message': 'User with this id not found'})
# You have a serializer that you specified which fields should be available in fo
serializer = UserSerializer(user)
# And here we send it those fields to our react component as json
# Check this json data on React side, parse it, render it as form.
return JsonResponse(serializer.data, safe=False)
# this method will be used when user try to update or save their profile
# for POST requests.
def post(self, request, pk, format=None):
try:
user = self.get_object(pk)
except:
return JsonResponse({'status': 0, 'message': 'User with this id not found'})
e_mail = request.data.get("email", None)
birth_date = request.data.get('birth_date', None)
job = request.data.get('job', None)
user.email = e_mail
user.birth_date = birth_date
user.job = job
try:
user.save()
return JsonResponse({'status': 1, 'message': 'Your profile updated successfully!'})
except:
return JsonResponse({'status': 0, 'message': 'There was something wrong while updating your profile.'})
And this is related url for this view:
urlpatterns = [
url(r'^api/profile/(?P<pk>[0-9]+)/$', ProfileFormView.as_view()),
]
I have something strange going on that I can't seem to crack. I'm building an API with Tastypie and when I issue this call in my browser against localserver, it works fine: localserver/api/v1/userfavorite/?user__username=testowner
However, in my code, I'm getting an error: "int() argument must be a string or a number, not 'SimpleLazyObject'". I realize it has to do with the user being treated as a request.user object, but I can't figure out where/why. I'm very confused why it works when issuing the API call in the browser, but in the code it is not working.
Here is my code:
# views.py
#login_required
def favorites(request):
'''
display a list of posts that a user has marked as favorite
'''
user = request.user
favorites_url = settings.BASE_URL + "/api/v1/userfavorite/?user__username=" + user.username
favorites = get_json(favorites_url)
return render(request, "maincontent/favorites.html", {'favorites':favorites})
# resources.py
class UserFavoriteResource(ModelResource):
'''
manage post favorites by a user. Users can use a favorites list
to easily view posts that they have liked or deemed important.
'''
user = fields.ForeignKey(UserResource, 'user')
post = fields.ForeignKey('blog.api.resources.PostResource', 'post', full=True)
class Meta:
queryset = UserFavorite.objects.all()
allowed_methods = ['get', 'post', 'delete']
authentication = Authentication()
authorization = Authorization()
filtering = {
'user':ALL_WITH_RELATIONS
}
def hydrate_user(self, bundle):
# build the current user to save for the favorite instance
bundle.data['user'] = bundle.request.user
return bundle
def get_object_list(self, request):
# filter results to the current user
return super(UserFavoriteResource, self).get_object_list(request)\
.filter(user=request.user)
# utils.py
def get_json(url):
# return the raw json from a request without any extraction
data = requests.get(url).json()
return data
Some notes:
1. I have the post method working to create the UserFavorite item
2. I can verify that the favorites_url is being generated correctly
3. I have tried hardcoding the favorites_url as well, same error.
EDIT: 4. I am logged in while doing this, and have verified that request.user returns the user
This doesn't work because there is Anonymous user in your request.user. You are using Authentication it does not require user to be logged in. So if you perform requests call that request is not authenticated and request.user is AnonymousUser and that error occurs when you try to save Anonymous user to db. Tastypie documentation advices to not using browsers to testing things up, just curl instead. Browsers stores a lot of data and yours one probably remembered you have been logged to admin panel in localhost:8000 on another tab that's why it worked in browser.
I would prefer something like this:
def hydrate_user(self, bundle):
"""\
Currently logged user is default.
"""
if bundle.request.method in ['POST', 'PUT']:
if not bundle.request.user.is_authenticated():
raise ValidationError('Must be logged in')
bundle.obj.user = bundle.request.user
bundle.data['user'] = \
'/api/v1/userauth/user/{}'.format(bundle.request.user.pk)
return bundle
I have a requirement here to build a comment-like app in my django project, the app has a view to receive a submitted form process it and return the errors to where ever it came from. I finally managed to get it to work, but I have doubt for the way am using it might be wrong since am passing the entire validated form in the session.
below is the code
comment/templatetags/comment.py
#register.inclusion_tag('comment/form.html', takes_context=True)
def comment_form(context, model, object_id, next):
"""
comment_form()
is responsible for rendering the comment form
"""
# clear sessions from variable incase it was found
content_type = ContentType.objects.get_for_model(model)
try:
request = context['request']
if request.session.get('comment_form', False):
form = CommentForm(request.session['comment_form'])
form.fields['content_type'].initial = 15
form.fields['object_id'].initial = 2
form.fields['next'].initial = next
else:
form = CommentForm(initial={
'content_type' : content_type.id,
'object_id' : object_id,
'next' : next
})
except Exception as e:
logging.error(str(e))
form = None
return {
'form' : form
}
comment/view.py
def save_comment(request):
"""
save_comment:
"""
if request.method == 'POST':
# clear sessions from variable incase it was found
if request.session.get('comment_form', False):
del request.session['comment_form']
form = CommentForm(request.POST)
if form.is_valid():
obj = form.save(commit=False)
if request.user.is_authenticated():
obj.created_by = request.user
obj.save()
messages.info(request, _('Your comment has been posted.'))
return redirect(form.data.get('next'))
else:
request.session['comment_form'] = request.POST
return redirect(form.data.get('next'))
else:
raise Http404
the usage is by loading the template tag and firing
{% comment_form article article.id article.get_absolute_url %}
my doubt is if am doing the correct approach or not by passing the validated form to the session. Would that be a problem? security risk? performance issues?
Please advise
Update
In response to Pol question. The reason why I went with this approach is because comment form is handled in a separate app. In my scenario, I render objects such as article and all I do is invoke the templatetag to render the form. What would be an alternative approach for my case?
You also shared with me the django comment app, which am aware of but the client am working with requires a lot of complex work to be done in the comment app thats why am working on a new one.
I dont see the problem with security, except situation when you using cookies for stroring session. The performance depends on what kind of session backand you are using as well. But I cant find the point why are you complicating things!
And I dont thing that touching session in template tag is a good idea at all.
And maybe Take a look at django Comments Framework
Update:
Ok. I cant see the problems in this approach except complication. For example in my project, i'm using ajax to send data and validate it right in the comments view, therefore I do not require to redirect to original page. Other thing is that I pass the initialized Form in article view, so i'm not using templatetags.
Can provide you with my approche for example purposes:
from forms import CommentForm
from models import Comment
from django.http import HttpResponseForbidden, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import simplejson
from etv_persons.person.models import Person
from django.contrib import messages
def create_comment(request,slug):
if request.method != 'POST' or not request.POST or request.user.is_anonymous():
return HttpResponseForbidden('Доступ запрещен')
person = get_object_or_404(Person,slug=slug)
form = CommentForm(data=request.POST)
if form.is_valid():
Comment.objects.create(user_id=request.user.id, person=person,text=form.cleaned_data['text'])
if request.is_ajax():
msg={'msg': 'Cement was send',}
else:
messages.info(request, 'COmment was send.')
else:
if request.is_ajax(): msg={'msg': 'Error.',}
else: messages.info(request, 'Error.')
if request.is_ajax():
return HttpResponse(simplejson.dumps(msg),content_type='application/json')
else:
return redirect('person_details',**{"slug":slug,"ptype":person.type})
And in the article view we just do:
response['comment_form'] = CommentForm()
And yes, I do not validate the comments form. There is no reason. Just one text input.