Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Send a file through Django Class Based Views

We use class based views for most of our project. We have run into an issue when we try and create a CSV Mixin that will allow the user to export the information from pretty much any page as a CSV file. Our particular problem deals with CSV files, but I believe my question is generic enough to relate to any file type.

The problem we are having is that the response from the view is trying to go to the template (say like from django.views.generic import TemplateView). We specify the template in the urls.py file.

url(r'^$', MyClassBasedView.as_view(template_name='my_template.html'))

How can you force the response to bypass the template and just return a standard HttpResponse? I'm guessing you'll need to override a method but I'm not sure which one.

Any suggestions?

EDIT1: It appears that I was unclear as to what we are trying to do. I have rendered a page (via a class based view) and the user will see reports of information. I need to put in a button "Export to CSV" for the user to press and it will export the information on their page and download a CSV on to their machine.

It is not an option to rewrite our views as method based views. We deal with almost all class based view types (DetailView, ListView, TemplateView, View, RedirectView, etc.)

like image 500
Rico Avatar asked Apr 29 '13 19:04

Rico


2 Answers

This is a generic problem when you need to provide different responses for the same data. The point at which you would want to interject is when the context data has already been resolved but the response hasn't been constructed yet.

Class based views that resolve to the TemplateResponseMixin have several attributes and class methods that control how the response object is constructed. Do not be boxed into thinking that the name implies that only HTML responses or those that need template processing can only be facilitated by this design. Solutions can range from creating custom, reusable response classes which are based on the behavior of the TemplateResponse class or creating a reusable mixin that provides custom behavior for the render_to_response method.

In lieu of writing a custom response class, developers more often provide a custom render_to_response method on the view class or separately in a mixin, as it's pretty simple and straightforward to figure out what's going on. You'll sniff the request data to see if some different kind of response has to be constructed and, if not, you'll simply delegate to the default implementation to render a template response.

Here's how one such implementation might look like:

import csv
from django.http import HttpResponse
from django.utils.text import slugify
from django.views.generic import TemplateView


class CSVResponseMixin(object):
    """
    A generic mixin that constructs a CSV response from the context data if
    the CSV export option was provided in the request.
    """
    def render_to_response(self, context, **response_kwargs):
        """
        Creates a CSV response if requested, otherwise returns the default
        template response.
        """
        # Sniff if we need to return a CSV export
        if 'csv' in self.request.GET.get('export', ''):
            response = HttpResponse(content_type='text/csv')
            response['Content-Disposition'] = 'attachment; filename="%s.csv"' % slugify(context['title'])

            writer = csv.writer(response)
            # Write the data from the context somehow
            for item in context['items']:
                writer.writerow(item)

            return response
        # Business as usual otherwise
        else:
            return super(CSVResponseMixin, self).render_to_response(context, **response_kwargs):

Here's where you can also see when a more elaborate design with custom response classes might be needed. While this works perfectly for adding ad-hoc support for a custom response type, it doesn't scale well if you wanted to support, say, five different response types.

In that case, you'd create and test separate response classes and write a single CustomResponsesMixin class which would know about all the response classes and provide a custom render_to_response method that only configures self.response_class and delegates everything else to the response classes.

like image 193
Filip Dupanović Avatar answered Nov 09 '22 05:11

Filip Dupanović


How can you force the response to bypass the template and just return a standard HttpResponse?

This kinda defeats the point of using a TemplateView. If the thing you're trying to return isn't a templated response, then it should be a different view.

However...

I'm guessing you'll need to override a method but I'm not sure which one.

...if you prefer to hack it into an existing TemplateView, note from the source code...

class TemplateView(TemplateResponseMixin, ContextMixin, View):
    """
    A view that renders a template.  This view will also pass into the context
    any keyword arguments passed by the url conf.
    """
    def get(self, request, *args, **kwargs):
        context = self.get_context_data(**kwargs)
        return self.render_to_response(context)

...so you'd have to override the get() method so it doesn't call render_to_response() when returning your CSV. For example...

class MyClassBasedView(TemplateView):
    def get(self, request, *args, **kwargs):
        if request.GET['csv'].lower() == 'true':
            # Build custom HTTP response
            return my_custom_response
        else:
            return TemplateView.get(request, *args, **kwargs)

If you need a generic mixin for all subclasses of View, I guess you could do something like...

class MyMixin(object):
    def dispatch(self, request, *args, **kwargs):
        if request.GET['csv'].lower() == 'true':
            # Build custom HTTP response
            return my_custom_response
        else:
            return super(MyMixin, self).dispatch(request, *args, **kwargs)

class MyClassBasedView(MyMixin, TemplateView):
    pass
like image 1
Aya Avatar answered Nov 09 '22 03:11

Aya