Пример #1
0
def includeme(config):
    """Pyramid convention that allows invocation of a function prior to
    server start and is found through `config.scan` in the main function"""
    scheduler = BackgroundScheduler()
    settings = config.get_settings()

    _get_send_email_flag(settings)

    log.info("Hostname in emails will be set to %s" % get_root_url(settings))

    should_run = bool(
        get_config_value(settings, constants.RUN_EMAIL_INTERVAL_JOB_KEY))
    if not should_run:
        log.info("Setting %s is false, not running email job." %
                 constants.RUN_EMAIL_INTERVAL_JOB_KEY)
        return

    log.info("Setting up email scheduler...")
    interval_s = int(
        get_config_value(settings, constants.CHECK_AND_SEND_EMAIL_INT_KEY))

    if not interval_s:
        msg = ("Settings %s is not set! Please set and restart the "
               "application" % constants.CHECK_AND_SEND_EMAIL_INT_KEY)
        raise ValueError(constants.MISCONFIGURATION_MESSAGE.format(error=msg))

    scheduler.add_job(email_job,
                      trigger="interval",
                      args=(settings, ),
                      seconds=interval_s)
    scheduler.add_listener(email_event_handler,
                           EVENT_JOB_EXECUTED | EVENT_JOB_ERROR)
    scheduler.start()
    log.info("Email scheduling setup completed!")
Пример #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 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}
Пример #4
0
def redirect_logo(request):
    logo_filename = get_config_value(request.registry.settings,
                                     constants.LOGO_FILENAME_KEY, "logo.png")
    is_https = get_config_value(request.registry.settings,
                                constants.SERVED_ON_HTTPS_KEY)
    raise HTTPFound(
        request.route_url(
            "catchall_static",
            subpath="assets/%s" % str(logo_filename),
            _scheme="https" if is_https else None,
        ))
Пример #5
0
def get_root_url(settings):
    hostname = get_config_value(settings, constants.DISPLAYED_HOSTNAME_KEY)
    if not hostname:
        hostname = socket.gethostname()
    if get_config_value(settings, constants.SERVED_ON_HTTPS_KEY):
        root_url = "https://%s" % hostname
    else:
        # when not running https, likely to require using ip
        port = get_config_value(settings, constants.FRONTEND_SERVER_PORT_KEY,
                                4200)
        root_url = "http://%s:%s" % (socket.gethostname(), port)
    return root_url
Пример #6
0
def includeme(config):
    """
    Initialize the model for a Pyramid app.
    Activate this setup using ``config.include('adaero.models')``.
    """
    settings = config.get_settings()

    talent_manager_usernames_string = get_config_value(
        settings, constants.TALENT_MANAGER_USERNAMES_KEY)
    if not talent_manager_usernames_string:
        raise ValueError(
            MISCONFIGURATION_MESSAGE.format(
                error="Talent manager usernames are not set"))
    talent_managers = json.loads(talent_manager_usernames_string)
    settings[constants.TALENT_MANAGER_USERNAMES_KEY] = talent_managers

    location = get_config_value(settings, constants.HOMEBASE_LOCATION_KEY)
    if not location:
        raise ValueError(
            MISCONFIGURATION_MESSAGE.format(
                error="Homebase location is not set"))

    # should_load_users = get_config_value(settings,
    #                                      constants
    #                                      .RELOAD_USERS_ON_APP_START_KEY,
    #                                      False)
    should_load_tms = get_config_value(
        settings, constants.LOAD_TALENT_MANAGERS_ON_APP_START_KEY, True)
    engine = get_engine(settings)

    for seq in SEQUENCES:
        seq.create(engine)
    Base.metadata.create_all(engine)

    session_factory = get_session_factory(engine)
    config.registry["dbsession_factory"] = session_factory

    dbsession = get_tm_session(session_factory, transaction.manager)
    ldapsource = ldapauth.build_ldapauth_from_settings(settings)
    if should_load_tms:
        load_talent_managers_only(dbsession, ldapsource, settings)

    # make request.dbsession available for use in Pyramid
    config.add_request_method(
        lambda request: get_tm_session(session_factory, transaction.manager),
        "dbsession",
        reify=True,
    )
Пример #7
0
def includeme(config):

    settings = config.get_settings()

    if get_config_value(settings, constants.ALLOW_PASSWORDLESS_ACCESS_KEY):
        log.warning(
            "PASSWORDLESS ACCESS IS ENABLED (has been set in "
            "config %s or envvar %s)" % (
                constants.ALLOW_PASSWORDLESS_ACCESS_KEY,
                get_envvar_name(constants.ALLOW_PASSWORDLESS_ACCESS_KEY),
            ))

    authn_policy = SimpleAuthenticationPolicy(callback=None)
    config.set_authentication_policy(authn_policy)
    config.set_authorization_policy(ACLAuthorizationPolicy())
    config.set_default_csrf_options(require_csrf=True,
                                    header=ANGULAR_2_XSRF_TOKEN_HEADER_NAME)
    config.add_request_method(request_user_callback, "user", reify=True)
    config.add_request_method(request_ldapauth_callback,
                              "ldapsource",
                              reify=True)

    config.add_request_method(lambda: check_if_production(settings),
                              "is_production",
                              reify=True)
    setup_cors(config)
Пример #8
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)
Пример #9
0
def load_talent_managers_only(
        dbsession,
        ldapsource,
        settings  # type: Session  # type: ldapauth.LDAPAuth
):
    custom_user_list_string = get_config_value(
        settings, constants.LOAD_USER_EMAIL_LIST_KEY)
    is_production = check_if_production(settings)
    email_usernames = []
    if custom_user_list_string:
        log.warning("Custom user list provided = %s, so storing emails for "
                    "these users in DB." % custom_user_list_string)
        email_usernames = json.loads(custom_user_list_string)
    elif is_production:
        log.warning("No custom user list provided and in production, "
                    "storing all user emails")
    else:
        log.warning("No custom user list provided, so not storing "
                    "emails to prevent spam.")
    use_email_list = is_production and len(
        email_usernames) or not is_production

    with transaction.manager:
        talent_managers = find_talent_managers(settings, ldapsource, {})
        new_tms = []
        for tm in talent_managers:
            user = dbsession.query(User).get(tm[ldapsource.username_key])
            if not user:
                new_tms.append(tm)
        _create_users(dbsession, ldapsource, new_tms, use_email_list,
                      email_usernames, None)
    log.info("Finished syncing Users LDAP cache")
Пример #10
0
def _get_send_email_flag(settings):
    # never send actualy email unless explicitly stated
    send_emails = get_config_value(settings, constants.ENABLE_SEND_EMAIL_KEY)
    if send_emails:
        log.info("Enable send email is set, will run `sendmail`")
    else:
        log.info("Enable send email is not set, will not run `sendmail`")
    return send_emails
Пример #11
0
def login(request):
    username = request.json_body["username"]
    password = request.json_body["password"]
    if not get_config_value(
            request.registry.settings, ALLOW_PASSWORDLESS_ACCESS_KEY
    ) and not request.ldapsource.auth_user(username, password):
        raise HTTPUnauthorized
    remember(request, username)
    return _build_user_data_response(request, username)
Пример #12
0
def setup_cors(config):
    settings = config.get_settings()
    allow_origin_string = get_config_value(settings,
                                           constants.CORS_ALLOW_ORIGIN_KEY)
    if allow_origin_string:
        log.warning("CORS enabled. Access-Control-Allow-Origin will be "
                    "restricted to %s" % allow_origin_string)
        config.add_subscriber(add_cors_callback_builder(allow_origin_string),
                              NewRequest)
Пример #13
0
def test_get_config_value(envvar, envvar_val, key, expected):
    configuration = pyramid.testing.setUp(settings=DEFAULT_SETTINGS)
    settings = configuration.get_settings()
    if envvar:
        os.environ[envvar] = envvar_val
    generated = config.get_config_value(settings, key)
    assert expected == generated
    if envvar:
        os.environ.pop(envvar)
Пример #14
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}
Пример #15
0
def put_summary(context, request):
    current_period = context.current_period
    location = get_config_value(request.registry.settings,
                                constants.HOMEBASE_LOCATION_KEY)

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

    return update_feedback_form(context, request, True)
Пример #16
0
def get_talent_manager_page_data(request):
    """
    Returns
    -------
    JSON-serialisable payload with data that personalises the talent manager
    panel as well as communicate current user count.
    """
    with transaction.manager:
        user_count = len(request.dbsession.query(User).all())
    settings = request.registry.settings
    generate_population_msg = get_config_value(
        settings, constants.TM_GENERATE_POPULATION_MSG_KEY)
    upload_new_population_msg = get_config_value(
        settings, constants.TM_UPLOAD_NEW_POPULATION_MSG_KEY)
    return {
        "userCount": user_count,
        "generatePopulationMsg": generate_population_msg,
        "uploadNewPopulationMsg": upload_new_population_msg,
    }
Пример #17
0
def get_metadata(request):
    """
    Return data that can be used to personalize the current user's UI
    """
    is_pwl_access = bool(
        get_config_value(request.registry.settings,
                         constants.ALLOW_PASSWORDLESS_ACCESS_KEY))
    unit_name = get_config_value(request.registry.settings,
                                 constants.BUSINESS_UNIT_KEY)
    login_password_message = get_config_value(request.registry.settings,
                                              constants.LOGIN_PASSWORD_MSG_KEY)
    login_username_message = get_config_value(request.registry.settings,
                                              constants.LOGIN_USERNAME_MSG_KEY)
    support_email = get_config_value(request.registry.settings,
                                     constants.SUPPORT_EMAIL_KEY)
    display_name = get_config_value(request.registry.settings,
                                    constants.COMPANY_NAME_KEY)
    return {
        "metadata": {
            "businessUnit": unit_name,
            "displayName": display_name,
            "loginPasswordMessage": login_password_message,
            "loginUsernameMessage": login_username_message,
            "passwordlessAccess": is_pwl_access,
            "supportEmail": support_email,
        }
    }
Пример #18
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())
Пример #19
0
def _build_user_data_response(request, username):
    request.response.status_int = 200
    request.response.set_cookie(ANGULAR_2_XSRF_TOKEN_COOKIE_NAME,
                                request.session.get_csrf_token())
    unit_name = get_config_value(request.registry.settings, BUSINESS_UNIT_KEY)
    return {
        "success": True,
        "data": {
            "displayName": request.user.display_name,
            "title": request.user.position,
            "principals": request.effective_principals,
            "businessUnit": unit_name,
        },
    }
Пример #20
0
def prepare_db(settings):
    db_url = config.get_config_value(
        settings, constants.DB_URL_KEY, raise_if_not_set=True
    )
    log.info("Connecting to DB %s", db_url)
    return create_engine(
        db_url,
        pool_size=5,
        max_overflow=40,
        echo_pool=True,
        pool_recycle=300,
        poolclass=QueuePool,
        echo=False,
    )
Пример #21
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)
Пример #22
0
def build_feedback_payload(context, request, is_summary):
    """
    Generate a JSON-serialisable dict that contains the `request.user`'s
    feedback form for the target `context.nominee`. If there is a
    `context.form` (which contains existing answers), use that, or else,
    build a blank form from the current period's configuration.

    Parameters
    ----------
    context
    request
    is_summary

    Returns
    -------
    A dict that is a JSON-serializable payload for display in the Web UI
    """
    form = context.form
    current_period = context.current_period
    nominee = context.nominee
    location = get_config_value(
        request.registry.settings, constants.HOMEBASE_LOCATION_KEY
    )
    items = []
    ordered_questions = sorted(
        [q for q in current_period.template.rows], key=lambda x: x.position
    )
    if is_summary:
        end_date_utc = current_period.approval_end_utc
        read_only = not current_period.subperiod(location) == Period.APPROVAL_SUBPERIOD
    else:
        end_date_utc = current_period.approval_start_utc
        read_only = not current_period.subperiod(location) == Period.ENTRY_SUBPERIOD
    end_date = datetimeformat(end_date_utc, nominee.user)

    if form:
        log.debug("existing form %s found" % form.id)
        answers_by_q_id = {f.question_id: f for f in form.answers}
        ordered_rows = [answers_by_q_id[r.question.id] for r in ordered_questions]
    else:
        log.debug("no existing form found. generating a new one for display")
        ordered_rows = ordered_questions

    for row in ordered_rows:
        raw_answer = None
        if is_summary:

            def p(x):
                return x

            answers = [
                p(text) for text in sorted(context.contributor_answers[row.question.id])
            ]
            raw_answer = "\n".join(answers)
            answer = raw_answer if not form else row.content
        else:
            answer = "" if not form else row.content
        answer = answer if answer is not None else ""
        items.append(
            {
                "questionId": row.question.id,
                "caption": row.question.caption,
                "question": row.question.question_template.format(
                    period_name=current_period.name,
                    display_name=nominee.user.display_name,
                ),
                "rawAnswer": raw_answer,
                "answerId": row.id if form else None,
                "answer": answer,
            }
        )

    return {
        "employee": {
            "displayName": nominee.user.display_name,
            "position": nominee.user.position,
        },
        "periodName": current_period.name,
        "endDate": end_date,
        "readOnly": read_only,
        "items": items,
    }
Пример #23
0
def build_ldapauth_from_settings(settings):
    ldap_uri = get_config_value(settings,
                                constants.LDAP_URI_KEY,
                                raise_if_not_set=True)
    user_bind_template = get_config_value(
        settings, constants.LDAP_USER_BIND_TEMPLATE_KEY)
    search_username = get_config_value(settings,
                                       constants.LDAP_SEARCH_BIND_DN_KEY,
                                       raise_if_not_set=True)
    search_password = get_config_value(settings,
                                       constants.LDAP_SEARCH_PASSWORD_KEY,
                                       raise_if_not_set=True)
    username_key = get_config_value(settings,
                                    constants.LDAP_USERNAME_KEY,
                                    raise_if_not_set=True)
    manager_key = get_config_value(settings,
                                   constants.LDAP_MANAGER_KEY,
                                   raise_if_not_set=True)
    location_key = get_config_value(settings,
                                    constants.LDAP_LOCATION_KEY,
                                    raise_if_not_set=True)
    uid_key = get_config_value(settings,
                               constants.LDAP_UID_KEY,
                               raise_if_not_set=True)
    department_key = get_config_value(settings,
                                      constants.LDAP_DEPARTMENT_KEY,
                                      raise_if_not_set=True)
    business_unit_key = get_config_value(settings,
                                         constants.LDAP_BUSINESS_UNIT_KEY,
                                         raise_if_not_set=True)
    base_dn = get_config_value(settings,
                               constants.LDAP_BASE_DN_KEY,
                               raise_if_not_set=True)
    dn_username_attr = get_config_value(
        settings,
        constants.LDAP_DN_USERNAME_ATTRIBUTE_KEY,
        raise_if_not_set=True)
    dn_username_regex = get_config_value(settings,
                                         constants.LDAP_DN_USERNAME_REGEX_KEY,
                                         raise_if_not_set=True)

    return LDAPAuth(
        ldap_uri,
        user_bind_template,
        search_username,
        search_password,
        username_key,
        manager_key,
        location_key,
        uid_key,
        base_dn,
        dn_username_attr,
        dn_username_regex,
        department_key,
        business_unit_key,
    )
Пример #24
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}
Пример #25
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")
Пример #26
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
Пример #27
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}
Пример #28
0
def fetch_feedback_history(dbsession, username, settings, fetch_full=False):
    """
    Parameters
    ----------
    dbsession: `sqlalchemy.session.Session`
    username: `str`
        Username of user to fetch the feedback for
    settings: `dict`
        Global settings that Pyramid generates from the ini file
    fetch_full: `bool`
        If `False`, only fetch the latest
        `constants.MANAGER_VIEW_HISTORY_LIMIT` feedbacks

    Returns
    -------
    JSON-serialisable payload that contains the feedback history of
    provided user.
    """
    location = get_config_value(settings, constants.HOMEBASE_LOCATION_KEY)
    q = dbsession.query(Period, FeedbackForm, Nominee)
    with transaction.manager:
        user = dbsession.query(User).get(username)
        history = (
            q.outerjoin(
                FeedbackForm,
                and_(
                    FeedbackForm.period_id == Period.id,
                    FeedbackForm.to_username == username,
                    FeedbackForm.is_summary == True,
                ),
            )  # noqa
            .outerjoin(
                Nominee,
                and_(Period.id == Nominee.period_id,
                     Nominee.username == username),
            ).order_by(desc(Period.enrollment_start_utc)))

        if fetch_full:
            history = history.all()
        else:
            history = history.limit(constants.MANAGER_VIEW_HISTORY_LIMIT)

        feedbacks = []
        for period, summary_form, nominee in history:
            if period.subperiod(location) != Period.REVIEW_SUBPERIOD:
                feedbacks.append({
                    "periodDescription": "%s pending" % period.name,
                    "enable": False,
                    "items": [],
                })
            elif not summary_form and not nominee:
                feedbacks.append({
                    "periodDescription":
                    "Did not request feedback for period "
                    "%s" % period.name,
                    "enable":
                    False,
                    "items": [],
                })
            elif not summary_form:
                feedbacks.append({
                    "periodDescription":
                    "No feedback available for period %s" % period.name,
                    "enable":
                    False,
                    "items": [],
                })
            else:
                feedback = {
                    "periodDescription": period.name,
                    "enable": True,
                    "items": [],
                }
                ordered_questions = sorted([qu for qu in period.template.rows],
                                           key=lambda x: x.position)
                answers_by_q_id = {
                    f.question_id: f
                    for f in summary_form.answers
                }
                ordered_rows = [
                    answers_by_q_id[r.question.id] for r in ordered_questions
                ]
                for answer in ordered_rows:
                    item = {
                        "question":
                        answer.question.question_template.format(
                            display_name=user.display_name,
                            period_name=period.name),
                        "answer":
                        answer.content,
                    }
                    feedback["items"].append(item)
                feedbacks.append(feedback)

        return {
            "feedback": {
                "displayName": user.display_name,
                "items": feedbacks
            }
        }