def update_destination_no_check(self, flow, node, destination, rule=None): # pragma: no cover """ Update the destination without doing a cycle check """ # look up our destination, we need this in order to set the correct destination_type destination_type = ACTION_SET action_destination = Flow.get_node(flow, destination, destination_type) if not action_destination: destination_type = RULE_SET ruleset_destination = Flow.get_node(flow, destination, destination_type) self.assertTrue(ruleset_destination, "Unable to find new destination with uuid: %s" % destination) actionset = ActionSet.get(flow, node) if actionset: actionset.destination = destination actionset.destination_type = destination_type actionset.save() ruleset = RuleSet.get(flow, node) if ruleset: rules = ruleset.get_rules() for r in rules: if r.uuid == rule: r.destination = destination r.destination_type = destination_type ruleset.set_rules(rules) ruleset.save() else: self.fail("Couldn't find node with uuid: %s" % node)
def advance_stuck_runs(apps, schema_editor): # this data migration is not forward-compatible from temba.flows.models import Flow, FlowStep, FlowRun, RuleSet from temba.msgs.models import Msg flows = Flow.objects.filter(flow_type="F", version_number=5) if flows: print "%d version 5 flows" % len(flows) for flow in flows: # looking for flows that start with a passive ruleset ruleset = RuleSet.objects.filter(uuid=flow.entry_uuid, flow=flow).first() if ruleset and not ruleset.is_pause(): # now see if there are any active steps at our current flow steps = FlowStep.objects.filter( run__is_active=True, step_uuid=ruleset.uuid, rule_value=None, left_on=None ).select_related("contact") if steps: print "\nAdvancing %d steps for %s:%s" % (len(steps), flow.org.name, flow.name) for idx, step in enumerate(steps): if (idx + 1) % 100 == 0: print "\n\n *** Step %d of %d\n\n" % (idx + 1, len(steps)) # force them to be handled again msg = Msg(contact=step.contact, text="", id=0) Flow.handle_destination(ruleset, step, step.run, msg)
def update_destination_no_check(self, flow, node, destination, rule=None): # pragma: no cover """ Update the destination without doing a cycle check """ # look up our destination, we need this in order to set the correct destination_type destination_type = FlowStep.TYPE_ACTION_SET action_destination = Flow.get_node(flow, destination, destination_type) if not action_destination: destination_type = FlowStep.TYPE_RULE_SET ruleset_destination = Flow.get_node(flow, destination, destination_type) self.assertTrue( ruleset_destination, "Unable to find new destination with uuid: %s" % destination) actionset = ActionSet.get(flow, node) if actionset: actionset.destination = destination actionset.destination_type = destination_type actionset.save() ruleset = RuleSet.get(flow, node) if ruleset: rules = ruleset.get_rules() for r in rules: if r.uuid == rule: r.destination = destination r.destination_type = destination_type ruleset.set_rules(rules) ruleset.save() else: self.fail("Couldn't find node with uuid: %s" % node)
def test_flow_event(self): self.setupChannel() org = self.channel.org org.save() from temba.flows.models import ActionSet, WebhookAction, Flow flow = self.create_flow() # replace our uuid of 4 with the right thing actionset = ActionSet.objects.get(x=4) actionset.set_actions_dict( [WebhookAction(org.get_webhook_url()).as_json()]) actionset.save() with patch('requests.Session.send') as mock: # run a user through this flow flow.start([], [self.joe]) # have joe reply with mauve, which will put him in the other category that triggers the API Action sms = self.create_msg(contact=self.joe, direction='I', status='H', text="Mauve") mock.return_value = MockResponse(200, "{}") Flow.find_and_handle(sms) # should have one event created event = WebHookEvent.objects.get() self.assertEquals('C', event.status) self.assertEquals(1, event.try_count) self.assertFalse(event.next_attempt) result = WebHookResult.objects.get() self.assertStringContains("successfully", result.message) self.assertEquals(200, result.status_code) self.assertTrue(mock.called) args = mock.call_args_list[0][0] prepared_request = args[0] self.assertStringContains(self.channel.org.get_webhook_url(), prepared_request.url) data = parse_qs(prepared_request.body) self.assertEquals(self.channel.pk, int(data['channel'][0])) self.assertEquals(actionset.uuid, data['step'][0]) self.assertEquals(flow.pk, int(data['flow'][0])) self.assertEquals(self.joe.uuid, data['contact'][0]) self.assertEquals(unicode(self.joe.get_urn('tel')), data['urn'][0]) values = json.loads(data['values'][0]) self.assertEquals('Other', values[0]['category']['base']) self.assertEquals('color', values[0]['label']) self.assertEquals('Mauve', values[0]['text']) self.assertTrue(values[0]['time']) self.assertTrue(data['time'])
def send(self, message, contact=None): if not contact: contact = self.contact if contact.is_test: Contact.set_simulation(True) incoming = self.create_msg(direction=INCOMING, contact=contact, text=message) Flow.find_and_handle(incoming) return Msg.all_messages.filter(response_to=incoming).order_by('pk').first()
def test_flow_event(self): self.setupChannel() org = self.channel.org org.save() from temba.flows.models import ActionSet, WebhookAction, Flow flow = self.create_flow() # replace our uuid of 4 with the right thing actionset = ActionSet.objects.get(x=4) actionset.set_actions_dict([WebhookAction(org.get_webhook_url()).as_json()]) actionset.save() with patch('requests.Session.send') as mock: # run a user through this flow flow.start([], [self.joe]) # have joe reply with mauve, which will put him in the other category that triggers the API Action sms = self.create_msg(contact=self.joe, direction='I', status='H', text="Mauve") mock.return_value = MockResponse(200, "{}") Flow.find_and_handle(sms) # should have one event created event = WebHookEvent.objects.get() self.assertEquals('C', event.status) self.assertEquals(1, event.try_count) self.assertFalse(event.next_attempt) result = WebHookResult.objects.get() self.assertStringContains("successfully", result.message) self.assertEquals(200, result.status_code) self.assertTrue(mock.called) args = mock.call_args_list[0][0] prepared_request = args[0] self.assertStringContains(self.channel.org.get_webhook_url(), prepared_request.url) data = parse_qs(prepared_request.body) self.assertEquals(self.channel.pk, int(data['channel'][0])) self.assertEquals(actionset.uuid, data['step'][0]) self.assertEquals(flow.pk, int(data['flow'][0])) self.assertEquals(self.joe.uuid, data['contact'][0]) self.assertEquals(unicode(self.joe.get_urn('tel')), data['urn'][0]) values = json.loads(data['values'][0]) self.assertEquals('Other', values[0]['category']['base']) self.assertEquals('color', values[0]['label']) self.assertEquals('Mauve', values[0]['text']) self.assertTrue(values[0]['time']) self.assertTrue(data['time'])
def send(self, message, contact=None): if not contact: contact = self.contact if contact.is_test: Contact.set_simulation(True) incoming = self.create_msg(direction=INCOMING, contact=contact, text=message) Flow.find_and_handle(incoming) return Msg.all_messages.filter( response_to=incoming).order_by('pk').first()
def send(self, message, contact=None): if not contact: contact = self.contact if contact.is_test: Contact.set_simulation(True) incoming = self.create_msg(direction=INCOMING, contact=contact, text=message) # evaluate the inbound message against our triggers first from temba.triggers.models import Trigger if not Trigger.find_and_handle(incoming): Flow.find_and_handle(incoming) return Msg.objects.filter(response_to=incoming).order_by('pk').first()
def test_export_import(self): # tweak our current channel to be twitter so we can create a channel-based trigger Channel.objects.filter(id=self.channel.id).update(channel_type=TWITTER) flow = self.create_flow() group = self.create_group("Trigger Group", []) # create a trigger on this flow for the follow actions but only on some groups trigger = Trigger.objects.create(org=self.org, flow=flow, trigger_type=Trigger.TYPE_FOLLOW, channel=self.channel, created_by=self.admin, modified_by=self.admin) trigger.groups.add(group) # export everything export = Flow.export_definitions([flow]) # remove our trigger Trigger.objects.all().delete() # and reimport them.. trigger should be recreated self.org.import_app(export, self.admin) trigger = Trigger.objects.get() self.assertEqual(trigger.trigger_type, Trigger.TYPE_FOLLOW) self.assertEqual(trigger.flow, flow) self.assertEqual(trigger.channel, self.channel) self.assertEqual(list(trigger.groups.all()), [group])
def pre_save(self, request, obj): # if it's before, negate the offset if self.cleaned_data['direction'] == 'B': obj.offset = -obj.offset if self.cleaned_data['unit'] == 'H' or self.cleaned_data[ 'unit'] == 'M': # pragma: needs cover obj.delivery_hour = -1 # if its a message flow, set that accordingly if self.cleaned_data['event_type'] == CampaignEvent.TYPE_MESSAGE: message_dict = {} for language in self.languages: iso_code = language.language['iso_code'] message_dict[iso_code] = self.cleaned_data.get(iso_code, '') if not obj.flow_id or not obj.flow.is_active or obj.flow.flow_type != Flow.MESSAGE: obj.flow = Flow.create_single_message(request.user.get_org(), request.user, message_dict) # set our single message on our flow obj.flow.update_single_message_flow(message_dict) obj.message = json.dumps(message_dict) # otherwise, it's an event that runs an existing flow else: obj.flow = Flow.objects.get(pk=self.cleaned_data['flow_to_start'])
def send_message( self, flow, message, restart_participants=False, contact=None, initiate_flow=False, assert_reply=True, assert_handle=True, ): """ Starts the flow, sends the message, returns the reply """ if not contact: contact = self.contact try: if contact.is_test: Contact.set_simulation(True) incoming = self.create_msg( direction=INCOMING, contact=contact, contact_urn=contact.get_urn(), text=message ) # start the flow if initiate_flow: flow.start( groups=[], contacts=[contact], restart_participants=restart_participants, start_msg=incoming ) else: flow.start(groups=[], contacts=[contact], restart_participants=restart_participants) (handled, msgs) = Flow.find_and_handle(incoming) Msg.mark_handled(incoming) if assert_handle: self.assertTrue(handled, "'%s' did not handle message as expected" % flow.name) else: self.assertFalse(handled, "'%s' handled message, was supposed to ignore" % flow.name) # our message should have gotten a reply if assert_reply: replies = Msg.objects.filter(response_to=incoming).order_by("pk") self.assertGreaterEqual(len(replies), 1) if len(replies) == 1: self.assertEqual(contact, replies.first().contact) return replies.first().text # if it's more than one, send back a list of replies return [reply.text for reply in replies] else: # assert we got no reply replies = Msg.objects.filter(response_to=incoming).order_by("pk") self.assertFalse(replies) return None finally: Contact.set_simulation(False)
def test_missed_call_trigger(self): self.login(self.admin) flow = self.create_flow() trigger_url = reverse("triggers.trigger_missed_call") response = self.client.get(trigger_url) self.assertEqual(response.status_code, 200) response = self.client.post(trigger_url, {"flow": flow.id}) self.assertEqual(response.status_code, 200) trigger = Trigger.objects.order_by("id").last() self.assertEqual(trigger.trigger_type, Trigger.TYPE_MISSED_CALL) self.assertEqual(trigger.flow, flow) other_flow = Flow.copy(flow, self.admin) response = self.client.post( reverse("triggers.trigger_update", args=[trigger.id]), {"flow": other_flow.id}) self.assertEqual(response.status_code, 302) trigger.refresh_from_db() self.assertEqual(trigger.flow, other_flow) # create ten missed call triggers for i in range(10): response = self.client.get(trigger_url) self.assertEqual(response.status_code, 200) self.client.post(trigger_url, {"flow": flow.id}) self.assertEqual(Trigger.objects.all().count(), i + 2) self.assertEqual( Trigger.objects.filter( is_archived=False, trigger_type=Trigger.TYPE_MISSED_CALL).count(), 1) # even unarchiving we only have one active trigger at a time triggers = Trigger.objects.filter( trigger_type=Trigger.TYPE_MISSED_CALL, is_archived=True) active_trigger = Trigger.objects.get( trigger_type=Trigger.TYPE_MISSED_CALL, is_archived=False) response = self.client.post(reverse("triggers.trigger_archived"), { "action": "restore", "objects": [t.id for t in triggers] }) self.assertEqual(response.status_code, 200) self.assertEqual( Trigger.objects.filter( is_archived=False, trigger_type=Trigger.TYPE_MISSED_CALL).count(), 1) self.assertNotEqual( active_trigger, Trigger.objects.filter(is_archived=False, trigger_type=Trigger.TYPE_MISSED_CALL)[0])
def migrate_flows(min_version=None): # pragma: no cover to_version = min_version or get_current_export_version() # get all flows below the min version old_versions = Flow.get_versions_before(to_version) flows_to_migrate = Flow.objects.filter(is_active=True, version_number__in=old_versions) flow_ids = list(flows_to_migrate.values_list("id", flat=True)) total = len(flow_ids) if not total: print("All flows up to date") return True print("Found %d flows to migrate to %s..." % (len(flow_ids), to_version)) num_updated = 0 errored = [] for id_batch in chunk_list(flow_ids, 1000): for flow in Flow.objects.filter(id__in=id_batch): try: flow.ensure_current_version(min_version=to_version) num_updated += 1 except Exception: print("Unable to migrate flow '%s' (#%d)" % (flow.name, flow.id)) errored.append(flow) print(" > Flows migrated: %d of %d (%d errored)" % (num_updated, total, len(errored))) if errored: print(" > Errored flows: %s" % (", ".join([str(e.id) for e in errored]))) return len(errored) == 0
def start_call(self, call, to, from_, status_callback): channel = call.channel Contact.get_or_create(channel.org, channel.created_by, urns=[(TEL_SCHEME, to)]) # Verboice differs from Twilio in that they expect the first block of twiml up front payload = unicode(Flow.handle_call(call, {})) # our config should have our http basic auth parameters and verboice channel config = channel.config_json() # now we can post that to verboice url = "%s?%s" % (self.endpoint, urlencode(dict(channel=config['channel'], address=to))) response = requests.post(url, data=payload, auth=(config['username'], config['password'])).json() # store the verboice call id in our IVRCall call.external_id = response['call_id'] call.status = IN_PROGRESS call.save()
def migrate_flows(min_version=None): # pragma: no cover to_version = min_version or Flow.FINAL_LEGACY_VERSION # get all flows below the min version old_versions = Flow.get_versions_before(to_version) flows_to_migrate = Flow.objects.filter(is_active=True, version_number__in=old_versions) flow_ids = list(flows_to_migrate.values_list("id", flat=True)) total = len(flow_ids) if not total: print("All flows up to date") return True print(f"Found {len(flow_ids)} flows to migrate to {to_version}...") num_updated = 0 num_errored = 0 for id_batch in chunk_list(flow_ids, 1000): for flow in Flow.objects.filter(id__in=id_batch): try: flow.ensure_current_version(min_version=to_version) num_updated += 1 except Exception: print(f"Unable to migrate flow '{flow.name}' ({str(flow.uuid)}):") print(traceback.format_exc()) num_errored += 1 print(f" > Flows migrated: {num_updated} of {total} ({num_errored} errored)") return num_errored == 0
def get_flow(self, filename, substitutions=None, flow_type=Flow.FLOW): flow = Flow.create(self.org, self.admin, name=filename, flow_type=flow_type) self.update_flow(flow, filename, substitutions) return flow
def form_valid(self, form): keyword = form.cleaned_data["keyword"] join_group = form.cleaned_data["action_join_group"] start_flow = form.cleaned_data["flow"] send_msg = form.cleaned_data["response"] org = self.request.user.get_org() group_flow = Flow.create_join_group(org, self.request.user, join_group, send_msg, start_flow) Trigger.objects.create( created_by=self.request.user, modified_by=self.request.user, org=self.request.user.get_org(), keyword=keyword, trigger_type=Trigger.TYPE_KEYWORD, flow=group_flow, ) analytics.track(self.request.user.username, "temba.trigger_created", dict(type="register")) response = self.render_to_response( self.get_context_data(form=form)) response["REDIRECT"] = self.get_success_url() return response
def pre_save(self, request, obj): org = self.user.get_org() # if it's before, negate the offset if self.cleaned_data["direction"] == "B": obj.offset = -obj.offset if self.cleaned_data["unit"] == "H" or self.cleaned_data["unit"] == "M": # pragma: needs cover obj.delivery_hour = -1 # if its a message flow, set that accordingly if self.cleaned_data["event_type"] == CampaignEvent.TYPE_MESSAGE: if self.instance.id: base_language = self.instance.flow.base_language else: base_language = org.primary_language.iso_code if org.primary_language else "base" translations = {} for language in self.languages: iso_code = language.language["iso_code"] translations[iso_code] = self.cleaned_data.get(iso_code, "") if not obj.flow_id or not obj.flow.is_active or obj.flow.flow_type != Flow.MESSAGE: obj.flow = Flow.create_single_message(org, request.user, translations, base_language=base_language) else: # set our single message on our flow obj.flow.update_single_message_flow(translations, base_language) obj.message = translations obj.full_clean() # otherwise, it's an event that runs an existing flow else: obj.flow = Flow.objects.get(org=org, id=self.cleaned_data["flow_to_start"])
def form_valid(self, form): keyword = form.cleaned_data["keyword"] join_group = form.cleaned_data["action_join_group"] start_flow = form.cleaned_data["flow"] send_msg = form.cleaned_data["response"] groups = form.cleaned_data["groups"] exclude_groups = form.cleaned_data["exclude_groups"] org = self.request.user.get_org() register_flow = Flow.create_join_group(org, self.request.user, join_group, send_msg, start_flow) Trigger.create( org, self.request.user, Trigger.TYPE_KEYWORD, register_flow, groups=groups, exclude_groups=exclude_groups, keyword=keyword, ) response = self.render_to_response( self.get_context_data(form=form)) response["REDIRECT"] = self.get_success_url() return response
def test_get_sorted_events(self): # create a campaign campaign = Campaign.create(self.org, self.user, "Planting Reminders", self.farmers) flow = self.create_flow() event1 = CampaignEvent.create_flow_event(self.org, self.admin, campaign, self.planting_date, offset=1, unit='W', flow=flow, delivery_hour='13') event2 = CampaignEvent.create_flow_event(self.org, self.admin, campaign, self.planting_date, offset=1, unit='W', flow=flow, delivery_hour='9') event3 = CampaignEvent.create_flow_event(self.org, self.admin, campaign, self.planting_date, offset=2, unit='W', flow=flow, delivery_hour='1') self.assertEqual(campaign.get_sorted_events(), [event2, event1, event3]) flow_json = self.get_flow_json('call_me_maybe')['definition'] flow = Flow.create_instance(dict(name='Call Me Maybe', org=self.org, flow_type=Flow.MESSAGE, created_by=self.admin, modified_by=self.admin, saved_by=self.admin, version_number=3)) FlowRevision.create_instance(dict(flow=flow, definition=flow_json, spec_version=3, revision=1, created_by=self.admin, modified_by=self.admin)) event4 = CampaignEvent.create_flow_event(self.org, self.admin, campaign, self.planting_date, offset=2, unit='W', flow=flow, delivery_hour='5') self.assertEqual(flow.version_number, 3) self.assertEqual(campaign.get_sorted_events(), [event2, event1, event3, event4]) flow.refresh_from_db() self.assertNotEqual(flow.version_number, 3) self.assertEqual(flow.version_number, get_current_export_version())
def pre_save(self, request, obj): org = self.user.get_org() # if it's before, negate the offset if self.cleaned_data["direction"] == "B": obj.offset = -obj.offset if self.cleaned_data["unit"] == "H" or self.cleaned_data["unit"] == "M": # pragma: needs cover obj.delivery_hour = -1 # if its a message flow, set that accordingly if self.cleaned_data["event_type"] == CampaignEvent.TYPE_MESSAGE: if self.instance.id: base_language = self.instance.flow.base_language else: base_language = org.primary_language.iso_code if org.primary_language else "base" translations = {} for language in self.languages: iso_code = language.language["iso_code"] translations[iso_code] = self.cleaned_data.get(iso_code, "") if not obj.flow_id or not obj.flow.is_active or not obj.flow.is_system: obj.flow = Flow.create_single_message(org, request.user, translations, base_language=base_language) else: # set our single message on our flow obj.flow.update_single_message_flow(translations, base_language) obj.message = translations obj.full_clean() # otherwise, it's an event that runs an existing flow else: obj.flow = Flow.objects.get(org=org, id=self.cleaned_data["flow_to_start"])
def migrate_to_version_11_5(json_flow, flow=None): """ Replaces @flow.foo and @flow.foo.value with @extra.webhook where foo is a webhook or resthook ruleset """ # figure out which rulesets are webhook or resthook calls rule_sets = json_flow.get("rule_sets", []) webhook_rulesets = set() non_webhook_rulesets = set() for r in rule_sets: slug = Flow.label_to_slug(r["label"]) if not slug: # pragma: no cover continue if r["ruleset_type"] in (RuleSet.TYPE_WEBHOOK, RuleSet.TYPE_RESTHOOK): webhook_rulesets.add(slug) else: non_webhook_rulesets.add(slug) # ignore any slugs of webhook rulesets which are also used by non-webhook rulesets slugs = webhook_rulesets.difference(non_webhook_rulesets) if not slugs: return json_flow # make a regex that matches a context reference to these (see https://regex101.com/r/65b2ZT/3) replace_pattern = r"flow\.(" + "|".join(slugs) + r")(\.value)?(?!\.\w)" replace_regex = regex.compile(replace_pattern, flags=regex.UNICODE | regex.IGNORECASE | regex.MULTILINE) replace_with = r"extra.\1" replace_templates(json_flow, lambda t: replace_regex.sub(replace_with, t)) return json_flow
def __init__(self, user, *args, **kwargs): flows = Flow.get_triggerable_flows(user.get_org()) super().__init__(user, flows, *args, **kwargs) self.fields["channel"].queryset = Channel.objects.filter( is_active=True, org=self.user.get_org(), schemes__overlap=list(ContactURN.SCHEMES_SUPPORTING_REFERRALS) )
def form_valid(self, form): keyword = form.cleaned_data['keyword'] join_group = form.cleaned_data['action_join_group'] start_flow = form.cleaned_data['flow'] send_msg = form.cleaned_data['response'] org = self.request.user.get_org() group_flow = Flow.create_join_group(org, self.request.user, join_group, send_msg, start_flow) Trigger.objects.create(created_by=self.request.user, modified_by=self.request.user, org=self.request.user.get_org(), keyword=keyword, trigger_type=Trigger.TYPE_KEYWORD, flow=group_flow) analytics.track(self.request.user.username, 'temba.trigger_created_register', dict(name=join_group.name)) response = self.render_to_response( self.get_context_data(form=form)) response['REDIRECT'] = self.get_success_url() return response
def start_call(self, call, to, from_, status_callback): if not settings.SEND_CALLS: raise IVRException("SEND_CALLS set to False, skipping call start") channel = call.channel Contact.get_or_create(channel.org, channel.created_by, urns=[URN.from_tel(to)]) # Verboice differs from Twilio in that they expect the first block of twiml up front payload = six.text_type(Flow.handle_call(call)) # now we can post that to verboice url = "%s?%s" % (self.endpoint, urlencode( dict(channel=self.verboice_channel, address=to))) response = requests.post(url, data=payload, auth=self.auth).json() if 'call_id' not in response: raise IVRException(_('Verboice connection failed.')) # store the verboice call id in our IVRCall call.external_id = response['call_id'] call.status = IVRCall.IN_PROGRESS call.save()
def start_call(self, call, to, from_, status_callback): if not settings.SEND_CALLS: raise ValueError("SEND_CALLS set to False, skipping call start") channel = call.channel Contact.get_or_create(channel.org, URN.from_tel(to), channel) # Verboice differs from Twilio in that they expect the first block of twiml up front payload = str(Flow.handle_call(call)) # now we can post that to verboice url = "%s?%s" % (self.endpoint, urlencode(dict(channel=self.verboice_channel, address=to))) response = requests.post(url, data=payload, auth=self.auth).json() if "call_id" not in response: call.status = IVRCall.FAILED call.save() raise IVRException(_("Verboice connection failed.")) # store the verboice call id in our IVRCall call.external_id = response["call_id"] # the call was successfully sent to the IVR provider call.status = IVRCall.WIRED call.save()
def create_flow(self): start = int(time.time()*1000) % 1000000 definition = dict(action_sets=[dict(uuid=uuid(start + 1), x=1, y=1, destination=uuid(start + 5), actions=[dict(type='reply', msg='What is your favorite color?')]), dict(uuid=uuid(start + 2), x=2, y=2, destination=None, actions=[dict(type='reply', msg='I love orange too!')]), dict(uuid=uuid(start + 3), x=3, y=3, destination=None, actions=[dict(type='reply', msg='Blue is sad. :(')]), dict(uuid=uuid(start + 4), x=4, y=4, destination=None, actions=[dict(type='reply', msg='That is a funny color.')]) ], rule_sets=[dict(uuid=uuid(start + 5), x=5, y=5, label='color', response_type='C', rules=[ dict(uuid=uuid(start + 12), destination=uuid(start + 2), test=dict(type='contains', test='orange'), category="Orange"), dict(uuid=uuid(start + 13), destination=uuid(start + 3), test=dict(type='contains', test='blue'), category="Blue"), dict(uuid=uuid(start + 14), destination=uuid(start + 4), test=dict(type='true'), category="Other"), dict(uuid=uuid(start + 15), test=dict(type='true'), category="Nothing")]) # test case with no destination ], entry=uuid(start + 1)) flow = Flow.create(self.org, self.admin, "Color Flow") flow.update(definition) return flow
def create_message_event( cls, org, user, campaign, relative_to, offset, unit, message, delivery_hour=-1, base_language=None ): if campaign.org != org: raise ValueError("Org mismatch") if relative_to.value_type != Value.TYPE_DATETIME: raise ValueError( f"Contact fields for CampaignEvents must have a datetime type, got {relative_to.value_type}." ) if isinstance(message, str): base_language = org.primary_language.iso_code if org.primary_language else "base" message = {base_language: message} flow = Flow.create_single_message(org, user, message, base_language) return cls.objects.create( campaign=campaign, relative_to=relative_to, offset=offset, unit=unit, event_type=cls.TYPE_MESSAGE, message=message, flow=flow, delivery_hour=delivery_hour, created_by=user, modified_by=user, )
def migrate_to_version_11_4(json_flow, flow=None): """ Replaces @flow.foo.text with @step.value for non-waiting rulesets, to bring old world functionality inline with the new engine, where @run.results.foo.input is always the router operand. """ # figure out which rulesets aren't waits rule_sets = json_flow.get("rule_sets", []) non_waiting = { Flow.label_to_slug(r["label"]) for r in rule_sets if r["ruleset_type"] not in RuleSet.TYPE_WAIT } # make a regex that matches a context reference to the .text on any result from these replace_pattern = r"flow\.(" + "|".join(non_waiting) + ")\.text" replace_regex = regex.compile(replace_pattern, flags=regex.UNICODE | regex.IGNORECASE | regex.MULTILINE) replace_with = "step.value" # for every action in this flow, replace such references with @step.text for actionset in json_flow.get("action_sets", []): for action in actionset.get("actions", []): if action["type"] in ["reply", "send", "say", "email"]: msg = action["msg"] if isinstance(msg, str): action["msg"] = replace_regex.sub(replace_with, msg) else: for lang, text in msg.items(): msg[lang] = replace_regex.sub(replace_with, text) return json_flow
def create_message_event(cls, org, user, campaign, relative_to, offset, unit, message, delivery_hour=-1): if campaign.org != org: # pragma: no cover raise ValueError("Org mismatch") flow = Flow.create_single_message(org, user, message) if isinstance(message, dict): message = json.dumps(message) return cls.objects.create(campaign=campaign, relative_to=relative_to, offset=offset, unit=unit, event_type=cls.TYPE_MESSAGE, message=message, flow=flow, delivery_hour=delivery_hour, created_by=user, modified_by=user)
def create_message_event(cls, org, user, campaign, relative_to, offset, unit, message, delivery_hour=-1, base_language=None): if campaign.org != org: # pragma: no cover raise ValueError("Org mismatch") if isinstance(message, six.string_types): base_language = org.primary_language.iso_code if org.primary_language else 'base' message = {base_language: message} flow = Flow.create_single_message(org, user, message, base_language) return cls.objects.create(campaign=campaign, relative_to=relative_to, offset=offset, unit=unit, event_type=cls.TYPE_MESSAGE, message=message, flow=flow, delivery_hour=delivery_hour, created_by=user, modified_by=user)
def save(self): """ Create or update our campaign event """ campaign = self.validated_data.get('campaign') offset = self.validated_data.get('offset') unit = self.validated_data.get('unit') delivery_hour = self.validated_data.get('delivery_hour') relative_to = self.validated_data.get('relative_to') message = self.validated_data.get('message') flow = self.validated_data.get('flow') if self.instance: # we are being set to a flow if flow: self.instance.flow = flow self.instance.event_type = CampaignEvent.TYPE_FLOW self.instance.message = None # we are being set to a message else: translations, base_language = message self.instance.message = translations # if we aren't currently a message event, we need to create our hidden message flow if self.instance.event_type != CampaignEvent.TYPE_MESSAGE: self.instance.flow = Flow.create_single_message( self.context['org'], self.context['user'], translations, base_language) self.instance.event_type = CampaignEvent.TYPE_MESSAGE # otherwise, we can just update that flow else: # set our single message on our flow self.instance.flow.update_single_message_flow( translations, base_language) # update our other attributes self.instance.offset = offset self.instance.unit = unit self.instance.delivery_hour = delivery_hour self.instance.relative_to = relative_to self.instance.save() self.instance.update_flow_name() else: if flow: self.instance = CampaignEvent.create_flow_event( self.context['org'], self.context['user'], campaign, relative_to, offset, unit, flow, delivery_hour) else: translations, base_language = message self.instance = CampaignEvent.create_message_event( self.context['org'], self.context['user'], campaign, relative_to, offset, unit, translations, delivery_hour, base_language) self.instance.update_flow_name() return self.instance
def __init__(self, user, *args, **kwargs): super().__init__(*args, **kwargs) self.user = user self.fields["omnibox"].set_user(user) flows = Flow.get_triggerable_flows(user.get_org()) self.fields["flow"].queryset = flows
def create_flow(self): start = int(time.time() * 1000) % 1000000 definition = dict( action_sets=[ dict(uuid=uuid(start + 1), x=1, y=1, destination=uuid(start + 5), actions=[ dict(type='reply', msg='What is your favorite color?') ]), dict(uuid=uuid(start + 2), x=2, y=2, destination=None, actions=[dict(type='reply', msg='I love orange too!')]), dict(uuid=uuid(start + 3), x=3, y=3, destination=None, actions=[dict(type='reply', msg='Blue is sad. :(')]), dict( uuid=uuid(start + 4), x=4, y=4, destination=None, actions=[dict(type='reply', msg='That is a funny color.')]) ], rule_sets=[ dict(uuid=uuid(start + 5), x=5, y=5, label='color', response_type='C', rules=[ dict(uuid=uuid(start + 12), destination=uuid(start + 2), test=dict(type='contains', test='orange'), category="Orange"), dict(uuid=uuid(start + 13), destination=uuid(start + 3), test=dict(type='contains', test='blue'), category="Blue"), dict(uuid=uuid(start + 14), destination=uuid(start + 4), test=dict(type='true'), category="Other"), dict(uuid=uuid(start + 15), test=dict(type='true'), category="Nothing") ]) # test case with no destination ], entry=uuid(start + 1)) flow = Flow.create(self.org, self.admin, "Color Flow") flow.update(definition) return flow
def post(self, request, *args, **kwargs): call = IVRCall.objects.filter(pk=kwargs['pk']).first() if not call: return HttpResponse("Not found", status=404) client = call.channel.get_ivr_client() if request.REQUEST.get('hangup', 0): if not request.user.is_anonymous(): user_org = request.user.get_org() if user_org and user_org.pk == call.org.pk: client.calls.hangup(call.external_id) return HttpResponse(json.dumps(dict(status='Canceled')), content_type="application/json") else: return HttpResponse("Not found", status=404) if client.validate(request): status = request.POST.get('CallStatus', None) duration = request.POST.get('CallDuration', None) call.update_status(status, duration) # update any calls we have spawned with the same for child in call.child_calls.all(): child.update_status(status, duration) child.save() call.save() # figure out if this is a callback due to an empty gather is_empty = '1' == request.GET.get('empty', '0') user_response = request.POST.copy() # if the user pressed pound, then record no digits as the input if is_empty: user_response['Digits'] = '' hangup = 'hangup' == user_response.get('Digits', None) if call.status in [IN_PROGRESS, RINGING] or hangup: if call.is_flow(): response = Flow.handle_call(call, user_response, hangup=hangup) return HttpResponse(unicode(response)) else: if call.status == COMPLETED: # if our call is completed, hangup run = FlowRun.objects.filter(call=call).first() if run: run.set_completed() return build_json_response(dict(message="Updated call status")) else: # pragma: no cover # raise an exception that things weren't properly signed raise ValidationError("Invalid request signature") return build_json_response(dict(message="Unhandled"))
def __init__(self, user, *args, **kwargs): super().__init__(*args, **kwargs) self.user = user org = user.get_org() flows = Flow.get_triggerable_flows(org) self.fields["start_datetime"].help_text = _("%s Time Zone" % org.timezone) self.fields["flow"].queryset = flows
def create_message_event(cls, org, user, campaign, relative_to, offset, unit, message, delivery_hour=-1): if campaign.org != org: # pragma: no cover raise ValueError("Org mismatch") flow = Flow.create_single_message(org, user, message) return cls.objects.create(campaign=campaign, relative_to=relative_to, offset=offset, unit=unit, event_type=MESSAGE_EVENT, message=message, flow=flow, delivery_hour=delivery_hour, created_by=user, modified_by=user)
def create_flow(self, name="Test Flow", *, flow_type=Flow.TYPE_MESSAGE, nodes=None, is_system=False, org=None): org = org or self.org flow = Flow.create(org, self.admin, name, flow_type=flow_type, is_system=is_system) if not nodes: nodes = [{ "uuid": "f3d5ccd0-fee0-4955-bcb7-21613f049eae", "actions": [{ "uuid": "f661e3f0-5148-4397-92ef-925629ad444d", "type": "send_msg", "text": "Hey everybody!" }], "exits": [{ "uuid": "72a3f1da-bde1-4549-a986-d35809807be8" }], }] definition = { "uuid": str(uuid4()), "name": name, "type": Flow.GOFLOW_TYPES[flow_type], "revision": 1, "spec_version": "13.1.0", "expire_after_minutes": Flow.DEFAULT_EXPIRES_AFTER, "language": "eng", "nodes": nodes, } flow.version_number = definition["spec_version"] flow.save() json_flow = Flow.migrate_definition(definition, flow) flow.save_revision(self.admin, json_flow) return flow
def post(self, request, *args, **kwargs): call = IVRCall.objects.filter(pk=kwargs['pk']).first() if not call: return HttpResponse("Not found", status=404) client = call.channel.get_ivr_client() if request.REQUEST.get('hangup', 0): if not request.user.is_anonymous(): user_org = request.user.get_org() if user_org and user_org.pk == call.org.pk: client.calls.hangup(call.external_id) return HttpResponse(json.dumps(dict(status='Canceled')), content_type="application/json") else: return HttpResponse("Not found", status=404) if client.validate(request): status = request.POST.get('CallStatus', None) duration = request.POST.get('CallDuration', None) call.update_status(status, duration) # update any calls we have spawned with the same for child in call.child_calls.all(): child.update_status(status, duration) child.save() call.save() # figure out if this is a callback due to an empty gather is_empty = '1' == request.GET.get('empty', '0') user_response = request.POST.copy() # if the user pressed pound, then record no digits as the input if is_empty: user_response['Digits'] = '' hangup = 'hangup' == user_response.get('Digits', None) if call.status == IN_PROGRESS or hangup: if call.is_flow(): response = Flow.handle_call(call, user_response, hangup=hangup) return HttpResponse(unicode(response)) else: if call.status == COMPLETED: # if our call is completed, hangup run = FlowRun.objects.filter(call=call).first() if run: run.set_completed() return build_json_response(dict(message="Updated call status")) else: # pragma: no cover # raise an exception that things weren't properly signed raise ValidationError("Invalid request signature") return build_json_response(dict(message="Unhandled"))
def __init__(self, user, *args, **kwargs): flows = Flow.get_triggerable_flows(user.get_org()) super().__init__(user, flows, *args, **kwargs) self.fields["flow"].required = False group_field = self.fields["action_join_group"] group_field.queryset = ContactGroup.user_groups.filter( org=self.user.get_org(), is_active=True).order_by("name") group_field.user = user
def create_flow(self, uuid_start=None, **kwargs): if 'org' not in kwargs: kwargs['org'] = self.org if 'user' not in kwargs: kwargs['user'] = self.user if 'name' not in kwargs: kwargs['name'] = "Color Flow" flow = Flow.create(**kwargs) flow.update(self.create_flow_definition(uuid_start)) return Flow.objects.get(pk=flow.pk)
def __init__(self, user, *args, **kwargs): flows = Flow.get_triggerable_flows(user.get_org()) super().__init__(user, flows, *args, **kwargs) self.fields["flow"].required = False group_field = self.fields["action_join_group"] group_field.queryset = ContactGroup.user_groups.filter(org=self.user.get_org(), is_active=True).order_by( "name" ) group_field.user = user
def save(self): """ Create or update our campaign event """ campaign = self.validated_data.get('campaign') offset = self.validated_data.get('offset') unit = self.validated_data.get('unit') delivery_hour = self.validated_data.get('delivery_hour') relative_to = self.validated_data.get('relative_to') message = self.validated_data.get('message') flow = self.validated_data.get('flow') if self.instance: # we are being set to a flow if flow: self.instance.flow = flow self.instance.event_type = CampaignEvent.TYPE_FLOW self.instance.message = None # we are being set to a message else: self.instance.message = message # if we aren't currently a message event, we need to create our hidden message flow if self.instance.event_type != CampaignEvent.TYPE_MESSAGE: self.instance.flow = Flow.create_single_message(self.context['org'], self.context['user'], message) self.instance.event_type = CampaignEvent.TYPE_MESSAGE # otherwise, we can just update that flow else: # set our single message on our flow self.instance.flow.update_single_message_flow(message=message) # update our other attributes self.instance.offset = offset self.instance.unit = unit self.instance.delivery_hour = delivery_hour self.instance.relative_to = relative_to self.instance.save() self.instance.update_flow_name() else: if flow: self.instance = CampaignEvent.create_flow_event(self.context['org'], self.context['user'], campaign, relative_to, offset, unit, flow, delivery_hour) else: self.instance = CampaignEvent.create_message_event(self.context['org'], self.context['user'], campaign, relative_to, offset, unit, message, delivery_hour) self.instance.update_flow_name() return self.instance
def test_icon(self): from temba.campaigns.models import Campaign from temba.triggers.models import Trigger from temba.flows.models import Flow from temba.utils.templatetags.temba import icon campaign = Campaign.create(self.org, self.admin, 'Test Campaign', self.create_group('Test group', [])) flow = Flow.create(self.org, self.admin, 'Test Flow') trigger = Trigger.objects.create(org=self.org, keyword='trigger', flow=flow, created_by=self.admin, modified_by=self.admin) self.assertEquals('icon-instant', icon(campaign)) self.assertEquals('icon-feed', icon(trigger)) self.assertEquals('icon-tree', icon(flow)) self.assertEquals("", icon(None))
def start_call(self, call, to, from_, status_callback): channel = call.channel Contact.get_or_create(channel.org, channel.created_by, urns=[(TEL_SCHEME, to)]) # Verboice differs from Twilio in that they expect the first block of twiml up front payload = unicode(Flow.handle_call(call, {})) # now we can post that to verboice url = "%s?%s" % (self.endpoint, urlencode(dict(channel=self.verboice_channel, address=to))) response = requests.post(url, data=payload, auth=self.auth).json() # store the verboice call id in our IVRCall call.external_id = response['call_id'] call.status = IN_PROGRESS call.save()
def form_valid(self, form): keyword = form.cleaned_data['keyword'] join_group = form.cleaned_data['action_join_group'] start_flow = form.cleaned_data['flow'] send_msg = form.cleaned_data['response'] group_flow = Flow.create_join_group_flow(self.request.user, join_group, send_msg, start_flow) Trigger.objects.create(created_by=self.request.user, modified_by=self.request.user, org=self.request.user.get_org(), keyword=keyword, trigger_type=KEYWORD_TRIGGER, flow=group_flow) analytics.track(self.request.user.username, 'temba.trigger_created_register', dict(name=join_group.name)) response = self.render_to_response(self.get_context_data(form=form)) response['REDIRECT'] = self.get_success_url() return response
def post(self, request, *args, **kwargs): from twilio.util import RequestValidator call = IVRCall.objects.filter(pk=kwargs['pk']).first() if not call: return HttpResponse("Not found", status=404) client = call.channel.get_ivr_client() if request.REQUEST.get('hangup', 0): if not request.user.is_anonymous(): user_org = request.user.get_org() if user_org and user_org.pk == call.org.pk: client.calls.hangup(call.external_id) return HttpResponse(json.dumps(dict(status='Canceled')), content_type="application/json") else: return HttpResponse("Not found", status=404) if client.validate(request): call.update_status(request.POST.get('CallStatus', None), request.POST.get('CallDuration', None)) call.save() hangup = 'hangup' == request.POST.get('Digits', None) if call.status == IN_PROGRESS or hangup: if call.is_flow(): response = Flow.handle_call(call, request.POST, hangup=hangup) return HttpResponse(unicode(response)) else: if call.status == COMPLETED: # if our call is completed, hangup run = FlowRun.objects.filter(call=call).first() if run: run.set_completed() run.expire() return build_json_response(dict(message="Updated call status")) else: # pragma: no cover # raise an exception that things weren't properly signed raise ValidationError("Invalid request signature") return build_json_response(dict(message="Unhandled"))
def start_call(self, call, to, from_, status_callback): channel = call.channel contact = Contact.get_or_create(channel.created_by, channel.org, urns=[(TEL_SCHEME, to)]) # Verboice differs from Twilio in that they expect the first block of twiml up front payload = unicode(Flow.handle_call(call, {})) # our config should have our http basic auth parameters and verboice channel config = channel.config_json() # now we can post that to verboice url = "%s?%s" % (self.endpoint, urlencode(dict(channel=config['channel'], address=to))) response = requests.post(url, data=payload, auth=(config['username'], config['password'])).json() # store the verboice call id in our IVRCall call.external_id = response['call_id'] call.status = IN_PROGRESS call.save()
def post(self, request, *args, **kwargs): from twilio.util import RequestValidator call = IVRCall.objects.filter(pk=kwargs['pk']).first() if not call: return HttpResponse("Not found", status=404) client = call.channel.get_ivr_client() if request.REQUEST.get('hangup', 0): if not request.user.is_anonymous(): user_org = request.user.get_org() if user_org and user_org.pk == call.org.pk: client.calls.hangup(call.external_id) return HttpResponse(json.dumps(dict(status='Canceled')), content_type="application/json") else: return HttpResponse("Not found", status=404) validator = RequestValidator(client.auth[1]) signature = request.META.get('HTTP_X_TWILIO_SIGNATURE', '') base_url = settings.TEMBA_HOST url = "https://%s%s" % (base_url, request.get_full_path()) # make sure this is coming from twilio if validator.validate(url, request.POST, signature): call.update_status(request.POST.get('CallStatus', None), request.POST.get('CallDuration', None)) call.save() if call.status == IN_PROGRESS: if call.is_flow(): response = Flow.handle_call(call, request.POST) return HttpResponse(unicode(response)) else: return build_json_response(dict(message="Updated call status")) else: # pragma: no cover # raise an exception that things weren't properly signed raise ValidationError("Invalid request signature") return build_json_response(dict(message="Unhandled"))
def pre_save(self, request, obj): # if it's before, negate the offset if self.cleaned_data['direction'] == 'B': obj.offset = -obj.offset if self.cleaned_data['unit'] == 'H' or self.cleaned_data['unit'] == 'M': obj.delivery_hour = -1 # if its a message flow, set that accordingly if self.cleaned_data['event_type'] == 'M': if not obj.flow_id or not obj.flow.is_active or obj.flow.flow_type != Flow.MESSAGE: obj.flow = Flow.create_single_message(request.user.get_org(), request.user, self.cleaned_data['message']) # set our single message on our flow obj.flow.update_single_message_flow(message=self.cleaned_data['message']) obj.message = self.cleaned_data['message'] # otherwise, it's an event that runs an existing flow else: obj.flow = Flow.objects.get(pk=self.cleaned_data['flow_to_start'])
def test_rule_first_ivr_flow(self): # connect it and check our client is configured self.org.connect_twilio("TEST_SID", "TEST_TOKEN") self.org.save() # import an ivr flow self.import_file('rule-first-ivr') flow = Flow.objects.filter(name='Rule First IVR').first() user_settings = self.admin.get_settings() user_settings.tel = '+18005551212' user_settings.save() # start our flow eric = self.create_contact('Eric Newcomer', number='+13603621737') eric.is_test = True eric.save() Contact.set_simulation(True) flow.start([], [eric]) # should be using the usersettings number in test mode self.assertEquals('Placing test call to +1 800-555-1212', ActionLog.objects.all().first().text) # we should have an outbound ivr call now call = IVRCall.objects.filter(direction=OUTGOING).first() self.assertEquals(0, call.get_duration()) self.assertIsNotNone(call) self.assertEquals('CallSid', call.external_id) # after a call is picked up, twilio will call back to our server post_data = dict(CallSid='CallSid', CallStatus='in-progress', CallDuration=20) response = self.client.post(reverse('ivr.ivrcall_handle', args=[call.pk]), post_data) self.assertContains(response, '<Say>Thanks for calling!</Say>') # make sure a message from the person on the call goes to the # inbox since our flow doesn't handle text messages msg = self.create_msg(direction='I', contact=eric, text="message during phone call") self.assertFalse(Flow.find_and_handle(msg))
def create_flow(self, definition=None, **kwargs): if "org" not in kwargs: kwargs["org"] = self.org if "user" not in kwargs: kwargs["user"] = self.user if "name" not in kwargs: kwargs["name"] = "Color Flow" flow = Flow.create(**kwargs) if not definition: # if definition isn't provided, generate simple single message flow node_uuid = str(uuid4()) definition = { "version": 10, "flow_type": "F", "base_language": "eng", "entry": node_uuid, "action_sets": [ { "uuid": node_uuid, "x": 0, "y": 0, "actions": [ {"msg": {"eng": "Hey everybody!"}, "media": {}, "send_all": False, "type": "reply"} ], "destination": None, } ], "rule_sets": [], } flow.version_number = definition["version"] flow.save() json_flow = FlowRevision.migrate_definition(definition, flow) flow.update(json_flow) return flow
def form_valid(self, form): keyword = form.cleaned_data["keyword"] join_group = form.cleaned_data["action_join_group"] start_flow = form.cleaned_data["flow"] send_msg = form.cleaned_data["response"] org = self.request.user.get_org() group_flow = Flow.create_join_group(org, self.request.user, join_group, send_msg, start_flow) Trigger.objects.create( created_by=self.request.user, modified_by=self.request.user, org=self.request.user.get_org(), keyword=keyword, trigger_type=Trigger.TYPE_KEYWORD, flow=group_flow, ) analytics.track(self.request.user.username, "temba.trigger_created_register", dict(name=join_group.name)) response = self.render_to_response(self.get_context_data(form=form)) response["REDIRECT"] = self.get_success_url() return response
def test_rule_first_ivr_flow(self): # connect it and check our client is configured self.org.connect_twilio("TEST_SID", "TEST_TOKEN", self.admin) self.org.save() # import an ivr flow flow = self.get_flow("rule_first_ivr") user_settings = self.admin.get_settings() user_settings.tel = "+18005551212" user_settings.save() # start our flow test_contact = Contact.get_test_contact(self.admin) Contact.set_simulation(True) flow.start([], [test_contact]) # should be using the usersettings number in test mode self.assertEquals("Placing test call to +1 800-555-1212", ActionLog.objects.all().first().text) # we should have an outbound ivr call now call = IVRCall.objects.filter(direction=OUTGOING).first() self.assertEquals(0, call.get_duration()) self.assertIsNotNone(call) self.assertEquals("CallSid", call.external_id) # after a call is picked up, twilio will call back to our server post_data = dict(CallSid="CallSid", CallStatus="in-progress", CallDuration=20) response = self.client.post(reverse("ivr.ivrcall_handle", args=[call.pk]), post_data) self.assertContains(response, "<Say>Thanks for calling!</Say>") # make sure a message from the person on the call goes to the # inbox since our flow doesn't handle text messages msg = self.create_msg(direction="I", contact=test_contact, text="message during phone call") self.assertFalse(Flow.find_and_handle(msg))