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
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)
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"])
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)
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}
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}
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))
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())
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)
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)
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} ]
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())
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)
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)
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
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
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)
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
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)
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
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}
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}
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")