Пример #1
0
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
Пример #2
0
    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))
Пример #3
0
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)
Пример #4
0
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)
Пример #5
0
 def datepicker_fmt(d: datetime):
     return datetime_to_str(d, "%Y-%m-%d %H:%M", self.org.timezone)
Пример #6
0
    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))
Пример #7
0
    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)
Пример #8
0
# 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',
Пример #9
0
# 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",
Пример #10
0
    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
Пример #11
0
    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.")
Пример #12
0
# 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",
Пример #13
0
    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")