Returning an HttpResponse with streamed object AND messages from messages framework

Hey,

I have a page with a bunch of reports/files (pdf, xlsx, etc) that can be generated & downloaded by users. For instance this mixin:

class StreamHttpResponseMixin:
    """ builds and HttpResponse with streamed file/content we want to return  """
    def build_http_response(self, doc):
        httpresponse = HttpResponse(content_type=doc["content_type"], content=doc["content"])
        httpresponse['Content-Disposition'] = f'attachment; filename={doc["filename"]}'
        httpresponse['Content-Transfer-Encoding'] = 'binary'
        return httpresponse

Where doc contains the xlsx files formatted by xlsxwriter. Similarly for PDF, with adjusted headers, content-types etc. Works fine, downloads the file as attachements.

The user clicks a button to obtain the report:
<button id="" type="submit" name="action" value="rapport_2" class="btn btn-secondary" data-toggle="tooltip" title="">Rapport 2</button>

Which submits via a POST (where I use the value attribute to determine what to build for a file response). THen I typically have some parameters the user can set for the different reports, which are processed as forms submitted.

That all works fine, however if I add stuff via the django.contrib.messages framework, they don’t get displayed. Guessing because I tell the browser I want to stream stuff, and it doesn’t refresh.

I could rewrite stuff so that I actually do the same thing ajax with jquery I guess (and once I get the response, resfresh the page to display the messages as per normal). However is that a way to just tweak the mixin/headers of the HttpResponse so I don’t have to rewrite as much stuff? Maybe tweaking the headers or http content so that the browsers knows to display the messages as well?

Not that I’m aware of. The Content-Disposition of “attachment” indicates a file is being sent, not a page for the browser to display. Your response can be one or the other, but not both in the same response.

See Content-Disposition - HTTP | MDN for a more detailed explanation.

1 Like

Yeah it’s probably not really possible. Reading the docs Ken linked above, MAYBE using multipart forms one could get to that. But I couldn’t find any good example and reading the raw http specifications is no fun.

So instead, I made the POST via jquery, and then I could handle the returned partials as one might do to re-render/append an html string to part of the view. Broadly speaking, something like this:

    ajax_post_handler: function (e, clickedBtn) {
        // clickedBtn is the "this" of the previous scope, e.g. the action btn that was clicked from the caller
        let url = $(clickedBtn).attr("data-url");
        let action = $(clickedBtn).attr("value");
        $input = $('<input id="action_name" type="text" name="action" display=none>').val(action);
        let csrftoken = $('input[name="csrfmiddlewaretoken"]').val();
        $('form#mainform').append($input)
        let data = $('form#mainform').serialize();
        $('#action_name').remove();
        $.ajax({
            url: url,
            type: "POST",
            data: data,
            headers: {'X-CSRFToken':csrftoken},
            success: function (response) {
                if (response.status==1) {  
                    if (response.download) {   // download the file from an ajax endpoint that returns it
                        window.location.href=response.download.url
                    } else if (response.current_page.next_url) { ...// do other stuff }
                }
                else if (response.status==0) {        // some sort of errors in the form
                    for (const [section_id, html_str] of Object.entries(response.partials_to_render)) {
                        $("#" + section_id).replaceWith(html_str);
                    }
                }
            },
            error: function (xhr, errmsg, err) {
                alert("Une erreur est survene - aucun changement n'a été enregistré.")
                $('#top_messages').html('<div class="alert alter-dismissible alert-danger" role="alert">\n' +
                    '                <button class="close" type="button" data-dismiss="alert" aria-label="Close">\n' +
                    '                    <span aria-hidden="true">&times;</span>\n' +
                    '                </button>\n' +
                    '               Erreur. Rien n&#39;a été enregistré. Svp revoir les données et ré-essayer.\n' +
                    '            </div>'); // add the error to the dom
                }
        });
    },

I sort of had to append a dummy with the selected action value (e.g. just some ID of the report I want to produce, rapport_1, rapport_2, etc…). Feels a bit hacky, but it’s just that the buttons all already had that info with them and the rest of the code uses it already. Then I have a partial in most pages to which I append everything from the messages framework (the section_id bit, which also manages other partials I sometimes want to resfresh in the view).

$(".btn_ajax_downloads").on("click", function(e){
    e.preventDefault();
    let clickedBtn = this;
    mainjs.ajax_post_handler (e, clickedBtn);
})

Then some ajax endpoints that fetches the data from the DB, formats a document somewhat as posted above for xlsx/pdf/etc. as a stream to be downloaded.

Since I have a bunch of buttons, it was easier to make the selector a class, and then sort out what report to build in the def post(…) of CBV:


    def post(self, request, *args, **kwargs):
        self.object = None              # crucial - otherwise fails has the object attribute is missing

        action = self.request.POST["action"]
        if action == "rapport_1":
            # do stuff