Pass a lazy translation string including variable to function in Django - django

Inside a Django view, I create a subject like that:
subject = _(u"%(user)s has posted a comment") % { 'user': user }
Then I pass this subject to a function, which handles email notifications:
send_notifications(request, subject, url)
In send_notifications, I iterate over all subscriptions and send emails. However, each user can have a different language, so I activate the user's language dynamically via Django's activate:
def send_notifications(request, subject, url):
from django.utils.translation import activate
for s in Subscription.objects.filter(url=url):
activate(s.user.userprofile.lang)
send_mail(subject, render_to_string('notification_email.txt', locals()), settings.SERVER_EMAIL, [s.user.email])
The template gets rendered in the correct language of each user. However, the subject is passed as an evaluated and translated string to send_notifications and thus, is not translated.
I played around with lazy translations and lambda functions as parameters, but without success. Any help appreciated :)

Instead of passing the translated subject, just pass it non translated:
subject = '%(user)s has posted a comment'
context = {'user': user}
def send_notifications(request, subject, url, context):
from django.utils.translation import activate
for s in Subscription.objects.filter(url=url):
activate(s.user.userprofile.lang)
send_mail(_(subject) % context, render_to_string('notification_email.txt', locals()), settings.SERVER_EMAIL, [s.user.email])
If you're not going to personalize the contents per user, then you might as well limit the number of renderings because that's a little confusing:
# also do your imports at the top to catch import issues early
from django.utils.translation import activate
from django.utils.translation import ugettext as _
def send_notifications(request, url,
translatable_subject, context,
body_template='notification_template.txt'):
previous_lang = None
for s in Subscription.objects.filter(url=url).order_by('user__userprofile__lang'):
if s.user.userprofile.lang != previous_lang:
activate(s.user.userprofile.lang)
subject = _(translatable_subject) % context
body = render_to_string(body_template, locals())
send_mail(subject, body, settings.SERVER_EMAIL, [s.user.email])
previous_lang = s.user.userprofile.lang
As such, it is much more obvious that you're not going to render emails per usage.
This slight rewrite should make you doubt about the original choice of a couple of names (locals, notification_template).
The above sample code is barely an "educated guess" and you should double check it and make sure you understand everything before you paste it.

Ok, found a solution myself. In case anybody runs into a similar problem:
from django.utils.translation import ugettext as _
# create subject as raw string in Django view
raw_subject = r"%(username)s has posted a comment"
# for the sake of generic variables, create a dictionary to pass to function
extra_context = { 'user': user }
# call function with raw string and dictionary as params
send_notifications(request, raw_subject, url, extra_context)
# translate the raw string inside send_notifications into the temporarily activated language
translated_subject = _(raw_subject) % extra_context
Appears to be working as desired :) Since we are working with several different notifications, I tried to avoid an extra template for each kind. However, calling a template with extra_context is also a possible solution.

I ended up creating a class that will handle the translation, passing the context of the string to this class.
from django.utils.translation import gettext_lazy as _
class Translatable(object):
def __init__(self, text, context):
self.text = text
self.context = context
def __str__(self):
return _(self.text).format(**self.context)
In your function, just call str(subject):
def send_notifications(request, subject, url):
from django.utils.translation import activate
for s in Subscription.objects.filter(url=url):
activate(s.user.userprofile.lang)
send_mail(str(subject), render_to_string('notification_email.txt', locals()), settings.SERVER_EMAIL, [s.user.email])
And passes subject to your function as following:
subject = Translatable(_("{user} has posted a comment"), context={'user': user})
send_notifications(request, subject, url)

Related

Publish a custom Django Flatpage at a set date and time

I have a custom Flatpage model:
from django.contrib.flatpages.models import FlatPage
class MyFlatPage(FlatPage):
publish = models.DateTimeField()
so that I can add a publish date in the future.
Now, I don't have a proper list of flatpages on the front end, my use for frontpages is more like 'one-offs', where I specific the URL and all that. For example, 'about', '2019prize', 'Today's walk', stuff like that.
The urls.py is set up to catch all the flatpages with:
from django.contrib.flatpages import views
re_path(r'^(?P<url>.*/)$', views.flatpage)
How can I set these pages I create to be displayed only after the publish date has arrived? I know that I can filter them by looking up something like pages.filter(publish__lte=now). Where and how should I put that code though?
Additional information
I suppose I need to create a custom view, is that correct? The original view is in ../lib/python3.8/site-packages/django/contrib/flatpages/views.py:
def flatpage(request, url)
if not url.startswith('/'):
url = '/' + url
site_id = get_current_site(request).id
try:
f = get_object_or_404(FlatPage, url=url, sites=site_id)
except Http404:
if not url.endswith('/') and settings.APPEND_SLASH:
url += '/'
f = get_object_or_404(FlatPage, url=url, sites=site_id)
return HttpResponsePermanentRedirect('%s/' % request.path)
else:
raise
return render_flatpage(request, f)
#csrf_protect
def render_flatpage(request, f):
if f.registration_required and not request.user.is_authenticated:
from django.contrib.auth.views import redirect_to_login
return redirect_to_login(request.path)
if f.template_name:
template = loader.select_template((f.template_name, DEFAULT_TEMPLATE))
else:
template = loader.get_template(DEFAULT_TEMPLATE)
f.title = mark_safe(f.title)
f.content = mark_safe(f.content)
return HttpResponse(template.render({'flatpage': f}, request))
How can I extend this, adding my if publish__lte=now code?
What I did is copy-paste the view code from ../lib/python3.8/site-packages/django/contrib/flatpages/views.py to my app.views, rename the two functions, and add the following to render_myflatpage:
def render_myflatpage(request, f):
[...]
if f.publish > now:
f.content = 'This content will be published on ' + str(f.publish)
I then assigned the new view in the catch-all urls.py code:
re_path(r'^(?P<url>.*/)$', myflatpage)
I know this goes against the DRY protocol; this works for me for the time being. If there's a more elegant solution please do let me know.

Django how to mark_safe a link but not the arguments in a gettext translation

Let say we have a message like this :
messages.add_message(request, messages.SUCCESS,
_('Document %(doc_type_name)s %(name)s (%(fname)s) created.') % {
'doc_type_name': conditional_escape(document.doc_type.name),
'name': conditional_escape(document.title),
'fname': conditional_escape(document.name),
'url': document.get_absolute_url()
})
Here it will work only if we display the message with {{ message|safe }} but we don't want that since if there is some code in %(name) it will be executed too.
If I use:
messages.add_message(request, messages.SUCCESS,
mark_safe(_('Document %(doc_type_name)s %(name)s (%(fname)s) created.') % {
'doc_type_name': conditional_escape(document.doc_type.name),
'name': conditional_escape(document.title),
'fname': conditional_escape(document.name),
'url': document.get_absolute_url()
}))
The mark_safe doesn't work.
I read a solution over there : https://stackoverflow.com/a/12600388/186202
But it is the reverse that I need here:
_('Document %s created.') % mark_safe('')
And as soon as it goes through the ugettext function it is not safe anymore.
How should I do?
You are trying to mix view and logic by placing HTML inside Python code. Well, sometimes you just have to do this but it is not the case.
mark_safe() returns SafeString object which is treated by Django templates specially. If SafeString evaluated by ugettext or % you will get string again, it is an expected behaviour. You can not mark safe only formatting string, either complete output with doc name/title etc or everything is not safe. Ie, it will not work this way.
You can put HTML into template and use render_to_string(), and probably it is the best option.
Are document title, name and doc_type.name set by user? If not, you can skip mark_safe and document using HTML in document properties as feature.
Previous response are correct: you should avoid mixing python and html as much as possible.
To solve your issue:
from django.utils import six # Python 3 compatibility
from django.utils.functional import lazy
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
mark_safe_lazy = lazy(mark_safe, six.text_type)
then:
lazy_string = mark_safe_lazy(_("<p>My <strong>string!</strong></p>"))
Found in django documentation:
https://docs.djangoproject.com/en/1.9/topics/i18n/translation/#s-other-uses-of-lazy-in-delayed-translations

Django: restrict access to a view, dependent upon referring url

I'm making a school records webapp. I want staff users to be able to view the user data pages for any pupil by going to the correct url, but without allowing pupils access to each others' pages. However I'm using the same view function for both urls.
I have a working #user_is_staff decorator based on the existence of a user.staff object. Pupil users have a user.pupil object instead. These are discrete, naturally, as no user can have both a .staff and a .pupil entry.
urls.py
(r'^home/(?P<subject>[^/]+)/$', 'myproject.myapp.views.display_pupil')
(r'^admin/user/(?P<user>\d+)/(+P<subject>[^/]+)/$', 'myproject.myapp.views.display_pupil')
views.py
#login_required
def display_pupil(request, subject, pupil=None):
if pupil:
try:
thepupil = get_object_or_404(Pupil, id = pupil, cohort__school = request.user.staff.school)
except Staff.DoesNotExist:
return HttpResponseForbidden()
else:
thepupil = request.user.pupil
thesubject = get_object_or_404(Subject, shortname = subject)
# do lots more stuff here
return render_to_response('pupilpage.html', locals(), context_instance=RequestContext(request))
Doing it this way works, but feels very hacky, particularly as my '#user_is_staff' decorator has a more elegant redirect to a login page than the 403 error here.
What I don't know is how to apply the #user_is_staff decorator to the function only when it has been accessed with the pupil kwarg. There's a lot more code in the real view function, so I don't want to write a second one as that would be severely non-DRY.
Sounds like you want two separate views - one for a specific pupil and one for the current user - and a utility function containing the shared logic.
#login_required:
def display_current_pupil(request, subject):
thepupil = request.user.pupil
return display_pupil_info(request, subject, thepupil)
#user_is_staff
def display_pupil(request, subject, pupil):
thepupil = get_object_or_404(Pupil, id=pupil, cohort__school=request.user.staff.school)
return display_pupil_info(request, subject, thepupil)
def display_pupil_info(request, subject, thepupil):
thesubject = get_object_or_404(Subject, shortname=subject)
# do lots more stuff here
return render_to_response('pupilpage.html', locals(), context_instance=RequestContext(request))

Get a list of objects in tastypie (in another view)

I'm trying to get a tastypie response to use in another view. I've seen the recipe in the cookbook. Problem is, I'd like to get the list view. In my case, /api/v1/source/. Here's what I've got so far:
sr = SourceResource()
objs = sr.get_object_list(request) # two objects returned
bun = sr.build_bundle(data=objs, request=request)
jsondata = sr.serialize(None, sr.full_dehydrate(bun), 'application/json')
Of course this all falls apart. bun.data doesn't have the required characteristics (a single object). So, has anyone done this successfully? How is it done?
Here's what I've come up with. I don't especially like that both the request and the QueryDict are copied, but I can't think of anything else at the moment, other than copying big portions of the tastypie code.
from copy import copy
from django.views.generic import TemplateView
from incremental.sources.resources import SourceResource
resource = SourceResource()
class AppView(TemplateView):
'Base view for the Source parts of the app'
template_name = 'sources/base.html'
def get_context_data(self, **data):
'get context data'
tmp_r = copy(self.request)
tmp_r.GET = tmp_r.GET.copy()
tmp_r.GET['format'] = 'json'
data.update({
'seed': resource.get_list(tmp_r).content
})
return data
In order to avoid the request copying stuff, you can set json as the default format, for instance in your Resource you can overload the following method:
SourceResource(Resource):
def determine_format(self, request):
return "application/json"

Django: switch language of message sent from admin panel

I have a model, Order, that has an action in the admin panel that lets an admin send information about the order to certain persons listed that order. Each person has language set and that is the language the message is supposed to be sent in.
A short version of what I'm using:
from django.utils.translation import ugettext as _
from django.core.mail import EmailMessage
lang = method_that_gets_customer_language()
body = _("Dear mister X, here is the information you requested\n")
body += some_order_information
subject = _("Order information")
email = EmailMessage(subject, body, 'customer#example.org', ['admin#example.org'])
email.send()
The customer information about the language he uses is available in lang. The default language is en-us, the translations are in french (fr) and german (de).
Is there a way to use the translation for the language specified in lang for body and subject then switch back to en-us? For example: lang is 'de'. The subject and body should get the strings specified in the 'de' translation files.
edit:
Found a solution.
from django.utils import translation
from django.utils.translation import ugettext as _
body = "Some text in English"
translation.activate('de')
print "%s" % _(body)
translation.activate('en')
What this does it take the body variable, translates it to German, prints it then returns the language to English.
Something like
body = _("Some text in English")
translation.activate('de')
print "%s" % body
prints the text in English though.
If you're using Python 2.6 (or Python 2.5 after importing with_statement from __future__) you can use the following context manager for convenience.
from contextlib import contextmanager
from django.utils import translation
#contextmanager
def language(lang):
if lang and translation.check_for_language(lang):
old_lang = translation.get_language()
translation.activate(lang)
try:
yield
finally:
if lang:
translation.activate(old_lang)
Example of usage:
message = _('English text')
with language('fr'):
print unicode(message)
This has the benefit of being safe in case something throws an exception, as well as restoring the thread's old language instead of the Django default.
Not sure if activating/deactivating translation is proper way to solve that problem(?)
If I were facing that problem I would try to build some model for storing subjects/body/language/type fields. Some code draft:
class ClientMessageTemplate(models.Model):
language = model.CharField(choices=AVAIALBLE_LANGUAGES,...)
subject = models.CharField(...)
body = models.CharField(...)
type = models.CharField(choices=AVAILABLE_MESSAGE_TYPES)
Then you can retreive easily ClientMessageTemplate you need base on type and client's language.
Advantage of this solution is that you can have all data maintainable via admin interface and do not need to recompile message files each time something changed.