uccser/cs-field-guide

View on GitHub
csfieldguide/utils/TranslatableModelLoader.py

Summary

Maintainability
B
4 hrs
Test Coverage
"""Module for TranslatableModelLoader abstract base class."""

from utils.BaseLoader import BaseLoader
from utils.language_utils import get_available_languages, get_default_language
from utils.errors.CouldNotFindYAMLFileError import CouldNotFindYAMLFileError
from utils.errors.CouldNotFindMarkdownFileError import CouldNotFindMarkdownFileError
from utils.errors.InvalidYAMLValueError import InvalidYAMLValueError
from utils.errors.MissingRequiredModelsError import MissingRequiredModelsError
from utils.errors.MissingRequiredFieldError import MissingRequiredFieldError
from django.utils import translation
from modeltranslation.utils import fallbacks


class TranslatableModelLoader(BaseLoader):
    """Abstract base class for loaders of translatable models."""

    def get_yaml_translations(self, filename, field_map=None, required_slugs=[], required_fields=[]):
        """Get a dictionary of translations for the given filename.

        Yaml files must be structured

            <object-slug-1>:
                <field-1>: <translated value for field-1>
                <field-2>: <translated value for field-2>
            <object-slug-2>
                ...

        where [object-slug-1, object-slug-2, ...] are slugs of objects of
        the same model (eg. ClassroomResource) and [field-1, field-2, ...]
        are the names of Char/TextFields defined on that model.

        Args:
            filename: (str) path to yaml file from the working directory of the loader
            field_map: (dict) optional mapping of field names in yaml file to
                field names in the resulting dictionary
            required_slugs: (list) list of slugs required to be present in the
                english strings file
            required_fields: (list) list of string fields required to be present
                for each model in the english strings file

        Returns:
            Dictonary of translations, structured as follows:
                {
                    <model-slug>: {
                        <language>: {
                            <field-name>:<value>
                        }
                    }
                }

        Raises:
            CouldNotFindYAMLFileError: the requested file could not be found in
                the /en directory, raised only if required_slugs is not empty.
            MissingRequiredModelsError: the file in the /en directory did not
                contain all slugs in required_slugs
            MissingRequiredFieldError: in the file in the /en directory, one
                of the models was missing an entry for a field in required_fields
            InvalidYAMLValueError: one of the 'translated strings' in the file
                was not a string.
        """
        translations = {}
        for language in get_available_languages():
            translations_filename = self.get_localised_file(language, filename)
            try:
                yaml = self.load_yaml_file(translations_filename)
            except CouldNotFindYAMLFileError:
                if required_slugs and language == get_default_language():
                    raise
                yaml = {}
            if language == get_default_language() and not set(required_slugs) <= set(yaml.keys()):
                raise MissingRequiredModelsError(
                    translations_filename,
                    set(required_slugs) - set(yaml.keys()),
                )

            for model_slug, model_fields in yaml.items():
                values_dict = {}
                for required_field in required_fields:
                    if language == get_default_language() and required_field not in model_fields:
                        raise MissingRequiredFieldError(
                            translations_filename,
                            [required_field],
                            "model ({})".format(model_slug)
                        )
                for field, value in model_fields.items():
                    if not isinstance(value, str):
                        raise InvalidYAMLValueError(
                            translations_filename,
                            "{}->{}".format(model_slug, field),
                            "String"
                        )
                    if field_map:
                        field = field_map.get(field, field)
                    values_dict[field] = value
                translations.setdefault(model_slug, dict())[language] = values_dict
        return translations

    def get_markdown_translations(self, filename, required=True, **kwargs):
        """Get dictionary of translations of the requested markdown file.

        Args:
            filename: (str) path to yaml file from the working directory of the loader
            required: (bool) raise an exception if the requested file is not present
                in the /en directory tree
            kwargs: (dict) kwargs passed through to convert_md_file

        Returns:
            dict mapping language codes to VertoResult objects

        Raises:
            CouldNotFindMarkdownFileError if the requested file could not be found
                in the /en directory tree
        """
        content_translations = {}
        for language in get_available_languages():
            try:
                content_translations[language] = self.convert_md_file(
                    self.get_localised_file(
                        language,
                        filename,
                    ),
                    self.structure_file_path,
                    **kwargs
                )
            except CouldNotFindMarkdownFileError:
                if required and language == get_default_language():
                    raise
        return content_translations

    @staticmethod
    def populate_translations(model, model_translations_dict):
        """Populate the translation fields of the given model.

        Args:
            model: TranslatableModel instance
            model_translations_dict: dictionary of form
                {
                    <language_code>: {
                        <field_name>:<value>,
                        <field_name>:<value>,...
                    },...
                }
        """
        for language, values_dict in model_translations_dict.items():
            with translation.override(language):
                for field, value in values_dict.items():
                    setattr(model, field, value)

    @staticmethod
    def mark_translation_availability(model, required_fields=[]):
        """Populate the available_languages field of a translatable model.

        Args:
            model: TranslatableModel instance
            required_fields: (list) list of field names that are required to be
                populated for the model to be marked available in that language.
                An empty list (the default value) will mark the model as available
                in every language.
        """
        available_languages = []
        for language in get_available_languages():
            with translation.override(language):
                with fallbacks(False):
                    if all(
                        [getattr(model, field) for field in required_fields]
                    ):
                        available_languages.append(language)
        model.languages = available_languages

    @staticmethod
    def get_blank_translation_dictionary():
        """Return a dictionary of blank dictionaries, keyed by all available language."""
        return {language: dict() for language in get_available_languages()}