Ejemplo n.º 1
0
def _company_factory(num_interactions, num_contacts, num_orders):
    """Factory for a company that has companies, interactions and OMIS orders."""
    company = CompanyFactory()
    ContactFactory.create_batch(num_contacts, company=company)
    CompanyInteractionFactory.create_batch(num_interactions, company=company)
    OrderFactory.create_batch(num_orders, company=company)
    return company
Ejemplo n.º 2
0
def _company_factory(
    num_interactions=0,
    num_contacts=0,
    num_investment_projects=0,
    num_orders=0,
    num_referrals=0,
    num_company_list_items=0,
    num_pipeline_items=0,
):
    """
    Factory for a company that has companies, interactions, investment projects and OMIS orders.
    """
    company = CompanyFactory()
    ContactFactory.create_batch(num_contacts, company=company)
    CompanyInteractionFactory.create_batch(num_interactions, company=company)
    CompanyReferralFactory.create_batch(num_referrals,
                                        company=company,
                                        contact=None)
    OrderFactory.create_batch(num_orders, company=company)
    CompanyListItemFactory.create_batch(num_company_list_items,
                                        company=company)
    PipelineItemFactory.create_batch(num_pipeline_items, company=company)

    fields_iter = cycle(INVESTMENT_PROJECT_COMPANY_FIELDS)
    fields = islice(fields_iter, 0, num_investment_projects)
    InvestmentProjectFactory.create_batch(
        num_investment_projects,
        **{field: company
           for field in fields},
    )
    return company
Ejemplo n.º 3
0
    def test_all(self):
        """Test getting all contacts"""
        ContactFactory.create_batch(5)

        url = reverse('api-v3:contact:list')
        response = self.api_client.get(url)

        assert response.status_code == status.HTTP_200_OK
        assert response.data['count'] == 5
Ejemplo n.º 4
0
def unrelated_objects():
    """
    Create some objects not related to a known company.

    This is used in tests below to make sure objects unrelated to the company being merged
    do not affect the counts of objects that will be affected by the merge.
    """
    ContactFactory.create_batch(2)
    CompanyInteractionFactory.create_batch(2)
    OrderFactory.create_batch(2)
    InvestmentProjectFactory.create_batch(2)
Ejemplo n.º 5
0
    def test_email_filter(self, opensearch_with_collector, contacts, filter_,
                          expected):
        """Tests the email filter"""
        ContactFactory.create_batch(len(contacts),
                                    email=factory.Iterator(contacts))

        opensearch_with_collector.flush_and_refresh()

        response = self.api_client.post(
            reverse('api-v3:search:contact'),
            data=dict(email=filter_),
        )

        assert response.status_code == status.HTTP_200_OK
        assert {res['email'] for res in response.data['results']} == expected
Ejemplo n.º 6
0
def _company_factory(
        num_interactions=0,
        num_contacts=0,
        num_orders=0,
        num_referrals=0,
        num_company_list_items=0,
):
    """Factory for a company that has companies, interactions and OMIS orders."""
    company = CompanyFactory()
    ContactFactory.create_batch(num_contacts, company=company)
    CompanyInteractionFactory.create_batch(num_interactions, company=company)
    CompanyReferralFactory.create_batch(num_referrals, company=company, contact=None)
    OrderFactory.create_batch(num_orders, company=company)
    CompanyListItemFactory.create_batch(num_company_list_items, company=company)
    return company
Ejemplo n.º 7
0
    def test_all_without_view_document_permission(self):
        """Test getting all contacts without view document permission."""
        ContactFactory.create_batch(
            5, archived_documents_url_path='https://some-docs')

        user = create_test_user(permission_codenames=('view_contact', ), )
        api_client = self.create_api_client(user=user)

        url = reverse('api-v3:contact:list')
        response = api_client.get(url)

        assert response.status_code == status.HTTP_200_OK
        assert response.data['count'] == 5
        assert all('archived_documents_url_path' not in contact
                   for contact in response.data['results'])
Ejemplo n.º 8
0
def make_matched_rows(num_records):
    """Make multiple interaction CSV rows that should pass contact matching."""
    adviser = AdviserFactory(
        first_name='Adviser for',
        last_name='Matched interaction',
    )
    service = random_service()
    communication_channel = random_communication_channel()
    contacts = ContactFactory.create_batch(
        num_records,
        email=factory.Sequence(lambda i: f'unique{i}@matched.uk'),
    )

    return [
        {
            'theme': Interaction.THEMES.export,
            'kind': Interaction.KINDS.interaction,
            'date': '01/01/2018',
            'adviser_1': adviser.name,
            'contact_email': contact.email,
            'service': service.name,
            'communication_channel': communication_channel.name,
        }
        for contact in contacts
    ]
Ejemplo n.º 9
0
    def test_successful_merge_creates_revision(self):
        """Test that a revision is created following a successful merge."""
        source_company = CompanyFactory()
        target_company = CompanyFactory()
        source_contacts = ContactFactory.create_batch(2,
                                                      company=source_company)

        confirm_merge_url = _make_confirm_merge_url(source_company,
                                                    target_company)

        frozen_time = datetime(2011, 2, 1, 14, 0, 10, tzinfo=utc)
        with freeze_time(frozen_time):
            response = self.client.post(confirm_merge_url, follow=True)

        assert response.status_code == status.HTTP_200_OK
        assert len(response.redirect_chain) == 1
        assert response.redirect_chain[0][0] == _get_changelist_url()

        source_company_versions = Version.objects.get_for_object(
            source_company)
        assert source_company_versions.count() == 1

        reversion = source_company_versions[0].revision
        assert reversion.date_created == frozen_time
        assert reversion.get_comment() == REVERSION_REVISION_COMMENT
        assert reversion.user == self.user

        contact_0_versions = Version.objects.get_for_object(source_contacts[0])
        assert contact_0_versions.count() == 1
        assert contact_0_versions[0].revision == reversion

        contact_1_versions = Version.objects.get_for_object(source_contacts[1])
        assert contact_1_versions.count() == 1
        assert contact_1_versions[0].revision == reversion
Ejemplo n.º 10
0
def test_contact_dbmodels_to_es_documents(es):
    """Tests conversion of db models to Elasticsearch documents."""
    contacts = ContactFactory.create_batch(2)

    result = ESContact.db_objects_to_es_documents(contacts)

    assert len(list(result)) == len(contacts)
Ejemplo n.º 11
0
def test_contact_dbmodels_to_documents(opensearch):
    """Tests conversion of db models to OpenSearch documents."""
    contacts = ContactFactory.create_batch(2)

    result = SearchContact.db_objects_to_documents(contacts)

    assert len(list(result)) == len(contacts)
Ejemplo n.º 12
0
    def test_filter_by_company(self):
        """Test getting contacts by company id"""
        company1 = CompanyFactory()
        company2 = CompanyFactory()

        ContactFactory.create_batch(3, company=company1)
        contacts = ContactFactory.create_batch(2, company=company2)

        url = reverse('api-v3:contact:list')
        response = self.api_client.get(url, data={'company_id': company2.id})

        assert response.status_code == status.HTTP_200_OK
        assert response.data['count'] == 2
        expected_contacts = {str(contact.id) for contact in contacts}
        assert {contact['id']
                for contact in response.data['results']} == expected_contacts
Ejemplo n.º 13
0
def _company_factory(num_interactions, num_contacts, num_investment_projects, num_orders):
    """
    Factory for a company that has companies, interactions, investment projects and OMIS orders.
    """
    company = CompanyFactory()
    ContactFactory.create_batch(num_contacts, company=company)
    CompanyInteractionFactory.create_batch(num_interactions, company=company)
    OrderFactory.create_batch(num_orders, company=company)

    fields_iter = cycle(INVESTMENT_PROJECT_COMPANY_FIELDS)
    fields = islice(fields_iter, 0, num_investment_projects)
    InvestmentProjectFactory.create_batch(
        num_investment_projects,
        **{field: company for field in fields},
    )
    return company
Ejemplo n.º 14
0
    def test_company_sector_descends_filter(
        self,
        hierarchical_sectors,
        opensearch_with_collector,
        sector_level,
    ):
        """Test the company_sector_descends filter."""
        num_sectors = len(hierarchical_sectors)
        sectors_ids = [sector.pk for sector in hierarchical_sectors]

        companies = CompanyFactory.create_batch(
            num_sectors,
            sector_id=factory.Iterator(sectors_ids),
        )
        contacts = ContactFactory.create_batch(
            3,
            company=factory.Iterator(companies),
        )

        other_companies = CompanyFactory.create_batch(
            3,
            sector=factory.LazyFunction(lambda: random_obj_for_queryset(
                SectorModel.objects.exclude(pk__in=sectors_ids), )),
        )
        ContactFactory.create_batch(
            3,
            company=factory.Iterator(other_companies),
        )

        opensearch_with_collector.flush_and_refresh()

        url = reverse('api-v3:search:contact')
        body = {
            'company_sector_descends': hierarchical_sectors[sector_level].pk,
        }
        response = self.api_client.post(url, body)
        assert response.status_code == status.HTTP_200_OK

        response_data = response.json()
        assert response_data['count'] == num_sectors - sector_level

        actual_ids = {
            uuid.UUID(contact['id'])
            for contact in response_data['results']
        }
        expected_ids = {contact.pk for contact in contacts[sector_level:]}
        assert actual_ids == expected_ids
Ejemplo n.º 15
0
 def test_get_contact_names(self, num_contacts, expected_display_value):
     """Test that contact names are formatted as expected."""
     interaction = CompanyInteractionFactory(
         contacts=ContactFactory.create_batch(num_contacts), )
     interaction_admin = InteractionAdmin(Interaction, site)
     first_contact = interaction.contacts.order_by('pk').first()
     formatted_expected_display_value = expected_display_value.format(
         first_contact_name=first_contact.name if first_contact else '', )
     assert interaction_admin.get_contact_names(
         interaction) == formatted_expected_display_value
Ejemplo n.º 16
0
    def test_intelligent_homepage_limit(self, setup_es):
        """Test the limit param."""
        CompanyInteractionFactory.create_batch(15, dit_adviser=self.user)
        ContactFactory.create_batch(15, created_by=self.user)

        setup_es.indices.refresh()

        url = reverse('dashboard:intelligent-homepage')
        response = self.api_client.get(
            url,
            data={
                'limit': 10,
            },
        )

        assert response.status_code == status.HTTP_200_OK
        response_data = response.json()
        assert len(response_data['contacts']) == 10
        assert len(response_data['interactions']) == 10
Ejemplo n.º 17
0
    def test_search_contact_by_archived(self, setup_es, setup_data, archived):
        """Tests filtering by archived."""
        ContactFactory.create_batch(5, archived=True)

        setup_es.indices.refresh()

        url = reverse('api-v3:search:contact')

        response = self.api_client.post(
            url,
            data={
                'archived': archived,
            },
        )

        assert response.status_code == status.HTTP_200_OK
        assert response.data['count'] > 0
        assert all(result['archived'] == archived
                   for result in response.data['results'])
Ejemplo n.º 18
0
def test_run(s3_stubber, caplog):
    """Test that the command updates the specified records (ignoring ones with errors)."""
    caplog.set_level('ERROR')

    original_datetime = datetime(2017, 1, 1, tzinfo=timezone.utc)

    with freeze_time(original_datetime):
        accepts_dit_email_marketing_values = [True, False, True]
        contacts = ContactFactory.create_batch(
            3,
            accepts_dit_email_marketing=factory.Iterator(
                accepts_dit_email_marketing_values),
        )

    bucket = 'test_bucket'
    object_key = 'test_key'
    csv_content = f"""id,accepts_dit_email_marketing
00000000-0000-0000-0000-000000000000,true
{contacts[0].pk},false
{contacts[1].pk},false
{contacts[2].pk},blah
"""

    s3_stubber.add_response(
        'get_object',
        {
            'Body': BytesIO(csv_content.encode(encoding='utf-8')),
        },
        expected_params={
            'Bucket': bucket,
            'Key': object_key,
        },
    )

    with freeze_time('2018-11-11 00:00:00'):
        call_command('update_contact_accepts_dit_email_marketing', bucket,
                     object_key)

    for contact in contacts:
        contact.refresh_from_db()

    assert 'Contact matching query does not exist' in caplog.text
    assert 'Must be a valid boolean' in caplog.text
    assert len(caplog.records) == 2

    assert [contact.accepts_dit_email_marketing for contact in contacts] == [
        False,
        False,
        True,
    ]
    assert all(contact.modified_on == original_datetime
               for contact in contacts)
Ejemplo n.º 19
0
    def test_interaction_permission(self, setup_es):
        """Test that the interaction view permission is enforced."""
        requester = create_test_user(permission_codenames=('view_contact', ), )
        CompanyInteractionFactory.create_batch(5, dit_adviser=requester)
        ContactFactory.create_batch(5, created_by=requester)

        setup_es.indices.refresh()

        api_client = self.create_api_client(user=requester)

        url = reverse('dashboard:intelligent-homepage')
        response = api_client.get(
            url,
            data={
                'limit': 10,
            },
        )

        assert response.status_code == status.HTTP_200_OK
        response_data = response.json()
        assert len(response_data['contacts']) == 5
        assert response_data['interactions'] == []
Ejemplo n.º 20
0
def test_simulate(s3_stubber, caplog):
    """Test that the command simulates updates if --simulate is passed in."""
    caplog.set_level('ERROR')

    original_datetime = datetime(2017, 1, 1, tzinfo=timezone.utc)

    with freeze_time(original_datetime):
        before_accepts_dit_email_marketing_values = [True, False]
        contacts = ContactFactory.create_batch(
            2,
            accepts_dit_email_marketing=factory.Iterator(
                before_accepts_dit_email_marketing_values, ),
        )

    bucket = 'test_bucket'
    object_key = 'test_key'
    csv_content = f"""id,accepts_dit_email_marketing
00000000-0000-0000-0000-000000000000,true
{contacts[0].pk},false
{contacts[1].pk},false
"""

    s3_stubber.add_response(
        'get_object',
        {
            'Body': BytesIO(csv_content.encode(encoding='utf-8')),
        },
        expected_params={
            'Bucket': bucket,
            'Key': object_key,
        },
    )

    with freeze_time('2018-11-11 00:00:00'):
        call_command(
            'update_contact_accepts_dit_email_marketing',
            bucket,
            object_key,
            simulate=True,
        )

    for contact in contacts:
        contact.refresh_from_db()

    assert 'Contact matching query does not exist' in caplog.text
    assert len(caplog.records) == 1

    after_accepts_dit_email_marketing_values = [
        contact.accepts_dit_email_marketing for contact in contacts
    ]
    assert after_accepts_dit_email_marketing_values == before_accepts_dit_email_marketing_values
Ejemplo n.º 21
0
    def test_filter_by_created_on_exists(self, setup_es, created_on_exists):
        """Tests filtering contact by created_on exists."""
        ContactFactory.create_batch(3)
        no_created_on = ContactFactory.create_batch(3)
        for contact in no_created_on:
            contact.created_on = None
            contact.save()

        setup_es.indices.refresh()

        url = reverse('api-v3:search:contact')
        request_data = {
            'created_on_exists': created_on_exists,
        }
        response = self.api_client.post(url, request_data)

        assert response.status_code == status.HTTP_200_OK

        response_data = response.json()
        results = response_data['results']
        assert response_data['count'] == 3
        assert all((not result['created_on'] is None) == created_on_exists
                   for result in results)
Ejemplo n.º 22
0
def make_multiple_matches_rows(num_records):
    """Make multiple interaction CSV rows that should have multiple contact matches."""
    adviser = AdviserFactory(
        first_name='Adviser for',
        last_name='Multi-matched interaction',
    )
    service = random_service()
    communication_channel = random_communication_channel()

    contact_email = '*****@*****.**'
    ContactFactory.create_batch(2, email=contact_email)

    return [
        {
            'theme': Interaction.THEMES.export,
            'kind': Interaction.KINDS.interaction,
            'date': '01/01/2018',
            'adviser_1': adviser.name,
            'contact_email': contact_email,
            'service': service.name,
            'communication_channel': communication_channel.name,
        }
        for _ in range(num_records)
    ]
    def test_cannot_add_more_contacts_to_event_service_delivery(self):
        """Test that an event service delivery cannot be updated to have multiple contacts."""
        service_delivery = EventServiceDeliveryFactory()
        new_contacts = ContactFactory.create_batch(2, company=service_delivery.company)

        url = reverse('api-v3:interaction:item', kwargs={'pk': service_delivery.pk})
        request_data = {
            'contacts': [{'id': contact.pk} for contact in new_contacts],
        }
        response = self.api_client.patch(url, data=request_data)

        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert response.json() == {
            'contacts': ['Only one contact can be provided for event service deliveries.'],
        }
Ejemplo n.º 24
0
    def test_opts_out_contacts(self, encoding):
        """
        Test that accepts_dit_email_marketing is updated for the contacts specified in the CSV
        file.
        """
        filename = 'filea.csv'
        emails = [
            'test1@datahub',
            'test1@datahub',
            'test2@datahub',
            'test2@datahub',
            'test3@datahub',
            'test4@datahub',
        ]
        marketing_status = [True, True, True, False, True, True]
        creation_time = datetime(2011, 2, 1, 14, 0, 10, tzinfo=utc)
        with freeze_time(creation_time):
            contacts = ContactFactory.create_batch(
                len(emails),
                email=factory.Iterator(emails),
                accepts_dit_email_marketing=factory.Iterator(marketing_status),
                modified_by=None,
            )

        file = io.BytesIO("""email\r
test1@datahub\r
TEST2@datahub\r
test6@datahub\r
""".encode(encoding=encoding))
        file.name = filename

        url = reverse(
            admin_urlname(Contact._meta, 'load-email-marketing-opt-outs'),
        )

        post_time = datetime(2014, 5, 3, 19, 0, 16, tzinfo=utc)
        with freeze_time(post_time):
            response = self.client.post(
                url,
                follow=True,
                data={
                    'email_list': file,
                },
            )

        assert response.status_code == status.HTTP_200_OK
        assert len(response.redirect_chain) == 1
        change_list_url = reverse(admin_urlname(Contact._meta, 'changelist'))
        assert response.redirect_chain[0][0] == change_list_url

        for contact in contacts:
            contact.refresh_from_db()

        assert [contact.accepts_dit_email_marketing for contact in contacts] == [
            False, False, False, False, True, True,
        ]
        assert [contact.modified_on for contact in contacts] == [
            post_time, post_time, post_time, creation_time, creation_time, creation_time,
        ]
        assert [contact.modified_by for contact in contacts] == [
            self.user, self.user, self.user, None, None, None,
        ]

        messages = list(response.context['messages'])
        assert len(messages) == 2
        assert messages[0].level == django_messages.SUCCESS
        assert messages[0].message == (
            '3 contacts opted out of marketing emails and 1 contacts already opted out'
        )
        assert messages[1].level == django_messages.WARNING
        assert messages[1].message == '1 email addresses did not match a contact'
Ejemplo n.º 25
0
    def test_export(
        self,
        es_with_collector,
        request_sortby,
        orm_ordering,
    ):
        """Test export of contact search results."""
        ArchivedContactFactory()
        ContactWithOwnAddressFactory()
        ContactFactory()

        # These are to test date of and team of latest interaction a bit more thoroughly
        CompanyInteractionFactory.create_batch(2)
        CompanyInteractionFactory(contacts=ContactFactory.create_batch(2))
        interaction_with_multiple_teams = CompanyInteractionFactory()
        InteractionDITParticipantFactory.create_batch(
            2,
            interaction=interaction_with_multiple_teams,
        )

        es_with_collector.flush_and_refresh()

        data = {}
        if request_sortby:
            data['sortby'] = request_sortby

        url = reverse('api-v3:search:contact-export')

        with freeze_time('2018-01-01 11:12:13'):
            response = self.api_client.post(url, data=data)

        assert response.status_code == status.HTTP_200_OK
        assert parse_header(response.get('Content-Type')) == ('text/csv', {
            'charset':
            'utf-8'
        })
        assert parse_header(response.get('Content-Disposition')) == (
            'attachment',
            {
                'filename': 'Data Hub - Contacts - 2018-01-01-11-12-13.csv'
            },
        )

        sorted_contacts = Contact.objects.annotate(
            computed_address_country_name=Coalesce(
                'address_country__name',
                'company__address_country__name',
            ), ).order_by(
                orm_ordering,
                'pk',
            )
        reader = DictReader(StringIO(response.getvalue().decode('utf-8-sig')))

        assert reader.fieldnames == list(
            SearchContactExportAPIView.field_titles.values())

        # E123 is ignored as there are seemingly unresolvable indentation errors in the dict below
        expected_row_data = [  # noqa: E123
            {
                'Name':
                contact.name,
                'Job title':
                contact.job_title,
                'Date created':
                contact.created_on,
                'Archived':
                contact.archived,
                'Link':
                f'{settings.DATAHUB_FRONTEND_URL_PREFIXES["contact"]}/{contact.pk}',
                'Company':
                get_attr_or_none(contact, 'company.name'),
                'Company sector':
                get_attr_or_none(contact, 'company.sector.name'),
                'Company link':
                f'{settings.DATAHUB_FRONTEND_URL_PREFIXES["company"]}/{contact.company.pk}',
                'Company UK region':
                get_attr_or_none(contact, 'company.uk_region.name'),
                'Country':
                contact.company.address_country.name
                if contact.address_same_as_company else
                contact.address_country.name,
                'Postcode':
                contact.company.address_postcode if
                contact.address_same_as_company else contact.address_postcode,
                'Phone number':
                ' '.join(
                    (contact.telephone_countrycode, contact.telephone_number)),
                'Email address':
                contact.email,
                'Accepts DIT email marketing':
                contact.accepts_dit_email_marketing,
                'Date of latest interaction':
                max(contact.interactions.all(), key=attrgetter('date')).date
                if contact.interactions.all() else None,
                'Teams of latest interaction':
                _format_interaction_team_names(
                    max(contact.interactions.all(), key=attrgetter('date')), )
                if contact.interactions.exists() else None,
                'Created by team':
                get_attr_or_none(contact, 'created_by.dit_team.name'),
            } for contact in sorted_contacts
        ]

        actual_row_data = [dict(row) for row in reader]
        assert actual_row_data == format_csv_data(expected_row_data)
Ejemplo n.º 26
0
def company_with_contacts_factory():
    """Factory for a company with contacts."""
    company = CompanyFactory()
    ContactFactory.create_batch(3, company=company)
    return company
Ejemplo n.º 27
0
    def test_export(
        self,
        opensearch_with_collector,
        request_sortby,
        orm_ordering,
        requests_mock,
        accepts_dit_email_marketing,
    ):
        """Test export of contact search results."""
        ArchivedContactFactory()
        ContactWithOwnAddressFactory()
        ContactFactory()
        ContactWithOwnAreaFactory()

        # These are to test date of and team of latest interaction a bit more thoroughly
        CompanyInteractionFactory.create_batch(2)
        CompanyInteractionFactory(contacts=ContactFactory.create_batch(2))
        interaction_with_multiple_teams = CompanyInteractionFactory()
        InteractionDITParticipantFactory.create_batch(
            2,
            interaction=interaction_with_multiple_teams,
        )

        opensearch_with_collector.flush_and_refresh()

        data = {}
        if request_sortby:
            data['sortby'] = request_sortby

        url = reverse('api-v3:search:contact-export')

        with freeze_time('2018-01-01 11:12:13'):
            response = self.api_client.post(url, data=data)

        assert response.status_code == status.HTTP_200_OK
        assert parse_header(response.get('Content-Type')) == ('text/csv', {
            'charset':
            'utf-8'
        })
        assert parse_header(response.get('Content-Disposition')) == (
            'attachment',
            {
                'filename': 'Data Hub - Contacts - 2018-01-01-11-12-13.csv'
            },
        )

        sorted_contacts = Contact.objects.annotate(
            computed_address_country_name=Coalesce(
                'address_country__name',
                'company__address_country__name',
            ), ).order_by(
                orm_ordering,
                'pk',
            )

        matcher = requests_mock.get(
            f'{settings.CONSENT_SERVICE_BASE_URL}'
            f'{CONSENT_SERVICE_PERSON_PATH_LOOKUP}',
            text=generate_hawk_response({
                'results': [{
                    'email':
                    contact.email,
                    'consents': [
                        CONSENT_SERVICE_EMAIL_CONSENT_TYPE,
                    ] if accepts_dit_email_marketing else [],
                } for contact in sorted_contacts],
            }),
            status_code=status.HTTP_200_OK,
        )

        reader = DictReader(StringIO(response.getvalue().decode('utf-8-sig')))
        assert reader.fieldnames == list(
            SearchContactExportAPIView.field_titles.values())

        expected_row_data = format_csv_data([{
            'Name':
            contact.name,
            'Job title':
            contact.job_title,
            'Date created':
            contact.created_on,
            'Archived':
            contact.archived,
            'Link':
            f'{settings.DATAHUB_FRONTEND_URL_PREFIXES["contact"]}/{contact.pk}',
            'Company':
            get_attr_or_none(contact, 'company.name'),
            'Company sector':
            get_attr_or_none(contact, 'company.sector.name'),
            'Company link':
            f'{settings.DATAHUB_FRONTEND_URL_PREFIXES["company"]}/{contact.company.pk}',
            'Company UK region':
            get_attr_or_none(contact, 'company.uk_region.name'),
            'Area': (contact.company.address_area
                     and contact.company.address_area.name)
            if contact.address_same_as_company else
            (contact.address_area and contact.address_area.name),
            'Country':
            contact.company.address_country.name if
            contact.address_same_as_company else contact.address_country.name,
            'Postcode':
            contact.company.address_postcode
            if contact.address_same_as_company else contact.address_postcode,
            'Phone number':
            contact.full_telephone_number,
            'Email address':
            contact.email,
            'Accepts DIT email marketing':
            accepts_dit_email_marketing,
            'Date of latest interaction':
            max(contact.interactions.all(), key=attrgetter('date')).date
            if contact.interactions.all() else None,
            'Teams of latest interaction':
            _format_interaction_team_names(
                max(contact.interactions.all(), key=attrgetter('date')), )
            if contact.interactions.exists() else None,
            'Created by team':
            get_attr_or_none(contact, 'created_by.dit_team.name'),
        } for contact in sorted_contacts])

        actual_row_data = [dict(row) for row in reader]
        assert len(actual_row_data) == len(expected_row_data)
        for index, row in enumerate(actual_row_data):
            assert row == expected_row_data[index]
        assert matcher.call_count == 1
        assert matcher.last_request.query == urllib.parse.urlencode(
            {'email': [c.email for c in sorted_contacts]},
            doseq=True,
        )