Ejemplo n.º 1
0
    def test_locale_missing_for_string(self):
        """String exists in translation table, but no value for the given locale."""

        self._db.translations.insert_one({
            'string': 'my text',
            'en': 'my text',
        })
        with self.assertRaises(i18n.TranslationMissingException):
            i18n.translate_string('my text', 'fr', self._db)
Ejemplo n.º 2
0
def get_in_a_departement_text(
        database: mongo.NoPiiMongoDatabase,
        departement_id: str,
        *,
        locale: str = 'fr',
        city_hint: Optional[geo_pb2.FrenchCity] = None) -> str:
    """Compute the French text for "in the Departement" for the given ID."""

    if city_hint and city_hint.departement_name:
        departement_name = city_hint.departement_name
    else:
        departement_name = get_departement_name(database, departement_id)
    if departement_name.startswith('La '):
        departement_name = departement_name[len('La '):]

    try:
        return i18n.translate_string(_IN_DEPARTEMENT, locale)\
            .format(departement_name=departement_name)
    except i18n.TranslationMissingException:
        pass

    if city_hint and city_hint.departement_prefix:
        prefix = city_hint.departement_prefix
    else:
        prefix = _DEPARTEMENTS.get_collection(database)[departement_id].prefix
    return prefix + departement_name
Ejemplo n.º 3
0
    def test_translate_strings(self) -> None:
        """Using fallback strings."""

        self.assertEqual(
            'mon texte',
            i18n.translate_string(['my text_plural', 'my text'], 'fr'),
        )
Ejemplo n.º 4
0
    def get_as_fake_email(
        self,
        user: user_pb2.User,
        *,
        database: mongo.NoPiiMongoDatabase,
        now: datetime.datetime,
    ) -> Optional[email_pb2.EmailSent]:
        """Get the campaign as fake email without sending it."""

        template_vars = self.get_vars(user, database=database, now=now)
        if template_vars is None:
            return None

        campaign_subject = get_campaign_subject(self.id)
        try:
            i18n_campaign_subject = i18n.translate_string(
                campaign_subject, user.profile.locale)
        except i18n.TranslationMissingException:
            i18n_campaign_subject = campaign_subject

        return email_pb2.EmailSent(
            subject=mustache.instantiate(i18n_campaign_subject, template_vars),
            mailjet_template=str(
                mailjet_templates.MAP[self.id]['mailjetTemplate']),
            campaign_id=self.id,
            is_coaching=self.is_coaching,
        )
Ejemplo n.º 5
0
    def test_translate_string(self):
        """Basic usage."""

        self._db.translations.insert_one({
            'string': 'my text',
            'fr': 'mon texte',
        })
        self.assertEqual('mon texte', i18n.translate_string('my text', 'fr', self._db))
Ejemplo n.º 6
0
    def test_locale_fallback(self) -> None:
        """String exists in translation table, but only for a simpler verison of the locale."""

        self._db.translations.insert_one({
            'string': 'my text',
            'en': 'my text in English',
        })
        self.assertEqual('my text in English', i18n.translate_string('my text', 'en_UK', self._db))
Ejemplo n.º 7
0
    def test_translate_string_use_cache(self) -> None:
        """Make sure that the translate_string is using the cache."""

        self._db.translations.insert_one({
            'string': 'my text',
            'fr': 'mon texte',
        })
        self.assertEqual(
            'mon texte',
            i18n.translate_string('my text', 'fr', self._db),
        )

        self._db.translations.update_one({'string': 'my text'}, {'$set': {'fr': 'updated text'}})
        self.assertEqual(
            'mon texte',
            i18n.translate_string('my text', 'fr', self._db),
        )
Ejemplo n.º 8
0
    def translate_string(self, string):
        """Translate a string to a language and locale defined by the project."""

        if self.user_profile.can_tutoie:
            try:
                return i18n.translate_string(string, 'fr_FR@tu', self._db)
            except i18n.TranslationMissingException:
                logging.exception('Falling back to vouvoiement')

        return string
Ejemplo n.º 9
0
    def test_translate_strings(self) -> None:
        """Using fallback strings."""

        self._db.translations.insert_one({
            'string': 'my text',
            'fr': 'mon texte',
        })
        self.assertEqual(
            'mon texte',
            i18n.translate_string(['my text_plural', 'my text'], 'fr', self._db),
        )
Ejemplo n.º 10
0
    def translate_string(self, string: str) -> str:
        """Translate a string to a language and locale defined by the project."""

        locale = self.user_profile.locale or (
            'fr@tu' if self.user_profile.can_tutoie else '')

        if locale and locale != 'fr':
            try:
                return i18n.translate_string(string, locale, self._db)
            except i18n.TranslationMissingException:
                logging.exception('Falling back to French on "%s"', string)

        return string
Ejemplo n.º 11
0
    def translate_string(
            self, string: str, is_genderized: bool = False, is_static: bool = False,
            context: str = '', can_log_exception: bool = True) -> str:
        """Translate a string to a language and locale defined by the project."""

        locale = self.user_profile.locale or 'fr'

        keys = [string]
        if context:
            keys.insert(0, f'{string}_{context}')
        if is_genderized and self.user_profile.gender:
            gender = user_profile_pb2.Gender.Name(self.user_profile.gender)
            keys.insert(0, f'{string}_{gender}')
            if context:
                keys.insert(0, f'{string}_{context}_{gender}')
        try:
            return i18n.translate_string(keys, locale, None if is_static else self._db)
        except i18n.TranslationMissingException:
            if not locale.startswith('fr') and can_log_exception:
                logging.exception('Falling back to French on "%s"', string)

        return string
Ejemplo n.º 12
0
    def send_mail(
            self,
            user: user_pb2.User,
            *,
            database: mongo.NoPiiMongoDatabase,
            users_database: Optional[mongo.UsersDatabase] = None,
            eval_database: Optional[mongo.NoPiiMongoDatabase] = None,
            now: datetime.datetime,
            action: 'Action' = 'dry-run',
            dry_run_email: Optional[str] = None,
            mongo_user_update: Optional[dict[str, Any]] = None,
            should_log_errors: bool = False
    ) -> Union[bool, email_pb2.EmailSent]:
        """Send an email for this campaign."""

        with set_time_locale(_LOCALE_MAP.get(user.profile.locale or 'fr')):
            template_vars = self.get_vars(user,
                                          should_log_errors=should_log_errors,
                                          database=database,
                                          now=now)
        if not template_vars:
            return False

        user_profile = user.profile

        if action == 'list':
            user_id = user.user_id
            logging.info('%s: %s %s', self.id, user_id, user_profile.email)
            return True

        if action == 'dry-run':
            user_profile.email = dry_run_email or '*****@*****.**'
            logging.info('Template vars:\n%s', template_vars)

        if action == 'ghost':
            email_sent = user.emails_sent.add()
            common_proto.set_date_now(email_sent.sent_at, now)
        else:
            res = mail_send.send_template(
                self.id,
                user_profile,
                template_vars,
                sender_email=self._sender_email,
                sender_name=template_vars['senderName'])
            logging.info(
                'Email sent to %s',
                user_profile.email if action == 'dry-run' else user.user_id)

            res.raise_for_status()

            maybe_email_sent = mail_send.create_email_sent_proto(res)
            if not maybe_email_sent:
                logging.warning(
                    'Impossible to retrieve the sent email ID:\n'
                    'Response (%d):\n%s\nUser: %s\nCampaign: %s',
                    res.status_code, res.json(), user.user_id, self.id)
                return False
            if action == 'dry-run':
                return maybe_email_sent

            email_sent = maybe_email_sent

        campaign_subject = get_campaign_subject(self.id)
        try:
            i18n_campaign_subject = i18n.translate_string(
                campaign_subject, user_profile.locale)
        except i18n.TranslationMissingException:
            i18n_campaign_subject = campaign_subject
        email_sent.subject = mustache.instantiate(i18n_campaign_subject,
                                                  template_vars)
        email_sent.mailjet_template = str(
            mailjet_templates.MAP[self.id]['mailjetTemplate'])
        email_sent.is_coaching = self.is_coaching
        email_sent.campaign_id = self.id
        if mongo_user_update and '$push' in mongo_user_update:  # pragma: no-cover
            raise ValueError(
                f'$push operations are not allowed in mongo_user_update:\n{mongo_user_update}'
            )
        user_id = user.user_id
        if not user_id or action == 'ghost' or not users_database:
            return email_sent

        users_database.user.update_one(
            {'_id': objectid.ObjectId(user_id)}, (mongo_user_update or {}) |
            {'$push': {
                'emailsSent': json_format.MessageToDict(email_sent),
            }})
        if eval_database:
            try:
                eval_database.sent_emails.update_one(
                    {'_id': self.id},
                    {'$set': {
                        'lastSent': proto.datetime_to_json_string(now)
                    }},
                    upsert=True)
            except pymongo.errors.OperationFailure:
                # We ignore this error silently: it's probably due to the base not being writeable
                # (which is the case in our demo servers). And the whole purpose of this update is
                # to update the monitoring info: if it fails, then the human being checking the
                # monitoring data will be warned that something is wrong as the data wasn't updated.
                pass
        return email_sent
Ejemplo n.º 13
0
class Campaign:
    """A Named tuple to define a campaign for blasting mails.

        - campaign_id: the ID for the Mailjet campaign.
        - mongo_filters: A filter on the mongoDB table for users, to select
          those that may receive this campaign.
        - get_vars: a function to retrieve the template variables from a user
          and a database.  This should be of the form get_vars(user, db) and
          return a dict that can be converted to JSON object, that can be
          accepted by MailJet templating API:
          https://dev.mailjet.com/template-language/reference/
          If a given user should not be sent the campaign, return None.
        - sender_name: the human readable name for the sender of this campaign.
        - sender_email: an email address for the sender of this email.
          Should be <something>@bob-emploi.fr
        - is_coaching: whether it's a coaching email and should be sent
          regularly as part of the coaching experience.
        - is_big_focus: whether it's a big focus on a topic.
    """
    def __init__(self,
                 campaign_id: mailjet_templates.Id,
                 *,
                 get_vars: '_GetVarsFuncType[user_pb2.User]',
                 sender_name: str,
                 sender_email: str,
                 mongo_filters: Optional[Mapping[str, Any]] = None,
                 get_mongo_filters: Optional[Callable[[], Optional[Mapping[
                     str, Any]]]] = None,
                 is_coaching: bool = False,
                 is_big_focus: bool = False) -> None:
        self._campaign_id = campaign_id
        self._mongo_filters = mongo_filters
        self._get_mongo_filters = get_mongo_filters
        self._get_vars = get_vars
        self._sender_name = sender_name
        self._sender_email = sender_email
        self._is_coaching = is_coaching
        self._is_big_focus = is_big_focus

    id = property(lambda self: self._campaign_id)
    is_coaching = property(lambda self: self._is_coaching)
    is_big_focus = property(lambda self: self._is_big_focus)

    @property
    def mongo_filters(self) -> Optional[Mapping[str, Any]]:
        """A filter on the MongoDB table for users, to select those that may receive this campaign.
        """

        static_filters = self._mongo_filters
        dynamic_filters = self._get_mongo_filters(
        ) if self._get_mongo_filters else None

        if not static_filters or not dynamic_filters:
            return static_filters or dynamic_filters
        return dict(static_filters) | dynamic_filters

    def get_vars(self,
                 user: user_pb2.User,
                 /,
                 *,
                 should_log_errors: bool = False,
                 database: mongo.NoPiiMongoDatabase,
                 now: datetime.datetime) -> Optional[dict[str, str]]:
        """Get template variables for the given user."""

        try:
            template_vars = self._get_vars(user, database=database, now=now)
        except (scoring.NotEnoughDataException, DoNotSend) as error:
            if should_log_errors:
                logging.error(error)
            return None
        try:
            i18n_sender_name = i18n.translate_string(self._sender_name,
                                                     user.profile.locale)
        except i18n.TranslationMissingException:
            i18n_sender_name = self._sender_name

        if '{' in i18n_sender_name:
            i18n_sender_name = mustache.instantiate(i18n_sender_name,
                                                    template_vars)
        return {'senderName': i18n_sender_name} | template_vars
Ejemplo n.º 14
0
    def test_locale_missing_for_string(self) -> None:
        """String exists in translation table, but no value for the given locale."""

        with self.assertRaises(i18n.TranslationMissingException):
            i18n.translate_string('my text', 'en')
Ejemplo n.º 15
0
    def test_no_data(self):
        """Absolutely no data."""

        with self.assertRaises(i18n.TranslationMissingException):
            i18n.translate_string('my text', 'fr', self._db)
Ejemplo n.º 16
0
    def test_translate_empty_string(self) -> None:
        """Translation of the empty string does not raise an error."""

        self.assertEqual('', i18n.translate_string('', 'fr', self._db))
Ejemplo n.º 17
0
    def test_translate_string(self) -> None:
        """Basic usage."""

        self.assertEqual('mon texte', i18n.translate_string('my text', 'fr'))
Ejemplo n.º 18
0
    def test_translate_strings_fail(self) -> None:
        """Using fallback strings and failing on all of them."""

        with self.assertRaises(i18n.TranslationMissingException) as error:
            i18n.translate_string(['my text_plural', 'my text'], 'fr', self._db)
        self.assertIn('my text_plural', str(error.exception))