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!")
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 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 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, ))
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
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, )
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)
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 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")
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
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)
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)
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)
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 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)
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, }
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, } }
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 _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, }, }
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, )
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 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, }
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, )
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 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")
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 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 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 } }