def get_question(current_user, brief_id, question_id): brief = briefs.find(id=brief_id).one_or_none() 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') question = brief_question_service.find(id=question_id, brief_id=brief.id).one_or_none() question_result = None if question: question_result = {"id": question.id, "data": question.data} return { "question": question_result, "brief": { "title": brief.data.get('title'), "id": brief.id, "lot": brief.lot.slug, "questionsCloseAt": brief.questions_closed_at, "closedAt": brief.closed_at, "internalReference": brief.data.get('internalReference'), "isOpenToAll": brief_business.is_open_to_all(brief), "status": brief.status } }
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 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 get_answers(brief_id): brief = briefs.find(id=brief_id).one_or_none() if not brief: raise NotFoundError("Invalid brief id '{}'".format(brief_id)) answers = brief_clarification_question_service.get_answers(brief_id) return { "answers": answers, "brief": { "title": brief.data.get('title'), "id": brief.id, "closedAt": brief.closed_at, "internalReference": brief.data.get('internalReference'), "isOpenToAll": brief_business.is_open_to_all(brief) }, "questionCount": get_counts(brief_id, answers=answers) }
def get_opportunity_to_edit(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 edit opportunity {}'.format(brief_id)) if brief.status != 'live': raise BriefError('Unable to edit opportunity {}'.format(brief_id)) domains = [] for domain in domain_service.get_active_domains(): domains.append({'id': str(domain.id), 'name': domain.name}) return { 'brief': brief.serialize(with_users=False), 'domains': domains, 'isOpenToAll': brief_business.is_open_to_all(brief) }
def get_opportunity_history(brief_id, show_documents=False, include_sellers=True): brief = brief_service.get(brief_id) if not brief: raise NotFoundError('Opportunity {} does not exist'.format(brief_id)) response = { 'brief': { 'framework': brief.framework.slug, 'id': brief.id, 'title': brief.data['title'] if 'title' in brief.data else '' } } edits = [] changes = brief_history_service.get_edits(brief.id) for i, change in enumerate(changes): source = brief if i == 0 else changes[i - 1] edit_data = get_changes_made_to_opportunity(source, change) if edit_data: edit_data['editedAt'] = change.edited_at if not include_sellers and 'sellers' in edit_data: del edit_data['sellers'] if not brief_business.is_open_to_all(brief) and not show_documents: if 'attachments' in edit_data: del edit_data['attachments'] if 'requirementsDocument' in edit_data: del edit_data['requirementsDocument'] if 'responseTemplate' in edit_data: del edit_data['responseTemplate'] edits.append(edit_data) response['edits'] = edits return response
def edit_opportunity(user_id, brief_id, edits): 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 edit opportunity {}'.format(brief_id)) if brief.status != 'live': raise BriefError('Unable to edit opportunity {}'.format(brief_id)) user = user_service.get(user_id) if not user: raise NotFoundError('User {} does not exist'.format(user_id)) previous_data = copy.deepcopy(brief.data) previous_data['closed_at'] = brief.closed_at.to_iso8601_string( extended=True) edit_title(brief, edits['title']) edit_summary(brief, edits['summary']) edit_closing_date(brief, edits['closingDate']) if 'documentsEdited' in edits and edits['documentsEdited']: if 'attachments' in edits: edit_attachments(brief, edits['attachments']) if 'requirementsDocument' in edits and 'requirementsDocument' in brief.data: edit_requirements_document(brief, edits['requirementsDocument']) if 'responseTemplate' in edits and 'responseTemplate' in brief.data: edit_response_template(brief, edits['responseTemplate']) organisation = None sellers_to_contact = [] if (title_was_edited(brief.data['title'], previous_data['title']) or summary_was_edited(brief.data['summary'], previous_data['summary']) or closing_date_was_edited( brief.closed_at.to_iso8601_string(extended=True), previous_data['closed_at']) or documents_were_edited(brief.data.get('attachments', []), previous_data.get('attachments', [])) or documents_were_edited( brief.data.get('requirementsDocument', []), previous_data.get('requirementsDocument', [])) or documents_were_edited(brief.data.get('responseTemplate', []), previous_data.get('responseTemplate', []))): organisation = agency_service.get_agency_name(user.agency_id) # We need to find sellers to contact about the current incoming edits before sellers are edited as we're # not sending additional sellers emails about the current edits that have been made. sellers_to_contact = brief_service.get_sellers_to_notify( brief, brief_business.is_open_to_all(brief)) sellers_to_invite = {} if 'sellers' in edits and sellers_were_edited( edits['sellers'], brief.data.get('sellers', {})): sellers_to_invite = get_sellers_to_invite(brief, edits['sellers']) edit_sellers(brief, sellers_to_invite) edit_seller_selector(brief, sellers_to_invite) # strip out any data keys not whitelisted brief = brief_business.remove_keys_not_whitelisted(brief) data_to_validate = copy.deepcopy(brief.data) # only validate the sellers being added in the edit if 'sellers' in edits and len(edits['sellers'].keys()) > 0: data_to_validate['sellers'] = copy.deepcopy(edits.get('sellers', {})) validator = None if brief.lot.slug == 'rfx': validator = RFXDataValidator(data_to_validate) elif brief.lot.slug == 'training2': validator = TrainingDataValidator(data_to_validate) elif brief.lot.slug == 'atm': validator = ATMDataValidator(data_to_validate) elif brief.lot.slug == 'specialist': validator = SpecialistDataValidator(data_to_validate) if validator is None: raise ValidationError('Validator not found for {}'.format( brief.lot.slug)) errors = [] if (title_was_edited(brief.data['title'], previous_data['title']) and not validator.validate_title()): errors.append('You must add a title') if (summary_was_edited(brief.data['summary'], previous_data['summary']) and not validator.validate_summary()): message = ('You must add what the specialist will do' if brief.lot.slug == 'specialist' else 'You must add a summary of work to be done') errors.append(message) if (brief.lot.slug != 'atm' and 'sellers' in edits and sellers_were_edited( edits['sellers'], brief.data.get('sellers', {})) and not validator.validate_sellers()): message = ( 'You must select some sellers' if brief.lot.slug == 'specialist' else 'You must select at least one seller and each seller must be assessed for the chosen category' ) errors.append(message) if (closing_date_was_edited( brief.closed_at.to_iso8601_string(extended=True), previous_data['closed_at']) and not validator.validate_closed_at(minimum_days=1)): message = ( 'The closing date must be at least 1 day into the future or not more than one year long' if brief.lot.slug == 'specialist' else 'The closing date must be at least 1 day into the future') errors.append(message) if len(errors) > 0: raise ValidationError(', '.join(errors)) brief_service.save(brief, do_commit=False) edit = BriefHistory(brief_id=brief.id, user_id=user_id, data=previous_data) brief_history_service.save(edit, do_commit=False) brief_service.commit_changes() if len(sellers_to_contact) > 0 and organisation: for email_address in sellers_to_contact: send_opportunity_edited_email_to_seller(brief, email_address, organisation) for code, data in sellers_to_invite.items(): supplier = supplier_service.get_supplier_by_code(code) if supplier: if brief.lot.slug == 'rfx': send_seller_invited_to_rfx_email(brief, supplier) elif brief.lot.slug == 'specialist': send_specialist_brief_seller_invited_email(brief, supplier) elif brief.lot.slug == 'training': send_seller_invited_to_training_email(brief, supplier) send_opportunity_edited_email_to_buyers(brief, user, edit) try: audit_service.log_audit_event( audit_type=audit_types.opportunity_edited, data={'briefId': brief.id}, db_object=brief, user=user.email_address) publish_tasks.brief.delay(publish_tasks.compress_brief(brief), 'edited', email_address=user.email_address, name=user.name) except Exception as e: rollbar.report_exc_info() return brief
def test_sellers_to_notify_has_email_address_used_to_invite_sellers(self, brief, suppliers): email_addresses = briefs_service.get_sellers_to_notify(brief, brief_business.is_open_to_all(brief)) assert '*****@*****.**' in email_addresses assert '*****@*****.**' in email_addresses
def test_sellers_to_notify_has_email_address_of_users_that_asked_questions(self, brief, brief_responses, brief_questions, suppliers): email_addresses = briefs_service.get_sellers_to_notify(brief, brief_business.is_open_to_all(brief)) assert '*****@*****.**' in email_addresses assert '*****@*****.**' in email_addresses
def test_sellers_to_notify_has_email_address_of_user_that_created_draft_response(self, audit_event, brief, brief_responses, suppliers): email_addresses = briefs_service.get_sellers_to_notify(brief, brief_business.is_open_to_all(brief)) assert '*****@*****.**' in email_addresses
def test_sellers_to_notify_includes_email_address_submitted_with_response(self, brief, brief_responses, suppliers): email_addresses = briefs_service.get_sellers_to_notify(brief, brief_business.is_open_to_all(brief)) assert '*****@*****.**' in email_addresses