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)
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
def test_translate_strings(self) -> None: """Using fallback strings.""" self.assertEqual( 'mon texte', i18n.translate_string(['my text_plural', 'my text'], 'fr'), )
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, )
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))
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))
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), )
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
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), )
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
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
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
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
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')
def test_no_data(self): """Absolutely no data.""" with self.assertRaises(i18n.TranslationMissingException): i18n.translate_string('my text', 'fr', self._db)
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))
def test_translate_string(self) -> None: """Basic usage.""" self.assertEqual('mon texte', i18n.translate_string('my text', 'fr'))
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))