Пример #1
0
def add_test_period_with_template(
    dbsession,
    subperiod,
    template_id,
    period_id=TEST_PERIOD_ID,
    period_name=TEST_PERIOD_NAME,
    offset_from_utc_now_days=0,
    add_nominees=True,
    days_in=1,
):

    days_away = partial(days_from_utcnow, offset=offset_from_utc_now_days)
    times = generate_period_dates(subperiod, days_away, days_in)
    with transaction.manager:
        period = Period(id=period_id,
                        name=period_name,
                        template_id=template_id,
                        **times)
        dbsession.add(period)

    with transaction.manager:
        if add_nominees:
            for nominee_username in TEST_NOMINEES:
                dbsession.add(
                    Nominee(period_id=period_id, username=nominee_username))
    return period_id
Пример #2
0
def send_invite_email(dbsession, settings, inviter, invitee):
    log.info("Sending invite email to %s..." % invitee.email)
    company_name = get_config_value(settings, constants.COMPANY_NAME_KEY, "")
    subject = _build_full_subject(
        company_name,
        "Invitation to give feedback to %s" % inviter.display_name)
    env = _build_template_env()
    template = env.get_template(os.path.join("email", "invite.html.j2"))
    current_period = Period.get_current_period(dbsession)
    app_host = get_root_url(settings)
    from_email = get_config_value(settings, constants.SUPPORT_EMAIL_KEY)
    rendered_html = template.render(invitee=invitee,
                                    inviter=inviter,
                                    period=current_period,
                                    app_host=app_host)
    message_root = _generate_message_root(rendered_html,
                                          from_email,
                                          subject,
                                          reply_to=inviter.email)
    send_emails = _get_send_email_flag(settings)
    if send_emails:
        s = smtplib.SMTP()
        s.connect()
        s.sendmail(from_email, [invitee.email], message_root.as_string())
    log.info("Successfully sent an invite email to %s!" % invitee.email)
Пример #3
0
def test_employee_can_get_correct_nomination_status_when_outside_enrollment_period(
        ldap_mocked_app_with_users, is_enrolled, body):  # noqa: E501
    dbsession = get_dbsession(ldap_mocked_app_with_users)

    with transaction.manager:
        period = Period(
            name=TEST_PERIOD_NAME,
            enrollment_start_utc=days_from_utcnow(-2),
            entry_start_utc=days_from_utcnow(-1),
            approval_start_utc=days_from_utcnow(2),
            approval_end_utc=days_from_utcnow(3),
        )
        dbsession.add(period)
        if is_enrolled:
            nominee = Nominee(username=TEST_EMPLOYEE_USERNAME, period=period)
            dbsession.add(nominee)

    app = successfully_login(ldap_mocked_app_with_users,
                             TEST_EMPLOYEE_USERNAME)
    csrf_token = app.cookies[ANGULAR_2_XSRF_TOKEN_COOKIE_NAME]
    response = app.get("/api/v1/self-nominate",
                       headers={ANGULAR_2_XSRF_TOKEN_HEADER_NAME: csrf_token})
    assert response.json_body["heading"] == ENROLLMENT_INACTIVE_TEMPLATE[
        "heading"].format(period_name=TEST_PERIOD_NAME)
    assert response.json_body["body"] == body
    assert (response.json_body["buttonText"] ==
            ENROLLMENT_INACTIVE_TEMPLATE["buttonText"])
    assert (response.json_body["buttonLink"] ==
            ENROLLMENT_INACTIVE_TEMPLATE["buttonLink"])
    assert (response.json_body["canNominate"] ==
            ENROLLMENT_INACTIVE_TEMPLATE["canNominate"])
Пример #4
0
def get_nomination_status(request):
    """
    Returns
    -------
    JSON-serialisable payload that includes:
    * Message to display to `request.user` on their current nomination status
      for the current period.
    * Whether to display a button, with an associated URL and display text
    """
    location = get_config_value(request.registry.settings,
                                constants.HOMEBASE_LOCATION_KEY)
    current_period = Period.get_current_period(request.dbsession,
                                               options=joinedload("nominees"))
    if not current_period:
        return interpolate_template(FEEDBACK_ENDED_TEMPLATE)

    username = request.user.username
    if username in (n.username for n in current_period.nominees):
        is_enrolled = True
    else:
        is_enrolled = False

    if current_period.subperiod(location) != Period.ENROLLMENT_SUBPERIOD:
        return interpolate_template(
            ENROLLMENT_INACTIVE_TEMPLATE,
            period_name=current_period.name,
            body=ENROLLED_BODY if is_enrolled else NOT_ENROLLED_BODY,
        )
    if is_enrolled:
        return interpolate_template(ENROLLMENT_EXISTS_TEMPLATE,
                                    period_name=current_period.name)

    return interpolate_template(ENROLLMENT_ACTIVE_TEMPLATE,
                                period_name=current_period.name)
Пример #5
0
def post_external_invite(request):
    """
    If allowed, records that `request.user` sent an external invite to a user
    identified by their email which must be in LDAP, and sends an email to
    that invited used.

    Parameters
    ----------
    request: `pyramid.request.Request`

    Returns
    -------
    JSON serialisable payload stating success
    """
    ldapsource = request.ldapsource
    email = request.json_body["email"]

    location = get_config_value(request.registry.settings,
                                constants.HOMEBASE_LOCATION_KEY)
    current_period = Period.get_current_period(request.dbsession)
    settings = request.registry.settings
    company_name = get_config_value(settings, constants.COMPANY_NAME_KEY,
                                    "company")
    support_email = get_config_value(settings, constants.SUPPORT_EMAIL_KEY,
                                     "your IT support for this tool")
    with transaction.manager:
        if current_period.subperiod(location) != Period.ENTRY_SUBPERIOD:
            raise HTTPBadRequest(explanation="Can only send invite during "
                                 'the "Give feedback" period.')
        ext_user_details = ldapsource.get_ldap_user_by_email(email)
        if not ext_user_details:
            raise HTTPNotFound(explanation="%s is not a valid %s "
                               "email. If you think it is, "
                               "please contact "
                               "%s." % (email, company_name, support_email))

        ext_user = User.create_from_ldap_details(ldapsource, ext_user_details)
        invite = ExternalInvite(
            to_username=ext_user.username,
            from_username=request.user.username,
            period=current_period,
        )
        invites = (request.dbsession.query(ExternalInvite).filter(
            ExternalInvite.to_username == ext_user.username,
            ExternalInvite.from_username == request.user.username,
            ExternalInvite.period_id == current_period.id,
        ).one_or_none())
        if not invites:
            request.dbsession.add(invite)
    mail.send_invite_email(
        request.dbsession,
        request.registry.settings,
        inviter=request.user,
        invitee=ext_user,
    )
    return {"success": True}
Пример #6
0
def get_external_invite_status(request):
    """
    Generate list of existing external invites that `request.user` has already
    sent for the current period.

    Parameters
    ----------
    request: `pyramid.request.Request`

    Returns
    -------
    JSON-serialisable dict that contains list of external users invited.
    """
    ldapsource = request.ldapsource
    location = get_config_value(request.registry.settings,
                                constants.HOMEBASE_LOCATION_KEY)
    current_period = Period.get_current_period(request.dbsession)
    if current_period.subperiod(location) in [
            Period.APPROVAL_SUBPERIOD,
            Period.REVIEW_SUBPERIOD,
    ]:
        return interpolate_template(ENTRY_ENDED_TEMPLATE)
    elif current_period.subperiod(location) in [Period.ENROLLMENT_SUBPERIOD]:
        dt = date.datetimeformat(current_period.entry_start_utc, request.user)
        return interpolate_template(PRIOR_ENTRY_TEMPLATE, entry_start=dt)

    with transaction.manager:
        is_nominated = (request.dbsession.query(Nominee).filter(
            Nominee.period == current_period).filter(
                Nominee.username == request.user.username).one_or_none())

    if not is_nominated:
        return interpolate_template(NOT_ENROLLED_TEMPLATE)

    invitee_users = []
    with transaction.manager:
        invites = (request.dbsession.query(ExternalInvite.to_username).filter(
            ExternalInvite.from_username == request.user.username,
            ExternalInvite.period_id == current_period.id,
        ).all())
    for invitee in invites:
        ldap_details = ldapsource.get_ldap_user_by_username(invitee[0])
        invitee_users.append(
            User.create_from_ldap_details(ldapsource, ldap_details))

    invitee_users.sort(key=lambda x: x.first_name)

    payload_users = []
    for user in invitee_users:
        payload_users.append({
            "displayName": user.display_name,
            "businessUnit": user.business_unit,
            "department": user.department,
            "email": user.email,
        })
    return {"canInvite": True, "invitees": payload_users}
Пример #7
0
def nominate_everyone(ctx):
    engine = ctx.obj[ENGINE_KEY]
    session_factory = get_session_factory(engine)
    dbsession = get_tm_session(session_factory, transaction.manager)

    with transaction.manager:
        period = Period.get_current_period(dbsession)
        for user, nominee in (dbsession.query(User, Nominee).outerjoin(
                Nominee, Nominee.username == User.username).all()):
            if not nominee:
                dbsession.add(Nominee(user=user, period=period))
Пример #8
0
def get_summarised_users(dbsession):
    with transaction.manager:
        period = Period.get_current_period(dbsession)
        return (dbsession.query(User).options(joinedload("manager")).join(
            FeedbackForm,
            and_(
                FeedbackForm.to_username == User.username,
                FeedbackForm.period_id == period.id,
                FeedbackForm.is_summary == True,
            ),
        )  # noqa
                .all())
Пример #9
0
def generate_periods(ctx):
    engine = ctx.obj[ENGINE_KEY]
    session_factory = get_session_factory(engine)
    dbsession = get_tm_session(session_factory, transaction.manager)
    with transaction.manager:
        for i in range(1, 3):
            dates_dict = generate_period_dates(
                Period.INACTIVE_SUBPERIOD,
                lambda days: (datetime.utcnow() - timedelta(days=i * 30) +
                              timedelta(days=days)),
            )
            period = Period(name=u"Period %s" % i, template_id=1, **dates_dict)
            dbsession.add(period)
Пример #10
0
def _add_extra_periods(dbsession):
    with transaction.manager:
        for i in range(1, 3):
            dates_dict = generate_period_dates(
                Period.INACTIVE_SUBPERIOD,
                lambda days: (datetime.utcnow() - timedelta(days=i * 30) +
                              timedelta(days=days)),
            )
            period = Period(id=i * 1000,
                            name="Period %s" % i,
                            template_id=1,
                            **dates_dict)
            dbsession.add(period)
Пример #11
0
def get_non_nominated_users(dbsession):
    with transaction.manager:
        period = Period.get_current_period(dbsession)
        users = (
            dbsession.query(User).options(joinedload("nominations"),
                                          joinedload("manager")).filter(
                                              User.is_staff == True)  # noqa
            .all())
        return [
            u for u in users
            if period.id not in {n.period_id
                                 for n in u.nominations}
        ]
Пример #12
0
    def __init__(self, request):  # pylint disable=unused-argument
        """Pre-check that the `request.user` is allowed to give feedback
        to `request.matchdict['username']`."""
        location = get_config_value(request.registry.settings,
                                    constants.HOMEBASE_LOCATION_KEY)
        self.current_period = Period.get_current_period(
            request.dbsession,
            options=(joinedload("template").joinedload("rows").joinedload(
                "question")),
        )

        self.current_nominees = (request.dbsession.query(Nominee).options(
            joinedload("user")).filter(Nominee.period == self.current_period))

        if self.current_period.subperiod(location) != Period.ENTRY_SUBPERIOD:
            raise HTTPNotFound(explanation="Currently not in the entry "
                               "period.")

        self.to_username = request.matchdict["username"]
        self.from_username = request.user.username

        if self.to_username == self.from_username:
            raise HTTPNotFound(explanation="Cannot use feedback on self.")

        self.nominee = self.current_nominees.filter(
            Nominee.username == self.to_username).one_or_none()

        if not self.nominee:
            raise HTTPNotFound(explanation='Nominee "%s" does not exist.' %
                               self.to_username)

        if EXTERNAL_BUSINESS_UNIT_ROLE in request.effective_principals:
            exists = (request.dbsession.query(ExternalInvite).filter(
                ExternalInvite.from_username == self.to_username,
                ExternalInvite.to_username == self.from_username,
                ExternalInvite.period_id == self.current_period.id,
            ).one_or_none())
            if not exists:
                raise HTTPNotFound(explanation='User "%s" did not invite you '
                                   "for feedback." % self.to_username)

        self.form = (
            request.dbsession.query(FeedbackForm).options(
                joinedload("answers").joinedload("question")).filter(
                    FeedbackForm.period_id == self.current_period.id,
                    FeedbackForm.to_username == self.to_username,
                    FeedbackForm.from_username == self.from_username,
                    FeedbackForm.is_summary == False,
                )  # noqa
            .one_or_none())
Пример #13
0
def reset_email_flags(ctx):
    engine = ctx.obj[ENGINE_KEY]
    session_factory = get_session_factory(engine)
    dbsession = get_tm_session(session_factory, transaction.manager)
    with transaction.manager:
        period = Period.get_current_period(dbsession)
        print("resetting email flags for period {.name}")
        period.enrol_email_last_sent = None
        period.enrol_reminder_email_last_sent = None
        period.entry_email_last_sent = None
        period.entry_reminder_email_last_sent = None
        period.review_email_last_sent = None
        period.feedback_available_mail_last_sent = None
        dbsession.merge(period)
Пример #14
0
def adjust(ctx):
    subperiod = SUBPERIOD_CHOICES[ctx.obj[SUBPERIOD_KEY]]
    engine = ctx.obj[ENGINE_KEY]
    session_factory = get_session_factory(engine)
    dbsession = get_tm_session(session_factory, transaction.manager)

    dates_dict = generate_period_dates(
        subperiod, lambda days: datetime.utcnow() + timedelta(days=days))

    with transaction.manager:
        period = Period.get_current_period(dbsession)
        print("setting dates for period {.name} to {}".format(
            period, pformat(dates_dict)))
        for k, v in dates_dict.items():
            setattr(period, k, v)
        dbsession.merge(period)
Пример #15
0
def app_in_enrollment_subperiod(ldap_mocked_app_with_users):

    app = ldap_mocked_app_with_users
    dbsession = get_dbsession(app)

    # requires no nominations
    with transaction.manager:
        period = Period(
            id=TEST_PERIOD_ID,
            name=TEST_PERIOD_NAME,
            enrollment_start_utc=days_from_utcnow(-1),
            entry_start_utc=days_from_utcnow(1),
            approval_start_utc=days_from_utcnow(2),
            approval_end_utc=days_from_utcnow(3),
        )
        dbsession.add(period)
    return app
Пример #16
0
def test_employee_cannot_self_nominate_when_not_in_valid_enrollment_subperiod(
        ldap_mocked_app_with_users):  # noqa: E501
    dbsession = get_dbsession(ldap_mocked_app_with_users)

    with transaction.manager:
        period = Period(
            name=TEST_PERIOD_NAME,
            enrollment_start_utc=days_from_utcnow(-2),
            entry_start_utc=days_from_utcnow(-1),
            approval_start_utc=days_from_utcnow(2),
            approval_end_utc=days_from_utcnow(3),
        )
        dbsession.add(period)

    app = successfully_login(ldap_mocked_app_with_users,
                             TEST_EMPLOYEE_USERNAME)
    csrf_token = app.cookies[ANGULAR_2_XSRF_TOKEN_COOKIE_NAME]
    response = app.post(
        "/api/v1/self-nominate",
        headers={ANGULAR_2_XSRF_TOKEN_HEADER_NAME: csrf_token},
        expect_errors=True,
    )
    assert response.status_code == 404
Пример #17
0
def self_nominate(request):
    """
    If the current period cycle is in the enrollment state,
    update `request.user` status for the current period to ENROLLED.

    Returns
    -------
    JSON-serialisable payload that includes:
    * Message to display to `request.user` on their current nomination status
      for the current period.
    * Whether to display a button, with an associated URL and display text
    """
    location = get_config_value(request.registry.settings,
                                constants.HOMEBASE_LOCATION_KEY)
    current_period = Period.get_current_period(request.dbsession,
                                               options=joinedload("nominees"))
    if not current_period:
        raise HTTPNotFound(explanation="The feedback process is closed for "
                           "the meantime. Please contact your "
                           "manager for more details.")
    elif current_period.subperiod(location) != Period.ENROLLMENT_SUBPERIOD:
        display_end_date = current_period.entry_start_utc.strftime(
            constants.DEFAULT_DISPLAY_DATETIME_FORMAT)
        raise HTTPNotFound(explanation="The enrollment period closed on "
                           "%s" % display_end_date)

    username = request.user.username
    if username in (n.username for n in current_period.nominees):
        raise HTTPBadRequest(explanation="You are already enrolled "
                             "for the current period %s" % current_period.name)
    period_name = current_period.name
    with transaction.manager:
        request.dbsession.add(Nominee(period=current_period,
                                      username=username))
    return interpolate_template(ENROLLMENT_SUCCESS_TEMPLATE,
                                period_name=period_name)
Пример #18
0
    def __init__(self, request):  # pylint disable=unused-argument
        """
        Pre-checks to ensure that:
        * `request.user` can actually summarise the targetted user as
        specified in the URL.
        * Data for targetted user is consistent.
        """
        self.current_period = Period.get_current_period(
            request.dbsession,
            options=(joinedload("template").joinedload("rows").joinedload(
                "question")),
        )

        location = get_config_value(request.registry.settings,
                                    constants.HOMEBASE_LOCATION_KEY)
        if self.current_period.subperiod(location) not in [
                Period.APPROVAL_SUBPERIOD,
                Period.REVIEW_SUBPERIOD,
        ]:
            raise HTTPNotFound(explanation="Currently not in the approval or "
                               "review period.")

        current_nominees = (request.dbsession.query(Nominee).options(
            joinedload("user")).filter(Nominee.period == self.current_period))

        if TALENT_MANAGER_ROLE not in request.effective_principals:
            direct_reports_usernames = [
                u.username for u in request.user.direct_reports
            ]
            current_nominees = current_nominees.filter(
                Nominee.username.in_(direct_reports_usernames))

        if not current_nominees:
            raise HTTPNotFound(explanation="User did not nominate or you do "
                               "not manage them.")

        self.current_nominees = current_nominees

        self.to_username = request.matchdict["username"]
        self.from_username = request.user.username

        if self.to_username == self.from_username:
            raise HTTPNotFound(explanation="Cannot use feedback on self.")

        contributor_forms = (
            request.dbsession.query(FeedbackForm).options(
                joinedload("answers")).
            filter(FeedbackForm.to_username == self.to_username).filter(
                FeedbackForm.period_id == self.current_period.id).filter(
                    FeedbackForm.is_summary == False)  # noqa: E712,E501
            .all())

        log.debug("%s contributor forms found!" % len(contributor_forms))
        contributor_answers = defaultdict(list)
        for form in contributor_forms:
            answer_set = form.answers
            for answer in answer_set:
                if not answer.content:
                    log.warning("Content for answer id %s is empty" %
                                answer.id)
                else:
                    contributor_answers[answer.question_id].append(
                        answer.content)

        for answer_list in contributor_answers.values():
            shuffle(answer_list)

        self.contributor_answers = contributor_answers

        self.nominee = self.current_nominees.filter(
            Nominee.username == self.to_username).one_or_none()

        if not self.nominee:
            raise HTTPNotFound(explanation='Nominee "%s" does not exist.' %
                               self.to_username)

        summary_forms = (
            request.dbsession.query(FeedbackForm).options(
                joinedload("answers").joinedload("question")).filter(
                    FeedbackForm.period_id == self.current_period.id).filter(
                        FeedbackForm.to_username == self.to_username).filter(
                            FeedbackForm.is_summary == True)  # noqa: E712
            .all())

        if len(summary_forms) > 1:
            raise HTTPBadRequest(
                "More than 1 summary was found during period "
                '"%s" given to username "%s"' %
                (self.current_period.period_name, self.to_username))

        self.form = summary_forms[0] if len(summary_forms) else None
Пример #19
0
def test_send_emails_according_to_configured_location(
    ldap_mocked_app_with_users,  # noqa: E501
    ldapsource,
    subperiod,
    location,
    utc_offset_tuples,
):
    app = ldap_mocked_app_with_users
    dbsession = get_dbsession(app)
    # so date saved in database is TEST_UTCNOW
    freezer = freeze_time(TEST_UTCNOW)
    freezer.start()
    add_test_data_for_stats(dbsession, current_subperiod=subperiod, days_in=0)
    freezer.stop()
    # needs to be outside configured employees at a minimum
    settings = {
        "adaero.talent_manager_usernames": [TEST_OTHER_MANAGER_USERNAME],
        "adaero.served_on_https": True,
        "adaero.homebase_location": location,
        "adaero.enable_send_email": True,
    }

    with patch("smtplib.SMTP") as smtp_mock, patch(
            "socket.gethostname") as gethostname_mock, patch(
                "getpass.getuser") as getuser_mock:
        gethostname_mock.return_value = TEST_PRODUCTION_HOSTNAME
        sendmail_mock = smtp_mock().sendmail
        getuser_mock.return_value = TEST_PRODUCTION_USER
        date_ = TEST_UTCNOW.date()

        def check_sent(num, t):
            assert num == sendmail_mock.call_count, (
                "Incorrect number of emails sent for time %s" % t)

        for time_, num_sent in utc_offset_tuples:
            new_dt = datetime.combine(date_, time_)
            freezer = freeze_time(new_dt)

            freezer.start()
            mail.check_and_send_email(
                dbsession,
                ldapsource,
                settings,
                force=False,
                delay_s=0,
                delay_between_s=0,
            )
            check_sent(num_sent, time_)
            # email has already sent, won't send again
            mail.check_and_send_email(
                dbsession,
                ldapsource,
                settings,
                force=False,
                delay_s=0,
                delay_between_s=0,
            )
            check_sent(num_sent, time_)
            freezer.stop()
            sendmail_mock.reset_mock()

            period = Period.get_current_period(dbsession)
            period.enrol_email_last_sent = None
            period.enrol_reminder_email_last_sent = None
            period.entry_email_last_sent = None
            period.entry_reminder_email_last_sent = None
            period.review_email_last_sent = None
            period.feedback_available_mail_last_sent = None
            with transaction.manager:
                dbsession.merge(period)
Пример #20
0
def test_send_correct_emails_are_sent_during_subperiods(
    ldap_mocked_app_with_users,  # noqa: E501
    ldapsource,
    subperiod,
    num_times,
    last_sent_email_code,
    last_sent_template_key,
    force,
    last_users,
):
    app = ldap_mocked_app_with_users
    dbsession = get_dbsession(app)
    add_test_data_for_stats(dbsession, current_subperiod=subperiod)
    settings = {
        "adaero.load_talent_managers_on_app_start": False,
        "adaero.talent_manager_usernames": [TEST_TALENT_MANAGER_USERNAME],
        "adaero.served_on_https": True,
        "adaero.homebase_location": "London",
        "adaero.production_hostname": TEST_PRODUCTION_HOSTNAME,
        "adaero.production_user": TEST_PRODUCTION_USER,
        "adaero.enable_send_email": True,
    }

    with patch("smtplib.SMTP") as smtp_mock, patch(
            "socket.gethostname") as gethostname_mock, patch(
                "getpass.getuser") as getuser_mock:
        sendmail_mock = smtp_mock().sendmail
        gethostname_mock.return_value = TEST_PRODUCTION_HOSTNAME
        getuser_mock.return_value = TEST_PRODUCTION_USER
        for i in range(num_times):
            kwargs = {"force": force, "delay_s": 0, "delay_between_s": 0}
            if force:
                kwargs["template_key"] = last_sent_template_key

            mail.check_and_send_email(dbsession, ldapsource, settings,
                                      **kwargs)

            if i < num_times - 1:
                sendmail_mock.reset_mock()
        with transaction.manager:
            period = Period.get_current_period(dbsession)

            assert period.get_email_flag_by_code(
                last_sent_email_code) == TEST_UTCNOW
            for code in EMAIL_CODES.difference({last_sent_email_code}):
                assert period.get_email_flag_by_code(code) != TEST_UTCNOW

        if not len(last_users):
            assert 0 == len(sendmail_mock.call_args_list)
            return

        # TODO: why is email a list now?
        confirm_email_call = sendmail_mock.call_args_list.pop(-1)
        normal_emails_calls = sorted(sendmail_mock.call_args_list,
                                     key=lambda c: c[0][1][0])
        sorted_by_email_users = sorted(last_users, key=lambda v: v["mail"])
        app_link = "https://%s" % TEST_PRODUCTION_HOSTNAME

        assert len(normal_emails_calls) == len(
            normal_emails_calls), "Incorrect number of normal emails sent"

        parser = Parser()

        # normal_emails_calls = sorted_by_email_calls[:-1]
        # confirm_email_call = sorted_by_email_calls[-1]
        for u, c in zip(sorted_by_email_users, normal_emails_calls):
            args = c[0]
            assert u["mail"] == args[1][0]
            generated_raw_message = args[2]
            message_root = parser.parsestr(generated_raw_message)
            messages = message_root.get_payload()
            assert len(messages) == 2
            plain = b64decode(messages[0].get_payload()).decode("utf-8")
            html = b64decode(messages[1].get_payload()).decode("utf-8")
            # assert no non interpolated variables
            assert plain.count("{") == 0
            assert plain.count(u["givenName"]) > 0
            assert plain.count(app_link) > 0

            assert html.count("{") == 0
            assert html.count(u["givenName"]) > 0
            assert html.count(app_link) > 1
            assert html.count(app_link) % 2 == 0
            assert html.count("</html>") == 1

        num_normal_emails = len(normal_emails_calls)
        tm_details = TEST_LDAP_FULL_DETAILS[TEST_TALENT_MANAGER_USERNAME]
        assert tm_details["mail"] == confirm_email_call[0][1][0]

        # Deal with confirm email sent to talent managers
        confirm_raw_message = confirm_email_call[0][2]
        confirm_root = parser.parsestr(confirm_raw_message)
        confirm_messages = confirm_root.get_payload()
        assert len(confirm_messages) == 2

        confirm_plain = b64decode(
            confirm_messages[0].get_payload()).decode("utf-8")
        confirm_html = b64decode(
            confirm_messages[1].get_payload()).decode("utf-8")

        for m in [confirm_plain, confirm_html]:
            assert m.count(
                EMAIL_TEMPLATE_MAP[last_sent_template_key]["summary"]) == 1
            # exclude confirmation email from count
            assert m.count("%s email addresses" % num_normal_emails) == 1
        assert confirm_html.count("</html>") == 1
Пример #21
0
def generate_stats_payload_from_dataframe(df, dbsession, settings):
    """
    From the return of `build_stats_dataframe`, transfrom it into a
    dict that can be serialised into a JSON payload.

    Parameters
    ----------
    df: `pandas.Dataframe
    dbsession: `sqlalchemy.orm.session.Session`
    settings: `dict`
        Global settings that Pyramid generates from the ini file

    Returns
    -------
    JSON serialisable `dict`
    """
    # build payload row by row, laying out order according to display
    # on the frontend. this is to exchange frontend complexity for
    # backend complexity, given the app will be maintained by
    # backend-inclined developers
    location = get_config_value(settings, constants.HOMEBASE_LOCATION_KEY)
    current_period = Period.get_current_period(dbsession)
    with transaction.manager:
        asc_periods_by_date = (
            dbsession.query(Period.name)
            .order_by(asc(Period.enrollment_start_utc))
            .all()
        )
        asc_period_names = [p[0] for p in asc_periods_by_date]
    current_period_name = current_period.name
    current_subperiod = current_period.subperiod(location)

    stats_dict = defaultdict(list)
    for (
        username,
        first_name,
        last_name,
        _,
        period_name,
        contributed,
        received,
        has_summary,
    ) in df.sort_values(["start_date"]).values:
        current_user = stats_dict[username]

        # add display_name to beginning of the row
        if not len(current_user):
            display_name = " ".join([first_name, last_name])
            stats_dict[username].append(
                {"displayName": display_name, "username": username}
            )

        stats_dict[username].extend([contributed, received])

        if period_name == current_period_name:
            button = {
                "buttonText": "Review feedback",
                "username": username,
                "enable": True,
                "hasExistingSummary": False,
            }
            if current_subperiod not in [
                Period.APPROVAL_SUBPERIOD,
                Period.REVIEW_SUBPERIOD,
            ]:
                button["buttonText"] = "Not in approval or review period"
                button["enable"] = False
            elif received == -1:  # not nominated
                button["buttonText"] = "Not enrolled for feedback"
                button["enable"] = False
            elif has_summary:
                button["buttonText"] = "Review existing summary"
                button["enable"] = True
                button["hasExistingSummary"] = True
            stats_dict[username].append(button)

    # sort by display name
    ordered_dict = OrderedDict()
    for key, value in sorted(stats_dict.items(), key=lambda k_v: k_v[0]):
        ordered_dict[key] = value

    values = list(ordered_dict.values())
    payload = {
        "periods": asc_period_names,
        "periodColumns": ["Given", "Received"],
        "values": values,
    }

    return {"stats": payload}
Пример #22
0
def get_nominees(request):
    """
    Returns
    -------
    JSON-serialisable payload with filtered list of nominees that `request.user`
    can view for the current period. Each nominees has labelled data to help
    with categorising client-side.
    """
    location = get_config_value(request.registry.settings,
                                constants.HOMEBASE_LOCATION_KEY)
    current_period = Period.get_current_period(request.dbsession)
    if not current_period:
        return interpolate_template(FEEDBACK_ENDED_TEMPLATE)

    if current_period.subperiod(location) == Period.ENROLLMENT_SUBPERIOD:
        return interpolate_template(ENTRY_PENDING_TEMPLATE,
                                    period_name=current_period.name)

    if current_period.subperiod(location) != Period.ENTRY_SUBPERIOD:
        return interpolate_template(ENTRY_ENDED_TEMPLATE,
                                    period_name=current_period.name)

    own_username = request.user.username

    query = request.dbsession.query(User, func.count(FeedbackForm.id)).join(
        Nominee, User.username == Nominee.username)
    base = (
        query.outerjoin(
            FeedbackForm,
            and_(
                User.username == FeedbackForm.to_username,
                FeedbackForm.from_username == own_username,
                FeedbackForm.is_summary == False,  # noqa
                FeedbackForm.period_id == Nominee.period_id,
            ),
        ).filter(Nominee.username != own_username).filter(
            Nominee.period_id == current_period.id))

    # restrict users outside configured business unit to see only those
    # employees that invited them
    if EXTERNAL_BUSINESS_UNIT_ROLE in request.effective_principals:
        base = base.join(
            ExternalInvite,
            and_(
                ExternalInvite.to_username == own_username,
                ExternalInvite.period_id == current_period.id,
                User.username == ExternalInvite.from_username,
            ),
        )

    joined = base.group_by(User).order_by(asc(User.first_name)).all()

    payload = []
    for nominated_user, form in joined:
        if not nominated_user:
            continue
        manager = nominated_user.manager
        if manager:
            manager_display_name = " ".join(
                [manager.first_name, manager.last_name])
        else:
            manager_display_name = "-"
        payload.append({
            "username": nominated_user.username,
            "displayName": nominated_user.display_name,
            "department": nominated_user.department,
            "managerDisplayName": manager_display_name,
            "position": nominated_user.position,
            "hasExistingFeedback": True if form else False,
        })
    request.response.status_int = 200
    return {"period": current_period.name, "nominees": payload}
Пример #23
0
def check_and_send_email(
    dbsession,
    ldapsource,
    settings,
    template_key=None,
    force=False,
    delay_s=None,
    delay_between_s=None,
):
    """
    Check current conditions and if we haven't sent the relevant email, send
    templated both plain text and HTML content to all relevant email addresses
    using the configured SMTP server.

    Parameters
    ----------
    dbsession:
      sqlalchemy session
    ldapsource:
      used for fetching talent manager email information
    settings:
      configpaste settings
    template_key:
      override relevant email by providing key from
      `adaero.constants.EMAIL_TEMPLATE_MAP`
    force:
      if particular email already sent, send anyway
    delay_s:
      number of seconds to delay before sending emails. If none, look in
      settings
    delay_between_s:
      number of seconds to delay before sending emails. If none, look in
      settings
    """
    log.info("Begin: Sending emails")
    current_period = Period.get_current_period(dbsession)
    location = get_config_value(settings, constants.HOMEBASE_LOCATION_KEY)
    if template_key:
        template_info = constants.EMAIL_TEMPLATE_MAP[template_key]
        log.info("Email template overriden to %s" % template_info["code"])
    else:
        template_info = current_period.current_email_template(location)
        if not template_info:
            log.warning("Attempted to send an email while period is inactive")
            return

    last_sent = current_period.get_email_flag_by_code(template_info["code"])
    if not force and last_sent:
        log.warning("Email code %s already sent at %s so not doing again, "
                    "override with `force=True` kwarg." %
                    (template_info["code"], last_sent))
        return

    audience = template_info["audience"]
    company_name = get_config_value(settings, constants.COMPANY_NAME_KEY, "")
    subject = _build_full_subject(company_name, template_info["summary"])
    if audience == "employee":
        users = get_employee_users(dbsession)
    elif audience == "non-nominated":
        users = get_non_nominated_users(dbsession)
    elif audience == "manager":
        users = get_manager_users(dbsession)
    elif audience == "summarised":
        users = get_summarised_users(dbsession)
    else:
        raise ValueError('Audience value "%s" not in allowed values "%s". '
                         "Please alert the application maintainer." %
                         (audience, ", ".join(constants.AUDIENCE_VALUES)))

    emailing_enabled = _get_send_email_flag(settings)
    app_host = get_root_url(settings)

    # calculate email stats
    users_with_emails = []
    for user in users:
        if not user.email:
            log.warning(
                "Unable to send email for user %s as no email available" %
                user.username)
        else:
            users_with_emails.append(user)

    if delay_s is None:
        delay_s = float(
            get_config_value(settings, constants.EMAIL_START_DELAY_S_KEY,
                             DEFAULT_EMAIL_DELAY_S))
    if delay_between_s is None:
        delay_between_s = float(
            get_config_value(
                settings,
                constants.EMAIL_DELAY_BETWEEN_S_KEY,
                DEFAULT_EMAIL_DELAY_BETWEEN_S,
            ))

    log.info('Sending %s "%s" emails in %s seconds...' %
             (template_info["code"], len(users_with_emails), delay_s))
    time.sleep(delay_s)
    log.info("Sending %s emails now..." % len(users_with_emails))

    env = _build_template_env()
    have_sent_emails = False

    from_email = get_config_value(settings, constants.SUPPORT_EMAIL_KEY)

    s = smtplib.SMTP()
    s.connect()

    for user in users:
        if not user.email:
            continue
        try:
            # because of the modelling of User <-> Manager, attempting to fetch
            # manager directly despite being joinloaded will result in an SELECT
            # to prevent db access by testing against local manager_username
            template = env.get_template(
                os.path.join("email", template_info["template"]))
            rendered_html = template.render(
                user=user,
                period=current_period,
                app_host=app_host,
                company_name=company_name,
            )
            message_root = _generate_message_root(rendered_html, from_email,
                                                  subject)

            if emailing_enabled:
                s.sendmail(from_email, [user.email], message_root.as_string())
                have_sent_emails = True
            log.debug("Email sent to %s" % user.email)
        except Exception as e:
            log.exception(e)
            log.error("Exception occured with sending email to %s, "
                      "skipping over and continuing..." % user.email)
        time.sleep(delay_between_s)

    tm_usernames = settings[constants.TALENT_MANAGER_USERNAMES_KEY]
    if not isinstance(tm_usernames, list):
        talent_managers = json.loads(
            settings[constants.TALENT_MANAGER_USERNAMES_KEY])
    else:
        talent_managers = settings[constants.TALENT_MANAGER_USERNAMES_KEY]

    for tm_username in talent_managers:
        try:
            tm_ldap = ldapsource.get_ldap_user_by_username(tm_username)
            if not tm_ldap:
                log.warning("Unable to find LDAP info for talent manager with "
                            "username {}, unable to send confirmation "
                            "email.".format(tm_username))
                continue
            tm = User.create_from_ldap_details(ldapsource, tm_ldap)

            # send confirmation email
            template = env.get_template(
                os.path.join("email", "tm_confirmation.html.j2"))
            rendered_html = template.render(
                talent_manager=tm,
                subject=subject,
                num_emails=len(users_with_emails),
                datetime_sent_utc=datetime.utcnow(),
                app_host=app_host,
            )
            message_root = _generate_message_root(
                rendered_html,
                from_email,
                _build_full_subject(company_name, "Emails sent"),
            )
            if emailing_enabled and have_sent_emails:
                s.sendmail(from_email, [tm.email], message_root.as_string())
        except Exception as e:
            log.exception(e)
            log.error("Exception occured with sending tm email to %s, "
                      "skipping over and continuing..." % tm_username)

    s.close()

    log.info("Sent %s emails!" % (len(users_with_emails) + 1))
    with transaction.manager:
        current_period.set_email_flag_by_code(template_info["code"])
        dbsession.merge(current_period)
    log.info("End: Sending emails")