Skip to content

Instantly share code, notes, and snippets.

@sveetch
Last active June 30, 2019 00:11
Show Gist options
  • Save sveetch/8cd738a6f5ddc638547fbfc8f6fc5b16 to your computer and use it in GitHub Desktop.
Save sveetch/8cd738a6f5ddc638547fbfc8f6fc5b16 to your computer and use it in GitHub Desktop.
Smart templatetag with translation and context driven from settings (save from an attempt on Richie)
"""
i18n utilities for our search app
"""
from django.conf import settings
def get_best_field_language(multilingual_field, best_language):
"""
Pick the best available language from a multilingual field.
A multilingual field is eg:
'title': {
'es': 'mi título',
'fr': 'mon titre',
}
1. Use the most appropriate language as determined by the consumer
2. Default to language #0, then #1, then #2, etc. in settings.LANGUAGES
"""
for language in [best_language] + [lang for lang, _ in settings.LANGUAGES]:
try:
return multilingual_field[language]
except KeyError:
pass
# Available social networks badges with parameters
SOCIAL_NETWORKS_BADGES = [
{
"name": "facebook-page",
"template": "social-networks/facebook.html",
"context": {
"title": {
"fr": "Page Facebook",
"en": "Facebook page",
},
"url": {
"en": "https://www.facebook.com/france.universite.numerique",
},
},
},
{
"name": "facebook-share",
"template": "social-networks/facebook.html",
"context": {
"title": {
"fr": "Partager sur Facebook",
"en": "Share on Facebook",
},
"url": {
"en": "http://www.facebook.com/share.php?u={{page_url}}",
},
},
},
{
"name": "mailto-course",
"template": "social-networks/mailto.html",
"context": {
"title": {
"fr": "Partager par courriel",
"en": "Share with email",
},
"subject": {
"fr": "Suivez un cours en ligne avec FUN",
},
"body": {
"fr": "Je viens de m'inscrire pour {{page_title}} via FUN {{page_url}}",
},
"url": {
"en": "mailto:?subject={{subject}}&body={{body}}",
},
},
},
{
"name": "mailto-blogpost",
"template": "social-networks/mailto.html",
"context": {
"title": {
"fr": "Partager par courriel",
"en": "Share with email",
},
"subject": {
"fr": "Actualité FUN: {{page_title}}",
},
"body": {
"fr": "{{page_url}}",
},
"url": {
"en": "mailto:?subject={{subject}}&body={{body}}",
},
},
},
{
"name": "twitter-blogpost",
"template": "social-networks/twitter.html",
"context": {
"title": {
"fr": "Partager sur Twitter",
"en": "Share on Twitter",
},
"body": {
"fr": "{{page_url}}",
},
"url": {
"en": "https://twitter.com/intent/tweet?text={{body}}",
},
},
},
{
"name": "twitter-course",
"template": "social-networks/twitter.html",
"context": {
"title": {
"fr": "Partager sur Twitter",
"en": "Share on Twitter",
},
"body": {
"fr": "Je viens de m'inscrire pour {{page_title}} via FUN {{page_url}}",
},
"url": {
"en": "https://twitter.com/intent/tweet?text={{body}}",
},
},
},
]
"""
Include badges one by one: ::
{% badges "fb-share" "twitter-intent" "mailto" %}
Use shortcut badge pack: ::
{% badges_pack "course" %}
{% badges_pack "blogpost" %}
* Badge config comes from settings.SOCIAL_NETWORKS_BADGES;
* Badge pack config comes from settings.SOCIAL_NETWORKS_PACKS;
* Each badge config has a context to give within String.format() for each text (subject, url, body);
* Config context is augmented with page_url and page_title;
* Each text should be urlencoded ? (twitter seems to require it but not other sharing methods?);
* Use "apps.search.utils.i18n.get_best_field_language" to find accurate text from current language;
"""
from django.conf import settings
from django import template
from django.template.defaultfilters import stringfilter
from django.template.loader import render_to_string
from django.utils.html import format_html
from django.utils.http import urlencode
from richie.apps.search.utils.i18n import get_best_field_language
register = template.Library()
class BadgeRenderer(object):
"""
Badge HTML renderer
Arguments:
lang (str): Language code to use for text translation selection.
Keyword Arguments:
page (cms.models.pagemodel.Page): Optional page object to get title
and url that will be added to template context.
"""
FORMATTABLE_ITEMS = ['url', 'content']
def __init__(self, lang, page=None):
self.page = page
self.lang = lang
def get_context(self, config_name, initial_context):
"""
Transform context so if an item is a dict it it passed to
"get_best_field_language"
"""
context = {
"badge_name": config_name,
}
if self.page:
context.update({
"page_title": self.page.get_title(language=self.lang),
"page_url": self.page.get_absolute_url(language=self.lang),
})
# Get context items with possible translations
for k,v in initial_context.items():
# Everything but dictionnary is just copied
if not isinstance(v, dict):
context[k] = template.Template(v).render(template.Context(context))
# Dictionnary is assumed to be a dict of translations, only the
# translation with the best available language is keeped
else:
value = get_best_field_language(v, self.lang)
if value is None:
msg = ("Social network badge '{config}' item '{item}' "
"have no translation for available language from "
"'settings.LANGUAGES'")
raise KeyError(msg.format(config=config_name, item=k))
context[k] = template.Template(value).render(template.Context(context))
# Second pass to augment formattable items with context
for item in self.FORMATTABLE_ITEMS:
if item in context:
context[item] = template.Template(context[item]).render(template.Context(context))
return context
def render(self, config):
"""
Render HTML from template and config
"""
config_name = config["name"]
template = config["template"]
context = self.get_context(config_name, config["context"])
return render_to_string(template, context)
@register.simple_tag(takes_context=True)
def badges(context, *args, **kwargs):
"""
Template tag to build badge HTML for each given badge name.
Badge name have to be a valid item ``name`` in
``settings.SOCIAL_NETWORKS_BADGES``, this item will be use for badge
configuration.
A badge configuration contains a ``template`` path item to render with
``context`` item. Badge context can contains translatable texts for
available languages from ``settings.LANGUAGES``
Example:
{% badges "fb-share" %}
OR
{% badges "fb-share" "twitter-intent" "mailto" %}
"""
badges = []
page = context.get("current_page", None)
lang = context.get("lang", None) or settings.LANGUAGE_CODE
renderer = BadgeRenderer(lang, page=page)
for name in args:
try:
config = [item for item in settings.SOCIAL_NETWORKS_BADGES if item.get("name")==name][0]
except IndexError:
raise IndexError(f"settings.SOCIAL_NETWORKS_BADGES has no item with name: {name}")
badges.append(renderer.render(config))
# TODO: return safe HTML
return "".join(badges)
"""
Unit tests for the PagePlaceholder template tag.
"""
from django.db import transaction
from django.test import RequestFactory
from cms.api import create_page, create_title
from cms.test_utils.testcases import CMSTestCase
from richie.apps.core.factories import UserFactory
from richie.plugins.simple_text_ckeditor.cms_plugins import CKEditorPlugin
from richie.apps.core.templatetags.social_networks import BadgeRenderer
class BadgeRendererTestCase(CMSTestCase):
"""
Unit test suite to validate the behavior of BadgeRenderer class.
"""
@transaction.atomic
def test_badgerenderer_get_context_nopage(self):
"""
Context should should not be troubled if no page was given as argument
"""
renderer = BadgeRenderer(lang=None)
self.assertEqual(renderer.get_context("dummy-badge", {}), {
"badge_name": "dummy-badge",
})
@transaction.atomic
def test_badgerenderer_get_context_withpage(self):
"""
Context should have related page variables when page is given as argument
"""
user = UserFactory()
page = create_page("Test", "richie/fullwidth.html", "en", published=True)
renderer = BadgeRenderer(lang="en", page=page)
self.assertEqual(renderer.get_context("dummy-badge", {}), {
"badge_name": "dummy-badge",
"page_title": "Test",
'page_url': '/en/test/',
})
@transaction.atomic
def test_badgerenderer_get_context_formattable(self):
"""
Only formattable items should correctly include any other context items
"""
user = UserFactory()
page = create_page("Test", "richie/fullwidth.html", "en", published=True)
renderer = BadgeRenderer(lang="en", page=page)
initial_context = {
"foo": "bar",
"ping": "pong {{page_title}}",
"url": "pong {{foo}} {{page_url}}",
"content": "pong {{page_title}} {{foo}}",
}
self.assertEqual(renderer.get_context("dummy-badge", initial_context), {
"badge_name": "dummy-badge",
"page_title": "Test",
'page_url': '/en/test/',
"foo": "bar",
"ping": "pong Test",
"url": "pong bar /en/test/",
"content": "pong Test bar",
})
@transaction.atomic
def test_badgerenderer_get_context_i18n_withpage(self):
"""
Context should be filled with the right variables from page with given
language
"""
user = UserFactory()
page = create_page(
language="en",
menu_title="A test",
title="A test",
slug="atest",
template="richie/fullwidth.html",
published=True
)
create_title(
page=page,
language="fr",
menu_title="Un test",
title="Un test",
slug="untest",
)
page.publish("fr")
renderer = BadgeRenderer(lang="fr", page=page)
self.assertEqual(renderer.get_context("dummy-badge", {}), {
"badge_name": "dummy-badge",
"page_title": "Un test",
'page_url': '/fr/untest/',
})
@transaction.atomic
def test_badgerenderer_get_context_i18n_no_page(self):
"""
When no page is provided to renderer, it works also.
"""
initial_context = {
"foo": {
"fr": "Mon test de badge",
"en": "My badge test",
},
"bar": {
"en": "Only english is available",
},
}
renderer = BadgeRenderer(lang="en")
self.assertEqual(renderer.get_context("dummy-badge", initial_context), {
"badge_name": "dummy-badge",
"foo": "My badge test",
"bar": "Only english is available",
})
@transaction.atomic
def test_badgerenderer_get_context_i18n_invalid_lang(self):
"""
When a text has no translation for any available languages from
``settings.LANGUAGES``, renderer should raise a relevant exception.
"""
initial_context = {
"foo": {
"fr": "Mon test de badge",
"en": "My badge test",
},
"bar": {
"es": "Mi case es su casa",
},
}
renderer = BadgeRenderer(lang="en")
with self.assertRaises(KeyError) as e:
renderer.get_context("dummy-badge", initial_context)
self.assertEqual(
str(e.exception),
('"Social network badge \'dummy-badge\' item \'bar\' have no '
'translation for available language from \'settings.LANGUAGES\'"')
)
@transaction.atomic
def test_badgerenderer_get_context_i18n_fr(self):
"""
Renderer should correctly use the best translation for given language.
"""
user = UserFactory()
# Create page in different language than the default one from
# settings.LANGUAGE_CODE
page = create_page(
language="fr",
menu_title="Un test",
title="Un test",
slug="untest",
template="richie/fullwidth.html",
published=True
)
create_title(
language="en",
menu_title="A test",
title="A test",
slug="atest",
page=page,
)
page.publish("en")
initial_context = {
"dummy": "A simple text without translation",
"foo": {
"fr": "Mon test de badge pour '{{page_title}}'",
"en": "My badge test for '{{page_title}}'",
},
"bar": {
"en": "Only english is available",
},
"ping": {
"fr": "Seulement du Français ici",
},
}
renderer = BadgeRenderer(lang="fr", page=page)
#response = self.client.get(page.get_absolute_url("fr"))
#print(response.context_data)
self.assertEqual(renderer.get_context("dummy-badge", initial_context), {
"badge_name": "dummy-badge",
"page_title": "Un test",
'page_url': '/fr/untest/',
"dummy": "A simple text without translation",
"foo": "Mon test de badge pour 'Un test'",
"bar": "Only english is available",
"ping": "Seulement du Français ici",
})
class BadgesTemplateTagsTestCase(CMSTestCase):
"""
Unit test suite to validate the behavior of the "badges" template tag.
"""
@transaction.atomic
def test_templatetags_badges_no_current_page(self):
"""
Templatetag should not be troubled when ``current_page`` variable does
not exists from template context, to ensure tag works out of CMS page
TODO: Will be finalized only when tag will render HTML
"""
user = UserFactory()
page = create_page("Test", "richie/fullwidth.html", "en", published=True)
request = RequestFactory().get("/")
request.current_page = page
request.user = user
template = (
'{% load cms_tags social_networks %}{% badges "facebook-page" %}'
)
output = self.render_template_obj(template, {"page": page}, request)
self.assertEqual("", output)
@transaction.atomic
def test_templatetags_badges_invalid_config_name(self):
"""
Tag should raise a specific error when given config name does not exists
"""
user = UserFactory()
page = create_page("Test", "richie/fullwidth.html", "en", published=True)
request = RequestFactory().get("/")
request.current_page = page
request.user = user
template = (
'{% load cms_tags social_networks %}{% badges "nope" %}'
)
with self.assertRaises(IndexError) as e:
output = self.render_template_obj(template, {"page": page}, request)
self.assertEqual(str(e.exception), "settings.SOCIAL_NETWORKS_BADGES has no item with name: nope")
@transaction.atomic
def test_templatetags_badges_i18n(self):
"""
Ensure template tag get the right language code from context
TODO: Will be finalized only when tag will render HTML
"""
user = UserFactory()
page = create_page(
language="fr",
menu_title="Un test",
title="Un test",
slug="untest",
template="richie/fullwidth.html",
published=True
)
create_title(
language="en",
menu_title="A test",
title="A test",
slug="atest",
page=page,
)
page.publish("en")
request = RequestFactory().get(page.get_absolute_url("fr"))
request.current_page = page
request.user = user
template = (
'{% load cms_tags social_networks %}{% badges "facebook-page" %}'
)
output = self.render_template_obj(template, {"page": page}, request)
print(output)
self.assertEqual("", output)
{% spaceless %}<a href="{{ url }}" target="_blank" class="badge badge--{{ name }}">
<span class="icon-facebook"></span>
</a>{% endspaceless %}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment