def msg_as_task(msg): """ Used to serialize msgs as tasks to courier """ msg_json = dict(id=msg.id, uuid=str(msg.uuid) if msg.uuid else "", org_id=msg.org_id, channel_id=msg.channel_id, channel_uuid=msg.channel.uuid, contact_id=msg.contact_id, contact_urn_id=msg.contact_urn_id, status=msg.status, direction=msg.direction, text=msg.text, high_priority=msg.high_priority, urn=msg.contact_urn.urn, error_count=msg.error_count, attachments=msg.attachments, metadata=msg.get_metadata(), response_to_id=msg.response_to_id, external_id=msg.external_id, tps_cost=msg.channel.calculate_tps_cost(msg), next_attempt=datetime_to_str(msg.next_attempt, ms=True), created_on=datetime_to_str(msg.created_on, ms=True), modified_on=datetime_to_str(msg.modified_on, ms=True), queued_on=datetime_to_str(msg.queued_on, ms=True), sent_on=datetime_to_str(msg.sent_on, ms=True)) if msg.contact_urn.auth: # pragma: no cover msg_json['contact_urn_auth'] = msg.contact_urn.auth return msg_json
def test_update_near_day_boundary(self): self.org.timezone = pytz.timezone("US/Eastern") self.org.save() tz = self.org.timezone omnibox = omnibox_serialize(self.org, [], [self.joe], json_encode=True) self.login(self.admin) post_data = dict(text="A scheduled message to Joe", omnibox=omnibox, sender=self.channel.pk, schedule=True) self.client.post(reverse("msgs.broadcast_send"), post_data, follow=True) bcast = Broadcast.objects.get() sched = bcast.schedule update_url = reverse("schedules.schedule_update", args=[sched.pk]) # way off into the future, but at 11pm NYT start_date = datetime(2050, 1, 3, 23, 0, 0, 0) start_date = tz.localize(start_date) start_date = pytz.utc.normalize(start_date.astimezone(pytz.utc)) post_data = dict() post_data["repeat_period"] = "D" post_data["start"] = "later" post_data["start_datetime"] = (datetime_to_str(start_date, "%Y-%m-%d %H:%M", self.org.timezone), ) self.client.post(update_url, post_data) sched = Schedule.objects.get(pk=sched.pk) # 11pm in NY should be 4am UTC the next day self.assertEqual("2050-01-04 04:00:00+00:00", str(sched.next_fire)) start_date = datetime(2050, 1, 3, 23, 45, 0, 0) start_date = tz.localize(start_date) start_date = pytz.utc.normalize(start_date.astimezone(pytz.utc)) post_data = dict() post_data["repeat_period"] = "D" post_data["start"] = "later" post_data["start_datetime"] = (datetime_to_str(start_date, "%Y-%m-%d %H:%M", self.org.timezone), ) self.client.post(update_url, post_data) sched = Schedule.objects.get(pk=sched.pk) # next fire should fall at the right hour and minute self.assertIn("04:45:00+00:00", str(sched.next_fire))
def format_datetime(context, dtime): if dtime.tzinfo is None: dtime = dtime.replace(tzinfo=pytz.utc) tz = pytz.UTC org = context.get("user_org") if org: tz = org.timezone dtime = dtime.astimezone(tz) if org: return org.format_datetime(dtime) return datetime_to_str(dtime, "%d-%m-%Y %H:%M", tz)
def format_datetime(context, dt, seconds: bool = False): if dt.tzinfo is None: dt = dt.replace(tzinfo=pytz.utc) tz = pytz.UTC org = context.get("user_org") if org: tz = org.timezone dt = dt.astimezone(tz) if org: return org.format_datetime(dt, seconds=seconds) fmt = "%d-%m-%Y %H:%M:%S" if seconds else "%d-%m-%Y %H:%M" return datetime_to_str(dt, fmt, tz)
def datepicker_fmt(d: datetime): return datetime_to_str(d, "%Y-%m-%d %H:%M", self.org.timezone)
def test_schedule_ui(self): self.login(self.admin) # test missing recipients omnibox = omnibox_serialize(self.org, [], [], json_encode=True) post_data = dict(text="message content", omnibox=omnibox, sender=self.channel.pk, schedule=True) response = self.client.post(reverse("msgs.broadcast_send"), post_data, follow=True) self.assertContains(response, "At least one recipient is required") # missing message omnibox = omnibox_serialize(self.org, [], [self.joe], json_encode=True) post_data = dict(text="", omnibox=omnibox, sender=self.channel.pk, schedule=True) response = self.client.post(reverse("msgs.broadcast_send"), post_data, follow=True) self.assertContains(response, "This field is required") # finally create our message post_data = dict(text="A scheduled message to Joe", omnibox=omnibox, sender=self.channel.pk, schedule=True) headers = {"HTTP_X_PJAX": "True"} response = self.client.post(reverse("msgs.broadcast_send"), post_data, **headers) self.assertIn("/broadcast/schedule_read", response["Temba-Success"]) # should have a schedule with no next fire bcast = Broadcast.objects.get() schedule = bcast.schedule self.assertIsNone(schedule.next_fire) self.assertEqual(Schedule.REPEAT_NEVER, schedule.repeat_period) # fetch our formax page response = self.client.get(response["Temba-Success"]) self.assertContains(response, "id-schedule") broadcast = response.context["object"] # update our message omnibox = omnibox_serialize(self.org, [], [self.joe], json_encode=True) post_data = dict(message="An updated scheduled message", omnibox=omnibox) self.client.post(reverse("msgs.broadcast_update", args=[broadcast.pk]), post_data) self.assertEqual(Broadcast.objects.get(id=broadcast.id).text, {"base": "An updated scheduled message"}) start = datetime(2045, 9, 19, hour=10, minute=15, second=0, microsecond=0) start = pytz.utc.normalize(self.org.timezone.localize(start)) # update the schedule post_data = dict( repeat_period=Schedule.REPEAT_WEEKLY, repeat_days_of_week="W", start="later", start_datetime=datetime_to_str(start, "%Y-%m-%d %H:%M", self.org.timezone), ) response = self.client.post(reverse("schedules.schedule_update", args=[broadcast.schedule.pk]), post_data) # assert out next fire was updated properly schedule.refresh_from_db() self.assertEqual(Schedule.REPEAT_WEEKLY, schedule.repeat_period) self.assertEqual("W", schedule.repeat_days_of_week) self.assertEqual(10, schedule.repeat_hour_of_day) self.assertEqual(15, schedule.repeat_minute_of_hour) self.assertEqual(start, schedule.next_fire) # manually set our fire in the past schedule.next_fire = timezone.now() - timedelta(days=1) schedule.save(update_fields=["next_fire"]) self.assertIsNotNone(str(schedule))
def _create_contact_batch(self, batch): """ Bulk creates a batch of contacts from flat representations """ for c in batch: c['object'] = Contact(org=c['org'], name=c['name'], language=c['language'], is_stopped=c['is_stopped'], is_blocked=c['is_blocked'], is_active=c['is_active'], created_by=c['user'], created_on=c['created_on'], modified_by=c['user'], modified_on=c['modified_on']) Contact.objects.bulk_create([c['object'] for c in batch]) # now that contacts have pks, bulk create the actual URN, value and group membership objects batch_urns = [] batch_values = [] batch_memberships = [] for c in batch: org = c['org'] c['urns'] = [] if c['tel']: c['urns'].append( ContactURN(org=org, contact=c['object'], priority=50, scheme=TEL_SCHEME, path=c['tel'], identity=URN.from_tel(c['tel']))) if c['twitter']: c['urns'].append( ContactURN(org=org, contact=c['object'], priority=50, scheme=TWITTER_SCHEME, path=c['twitter'], identity=URN.from_twitter(c['twitter']))) if c['gender']: batch_values.append( Value(org=org, contact=c['object'], contact_field=org.cache['fields']['gender'], string_value=c['gender'])) if c['age']: batch_values.append( Value(org=org, contact=c['object'], contact_field=org.cache['fields']['age'], string_value=str(c['age']), decimal_value=c['age'])) if c['joined']: batch_values.append( Value(org=org, contact=c['object'], contact_field=org.cache['fields']['joined'], string_value=datetime_to_str(c['joined']), datetime_value=c['joined'])) if c['ward']: batch_values.append( Value(org=org, contact=c['object'], contact_field=org.cache['fields']['ward'], string_value=c['ward'].name, location_value=c['ward'])) if c['district']: batch_values.append( Value(org=org, contact=c['object'], contact_field=org.cache['fields']['district'], string_value=c['district'].name, location_value=c['district'])) if c['state']: batch_values.append( Value(org=org, contact=c['object'], contact_field=org.cache['fields']['state'], string_value=c['state'].name, location_value=c['state'])) for g in c['groups']: batch_memberships.append( ContactGroup.contacts.through(contact=c['object'], contactgroup=g)) batch_urns += c['urns'] ContactURN.objects.bulk_create(batch_urns) Value.objects.bulk_create(batch_values) ContactGroup.contacts.through.objects.bulk_create(batch_memberships)
# allow this much absolute change from previous results (milliseconds) ALLOWED_CHANGE_MAXIMUM = 50 # allow this much percentage change from previous results ALLOWED_CHANGE_PERCENTAGE = 5 # a org specific context used in URL generation URL_CONTEXT_TEMPLATE = { 'first-group': lambda org: ContactGroup.user_groups.filter(org=org).order_by('id').first( ).uuid, 'last-group': lambda org: ContactGroup.user_groups.filter(org=org).order_by('-id').first( ).uuid, '1-year-ago': lambda org: datetime_to_str(now() - timedelta(days=365), get_datetime_format(org.get_dayfirst())[0]) } TEST_URLS = ( '/api/v2/channels.json', '/api/v2/channel_events.json', '/api/v2/contacts.json', '/api/v2/contacts.json?deleted=true', '/api/v2/contacts.json?group={first-group}', '/api/v2/groups.json', '/api/v2/fields.json', '/api/v2/labels.json', '/api/v2/messages.json?folder=incoming', '/api/v2/messages.json?folder=inbox', '/api/v2/messages.json?folder=flows', '/api/v2/messages.json?folder=archived',
# allow this maximum request time (milliseconds) DEFAULT_ALLOWED_MAXIMUM = 3000 # allow this much absolute change from previous results (milliseconds) ALLOWED_CHANGE_MAXIMUM = 50 # allow this much percentage change from previous results ALLOWED_CHANGE_PERCENTAGE = 5 # a org specific context used in URL generation URL_CONTEXT_TEMPLATE = { "first-group": lambda org: ContactGroup.user_groups.filter(org=org).order_by("id").first().uuid, "last-group": lambda org: ContactGroup.user_groups.filter(org=org).order_by("-id").first().uuid, "1-year-ago": lambda org: datetime_to_str( now() - timedelta(days=365), get_datetime_format(org.get_dayfirst())[0], pytz.UTC ), } TEST_URLS = ( "/api/v2/channels.json", "/api/v2/channel_events.json", "/api/v2/contacts.json", "/api/v2/contacts.json?deleted=true", "/api/v2/contacts.json?group={first-group}", "/api/v2/groups.json", "/api/v2/fields.json", "/api/v2/labels.json", "/api/v2/messages.json?folder=incoming", "/api/v2/messages.json?folder=inbox", "/api/v2/messages.json?folder=flows",
def trigger_flow_webhook_legacy(cls, run, webhook_url, node_uuid, msg, action='POST', resthook=None, headers=None): # pragma: no cover flow = run.flow org = flow.org contact = run.contact api_user = get_api_user() json_time = datetime_to_str(timezone.now()) values = [] for key, result in six.iteritems(run.get_results()): category = result[FlowRun.RESULT_CATEGORY] values.append({ 'node': result[FlowRun.RESULT_NODE_UUID], 'label': result[FlowRun.RESULT_NAME], 'category': category, 'category_localized': result.get(FlowRun.RESULT_CATEGORY_LOCALIZED, category), 'text': result[FlowRun.RESULT_INPUT], 'value': result[FlowRun.RESULT_VALUE], 'rule_value': result[FlowRun.RESULT_VALUE], 'time': datetime_to_str( iso8601.parse_date(result[FlowRun.RESULT_CREATED_ON])), }) if msg: text = msg.text attachments = msg.get_attachments() channel = msg.channel contact_urn = msg.contact_urn else: # if the action is on the first node we might not have an sms (or channel) yet channel = None text = None attachments = [] contact_urn = contact.get_urn() steps = [] for step in run.steps.prefetch_related( 'messages', 'broadcasts').order_by('arrived_on'): steps.append( dict(type=step.step_type, node=step.step_uuid, arrived_on=datetime_to_str(step.arrived_on), left_on=datetime_to_str(step.left_on), text=step.get_text(), value=step.rule_value)) data = dict(channel=channel.id if channel else -1, channel_uuid=channel.uuid if channel else None, relayer=channel.id if channel else -1, flow=flow.id, flow_uuid=flow.uuid, flow_name=flow.name, flow_base_language=flow.base_language, run=run.id, text=text, attachments=[a.url for a in attachments], step=six.text_type(node_uuid), phone=contact.get_urn_display(org=org, scheme=TEL_SCHEME, formatted=False), contact=contact.uuid, contact_name=contact.name, urn=six.text_type(contact_urn), values=json.dumps(values), steps=json.dumps(steps), time=json_time) if not action: # pragma: needs cover action = 'POST' webhook_event = cls.objects.create(org=org, event=cls.TYPE_FLOW, channel=channel, data=json.dumps(data), run=run, try_count=1, action=action, resthook=resthook, created_by=api_user, modified_by=api_user) status_code = -1 message = "None" body = None start = time.time() # webhook events fire immediately since we need the results back try: # no url, bail! if not webhook_url: raise Exception("No webhook_url specified, skipping send") # only send webhooks when we are configured to, otherwise fail if settings.SEND_WEBHOOKS: requests_headers = http_headers(extra=headers) # some hosts deny generic user agents, use Temba as our user agent if action == 'GET': response = requests.get(webhook_url, headers=requests_headers, timeout=10) else: response = requests.post(webhook_url, data=data, headers=requests_headers, timeout=10) body = response.text if body: body = body.strip() status_code = response.status_code else: print("!! Skipping WebHook send, SEND_WEBHOOKS set to False") body = 'Skipped actual send' status_code = 200 # process the webhook response try: response_json = json.loads(body, object_pairs_hook=OrderedDict) # only update if we got a valid JSON dictionary or list if not isinstance(response_json, dict) and not isinstance( response_json, list): raise ValueError( "Response must be a JSON dictionary or list, ignoring response." ) run.update_fields(response_json) message = "Webhook called successfully." except ValueError: message = "Response must be a JSON dictionary, ignoring response." if 200 <= status_code < 300: webhook_event.status = cls.STATUS_COMPLETE else: webhook_event.status = cls.STATUS_FAILED message = "Got non 200 response (%d) from webhook." % response.status_code raise Exception("Got non 200 response (%d) from webhook." % response.status_code) except Exception as e: import traceback traceback.print_exc() webhook_event.status = cls.STATUS_FAILED message = "Error calling webhook: %s" % six.text_type(e) finally: webhook_event.save() # make sure our message isn't too long if message: message = message[:255] request_time = (time.time() - start) * 1000 contact = None if webhook_event.run: contact = webhook_event.run.contact result = WebHookResult.objects.create(event=webhook_event, contact=contact, url=webhook_url, status_code=status_code, body=body, message=message, data=urlencode(data, doseq=True), request_time=request_time, created_by=api_user, modified_by=api_user) # if this is a test contact, add an entry to our action log if run.contact.is_test: log_txt = "Triggered <a href='%s' target='_log'>webhook event</a> - %d" % ( reverse('api.log_read', args=[webhook_event.pk ]), status_code) ActionLog.create(run, log_txt, safe=True) return result
def test_trigger_schedule(self, mock_async_start): self.login(self.admin) create_url = reverse("triggers.trigger_schedule") flow = self.create_flow() background_flow = self.get_flow("background") self.get_flow("media_survey") chester = self.create_contact("Chester", phone="+250788987654") shinoda = self.create_contact("Shinoda", phone="+250234213455") linkin_park = self.create_group("Linkin Park", [chester, shinoda]) stromae = self.create_contact("Stromae", phone="+250788645323") response = self.client.get(create_url) # the normal flow and background flow should be options but not the surveyor flow self.assertEqual( list(response.context["form"].fields["flow"].queryset), [background_flow, flow]) now = timezone.now() tommorrow = now + timedelta(days=1) omnibox_selection = omnibox_serialize(flow.org, [linkin_park], [stromae], True) # try to create trigger without a flow or omnibox response = self.client.post( create_url, { "omnibox": omnibox_selection, "repeat_period": "D", "start": "later", "start_datetime": datetime_to_str(tommorrow, "%Y-%m-%d %H:%M", self.org.timezone), }, ) self.assertEqual(list(response.context["form"].errors.keys()), ["flow"]) self.assertFalse(Trigger.objects.all()) self.assertFalse(Schedule.objects.all()) # this time provide a flow but leave out omnibox.. response = self.client.post( create_url, { "flow": flow.id, "repeat_period": "D", "start": "later", "start_datetime": datetime_to_str(tommorrow, "%Y-%m-%d %H:%M", self.org.timezone), }, ) self.assertEqual(list(response.context["form"].errors.keys()), ["omnibox"]) self.assertFalse(Trigger.objects.all()) self.assertFalse(Schedule.objects.all()) # ok, really create it self.client.post( create_url, { "flow": flow.id, "omnibox": omnibox_selection, "repeat_period": "D", "start": "later", "start_datetime": datetime_to_str(tommorrow, "%Y-%m-%d %H:%M", self.org.timezone), }, ) self.assertEqual(Trigger.objects.count(), 1) self.client.post( create_url, { "flow": flow.id, "omnibox": omnibox_selection, "repeat_period": "D", "start": "later", "start_datetime": datetime_to_str(tommorrow, "%Y-%m-%d %H:%M", self.org.timezone), }, ) self.assertEqual(2, Trigger.objects.all().count()) trigger = Trigger.objects.order_by("id").last() self.assertTrue(trigger.schedule) self.assertEqual(trigger.schedule.repeat_period, "D") self.assertEqual(set(trigger.groups.all()), {linkin_park}) self.assertEqual(set(trigger.contacts.all()), {stromae}) update_url = reverse("triggers.trigger_update", args=[trigger.pk]) # try to update a trigger without a flow response = self.client.post( update_url, { "omnibox": omnibox_selection, "repeat_period": "O", "start": "later", "start_datetime": datetime_to_str(now, "%Y-%m-%d %H:%M", self.org.timezone), }, ) self.assertEqual(list(response.context["form"].errors.keys()), ["flow"]) # provide flow this time, update contact self.client.post( update_url, { "flow": flow.id, "omnibox": omnibox_serialize(flow.org, [linkin_park], [shinoda], True), "repeat_period": "D", "start": "later", "start_datetime": datetime_to_str(now, "%Y-%m-%d %H:%M", self.org.timezone), }, ) trigger.refresh_from_db() self.assertTrue(trigger.schedule) self.assertEqual(trigger.schedule.repeat_period, "D") self.assertTrue(trigger.schedule.next_fire) self.assertEqual(set(trigger.groups.all()), {linkin_park}) self.assertEqual(set(trigger.contacts.all()), {shinoda}) # can't submit weekly repeat without specifying the days to repeat on response = self.client.post( update_url, { "flow": flow.id, "omnibox": omnibox_selection, "repeat_period": "W", "start": "later", "start_datetime": datetime_to_str(now, "%Y-%m-%d %H:%M", self.org.timezone), }, ) self.assertFormError(response, "form", "__all__", "Must specify at least one day of the week") # or submit with invalid days response = self.client.post( update_url, { "flow": flow.id, "omnibox": omnibox_selection, "repeat_period": "W", "repeat_days_of_week": "X", "start": "later", "start_datetime": datetime_to_str(now, "%Y-%m-%d %H:%M", self.org.timezone), }, ) self.assertFormError( response, "form", "repeat_days_of_week", "Select a valid choice. X is not one of the available choices.")
# allow this much absolute change from previous results (milliseconds) ALLOWED_CHANGE_MAXIMUM = 50 # allow this much percentage change from previous results ALLOWED_CHANGE_PERCENTAGE = 5 # a org specific context used in URL generation URL_CONTEXT_TEMPLATE = { "first-group": lambda org: ContactGroup.user_groups.filter(org=org).order_by("id").first( ).uuid, "last-group": lambda org: ContactGroup.user_groups.filter(org=org).order_by("-id").first( ).uuid, "1-year-ago": lambda org: datetime_to_str(now() - timedelta(days=365), org.get_datetime_formats()[0], pytz.UTC), } TEST_URLS = ( "/api/v2/channels.json", "/api/v2/channel_events.json", "/api/v2/contacts.json", "/api/v2/contacts.json?deleted=true", "/api/v2/contacts.json?group={first-group}", "/api/v2/groups.json", "/api/v2/fields.json", "/api/v2/labels.json", "/api/v2/messages.json?folder=incoming", "/api/v2/messages.json?folder=inbox", "/api/v2/messages.json?folder=flows", "/api/v2/messages.json?folder=archived",
def test_update(self): self.login(self.admin) # create a schedule broadcast self.client.post( reverse("msgs.broadcast_send"), { "text": "A scheduled message to Joe", "omnibox": omnibox_serialize(self.org, [], [self.joe], True), "sender": self.channel.id, "schedule": True, }, ) schedule = Broadcast.objects.get().schedule update_url = reverse("schedules.schedule_update", args=[schedule.id]) # viewer can't access self.login(self.user) response = self.client.get(update_url) self.assertLoginRedirect(response) # editor can access self.login(self.editor) response = self.client.get(update_url) self.assertEqual(response.status_code, 200) # as can admin user self.login(self.admin) response = self.client.get(update_url) self.assertEqual(response.status_code, 200) now = timezone.now() tommorrow = now + timedelta(days=1) # user in other org can't make changes self.login(self.admin2) response = self.client.post(update_url, {"start": "never", "repeat_period": "D"}) self.assertLoginRedirect(response) # check schedule is unchanged schedule.refresh_from_db() self.assertEqual("O", schedule.repeat_period) self.login(self.admin) # update to never start response = self.client.post(update_url, {"start": "never", "repeat_period": "O"}) self.assertEqual(302, response.status_code) schedule.refresh_from_db() self.assertIsNone(schedule.next_fire) self.client.post(update_url, {"start": "stop", "repeat_period": "O"}) schedule.refresh_from_db() self.assertIsNone(schedule.next_fire) response = self.client.post( update_url, { "start": "now", "repeat_period": "O", "start_datetime": datetime_to_str(now, "%Y-%m-%d %H:%M", self.org.timezone), }, ) self.assertEqual(302, response.status_code) schedule.refresh_from_db() self.assertEqual(schedule.repeat_period, "O") self.assertFalse(schedule.next_fire) response = self.client.post( update_url, { "repeat_period": "D", "start": "later", "start_datetime": datetime_to_str(tommorrow, "%Y-%m-%d %H:%M", self.org.timezone), }, ) self.assertEqual(302, response.status_code) schedule.refresh_from_db() self.assertEqual(schedule.repeat_period, "D") response = self.client.post( update_url, { "repeat_period": "D", "start": "later", "start_datetime": datetime_to_str(tommorrow, "%Y-%m-%d %H:%M", self.org.timezone), }, ) self.assertEqual(302, response.status_code) schedule.refresh_from_db() self.assertEqual(schedule.repeat_period, "D") # can't omit repeat_days_of_week for weekly response = self.client.post( update_url, { "repeat_period": "W", "start": "later", "repeat_days_of_week": "", "start_datetime": datetime_to_str(now, "%Y-%m-%d %H:%M", self.org.timezone), }, ) self.assertFormError(response, "form", "__all__", "Must specify at least one day of the week") schedule.refresh_from_db() self.assertEqual(schedule.repeat_period, "D") # unchanged self.assertEqual(schedule.repeat_days_of_week, "") # can't set repeat_days_of_week to invalid day response = self.client.post( update_url, { "repeat_period": "W", "start": "later", "repeat_days_of_week": "X", "start_datetime": datetime_to_str(now, "%Y-%m-%d %H:%M", self.org.timezone), }, ) self.assertFormError( response, "form", "repeat_days_of_week", "Select a valid choice. X is not one of the available choices." ) schedule.refresh_from_db() self.assertEqual(schedule.repeat_period, "D") # unchanged self.assertEqual(schedule.repeat_days_of_week, "") # can set to valid days response = self.client.post( update_url, { "repeat_period": "W", "start": "later", "repeat_days_of_week": ["M", "F"], "start_datetime": datetime_to_str(now, "%Y-%m-%d %H:%M", self.org.timezone), }, ) self.assertEqual(response.status_code, 302) schedule.refresh_from_db() self.assertEqual(schedule.repeat_period, "W") self.assertEqual(schedule.repeat_days_of_week, "MF")