Flask-wtf with trix-editor - How do I upload images - javascript

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.

Related

Weasyprint django - generates an empty PDF

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

How to download a document generated with python-docx in Django project?

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

How to download multiple files with window.open()?

I am trying to download multiple files at once. At first I use window.location = url but now it doesn't seem to work.
I've changed it to window.open(url, "_blank") and it only downloads the first one:
window.open("/host/Controller/DownloadDasFiles?paramId=204");
window.open("/host/Controller/DownloadDasFiles?paramId=205");
window.open("/host/Controller/DownloadDasFiles?paramId=206");
public FileResult DownloadDasFiles(int paramId)
{
var dasControl = UnityConfig.container.Resolve<IDasControlService>();
var filename = dasControl.GetDasFileToDownload(paramId);
return File(filename, "application/octet-stream", Path.GetFileName(filename));
}
In my real case I do this after AJAX success in a javascript loop, but this code should work, shouldn't it?
This may help. It will open all 3 windows at same time.
let test = [204, 205, 206];
let downLoadFileBaseUrl = '/host/Controller/DownloadDasFiles?paramId='
test.forEach(element => {
let file = `${downLoadFileBaseUrl}+${element}`;
window.open(file, '_blank', 'File :' + element + ',scrollbars=1,menubar=0,resizable=1,width=850,height=500');
});

Change name of uploaded file on client

I have the following.
<form method="post" action="/send" enctype="multipart/form-data">
<input type="file" name="filename" id="AttachFile">
</form>
I want to change the name of the file the user uploads.
If the user selects "Document.docx" I want to change it to "Bank - Document.docx".
I still want to read the file the user selected, not some other file, just use a different name for it when sending to the server.
I'm working within bounds of an application which doesn't allow control of the server side, so ideally I need to do this on the client. Furthermore I need this to work within the confines of a form.
I have tried variations of the following without success:
document.getElementById("AttachFile").name = "test.txt"
document.getElementById("AttachFile").files = "test.txt"
document.getElementById("AttachFile").value ="test.txt"
You can do it through the File API. We can also use the Blob API to be compatible with Microsoft edge.
var file = document.getElementById("AttachFile").files[0];
var newFile = new File([file], "Bank - Document.docx", {
type: file.type,
});
Here's a complete example — see comments:
HTML:
<input type="file" id="AttachFile">
<input type="button" id="BtnSend" value="Send">
JavaScript:
document.getElementById("BtnSend").addEventListener("click", function() {
// Get the file the user picked
var files = document.getElementById("AttachFile").files;
if (!files.length) {
return;
}
var file = files[0];
// Create a new one with the data but a new name
var newFile = new File([file], "Bank - Document.docx", {
type: file.type,
});
// Build the FormData to send
var data = new FormData();
data.set("AttachFile", newFile);
// Send it
fetch("/some/url", {
method: "POST",
body: data
})
.then(response => {
if (!response.ok) {
throw new Error("HTTP error " + response.status);
}
return response.text(); // or response.json() or whatever
})
.then(response => {
// Do something with the response
})
.catch(error => {
// Do something with the error
});
});
You can't rename the file using a standard form submission. The name of the file being uploaded is read-only. To do this, you'd have to do it server-side. (The designers of file uploads seem to have either not considered this rename-on-upload use case or not felt it needed to be addressed by the API.)
However, you can prevent the default form submission and instead submit it programmatically via ajax, which does allow you to rename the file; see man tou's answer.
If you cannot work on the server side then you have to either rename the file BEFORE upload or AFTER download. How you present the name for the user is you to decide.

On Bootstrap modal confirm button click ,create and download file jquery or knockout js

I have a bootstrap modal( bootbox ), on which confirm action, i want to create a file which contains some numbers and download the file as a text file.
I have already referred Create a file in memory for user to download, not through server but its not helpful in my case.
Here is the code :
bootbox.confirm("Item not found! Do you want to download the text file?", (yes) => {
var items = response.itemNumbers;
this.href = "data:text/plain;charset=UTF-8," + encodeURIComponent(items);
});
I have tried this as well :
bootbox.confirm("item not found! Do you want to download the text file?", (yes) => {
var itemNumbers = response.itemNumbers;
var textFile = null;
var data = new Blob([itemNumbers], { type: 'text/plain' });
// deleting old text file
if (textFile !== null) {
window.URL.revokeObjectURL(textFile);
}
textFile = window.URL.createObjectURL(data);
var link = $(textFile).attr("href");
document.location.href = link;
});
EDIT : When I run this, first code runs and nothing happens. Second gives error about Blob conversion : Syntax error, unrecognized expression: blob:http%3A//localhost%3A51876/028f2122-1646-4b2e-ae2c-2a1b588fdd8d
Got it! thanks #kb. for helping. The link you provided had the answer. Just posting here for future visitors: below code would create a file which contains "abcd,wxyz" and prompt for download :
var items = "abcd,wxyz";
var json = JSON.stringify(items);
var blob = new Blob([json], {type: "octet/stream"});
var url = window.URL.createObjectURL(blob);
window.location.assign(url);
reference : JavaScript blob filename without link

Categories