Writing a test for a Django View get_context_data() method - django

I am writing a test for a View where I update context to pass additional information to the template.
Problem
In writing the test, I'm having trouble accessing context from the RequestFactory.
Code
View
class PlanListView(HasBillingRightsMixin, ListView):
"""Show the Plans for user to select."""
headline = "Select a Plan"
model = Plan
template_name = "billing/plan_list.html"
def get_context_data(self, *args, **kwargs):
context = super(PlanListView, self).get_context_data(**kwargs)
context.update({
"customer": self.get_customer()
})
return context
Test
class TestPlanListView(BaseTestBilling):
def setUp(self):
super(TestPlanListView, self).setUp()
request = self.factory.get('billing:plan_list')
request.user = self.user
request.company_uuid = self.user.company_uuid
self.view = PlanListView()
self.view.request = request
self.response = PlanListView.as_view()(request)
def test_get_context_data(self, **kwargs):
context = super(self.view, self).get_context_data(**kwargs)
context.update({"customer": self.view.get_customer()})
self.assertEqual(
self.view.get_context_data(),
context
)
Question
How can I test the view's get_context_data() method?

Using a test client gives you access to your context.
def test_context(self):
# GET response using the test client.
response = self.client.get('/list/ofitems/')
# response.context['your_context']
self.assertIsNone(response.context['page_obj'])
self.assertIsNone(response.context['customer']) # or whatever assertion.
.....

If you don't want to test the full browser behavior you could use the RequestFactory instead. This factory provides a request instance that you can pass to your view. The benefit in my case was that I can test a single view function as a black box, with exactly known inputs, testing for specific outputs. Just as described in the docs.
class TestView(TemplateView):
template_name = 'base.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = {'1': 11337}
return context
# ...
def test_context(self):
factory = RequestFactory()
request = factory.get('/customer/details')
response = TestView.as_view()(request)
self.assertIsInstance(response.context_data, dict)
self.assertEqual(response.context_data['1'], 1337)

Related

How can I test the URLS for my Class Based Views in Django?

I'm trying to test the URL resolutions for my first Django project- I have successfully tested my function-based views, however I am having trouble testing my Class Based Views.
I'm getting the below error when I run the test on my CBV:
AssertionError: <function UpdateHealth at 0x7f538f023e50> != <HealthHub.views.UpdateHealth object at 0x7f538d7aec10>
Tests.py (CBV test in question):
def test_health_hub_update_url_is_resolved(self):
url = reverse('HealthHub:health_hub_update')
self.assertEqual(resolve(url).func, views.UpdateHealth())
views.py (view in question):
class UpdateHealth(View):
'''View for the Update Health page.
Uses StatUpdateForm to allow the user to update their stats.'''
def get(self, request, *args, **kwargs):
stats = HealthStats
update_form = StatUpdateForm
context = {
'stats': stats,
'update_form': update_form,
'user': stats.user,
'weight': stats.weight,
'date': stats.date,
}
return render(request, 'health_hub_update.html', context)
def post(self, request, *args, **kwargs):
update_form = StatUpdateForm(data=request.POST)
if update_form.is_valid():
obj = update_form.save(commit=False)
obj.user = request.user
obj.save()
return redirect("HealthHub:health_hub")
Urls.py:
path('MyHealth/update', views.UpdateHealth.as_view(), name='health_hub_update'),
Any help would be much appreciated, as I seem to have hit a dead-end.
You can check with the .view_class attribute attached to the function:
def test_health_hub_update_url_is_resolved(self):
url = reverse('HealthHub:health_hub_update')
self.assertEqual(resolve(url).func.view_class, views.UpdateHealth)

Subclassing TemplateView with Mixins - a bad idea?

I have several "Listing" views which are very similar and I feel like I'm unecessarily repeating myself. Subclassing seems like the answer, but I've run into problems down the line before when subclassing things in Django so I'd like to ask before doing so.
If I have 2 views, like this, which use the same template, a variable message and different querysets:
class MyGiverView(LoginRequiredMixin, TemplateView):
template_name = "generic_listings.html"
message = ""
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["list_of_stuff"] = MyModel.objects.filter(
giver=self.request.user,
status=1,
)
context["message"] = self.message
return context
class MyTakerView(LoginRequiredMixin, TemplateView):
template_name = "generic_listings.html" # this hasn't changed
message = ""
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["list_of_stuff"] = MyModel.objects.filter(
taker=self.request.user, # this has changed
status__in=(1,2,3), # this has changed
)
context["message"] = self.message # this hasn't changed
return context
Am I going to screw it up by creating a base class like:
class MyBaseView(LoginRequiredMixin, TemplateView):
template_name = "generic_listings.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["list_of_stuff"] = self.qs
context["message"] = self.message
return context
And using it in my views as such:
class MyGiverView(MyBaseView):
qs = MyModel.objects.filter(
giver=self.request.user,
status=1,
)
message = ""
class MyTakerView(MyBaseView):
qs = MyModel.objects.filter(
taker=self.request.user,
status__in=(1,2,3),
)
message = ""
It's DRYer, but I'm unsure of the implications regarding what goes on "under the hood".
Instead of creating a base class, it is better to create a mixin class for what you need.
Also, using ListView would be better for what you need.
This is my suggestion:
class InjectMessageMixin:
message = ""
# Always use this method to get the message. This allows user
# to override the message in case it is needed to do on a request
# basis (more dynamic) instead of using the class variable "message".
def get_message(self):
return self.message
def get_context_data(self, **kwargs):
# This line is super important so you can normally use
# this mixin with other CBV classes.
context = super().get_context_data(**kwargs)
context.update({'message': self.get_message()})
return context
class MyGiverView(LoginRequiredMixin, InjectMessageMixin, ListView):
# Even though this doesn't change I would keep it repeated. Normally,
# templates are not reused between views so I wouldn't say it's necessary
# to extract this piece of code.
template_name = "generic_listings.html"
message = "giver message"
def get_queryset(self):
return MyModel.objects.filter(
giver=self.request.user,
status=1,
)
class MyTakerView(LoginRequiredMixin, InjectMessageMixin, ListView):
# Even though this doesn't change I would keep it repeated. Normally,
# templates are not reused between views so I wouldn't say it's necessary
# to extract this piece of code.
template_name = "generic_listings.html"
message = "taker message"
def get_queryset(self, **kwargs):
return MyModel.objects.filter(
taker=self.request.user, # this has changed
status__in=(1,2,3), # this has changed
)

Send context the template in a class based view

How can I send context to my template in a class-based view with the get_object function?
This is my class:
class DetailMessages(LoginRequiredMixin, DetailView, ChannelFormMixin):
template_name = 'DM/chat.html'
def get_object(self, *args, **kwargs):
my_username = self.request.user.username
username = self.kwargs.get("username")
channel, _ = Channel.objects.get_dm_channel(my_username, username)
if channel == None:
raise Http404
context = {"example1" : "I want this on the template", "example2" : "I want this on the template too"}
return channel
It's usually not good idea to mix methods in class based views. You can pass context in two ways: in get() or get_context_data(). Examples:
# or
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["example1"] = "I want this on the template"
context["example2"] = "I want this on the template too"
return context
# or
def get(self, request, *args, **kwargs):
context = {}
context["example1"] = "I want this on the template"
context["example2"] = "I want this on the template too"
return render(..., context=context)
If you don't actually need to operate with get() (or post()) method, then much better way is to leave context managing to get_context_data() method.

TypeError: context must be a dict rather than HttpResponseRedirect

I'm using Django 2.0 and have been trying to redirect user to other view from get_context_data
my url pattern is
mainapp.urls
urlpatterns = [
path('learn/', include('learn.urls', namespace='learn')),
path('admin/', admin.site.urls),
]
app.url
app_name = 'learn'
urlpatterns = [
path('success/<course_learn_id>/<session>', LearnSuccess.as_view(), name='success'),
]
and LearnSuccess view
class LearnQuestion(FormView):
form_class = SessionForm
template_name = 'learn/learn_question.html'
def get_context_data(self, **kwargs):
context = super(LearnQuestion, self).get_context_data(**kwargs)
course_learn = CourseLearn.objects.get(pk=self.kwargs['course_learn_id'])
session = self.request.GET['session']
question, question_type, options, complete = CourseLearn.objects.get_next_question(course_learn, session)
if complete:
return redirect('learn:success', course_learn_id=course_learn.pk, session=session)
context['complete'] = complete
context['question'] = question
context['question_type'] = context_type
context['options'] = options
context['session'] = session
return context
#method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
return super(self.__class__, self).dispatch(request, *args, **kwargs)
I'm using Ajax to render this view and want to redirect user when complete is True
But this is giving error as
TypeError: context must be a dict rather than HttpResponseRedirect.
I even tried return reverse() but it is also giving error.
Trying
return redirect('learn:success', kwargs={'course_learn_id':course_learn.pk, 'session':session})
gives error
django.urls.exceptions.NoReverseMatch: Reverse for 'success' with keyword arguments
'{'kwargs': {'course_learn_id': UUID('374ccfcd-37b5-40d3-8673-01ca111f42bc'), 'session': '1524972935'}}' not found.
1 pattern(s) tried: ['learn\\/success\\/(?P<course_learn_id>[^/]+)\\/(?P<session>[^/]+)$']
get_context_data() is supposed to be for you to add additional context to the template before it is rendered. It is not for doing other view-level logic.
You are trying to return a redirect response object from there, which is invalid - the return value of get_context_data() can only be a dictionary.
The logic you are currently trying to perform here should instead be in your view's get() method, something like:
def get(self, *args, **kwargs):
course_learn = CourseLearn.objects.get(pk=kwargs['course_learn_id'])
session = self.request.GET['session']
question, question_type, options, complete = CourseLearn.objects.get_next_question(course_learn, session)
if complete:
return redirect('learn:success', course_learn_id=course_learn.pk, session=session)
return super().get(*args, **kwargs)
Solved it by using render_to_response. For those who may need it, add a function inside the class
def render_to_response(self, context, **response_kwargs):
if context['complete']:
return redirect(reverse('learn:success',
kwargs={
'course_learn_id': context['course_learn'].pk,
'session': context['session']
}))
return super(LearnQuestion, self).render_to_response(context, **response_kwargs)
and from get_context_data() send the context data
if complete:
context['complete'] = complete
context['course_learn'] = course_learn
context['session'] = session
return context

django variable of one view to another from session

I m very confused on this and I dont have any idea how to do this..
I have a view where I have listed all the news from my news table. To display the news I have passed context data in list view. Here is my view
class SingleNewsView(ListView):
model = News
form_class = SearchForm
template_name = "single_news.html"
# def post(self, request, **kwargs):
# print "request"
# form = SearchForm(request.user)
def get(self, request, pk, **kwargs):
#form = SearchForm(request.user)
self.pk = pk
self.pub_from = request.GET.get('pub_date_from',False)
self.pub_to = request.GET.get('pub_date_to',False)
self.crawlers = request.GET.get('crawler',False)
print self.crawlers
return super(SingleNewsView,self).get(request,pk, **kwargs)
def get_context_data(self, **kwargs):
context = super(SingleNewsView,self).get_context_data(**kwargs)
context["form"] = SearchForm
if self.pub_from and self.pub_to and self.crawlers:
context["something"] = News.objects.filter(category_id=self.pk).filter(published_date__range=(self.pub_from,self.pub_to), crawler=self.crawlers)
else:
context["something"] = News.objects.filter(category_id=self.pk)
return context
and I have written view that I referenced from django doc to download the news in csv format.. I have also included a search form to filter the news. In my first view I have passed context["something"] to display the list of news in the template.
Now what I want is to download that news in csv. I have written a view for this
def CSVView(request):
response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = 'attachment; filename=somefilename.csv"'
some_val = request.session["something"]
print some_val
print "this"
writer = csv.writer(response)
writer.writerow(some_val)
return response
This is my next view to download the csv. Here what I am trying to do is to download the news that come after filter. In my first view context["something "] gives the list of news. I did all but dont know how to get it. Lastly I m trying to get the value of contxt["something"] from session but I am failed in that too. How can I get the value of one view to another. Or anyone have better idea how can I download the news that is returned by context["something"]. What am I doing wrong.
Setting data in context does not put it in session. You need to set data in session to store it there. Also, storing objects directly in session is not a good idea. You may have to serialize them.
Better way would be create list of pks of objects you want into session.
Something like:
def get_context_data(self, **kwargs):
context = super(SingleNewsView,self).get_context_data(**kwargs)
context["form"] = SearchForm
if self.pub_from and self.pub_to and self.crawlers:
qs = News.objects.filter(category_id=self.pk).filter(published_date__range=(self.pub_from,self.pub_to), crawler=self.crawlers)
else:
qs = News.objects.filter(category_id=self.pk)
context["something"] = qs
#set data in session
self.request.session['something_pks'] = [ i.pk for i in qs ]
return context
Then in CSVView you can get them with `request.session['something_pks'] and do the query for objects.