Skip to content

Commit 25e0034

Browse files
committed
Add many-to-many contacts field to interactions
This adds a many-to-many contacts field to the interaction model. When the contact field is set via the API or via the admin site, the value is mirrored to the contacts field. For now, this field is not exposed via the API or admin as the data in contact has not been fully replicated to contacts.
1 parent 4d08738 commit 25e0034

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)