"""Util functions to render a template.""" import datetime import math import re import pytz from htmlmin.minify import html_minify from jinja2 import Environment, FileSystemLoader, TemplateNotFound from notifications_delivery import config from notifications_delivery.constants.notifications import FORMAT_REPLACEMENTS, TEMPLATE_EXTENSIONS from notifications_delivery.utils.translations import get_translations env = Environment( loader=FileSystemLoader(config.TEMPLATE_PATH), extensions=['jinja2.ext.i18n', 'jinja2.ext.do', 'jinja2.ext.loopcontrols'] ) env.policies['ext.i18n.trimmed'] = True def _get_template(template_name: str): """Select a template based on template name and all available extensions.""" try: template = env.select_template([f'{template_name}{ext}' for ext in TEMPLATE_EXTENSIONS]) except TemplateNotFound: err = (f"Template '{template_name}' not found with " f"{' or '.join(TEMPLATE_EXTENSIONS)} extension") raise FileNotFoundError(err) return template def render_template(template_name: str, context: dict, user_locale: str) -> str: """Render a Jinja2 template. Args: template_name (str): the template name context (dict): the context to extend the template user_locale (str): the user's locale (e.g. "en") Returns: str: the rendered template """ translations = get_translations(user_locale) env.install_gettext_translations(translations) template = _get_template(template_name) return html_minify(template.render(**context)) def datetimeformat(value, date_format='%H:%M / %Y-%m-%d', has_ordinal=False): """Format date. Args: value (str): The date as a string representation. date_format (str): The format of the date. has_ordinal (bool): Flag that is used to produce dates with the day in ordindal format Returns: str: the formatted date. """ date = datetime.datetime.strptime(value, '%Y-%m-%d') if has_ordinal: formatted_date = date.strftime(date_format) formatted_date += ' {0}'.format(ordinal(date.day)) else: formatted_date = date.strftime(date_format) return formatted_date def ordinal(n): """Get the ordinal of a number. e.g. For number 1 the ordinal is 1st. Args: n (int): the integer that needs to be converted Returns str: with the input number transformed as ordinal """ return '%d%s' % ( n, 'tsnrhtdd'[(math.floor(n / 10) % 10 != 1) * (n % 10 < 4) * n % 10::4]) def format_number(value): """Format the number with specific fraction of digits. Args: value (int): The numeric value Returns: str: the formatted number. """ trillion = 1000000000000 billion = 1000000000 million = 1000000 thousand = 1000 fmt = '{0:.1f}' if value >= trillion: fraction_value = value / trillion value = fmt.format(fraction_value) return '{value}T'.format(value=value) if value >= billion: fraction_value = value / billion value = fmt.format(fraction_value) return '{value}B'.format(value=value) if value >= million: fraction_value = value / million value = fmt.format(fraction_value) return '{value}M'.format(value=value) if value >= thousand: fraction_value = value / thousand value = fmt.format(fraction_value) return '{value}K'.format(value=value) if value < thousand: floored_value = math.floor(value) return '{value}'.format(value=floored_value) return str(value) def replace_format(format_value): """Replace format with the commercial term. Args: format_value (str): The format_value Returns: str: with the replaced value """ if format_value in FORMAT_REPLACEMENTS: return FORMAT_REPLACEMENTS[format_value] else: return format_value def format_growth_percentage(value): """Format a float value to a growth percentage string. Args: value (float): the float value Returns: str: the growth percentage string """ if not value: return '-' growth = round(value) if growth > 0: return '+{0}%'.format(growth) else: return '{0}%'.format(growth) def format_country_code(code): """Format country code to a full name. Args: code (str): the country code Returns: str: formatted full country code """ if not code: return '' else: return pytz.country_names[code] def format_artist_names(values, max_names=3, max_length=None): """Format an array to a single string. Args: values (iterable): the array value max_names (int): the maximum number of names to format max_length (int): truncate string length to this many characters Returns: str: the artist names string """ if not values: return '' names = values[:max_names] formatted = ', '.join(names) result = formatted[:max_length] if ((max_names is not None and len(values) > max_names) or (max_length is not None and len(formatted) > max_length)): result = f'{result}...' return result def format_spike_playlist_artwork(url, store): """Format spike playlist url if needed. Args: url (str): the url value store (str): the store name Returns: str: the formatted url """ if not url: icon_cdn = 'https://cdn.theorchard.io/web-icons/' if store == 'spotify': return icon_cdn + 'empty_artwork_spotify.png' if store == 'apple music': return icon_cdn + 'empty_artwork_apple_music.png' return '' pattern = re.compile('{w}x{h}') if pattern.search(url): return url.replace('{w}x{h}', '50x50') else: return url def get_login_url(applications: list[dict]) -> str: """Get the login url from the applications list. Args: applications (list[dict]): the applications list Returns: str: the login url """ if not applications: return '' return applications[0].get('url', '') def get_name(i: dict) -> str: """Get the name from the identity. Args: i (dict): the identity dictionary Returns: str: the display name from the identity """ return i.get('name') or f'{i.get("first_name", "")} {i.get("last_name", "")}'.strip() or 'Admin' # Add filters to the template environment env.filters['datetimeformat'] = datetimeformat env.filters['format_number'] = format_number env.filters['replace_format'] = replace_format env.filters['format_growth_percentage'] = format_growth_percentage env.filters['format_artist_names'] = format_artist_names env.filters['format_country_code'] = format_country_code env.filters['format_spike_playlist_artwork'] = format_spike_playlist_artwork env.filters['get_name'] = get_name # Add functions to the template environment env.globals['get_login_url'] = get_login_url