Skip to content

Commit

Permalink
Merge pull request #2056 from nyaruka/use_systemfields_in_dynamic_groups
Browse files Browse the repository at this point in the history
馃 Use SystemContactFields for Dynamic Groups
  • Loading branch information
rowanseymour committed Aug 30, 2018
2 parents 61df204 + 7da6aad commit 0cdc8d4
Show file tree
Hide file tree
Showing 18 changed files with 628 additions and 186 deletions.
2 changes: 1 addition & 1 deletion temba/api/v1/serializers.py
Expand Up @@ -398,7 +398,7 @@ def save(self):

# save our contact if it changed
if changed:
self.instance.save(update_fields=changed)
self.instance.save(update_fields=changed, handle_update=True)

# update our fields
if fields is not None:
Expand Down
2 changes: 1 addition & 1 deletion temba/api/v2/serializers.py
Expand Up @@ -522,7 +522,7 @@ def save(self):
self.instance.update_urns(self.context["user"], urns)

if changed:
self.instance.save(update_fields=changed)
self.instance.save(update_fields=changed, handle_update=True)
else:
self.instance = Contact.get_or_create_by_urns(
self.context["org"], self.context["user"], name, urns=urns, language=language
Expand Down
31 changes: 25 additions & 6 deletions temba/api/v2/tests.py
Expand Up @@ -1440,6 +1440,11 @@ def test_contacts(self):
with ESMockWithScroll():
dyn_group = self.create_group("Dynamic Group", query="nickname is jado")

lang_group = self.create_group("Language Group", query="language = fra")
eng_lang_group = self.create_group("Language Group English", query="language = eng")

jason_group = self.create_group("Jason Group", query="name has Jason")

# create with all fields
response = self.postJSON(
url,
Expand All @@ -1461,7 +1466,7 @@ def test_contacts(self):
nickname = ContactField.get_by_key(self.org, "nickname")
jean = Contact.objects.filter(name="Jean", language="fra").order_by("-pk").first()
self.assertEqual(set(jean.urns.values_list("identity", flat=True)), {"tel:+250783333333", "twitter:jean"})
self.assertEqual(set(jean.user_groups.all()), {group, dyn_group})
self.assertEqual(set(jean.user_groups.all()), {group, dyn_group, lang_group})
self.assertEqual(jean.get_field_value(nickname), "Jado")

# create with invalid fields
Expand Down Expand Up @@ -1493,28 +1498,42 @@ def test_contacts(self):
self.assertEqual(jean.name, "Jean")
self.assertEqual(jean.language, "fra")
self.assertEqual(set(jean.urns.values_list("identity", flat=True)), {"tel:+250783333333", "twitter:jean"})
self.assertEqual(set(jean.user_groups.all()), {group, dyn_group})
self.assertEqual(set(jean.user_groups.all()), {group, dyn_group, lang_group})
self.assertEqual(jean.get_field_value(nickname), "Jado")

# update by UUID and change all fields
response = self.postJSON(
url,
"uuid=%s" % jean.uuid,
{
"name": "Jean II",
"language": "eng",
"name": "Jason Undead",
"language": "ita",
"urns": ["tel:+250784444444"],
"groups": [],
"fields": {"nickname": "John"},
},
)
self.assertEqual(response.status_code, 200)

jean = Contact.objects.get(pk=jean.pk)
self.assertEqual(jean.name, "Jason Undead")
self.assertEqual(jean.language, "ita")
self.assertEqual(set(jean.urns.values_list("identity", flat=True)), {"tel:+250784444444"})
self.assertEqual(set(jean.user_groups.all()), {jason_group})
self.assertEqual(jean.get_field_value(nickname), "John")

# change the language field
response = self.postJSON(
url,
"uuid=%s" % jean.uuid,
{"name": "Jean II", "language": "eng", "urns": ["tel:+250784444444"], "groups": [], "fields": {}},
)
self.assertEqual(response.status_code, 200)
jean = Contact.objects.get(pk=jean.pk)
self.assertEqual(jean.name, "Jean II")
self.assertEqual(jean.language, "eng")
self.assertEqual(set(jean.urns.values_list("identity", flat=True)), {"tel:+250784444444"})
self.assertEqual(set(jean.user_groups.all()), set())
self.assertEqual(set(jean.user_groups.all()), {eng_lang_group})
self.assertEqual(jean.get_field_value(nickname), "John")

# update by URN (which should be normalized)
Expand Down Expand Up @@ -2685,7 +2704,7 @@ def test_runs(self):
# allow Frank to run the flow in French
self.org.set_languages(self.admin, ["eng", "fra"], "eng")
self.frank.language = "fra"
self.frank.save(update_fields=("language",))
self.frank.save(update_fields=("language",), handle_update=False)

flow1 = self.get_flow("color")
flow2 = Flow.copy(flow1, self.user)
Expand Down
20 changes: 11 additions & 9 deletions temba/campaigns/models.py
Expand Up @@ -648,29 +648,31 @@ def update_events_for_contact(cls, contact):
# remove all pending fires for this contact
EventFire.objects.filter(contact=contact, fired=None).delete()

# get all the groups this user is in
groups = [g.id for g in contact.cached_user_groups]

# for each campaign that might affect us
for campaign in Campaign.objects.filter(
group__in=groups, org=contact.org, is_active=True, is_archived=False
group__in=contact.user_groups, org=contact.org, is_active=True, is_archived=False
).distinct():
# update all the events for the campaign
EventFire.update_campaign_events_for_contact(campaign, contact)

@classmethod
def update_events_for_contact_field(cls, contact, key):
def update_events_for_contact_field(cls, contact, keys, is_new=False):
"""
Updates all the events for a contact, across all campaigns.
Should be called anytime a contact field or contact group membership changes.
"""
# get all the groups this user is in
groups = [_.id for _ in contact.cached_user_groups]

# get all events which are in one of these groups and on this field
events = CampaignEvent.objects.filter(
campaign__group__in=groups, relative_to__key=key, campaign__is_archived=False, is_active=True
campaign__group__in=contact.user_groups,
relative_to__key__in=keys,
campaign__is_archived=False,
is_active=True,
).prefetch_related("relative_to")

if is_new is False:
# only new contacts can trigger campaign event reevaluation that are relative to immutable fields
events.exclude(relative_to__key__in=ContactField.IMMUTABLE_FIELDS)

for event in events:
# remove any unfired events, they will get recreated below
EventFire.objects.filter(event=event, contact=contact, fired=None).delete()
Expand Down
4 changes: 2 additions & 2 deletions temba/channels/tests.py
Expand Up @@ -3315,7 +3315,7 @@ def test_send(self):
self.clear_cache()

self.joe.is_stopped = False
self.joe.save(update_fields=("is_stopped",))
self.joe.save(update_fields=("is_stopped",), handle_update=False)
testers.update_contacts(self.user, [self.joe], add=True)

with patch("requests.sessions.Session.post") as mock:
Expand All @@ -3340,7 +3340,7 @@ def test_send(self):
self.clear_cache()

self.joe.is_stopped = False
self.joe.save(update_fields=("is_stopped",))
self.joe.save(update_fields=("is_stopped",), handle_update=False)
testers.update_contacts(self.user, [self.joe], add=True)

with patch("requests.sessions.Session.post") as mock:
Expand Down
49 changes: 49 additions & 0 deletions temba/contacts/migrations/0090_auto_20180809_1336.py
@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.14 on 2018-08-09 13:36
from __future__ import unicode_literals

import itertools

from django.db import migrations

from temba.values.constants import Value


def contact_field_generator(apps):
Org = apps.get_model("orgs.Org")
ContactField = apps.get_model("contacts.ContactField")

for org in Org.objects.all():
field_id = ContactField(
org_id=org.id,
label="ID",
key="id",
value_type=Value.TYPE_NUMBER,
show_in_table=False,
created_by=org.created_by,
modified_by=org.modified_by,
field_type="S",
)
yield field_id


def add_system_contact_fields(apps, schema_editor):
ContactField = apps.get_model("contacts.ContactField")
all_contact_fields = contact_field_generator(apps)

# https://docs.djangoproject.com/en/2.0/ref/models/querysets/#bulk-create
batch_size = 1000
while True:
batch = list(itertools.islice(all_contact_fields, batch_size))

if len(batch) == 0:
break

ContactField.all_fields.bulk_create(batch, batch_size)


class Migration(migrations.Migration):

dependencies = [("contacts", "0089_auto_20180723_1347")]

operations = [migrations.RunPython(add_system_contact_fields)]

0 comments on commit 0cdc8d4

Please sign in to comment.