I'm using django-post_office to send emails to users. Post office uses templates stored in the database in the EmailTemplate model. I'd prefer to use file-based templates so I can keep them under version control.
An Email is created and the template is rendered in mail.py:
def create(sender, recipients=None, cc=None, bcc=None, subject='', message='',
html_message='', context=None, scheduled_time=None, headers=None,
template=None, priority=None, render_on_delivery=False,
commit=True):
...
if template:
subject = template.subject
message = template.content
html_message = template.html_content
if context:
_context = Context(context)
subject = Template(subject).render(_context)
message = Template(message).render(_context)
html_message = Template(html_message).render(_context)
Can anyone suggest a nice way to override this behavior? I was thinking I'd like to be able to pass a string with the template location, and render based on this (some context variables), but any input would be appreciated.
One option is to render the email template before calling mail.send() along with any extra context variables.
from django.template import Context, loader
template = loader.get_template(template_name)
context = Context(extra_context)
html_message = template.render(context)
Then instead of passing a template:
mail.send(template=template)
pass rendered html with the html_message argument:
mail.send(html_message=html_message)
I don't think that is very simple, as when we see the code which creates and sends an email, I see that the mail.send(...) calls internally mail.create(...)
Now here is that code:
def send(recipients=None, sender=None, template=None, context=None, subject='',
message='', html_message='', scheduled_time=None, headers=None,
priority=None, attachments=None, render_on_delivery=False,
log_level=None, commit=True, cc=None, bcc=None):
#-------------
#code sections
#-------------
if template:
if subject:
raise ValueError('You can\'t specify both "template" and "subject" arguments')
if message:
raise ValueError('You can\'t specify both "template" and "message" arguments')
if html_message:
raise ValueError('You can\'t specify both "template" and "html_message" arguments')
# template can be an EmailTemplate instance or name
if isinstance(template, EmailTemplate):
template = template
else:
template = get_email_template(template)
email = create(sender, recipients, cc, bcc, subject, message, html_message,
context, scheduled_time, headers, template, priority,
render_on_delivery, commit=commit)
The interesting piece of code is:
# template can be an EmailTemplate instance or name
if isinstance(template, EmailTemplate):
template = template
else:
template = get_email_template(template)
The get_email_template gets the template using either the EmailTemplate or name of the EmailTemplate which is during cache creation.
Even if you do not pass the object of EmailTemplate (in their case the model), they have just two cases:
Either get the data from the cache (which is again stored for first access from DB, since template wont change)
Get directly from DB
Either way they use DB and have no interface for using from a file.
I would suggest something like this:
Create a json file that stores your template in the format that suffices the EmailTemplate object creation. Here are the properties of EmailTemplate model class:
name = models.CharField(max_length=255, help_text=("e.g: 'welcome_email'"))
description = models.TextField(blank=True, help_text='Description of this template.')
subject = models.CharField(max_length=255, blank=True, validators=[validate_template_syntax])
content = models.TextField(blank=True, validators=[validate_template_syntax])
html_content = models.TextField(blank=True, validators=[validate_template_syntax])
created = models.DateTimeField(auto_now_add=True)
last_updated = models.DateTimeField(auto_now=True)
Read that json file in python using file read and convert the read string to python object by json dump or something (i guess you can figure that out)
Create the EmailTemplate object using the value you need and assign it to template in mail.send(...
Update:
If you use html_message, you still have maintain separate subject parameter, which can be avoided if you use template. So if you use json file, in your version control, from the subject,content,name and html_message everything will be stored in one file which can be handled in the file source control and recreate the template object and not worry about segregated values in the files to maintain.
Related
Maybe I am not asking the right question in the search area, but I can't find an answer for this. I am pretty sure that many people have this use case, but as a beginner in Django + Python, I need to ask it.
I have user that fills up a form and the data is stored in the database. Basically this form asks for an access to a Database and after the form is submitted I want my program to send an email to the user's manager and to the DBA to APPROVE or DENY it. Very simple, right?
My idea is that in this e-mail I send two URL's, one for approving and one for denying the request. When the URL the is clicked I send a response to the server with an update in the manager_approval field.
Has anyone implemented this solution, or could point me to something that could help me?
I am doing everything using Django + Python.
Regards,
Marcos Freccia
Basically this technique used in email verification. This is where you should look into.
Let's say you have model, named request, which has field like username to identify the person who requested access, database name, well, everything. But it will also have two "password-like" fields which will be used to determine if request was declined or not.
class Request(models.Model):
user = models.ForeignKey ...
databasename =
date =
...
access_granted = models.BooleanField(default=False)
deny_token = models.CharField()
allow_token = models.CharField()
The point is to generate those tokens on saving request in the View:
if request.method == POST:
form = RequestForm(request.POST)
if form.is_valid():
data['user'] = form.cleaned_data['user'])
data['databasename'] = form.cleaned_data['databasename'])
...
data['access_token'] = GENERATE_USING_HASH_FUNCTION()
data['deny_token'] = GENERATE_USING_HASH_FUNCTION()
form.save(data)
Then you can use module EmailMultiAlternatives to send html email like so:
subject, from_email, to = 'Request', 'admin#example.com', form.cleaned_data['manager_email']
html_content = render_to_string(HTML_TEMPLATE, CONTEXT) # Just as any regular templates
text_content = strip_tags(html_content)
msg = EmailMultiAlternatives(subject, text_content, from_email, [to], reply_to=["admin#example.com"])
msg.attach_alternative(html_content, "text/html")
msg.send()
And inside that template you construct reverse url:
{% url 'app:grant_access' allow_token=token %} # "token" you get from context
{% url 'app:deny_access' deny_token=token %} # will become example.com/deny_access/7ea3c95, where 7ea3c95 is token
Then add lines to urls.py of your app like that:
url(r'^allow_access/(?P<allow_token>[0-9]+)$', CheckAcessView.as_view(), name="app:grant_access"),
url(r'^deny_access/(?P<deny_token>[0-9]+)$', CheckAcessView.as_view(), name="app:deny_access"),]
Then create CheckAcessView view. Where you access request stored in your database and check if, for example, parameter of url "allow_token" is equal stored allow_token. If so, change request status to allowed.
I use html as message in one email and pass some variables like this:
subject = 'Some Subject'
plain = render_to_string('templates/email/message.txt',{'name':variableWithSomeValue,'email':otherVariable})
html = render_to_string('templates/email/message.html',{'name':variableWithSomeValue,'email':otherVariable})
from_email = setting.EMAIL_HOST_USER
send_email(subject, plain, from_email, [variableToEmail], fail_silently=False, html_message=html)
That works good but now I need to take the message content from one table from the database, the table have three columns, in the first register have this values in each column. Column subject have Account Info, column plain have Hello {{name}}. Now you can access to the site using this email address {{email}}. and the column html have <p>Hello <strong>{{name}}</strong>.</p> <p>Now you can access to the site using this email address <strong>email</strong>.</p>.
So to take the values from the database I do this obj = ModelTable.objects.get(id=1) then this:
subject = obj.subject
plain = (obj.plain,{'name':variableWithSomeValue,'email':otherVariable})
html = (obj.html,{'name':variableWithSomeValue,'email':otherVariable})
from_email = setting.EMAIL_HOST_USER
send_email(subject, plain, from_email, [variableToEmail], fail_silently=False, html_message=html)
But this give me the error
AttributeError: 'tuple' object has no attribute 'encode'
so I tried to passing .encode(´utf-8´) for the values and gives me the same error, then change the value for each variable and find that the problem comes from plain = (obj.plain,{'name':variableWithSomeValue,'email':otherVariable}) and html = (obj.html,{'name':variableWithSomeValue,'email':otherVariable}) so I think that I passing the variables in the wrong way, so How can I do it in the right way? or maybe is for the encoding of the database but I think that using .encode(utf-8) should solve that problem but I really think that I pass the variables name and email in the wrong way.
Sorry for the long post and my bad grammar, if need more info please let me know.
I'm assuming that obj.plain and obj.html are strings representing your templates (as stored in the database)?
If that is the case, then you still need to render your email content. However, instead of using render_to_string, which takes as it's first argument a template path, you will want to create a template based on your string, and then render that template. Consider something like the following:
...
from django.template import Context, Template
plain_template = Template(obj.plain)
context = Context({'name':variableWithSomeValue,'email':otherVariable})
email_context = plain_template.render(context)
...
send_email(...)
Here's a link that better explains rendering string templates, as opposed to rendering template files.
https://docs.djangoproject.com/en/1.7/ref/templates/api/#rendering-a-context
So i have implement the snippet hosted below:
https://djangosnippets.org/snippets/1303/
here is my code so far:
models.py
class Vehicle(models.Model):
pub_date = models.DateTimeField('Date Published', auto_now_add=True)
make = models.CharField(max_length=200)
model = models.CharField(max_length=200)
picture = models.FileField(upload_to='picture')
def __unicode__(self):
return self.title
def get_absolute_url(self):
return reverse('recipe_edit', kwargs={'pk': self.pk})
views.py
def vehicle_list(request, template_name='vehicle/vehicle_list.html'):
if request.POST:
form = VehicleForm(request.POST, request.FILES)
if form.is_valid():
form.save()
return redirect('vehicle_list')
else:
form = VehicleForm() # Create empty form
vehicles = Vehicle.objects.all() # Retrieve all vehicles from DB
return render(request, template_name, {
'vehicles': vehicles,
'form': form
},context_instance=RequestContext(request))
forms.py
class VehicleForm(forms.ModelForm):
class Meta:
model = Vehicle
def clean_picture(self):
content = self.cleaned_data['picture']
content_type = content.content_type.split('/')[0]
if content_type in settings.CONTENT_TYPES:
if content.size > settings.MAX_UPLOAD_SIZE:
raise forms.ValidationError('Please keep file size under %s', filesizeformat(content.size))
else:
raise forms.ValidationError('File type is not supported')
From what i understand, this approach can still easily overridden by modifying the header. What i am asking is, whether there is a better approach for this situation?
In order to verify that the given file content matches the given content type by the client you need a full fledged database which describes how the content type can be detected.
You can rely on the libmagic project instead though. There are bindings for this library available on the pypi: python-magic
You need to adjust your VehicleForm so that it does the content type detection:
class VehicleForm(forms.ModelForm):
class Meta(object):
model = Vehicle
def clean_picture(self):
content = self.cleaned_data['picture']
try:
content.open()
# read only a small chunk or a large file could nuke the server
file_content_type = magic.from_buffer(content.read(32768),
mime=True)
finally:
content.close()
client_content_root_type = content.content_type.split('/')[0]
file_content_root_type = file_content_type.split('/')[0]
if client_content_root_type in settings.CONTENT_TYPES and \
file_content_root_type in settings.CONTENT_TYPES:
if content.size > settings.MAX_UPLOAD_SIZE:
raise forms.ValidationError('Please keep file size under %s',
filesizeformat(content.size))
else:
raise forms.ValidationError('File type is not supported')
return content
This chunk of code was written to show how it works, not with reducing redundant code in mind.
I wouldn't recommend doing this by yourself in production Code. I would recommend using already present code. I would recommend the ImageField form field if you really only need to verify that an image has been uploaded and can be viewed. Please notice that the ImageField uses Pillow to ensure that the image can be opened. This might or might not pose a threat to your server.
There are also several other projects available which exactly implements the desired feature of ensuring that a file of a certain content type has been uploaded.
I need to validate the file type of the uploaded file and should allow only pdf, plain test and MS word files. Here is my model and and the form with validation function. But, I'm able to upload files even without the extension.
class Section(models.Model):
content = models.FileField(upload_to="documents")
class SectionForm(forms.ModelForm):
class Meta:
model = Section
FILE_EXT_WHITELIST = ['pdf','text','msword']
def clean_content(self):
content = self.cleaned_data['content']
if content:
file_type = content.content_type.split('/')[0]
print file_type
if len(content.name.split('.')) == 1:
raise forms.ValidationError("File type is not supported.")
if content.name.split('.')[-1] in self.FILE_EXT_WHITELIST:
return content
else:
raise forms.ValidationError("Only '.txt' and '.pdf' files are allowed.")
Here is the view,
def section_update(request, object_id):
section = models.Section.objects.get(pk=object_id)
if 'content' in request.FILES:
if request.FILES['content'].name.split('.')[-1] == "pdf":
content_file = ContentFile(request.FILES['content'].read())
content_type = "pdf"
section.content.save("test"+'.'+content_type , content_file)
section.save()
In my view, I'm just saving the file from the request.FILE. I thought while save() it'll call the clean_content and do content-type validation. I guess, the clean_content is not at all calling for validation.
You approach will not work: As an attacker, I could simply forge the HTML header to send you anything with the mime type text/plain.
The correct solution is to use a tool like file(1) on Unix to examine the content of the file to determine what it is. Note that there is no good way to know whether something is really plain text. If the file is saved in 16 bit Unicode, the "plain text" can even contain 0 bytes.
See this question for options how to do this: How to find the mime type of a file in python?
You can use python-magic
import magic
magic.from_file('/my/file.jpg', mime=True)
# image/jpeg
This is an old question, but for later users main question as mentioned in comments is why field validation not happens, and as described in django documentation field validation execute when you call is_valid(). So must use something sa bellow in view to activate field validation:
section = models.Section.objects.get(pk=object_id)
if request.method == 'POST':
form = SectionForm(request.POST, request.FILES)
if form.is_valid:
do_something_with_form
Form validation happens when the data is cleaned. If you want to customize this process, there are various places to make changes, each one serving a different purpose. Three types of cleaning methods are run during form processing. These are normally executed when you call the is_valid() method on a form
I know how to get it in views.py....
request.META['REMOTE_ADDR']
However, how do I get it in models.py when one of my forms is being validateD?
You can pass the request object to the form/model code that is being called: this will then provide access to request.META['REMOTE_ADDR']. Alternatively, just pass that in.
Ona possible way, but i am not sure if it is the best or not...
define your own clean method,
class someForm(forms.Form):
afield = CharField()
def clean(self, **kwargs):
cleaned_data = self.cleaned_data
afield = cleaned_data.get('afield')
if 'ip' in kwargs:
ip = kwargs['ip']
# ip check block, you migth use your cleaned data in here
return cleaned_data
some_info = {'afield':123} #you will wish to use post or gt form data instead, but tihs iis for example
form = someForm(some_info)
if form.is_valid():
data = form.clean({'ip':request.META['REMOTE_ADDR']}) # you pass a dict with kwargs, which wwill be used in custom clean method
If you are validating at form level or at model level, both instances know nothing about the HTTP request (where the client IP info is stored).
I can think of two options:
Validate at the view level where you can insert errors into the form error list.
You can put the user IP (may be encrypted) in a hidden field at your form.