def send_decline_master_agreement_email(supplier_code): from app.api.services import (audit_service, audit_types, suppliers) supplier = suppliers.get_supplier_by_code(supplier_code) to_addresses = [ e['email_address'] for e in suppliers.get_supplier_contacts(supplier_code) ] # prepare copy email_body = render_email_template( 'seller_edit_decline.md', frontend_url=current_app.config['FRONTEND_ADDRESS']) subject = "You declined the new Master Agreement" send_or_handle_error( to_addresses, email_body, subject, current_app.config['DM_GENERIC_NOREPLY_EMAIL'], current_app.config['DM_GENERIC_SUPPORT_NAME'], event_description_for_errors='declined master agreement email') audit_service.log_audit_event( audit_type=audit_types.declined_master_agreement_email, user='', data={ "to_address": to_addresses, "email_body": email_body, "subject": subject }, db_object=supplier)
def create(current_user): lot = lots_service.find(slug='training2').one_or_none() framework = frameworks_service.find( slug='digital-marketplace').one_or_none() user = users.get(current_user.id) agency_name = '' email_domain = user.email_address.split('@')[1] agency = agency_service.find(domain=email_domain).one_or_none() if agency: agency_name = agency.name domain = domain_service.find( name='Training, Learning and Development').one_or_none() seller_category = None if domain: seller_category = str(domain.id) else: raise Exception('Training, Learning and Development domain not found') brief = briefs.create_brief(user, current_user.get_team(), framework, lot, data={ 'organisation': agency_name, 'sellerCategory': seller_category }) audit_service.log_audit_event(audit_type=audit_types.create_brief, user=current_user.email_address, data={'briefId': brief.id}, db_object=brief) return brief
def send_removed_team_member_notification_emails(team_id, user_ids): team = team_service.find(id=team_id).first() to_addresses = [] for user_id in user_ids: user = users.get(user_id) to_addresses.append(user.email_address) if len(to_addresses) == 0: return email_body = render_email_template( 'team_member_removed.md', frontend_url=current_app.config['FRONTEND_ADDRESS'], team_lead=escape_markdown(current_user.name), team_name=escape_markdown(team.name)) subject = 'You have been removed from the {} team'.format( team.name.encode('utf-8')) send_or_handle_error( to_addresses, email_body, subject, current_app.config['DM_GENERIC_NOREPLY_EMAIL'], current_app.config['DM_GENERIC_SUPPORT_NAME'], event_description_for_errors='team member removed email') audit_service.log_audit_event(audit_type=audit_types.team_member_removed, data={ 'to_address': to_addresses, 'subject': subject }, db_object=team, user='')
def send_seller_requested_feedback_from_buyer_email(brief): from app.api.services import audit_service, audit_types # to circumvent circular dependency to_addresses = get_brief_emails(brief) # prepare copy email_body = render_email_template( 'seller_requested_feedback_from_buyer_email.md', frontend_url=current_app.config['FRONTEND_ADDRESS'], brief_name=brief.data['title'], brief_id=brief.id ) subject = "Buyer notifications to unsuccessful sellers" send_or_handle_error( to_addresses, email_body, subject, current_app.config['DM_GENERIC_NOREPLY_EMAIL'], current_app.config['DM_GENERIC_SUPPORT_NAME'], event_description_for_errors='seller_requested_feedback_from_buyer_email' ) audit_service.log_audit_event( audit_type=audit_types.seller_requested_feedback_from_buyer_email, user='', data={ "to_addresses": ', '.join(to_addresses), "email_body": email_body, "subject": subject }, db_object=brief)
def publish_answer(current_user, brief_id, data): brief = briefs.get(brief_id) if not brief: raise NotFoundError("Invalid brief id '{}'".format(brief_id)) if not briefs.has_permission_to_brief(current_user.id, brief.id): raise UnauthorisedError('Unauthorised to publish answer') publish_question = data.get('question') if not publish_question: raise ValidationError('Question is required') answer = data.get('answer') if not answer: raise ValidationError('Answer is required') brief_clarification_question = brief_clarification_question_service.save( BriefClarificationQuestion(_brief_id=brief.id, question=publish_question, answer=answer, user_id=current_user.id)) question_id = data.get('questionId') if question_id: question = brief_question_service.get(question_id) if question.brief_id == brief.id: question.answered = True brief_question_service.save(question) audit_service.log_audit_event( audit_type=audit_types.create_brief_clarification_question, user=current_user.email_address, data={'briefId': brief.id}, db_object=brief_clarification_question)
def close_opportunity_early(user_id, brief_id): brief = brief_service.get(brief_id) if not brief: raise NotFoundError('Opportunity {} does not exist'.format(brief_id)) if not brief_service.has_permission_to_brief(user_id, brief_id): raise UnauthorisedError( 'Not authorised to close opportunity {}'.format(brief_id)) if not can_close_opportunity_early(brief): raise BriefError('Unable to close opportunity {}'.format(brief_id)) user = users.get(user_id) if not user: raise NotFoundError('User {} does not exist'.format(user_id)) brief = brief_service.close_opportunity_early(brief) create_responses_zip(brief.id) send_opportunity_closed_early_email(brief, user) try: audit_service.log_audit_event( audit_type=audit_types.close_opportunity_early, data={'briefId': brief.id}, db_object=brief, user=user.email_address) publish_tasks.brief.delay(publish_tasks.compress_brief(brief), 'closed_early', email_address=user.email_address, name=user.name) except Exception as e: rollbar.report_exc_info() return brief
def send_seller_requested_feedback_from_buyer_email(brief): from app.api.services import audit_service, audit_types # to circumvent circular dependency to_addresses = [user.email_address for user in brief.users if user.active] # prepare copy email_body = render_email_template( 'seller_requested_feedback_from_buyer_email.md', frontend_url=current_app.config['FRONTEND_ADDRESS'], brief_name=brief.data['title'], brief_id=brief.id ) subject = "Buyer notifications to unsuccessful sellers" send_or_handle_error( to_addresses, email_body, subject, current_app.config['DM_GENERIC_NOREPLY_EMAIL'], current_app.config['DM_GENERIC_SUPPORT_NAME'], event_description_for_errors='seller_requested_feedback_from_buyer_email' ) audit_service.log_audit_event( audit_type=audit_types.seller_requested_feedback_from_buyer_email, user='', data={ "to_addresses": ', '.join(to_addresses), "email_body": email_body, "subject": subject }, db_object=brief)
def create_team(): user = users.get(current_user.id) created_teams = team_service.get_teams_for_user(user.id, 'created') completed_teams = team_service.get_teams_for_user(user.id) if len(completed_teams) == 0: if len(created_teams) == 0: team = team_service.save( Team(name='', status='created', team_members=[ TeamMember(user_id=user.id, is_team_lead=True) ])) audit_service.log_audit_event(audit_type=audit_types.create_team, data={}, db_object=team, user=current_user.email_address) publish_tasks.team.delay(publish_tasks.compress_team(team), 'created') return get_team(team.id) created_team = created_teams.pop() return get_team(created_team.id) else: team = completed_teams[0] raise TeamError( 'You can only be in one team. You\'re already a member of {}.'. format(team.name))
def decline_agreement(user_info): supplier_code = user_info.get('supplier_code') email_address = user_info.get('email_address') supplier = suppliers.get_supplier_by_code(supplier_code) mandatory_supplier_checks(supplier) if email_address != supplier.data.get('email'): raise UnauthorisedError('Unauthorised to decline agreement') supplier_users = users.find(supplier_code=supplier_code).all() supplier.status = 'deleted' for user in supplier_users: user.active = False users.add_to_session(user) users.commit_changes() suppliers.save(supplier) send_decline_master_agreement_email(supplier.code) publish_tasks.supplier.delay(publish_tasks.compress_supplier(supplier), 'declined_agreement', updated_by=email_address) audit_service.log_audit_event( audit_type=audit_types.declined_master_agreement, user=email_address, data={ 'supplierCode': supplier.code, 'supplierData': supplier.data }, db_object=supplier)
def send_brief_clarification_to_seller(brief, brief_question, to_address): from app.api.services import (audit_service, audit_types ) # to circumvent circular dependency # prepare copy email_body = render_email_template( 'brief_question_to_seller.md', frontend_url=current_app.config['FRONTEND_ADDRESS'], brief_id=brief.id, brief_name=escape_markdown(brief.data.get('title')), brief_organisation=brief.data.get('organisation'), publish_by_date=brief.questions_closed_at.strftime('%d/%m/%Y'), message=escape_markdown(brief_question.data.get('question'))) subject = u"You submitted a question for {} ({}) successfully".format( brief.data.get('title'), brief.id) send_or_handle_error( to_address, email_body, subject, current_app.config['DM_GENERIC_NOREPLY_EMAIL'], current_app.config['DM_GENERIC_SUPPORT_NAME'], event_description_for_errors='brief question email sent to seller') audit_service.log_audit_event( audit_type=audit_types.sent_brief_question_to_seller, user='', data={ "to_addresses": to_address, "email_body": email_body, "subject": subject }, db_object=brief)
def post_brief_response(brief_id): brief_response_json = get_json_from_request() supplier, brief = _can_do_brief_response(brief_id) try: brief_response = BriefResponse( data=brief_response_json, supplier=supplier, brief=brief ) brief_response.validate() db.session.add(brief_response) db.session.flush() except ValidationError as e: brief_response_json['brief_id'] = brief_id rollbar.report_exc_info(extra_data=brief_response_json) message = "" if 'essentialRequirements' in e.message and e.message['essentialRequirements'] == 'answer_required': message = "Essential requirements must be completed" del e.message['essentialRequirements'] if 'attachedDocumentURL' in e.message: if e.message['attachedDocumentURL'] == 'answer_required': message = "Documents must be uploaded" if e.message['attachedDocumentURL'] == 'file_incorrect_format': message = "Uploaded documents are in the wrong format" del e.message['attachedDocumentURL'] if 'criteria' in e.message and e.message['criteria'] == 'answer_required': message = "Criteria must be completed" if len(e.message) > 0: message += json.dumps(e.message) return jsonify(message=message), 400 except Exception as e: brief_response_json['brief_id'] = brief_id rollbar.report_exc_info(extra_data=brief_response_json) return jsonify(message=e.message), 400 try: send_brief_response_received_email(supplier, brief, brief_response) except Exception as e: brief_response_json['brief_id'] = brief_id rollbar.report_exc_info(extra_data=brief_response_json) audit_service.log_audit_event( audit_type=AuditTypes.create_brief_response, user=current_user.email_address, data={ 'briefResponseId': brief_response.id, 'briefResponseJson': brief_response_json, }, db_object=brief_response) publish_tasks.brief_response.delay( publish_tasks.compress_brief_response(brief_response), 'submitted', user=current_user.email_address ) return jsonify(briefResponses=brief_response.serialize()), 201
def send_opportunity_withdrawn_email_to_buyers(brief, current_user): # to circumvent circular dependencies from app.api.business.brief import brief_business from app.api.services import audit_service, audit_types to_addresses = get_brief_emails(brief) seller_message = '' invited_seller_codes = brief.data.get('sellers', {}).keys() if brief_business.is_open_to_all(brief): seller_message = 'We have notified sellers who have drafted or submitted responses to this opportunity' elif len(invited_seller_codes) == 1: invited_seller_code = invited_seller_codes.pop() seller_name = brief.data['sellers'][invited_seller_code]['name'] seller_message = '{} has been notified'.format(seller_name) else: seller_message = 'All invited sellers have been notified' email_body = render_email_template( 'opportunity_withdrawn_buyers.md', brief_id=brief.id, framework=brief.framework.slug, frontend_url=current_app.config['FRONTEND_ADDRESS'], seller_message=escape_markdown(seller_message), title=escape_markdown(brief.data['title']), user=escape_markdown(current_user.name), withdrawal_reason=escape_markdown(brief.data['reasonToWithdraw']) ) subject = "'{}' ({}) is withdrawn from the Digital Marketplace".format( brief.data['title'], brief.id ) send_or_handle_error( to_addresses, email_body, subject, current_app.config['DM_GENERIC_NOREPLY_EMAIL'], current_app.config['DM_GENERIC_SUPPORT_NAME'], event_description_for_errors=audit_types.withdraw_opportunity ) audit_service.log_audit_event( audit_type=audit_types.sent_opportunity_withdrawn_email_to_buyers, user='', data={ "to_addresses": ', '.join(to_addresses), "email_body": email_body, "subject": subject }, db_object=brief )
def send_brief_closed_email(brief): from app.api.services import audit_service, audit_types # to circumvent circular dependency from app.tasks.s3 import create_resumes_zip, CreateResumesZipException brief_email_sent_audit_event = audit_service.find( type=audit_types.sent_closed_brief_email.value, object_type="Brief", object_id=brief.id).count() if (brief_email_sent_audit_event > 0): return # create the resumes zip has_resumes_zip = False try: create_resumes_zip(brief.id) has_resumes_zip = True except Exception as e: rollbar.report_exc_info() pass to_addresses = [ user.email_address for user in brief.users if user.active and user.email_address.endswith('@digital.gov.au') ] # prepare copy email_body = render_email_template( 'brief_closed.md', frontend_url=current_app.config['FRONTEND_ADDRESS'], brief_name=brief.data['title'], brief_id=brief.id, has_resumes_zip=has_resumes_zip) subject = "Your brief has closed - please review all responses." send_or_handle_error(to_addresses, email_body, subject, current_app.config['DM_GENERIC_NOREPLY_EMAIL'], current_app.config['DM_GENERIC_SUPPORT_NAME'], event_description_for_errors='brief closed') audit_service.log_audit_event( audit_type=audit_types.sent_closed_brief_email, user='', data={ "to_addresses": ', '.join(to_addresses), "email_body": email_body, "subject": subject }, db_object=brief)
def send_specialist_brief_response_withdrawn_email(supplier, brief, brief_response, supplier_user=None): from app.api.services import audit_service, audit_types # to circumvent circular dependency to_address = brief_response.data['respondToEmailAddress'] specialist_name = '{} {}'.format( brief_response.data.get('specialistGivenNames', ''), brief_response.data.get('specialistSurname', '') ) subject = "{}'s response to '{}' ({}) has been withdrawn".format( specialist_name, brief.data['title'], brief.id ) brief_url = '{}/2/{}/opportunities/{}'.format( current_app.config['FRONTEND_ADDRESS'], brief.framework.slug, brief.id ) email_body = render_email_template( 'specialist_brief_response_withdrawn.md', specialist_name=specialist_name, brief_url=brief_url, brief_name=brief.data['title'], brief_id=brief.id, frontend_url=current_app.config['FRONTEND_ADDRESS'], brief_organisation=brief.data['organisation'], supplier_user=supplier_user ) send_or_handle_error( to_address, email_body, subject, current_app.config['DM_GENERIC_NOREPLY_EMAIL'], current_app.config['DM_GENERIC_SUPPORT_NAME'], event_description_for_errors='brief response withdrawn' ) audit_service.log_audit_event( audit_type=audit_types.specialist_brief_response_withdrawn_email, user='', data={ "to_address": to_address, "email_body": email_body, "subject": subject }, db_object=brief_response)
def update(agency_id, agency, updated_by): existing = agency_service.get_agency_for_update(agency_id) if agency.get('name'): existing.name = agency.get('name') if agency.get('category'): existing.category = agency.get('category') if agency.get('bodyType'): existing.body_type = agency.get('bodyType') if agency.get('whitelisted', None) is not None: existing.whitelisted = agency.get('whitelisted') if agency.get('reports', None) is not None: existing.reports = agency.get('reports') if agency.get('must_join_team', None) is not None: existing.must_join_team = agency.get('must_join_team') if agency.get('state'): existing.state = agency.get('state') if agency.get('domains', None) is not None: domains = agency.get('domains', []) to_remove = [] to_add = [] for e in existing.domains: if e.domain not in domains: to_remove.append(e) for d in domains: if d not in [e.domain for e in existing.domains]: to_add.append(AgencyDomain(active=True, domain=d)) for e in to_remove: existing.domains.remove(e) for e in to_add: existing.domains.append(e) updated = agency_service.save(existing) result = get_agency(updated.id) audit_service.log_audit_event(audit_type=audit_types.agency_updated, user=updated_by, data={ 'incoming': agency, 'saved': result }, db_object=updated) return result
def send_specialist_brief_closed_email(brief): from app.api.services import ( audit_service, audit_types, brief_responses_service ) # to circumvent circular dependency if brief.lot.slug != 'specialist': return audit_event = audit_service.find(type=audit_types.specialist_brief_closed_email.value, object_type="Brief", object_id=brief.id).count() if (audit_event > 0): return responses = brief_responses_service.get_brief_responses(brief.id, None, submitted_only=True) to_addresses = get_brief_emails(brief) # prepare copy email_body = render_email_template( 'specialist_brief_closed.md', frontend_url=current_app.config['FRONTEND_ADDRESS'], brief_name=brief.data['title'], brief_id=brief.id, number_of_responses='{}'.format(len(responses)), number_of_responses_plural='s' if len(responses) > 1 else '' ) subject = 'Your "{}" opportunity has closed.'.format(brief.data['title']) send_or_handle_error( to_addresses, email_body, subject, current_app.config['DM_GENERIC_NOREPLY_EMAIL'], current_app.config['DM_GENERIC_SUPPORT_NAME'], event_description_for_errors='brief closed' ) audit_service.log_audit_event( audit_type=audit_types.specialist_brief_closed_email, user='', data={ "to_addresses": ', '.join(to_addresses), "email_body": email_body, "subject": subject }, db_object=brief)
def create_atm_brief(): """Create ATM brief (role=buyer) --- tags: - brief definitions: ATMBriefCreated: type: object properties: id: type: number lot: type: string status: type: string author: type: string responses: 200: description: Brief created successfully. schema: $ref: '#/definitions/ATMBriefCreated' 400: description: Bad request. 403: description: Unauthorised to create ATM brief. 500: description: Unexpected error. """ try: lot = lots_service.find(slug='atm').one_or_none() framework = frameworks_service.find(slug='digital-marketplace').one_or_none() user = users.get(current_user.id) brief = briefs.create_brief(user, framework, lot) except Exception as e: rollbar.report_exc_info() return jsonify(message=e.message), 400 try: audit_service.log_audit_event( audit_type=AuditTypes.create_brief, user=current_user.email_address, data={ 'briefId': brief.id }, db_object=brief) except Exception as e: rollbar.report_exc_info() return jsonify(brief.serialize(with_users=False))
def send_notify_auth_rep_email(supplier_code): from app.api.services import ( audit_service, audit_types, suppliers, key_values_service ) supplier = suppliers.get_supplier_by_code(supplier_code) to_address = supplier.data.get('email', '').encode('utf-8') agreement = get_new_agreement() if agreement is None: agreement = get_current_agreement() start_date = pendulum.now('Australia/Canberra').date() if agreement: start_date = agreement.start_date.in_tz('Australia/Canberra') # prepare copy email_body = render_email_template( 'seller_edit_notify_auth_rep.md', ma_start_date=start_date.strftime('%-d %B %Y'), supplier_name=supplier.name, supplier_code=supplier.code, auth_rep_name=escape_markdown(supplier.data.get('representative', '')), frontend_url=current_app.config['FRONTEND_ADDRESS'] ) subject = "Accept the Master Agreement for the Digital Marketplace" send_or_handle_error( to_address, email_body, subject, current_app.config['DM_GENERIC_NOREPLY_EMAIL'], current_app.config['DM_GENERIC_SUPPORT_NAME'], event_description_for_errors='notify auth rep email' ) audit_service.log_audit_event( audit_type=audit_types.notify_auth_rep_accept_master_agreement, user='', data={ "to_address": to_address, "email_body": email_body, "subject": subject }, db_object=supplier)
def withdraw_opportunity(user_id, brief_id, withdrawal_reason): brief = brief_service.get(brief_id) if not brief: raise NotFoundError('Opportunity {} does not exist'.format(brief_id)) if not brief_service.has_permission_to_brief(user_id, brief_id): raise UnauthorisedError( 'Not authorised to withdraw opportunity {}'.format(brief_id)) if brief.status != 'live': raise BriefError('Unable to withdraw opportunity {}'.format(brief_id)) if not withdrawal_reason: raise ValidationError( 'Withdrawal reason is required for opportunity {}'.format( brief_id)) user = users.get(user_id) if not user: raise NotFoundError('User {} does not exist'.format(user_id)) brief = brief_service.withdraw_opportunity(brief, withdrawal_reason) organisation = agency_service.get_agency_name(user.agency_id) sellers_to_contact = brief_service.get_sellers_to_notify( brief, brief_business.is_open_to_all(brief)) for email_address in sellers_to_contact: send_opportunity_withdrawn_email_to_seller(brief, email_address, organisation) send_opportunity_withdrawn_email_to_buyers(brief, user) try: audit_service.log_audit_event( audit_type=audit_types.withdraw_opportunity, data={'briefId': brief.id}, db_object=brief, user=user.email_address) publish_tasks.brief.delay(publish_tasks.compress_brief(brief), 'withdrawn', email_address=user.email_address, name=user.name) except Exception as e: rollbar.report_exc_info() return brief
def notify_auth_rep(user_info): supplier_code = user_info.get('supplier_code') email_address = user_info.get('email_address') supplier = suppliers.get_supplier_by_code(supplier_code) mandatory_supplier_checks(supplier) send_notify_auth_rep_email(supplier.code) audit_service.log_audit_event( audit_type=audit_types.notify_auth_rep_accept_master_agreement, user=email_address, data={ 'supplierCode': supplier.code, 'supplierData': supplier.data }, db_object=supplier)
def send_specialist_brief_seller_invited_email(brief, invited_supplier): from app.api.services import audit_service, audit_types # to circumvent circular dependency if brief.lot.slug != 'specialist': return to_addresses = [] if 'contact_email' in invited_supplier.data: to_addresses = [invited_supplier.data['contact_email']] elif 'email' in invited_supplier.data: to_addresses = [invited_supplier.data['email']] if len(to_addresses) > 0: number_of_suppliers = int(brief.data['numberOfSuppliers']) email_body = render_email_template( 'specialist_brief_invite_seller.md', frontend_url=current_app.config['FRONTEND_ADDRESS'], brief_name=brief.data['title'], brief_id=brief.id, brief_organisation=brief.data['organisation'], brief_close_date=brief.closed_at.strftime('%d/%m/%Y'), question_close_date=brief.questions_closed_at.strftime('%d/%m/%Y'), number_of_suppliers=number_of_suppliers, number_of_suppliers_plural='s' if number_of_suppliers > 1 else '' ) subject = "You're invited to submit candidates for {}".format(brief.data['title']) send_or_handle_error( to_addresses, email_body, subject, current_app.config['DM_GENERIC_NOREPLY_EMAIL'], current_app.config['DM_GENERIC_SUPPORT_NAME'], event_description_for_errors='seller_invited_to_specialist_opportunity' ) audit_service.log_audit_event( audit_type=audit_types.seller_invited_to_specialist_opportunity, user='', data={ "to_addresses": ', '.join(to_addresses), "email_body": email_body, "subject": subject }, db_object=brief)
def send_team_member_notification_emails(team_id, user_ids=None): team = team_service.find(id=team_id).first() if user_ids is None or len(user_ids) == 0: # Team members added through the create flow members = team_member_service.find(team_id=team_id, is_team_lead=False).all() else: # Team members added through the edit flow members = team_member_service.get_team_members_by_user_id( team_id, user_ids) to_addresses = [] for member in members: user = users.get(member.user_id) to_addresses.append(user.email_address) if len(to_addresses) == 0: return email_body = render_email_template( 'team_member_added.md', frontend_url=current_app.config['FRONTEND_ADDRESS'], team_lead=escape_markdown(current_user.name), team_name=escape_markdown(team.name)) subject = '{} added you as a member of {}'.format( current_user.name, team.name.encode('utf-8')) send_or_handle_error( to_addresses, email_body, subject, current_app.config['DM_GENERIC_NOREPLY_EMAIL'], current_app.config['DM_GENERIC_SUPPORT_NAME'], event_description_for_errors='team member added email') audit_service.log_audit_event(audit_type=audit_types.team_member_added, data={ 'to_address': to_addresses, 'subject': subject }, db_object=team, user='')
def send_opportunity_edited_email_to_seller(brief, email_address, buyer): # to circumvent circular dependencies from app.api.services import audit_service, audit_types candidate_message = '' if brief.lot.slug == 'specialist': candidate_message = "candidate's " formatted_closing_date = ( brief.closed_at.in_timezone('Australia/Canberra').format('%A %-d %B %Y at %-I:%M%p (in Canberra)') ) email_body = render_email_template( 'opportunity_edited_sellers.md', brief_id=brief.id, buyer=buyer, candidate_message=candidate_message, closing_date=formatted_closing_date, framework=brief.framework.slug, frontend_url=current_app.config['FRONTEND_ADDRESS'], title=escape_markdown(brief.data['title']) ) subject = "Changes made to '{}' opportunity".format(brief.data['title']) send_or_handle_error( email_address, email_body, subject, current_app.config['DM_GENERIC_NOREPLY_EMAIL'], current_app.config['DM_GENERIC_SUPPORT_NAME'], event_description_for_errors=audit_types.opportunity_edited ) audit_service.log_audit_event( audit_type=audit_types.sent_opportunity_edited_email_to_seller, user='', data={ "to_addresses": email_address, "email_body": email_body, "subject": subject }, db_object=brief )
def send_brief_closed_email(brief): from app.api.services import audit_service, audit_types # to circumvent circular dependency if brief.lot.slug in ['specialist']: return brief_email_sent_audit_event = audit_service.find(type=audit_types.sent_closed_brief_email.value, object_type="Brief", object_id=brief.id).count() if (brief_email_sent_audit_event > 0): return to_addresses = get_brief_emails(brief) # prepare copy email_body = render_email_template( 'brief_closed.md', frontend_url=current_app.config['FRONTEND_ADDRESS'], brief_name=brief.data['title'], brief_id=brief.id ) subject = "Your opportunity has closed - please review all responses." send_or_handle_error( to_addresses, email_body, subject, current_app.config['DM_GENERIC_NOREPLY_EMAIL'], current_app.config['DM_GENERIC_SUPPORT_NAME'], event_description_for_errors='brief closed' ) audit_service.log_audit_event( audit_type=audit_types.sent_closed_brief_email, user='', data={ "to_addresses": ', '.join(to_addresses), "email_body": email_body, "subject": subject }, db_object=brief)
def send_request_access_email(permission): if not current_user.is_part_of_team(): return user_team = current_user.get_team() team = team_service.find(id=user_team.get('id'), status='completed').one_or_none() if not team: return to_addresses = [ tm.user.email_address for tm in team.team_members if tm.is_team_lead ] if len(to_addresses) == 0: return email_body = render_email_template( 'request_access.md', frontend_url=current_app.config['FRONTEND_ADDRESS'], name=escape_markdown(current_user.name), permission=escape_markdown(permission)) subject = '{} has requested a change to their permissions'.format( current_user.name) send_or_handle_error(to_addresses, email_body, subject, current_app.config['DM_GENERIC_NOREPLY_EMAIL'], current_app.config['DM_GENERIC_SUPPORT_NAME'], event_description_for_errors='request access email') audit_service.log_audit_event(audit_type=audit_types.sent_request_access, data={ 'to_address': to_addresses, 'subject': subject, 'email_body': email_body }, db_object=team, user=current_user.email_address)
def delete_draft_evidence(evidence_id, actioned_by): evidence = evidence_service.get_evidence_by_id(evidence_id) if not evidence or not evidence.status == 'draft': return False evidence_service.delete(evidence) audit_service.log_audit_event( audit_type=audit_types.evidence_draft_deleted, user=actioned_by, data={ "id": evidence.id, "domainId": evidence.domain_id, "briefId": evidence.brief_id, "status": evidence.status, "supplierCode": evidence.supplier_code, "data": evidence.data }, db_object=evidence ) return True
def send_team_lead_notification_emails(team_id, user_ids=None): team = team_service.find(id=team_id).first() if user_ids is None or len(user_ids) == 0: # Team leads added through the create flow team_leads = team_member_service.find(team_id=team_id, is_team_lead=True).all() team_leads = [ team_lead for team_lead in team_leads if team_lead.user_id != current_user.id ] else: # Team leads added through the edit flow team_leads = team_member_service.get_team_leads_by_user_id( team_id, user_ids) to_addresses = [] for team_lead in team_leads: user = users.get(team_lead.user_id) to_addresses.append(user.email_address) if len(to_addresses) == 0: return email_body = render_email_template( 'team_lead_added.md', frontend_url=current_app.config['FRONTEND_ADDRESS'], team_name=escape_markdown(team.name)) subject = 'You have been upgraded to a team lead' send_or_handle_error(to_addresses, email_body, subject, current_app.config['DM_GENERIC_NOREPLY_EMAIL'], current_app.config['DM_GENERIC_SUPPORT_NAME'], event_description_for_errors='team lead added email') audit_service.log_audit_event(audit_type=audit_types.team_lead_added, data={'to_address': to_addresses}, db_object=team, user='')
def send_decline_request_to_join_team_leaders_email(team_id, requester_name, requester_email, reason): team = team_service.find(id=team_id, status='completed').one_or_none() if not team: return to_addresses = [ tm.user.email_address for tm in team.team_members if tm.is_team_lead ] if len(to_addresses) == 0: return email_body = render_email_template( 'request_to_join_declined.md', lead_name=escape_markdown(current_user.name), requester_name=escape_markdown(requester_name), requester_email=escape_markdown(requester_email), reason=escape_markdown(reason), team=escape_markdown(team.name)) extra = '' if requester_name.endswith('s') else 's' subject = "{}'{} request to join {} has been declined".format( requester_name, extra, team.name) send_or_handle_error(to_addresses, email_body, subject, current_app.config['DM_GENERIC_NOREPLY_EMAIL'], current_app.config['DM_GENERIC_SUPPORT_NAME'], event_description_for_errors='request to join email') audit_service.log_audit_event( audit_type=audit_types.sent_request_to_join_team_decline, data={ 'to_address': to_addresses, 'subject': subject, 'email_body': email_body }, db_object=team, user=current_user.email_address)
def update_supplier(data, user_info): supplier_code = user_info.get('supplier_code') email_address = user_info.get('email_address') supplier = suppliers.find(code=supplier_code).one_or_none() mandatory_supplier_checks(supplier) whitelist_fields = ['representative', 'email', 'phone'] for wf in whitelist_fields: if wf not in data.get('data'): raise ValidationError('{} is not recognised'.format(wf)) if 'email' in data.get('data'): email = data['data']['email'] data['data']['email'] = email.encode('utf-8').lower() supplier.update_from_json(data.get('data')) messages = SupplierValidator(supplier).validate_representative( 'representative') if len([m for m in messages if m.get('severity', '') == 'error']) > 0: raise ValidationError(',\n'.join([ m.get('message') for m in messages if m.get('severity', '') == 'error' ])) suppliers.save(supplier) publish_tasks.supplier.delay(publish_tasks.compress_supplier(supplier), 'updated', updated_by=email_address) audit_service.log_audit_event(audit_type=audit_types.update_supplier, user=email_address, data={ 'supplierCode': supplier.code, 'supplierData': supplier.data }, db_object=supplier) process_auth_rep_email(supplier, data, user_info)
def send_brief_closed_email(brief): from app.api.services import audit_service, audit_types # to circumvent circular dependency brief_email_sent_audit_event = audit_service.find(type=audit_types.sent_closed_brief_email.value, object_type="Brief", object_id=brief.id).count() if (brief_email_sent_audit_event > 0): return to_addresses = [user.email_address for user in brief.users if user.active] # prepare copy email_body = render_email_template( 'brief_closed.md', frontend_url=current_app.config['FRONTEND_ADDRESS'], brief_name=brief.data['title'], brief_id=brief.id ) subject = "Your brief has closed - please review all responses." send_or_handle_error( to_addresses, email_body, subject, current_app.config['DM_GENERIC_NOREPLY_EMAIL'], current_app.config['DM_GENERIC_SUPPORT_NAME'], event_description_for_errors='brief closed' ) audit_service.log_audit_event( audit_type=audit_types.sent_closed_brief_email, user='', data={ "to_addresses": ', '.join(to_addresses), "email_body": email_body, "subject": subject }, db_object=brief)
def send_request_to_join_team_leaders_email(team_id, token): team = team_service.find(id=team_id, status='completed').one_or_none() if not team: return to_addresses = [ tm.user.email_address for tm in team.team_members if tm.is_team_lead ] if len(to_addresses) == 0: return email_body = render_email_template( 'request_to_join.md', frontend_url=current_app.config['FRONTEND_ADDRESS'], name=escape_markdown(current_user.name), requester_email=escape_markdown(current_user.email_address), team=escape_markdown(team.name), team_id=team.id, token=token) subject = '{} has requested to join your team'.format(current_user.name) send_or_handle_error(to_addresses, email_body, subject, current_app.config['DM_GENERIC_NOREPLY_EMAIL'], current_app.config['DM_GENERIC_SUPPORT_NAME'], event_description_for_errors='request to join email') audit_service.log_audit_event( audit_type=audit_types.sent_request_to_join_team, data={ 'to_address': to_addresses, 'subject': subject, 'email_body': email_body }, db_object=team, user=current_user.email_address)
def send_seller_invited_to_training_email(brief, invited_supplier): from app.api.services import audit_service, audit_types # to circumvent circular dependency if brief.lot.slug != 'training2': return to_addresses = [] if 'contact_email' in invited_supplier.data: to_addresses = [invited_supplier.data['contact_email']] elif 'email' in invited_supplier.data: to_addresses = [invited_supplier.data['email']] if len(to_addresses) > 0: email_body = render_email_template( 'brief_training_invite_seller.md', frontend_url=current_app.config['FRONTEND_ADDRESS'], brief_name=brief.data['title'], brief_id=brief.id ) subject = "You have been invited to respond to an opportunity" send_or_handle_error( to_addresses, email_body, subject, current_app.config['DM_GENERIC_NOREPLY_EMAIL'], current_app.config['DM_GENERIC_SUPPORT_NAME'], event_description_for_errors='seller_invited_to_training_opportunity' ) audit_service.log_audit_event( audit_type=audit_types.seller_invited_to_training_opportunity, user='', data={ "to_addresses": ', '.join(to_addresses), "email_body": email_body, "subject": subject }, db_object=brief)
def send_opportunity_closed_early_email(brief, current_user): # to circumvent circular dependencies from app.api.services import audit_service, audit_types to_addresses = get_brief_emails(brief) supplier_code, seller = next(iter(brief.data.get('sellers', {}).items())) email_body = render_email_template( 'opportunity_closed_early.md', brief_id=brief.id, framework=brief.framework.slug, frontend_url=current_app.config['FRONTEND_ADDRESS'], possessive="'" if seller['name'].lower().endswith('s') else "'s", seller_name=escape_markdown(seller['name']), title=escape_markdown(brief.data['title']), user=escape_markdown(current_user.name) ) subject = "'{}' has been closed early".format(brief.data['title']) send_or_handle_error( to_addresses, email_body, subject, current_app.config['DM_GENERIC_NOREPLY_EMAIL'], current_app.config['DM_GENERIC_SUPPORT_NAME'], event_description_for_errors=audit_types.close_opportunity_early ) audit_service.log_audit_event( audit_type=audit_types.sent_opportunity_closed_early_email, user='', data={ "to_addresses": ', '.join(to_addresses), "email_body": email_body, "subject": subject }, db_object=brief )
def send_brief_clarification_to_buyer(brief, brief_question, supplier): from app.api.services import ( audit_service, audit_types ) # to circumvent circular dependency to_addresses = get_brief_emails(brief) # prepare copy email_body = render_email_template( 'brief_question_to_buyer.md', frontend_url=current_app.config['FRONTEND_ADDRESS'], brief_id=brief.id, brief_name=escape_markdown(brief.data.get('title')), publish_by_date=brief.closed_at.strftime('%d/%m/%Y'), message=escape_markdown(brief_question.data.get('question')), supplier_name=escape_markdown(supplier.name) ) subject = "You received a new question for ‘{}’".format(brief.data.get('title')) send_or_handle_error( to_addresses, email_body, subject, current_app.config['DM_GENERIC_NOREPLY_EMAIL'], current_app.config['DM_GENERIC_SUPPORT_NAME'], event_description_for_errors='brief question email sent to buyer' ) audit_service.log_audit_event( audit_type=audit_types.sent_brief_question_to_buyer, user='', data={ "to_addresses": ', '.join(to_addresses), "email_body": email_body, "subject": subject }, db_object=brief)
def send_seller_invited_to_rfx_email(brief, invited_supplier): from app.api.services import audit_service, audit_types # to circumvent circular dependency to_addresses = [] if 'contact_email' in invited_supplier.data: to_addresses = [invited_supplier.data['contact_email']] elif 'email' in invited_supplier.data: to_addresses = [invited_supplier.data['email']] if len(to_addresses) > 0: email_body = render_email_template( 'brief_rfx_invite_seller.md', frontend_url=current_app.config['FRONTEND_ADDRESS'], brief_name=brief.data['title'], brief_id=brief.id ) subject = "You have been invited to respond to an opportunity" send_or_handle_error( to_addresses, email_body, subject, current_app.config['DM_GENERIC_NOREPLY_EMAIL'], current_app.config['DM_GENERIC_SUPPORT_NAME'], event_description_for_errors='seller_invited_to_rfx_opportunity' ) audit_service.log_audit_event( audit_type=audit_types.seller_invited_to_rfx_opportunity, user='', data={ "to_addresses": ', '.join(to_addresses), "email_body": email_body, "subject": subject }, db_object=brief)
def update_brief(brief_id): """Update RFX brief (role=buyer) --- tags: - brief definitions: RFXBrief: type: object properties: title: type: string organisation: type: string location: type: array items: type: string summary: type: string industryBriefing: type: string sellerCategory: type: string sellers: type: object attachments: type: array items: type: string requirementsDocument: type: array items: type: string responseTemplate: type: array items: type: string evaluationType: type: array items: type: string proposalType: type: array items: type: string evaluationCriteria: type: array items: type: object includeWeightings: type: boolean closedAt: type: string contactNumber: type: string startDate: type: string contractLength: type: string contractExtensions: type: string budgetRange: type: string workingArrangements: type: string securityClearance: type: string parameters: - name: brief_id in: path type: number required: true - name: body in: body required: true schema: $ref: '#/definitions/RFXBrief' responses: 200: description: Brief updated successfully. schema: $ref: '#/definitions/RFXBrief' 400: description: Bad request. 403: description: Unauthorised to update RFX brief. 404: description: Brief not found. 500: description: Unexpected error. """ brief = briefs.get(brief_id) if not brief: not_found("Invalid brief id '{}'".format(brief_id)) if brief.status != 'draft': abort('Cannot edit a {} brief'.format(brief.status)) if brief.lot.slug not in ['rfx', 'atm']: abort('Brief lot not supported for editing') if current_user.role == 'buyer': brief_user_ids = [user.id for user in brief.users] if current_user.id not in brief_user_ids: return forbidden('Unauthorised to update brief') data = get_json_from_request() publish = False if 'publish' in data and data['publish']: del data['publish'] publish = True if brief.lot.slug == 'rfx': # validate the RFX JSON request data errors = RFXDataValidator(data).validate(publish=publish) if len(errors) > 0: abort(', '.join(errors)) if brief.lot.slug == 'atm': # validate the ATM JSON request data errors = ATMDataValidator(data).validate(publish=publish) if len(errors) > 0: abort(', '.join(errors)) if brief.lot.slug == 'rfx' and 'evaluationType' in data: if 'Written proposal' not in data['evaluationType']: data['proposalType'] = [] if 'Response template' not in data['evaluationType']: data['responseTemplate'] = [] if brief.lot.slug == 'rfx' and 'sellers' in data and len(data['sellers']) > 0: data['sellerSelector'] = 'someSellers' if len(data['sellers']) > 1 else 'oneSeller' data['areaOfExpertise'] = '' if brief.lot.slug == 'atm' and 'openTo' in data: if data['openTo'] == 'all': data['sellerSelector'] = 'allSellers' data['sellerCategory'] = '' elif data['openTo'] == 'category': data['sellerSelector'] = 'someSellers' brief_domain = ( domain_service.get_by_name_or_id(int(data['sellerCategory'])) if data['sellerCategory'] else None ) if brief_domain: data['areaOfExpertise'] = brief_domain.name previous_status = brief.status if publish: brief.publish(closed_at=data['closedAt']) if 'sellers' in brief.data and data['sellerSelector'] != 'allSellers': for seller_code, seller in brief.data['sellers'].iteritems(): supplier = suppliers.get_supplier_by_code(seller_code) if brief.lot.slug == 'rfx': send_seller_invited_to_rfx_email(brief, supplier) try: brief_url_external = '{}/2/digital-marketplace/opportunities/{}'.format( current_app.config['FRONTEND_ADDRESS'], brief_id ) _notify_team_brief_published( brief.data['title'], brief.data['organisation'], current_user.name, current_user.email_address, brief_url_external ) except Exception as e: pass brief.data = data briefs.save_brief(brief) if publish: brief_url_external = '{}/2/digital-marketplace/opportunities/{}'.format( current_app.config['FRONTEND_ADDRESS'], brief_id ) publish_tasks.brief.delay( publish_tasks.compress_brief(brief), 'published', previous_status=previous_status, name=current_user.name, email_address=current_user.email_address, url=brief_url_external ) try: audit_service.log_audit_event( audit_type=AuditTypes.update_brief, user=current_user.email_address, data={ 'briefId': brief.id, 'briefData': brief.data }, db_object=brief) except Exception as e: rollbar.report_exc_info() return jsonify(brief.serialize(with_users=False))
def send_document_expiry_campaign(client, sellers): folder_id = getenv('MAILCHIMP_MARKETPLACE_FOLDER_ID') if not folder_id: raise MailChimpConfigException('Failed to get MAILCHIMP_MARKETPLACE_FOLDER_ID from the environment variables.') list_id = getenv('MAILCHIMP_SELLER_LIST_ID') if not list_id: raise MailChimpConfigException('Failed to get MAILCHIMP_SELLER_LIST_ID from the environment variables.') title = 'Expiring documents - {}'.format(pendulum.today().to_date_string()) sent_expiring_documents_audit_event = audit_service.filter( AuditEvent.type == audit_types.sent_expiring_documents_email.value, AuditEvent.data['campaign_title'].astext == title ).one_or_none() if (sent_expiring_documents_audit_event > 0): return conditions = [] for seller in sellers: for email_address in seller['email_addresses']: conditions.append({ 'condition_type': 'EmailAddress', 'op': 'is', 'field': 'EMAIL', 'value': email_address }) recipients = { 'list_id': list_id, 'segment_opts': { 'match': 'any', 'conditions': conditions } } settings = { 'folder_id': folder_id, 'preview_text': 'Please update your documents', 'subject_line': 'Your documents are soon to expire or have expired', 'title': title } campaign = create_campaign(client, recipients, settings) template = template_env.get_template('mailchimp_document_expiry.html') email_body = template.render(current_year=pendulum.today().year) update_campaign_content(client, campaign['id'], email_body) schedule_campaign(client, campaign['id'], pendulum.now('Australia/Sydney').at(10, 0, 0).in_timezone('UTC')) audit_service.log_audit_event( audit_type=audit_types.sent_expiring_documents_email, data={ 'campaign_title': title, 'sellers': sellers }, db_object=None, user=None )