コード例 #1
0
    def test_returned_old_user(self, mock_info: mock.MagicMock) -> None:
        """An inactive user back on Bob doesn't get deleted."""

        self._db.user.insert_one({
            'hasAccount': True,
            'profile': {'name': 'Sil'},
            'registeredAt': '2014-07-03T00:00:00Z',
            'emailsSent': [{
                'campaignId': 'whatever-email',
                'sentAt': proto.datetime_to_json_string(
                    datetime.datetime.now() - datetime.timedelta(days=12))
            }],
            'requestedByUserAtDate': proto.datetime_to_json_string(
                datetime.datetime.now() - datetime.timedelta(days=10))
        })
        clean_users.main(['--no-dry-run', '--disable-sentry'])
        mock_info.assert_called_once_with(
            'Cleaned %d users, set check date for %d users and got %d errors', 0, 1, 0)
        db_user = self._db.user.find_one({})
        assert db_user
        req_user_by_date = _zulu_time_to_datetime(db_user.get('requestedByUserAtDate'))
        chck_for_deletion_date = _zulu_time_to_datetime(db_user.get('checkForDeletionDate'))

        self.assertEqual('Sil', db_user.get('profile', {}).get('name'))
        self.assertFalse(db_user.get('deletedAt'))
        self.assertEqual(
            req_user_by_date.replace(microsecond=0) + datetime.timedelta(days=90),
            chck_for_deletion_date.replace(microsecond=0))
コード例 #2
0
def main(string_args: Optional[list[str]] = None) -> None:
    """Clean all support tickets marked for deletion."""

    user_db = mongo.get_connections_from_env().user_db

    parser = argparse.ArgumentParser(
        description='Clean support tickets from the database.')
    report.add_report_arguments(parser)

    args = parser.parse_args(string_args)
    if not report.setup_sentry_logging(args):
        return

    instant = proto.datetime_to_json_string(now.get())
    result = user_db.user.update_many(
        {}, {'$pull': {
            'supportTickets': {
                'deleteAfter': {
                    '$lt': instant
                }
            }
        }})
    logging.info('Removed deprecated support tickets for %d users.',
                 result.modified_count)
    clean_result = user_db.user.update_many({'supportTickets': {
        '$size': 0
    }}, {'$unset': {
        'supportTickets': ''
    }})
    if clean_result.matched_count:
        logging.info('Removed empty support ticket list for %d users.',
                     clean_result.modified_count)
コード例 #3
0
ファイル: focus.py プロジェクト: bayesimpact/bob-emploi
def _send_focus_emails(
        action: 'campaign.NoGhostAction', dry_run_email: str,
        restricted_campaigns: Optional[Iterable[mailjet_templates.Id]] = None) -> None:
    database, users_database, eval_database = mongo.get_connections_from_env()

    instant = now.get()
    email_errors = 0
    counts = {
        campaign_id: 0
        for campaign_id in sorted(get_possible_campaigns(database, restricted_campaigns))
    }
    potential_users = users_database.user.find({
        'profile.email': {
            '$regex': re.compile(r'[^ ]+@[^ ]+\.[^ ]+'),
            '$not': re.compile(r'@example.com$'),
        },
        'projects': {'$elemMatch': {
            'isIncomplete': {'$ne': True},
        }},
        'profile.coachingEmailFrequency': {'$in': [
            email_pb2.EmailFrequency.Name(setting) for setting in _EMAIL_PERIOD_DAYS]},
        # Note that "not >" is not equivalent to "<=" in the case the field
        # is not defined: in that case we do want to select the user.
        'sendCoachingEmailAfter': {'$not': {'$gt': proto.datetime_to_json_string(instant)}},
    })
    restricted_campaigns_set: Optional[Set[mailjet_templates.Id]]
    if restricted_campaigns:
        restricted_campaigns_set = set(restricted_campaigns)
    else:
        restricted_campaigns_set = None
    for user_dict in potential_users:
        user_id = user_dict.pop('_id')
        user = proto.create_from_mongo(user_dict, user_pb2.User)
        user.user_id = str(user_id)

        try:
            campaign_id = send_focus_email_to_user(
                action, user, dry_run_email=dry_run_email, database=database,
                users_database=users_database, eval_database=eval_database, instant=instant,
                restricted_campaigns=restricted_campaigns_set)
        except requests.exceptions.HTTPError as error:
            if action == 'dry-run':
                raise
            logging.warning('Error while sending an email: %s', error)
            email_errors += 1
            continue

        if campaign_id:
            counts[campaign_id] += 1
            if action == 'dry-run':
                break
            continue

    report_message = 'Focus emails sent today:\n' + '\n'.join([
        f' • *{campaign_id}*: {count} email{"s" if count > 1 else ""}'
        for campaign_id, count in counts.items()
    ])
    if action == 'send':
        report.notify_slack(report_message)
    logging.info(report_message)
コード例 #4
0
ファイル: timeout_reviews.py プロジェクト: b3rday/bob-emploi
def main(string_args: Optional[List[str]] = None) -> None:
    """Time out CVS and motivation letters reviews."""

    parser = argparse.ArgumentParser(
        description='Time out CVs and motivation letters reviews.',
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)

    parser.add_argument('--days-before-timeout', default='5', type=int)

    args = parser.parse_args(string_args)

    timeout_date = now.get() - datetime.timedelta(
        days=args.days_before_timeout)

    documents = _USER_DB.cvs_and_cover_letters.find({
        'reviews': {
            '$elemMatch': {
                'sentAt': {
                    '$lt': proto.datetime_to_json_string(timeout_date)
                },
                'status': 'REVIEW_SENT',
            }
        },
    })
    for document in documents:
        _timeout_old_reviews(document, timeout_date)
コード例 #5
0
ファイル: evaluation.py プロジェクト: bayesimpact/bob-emploi
def _log_request(email: str, requester_email: str, database: mongo.NoPiiMongoDatabase) -> None:
    try:
        # Log that we've tried to access to a specific user.
        database.email_requests.insert_one({
            'email': email,
            'registeredAt': proto.datetime_to_json_string(now.get()),
            'requesterEmail': requester_email,
            'action': 'eval',
        })
    except errors.OperationFailure:
        flask.abort(401, "Vous n'avez pas accès en écriture à la base de données.")
コード例 #6
0
    def test_create_support_ticket(self) -> None:
        """A user is assigned a support ID if requested."""

        user_id, token = self.create_user_with_token()

        response = self.app.post(f'/api/support/{user_id}',
                                 headers={'Authorization': 'Bearer ' + token},
                                 content_type='application/json')
        ticket = self.json_from_response(response)
        self.assertTrue(ticket.get('ticketId'))
        delete_after = ticket.get('deleteAfter')
        do_not_delete_before = proto.datetime_to_json_string(
            now.get() + datetime.timedelta(days=1))
        delete_before = proto.datetime_to_json_string(now.get() +
                                                      datetime.timedelta(
                                                          days=30))
        self.assertGreater(delete_after, do_not_delete_before)
        self.assertLess(delete_after, delete_before)
        user_data = self.get_user_info(user_id, token)
        last_saved_ticket = typing.cast(
            Dict[str, str],
            user_data.get('supportTickets', [])[-1])
        self.assertEqual(ticket, last_saved_ticket)
コード例 #7
0
    def test_days_ago(self, mock_info: mock.MagicMock) -> None:
        """Only guests users connected before the 1 week period are deleted."""

        self._db.user.insert_many([{
            'profile': {'name': 'Cyrille'},
            'projects': [{'projectId': str(i)}],
            'requestedByUserAtDate': proto.datetime_to_json_string(
                datetime.datetime.now() - datetime.timedelta(days=i, hours=1)),
        } for i in range(4, 11)])
        clean_users.main(['--no-dry-run', '--disable-sentry'])
        mock_info.assert_called_once_with(
            'Cleaned %d users, set check date for %d users and got %d errors', 4, 3, 0)
        self.assertCountEqual(
            ['4', '5', '6'],
            [user['projects'][0]['projectId'] for user in self._db.user.find({'deletedAt': None})])
コード例 #8
0
def _send_focus_emails(action: 'campaign.Action', dry_run_email: str) -> None:
    database, users_database, unused_eval_database = mongo.get_connections_from_env()

    instant = now.get()
    email_errors = 0
    counts = {campaign_id: 0 for campaign_id in _FOCUS_CAMPAIGNS}
    potential_users = users_database.user.find({
        'profile.email': re.compile('.+@.+'),
        'projects': {'$elemMatch': {
            'isIncomplete': {'$ne': True},
        }},
        'profile.coachingEmailFrequency': {'$in': [
            user_pb2.EmailFrequency.Name(setting) for setting in _EMAIL_PERIOD_DAYS]},
        # Note that "not >" is not equivalent to "<=" in the case the field
        # is not defined: in that case we do want to select the user.
        'sendCoachingEmailAfter': {'$not': {'$gt': proto.datetime_to_json_string(instant)}},
    })
    for user_dict in potential_users:
        user_id = user_dict.pop('_id')
        user = typing.cast(user_pb2.User, proto.create_from_mongo(user_dict, user_pb2.User))
        user.user_id = str(user_id)

        try:
            campaign_id = send_focus_email_to_user(
                action, user, dry_run_email=dry_run_email, database=database,
                users_database=users_database, instant=instant)
        except requests.exceptions.HTTPError as error:
            if action == 'dry-run':
                raise
            logging.warning('Error while sending an email: %s', error)
            email_errors += 1
            continue

        if campaign_id:
            counts[campaign_id] += 1
            continue

    report_message = 'Focus emails sent:\n' + '\n'.join([
        f' • *{campaign_id}*: {count} email{"s" if count > 1 else ""}'
        for campaign_id, count in counts.items()
    ])
    if action == 'send':
        report.notify_slack(report_message)
    logging.info(report_message)
コード例 #9
0
    def test_recent_email_reader(self, mock_info: mock.MagicMock) -> None:
        """A user that has read an email in the pas 2 months doesn't get deleted."""

        self._db.user.insert_one({
            'hasAccount': True,
            'profile': {'name': 'Sil'},
            'registeredAt': '2014-07-03T00:00:00Z',
            'emailsSent': [{
                'campaignId': 'christmas',
                'sentAt': proto.datetime_to_json_string(
                    datetime.datetime.now() - datetime.timedelta(days=79)),
                'status': email_pb2.EMAIL_SENT_OPENED
            }],
            'requestedByUserAtDate': '2014-07-10T00:00:00Z'
        })
        clean_users.main(['--no-dry-run', '--disable-sentry'])
        mock_info.assert_called_once_with(
            'Cleaned %d users, set check date for %d users and got %d errors', 0, 1, 0)
        db_user = self._db.user.find_one({})
        assert db_user
        self.assertFalse(db_user.get('deletedAt'))
コード例 #10
0
    def test_old_user(self, mock_info: mock.MagicMock) -> None:
        """An inactive user gets deleted."""

        self._db.user.insert_one({
            'hasAccount': True,
            'profile': {'name': 'Sil'},
            'registeredAt': '2014-07-03T00:00:00Z',
            'emailsSent': [{
                'campaignId': 'account-deletion-notice',
                'sentAt': proto.datetime_to_json_string(
                    datetime.datetime.today() - datetime.timedelta(days=9))
            }],
            'requestedByUserAtDate': '2014-07-10T00:00:00Z'
        })
        clean_users.main(['--no-dry-run', '--disable-sentry'])
        mock_info.assert_called_once_with(
            'Cleaned %d users, set check date for %d users and got %d errors', 1, 0, 0)
        db_user = self._db.user.find_one({})
        assert db_user
        self.assertEqual('REDACTED', db_user.get('profile', {}).get('name'))
        self.assertTrue(db_user.get('deletedAt'))
コード例 #11
0
    def test_days_ago(self, mock_info: mock.MagicMock) -> None:
        """Only users created before the given days delta are deleted."""

        self._db.user.insert_many([{
            'profile': {
                'name': 'Cyrille'
            },
            'projects': [{
                'projectId': str(i)
            }],
            'registeredAt':
            proto.datetime_to_json_string(datetime.datetime.now() -
                                          datetime.timedelta(days=i, hours=1)),
        } for i in range(7)])
        clean_guests.main(
            ['--no-dry-run', '--disable-sentry', '--registered-to-days-ago=3'])
        mock_info.assert_called_once_with('Cleaned %d users and got %d errors',
                                          4, 0)
        self.assertCountEqual(['0', '1', '2'], [
            user['projects'][0]['projectId']
            for user in self._db.user.find({'deletedAt': None})
        ])
コード例 #12
0
def main(string_args: Optional[List[str]] = None) -> None:
    """Clean all support tickets marked for deletion."""

    parser = argparse.ArgumentParser(
        description='Clean support tickets from the database.')
    parser.add_argument('--disable-sentry',
                        action='store_true',
                        help='Disable logging to Sentry.')

    args = parser.parse_args(string_args)
    logging.basicConfig(level='INFO')
    if not args.disable_sentry:
        try:
            report.setup_sentry_logging(os.getenv('SENTRY_DSN'))
        except ValueError:
            logging.error(
                'Please set SENTRY_DSN to enable logging to Sentry, or use --disable-sentry option'
            )
            return

    instant = proto.datetime_to_json_string(now.get())
    result = _DB.user.update_many(
        {}, {'$pull': {
            'supportTickets': {
                'deleteAfter': {
                    '$lt': instant
                }
            }
        }})
    logging.info('Removed deprecated support tickets for %d users.',
                 result.modified_count)
    clean_result = _DB.user.update_many({'supportTickets': {
        '$size': 0
    }}, {'$unset': {
        'supportTickets': ''
    }})
    if clean_result.matched_count:
        logging.info('Removed empty support ticket list for %d users.',
                     clean_result.modified_count)
コード例 #13
0
from typing import Optional, Tuple

import bson
from bson import objectid
import pymongo

from bob_emploi.frontend.api import user_pb2
from bob_emploi.frontend.server import auth
from bob_emploi.frontend.server import mongo
from bob_emploi.frontend.server import proto
from bob_emploi.frontend.server.asynchronous import report
from bob_emploi.frontend.server.mail import mail_send

_MAX_GUEST_IDLE_TIME = datetime.timedelta(7)
_MAX_SIGNED_IN_USER_IDLE_TIME = datetime.timedelta(90)
_TODAY_STRING = proto.datetime_to_json_string(datetime.datetime.now())


def _get_last_interaction_date(user_proto: user_pb2.User) -> datetime.datetime:
    """Get the date of user's last email or app interaction."""

    last_interaction_date = user_proto.registered_at.ToDatetime()
    if user_proto.registered_at:
        last_interaction_date = user_proto.requested_by_user_at_date.ToDatetime(
        )

    email_interaction_dates = [
        email.sent_at.ToDatetime() for email in user_proto.emails_sent
        if email.campaign_id != 'account-deletion-notice'
        and email.status in mail_send.READ_EMAIL_STATUSES and email.sent_at
    ]
コード例 #14
0
ファイル: deletion.py プロジェクト: bayesimpact/bob-emploi
"""Campaigns for the account deletion emails."""

import datetime
from typing import Any

from bob_emploi.frontend.api import user_pb2
from bob_emploi.frontend.server import i18n
from bob_emploi.frontend.server import proto
from bob_emploi.frontend.server.mail import campaign
from bob_emploi.frontend.server.mail import mail_send

_TWO_YEARS_AGO_STRING = \
    proto.datetime_to_json_string(datetime.datetime.now() - datetime.timedelta(730))


def _account_deletion_notice_vars(user: user_pb2.User, **unused_kwargs: Any) -> dict[str, str]:
    return dict(
        campaign.get_default_vars(user),
        loginUrl=campaign.create_logged_url(user.user_id))


campaign.register_campaign(campaign.Campaign(
    campaign_id='account-deletion-notice',
    mongo_filters={
        # User hasn't been on Bob for two years.
        'registeredAt': {'$lt': _TWO_YEARS_AGO_STRING},
        'requestedByUserAtDate': {'$not': {'$gt': _TWO_YEARS_AGO_STRING}},
        # User hasn't read any email we sent to them in the last two years.
        'emailsSent': {'$not': {'$elemMatch': {
            'sentAt': {'$gt': _TWO_YEARS_AGO_STRING},
            'status': {'$in': list(mail_send.READ_EMAIL_STATUS_STRINGS)},
コード例 #15
0
ファイル: focus.py プロジェクト: bayesimpact/bob-emploi
def send_focus_email_to_user(
        action: 'campaign.Action', user: user_pb2.User, *, dry_run_email: Optional[str] = None,
        database: mongo.NoPiiMongoDatabase,
        users_database: Optional[mongo.UsersDatabase] = None,
        eval_database: Optional[mongo.NoPiiMongoDatabase] = None,
        instant: datetime.datetime,
        restricted_campaigns: Optional[Set[mailjet_templates.Id]] = None) \
        -> Optional[mailjet_templates.Id]:
    """Try to send a focus email to the user and returns the campaign ID."""

    if user.profile.coaching_email_frequency == email_pb2.EMAIL_NONE:
        return None
    if not user.HasField('send_coaching_email_after'):
        send_coaching_email_after = _compute_next_coaching_email_date(user)
        if send_coaching_email_after > instant:
            user.send_coaching_email_after.FromDatetime(send_coaching_email_after)
            if user.user_id and action != 'ghost' and users_database:
                users_database.user.update_one(
                    {'_id': objectid.ObjectId(user.user_id)}, {'$set': {
                        'sendCoachingEmailAfter': proto.datetime_to_json_string(
                            send_coaching_email_after,
                        ),
                    }})
            return None

    # Compute next send_coaching_email_after.
    next_send_coaching_email_after = instant + _compute_duration_to_next_coaching_email(user)

    focus_emails_sent: Set[mailjet_templates.Id] = set()
    last_focus_email_sent = None
    for email_sent in user.emails_sent:
        if email_sent.campaign_id not in _FOCUS_CAMPAIGNS:
            continue
        last_focus_email_sent = email_sent
        focus_emails_sent.add(typing.cast(mailjet_templates.Id, email_sent.campaign_id))

    project = scoring.ScoringProject(
        user.projects[0] if user.projects else project_pb2.Project(),
        user, database, instant,
    )

    possible_campaigns = get_possible_campaigns(database, restricted_campaigns)
    campaigns_scores = {
        campaign_id: (
            project.score(possible_campaign.scoring_model)
            if possible_campaign.scoring_model else 2
        )
        for campaign_id, possible_campaign in possible_campaigns.items()
    }

    focus_emails_project_score_zero = {
        campaign for campaign, score in campaigns_scores.items() if not score
    }

    potential_campaigns = _shuffle(
        possible_campaigns.keys() - focus_emails_sent - focus_emails_project_score_zero,
        last_focus_email_sent, campaigns_scores,
        user.profile.coaching_email_frequency)

    for campaign_id in potential_campaigns:
        if _FOCUS_CAMPAIGNS[campaign_id].send_mail(
                user, database=database, users_database=users_database, eval_database=eval_database,
                action=action, dry_run_email=dry_run_email,
                mongo_user_update={'$set': {
                    'sendCoachingEmailAfter': proto.datetime_to_json_string(
                        next_send_coaching_email_after,
                    ),
                }}, now=instant):
            user.send_coaching_email_after.FromDatetime(next_send_coaching_email_after)
            return campaign_id

    # No focus email was supported: it seems that we have sent all the
    # ones we had. However maybe in the future we'll add more focus
    # emails so let's wait the same amount of time we have waited until
    # this email (this makes to wait 1 period, then 2, 4, …).
    last_coaching_email_sent_at = \
        _compute_last_coaching_email_date(user, user.registered_at.ToDatetime())
    send_coaching_email_after = instant + (instant - last_coaching_email_sent_at)
    user.send_coaching_email_after.FromDatetime(send_coaching_email_after)
    if user.user_id and action != 'ghost' and users_database:
        logging.debug('No more available focus email for "%s"', user.user_id)
        users_database.user.update_one({'_id': objectid.ObjectId(user.user_id)}, {'$set': {
            'sendCoachingEmailAfter': proto.datetime_to_json_string(send_coaching_email_after),
        }})
    return None
コード例 #16
0
def main(string_args: Optional[list[str]] = None) -> None:
    """Check the status of sent emails on MailJet and update our Database.
    """

    parser = argparse.ArgumentParser(
        description='Update email status on sent emails.',
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    report.add_report_arguments(parser)

    parser.add_argument(
        '--campaigns',
        choices=mail_blast.campaign.list_all_campaigns(),
        nargs='*',
        help='Campaign IDs to check. If not specified, run for all campaigns.')

    parser.add_argument('--mongo-collection',
                        default='user',
                        help='Name of the mongo collection to update.')

    args = parser.parse_args(string_args)

    if not report.setup_sentry_logging(args):
        return

    email_mongo_filter = {
        'mailjetMessageId': {
            '$exists': True
        },
    }
    if args.campaigns:
        email_mongo_filter['campaignId'] = {'$in': args.campaigns}
    yesterday = proto.datetime_to_json_string(now.get() -
                                              datetime.timedelta(days=1))
    mongo_filter = {
        '$or': [
            # Emails that we've never checked.
            {
                'emailsSent': {
                    '$elemMatch':
                    dict({
                        'lastStatusCheckedAt': {
                            '$exists': False
                        },
                    }, **email_mongo_filter),
                },
            },
            # Emails checked less than two weeks after they have been sent and
            # that we haven't checked today.
            {
                'emailsSent': {
                    '$elemMatch':
                    dict(
                        {
                            'lastStatusCheckedAt': {
                                '$lt': yesterday
                            },
                            'lastStatusCheckedAfterDays': {
                                '$not': {
                                    '$gte': 14
                                }
                            },
                        }, **email_mongo_filter),
                },
            },
            # Emails sent less than 24 hours ago.
            {
                'emailsSent': {
                    '$elemMatch':
                    dict({
                        'sentAt': {
                            '$gt': yesterday
                        },
                    }, **email_mongo_filter),
                },
            },
        ],
    }

    user_db = mongo.get_connections_from_env().user_db
    mongo_collection = user_db.get_collection(args.mongo_collection)
    selected_users = mongo_collection.find(mongo_filter, {'emailsSent': 1})
    treated_users = 0
    # TODO(cyrille): Make sure errors are logged to sentry.
    # TODO(cyrille): If it fails on a specific user, keep going.
    for user in selected_users:
        emails_sent = user.get('emailsSent', [])
        updated_emails_sent = [
            _update_email_sent_status(email,
                                      yesterday,
                                      campaign_ids=args.campaigns)
            for email in emails_sent
        ]
        mongo_collection.update_one(
            {'_id': user['_id']},
            {'$set': {
                'emailsSent': updated_emails_sent
            }})
        treated_users += 1
        if not treated_users % 100:
            logging.info('Treated %d users', treated_users)
コード例 #17
0
ファイル: campaign.py プロジェクト: bayesimpact/bob-emploi
    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
コード例 #18
0
def main(string_args: Optional[List[str]] = None) -> None:
    """Check the status of sent emails on MailJet and update our Database.
    """

    parser = argparse.ArgumentParser(
        description='Update email status on sent emails.',
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)

    parser.add_argument(
        '--campaigns',
        choices=mail_blast.campaign.list_all_campaigns(),
        nargs='*',
        help='Campaign IDs to check. If not specified, run for all campaigns.')

    parser.add_argument('--mongo-collection',
                        default='user',
                        help='Name of the mongo collection to update.')

    parser.add_argument('--disable-sentry',
                        action='store_true',
                        help='Disable logging to Sentry.')

    args = parser.parse_args(string_args)

    if not args.disable_sentry:
        try:
            report.setup_sentry_logging(os.getenv('SENTRY_DSN'))
        except ValueError:
            logging.error(
                'Please set SENTRY_DSN to enable logging to Sentry, or use --disable-sentry option'
            )
            return

    email_mongo_filter = {
        'mailjetMessageId': {
            '$exists': True
        },
    }
    if args.campaigns:
        email_mongo_filter['campaignId'] = {'$in': args.campaigns}
    yesterday = proto.datetime_to_json_string(now.get() -
                                              datetime.timedelta(days=1))
    mongo_filter = {
        '$or': [
            # Emails that we've never checked.
            {
                'emailsSent': {
                    '$elemMatch':
                    dict({
                        'lastStatusCheckedAt': {
                            '$exists': False
                        },
                    }, **email_mongo_filter),
                },
            },
            # Emails checked less than two weeks after they have been sent and
            # that we haven't checked today.
            {
                'emailsSent': {
                    '$elemMatch':
                    dict(
                        {
                            'lastStatusCheckedAt': {
                                '$lt': yesterday
                            },
                            'lastStatusCheckedAfterDays': {
                                '$not': {
                                    '$gte': 14
                                }
                            },
                        }, **email_mongo_filter),
                },
            },
            # Emails sent less than 24 hours ago.
            {
                'emailsSent': {
                    '$elemMatch':
                    dict({
                        'sentAt': {
                            '$gt': yesterday
                        },
                    }, **email_mongo_filter),
                },
            },
        ],
    }

    mongo_collection = _DB.get_collection(args.mongo_collection)
    selected_users = mongo_collection.find(mongo_filter, {'emailsSent': 1})
    treated_users = 0
    for user in selected_users:
        emails_sent = user.get('emailsSent', [])
        updated_emails_sent = [
            _update_email_sent_status(email,
                                      yesterday,
                                      campaign_ids=args.campaigns)
            for email in emails_sent
        ]
        mongo_collection.update_one(
            {'_id': user['_id']},
            {'$set': {
                'emailsSent': updated_emails_sent
            }})
        treated_users += 1
        if not treated_users % 100:
            logging.info('Treated %d users', treated_users)
コード例 #19
0
def send_focus_email_to_user(
        action: 'campaign.Action', user: user_pb2.User, *, dry_run_email: Optional[str] = None,
        database: pymongo.database.Database, users_database: pymongo.database.Database,
        instant: datetime.datetime) -> Optional[str]:
    """Try to send a focus email to the user and returns the campaign ID."""

    if not user.HasField('send_coaching_email_after'):
        send_coaching_email_after = _compute_next_coaching_email_date(user)
        if send_coaching_email_after > instant:
            user.send_coaching_email_after.FromDatetime(send_coaching_email_after)
            if user.user_id:
                users_database.user.update_one(
                    {'_id': objectid.ObjectId(user.user_id)}, {'$set': {
                        'sendCoachingEmailAfter': proto.datetime_to_json_string(
                            send_coaching_email_after,
                        ),
                    }})
            return None

    # Compute next send_coaching_email_after.
    next_send_coaching_email_after = instant + _compute_duration_to_next_coaching_email(user)

    focus_emails_sent = set()
    last_focus_email_sent = None
    for email_sent in user.emails_sent:
        if email_sent.campaign_id not in _FOCUS_CAMPAIGNS:
            continue
        last_focus_email_sent = email_sent
        focus_emails_sent.add(email_sent.campaign_id)

    last_one_was_big = last_focus_email_sent and \
        _FOCUS_CAMPAIGNS[last_focus_email_sent.campaign_id].is_big_focus
    potential_campaigns = sorted(
        _POTENTIAL_CAMPAIGNS - focus_emails_sent,
        key=lambda c: (
            1 if _FOCUS_CAMPAIGNS[c].is_big_focus == last_one_was_big else 0,
            random.random(),
        )
    )

    for campaign_id in potential_campaigns:
        if _FOCUS_CAMPAIGNS[campaign_id].send_mail(
                campaign_id, user, database=database, users_database=users_database, action=action,
                dry_run_email=dry_run_email,
                mongo_user_update={'$set': {
                    'sendCoachingEmailAfter': proto.datetime_to_json_string(
                        next_send_coaching_email_after,
                    ),
                }}, now=instant):
            user.send_coaching_email_after.FromDatetime(next_send_coaching_email_after)
            return campaign_id

    # No focus email was supported: it seems that we have sent all the
    # ones we had. However maybe in the future we'll add more focus
    # emails so let's wait the same amount of time we have waited until
    # this email (this makes to wait 1 period, then 2, 4, …).
    last_coaching_email_sent_at = typing.cast(
        datetime.datetime,
        _compute_last_coaching_email_date(user, user.registered_at.ToDatetime()))
    send_coaching_email_after = instant + (instant - last_coaching_email_sent_at)
    user.send_coaching_email_after.FromDatetime(send_coaching_email_after)
    if user.user_id and action != 'ghost':
        logging.debug('No more available focus email for "%s"', user.user_id)
        users_database.user.update_one({'_id': objectid.ObjectId(user.user_id)}, {'$set': {
            'sendCoachingEmailAfter': proto.datetime_to_json_string(send_coaching_email_after),
        }})
    return None