I am trying to use wkhtmltopdf to generate a PDF and send it as an attachment via email. Here is my view:
class MYPDFView(View):
template = 'pdftemplate.html'
def get(self, request):
data = {}
response = PDFTemplateResponse(
request=request,
template=self.template,
filename="hello.pdf",
context= data,
show_content_in_browser=True,
cmd_options={'margin-top': 10,
"zoom":1,
"viewport-size" :"1366 x 513",
'javascript-delay':1000,
'footer-center' :'[page]/[topage]',
"no-stop-slow-scripts":True},
)
email = EmailMessage(
'Hello',
'Body goes here',
'from#example.com',
['to1#example.com', 'to2#example.com'],
['bcc#example.com'],
reply_to=['another#example.com'],
headers={'Message-ID': 'foo'},
attachments=[('demo.pdf', response, 'application/pdf')]
)
email.send()
return response
The error I am getting is a TypeError and it says expected bytes-like object, not PDFTemplateResponse.
I am assuming that my response variable, which I am returning to see my PDF is not the type that I am supposed to provide in the attachments attribute.
My question is, how can I convert the PDFTemplateResponse to bytes-like object before providing it in attachments triple?
return_file = "tmp/hello.pdf"
temp_file = response.render_to_temporary_file("hello.html")
wkhtmltopdf(pages=[temp_file.name], output=return_file)
email.attach_file(return_file)
Source
Related
My goal is to create a pdf using WeasyPrint and add it the the payload sent to the Docusign Api when requesting an envelope to be created.
Here are my steps:
generate the a pdf with WeasyPrint and return a based64 string
def generate_envelope_document(document_name: str, context: dict):
content = render_to_string(f"insurance_contracts/{document_name}.html",
context=context)
css = find(f"insurance_contracts/{document_name}.css")
doc = HTML(string=content, media_type="screen").write_pdf(stylesheets=[css],
zoom=0.8)
return base64.b64encode(doc).decode("utf-8")
create my envelope definition:
def create_envelope_definition(envelope_data: dict, context: dict, custom_fields: dict = None):
mandate = Document(
document_base64=generate_envelope_document("name1", context),
name="name1",
file_extension="pdf",
document_id=1,
)
conditions = Document(
document_base64=generate_envelope_document("name2", context),
name="name2",
file_extension="pdf",
document_id=2,
)
signer = Signer(
email=envelope_data["signer_email"],
name=envelope_data["signer_name"],
recipient_id="1",
routing_order="1",
)
signer.tabs = Tabs(
sign_here_tabs=[
SignHere(
anchor_string="Sign",
anchor_units="pixels",
anchor_y_offset="50",
anchor_x_offset_metadata="50",
)
]
)
envelope_definition = EnvelopeDefinition(
status="sent", documents=[mandate, conditions], recipients=Recipients(signers=[signer])
)
if custom_fields:
envelope_definition.custom_fields = CustomFields(
text_custom_fields=[
TextCustomField(name=field_name, value=field_value, required=False)
for field_name, field_value in enumerate(custom_fields)
]
)
return envelope_definition
create a Docusign Api object:
def get_envelopes_api_client():
"""
Create the docusign api client object
Return EnvelopesApi object
"""
api_client = ApiClient()
api_client.host = settings.DOCUSIGN_BASE_PATH
api_client.set_default_header("Authorization", "Bearer " + get_access_token())
envelope_api = EnvelopesApi(api_client)
return envelope_api
create and send the Docusign envelope:
envelope_api = get_envelopes_api_client()
try:
envelope = envelope_api.create_envelope(
settings.DOCUSIGN_ACCOUNT_ID, envelope_definition=envelope_definition
)
except ApiException as e:
logger.error(e.body.decode())
return None
return envelope
at the moment I'm getting this error:
{"errorCode":"INVALID_REQUEST_BODY","message":"The request body is missing or improperly formatted. Could not cast or convert from System.String to API_REST.Models.v2_1.propertyMetadata."}
I don't understand what I could be doing wrong. Is my envelope definition not correct or is there something else I am missing. I can't seem to find official documentation on how to do this. All I have found is [https://developers.docusign.com/docs/esign-rest-api/how-to/send-binary/][1] which does not use the docusign SDK.
Any help would be welcome. Thanks!
email_subject needs to be added to envelope_definition and has some value. That's the subject of the email sent out by DocuSign.
document_id="2" instead of document_id=2
anchor_x_offset_metadata should not be used here and is probably the reason for your error.
I am trying to send a pdf as a Gmail attachment in Django, which is just generated by the same view. For generating the pdf, I use to try this tutorial link.
my views.py:
def submit_report(request, pk):
template = get_template('app/pdf_rprt.html')
Industry_obj = Industry.objects.get(id=pk)
Industry_Report_obj = Industry_obj.industry_report_set.all()
report_tableA_obj = report_tableA.objects.filter(industry_report__industry=Industry_obj)
context = {
'industry' : Industry_obj,
'Industry_Report' : Industry_Report_obj,
'report_tableA' : report_tableA_obj,
}
html = template.render(context)
pdf = render_to_pdf('app/pdf_rprt.html', context)
if pdf:
to = "kanchon2199#gmail.com"
email = EmailMultiAlternatives(
#subject =
"final report sending (beta)",
#content =
'hello, this is test report sending mail',
#from email
settings.EMAIL_HOST_USER,
#list of recipent
[to]
)
email.attach_file(pdf)
email.send()
return redirect('app:index')
here the render_to_pdf comes from a custom build function in utils.py:
def render_to_pdf(template_src, context_dict={}):
template = get_template(template_src)
html = template.render(context_dict)
result = BytesIO()
pdf = pisa.pisaDocument(BytesIO(html.encode("ISO-8859-1")), result)
if not pdf.err:
return HttpResponse(result.getvalue(), content_type='application/pdf')
return None
But it says error like (for the line email.attach_file(pdf)):
TypeError at /submit_report/1/
expected str, bytes or os.PathLike object, not HttpResponse
How can I fix it?
However, I have found a solution to this problem. the return type of render_to_pdf inside the utils.py should be ContentFile like this:
def render_to_pdf(template_src, context_dict={}):
template = get_template(template_src)
html = template.render(context_dict)
result = BytesIO()
pdf = pisa.pisaDocument(BytesIO(html.encode("ISO-8859-1")), result)
if not pdf.err:
return ContentFile(result.getvalue(), 'sampleReport.pdf')
return None
and inside views.py the email.attach_file() need dir path inside it, so we can use our database to save the pdf file and then attach it like this:
def submit_report(request, pk):
template = get_template('app/pdf_rprt.html')
context = {
'industry' : Industry_obj,
'Industry_Report' : Industry_Report_obj,
'report_tableA' : report_tableA_obj,
}
html = template.render(context)
pdf = render_to_pdf('app/pdf_rprt.html', context)
if pdf:
Final_report_obj = Final_report.objects.create(pdf=this_pdf)
Final_report_obj.save()
to = [gmail1#gmail.com, gmail2#gmail.com.....]
email = EmailMultiAlternatives(
"your Subject",
"your content.... ",
settings.EMAIL_HOST_USER,
to
)
email.attach_file(os.path.join(settings.MEDIA_ROOT, Final_report_obj.pdf.name))
email.send()
return redirect('app:index')
Here I use the Final_report_obj named model just to store my pdf file, we can use another type of model also.
I've been unable resolve this issue on IRC, hoping I could find some guidance here. I have the following test:
def test_validation_errors_return_hops_list_page(self):
response = self.client.post(
'/beerdb/add/hops',
data={
'name': '',
'min_alpha_acid': '',
'max_alpha_acid': '',
'country': '',
'comments': ''
}, follow=True
)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'homebrewdatabase/addhops.html')
name_validation_error = escape("A hop name is required")
min_alpha_acid_error = escape("You must enter a min alpha acid")
max_alpha_acid_error = escape("You must enter a max alpha acid")
country_error = escape("You must enter a country")
comments_error = escape("You must enter a comment")
self.assertContains(response, name_validation_error)
self.assertContains(response, min_alpha_acid_error)
self.assertContains(response, max_alpha_acid_error)
self.assertContains(response,country_error)
self.assertContains(response, comments_error)
It's failing on self.assertContains(response, name_validation_error). Here's the trace back:
Failure
Traceback (most recent call last):
File "/Users/USER/workspace/PycharmProjects/hashtagbrews/homebrewdatabase/tests/test_views.py", line 189, in test_validation_errors_return_hops_list_page
self.assertContains(response, name_validation_error)
File "/Users/USER/workspace/Envs/hashtagbrews/lib/python3.4/site-packages/django/test/testcases.py", line 398, in assertContains
msg_prefix + "Couldn't find %s in response" % text_repr)
AssertionError: False is not true : Couldn't find 'A hop name is required' in response
My view in views.py renders the hops.html template with errors when the form is invalid:
def addhops(request):
add_form = HopForm(request.POST or None)
if request.method == 'POST':
if add_form.is_valid():
Hop.objects.create(name=request.POST['name'],
min_alpha_acid=request.POST['min_alpha_acid'],
max_alpha_acid=request.POST['max_alpha_acid'],
country=request.POST['country'],
comments=request.POST['comments']
)
return redirect('hops_list')
else:
hops_list = Hop.objects.all()
return render(request, 'homebrewdatabase/hops.html', {'hops': hops_list, 'form': add_form})
return render(request, 'homebrewdatabase/addhops.html', {'form': add_form})
When I manually click through the site, I get exactly what I'm looking for: a redirect from the modal to the main hops page list with a Bootstrap alert box at the top containing an unordered list of errors from add_hops.errors. I've printed out the response after the post request (self.client.post('url', data={invalid data})) and it only contains the modal form. What would be the proper method to finish this test? Or do I need to rewrite my form validation?
The issue here, as identified in the comments, is that the Django test client runs a GET request on the addhops view method after executing the post request. According to the view logic, if the method isn't POST, then it returns the bootstrap modal, which does not contain the form errors by design. So the test will fail when using the test client. However, the test can be altered to, instead, use the HttpRequest object to send invalid data in a POST request and then assert that the content contains the form errors. So I rewrote the test to use the following, which passes--
def test_validation_errors_return_hops_list_page(self):
request = HttpRequest()
request.method = 'POST'
request.POST['name'] = ''
request.POST['min_alpha_acid'] = ''
request.POST['max_alpha_acid'] = ''
request.POST['country'] = 'USA'
request.POST['comments'] = ''
response = addhops(request)
self.assertEqual(response.status_code, 200)
name_validation_error = escape("A hop name is required")
min_alpha_acid_error = escape("You must enter a min alpha acid")
max_alpha_acid_error = escape("You must enter a max alpha acid")
comments_error = escape("You must enter a comment")
self.assertContains(response, name_validation_error)
self.assertContains(response, min_alpha_acid_error)
self.assertContains(response, max_alpha_acid_error)
self.assertContains(response, comments_error)
I cannot assert which template was used as assertTemplateUsed is a method that belongs to the test client, but my functional tests with selenium should be enough to check that the required elements are in the rendered view. This may have to be changed in the future, but for now, it's enough to work with.
first of all i'm sorry because of my duplicated question but actually the other didn't work for me at all.
my problem is that I have 2 views which the first one is returning a Httpresponse to the 2nd, and what I want is to convert this Httpresponse to dictionary in the 2nd view and have access to it's elements.
here is the code :
1st view:
def base_tag_upload(request, tag):
error = False
upload_msg = "success"
user = request.user
response_data = {"error": error, "upload_msg": upload_msg, "file_id": None, "file_version": None}
if request.method == 'POST':
form = UploadForm(request.POST or None, request.FILES or None, tag=tag, user=user)
if form.is_valid():
cd = form.cleaned_data
uploaded_file = cd['file']
collection_name = cd['new_name'] or uploaded_file.name.split('.')[0].strip()
response_data.update(
{"uf_name": uploaded_file.name, "uf_size": uploaded_file.size, "uf_colname": collection_name})
set_primary = True # first file in collection
# Finding/Creating Related FileCollection
collection = FileCollection.objects.create(name=collection_name)
is_major = cd['version_type'] == 'mj'
file_obj = collection.upload_file(uploaded_file, set_primary, Major_version=is_major)
file_obj.author = user
collection.tags.add(tag)
collection.get_tag_permissions(tag, False)
file_obj.get_collection_permissions(collection, False)
set_user_ownership(collection, tag, user)
set_user_ownership(file_obj, tag, user)
collection.save()
file_obj.collection = collection
file_obj.save()
response_data.update({'file_id':file_obj.id, 'file_version': file_obj.version})
ActionLog.log(action=Action.objects.get(name="create"), target=file_obj,
user=user, request=request, details=None, extra_details=None)
redirect_url = reverse('file_history', kwargs={'collection_id': collection.id})
response_data.update({'redirect': redirect_url})
return HttpResponse(json.dumps([response_data]))
and the 2nd one :
def tag_upload(request, tag_id):
try:
tag = Tag.objects.get(id=tag_id)
except Tag.DoesNotExist:
return HttpResponse(simplejson.dumps([{'error': 'value_error', 'upload_msg': "no such folder"}]))
k = base_tag_upload(request, tag)
k.response.decode('utf-8')
print k
return base_tag_upload(request, tag)
but I got this error when I wanted to decode the Httpresponse as shown above :
AttributeError: 'HttpResponse' object has no attribute 'response'
I have 2 views which the first one is returning a Httpresponse to the 2nd
Then you didn't structured your code properly - a view has no business calling another view not messing with the response.
If you need to share some common code between two views, then extract this code in a distinct function (that would in this case return a plain dict) and call this function from both your views.
It's k.content.decode('utf-8').
I have these methods:
def get_all_from_database():
urls = Url.objects.all()
ips = Ip.objects.all()
context = {
'urls': serializers.serialize('json', urls),
'ip': serializers.serialize('json', ips)
}
return context
and the method that sends data to using ajax:
def send_results(request):
if request.is_ajax():
address = request.POST.get('url')
process_data(address, email_to, email_from)
context = get_all_from_database()
return HttpResponse(json.dumps(context), content_type='application/json')
But this raises error : INTERNAL SERVER ERROR 500 'dict' object has no attribute '_meta'.
Wheres the mistake, and how to correct it ?
You cant use serializers.serialize method with dict list that you got from values call:
urls = Url.objects.all().values('address', 'cnt')
Use default queryset:
urls = Url.objects.all()
ips = Ip.objects.all()
In you example context['urls'] value already in json format, and you cant use json.dumps() for json data.
You can use this example:
json.dumps({
'urls': Urls.objects.all().values_list('address', 'cnt'),
'ips': Ip.objects.all().values_list('address', 'cnt')
}), 'json')
urls = Url.objects.all().values('address', 'cnt')
ips = Ip.objects.all().values('address', 'cnt')
The above lines returns dict objects, try:
urls = Url.objects.all().values('address', 'cnt').values_list()
ips = Ip.objects.all().values('address', 'cnt').values_list()
Then you will have urls as a list containing the tuples:
[(address_1, cnt_1), (address_2, cnt_2), ...]
see: QuerySet API reference
I think it should be :
res=json.dumps({
'urls': list(Urls.objects.all().values_list('address', 'cnt')),
'ips': list(Ip.objects.all().values_list('address', 'cnt'))
}), 'json')
return HttpResponse(res,content_type="application/json")