Test client doesn't throw expected 404 error - django

I'm facing a weird error in which a Django test I wrote to test a view's response isn't giving me the expected behavior.
Consider the following model for a simple blogging application:
class Post(models.Model):
"""Represents a Post in the blogging system."""
# is the post published or in draft stage?
STATUS_CHOICES = (
('draft', 'Draft'),
('published', 'Published'),
('deleted', 'Deleted'),
)
# the default mode for posts
DRAFT = STATUS_CHOICES[0][0]
title = models.CharField(max_length=250, null=True)
slug = models.SlugField(max_length=200, blank=True)
text = models.TextField(null=True)
last_modified = models.DateTimeField(default=timezone.now)
date_published = models.DateTimeField(default=timezone.now, null=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=DRAFT)
category = models.ForeignKey('Category', null=True)
author = models.ForeignKey('Author', null=True)
class Meta:
ordering = ('-date_published',)
#property
def truncated_text(self):
if len(self.text) > 200:
return '{} . . .'.format(self.text[0:300])
else:
return self.text
def publish(self):
self.date_published = timezone.now()
self.status = 'published'
self.save()
def save(self, *args, **kwargs):
# Set slug only on new posts
if not self.id:
self.slug = slugify(self.title)
self.last_modified = timezone.now()
super().save(*args, **kwargs)
def __str__(self):
return self.title
Here's the view (you can see that I'm insisting on getting published posts only):
def post_view(request, slug):
post = get_object_or_404(Post, slug=slug, status='published')
return render(request, 'blog/single_post.html', {'post': post})
Here's the URL entry:
url(r'^blog/(?P<slug>[-\w]+)/$', blog_view.post_view, name='single_post'),
And finally, here's the test:
class TestBlogViews(TestCase):
"""Class to test the views of our blog app"""
def setUp(self):
"""Make sure we have an environment in which to render the templates"""
# An author
self.author = Author()
self.author.name = 'Ankush'
self.author.short_name = 'ank'
self.author.save()
# A category
self.cat = Category()
self.cat.name = 'Programming'
self.cat.save()
# A post
self.post = Post()
self.post.title = 'Dummy title'
self.post.text = 'Dummy text that does nuffin'
self.post.author = self.author
self.post.category = self.cat
setup_test_environment()
self.client = Client()
def test_unpublished_post_raises_404(self):
self.post.save()
response = self.client.get(reverse('single_post', args=('dummy-title',)))
self.assertEqual(response.status_code, 404)
So basically, my test fails because I'm getting 200 != 404. I also ran the Post api from shell, and got a DoesNotExist when looking for status = 'published' on a post that only had the save() method called. I also printed the response.content in my test, and it contains the custom 404 template content. Why isn't it throwing 404 when the object clearly doesn't exist? Something wrong with get_object_or_404()?
P.S. Please let me know if you need more snippets of code.

All right, I was able to figure it out. Turns out my custom 404 view was screwed up:
It was:
def not_found(request):
return render(request, 'ankblog/not_found.html')
So naturally the status code was 200. It got fixed by changing it to return render(request, 'ankblog/not_found.html', status=404).
I do find it funny, though. I would've thought that the custom 404 would have raised a 404 error automatically.

Related

Django tests AssertionError for update view

I tried to create Django test for UpdateView
but I have such problem as:
self.assertEqual(application.credit_purpose, 'House Loan')
AssertionError: 'Car Loan' != 'House Loan'
Car Loan
House Loa
def test_application_update(self):
application = Application.objects.create(customer=self.customer, credit_amount=10000, credit_term=12,
credit_purpose='Car Loan', credit_pledge=self.pledge,
product=self.product,
number_request=2, date_posted='2020-01-01', reason='None',
repayment_source=self.repayment, possible_payment=1000,
date_refuse='2020-01-02', protokol_number='123457',
status=self.status,
language=0, day_of_payment=1, credit_user=self.user)
response = self.client.post(
reverse('application_update', kwargs={'pk': application.id}),
{'credit_purpose': 'House Loan'})
self.assertEqual(response.status_code, 200)
application.refresh_from_db()
self.assertEqual(application.credit_purpose, 'House Loan')
This is my model
class Application(AbstractCredit):
number_request = models.IntegerField(verbose_name='Номер заявки', unique=True, default=number_auto) # Добавить автоинкремент
date_posted = models.DateField(verbose_name='Дата заявки', auto_now_add=True)
reason = models.CharField(max_length=200, null=True, blank=True, verbose_name='Причина отказа/Условия одобрения')
repayment_source = models.ForeignKey(Repayment, on_delete=models.CASCADE, verbose_name='Источник погашения')
possible_payment = models.IntegerField(verbose_name='Желаемая сумма ежемесячного взноса')
date_refuse = models.DateField(default=one_day_more, null=True, blank=True, verbose_name='Дата отказа/одобрения')
protokol_number = models.CharField(max_length=20, unique=True, null=True, blank=True,
verbose_name='Номер протокола')
status = models.ForeignKey(Status, on_delete=models.CASCADE, default=1, verbose_name='Статус')
language = models.IntegerField(choices=LANGUAGES_CHOICES, verbose_name='Язык договора', blank=True, null=True)
day_of_payment = models.IntegerField(choices=DAY_OF_PAYMENT_CHOICES,
verbose_name='Предпочитаемый день оплаты по кредиту')
credit_user = models.ForeignKey(User, on_delete=models.SET(0), verbose_name='Кредитный специалист')
This is my view
class ApplicationUpdate(BasePermissionMixin, SuccessMessageMixin, UpdateView):
model = Application
form_class = ApplicationForm
template_name = 'standart_form.html'
permission_required = 'Изменение заявки'
success_message = 'Заявка успешно изменена'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['button_name'] = 'Сохранить'
return context
def get_success_url(self):
return reverse_lazy('application_detail', kwargs={'pk': self.get_object().id})
So, I also got stuck in this for a while. The main problem is at:
...
response = self.client.post(
reverse('application_update', kwargs={'pk': application.id}),
{'credit_purpose': 'House Loan'}
)
...
I was able to understand thanks to this answer. It happens because you are posting to a form and it expects to have all fields filled, otherwise it will fail validation and thus will not call .save().
What you can do is, after creating the object copy its data into a dictionary, then modify it before posting:
tests.py
from django.forms.models import model_to_dict
class ApplicationTestCase(TestCase):
def setUp(self):
customer = Customer.objects.create(...)
...
self.data = {
'customer': customer, 'credit_amount': 10000, 'credit_term': 12,
'credit_purpose': 'Car Loan', ...
}
def test_application_update(self):
application = Application.objects.create(**self.data)
post_data = model_to_dict(application)
post_data['credit_purpose'] = 'House Loan'
response = self.client.post(
reverse(
'app:view-name', kwargs={'pk': application.id}),
post_data
)
# print(application.credit_purpose)
# application.refresh_from_db()
# print(application.credit_purpose)
# It returns a redirect so code is 302 not 200.
self.assertEqual(response.status_code, 302)
self.assertEqual(application.credit_purpose, 'House Loan')
Also, get_absolute_url() is set in the wrong place should be under models:
models.py
class Application(AbstractCredit):
...
def get_absolute_url(self):
return reverse('app:view-name', kwargs={'pk': self.pk})

Hit counter for Django blog post

In my search to find a way to add hit counter to my blog posts without any third party library I found this answer on StackOverflow.
However as I'm not a Django expert I can't figure out how to use that Mixin with my view.
Here's how my model is defined:
class Post(models.Model):
STATUS_CHOICES = (('draft', 'Draft'), ('published', 'Published'))
title = models.CharField(max_length=250)
slug = models.SlugField(max_length=250, unique_for_date='publish')
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='blog_posts')
body = models.TextField()
publish = models.DateTimeField(default=timezone.now)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
And my views:
def post_list(request):
posts = Post.published.all()
return render(request, 'blog/post/list.html', {'posts': posts})
class PostDetailView(DetailView):
model = Post
context_object_name = 'post'
And here's the mixin provided in that answer:
class BlogPostCounterMixin(object):
def get_context_data(self, **kwargs):
context = super(BlogPostCounterMixin, self).get_context_data(**kwargs)
blog_post_slug = self.kwargs['slug']
if not blog_post_slug in self.request.session:
bp = BlogPost.objects.filter(slug=blog_post_slug).update(counter=+1)
# Insert the slug into the session as the user has seen it
self.request.session[blog_post_slug] = blog_post_slug
return context
I would ask the user provided that answer, but he's inactive for more than 2 years.
Appreciate your help.
you can create a IntegerField called views in your Model
then in your post_detail view you could do this just before return statement
post.views += 1
post.save(update_fields=['views'])
Obviously this solution has the drawback of some views that happen at exactly the same moment being missed.
Edit:
before using a mixin you must first use a class based view instead of a function based view then you can use this mixin and in your case you would want to use a DetailView
https://docs.djangoproject.com/en/3.2/ref/class-based-views/generic-display/#detailview
class BlogPostCounterMixin:
def get_object(self, *args, **kwargs):
# get_object will be available if you use a DetailView
obj = super().get_object(*args, **kwargs):
post_unique_key = 'post_%s' % obj.pk # or whatever unique key is
if not post_unique_key in self.request.session:
obj.views += 1
obj.save(update_fields=['views'])
self.request.session[post_unique_key] = post_unique_key
return obj
and can be used like this
class PostDetailView(BlogPostCounterMixin, DetailView):
model = Post
context_object_name = 'post'

Is this the correct way to add post to favourites using django rest framework?

I have a doubt regarding django rest framework for this function. I have done it the same way I do in a normal django website.
can someone please check and tell me whether this is the right way to do it using django rest framework so that, this can be used to connect to a front end later..
view
def favourite_post_api(request, slug):
post = get_object_or_404(Post, slug=slug)
user = request.user
serializer = PostSerializer(post)
if user in post.favourite.all():
post.favourite.remove(user)
return Response("Removed from favourites.", status=status.HTTP_201_CREATED)
else:
post.favourite.add(user)
return Response("Added to favourites.", status=status.HTTP_201_CREATED)
model
class Post(models.Model):
title = models.TextField(max_length=5000, blank=False, null=False)
image = models.ImageField(upload_to='posts/postimage/', null=True)
post_date = models.DateTimeField(auto_now_add=True, verbose_name="Date Posted")
updated = models.DateTimeField(auto_now_add=True, verbose_name="Date Updated")
likes = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='post_likes', blank=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
slug = models.SlugField(blank=True, unique=True, max_length=255)
favourite = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='post_favourite', blank=True)
Thanks
So, I will show you my approach for your case:
views.py
from rest_framework.views import APIView
class PostView(APIView):
bad_request_message = 'An error has occurred'
def post(self, request):
post = get_object_or_404(Post, slug=request.data.get('slug'))
if request.user not in post.favourite.all():
post.favourite.add(request.user)
return Response({'detail': 'User added to post'}, status=status.HTTP_200_OK)
return Response({'detail': self.bad_request_message}, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request):
post = get_object_or_404(Post, slug=request.data.get('slug'))
if request.user in post.favourite.all():
post.favourite.remove(request.user)
return Response({'detail': 'User removed from post'}, status=status.HTTP_204_NO_CONTENT)
return Response({'detail': self.bad_request_message}, status=status.HTTP_400_BAD_REQUEST)
urls.py
import yourapp.views as views
urlpatterns = [
path('dummy-path/', views.PostView.as_view()),
]
What you need to do now is sent a json like {"slug": "your_data"} on /dummy-path url using the POST method for add user or DELETE method for user removal.

Redirection issue using PK

I Have a very strange phenomenon
In my app the user Create a project and is redirected to that project detail using its pk. On that project detail page he is asked to create a team and when the team is created he is redirected again to the project detail page but to the wrong pk
for example: I just created a project and I got redirected to .../project/24. I was asked to create a team, I created it but got redirected to ../project/17 any idea why and how to redirect my page to the right URL ?
model.py:
class Team(models.Model):
team_name = models.CharField(max_length=100, default = '')
team_hr_admin = models.ForeignKey(MyUser, blank=True, null=True)
def get_absolute_url(self):
return reverse('website:ProjectDetails', kwargs = {'pk' : self.pk})
def __str__(self):
return self.team_name
class TeamMember(models.Model):
user = models.ForeignKey(MyUser)
team = models.ForeignKey(Team)
def __str__(self):
return self.user.first_name
class Project(models.Model):
name = models.CharField(max_length=250)
team_id = models.ForeignKey(Team, blank=True, null=True)
project_hr_admin = models.ForeignKey(MyUser, blank=True, null=True)
def get_absolute_url(self):
return reverse('website:ProjectDetails', kwargs = {'pk' : self.pk})
def __str__(self):
return self.name
views.py
class TeamCreate(CreateView):
model = Team
fields = ['team_name']
template_name = 'team_form.html'
def form_valid(self, form):
valid = super(TeamCreate, self).form_valid(form)
form.instance.team_hr_admin = self.request.user
obj = form.save()
#SELECT * FROM project WHERE user = 'current_user' AND team_id = NULL
obj2 = Project.objects.get(project_hr_admin=self.request.user, team_id=None)
obj2.team_id = obj
obj2.save()
return valid
return super(TeamCreate, self).form_valid(form)
def get_success_url(self):
project = Project.objects.get(team_id=self.obj, project_hr_admin=self.request.user)
return project.get_absolute_url()
The problem here is your CreateView is refering to a TeamObject and not project.
You should override the get_success_url method:
def get_success_url(self):
project = Porject.objects.get(team_id=self.object, project_hr_admin=self.request.user)
return project.get_absolute_url()
The function called was the get_absolute_url of your Team model. So you're calling the project detail view but with the team pk => you get a random project assuming there's a project with a pk which has the same value as your project or, the pk you're sending doesn't exist and you'll have a 404 error (pk doesn't exist).
def get_absolute_url(self):
return reverse('website:ProjectDetails', kwargs = {'pk' : self.pk})
That's the one in your Team model, but you call ProjectDetails. So here, self.pk is Teaminstance.pk.
What I do in the code i gave you is to call the get_absolute_url of the project instance.
But as told in the other answer, you should remove or change your get_absolute_url of your team model.
class Team(models.Model):
# ...
def get_absolute_url(self):
return reverse('website:ProjectDetails', kwargs = {'pk' : self.pk})
^^^^^^^
Here, the wrong pk will be delived. Thx to #Bestasttung for clarification

How can I link an edit page to my detail profile page in django?

So I am trying to link the template where I can edit a user_profile like this:
Edit
But is giving me this error:
NoReverseMatch at /user_profile/9/
Reverse for 'user_profile_update' with arguments '()' and keyword arguments '{u'id': ''}' not found. 1 pattern(s) tried: [u'user_profile/(?P\d+)/edit/$']
But I can get access to the template like this without an error: /user_profile/(id)/edit
This is my view:
def user_profile_update(request, id=None):
instance = get_object_or_404(user_profile, id=id)
form = user_profileForm(request.POST or None, request.FILES or None, instance=instance)
if form.is_valid():
instance = form.save(commit=False)
instance.save()
return HttpResponseRedirect(instance.get_absolute_url())
context = {
"first_name": instance.first_name,
"instance": instance,
"form":form,
}
return render(request, "user_profile/user_profile_form.html", context)
This is my url:
url(r'^create/$', user_profile_create,name='create'),
url(r'^(?P<id>\d+)/$', user_profile_detail, name='detail'),
url(r'^(?P<id>\d+)/edit/$',user_profile_update, name='edit'),
url(r'^(?P<id>\d+)/delete/$', user_profile_delete),
And this is my model:
class user_profile(models.Model):
first_name = models.CharField(null=True,max_length=100)
last_name = models.CharField(null=True,max_length=100)
address_1 = models.CharField(_("Address"), max_length=128)
address_2 = models.CharField(_("Address 1"), max_length=128, blank=True)
city = models.CharField(_("City"), max_length=64, default="pune")
country_name = models.CharField(max_length=60)
pin_code = models.CharField(_("pin_code"), max_length=6, default="411028")
updated = models.DateTimeField(auto_now=True, auto_now_add=False)
timestamp = models.DateTimeField(auto_now=False, auto_now_add=True)
def __unicode__(self):
return self.first_name
def __str__(self):
return self.first_name
def get_absolute_url(self):
return reverse("user_profile:detail", kwargs={"id": self.id})
class Meta:
ordering = ["-timestamp", "-updated"]
I would be really glad if someone could help me!
You need separate views and URL-conf for editing and detail view. You only have ^user_profile/(?P\d+)/edit/$' in your URL-conf, so you can only access the view from user_profile/123/edit/. So you need to add another URL '^user_profile/(?P\d+)/$ to access from user_profile/123/.
The same with views, you need two separate ones for the simplest solution.