Skip to content

Commit 1abfb18

Browse files
authored
Merge pull request #1404 from uktrade/feature/multiple-contacts-pt-1
Add many-to-many contacts field to interactions
2 parents 4d08738 + 25e0034 commit 1abfb18

File tree

12 files changed

+286
-5
lines changed

12 files changed

+286
-5
lines changed
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The table ``interaction_interaction_contacts`` table with columns ``("id" serial NOT NULL PRIMARY KEY, "interaction_id" uuid NOT NULL, "contact_id" uuid NOT NULL)`` was added. This is a many-to-many table linking interactions with contacts. The table had not been fully populated with data yet; continue to use ``interaction_interaction.contact_id`` for the time being.

datahub/cleanup/management/commands/delete_old_records.py

+1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ class Command(BaseCleanupCommand):
7878
# projects, OMIS orders or OMIS quotes. We wait for those records to expire
7979
# before we delete the related contacts.
8080
Contact._meta.get_field('interactions'): (),
81+
Contact._meta.get_field('interactions_m2m'): (),
8182
Contact._meta.get_field('investment_projects'): (),
8283
Contact._meta.get_field('orders'): (),
8384
Quote._meta.get_field('accepted_by').remote_field: (),

datahub/cleanup/test/commands/factories.py

+17
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from datahub.core.test.factories import to_many_field
2+
from datahub.interaction.test.factories import CompanyInteractionFactory
23
from datahub.investment.test.factories import InvestmentProjectFactory
34

45

@@ -16,3 +17,19 @@ class ShallowInvestmentProjectFactory(InvestmentProjectFactory):
1617
def client_contacts(self):
1718
"""No client contacts."""
1819
return []
20+
21+
22+
class CompanyInteractionFactoryWithoutContacts(CompanyInteractionFactory):
23+
"""
24+
Same as CompanyInteractionFactory but with reduced dependencies
25+
so that we can test specific references without extra noise.
26+
27+
TODO: Remove once Interaction.contact has been removed.
28+
"""
29+
30+
contact = None
31+
32+
@to_many_field
33+
def contacts(self):
34+
"""Default to no contacts."""
35+
return []

datahub/cleanup/test/commands/test_delete_old_records.py

+14-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
ORDER_MODIFIED_ON_CUT_OFF,
2424
)
2525
from datahub.cleanup.query_utils import get_relations_to_delete
26+
from datahub.cleanup.test.commands.factories import CompanyInteractionFactoryWithoutContacts
2627
from datahub.company.test.factories import (
2728
CompanyFactory,
2829
ContactFactory,
@@ -206,7 +207,7 @@
206207
],
207208
'relations': [
208209
{
209-
'factory': CompanyInteractionFactory,
210+
'factory': CompanyInteractionFactoryWithoutContacts,
210211
'field': 'contact',
211212
'expired_objects_kwargs': [],
212213
'unexpired_objects_kwargs': [
@@ -216,6 +217,17 @@
216217
},
217218
],
218219
},
220+
{
221+
'factory': CompanyInteractionFactoryWithoutContacts,
222+
'field': 'contacts',
223+
'expired_objects_kwargs': [],
224+
'unexpired_objects_kwargs': [
225+
{
226+
'created_on': CONTACT_DELETE_BEFORE_DATETIME - relativedelta(days=1),
227+
'modified_on': CONTACT_DELETE_BEFORE_DATETIME - relativedelta(days=1),
228+
},
229+
],
230+
},
219231
{
220232
'factory': InvestmentProjectFactory,
221233
'field': 'client_contacts',
@@ -253,7 +265,7 @@
253265
},
254266
'interaction.Interaction': {
255267
'factory': CompanyInteractionFactory,
256-
'implicitly_deletable_models': set(),
268+
'implicitly_deletable_models': {'interaction.Interaction_contacts'},
257269
'expired_objects_kwargs': [
258270
{
259271
'date': INTERACTION_DELETE_BEFORE_DATETIME - relativedelta(days=1),

datahub/cleanup/test/commands/test_delete_orphans.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111

1212
from datahub.cleanup.management.commands import delete_orphans
1313
from datahub.cleanup.query_utils import get_relations_to_delete
14-
from datahub.cleanup.test.commands.factories import ShallowInvestmentProjectFactory
14+
from datahub.cleanup.test.commands.factories import (
15+
CompanyInteractionFactoryWithoutContacts,
16+
ShallowInvestmentProjectFactory,
17+
)
1518
from datahub.company.test.factories import (
1619
CompanyFactory,
1720
ContactFactory,
@@ -43,7 +46,8 @@
4346
'company.Contact': {
4447
'factory': ContactFactory,
4548
'dependent_models': (
46-
(CompanyInteractionFactory, 'contact'),
49+
(CompanyInteractionFactoryWithoutContacts, 'contact'),
50+
(CompanyInteractionFactoryWithoutContacts, 'contacts'),
4751
(OrderFactory, 'contact'),
4852
(QuoteFactory, 'accepted_by'),
4953
(InvestmentProjectFactory, 'client_contacts'),

datahub/interaction/admin.py

+14
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,18 @@ class InteractionAdmin(BaseModelAdminMixin, VersionAdmin):
6161
'created_by',
6262
'modified_on',
6363
'modified_by',
64+
'contacts',
6465
)
66+
67+
def save_model(self, request, obj, form, change):
68+
"""
69+
Saves the object, populating contacts from contact.
70+
71+
TODO: Remove once the migration from contact to contacts is complete.
72+
"""
73+
if 'contact' in form.cleaned_data:
74+
contact = form.cleaned_data['contact']
75+
contacts = [contact] if contact else []
76+
obj.contacts.set(contacts)
77+
78+
super().save_model(request, obj, form, change)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 2.1.5 on 2019-01-21 15:08
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('company', '0060_trading_names_not_null'),
10+
('interaction', '0040_make_policy_provided_non_nullable'),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name='interaction',
16+
name='contacts',
17+
field=models.ManyToManyField(blank=True, related_name='interactions_m2m', to='company.Contact'),
18+
),
19+
]

datahub/interaction/models.py

+8
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,21 @@ class Interaction(BaseModel):
9999
null=True,
100100
on_delete=models.CASCADE,
101101
)
102+
# TODO: contact is being replaced with contacts, and contact will be removed once the
103+
# migration to a to-many field is complete
102104
contact = models.ForeignKey(
103105
'company.Contact',
104106
related_name="%(class)ss", # noqa: Q000
105107
blank=True,
106108
null=True,
107109
on_delete=models.CASCADE,
108110
)
111+
contacts = models.ManyToManyField(
112+
'company.Contact',
113+
# TODO: change related_name to interactions once this field has fully replaced contact
114+
related_name='interactions_m2m',
115+
blank=True,
116+
)
109117
event = models.ForeignKey(
110118
'event.Event',
111119
related_name="%(class)ss", # noqa: Q000

datahub/interaction/serializers.py

+5
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ def validate(self, data):
9191
"""
9292
if 'is_event' in data:
9393
del data['is_event']
94+
95+
if 'contact' in data:
96+
# Note: null contacts are not allowed in the API
97+
data['contacts'] = [data['contact']]
98+
9499
return data
95100

96101
class Meta:

datahub/interaction/test/factories.py

+9
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ class InteractionFactoryBase(factory.django.DjangoModelFactory):
3535
archived_documents_url_path = factory.Faker('uri_path')
3636
was_policy_feedback_provided = False
3737

38+
@to_many_field
39+
def contacts(self):
40+
"""
41+
Contacts field.
42+
43+
Defaults to the contact from the contact field.
44+
"""
45+
return [self.contact] if self.contact else []
46+
3847
class Meta:
3948
model = 'interaction.Interaction'
4049

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
from uuid import uuid4
2+
3+
from django.contrib.admin.templatetags.admin_urls import admin_urlname
4+
from django.urls import reverse
5+
from rest_framework import status
6+
7+
from datahub.company.test.factories import CompanyFactory, ContactFactory
8+
from datahub.core.admin import get_change_url
9+
from datahub.core.test_utils import AdminTestMixin, random_obj_for_model
10+
from datahub.interaction.models import CommunicationChannel, Interaction
11+
from datahub.interaction.test.factories import CompanyInteractionFactory
12+
from datahub.metadata.models import Service, Team
13+
14+
15+
class TestInteractionAdmin(AdminTestMixin):
16+
"""
17+
Tests for interaction admin.
18+
19+
TODO: these tests will be removed once the migration from contact to contacts is complete.
20+
"""
21+
22+
def test_add(self):
23+
"""Test that adding an interaction also sets the contacts field."""
24+
company = CompanyFactory()
25+
contact = ContactFactory()
26+
communication_channel = random_obj_for_model(CommunicationChannel)
27+
28+
url = reverse(admin_urlname(Interaction._meta, 'add'))
29+
data = {
30+
'id': uuid4(),
31+
'kind': Interaction.KINDS.interaction,
32+
'communication_channel': communication_channel.pk,
33+
'subject': 'whatever',
34+
'date_0': '2018-01-01',
35+
'date_1': '00:00:00',
36+
'dit_adviser': self.user.pk,
37+
'company': company.pk,
38+
'contact': contact.pk,
39+
'service': random_obj_for_model(Service).pk,
40+
'dit_team': random_obj_for_model(Team).pk,
41+
'was_policy_feedback_provided': False,
42+
}
43+
response = self.client.post(url, data, follow=True)
44+
45+
assert response.status_code == status.HTTP_200_OK
46+
47+
assert Interaction.objects.count() == 1
48+
interaction = Interaction.objects.first()
49+
50+
assert interaction.contact == contact
51+
assert list(interaction.contacts.all()) == [contact]
52+
53+
def test_update_contact_to_non_null(self):
54+
"""
55+
Test that updating an interaction with a non-null contact also sets the contacts
56+
field.
57+
"""
58+
interaction = CompanyInteractionFactory(contacts=[])
59+
new_contact = ContactFactory(company=interaction.company)
60+
61+
url = get_change_url(interaction)
62+
data = {
63+
# Unchanged values
64+
'id': interaction.pk,
65+
'kind': Interaction.KINDS.interaction,
66+
'communication_channel': interaction.communication_channel.pk,
67+
'subject': interaction.subject,
68+
'date_0': interaction.date.date().isoformat(),
69+
'date_1': interaction.date.time().isoformat(),
70+
'dit_adviser': interaction.dit_adviser.pk,
71+
'company': interaction.company.pk,
72+
'service': interaction.service.pk,
73+
'dit_team': interaction.dit_team.pk,
74+
'was_policy_feedback_provided': interaction.was_policy_feedback_provided,
75+
'policy_feedback_notes': interaction.policy_feedback_notes,
76+
'policy_areas': [],
77+
'policy_issue_types': [],
78+
'event': '',
79+
80+
# Changed values
81+
'contact': new_contact.pk,
82+
}
83+
response = self.client.post(url, data=data, follow=True)
84+
85+
assert response.status_code == status.HTTP_200_OK
86+
87+
interaction.refresh_from_db()
88+
assert interaction.contact == new_contact
89+
assert list(interaction.contacts.all()) == [new_contact]
90+
91+
def test_update_contact_to_null(self):
92+
"""Test that updating an interaction with a null contact clears the contacts field."""
93+
interaction = CompanyInteractionFactory(contacts=[])
94+
95+
url = get_change_url(interaction)
96+
data = {
97+
# Unchanged values
98+
'id': interaction.pk,
99+
'kind': Interaction.KINDS.interaction,
100+
'communication_channel': interaction.communication_channel.pk,
101+
'subject': interaction.subject,
102+
'date_0': interaction.date.date().isoformat(),
103+
'date_1': interaction.date.time().isoformat(),
104+
'dit_adviser': interaction.dit_adviser.pk,
105+
'company': interaction.company.pk,
106+
'service': interaction.service.pk,
107+
'dit_team': interaction.dit_team.pk,
108+
'was_policy_feedback_provided': interaction.was_policy_feedback_provided,
109+
'policy_feedback_notes': interaction.policy_feedback_notes,
110+
'policy_areas': [],
111+
'policy_issue_types': [],
112+
'event': '',
113+
114+
# Changed values
115+
'contact': '',
116+
}
117+
response = self.client.post(url, data=data, follow=True)
118+
119+
assert response.status_code == status.HTTP_200_OK
120+
121+
interaction.refresh_from_db()
122+
assert interaction.contact is None
123+
assert interaction.contacts.count() == 0

0 commit comments

Comments
 (0)