Beispiel #1
0
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
Beispiel #4
0
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
Beispiel #8
0
 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
Beispiel #9
0
 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
Beispiel #10
0
 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
Beispiel #11
0
 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