I'm trying to figure how to download a word document generated with python-docx in my django app (I'm still learning and this is the first time I working with documents); with the help of ajax I send all the information needed to the view and call a function that uses that information and returns the document, then I'm trying to send this document as response in order to download it with the help of a "Download" button (or show web browser download dialog) in the same template from where I'm submitting the data, but here is where I'm stuck.
to send this document as response in order to download it with the help of a "Download" button (or show web browser download dialog) in the same template from where I'm submitting the data, but here is where I'm stuck.
What I have until now is:
1) In javascript I'm sending the information as follows:
data = {
categoria: cat,
familia: fam,
Gcas: gcas,
FI: FI,
FF: FF,
Test: test,
Grafica: grafica
},
$.ajax({
type: 'post',
headers: {
"X-CSRFToken": csrftoken
},
url: url,
data: { json_data: JSON.stringify(data) },
success: function (response) {
$('#instrucciones').hide(); //Hide a div with a message
$('#btndesc').show(); //Show the button to download the file generated
}
});
return false;
}
2) In my Django view:
def Documento(request):
if request.method == "GET":
context={}
context['form'] = catForm
return render(request, 'report/report_base.html', context)
if request.method == 'POST':
#Data from ajax
datos = request.POST.get('json_data')
jsondata = json.loads(datos)
Gcas = jsondata['Gcas']
FI = jsondata['FI']
FF = jsondata['FF']
grafica = jsondata['Grafica']
#Using function to create the report
Reporte = ReporteWord(Gcas, FI, FF, grafica)
#Response
response = HttpResponse(content_type='application/vnd.openxmlformats-
officedocument.wordprocessingml.document')
response['Content-Disposition'] = 'attachment; filename = "Reporte.docx"'
response['Content-Encoding'] = 'UTF-8'
Reporte.save(response)
return response
3) My function to create the document looks like:
def ReporteWord( gcas, FI, FF, Chart):
#Cargamos el template
template = finders.find('otros/Template_reporte.docx')
document = Document(template)
#Header
logo = finders.find('otros/logo.png')
header = document.sections[0].header
paragraph = header.paragraphs[0]
r = paragraph.add_run()
r.add_picture(logo)
#Adding title
titulo = document.add_heading('', 0)
titulo.add_run('Mi reporte').bold = True
titulo.style.font.size=Pt(13)
.
Many other steps to add more content
.
.
#IF I SAVE THE FILE NORMALLY ALL WORKS FINE
#document.save(r'C:\tests\new_demo.docx')
return document
I'll be very grateful for any idea or suggestion, many thanks in advance.
NOTE: I've reviewed these answers (and others) without luck.
Q1, Q2, Q3, Q4
UPDATE: Thanks to the feedback received I finally found how to generate the document and show the download dialog:
As was suggested the best way to achieve its using the view and not ajax, so the final updates in the code are:
a) Update view to work as show in feedback
b) JavaScript - Ajax control for POST method was removed and now all is handled directly with python (no extra code needed)
1) View:
def Reporte(request):
if request.method == "GET":
context={}
context['form'] = catForm
return render(request, 'reportes/reporte_base.html', context)
if request.method == 'POST':
#Getting data needed after submit the form in the page
GcasID = request.POST.get('GCASS')
FI = request.POST.get('dp1')
FF = request.POST.get('dp2')
Grafica = request.POST.get('options')
#Function to obtain complete code from GcasID
Gcas = GcasNumber(GcasID)
#Report creation
Reporte = ReporteWord(Gcas, FI, FF, Grafica)
#PART UPDATED TO SHOW DOWNLOAD REPORT DIALOG
bio = io.BytesIO()
Reporte.save(bio) # save to memory stream
bio.seek(0) # rewind the stream
response = HttpResponse(
bio.getvalue(), # use the stream's contents
content_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
)
response["Content-Disposition"] = 'attachment; filename = "Reporte.docx"'
response["Content-Encoding"] = "UTF-8"
return response
With those changes now when I press "Create report" (submit button of form) all works as expected (as a plus no more libraries are necessary). At the end as you suggested its easier do it in this way than using ajax.
Many thanks to all for your kind help.
Python-docx's Document.save() method accepts a stream instead of a filename. Thus, you can initialize an io.BytesIO() object to save the document into, then dump that to the user.
Reporte = ReporteWord(Gcas, FI, FF, grafica)
bio = io.BytesIO()
Reporte.save(bio) # save to memory stream
bio.seek(0) # rewind the stream
response = HttpResponse(
bio.getvalue(), # use the stream's contents
content_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
)
response["Content-Disposition"] = 'attachment; filename = "Reporte.docx"'
response["Content-Encoding"] = "UTF-8"
return response
This will work if you use a regular link or a form to submit the request, but since you're using $.ajax, you may need to do additional work on the browser end to have the client download the file. It would be easier not to use $.ajax.
Yep, a cleaner options, as stated by wardk would be, using https://python-docx.readthedocs.org/:
from docx import Document
from django.http import HttpResponse
def download_docx(request):
document = Document()
document.add_heading('Document Title', 0)
response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document')
response['Content-Disposition'] = 'attachment; filename=download.docx'
document.save(response)
return response
Know more
Related
I'm using Flask with one of my wtforms TextAreaFields mapped to Trix-Editor. All works well except for images using the built toolbar attach button.
I'd like to save the images to a directory on the backend and have a link to it in the trix-editor text. I'm saving this to a database.
I can make this work by adding an <input type='file'/>in my template like so:
{{ form.description }}
<trix-editor input="description"></trix-editor>
<input type="file"/>
and the following javascript which I found somewhere as an example.
document.addEventListener('DOMContentLoaded', ()=> {
let contentEl = document.querySelector('[name="description"]');
let editorEl = document.querySelector('trix-editor');
document.querySelector('input[type=file]').addEventListener('change', ({ target })=> {
let reader = new FileReader();
reader.addEventListener('load', ()=> {
let image = document.createElement('img');
image.src = reader.result;
let tmp = document.createElement('div');
tmp.appendChild(image);
editorEl.editor.insertHTML(tmp.innerHTML);
target.value = '';
}, false);
reader.readAsDataURL(target.files[0]);
});
// document.querySelector('[role="dump"]').addEventListener('click', ()=> {
// document.querySelector('textarea').value = contentEl.value;
// });
});
This saves the image embedded in the text. I don't want that because large images will take up a lot of space in the database and slow down loading of the editor when I load this data back into it from the database.
It is also ugly having the extra button when Trix has an attachment button in it's toolbar. So, I'd like to be able to click the toolbar button and have it upload or if that is too hard, have the built in toolbar button save the image embedded.
To save the images to a folder instead of embedded, the Trix-editor website says to use this javascript https://trix-editor.org/js/attachments.js
In this javascript I have to provide a HOST so I use
var HOST = "http://localhost:5000/upload/"
and I set up a route in my flask file:
#tickets.post('/_upload/')
def upload():
path = current_app.config['UPLOAD_DIRECTORY']
if request.method == 'POST':
if 'file' not in request.files:
flash('No file part')
return redirect(request.url)
file = request.files['file']
if file.filename == '':
flash('No selected file')
return redirect(request.url)
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
session["id"] = filename
file.save(os.path.join(path, filename))
return send_from_directory(path, filename)
I can select an image and it shows in the editor and it uploads to the directory on my backend as expected. But when I save the form the location of the image is not in in the document text (should be in there as something like <img src="uploads/image.png>
On the python console I see
"POST /_upload/ HTTP/1.1" 404 -
I can make this go away if I change the return on my route to something like return "200" But all the examples I have seen about uploading files have this or a render_template. I don't want to render a template so I'm using this although I don't really understand what it does.
I'm assuming I need to return something the javasript can use to embed the image link in the document. But I'm a total newbie (like you didn't figure that out already) so I don't know what to do for the return statement (assuming this is where the problem lies).
If anyone else is trying to figure this out this is what I ended up doing.
Still needs a but of tweaking but works.
First I modified the example javascript for uploading to use Fetch instead of XMLHttpRequest
const editor = document.querySelector('trix-editor');
(function() {
HOST = '/_upload/'
addEventListener("trix-attachment-add", function(event) {
if (event.attachment.file) {
uploadFileAttachment(event.attachment)
}
// get rid of the progress bar as Fetch does not support progress yet
// this code originally used XMLHttpRequest instead of Fetch
event.attachment.setUploadProgress(100)
})
function uploadFileAttachment(attachment) {
uploadFile(attachment.file, setAttributes)
function setAttributes(attributes) {
attachment.setAttributes(attributes)
alert(attributes)
}
}
function uploadFile(file, successCallback) {
var key = createStorageKey(file)
var formData = createFormData(key, file)
fetch(HOST, {method: 'POST', body: formData}).then(function(response){
response.json().then(function(data){
alert(data.file, data.status)
if (data.status == 204) {
var attributes = {
url: HOST + key,
href: HOST + key + "?content-disposition=attachment"
}
console.log(attributes)
successCallback(attributes)
}
})
})
}
function createStorageKey(file) {
var date = new Date()
var day = date.toISOString().slice(0,10)
var name = date.getTime() + "-" + file.name
return [day, name ].join("/")
}
function createFormData(key, file) {
var data = new FormData()
data.append("key", key)
data.append("Content-Type", file.type)
data.append("file", file)
return data
}
})();
Then modified my Flask route (which I'll refactor, this was just slapped together to make it work):
def upload():
path = current_app.config['UPLOAD_DIRECTORY']
new_path = request.form["key"].split('/')[0]
file_upload_name = os.path.join(path, request.form["key"])
print(file_upload_name)
upload_path = os.path.join(path, new_path)
if request.method == 'POST':
if 'file' not in request.files:
flash('No file part')
return redirect(request.url)
file = request.files['file']
if file.filename == '':
flash('No selected file')
return redirect(request.url)
if file and allowed_file(file.filename):
if not os.path.exists(upload_path):
os.mkdir(upload_path)
filename = secure_filename(file.filename)
session["id"] = filename
attachment = os.path.join(upload_path, filename)
file.save(attachment)
file.close()
os.rename(attachment, file_upload_name)
print(os.listdir(upload_path))
return jsonify({'file': attachment, 'status': 204})
return f'Nothing to see here'
Anyway, I hope that helps as it took me ages to figure out.
I'm trying to edit an existing form on my site, and save the edits using Javascript (without requiring a refresh of the page). I'm using Django as well.
So far, when the user clicks 'edit' on the page, the form appropriately appears, showing the information already saved there. But when I click 'save' I get a 404 error.
The issue is in the Javascript function edit_post. I'm not sure if I have used stringify correctly either, I'm new to using Javascript with Django. Any help is appreciated.
function edit_handeler(element) {
id = element.getAttribute("data-id");
document.querySelector(`#post-edit-${id}`).style.display = "block";
document.querySelector(`#post-content-${id}`).style.display = "none";
// everything above this works and opens up the form for editing
edit_btn = document.querySelector(`#edit-btn-${id}`);
edit_btn.textContent = "Save";
edit_btn.setAttribute("class", "text-success edit");
if (edit_btn.textContent == "Save") {
edit_post(id, document.querySelector(`#post-edit-${id}`).value); //here
edit_btn.textContent = "Edit";
edit_btn.setAttribute("class", "text-primary edit");
}}
function edit_post(id, post) {
const body = document.querySelector(`#post-content-${id}`).value;
fetch("/edit_post/", {
method: "POST",
body: JSON.stringify({
body:body
})
}).then((res) => {
document.querySelector(`#post-content-${id}`).textContent = post;
document.querySelector(`#post-content-${id}`).style.display = "block";
document.querySelector(`#post-edit-${id}`).style.display = "none";
document.querySelector(`#post-edit-${id}`).value = post.trim();
});
}
Relevant html - this is inside a card, for the post itself in the html file:
<span id="post-content-{{i.id}}" class="post">{{i.text}}</span> <br>
<textarea data-id="{{i.id}}" id="post-edit-{{i.id}}"
style="display:none;" class="form-control textarea" row="3">{{i.text}}</textarea>
<button class="btn-btn primary" data-id="{{i.id}}" id="edit-btn-{{i.id}}"
onclick="edit_handeler(this)" >Edit</button> <br><br>
views.py
def edit_post(request, pk):
post = Post.objects.get(id=pk)
form = PostForm(instance=post)
if request.method == "POST":
form = PostForm(request.POST, instance=post)
if form.is_valid():
form.save()
return JsonResponse({}, status=201) # this works to edit and save to db
else:
if request.method == "GET":
form = PostForm(instance=post)
form_for_post = {'form': PostForm()}
return render(request, "network/make_post.html", {
"post": post,
"form_for_post": form,
})
urls.py (relevant ones)
path('edit_post/<str:pk>/', views.edit_post, name="edit_post"),
path('edit_post/', views.edit_post),
path("profile/<str:username>", views.profile, name="profile"),
Assuming you're running your django server locally and you're getting a 404 returned from your fetch request, then that means the url path does not exist. Either your django server isn't live or the url you're supplying the fetch request with is incorrect. If /edit_post/ is your desired endpoint, try fetching with the servers full request URL
Something like this
http://localhost:8000/edit_post
Replace 8000 with whatever port your server is running at.
Your fetch body is structured properly btw.
I have a tornado app using stream_request_body for uploading a file to server. File selection is a HTML form where JS onsubmit function is used to execute the upload handler. The JS function is async with await fetch. In case the user chooses a file above max allowed size then I use self.set_status(400) in def prepare(self). I would in this case also like to send/write a text string (self.write('File too big')?) that should be displayed in an element in the document as information to the user, how do I do this?
With my current JS script I get an error in the browser console:
Promise { <state>: "pending" }
TypeError: Response.json: Body has already been consumed.
Another issue I have with the setup of the tornado server is that eventhough I have a return in the def prepare(self) function when the file is larger than max allowed, then def data_received and def post are executed (the file is actually uploaded to server), why is that?
Any help/hints appreciated. I am new to tornado and JS, so sorry if the questions are very basic.
Using tornado ver 6.1, python 3.9
application.py
from tornado import version as tornado_version
from tornado.ioloop import IOLoop
import tornado.web
import uuid
import os
import json
MB = 1024 * 1024
GB = 1024 * MB
MAX_STREAMED_SIZE = 1024 #20 * GB
#tornado.web.stream_request_body
class UploadHandler(tornado.web.RequestHandler):
def initialize(self):
self.bytes_read = 0
self.loaded = 0
self.data = b''
def prepare(self):
self.content_len = int(self.request.headers.get('Content-Length'))
if self.content_len > MAX_STREAMED_SIZE:
txt = "Too big file"
print(txt)
self.set_status(400)
# how do I pass this txt to an document element?
self.write(json.dumps({'error': txt}))
# eventhough I have a return here execution is continued
# in data_received() and post() functions
# Why is that?
return
def data_received(self, chunk):
self.bytes_read += len(chunk)
self.data += chunk
def post(self):
value = self.data
fname = str(uuid.uuid4())
with open(fname, 'wb') as f:
f.write(value)
data = {'filename': fname}
print(json.dumps(data))
self.write(json.dumps(data))
class IndexHandler(tornado.web.RequestHandler):
def get(self):
self.render('index.html')
def main():
handlers = [(r'/', IndexHandler), (r'/upload', UploadHandler)]
settings = dict(debug=True, template_path=os.path.dirname(__file__))
app = tornado.web.Application(handlers, **settings)
print(app)
app.listen(9999, address='localhost')
IOLoop().current().start()
if __name__ == '__main__':
print('Listening on localhost:9999')
print('Tornado ver:', tornado_version)
main()
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Upload something!</title>
</head>
<body>
<h1>Upload</h1>
<form id="uploadForm">
<input type="file" name="file" id="file" />
<br />
<input type="submit" value="Upload">
</form>
<p><span id='display'></span></p>
<script>
uploadForm.onsubmit = async (e) => {
e.preventDefault();
var fileInput = document.getElementById('file');
var fileAttr = fileInput.files[0];
console.log(fileAttr);
var filename = fileInput.files[0].name;
console.log(filename);
document.getElementById('display').innerHTML =
'Uploading ' + document.getElementById("file").value;
let formData = new FormData(document.getElementById('uploadForm'));
try {
let response = await fetch(`${window.origin}/upload`, {
method: "POST",
body: formData,
});
if (!response.ok) {
console.log('error')
console.log(response.json());
// how do I update document.getElementById('display').innerHTML
// with tornado self.write when error response?
}
let result = await response.json();
console.log(result);
document.getElementById('display').innerHTML = 'Finished';
} catch(exception) {
console.log(exception);
}
};
</script>
</body>
</html>
In prepare it is not enough to return, you need to raise an exception to stop the processing.
So you have two options:
use provided features: overwrite write_error on your RequestHandler to create custom error responses, then raise tornado.web.HTTPError(400)[1] in prepare after your print
do everything yourself: use self.set_status to set an error status code, self.write, to write out whatever you need on the spot, then raise tornado.web.Finish to short circuit the processing of the request.
With your code as it is, you basically only need to replace the return in prepare with a raise tornado.web.Finish(). Obviously if you were going to do this in multiple places it makes sense to use #1, but if you only have the script you have now, #2 will do just fine.
Good afternoon,
I have the following problem in my app:
I am working on generating pdfs via WEASYPRINT for some of my views. My app is a search engine that aggregates external information, what I am looking for is:
That the user makes a search and the results are shown.
When he clicks on report, he can download a pdf with the search results. (via AJAX without having to reload the page).
So far this works perfect using xhtml2pdf, but I want to change it to WEASYPRINT because it allows more flexibility in the design of the pdf.
As I said there is an ajax function that sends the data to the pdf generation view, it receives them and generates a pdf with converting the html to pdf and sends a response that with a ".done()' function in javascript activates the download of the pdf. The problem is that this pdf is shown empty because there must be some decoding problem, or so I think.
Views.py
def ViewPDF(request, *args, **kwargs):
response = request.POST.get('nombre', None)
hits = request.POST.get('hits', None)
response2 = request.POST.get('query1', None)
info = {'searched': str(response2), 'customer': request.user.customer.name, 'type_of_search': '',
'lists_covered': 'OFAC', 'Date_of_search': str(request.POST.get('date', None)), 'hits': hits}
if response is not None:
# json_search = json.loads(response2)
if hits == 'YES':
json_data = json.loads(response)
info['type_of_search'] = 'basic search'
data = []
for i in range(len(json_data)):
data.append({'body': {
'Name': json_data[i]['name'],
'Description': json_data[i]['notes'],
'Occupation': json_data[i]['ocupation'],
'Place_of_Birth': json_data[i]['POB'],
'Date_of_Birth': json_data[i]['DOB'],
'Position': json_data[i]['other'],
'Citizenship': json_data[i]['nationality']
}})
data = {'info': info, 'body': data}
template = get_template('core/reports/free_search.html')
html = template.render(data)
pdf = HTML(string=html).write_pdf()
response = HttpResponse(pdf, content_type='application/pdf')
response['Content-Transfer-Encoding'] = 'utf-8'
if request.is_ajax():
return response
main.js
var url = '/pdf/' + query['s_pk']
$report0 = $('#report');
$report0.on('click', function () {
$.ajax({
url: url,
type: 'POST',
data: {
query1: query['search'],
nombre: JSON.stringify(query['responseData']),
date: $time,
hits: 'YES',
'csrfmiddlewaretoken': $("input[name=csrfmiddlewaretoken]").val()
}
}).done(function (response) {
console.log(response)
let blob = new Blob([response]);
let link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = query['search'] + '.pdf';
link.click();
});
The response to the request
enter image description here
I guess it has something to do with encoding, but I would appreaciate some help here... Where do you think is the problem?
Thanks in advance.
I managed to do it in the following way
Simple question. I have a .doc file generated in my mojolicious app. I want to download it. That's my question, how do I get the browser to download it?
I'm using the CPAN module MsOffice::Word::HTML::Writer to generate the doc.
Here is the sub routine in my mojolicious app, it is called by an Ajax request in Jquery:
sub down_doc {
my $self = shift;
my $doc = MsOffice::Word::HTML::Writer->new(
title => "My new Doc",
WordDocument => {View => 'Print'},
);
$doc->write("Content and Stuff");
my $save = $doc->save_as("/docs/file.doc");
$self->res->headers->content_disposition("attachment;filename=file.doc");
$self->res->headers->content_type('application/msword');
$self->render(data => $doc->content);
}
Here is my Ajax request in Jquery:
var request = $.ajax({
url: "/down_doc",
type: "post",
data: {'data': data},
});
request.done(function(response, textStatus, jqXHR) {
window.location.href = response;
});
I know that my Ajax "done" handler is wrong, I was just experimenting. How do I make my webpage prompt to save and download the .doc file?
You where pretty close, but I would recommend either of the following options...
File Download Processing with Mojolicious
You can install the plugin Mojolicious::Plugin::RenderFile to make this easy.
Example
plugin 'RenderFile';
sub down_doc {
my $self = shift;
my $doc = MsOffice::Word::HTML::Writer->new(
title => "My new Doc",
WordDocument => {View => 'Print'},
);
$doc->write("Content and Stuff");
my $save = $doc->save_as("/docs/file.doc");
$self->render_file('filepath' => "/docs/file.doc");
}
Or if you want to only use Mojo the following will work, and is explained further at the link below.
use Cwd;
app->static->paths->[0] = getcwd;
sub down_doc {
my $self = shift;
my $doc = MsOffice::Word::HTML::Writer->new(
title => "My new Doc",
WordDocument => {View => 'Print'},
);
$doc->write("Content and Stuff");
my $save = $doc->save_as("/docs/file.doc");
shift->render_static("/docs/file.doc");
}
Reference
This really isn't a problem on the server side, but rather that you can't save the response from an ajax request without using the (relatively new) File API. I would suggest replacing the ajax with a temporary form:
$('<form method="post" action="/down_doc">')
.append(
$('<input type="hidden" name="data">')
.attr("value", JSON.stringify(data))
)
.appendTo('body')
.submit();
When form is submitted and your /down_doc handler replies with the appropriate content-disposition header and the document data, the browser will do the work of handling the file save.
If you're not planning to use the file on the server after the request, this line can be removed:
my $save = $doc->save_as("/docs/file.doc");