Skip to content

Commit

Permalink
Rule tests and actions should use pk rather than UUIDs in json serial…
Browse files Browse the repository at this point in the history
…ization as labels may not have UUID
  • Loading branch information
rowanseymour committed Apr 15, 2016
1 parent f7e3975 commit e58b12b
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 17 deletions.
51 changes: 40 additions & 11 deletions casepro/rules/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,27 @@
import six

from abc import ABCMeta, abstractmethod
from collections import defaultdict
from django.utils.translation import ugettext_lazy as _
from enum import Enum

from casepro.backend import get_backend
from casepro.contacts.models import Group
from casepro.msgs.models import Label, Message
from casepro.utils import normalize
from collections import defaultdict
from enum import Enum


class Quantifier(Enum):
"""
Tests are typically composed of multiple conditions, e.g. contains ANY of X, Y or Z.
"""
NONE = 1
ANY = 2
ALL = 3
NONE = (1, _("none of"))
ANY = (2, _("any of"))
ALL = (3, _("all of"))

def __init__(self, val, text):
self.val = val
self.text = text

@classmethod
def from_json(cls, val):
Expand All @@ -44,6 +50,9 @@ def evaluate(self, condition_callables):
return False
return True

def __unicode__(self):
return unicode(self.text)


class DeserializationContext(object):
"""
Expand Down Expand Up @@ -90,6 +99,9 @@ def __eq__(self, other):
def __ne__(self, other):
return not self.__eq__(other)

def __str__(self):
return str(unicode(self))


class ContainsTest(Test):
"""
Expand Down Expand Up @@ -128,6 +140,10 @@ def __eq__(self, other):
return other and self.TYPE == other.TYPE \
and self.keywords == other.keywords and self.quantifier == other.quantifier

def __unicode__(self):
quoted_keywords = ['"%s"' % w for w in self.keywords]
return "message contains %s %s" % (six.text_type(self.quantifier), ", ".join(quoted_keywords))


class GroupsTest(Test):
"""
Expand All @@ -141,11 +157,11 @@ def __init__(self, groups, quantifier):

@classmethod
def from_json(cls, json_obj, context):
groups = list(Group.objects.filter(org=context.org, uuid__in=json_obj['groups']).order_by('pk'))
groups = list(Group.objects.filter(org=context.org, pk__in=json_obj['groups']).order_by('pk'))
return cls(groups, Quantifier.from_json(json_obj['quantifier']))

def to_json(self):
return {'type': self.TYPE, 'groups': [g.uuid for g in self.groups], 'quantifier': self.quantifier.to_json()}
return {'type': self.TYPE, 'groups': [g.pk for g in self.groups], 'quantifier': self.quantifier.to_json()}

def matches(self, message):
contact_groups = set(message.contact.groups.all())
Expand All @@ -160,6 +176,10 @@ def group_check(g):
def __eq__(self, other):
return other and self.TYPE == other.TYPE and self.groups == other.groups and self.quantifier == other.quantifier

def __unicode__(self):
group_names = [g.name for g in self.groups]
return "contact belongs to %s %s" % (six.text_type(self.quantifier), ", ".join(group_names))


class FieldTest(Test):
"""
Expand Down Expand Up @@ -189,6 +209,10 @@ def matches(self, message):
def __eq__(self, other):
return other and self.TYPE == other.TYPE and self.key == other.key and self.values == other.values

def __unicode__(self):
quoted_values = ['"%s"' % v for v in self.values]
return "contact.%s is %s %s" % (self.key, Quantifier.ANY, ", ".join(quoted_values))


class Action(object):
"""
Expand Down Expand Up @@ -236,10 +260,10 @@ def __init__(self, label):

@classmethod
def from_json(cls, json_obj, context):
return cls(Label.objects.get(org=context.org, uuid=json_obj['label']))
return cls(Label.objects.get(org=context.org, pk=json_obj['label']))

def to_json(self):
return {'type': self.TYPE, 'label': self.label.uuid}
return {'type': self.TYPE, 'label': self.label.pk}

def apply_to(self, org, messages):
for msg in messages:
Expand Down Expand Up @@ -304,9 +328,11 @@ def __init__(self, tests, actions):

@classmethod
def get_all(cls, org):
# load all org labels and converts them to rules
"""
Loads all org labels and converts them to rules
"""
rules = []
for label in Label.get_all(org):
for label in Label.get_all(org).order_by('pk'):
rule = cls.from_label(label)
if rule:
rules.append(rule)
Expand All @@ -329,6 +355,9 @@ def matches(self, message):
return False
return True

def get_tests_description(self):
return _(" and ").join([six.text_type(t) for t in self.tests])

class BatchProcessor(object):
"""
Applies a set of rules to a batch of messages in a way that allows same actions to be merged and reduces needed
Expand Down
35 changes: 29 additions & 6 deletions casepro/rules/tests.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from __future__ import unicode_literals

import six

from mock import patch, call

from casepro.msgs.models import Message
from casepro.test import BaseCasesTest
from mock import patch, call

from .models import Action, LabelAction, ArchiveAction, FlagAction
from .models import Test, ContainsTest, Rule, DeserializationContext, Quantifier
from .models import Test, ContainsTest, GroupsTest, FieldTest, Rule, DeserializationContext, Quantifier


class TestsTest(BaseCasesTest):
Expand All @@ -29,6 +33,7 @@ def test_contains(self):
self.assertEqual(test.keywords, ["red", "blue"])
self.assertEqual(test.quantifier, Quantifier.ANY)
self.assertEqual(test.to_json(), {'type': 'contains', 'keywords': ["red", "blue"], 'quantifier': 'any'})
self.assertEqual(six.text_type(test), 'message contains any of "red", "blue"')

self.assertTest(test, self.ann, "Fred Blueth", False)
self.assertTest(test, self.ann, "red", True)
Expand All @@ -47,11 +52,17 @@ def test_contains(self):
self.assertTest(test, self.ann, "yo RED Blue", False)

def test_groups(self):
test = Test.from_json({'type': 'groups', 'groups': ["G-002", "G-003"], 'quantifier': 'any'}, self.context)
test = Test.from_json(
{'type': 'groups', 'groups': [self.females.pk, self.reporters.pk], 'quantifier': 'any'},
self.context
)
self.assertEqual(test.TYPE, 'groups')
self.assertEqual(set(test.groups), {self.females, self.reporters})
self.assertEqual(test.quantifier, Quantifier.ANY)
self.assertEqual(test.to_json(), {'type': 'groups', 'groups': ["G-002", "G-003"], 'quantifier': 'any'})
self.assertEqual(test.to_json(), {
'type': 'groups', 'groups': [self.females.pk, self.reporters.pk], 'quantifier': 'any'
})
self.assertEqual(six.text_type(test), 'contact belongs to any of Females, Reporters')

self.assertTest(test, self.ann, "Yes", True)
self.assertTest(test, self.bob, "Yes", False)
Expand All @@ -75,6 +86,7 @@ def test_field(self):
self.assertEqual(test.key, "city")
self.assertEqual(test.values, ["kigali", "lusaka"])
self.assertEqual(test.to_json(), {'type': 'field', 'key': "city", 'values': ["kigali", "lusaka"]})
self.assertEqual(six.text_type(test), 'contact.city is any of "kigali", "lusaka"')

self.assertTest(test, self.ann, "Yes", True)
self.assertTest(test, self.bob, "Yes", False)
Expand All @@ -101,10 +113,10 @@ def setUp(self):
self.ann = self.create_contact(self.unicef, 'C-001', "Ann")

def test_label(self):
action = Action.from_json({'type': 'label', 'label': 'L-001'}, self.context)
action = Action.from_json({'type': 'label', 'label': self.aids.pk}, self.context)
self.assertEqual(action.TYPE, 'label')
self.assertEqual(action.label, self.aids)
self.assertEqual(action.to_json(), {'type': 'label', 'label': 'L-001'})
self.assertEqual(action.to_json(), {'type': 'label', 'label': self.aids.pk})

msg = self.create_message(self.unicef, 102, self.ann, "red")
action.apply_to(self.unicef, [msg])
Expand Down Expand Up @@ -148,6 +160,17 @@ def test_get_all(self):
self.assertEqual(rules[1].tests, [ContainsTest(["pregnant", "pregnancy"], Quantifier.ANY)])
self.assertEqual(rules[1].actions, [LabelAction(self.pregnancy)])

def test_get_tests_description(self):
rule = Rule([
ContainsTest(["aids", "HIV"], Quantifier.ANY),
GroupsTest([self.females, self.reporters], Quantifier.ALL),
FieldTest("city", ["Kigali", "Lusaka"])
], [])

self.assertEqual(rule.get_tests_description(), 'message contains any of "aids", "hiv" '
'and contact belongs to all of Females, Reporters '
'and contact.city is any of "kigali", "lusaka"')

@patch('casepro.test.TestBackend.label_messages')
@patch('casepro.test.TestBackend.flag_messages')
@patch('casepro.test.TestBackend.archive_messages')
Expand Down
3 changes: 3 additions & 0 deletions casepro/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ def create_case(self, org, contact, assignee, message, labels=(), **kwargs):
return case

def assertNotCalled(self, mock):
"""
Because mock.assert_not_called doesn't exist in Python 2
"""
self.assertEqual(len(mock.mock_calls), 0, "Expected no calls, called %d times" % len(mock.mock_calls))

def assertExcelRow(self, sheet, row_num, values, tz=None):
Expand Down

0 comments on commit e58b12b

Please sign in to comment.