0

On my Django app, users get a unique (partly random) output for each request. I would like to allow the user to download the exact same output (shown in an HTML table) as CSV.

My difficulty is how to design this flow so the result generated from form.generate_cassettes() will be used for the csv export.

Where should the export logic go? Where should the csv response be located in my view? What should the "export to csv" button in the HTML page be calling?

This is the view in my django app:

def generate_rna(request):
if request.method == 'POST':
    form = RNAGeneratorForm(request.POST)

    if form.is_valid():
        result = form.generate_cassettes()
        context = {
            "form": form,
            "cassettes": result
        }

        return render(request, 'rnagen/RNA.html', context)

else:
    form = RNAGeneratorForm()

context = {
    "form": form
}

return render(request, 'rnagen/RNA.html', context)

The "cassettes": result and the and the input form (field name string) is all the information I need to generate the csv.

2 Answers2

0

Assumptions

  • the method generate_cassettes() produces a small dataset that won't cause any memory issues.
  • No models(and hence model instances) are being used.

Approach

  • We have 2 views, namely generate_rna and download_csv. generate_rna points to myapp/(in my case, it can point to anything else as desirable in general) whereas download_csv points to myapp/download/csv(in my case, it can point to anything else as desirable in general).
  • When you hit GET myapp/, generate_rna renders an empty form
  • When you hit POST myapp/, generate_rna takes in the data submitted by the user, validates it, and if the data is valid, it calls generate_cassettes, where it creates the data, and then it renders a form with populated data, a table showing the data generated by generate_cassettes and also shows a "Download CSV" button
  • When you hit POST myapp/download/csv with cassette data(generated by generate_cassettes) as payload, it creates a csv file out of the data(using python csv module and changing Content-Type and Content-Disposition headers) and serves it for download.

Here is some code myapp urls.py

from django.urls import path
from .views import generate_rna, download_csv
urlpatterns = [
    path('', generate_rna),
    path('download/csv/', download_csv)
]

myapp forms.py

from django import forms
    class RNAGeneratorForm(forms.Form):
        name = forms.CharField(max_length=100)
        price = forms.DecimalField(decimal_places=2)
        def generate_cassettes(self):
            #The behaviour of this function is not given in question and hence is assumed.
            return [['Name', 'Price'], ['Cassette 1', '130'], ['Cassette 2', '140']]

RNA.html

<html>
    <head>
        <style>
            table, th, td {
              border: 1px solid black;
            }
        </style>
        <title> RNA Form </title>
    </head>
<body>
    <!-- This form takes input from user -->
<form action="/myapp/" method="POST">
    {% csrf_token %}
    {{ form }}
    <input type="submit" value = "Submit" />
</form>

{% if cassettes %}

<table>
    <tr>
        {% for column_name in cassettes.0 %}
            <th> {{ column_name }} </th>
        {% endfor %}
    </tr>
    {% for row in cassettes|slice:"1:" %}
    <tr>
        {% for column in row %}
            <td> {{ column }} </td>
        {% endfor %}
    </tr>
    {% endfor %}
</table>

<form action="/myapp/download/csv/" method="POST">
    {% csrf_token %}
    <input type="submit" value="Download CSV">
    <input type="hidden" id="cassette-data" name="cassette-data">
</form>
{{ cassettes|json_script:"cassette-json" }}
<script>
    document.getElementById("cassette-data").value = document.getElementById("cassette-json").textContent;
</script>
{% else %}
No Cassettes
{% endif %}
</body>
</html> 

The Approach Used in RNA.html

  • If cassettes exist(for example, if the server responds to a POST myapp/), I render the data as a table and show the "Download CSV button". If you see this link https://docs.djangoproject.com/en/3.0/ref/templates/builtins/#json-script, by using json_script there is a way to expose an object in the form of json. I then have a script with id "cassette-json"(Please refer to this line in the html source {{ cassettes|json_script:"cassette-json" }}). I have another script tag in which i assign the cassettes as json to a hidden input(of name "cassette-data"). The "Download CSV" button is embedded in another form that sends "cassette-data" as payload to myapp/download/csv.

myapp views.py

from django.shortcuts import render, HttpResponse
from .forms import RNAGeneratorForm
import csv
import json

def generate_rna(request):
    if request.method == 'POST':
        form = RNAGeneratorForm(request.POST)
        if form.is_valid():
            result = form.generate_cassettes()
            context = {
                "form": form,
                "cassettes": result
            }

            return render(request, 'myapp/RNA.html', context)

    else:
        form = RNAGeneratorForm()

    context = {
        "form": form
    }

    return render(request, 'myapp/RNA.html', context)

def download_csv(request):
    if request.method == 'POST':
        data = request.POST
        response = HttpResponse(content_type='text/csv')
        response['Content-Disposition'] = 'attachment; filename="cassettes.csv"'

        csv_writer = csv.writer(response)
        for row in json.loads(data['cassette-data']):
            print(row)
            csv_writer.writerow(row)
        return response 

Please reach out in case of any queries.

Community
  • 1
  • 1
punter147
  • 302
  • 2
  • 7
0
  • cassettes is an object that contains django model objects sequence

  • sequence has it's own get_type and score attribute needed to parse:

    class Cassette:
    def __init__(self):
        self.sequences = []  # list of BaseSequences
        self.score = 0.0     # total cassette score
        self.scores = {}     # score per protein

    def add(self, sequence):
        self.sequences.append(sequence)
        self.score += sequence.score

        if isinstance(sequence, Binding):
            sequence_type = sequence.get_type()
            self.scores[sequence_type] = self.scores.get(sequence_type, 0) + sequence.score

    def add_list(self, sequence_list):
        for s in sequence_list:
            self.add(s)

So I get Object of type Cassette is not JSON serializable error.

I am trying to find a way no to seralize this object on client-side and deserialize it on the views.

jossefaz
  • 3,312
  • 4
  • 17
  • 40