Example #1
0
 def setUp(self):
     super(ViewsEmptyTestCase, self).setUp()
     user = UserFactory(password='******')
     user.is_superuser = True
     user.save()
     self.assertTrue(
         self.client.login(username=user.username, password='******'))
Example #2
0
class ViewsInProgressJobTestCase(ResponseCheckerMixin, TestJobBase):
    """Exercise views when a job is in progress"""
    @property
    def faux_output_dir(self):
        return os.path.normpath(os.path.join(self.output_path, '..'))

    def setUp(self):
        super(ViewsInProgressJobTestCase, self).setUp()
        self.user = UserFactory(password='******')
        self.user.is_superuser = True
        self.user.save()
        self.assertTrue(self.client.login(username=self.user.username, password='******'))

        # I would like to create an in-progress job "organically", but that's hard to do under
        # test conditions. Instead I simulate the conditions of in-progress job.
        with open(os.path.join(self.output_path, ROLLGEN_FLAG_FILENAME), 'w') as f:
            f.write(' ')

        self.dirname = os.path.basename(self.output_path)

    def test_overview_view(self):
        """Generate a job view and test the context it passes to the template"""
        with override_settings(ROLLGEN_OUTPUT_DIR=self.faux_output_dir):
            response = self.client.get(reverse('rollgen:overview'))

        self.assertResponseOK(response)
        self.assertTemplateUsed(response, 'rollgen/overview.html')

        context = response.context
        expected_keys = ('jobs', )
        self.assertTrue(set(expected_keys) < set(context.keys()))
        self.assertEqual(1, len(context['jobs']))
        self.assertTrue(context['jobs'][0].in_progress)
        # There should not be a link to the job page.
        self.assertNotContains(response, reverse('rollgen:browse_job_offices',
                               args=(self.dirname, )))

    def test_browse_job_offices_view(self):
        """Generate a job offices view and test the context it passes to the template"""
        with override_settings(ROLLGEN_OUTPUT_DIR=self.faux_output_dir):
            response = self.client.get(reverse('rollgen:browse_job_offices', args=(self.dirname, )))

        self.assertResponseOK(response)
        self.assertTemplateUsed(response, 'rollgen/job_in_progress_view.html')
        context = response.context
        expected_keys = ('job', )
        self.assertTrue(set(expected_keys) < set(context.keys()))
        self.assertTrue(context['job'].in_progress)

    def test_browse_job_centers_view(self):
        """Generate a job centers view and test the context it passes to the template"""
        with override_settings(ROLLGEN_OUTPUT_DIR=self.faux_output_dir):
            response = self.client.get(reverse('rollgen:browse_job_centers', args=(self.dirname, )))

        self.assertResponseOK(response)
        self.assertTemplateUsed(response, 'rollgen/job_in_progress_view.html')
        context = response.context
        expected_keys = ('job', )
        self.assertTrue(set(expected_keys) < set(context.keys()))
        self.assertTrue(context['job'].in_progress)
Example #3
0
 def create_staff_user(self):
     user = UserFactory(username=self.username,
                        email=self.email,
                        password=self.password)
     user.is_staff = True
     user.save()
     return user
class TestElectionSelection(TestCase):

    def setUp(self):
        self.election_1 = ElectionFactory(
            polling_start_time=now() - timedelta(days=10),
            polling_end_time=now() - timedelta(days=9)
        )
        self.election_2 = ElectionFactory(
            polling_start_time=now() - timedelta(days=4),
            polling_end_time=now() - timedelta(days=3)
        )
        self.staff_user = UserFactory()
        self.staff_user.is_staff = True
        self.staff_user.save()

    def test_election_in_session(self):
        assert self.client.login(username=self.staff_user.username, password=DEFAULT_USER_PASSWORD)
        self.client.get(reverse('vr_dashboard:election-day'))
        self.assertEqual(self.client.session[ELECTION_SESSION_KEY],
                         Election.objects.get_most_current_election().id)
        self.client.get(reverse('vr_dashboard:election-day') + '?election=%d' % self.election_1.id)
        self.assertEqual(self.client.session[ELECTION_SESSION_KEY], self.election_1.id)
        self.client.get(reverse('vr_dashboard:election-day-center'))
        self.assertEqual(self.client.session[ELECTION_SESSION_KEY], self.election_1.id)
        self.client.get(reverse('vr_dashboard:election-day-center') +
                        '?election=%d' % self.election_2.id)
        self.assertEqual(self.client.session[ELECTION_SESSION_KEY], self.election_2.id)
        self.client.get(reverse('vr_dashboard:election-day'))
        self.assertEqual(self.client.session[ELECTION_SESSION_KEY], self.election_2.id)
Example #5
0
class TestStaffView(TestCase):
    def setUp(self):
        self.password = '******'
        self.user = UserFactory(username='******', password=self.password)
        self.user.is_staff = True
        self.user.save()
        self.assertTrue(self.client.login(username=self.user.username, password=self.password))
        self.staff_url = reverse('staff')
        self.httptester_url = reverse('httptester-index')

    def test_non_production_setting_shows_httptester(self):
        """Only superusers can see the httptester URL"""
        self.user.is_superuser = True
        self.user.save()
        rsp = self.client.get(self.staff_url)
        self.assertContains(rsp, self.httptester_url)

    @override_settings(ENVIRONMENT='production')
    def test_production_settings_no_httptester(self):
        rsp = self.client.get(self.staff_url)
        self.assertNotContains(rsp, self.httptester_url)

    def test_rollgen_visible_for_superuser(self):
        """ensure superusers see the rollgen URL"""
        self.user.is_staff = True
        self.user.is_superuser = True
        self.user.save()
        self.client.logout()
        self.assertTrue(self.client.login(username=self.user.username, password=self.password))

        rsp = self.client.get(self.staff_url)
        self.assertContains(rsp, reverse('rollgen:overview'))

    def test_rollgen_visible_for_rollgen_group_member(self):
        """ensure staff users in the rollgen_view_job group see the rollgen URL in the staff view"""
        self.user.is_staff = True
        self.user.groups.add(Group.objects.get(name='rollgen_view_job'))
        self.user.save()
        self.client.logout()
        self.assertTrue(self.client.login(username=self.user.username, password=self.password))
        rsp = self.client.get(self.staff_url)
        self.assertContains(rsp, reverse('rollgen:overview'))

    def test_rollgen_not_visible_for_rollgen_group_nonmember(self):
        """ensure staff users not in the rollgen_view_job group don't see the rollgen URL"""
        rsp = self.client.get(self.staff_url)
        self.assertNotContains(rsp, reverse('rollgen:overview'))

    def test_staff_page_redirects_to_login_if_not_logged_in(self):
        self.client.logout()
        rsp = self.client.get(self.staff_url)
        self.assertRedirects(rsp, reverse(settings.LOGIN_URL) + "?next=" + self.staff_url)

    def test_staff_page_responds_403_for_non_staff(self):
        self.user.is_staff = False
        self.user.save()
        rsp = self.client.get(self.staff_url)
        self.assertEqual(FORBIDDEN, rsp.status_code)
Example #6
0
 def create_staff_user(self):
     user = UserFactory(username=self.username, email=self.email, password=self.password)
     user.is_staff = True
     user.save()
     if self.model:
         content_type = ContentType.objects.get_for_model(self.model)
         for perm in self.permissions:
             user.user_permissions.add(Permission.objects.get(content_type=content_type,
                                                              codename=perm))
     return user
Example #7
0
class ViewsFailedJobTestCase(ResponseCheckerMixin, TestJobBase):
    """Exercise views when a job has failed"""
    @property
    def faux_output_dir(self):
        return os.path.normpath(os.path.join(self.output_path, '..'))

    def setUp(self):
        super(ViewsFailedJobTestCase, self).setUp()
        self.user = UserFactory(password='******')
        self.user.is_superuser = True
        self.user.save()
        self.assertTrue(self.client.login(username=self.user.username, password='******'))

        # Generate a center with no voters to force an error when the job runs.
        self.no_voters_center = RegistrationCenterFactory(name=generate_arabic_place_name())

        phase = 'in-person'

        self.input_arguments['phase'] = phase
        self.input_arguments['center_ids'] = [self.no_voters_center.center_id]

        with override_settings(ROLLGEN_OUTPUT_DIR=self.faux_output_dir):
            self.job = Job(phase, [self.no_voters_center], self.input_arguments, self.user.username,
                           self.output_path)
            try:
                self.job.generate_rolls()
            except NoVotersError as exception:
                # This is expected. (In fact, it's the whole point of the test.)
                handle_job_exception(exception, self.job.output_path)

        self.dirname = os.path.basename(self.job.output_path)

    def test_browse_job_offices_view(self):
        """Generate a job offices view and test the context it passes to the template"""
        with override_settings(ROLLGEN_OUTPUT_DIR=self.faux_output_dir):
            response = self.client.get(reverse('rollgen:browse_job_offices', args=(self.dirname, )))

        self.assertResponseOK(response)
        self.assertTemplateUsed(response, 'rollgen/job_failed_view.html')
        context = response.context
        expected_keys = ('job', )
        self.assertTrue(set(expected_keys) < set(context.keys()))
        self.assertEqual(JobOverview(self.output_path).raw_metadata, context['job'].raw_metadata)

    def test_browse_job_centers_view(self):
        """Generate a job centers view and test the context it passes to the template"""
        with override_settings(ROLLGEN_OUTPUT_DIR=self.faux_output_dir):
            response = self.client.get(reverse('rollgen:browse_job_centers', args=(self.dirname, )))

        self.assertResponseOK(response)
        self.assertTemplateUsed(response, 'rollgen/job_failed_view.html')
        context = response.context
        expected_keys = ('job', )
        self.assertTrue(set(expected_keys) < set(context.keys()))
        self.assertEqual(JobOverview(self.output_path).raw_metadata, context['job'].raw_metadata)
Example #8
0
 def create_staff_user(self):
     user = UserFactory(username=self.username,
                        email=self.email,
                        password=self.password)
     user.is_staff = True
     user.save()
     if self.model:
         content_type = ContentType.objects.get_for_model(self.model)
         for perm in self.permissions:
             user.user_permissions.add(
                 Permission.objects.get(content_type=content_type,
                                        codename=perm))
     return user
Example #9
0
    def setUp(self):
        super(ViewsNonEmptyTestCase, self).setUp()
        user = UserFactory(password='******')
        user.is_superuser = True
        user.save()
        self.assertTrue(self.client.login(username=user.username, password='******'))

        phase = 'in-person'

        self.input_arguments['phase'] = phase

        with override_settings(ROLLGEN_OUTPUT_DIR=self.faux_output_dir):
            self.job = Job(phase, [self.center], self.input_arguments, self.user.username,
                           self.output_path)
            self.job.generate_rolls()

        self.dirname = os.path.basename(self.job.output_path)
Example #10
0
    def setUp(self):
        super(ViewsNonEmptyTestCase, self).setUp()
        user = UserFactory(password='******')
        user.is_superuser = True
        user.save()
        self.assertTrue(
            self.client.login(username=user.username, password='******'))

        phase = 'in-person'

        self.input_arguments['phase'] = phase

        with override_settings(ROLLGEN_OUTPUT_DIR=self.faux_output_dir):
            self.job = Job(phase, [self.center], self.input_arguments,
                           self.user.username, self.output_path)
            self.job.generate_rolls()

        self.dirname = os.path.basename(self.job.output_path)
class TestWithNoRegistrationData(TestCase):

    def setUp(self):
        create_test_data.create(num_registrations=0, num_registration_dates=0)
        tasks.election_day()
        tasks.registrations()
        self.staff_user = UserFactory()
        self.staff_user.is_staff = True
        self.staff_user.save()

    def test_basic_operation(self):
        """  For the time being, simply ensure that the VR dashboard pages (and report generation tasks)
        don't blow up when there aren't any registrations. """
        assert self.client.login(username=self.staff_user.username, password=DEFAULT_USER_PASSWORD)
        for uri_name in ALL_URI_NAMES:
            uri = reverse(URI_NAMESPACE + uri_name)
            rsp = self.client.get(uri)
            self.assertEqual(200, rsp.status_code,
                             'Request to %s failed with status %d' % (uri, rsp.status_code))
class TestWithNoGeneratedReports(TestCase):

    @classmethod
    def setUpClass(cls):  # No database changes
        empty_report_store()

    def setUp(self):
        self.staff_user = UserFactory()
        self.staff_user.is_staff = True
        self.staff_user.save()

    def test_no_report_error(self):
        expected_page_flags = {
            'election-day': ['election_day_overview_page', 'staff_page'],
            'election-day-center': ['election_day_center_page', 'staff_page'],
            'election-day-hq': ['election_day_hq_page', 'staff_page'],
            'election-day-preliminary': ['election_day_preliminary_votes_page', 'staff_page']
        }
        expected_status_code = 503
        assert self.client.login(username=self.staff_user.username, password=DEFAULT_USER_PASSWORD)
        for uri_name in ALL_URI_NAMES:
            uri = reverse(URI_NAMESPACE + uri_name)
            rsp = self.client.get(uri)
            self.assertEqual(expected_status_code, rsp.status_code,
                             'Request to %s had status %d instead of %d'
                             % (uri, rsp.status_code, expected_status_code))
            if uri_name in expected_page_flags:
                for expected in expected_page_flags[uri_name]:
                    self.assertIn(
                        expected, rsp.context,
                        'Error page for %s doesn\'t set page flag "%s"' % (uri_name, expected)
                    )
        for uri_name in ['election-day-office-n', 'election-day-center-n']:
            uri = reverse(URI_NAMESPACE + uri_name, args=[999999])
            rsp = self.client.get(uri)
            self.assertEqual(expected_status_code, rsp.status_code,
                             'Request to %s had status %d instead of %d'
                             % (uri, rsp.status_code, expected_status_code))
Example #13
0
class ValidateUploadTest(ResponseCheckerMixin, LibyaTest):

    def setUp(self):
        self.home_url = reverse('upload_broadcast')
        self.staff_user = UserFactory(username='******', password='******')
        self.staff_user.is_staff = True
        self.staff_user.save()
        self.staff_user.user_permissions.add(Permission.objects.get(codename='add_broadcast'))
        self.staff_user.user_permissions.add(Permission.objects.get(codename='approve_broadcast'))
        self.client.login(username='******', password='******')

    def test_valid_upload_creates_bulkmessages(self):
        f = SimpleUploadedFile('file.csv', GOOD_CSV_DATA)
        data = {'name': u'A name', 'csv': f}
        self.client.post(self.home_url, data=data, follow=True)
        self.assertEqual(BulkMessage.objects.count(), 2)

    def test_line_endings(self):
        f = SimpleUploadedFile('file.csv', LINE_ENDINGS)
        data = {'name': u'A name', 'csv': f}
        self.client.post(self.home_url, data=data, follow=True)
        self.assertEqual(BulkMessage.objects.count(), 3)

    def test_invalid_characters(self):
        f = SimpleUploadedFile('file.csv', INVALID_CHARACTERS)
        data = {'name': u'A name', 'csv': f}
        rsp = self.client.post(self.home_url, data=data, follow=True)
        form = rsp.context['form']
        self.assertFalse(form.is_valid())
        self.assertIn(u'The uploaded file had invalid characters.', form['csv'].errors[0])
        self.assertEqual(BulkMessage.objects.count(), 0)

    def test_invalid_phone(self):
        f = SimpleUploadedFile('file.csv', INVALID_PHONE_DATA)
        data = {'name': u'A name', 'csv': f}
        rsp = self.client.post(self.home_url, data=data, follow=True)
        form = rsp.context['form']
        self.assertFalse(form.is_valid())
        self.assertIn(u'Unable to parse number', form['csv'].errors[0])
        self.assertEqual(BulkMessage.objects.count(), 0)

    def test_blank_message(self):
        f = SimpleUploadedFile('file.csv', BLANK_MESSAGE_DATA)
        data = {'name': u'A name', 'csv': f}
        rsp = self.client.post(self.home_url, data=data, follow=True)
        form = rsp.context['form']
        self.assertFalse(form.is_valid())
        self.assertIn(u'Message is blank', form['csv'].errors[0])
        self.assertEqual(BulkMessage.objects.count(), 0)

    def test_invalid_csv_structure(self):
        f = SimpleUploadedFile('file.csv', FAILS_PARSING_DATA)
        data = {'name': u'A name', 'csv': f}
        rsp = self.client.post(self.home_url, data=data, follow=True)
        form = rsp.context['form']
        self.assertFalse(form.is_valid())
        self.assertIn(u'The row should only have the following columns', form['csv'].errors[0])
        self.assertEqual(BulkMessage.objects.count(), 0)

    def test_bulk_create(self):
        # we bulk create every 10000 records, so make sure we use a number
        # in between checkpoints to make sure we don't leave any strays
        f = SimpleUploadedFile('file.csv', GOOD_CSV_DATA * 11000)
        data = {'name': u'A name', 'csv': f}
        self.client.post(self.home_url, data=data, follow=True)
        self.assertEqual(BulkMessage.objects.count(), 22000)

    # Test permissions
    def test_no_perms_cant_see_home_page(self):
        self.staff_user.user_permissions.remove(Permission.objects.get(codename='add_broadcast'))
        self.staff_user.user_permissions.remove(
            Permission.objects.get(codename='approve_broadcast'))
        # logged in as regular staff user
        self.assertForbidden(self.client.get(self.home_url))

    def test_add_broadcast_perm_sees_detail_page_but_cant_review(self):
        self.staff_user.user_permissions.remove(
            Permission.objects.get(codename='approve_broadcast'))
        self.staff_user.user_permissions.add(Permission.objects.get(codename='read_broadcast'))
        batch = BatchFactory(status=Batch.PENDING)
        broadcast = Broadcast.objects.create(
            created_by=batch.created_by,
            batch=batch,
            audience=Broadcast.CUSTOM,
            message=batch.description
        )
        review_url = reverse('approve_reject_broadcast', kwargs=dict(broadcast_id=broadcast.id))
        # can see detail page
        rsp = self.client.get(reverse('read_broadcast', kwargs=dict(pk=broadcast.id)))
        self.assertEqual(200, rsp.status_code)
        # and can't manually go to review url
        rsp = self.client.post(review_url)
        self.assertEqual(403, rsp.status_code)

    def test_approve_perm_cant_directly_post_upload_form(self):
        self.staff_user.user_permissions.remove(Permission.objects.get(codename='add_broadcast'))
        self.staff_user.user_permissions.add(Permission.objects.get(codename='approve_broadcast'))
        f = SimpleUploadedFile('file.csv', GOOD_CSV_DATA)
        data = {'name': u'A name', 'csv': f}
        self.assertForbidden(self.client.post(reverse('upload_broadcast'), data=data))
        self.assertEqual(BulkMessage.objects.count(), 0)

    def test_user_with_both_permissions_can_upload(self):
        self.staff_user.user_permissions.add(Permission.objects.get(codename='add_broadcast'))
        self.staff_user.user_permissions.add(Permission.objects.get(codename='approve_broadcast'))
        rsp = self.client.get(reverse('upload_broadcast'))
        # can see upload form
        self.assertIn('form', rsp.context)
        f = SimpleUploadedFile('file.csv', GOOD_CSV_DATA)
        data = {'name': u'A name', 'csv': f}
        rsp = self.client.post(reverse('upload_broadcast'), data=data, follow=True)
        # and messages are uploaded
        self.assertEqual(BulkMessage.objects.count(), 2)

    def test_user_with_both_permissions_can_approve(self):
        self.staff_user.user_permissions.add(Permission.objects.get(codename='add_broadcast'))
        self.staff_user.user_permissions.add(Permission.objects.get(codename='approve_broadcast'))
        batch = BatchFactory(status=Batch.PENDING)
        broadcast = Broadcast.objects.create(
            created_by=batch.created_by,
            batch=batch,
            audience=Broadcast.CUSTOM,
            message=batch.description
        )
        kwargs = dict(broadcast_id=broadcast.id)
        review_url = reverse('approve_reject_broadcast', kwargs=kwargs)
        data = {'approve': True}
        rsp = self.client.post(review_url, data=data)
        self.assertEqual(302, rsp.status_code)
Example #14
0
class TestEndToEnd(TestCase):
    def setUp(self):
        self.staff_user = UserFactory()
        self.staff_user.is_staff = True
        self.staff_user.save()
        assert self.client.login(username=self.staff_user.username,
                                 password=DEFAULT_USER_PASSWORD)
        self.reporting_user = test_reports.TEST_USERNAME
        self.reporting_password = test_reports.TEST_PASSWORD
        REPORT_USER_DB[self.reporting_user] = self.reporting_password
        # Pick a start time that represents different days in Libya vs UTC
        tz = timezone(settings.TIME_ZONE)
        polling_start_time = astz(FUTURE_DAY.replace(hour=22), tz)
        polling_end_time = tz.normalize(polling_start_time +
                                        timedelta(hours=16))
        self.election = ElectionFactory(
            polling_start_time=polling_start_time,
            polling_end_time=polling_end_time,
        )
        self.election_day_dt = self.election.polling_start_time
        # Create "decoy" election just to confirm that it doesn't break reports.
        decoy_start_time = tz.normalize(polling_start_time -
                                        timedelta(days=10))
        decoy_end_time = tz.normalize(decoy_start_time + timedelta(hours=16))
        ElectionFactory(
            polling_start_time=decoy_start_time,
            polling_end_time=decoy_end_time,
        )
        self.all_centers = []
        self.rc_1 = RegistrationCenterFactory()
        self.all_centers.append(self.rc_1)
        self.rc_2 = RegistrationCenterFactory()
        self.all_centers.append(self.rc_2)
        self.rc_3 = RegistrationCenterFactory()
        self.all_centers.append(self.rc_3)
        self.rc_4 = RegistrationCenterFactory()
        self.all_centers.append(self.rc_4)
        self.copy_of_rc_1 = RegistrationCenterFactory(copy_of=self.rc_1,
                                                      office=self.rc_1.office)
        self.all_centers.append(self.copy_of_rc_1)
        # rc_5 is inactive for this election
        self.rc_5 = RegistrationCenterFactory(office=self.rc_1.office)
        self.all_centers.append(self.rc_5)
        inactive_on_election = CenterClosedForElection(
            registration_center=self.rc_5, election=self.election)
        inactive_on_election.full_clean()
        inactive_on_election.save()
        self.all_office_ids = [center.office_id for center in self.all_centers]
        self.carrier_1 = BackendFactory()
        self.citizen_1 = CitizenFactory()

        # Create registrations on the 4 days leading up to election day
        # Put the registrations at different hours of the day to stress TZ handling.
        self.registration_dates = []
        self.registration_date_strs = []
        hour_of_day = 0
        for delta_days in range(10, 4, -1):
            assert hour_of_day < 24
            reg_date = astz(self.election_day_dt - timedelta(days=delta_days), tz)\
                .replace(hour=hour_of_day)
            hour_of_day += 4
            self.registration_dates.append(reg_date)
            self.registration_date_strs.append(reg_date.strftime('%Y-%m-%d'))
        self.yesterday_date, _ = calc_yesterday(self.registration_date_strs)
        self.yesterday_date_dm = self.yesterday_date.strftime('%d/%m')
        # yesterday_date is a date; get a datetime form
        self.yesterday_date_dt = tz.localize(
            datetime(self.yesterday_date.year, self.yesterday_date.month,
                     self.yesterday_date.day, 0, 0, 0))
        self.staff_phone_number = STAFF_PHONE_NUMBER_PATTERN % 12345

    def _describe_infra(self):
        logger.info("Registration Centers:")
        for center in self.all_centers:
            logger.info('  %s' % center)
            logger.info('    office id %d' % center.office.id)
            if center.copy_of:
                logger.info('    copy of center %d' % center.center_id)

    def _request(self, url, **extra):
        """ Request the specified URL using self.client, perform any common
        processing like checking the status code and logging the response.
        """
        logger.info(url)
        if extra:
            logger.info(extra)
        rsp = self.client.get(url, **extra)
        self.assertEqual(200, rsp.status_code)
        logger.info(rsp.content)
        return rsp

    def _request_csv(self, url, **extra):
        """ Like _request() above, but also parse the response body as a CSV
        as created by the voter registration dashboard and log in parsed form.

        This adds the query arg which specifies CSV rendering.
        """
        url += '?format=csv'
        rsp = self._request(url, **extra)
        content = rsp.content[2:]  # skip BOM
        reader = UnicodeReader(StringIO(content),
                               encoding="utf-16-le",
                               delimiter='\t')
        rows = []
        for row in reader:
            rows.append(row)
            logger.info(row)
        return rows

    def _msg_type_to_str(self, t):
        """ Get string form of the provided SMS message type.
        """
        # unicode() resolves the lazy translation object
        return unicode([x for x in SMS.MESSAGE_TYPES if x[0] == t][0][1])

    def _create_registrations(self, expected_stats):
        """ Create different numbers of registrations on each of the chosen registration dates
        so we can be sure that a count was assigned to the correct date.
        """
        msg_type_str = self._msg_type_to_str(SMS.REGISTRATION)
        expected_stats['message_stats'][msg_type_str] = dict()
        # Accumulators for by-center and total number of registrations
        expected_stats['by_center'][self.rc_1.center_id]['registrations'] = 0
        expected_stats['by_center'][self.rc_2.center_id]['registrations'] = 0
        expected_stats['by_center'][self.rc_3.center_id]['registrations'] = 0
        expected_stats['by_center'][self.rc_4.center_id]['registrations'] = 0
        expected_stats['by_center'][
            self.copy_of_rc_1.center_id]['registrations'] = ''
        expected_stats['by_center'][self.rc_5.center_id]['registrations'] = 0
        expected_stats['message_stats'][msg_type_str]['total'] = 0
        for i, reg_date in enumerate(self.registration_dates):
            regs_on_date = i + 1
            for j in range(regs_on_date):
                citizen = CitizenFactory()
                s = SMS(from_number='12345',
                        to_number='12345',
                        citizen=citizen,
                        direction=INCOMING,
                        message='my reg message',
                        msg_type=SMS.REGISTRATION,
                        message_code=MESSAGE_1,
                        carrier=self.carrier_1,
                        creation_date=reg_date)
                s.full_clean()
                s.save()
                r = Registration(citizen=citizen,
                                 registration_center=self.rc_1,
                                 archive_time=None,
                                 sms=s,
                                 creation_date=reg_date,
                                 modification_date=reg_date)
                r.full_clean()
                r.save()
                expected_stats['by_center'][
                    self.rc_1.center_id]['registrations'] += 1
            expected_stats['message_stats'][msg_type_str][
                'total'] += regs_on_date
            # Capture the count on "yesterday" (reported on SMS page)
            if self.yesterday_date == reg_date.date():
                expected_stats['message_stats'][msg_type_str][self.yesterday_date_dm] = \
                    regs_on_date

    @classmethod
    def _max_report_time(cls, time1, time2):
        """ Return the greater of the two times.  time1 can be None, which
        means long, long ago.
        """
        return max(time1 or time2, time2)

    def _create_election_day_data(self, expected_stats):
        """Create various types of election data for testing of the election
        day dashboard."""

        # Pick open times that could vary by date based on time zone.
        rc_1_open_time = self.election_day_dt.replace(hour=1, minute=23)
        rc_2_open_time = self.election_day_dt.replace(hour=10, minute=23)
        # This center open time is before the election time really starts,
        # so it will be reported under the corresponding office as an
        # unopened center.
        open_time_3 = self.election.start_time - timedelta(hours=6)

        # configure election day activities by registration center
        center_activities = []

        center_activities.append({
            'center':
            self.rc_1,
            'open_time':
            rc_1_open_time,
            'phone_number':
            STAFF_PHONE_NUMBER_PATTERN % 1,
        })

        center_activities.append({
            'center':
            self.rc_2,
            'open_time':
            rc_2_open_time,
            'phone_number':
            STAFF_PHONE_NUMBER_PATTERN % 1,
            'prelim_time':
            self.election_day_dt,
            'prelim_option':
            9,
            'prelim_votes':
            7312,  # four digits to test intcomma formatting
            'period_4_time':
            rc_2_open_time + timedelta(hours=6),
            'period_4_count':
            79,
            # period "5" is a report for period 4 sent on following day
            'period_5_time':
            self.election_day_dt + timedelta(days=1),
            'period_5_count':
            82,
        })

        center_activities.append({
            'center':
            self.rc_3,
            'open_time':
            open_time_3,
            'phone_number':
            STAFF_PHONE_NUMBER_PATTERN % 2,
        })

        center_activities.append({
            'center': self.rc_4,
            # DOES NOT SEND CenterOpen or anything else
        })

        center_activities.append({
            'center': self.copy_of_rc_1,
            # The copy center opened, coincidentally at the same time as the copied center.
            'open_time': rc_1_open_time,
            'phone_number': STAFF_PHONE_NUMBER_PATTERN % 3,
            # vote report for period 2
            'period_2_time': self.election_day_dt,
            'period_2_count': 4321,  # four digits to test intcomma formatting
        })

        center_activities.append({
            'center': self.rc_5,
            # DOES NOT SEND CenterOpen or anything else
            # This shares an office id with rc_1, and is also marked as
            # inactive for this particular election.
        })

        # shortcuts into dictionaries
        expected_center_stats = expected_stats['by_center']
        expected_office_stats = expected_stats['by_office']
        expected_summary_stats = expected_stats['summary']

        # Clear office-level summaries
        # (Some offices will be repeated, but it doesn't matter.)
        for activity in center_activities:
            office_id = activity['center'].office_id
            for key in ('opened', 'closed', 'not_reported_1', 'not_reported_2',
                        'not_reported_3', 'not_reported_4', 'unopened'):
                expected_office_stats[office_id][key] = 0
            expected_office_stats[office_id]['summary'] = deepcopy(
                EMPTY_SUMMARY)

        # Create the messages, increment/set counters/fields to represent
        # expected dashboard data.
        for activity in center_activities:
            # shortcuts specific to this center
            expected_for_this_center = expected_center_stats[
                activity['center'].center_id]
            expected_for_this_office = expected_office_stats[
                activity['center'].office_id]
            expected_summary_for_this_office = expected_for_this_office[
                'summary']

            last_report_dt = None  # track the last report from this center

            open_time = activity.get('open_time', None)
            if open_time:
                open_msg = CenterOpen(election=self.election,
                                      phone_number=activity['phone_number'],
                                      registration_center=activity['center'],
                                      creation_date=activity['open_time'])
                open_msg.full_clean()
                open_msg.save()
                last_report_dt = self._max_report_time(last_report_dt,
                                                       activity['open_time'])

            # It does not count as an open if it happened too early
            if open_time and open_time >= self.election.start_time:
                expected_for_this_center['ed_open'] = open_time.strftime(
                    '%d/%m %H:%M')
                expected_for_this_center['opened_hm'] = open_time.strftime(
                    '%H:%M')
                expected_for_this_office['opened'] += 1
                expected_summary_stats['opened'] += 1
                expected_summary_for_this_office['opened'] += 1
            else:
                expected_for_this_center['ed_open'] = None
                expected_for_this_center['opened_hm'] = None
                expected_for_this_office['unopened'] += 1
                expected_summary_stats['unopened'] += 1
                expected_summary_for_this_office['unopened'] += 1

            for period in ('1', '2', '3', '4'):
                report_time, report_count = \
                    activity.get('period_' + period + '_time', None), \
                    activity.get('period_' + period + '_count', None)

                if report_time:
                    r = PollingReport(election=self.election,
                                      phone_number=activity['phone_number'],
                                      registration_center=activity['center'],
                                      period_number=int(period),
                                      num_voters=report_count,
                                      creation_date=report_time)
                    r.full_clean()
                    r.save()
                    last_report_dt = self._max_report_time(
                        last_report_dt, report_time)

                    expected_for_this_center['votes_reported_' +
                                             period] = report_count
                    expected_for_this_center['reported_period_' +
                                             period] = 'has_reported'
                    expected_for_this_center['reported_period_' + period +
                                             '_count'] = report_count
                    expected_for_this_office['votes_reported_' +
                                             period] = report_count
                    expected_summary_stats['votes_reported_' +
                                           period] += report_count
                    expected_summary_for_this_office['votes_reported_' +
                                                     period] += report_count
                    if period == '4':  # got period 4 report, so didn't close
                        expected_for_this_center['is_closed'] = 'Yes'
                        expected_for_this_office['closed'] += 1
                else:
                    if open_time and open_time >= self.election.start_time:
                        # The effective time of the reports was just after period 2, so
                        # if this is the period 1 or 2 report then it is overdue, and
                        # if this is the period 3 or 4 report then it is not due yet.
                        flag = 'has_not_reported' if period in (
                            '1', '2') else 'not_due'
                        expected_for_this_center['reported_period_' +
                                                 period] = flag
                    else:
                        expected_for_this_center['reported_period_' +
                                                 period] = 'no_data'
                    expected_for_this_center['reported_period_' + period +
                                             '_count'] = 0
                    expected_for_this_office['not_reported_' + period] += 1
                    if period == '4':  # no period 4 report, so didn't close
                        expected_for_this_center['is_closed'] = 'No'

            # Very basic support for sending period 4 report on day after election
            #
            # It assumes that a period 4 report was also sent on election day, which
            # simplifies handling of votes_reported_4 counters and information on
            # closing.
            #
            # Period "5" is period 4 on the following day.
            period_5_time = activity.get('period_5_time', None)
            if period_5_time:
                period_5_count = activity['period_5_count']
                period_4_count = activity['period_4_count']

                r = PollingReport(election=self.election,
                                  phone_number=activity['phone_number'],
                                  registration_center=activity['center'],
                                  period_number=4,
                                  num_voters=period_5_count,
                                  creation_date=period_5_time)
                r.full_clean()
                r.save()
                last_report_dt = self._max_report_time(last_report_dt,
                                                       period_5_time)

                # Add in delta to prior period 4 report
                delta = period_5_count - period_4_count
                expected_for_this_center['votes_reported_4'] += delta
                expected_for_this_center['reported_period_4_count'] += delta
                expected_for_this_office['votes_reported_4'] += delta
                expected_summary_stats['votes_reported_4'] += delta
                expected_summary_for_this_office['votes_reported_4'] += delta

            prelim_time = activity.get('prelim_time', None)
            if prelim_time:
                prelim = PreliminaryVoteCount(
                    election=self.election,
                    phone_number=activity['phone_number'],
                    registration_center=activity['center'],
                    option=activity['prelim_option'],
                    num_votes=activity['prelim_votes'],
                    creation_date=prelim_time)
                prelim.full_clean()
                prelim.save()
                last_report_dt = self._max_report_time(last_report_dt,
                                                       prelim_time)

                expected_for_this_office['prelim'] = {
                    str(activity['prelim_option']):
                    intcomma(activity['prelim_votes'])
                }

            expected_for_this_center['last_report'] = \
                'Not Reported' if not last_report_dt else \
                last_report_dt.strftime('%d/%m %H:%M')

        # rc_5 is inactive for this election
        # (CenterClosedForElection created when center was created)
        # Now that the office 'summary' has been set up, note where inactive should show up.
        expected_center_stats[self.rc_5.center_id]['inactive'] = True
        expected_office_stats[self.rc_5.office.id]['summary']['inactive'] += 1

    def _create_sms_messages(self, expected_stats):
        """ Create SMS messages of a certain type at different times "yesterday".
        The times should have the same date in local TZ but different dates when
        the TZ is bungled somewhere.  By making them "yesterday", the counts will show
        up in the SMS page in the yesterday column.
        """
        msg_type = SMS.INVALID_CENTRE_CODE_LENGTH
        msg_type_str = self._msg_type_to_str(msg_type)
        expected_stats['message_stats'][msg_type_str] = dict()
        num_staff_messages = 8
        for msg_hour in range(num_staff_messages):
            msg_time = self.yesterday_date_dt.replace(hour=msg_hour, minute=23)
            s = SMS(from_number=self.staff_phone_number,
                    to_number='12345',
                    citizen=self.citizen_1,
                    direction=INCOMING,
                    message='my message',
                    msg_type=msg_type,
                    message_code=MESSAGE_1,
                    carrier=self.carrier_1,
                    creation_date=msg_time)
            s.full_clean()
            s.save()
        expected_stats['message_stats'][msg_type_str][self.yesterday_date_dt.strftime('%d/%m')] = \
            num_staff_messages
        expected_stats['message_stats'][msg_type_str][
            'total'] = num_staff_messages
        expected_stats['phone_history'][self.staff_phone_number] = {
            'message_count': num_staff_messages,
        }

    @classmethod
    def _str_to_int(cls, s):
        """ Convert the input numeric string, which may contain comma
        separators, to an int.
        """
        return int(s.replace(',', ''))

    @classmethod
    def _extract_int_from_span(cls, s):
        """Input s contains a number we need to convert to an int, embedded
        in a <span...></span>."""
        m = re.search('<span.*>(.+)</span>', s)
        if m:
            return cls._str_to_int(m.group(1))
        else:
            raise ValueError('Argument "%" does not contain <span></span>')

    def _parse_headline(self, headline, has_inactive=False):
        return {
            # Grab the number from u'3 centers have opened' and similar for unopened and inactive
            'opened':
            self._extract_int_from_span(headline['open_centers']),
            'unopened':
            self._extract_int_from_span(headline['unopen_centers']),
            'inactive':
            self._extract_int_from_span(headline['inactive_centers'])
            if has_inactive else 0,
            # Grab the number from u'Votes reported period 1: 0'
            'votes_reported_1':
            self._extract_int_from_span(headline['period1']),
            'votes_reported_2':
            self._extract_int_from_span(headline['period2']),
            'votes_reported_3':
            self._extract_int_from_span(headline['period3']),
            'votes_reported_4':
            self._extract_int_from_span(headline['period4']),
        }

    def _read_dashboard(self, actual_stats):
        """ Read parts of the dashboard that contain data we're testing and fill in the
        provided dictionary with the stats we observe.

        This only supports a small subset of the stats on the dashboard.
        """

        # Process the office-specific election day screen
        for office_id in self.all_office_ids:
            url = reverse('vr_dashboard:election-day-office-n',
                          args=(office_id, ))
            rsp = self._request(url)
            actual_stats['by_office'][office_id]['summary'] = \
                self._parse_headline(rsp.context['headline'], has_inactive=True)
            for row in rsp.context['office_centers_table']:
                center_id = row['polling_center_code']
                assert center_id in actual_stats['by_center'], \
                    'Center id %s is unexpected (not one of %s)' % \
                    (center_id, actual_stats['by_center'].keys())
                if 'opened_today' in row:
                    open_dt = row['opened_today']
                    actual_stats['by_center'][center_id]['ed_open'] = open_dt
                if 'inactive_for_election' in row:
                    actual_stats['by_center'][center_id]['inactive'] = True
                    self.assertEqual(row['tr_class'], 'inactive_for_election')
                for period in ['1', '2', '3', '4']:
                    votes_reported_period = 'votes_reported_' + period
                    if votes_reported_period in row:
                        actual_stats['by_center'][center_id][votes_reported_period] = \
                            row[votes_reported_period]
                        actual_stats['by_office'][office_id][votes_reported_period] = \
                            row[votes_reported_period]
                    actual_stats['by_center'][center_id]['reported_period_' + period] = \
                        row['reported_period_' + period]

        # prelim vote counts
        url = reverse('vr_dashboard:election-day-preliminary')
        rsp = self._request(url)
        for office in rsp.context['offices']:
            if PRELIMINARY_VOTE_COUNTS in office:
                actual_stats['by_office'][office['office_id']]['prelim'] = {
                    key: intcomma(value)
                    for key, value in
                    office[PRELIMINARY_VOTE_COUNTS].iteritems()
                }

        # process the sms screen
        url = reverse('vr_dashboard:sms')
        rsp = self._request(url)
        yesterday_str = rsp.context['sms_stats']['last_date']
        for stats in rsp.context['message_stats_by_type']:
            msg_type = stats['translated_sms_type']
            msg_yesterday_count = int(stats['last'])
            msg_total_count = int(stats['total'])
            actual_stats['message_stats'][msg_type] = {
                yesterday_str: msg_yesterday_count,
                'total': msg_total_count
            }

        # look at summary stats from main election_day page
        url = reverse('vr_dashboard:election-day')
        rsp = self._request(url)
        actual_stats['summary'] = self._parse_headline(rsp.context['headline'])
        for row in rsp.context['offices']:
            actual_office_stats = actual_stats['by_office'][row['office_id']]
            actual_office_stats['opened'] = row['opened']
            actual_office_stats['unopened'] = row['not_opened']
            actual_office_stats['closed'] = row['closed']
            actual_office_stats['not_reported_1'] = row['not_reported_1']
            actual_office_stats['not_reported_2'] = row['not_reported_2']
            actual_office_stats['not_reported_3'] = row['not_reported_3']
            actual_office_stats['not_reported_4'] = row['not_reported_4']

        # process the election day CSVs, testing something from each data row
        csv = self._request_csv(reverse('vr_dashboard:election-day'))
        # office data starts in 4th row, country-wide is last row
        for row in csv[3:-1]:
            office_id = int(row[0].split()[0])
            opened = int(row[2])
            # already grabbed from normal view, so make sure it matches
            self.assertEquals(actual_stats['by_office'][office_id]['opened'],
                              opened)

        csv = self._request_csv(reverse('vr_dashboard:election-day-center'))
        # center data starts in 4th row
        for row in csv[3:]:
            center_id = int(row[1])
            total_regs = '' if not row[3] else int(
                row[3])  # '' for copy center
            actual_stats['by_center'][center_id]['registrations'] = total_regs
            # inactive already read from election-day-office-n; make sure it matches
            active_flag = 'No' if 'inactive' in actual_stats['by_center'][
                center_id] else 'Yes'
            self.assertEqual(row[5], active_flag)
            actual_stats['by_center'][center_id][
                'opened_hm'] = row[6] if row[6] else None
            actual_stats['by_center'][center_id]['is_closed'] = row[11]
            # votes_reported_N already read from election-day-office-n; make sure it matches
            for period, where_in_row in (('1', 7), ('2', 8), ('3', 9), ('4',
                                                                        10)):
                period_key = 'votes_reported_' + period
                if row[where_in_row]:
                    self.assertEquals(
                        actual_stats['by_center'][center_id][period_key],
                        int(row[where_in_row]))
                else:
                    self.assertNotIn(period_key,
                                     actual_stats['by_center'][center_id])

        for office_id in self.all_office_ids:
            csv = self._request_csv(
                reverse('vr_dashboard:election-day-office-n',
                        args=[office_id]))
            # The center open time has already been read from the HTML, so just
            # make sure that it is consistent in the CSV.
            # Note that if there's no CenterOpen, the context will have None but
            # the screen and CSV will have '-'.
            center_id = int(csv[3][1])
            open_dt = csv[3][3]
            self.assertEqual(
                actual_stats['by_center'][center_id]['ed_open'] or '-',
                open_dt,
                'Open for center %d different between HTML and CSV (%s, %s)' %
                (center_id, actual_stats['by_center'][center_id]['ed_open'],
                 open_dt))
            for row in csv[3:]:
                center_id = int(row[1])
                # inactive already read from election-day-office-n; make sure it matches
                active_flag = 'No' if 'inactive' in actual_stats['by_center'][
                    center_id] else 'Yes'
                self.assertEqual(row[2], active_flag)

        for center in self.all_centers:
            center_id = center.center_id
            rsp = self._request(
                reverse('vr_dashboard:election-day-center-n',
                        args=[center_id]))
            stats = rsp.context['stats']
            actual_center_stats = actual_stats['by_center'][center_id]
            # The last opened time was already read (set to None if it didn't open);
            # make sure it matches the form on this page, which uses 'Not Opened'
            # instead of None for unopened.
            self.assertEqual(
                actual_center_stats['ed_open'] or 'Not Opened',
                stats['last_opened'],
                'Open for center %d different between by-office and by-center pages (%s, %s)'
                % (center_id, actual_center_stats['ed_open'],
                   stats['last_opened']))
            actual_center_stats['last_report'] = stats['last_report']
            for period in ('1', '2', '3', '4'):
                # votes for period is either '' or string form of number
                orig_key = 'reported_period_' + period
                new_key = 'reported_period_' + period + '_count'
                actual_center_stats[new_key] = \
                    self._extract_int_from_span(stats[orig_key]) if stats[orig_key] else 0
            # consistency between what was already extracted for 'inactive' and this response?
            self.assertEqual(
                'inactive' in actual_stats['by_center'][center_id],
                'inactive_for_election' in rsp.context['center'])

        # messages from staff phone
        history = self._request(
            reverse('vr_dashboard:phone-history') +
            '?phone=%s' % self.staff_phone_number)
        actual_stats['phone_history'][self.staff_phone_number] = {
            'message_count': len(history.context['sms_messages']),
        }

    def test(self):
        """ This is the one test case in this class; it performs end-to-end
        testing on reporting-api and vr-dashboard, using the following
        steps:

        1. create basic objects like RegistrationCenter (in setUp())
        2. initialize expected and actual stats dictionaries
        3. create different types of data, updating the expected stats
           dictionary to indicate what should appear on the dashboard
        4. run Celery tasks (directly) to regenerate reports based on the
           data created
        5. fetch and log the JSON reports
        6. scrape dashboard screens to get the actual stats reported
        7. compare expected and actual stats
        """
        expected_stats = {
            'by_center': {
                self.rc_1.center_id: {
                    # nothing yet
                },
                self.rc_2.center_id: {
                    # nothing yet
                },
                self.rc_3.center_id: {
                    # nothing yet
                },
                self.rc_4.center_id: {
                    # nothing yet
                },
                self.copy_of_rc_1.center_id: {
                    # nothing yet
                },
                self.rc_5.center_id: {
                    # nothing yet
                }
            },
            'by_office': {
                self.rc_1.office_id: {
                    # nothing yet
                },
                self.rc_2.office_id: {
                    # nothing yet
                },
                self.rc_3.office_id: {
                    # nothing yet
                },
                self.rc_4.office_id: {
                    # nothing yet
                },
                self.copy_of_rc_1.office_id: {
                    # nothing yet
                },
            },
            'summary': deepcopy(EMPTY_SUMMARY),
            'message_stats': {},
            'phone_history': {},
        }
        actual_stats = deepcopy(expected_stats)

        self._describe_infra()
        self._create_election_day_data(expected_stats)
        self._create_registrations(expected_stats)
        self._create_sms_messages(expected_stats)

        # Regenerate reports based on current database contents

        # Make the election day report code think it is now 3:35 p.m. on election day (just
        # after the period 2 time) so that it will flag missing period 1 and 2 reports.
        middle_of_election_day = self.election.polling_start_time.replace(
            hour=15, minute=35)
        with patch.object(reports,
                          'get_effective_reminder_time') as mock_reminder_time:
            mock_reminder_time.return_value = middle_of_election_day
            tasks.election_day()
            mock_reminder_time.assert_called()

        tasks.registrations()

        # Log the JSON reports to help with debugging
        credentials = base64.b64encode(self.reporting_user + ':' +
                                       self.reporting_password)
        auth_headers = {'HTTP_AUTHORIZATION': 'Basic ' + credentials}
        for report_rel_url in [
                test_reports.REGISTRATIONS_REL_URI,
                test_reports.ELECTION_DAY_LOG_REL_URI,
                test_reports.ELECTION_DAY_REPORT_REL_URI
        ]:
            url = test_reports.BASE_URI + report_rel_url
            self._request(url, **auth_headers)

        # Scrape the dashboard screens
        self._read_dashboard(actual_stats)

        # Compare expected and actual stats
        logger.info('Expected:')
        logger.info(expected_stats)
        logger.info('Actual:')
        logger.info(actual_stats)
        # Compare some slices of dictionary first to narrow in on the problem
        self.assertDictEqual(expected_stats['summary'],
                             actual_stats['summary'])
        self.assertDictEqual(expected_stats['by_center'],
                             actual_stats['by_center'])
        self.assertDictEqual(expected_stats['by_office'],
                             actual_stats['by_office'])
        # Everything
        self.assertDictEqual(expected_stats, actual_stats)
Example #15
0
class TestStaffView(TestCase):
    def setUp(self):
        self.password = '******'
        self.user = UserFactory(username='******', password=self.password)
        self.user.is_staff = True
        self.user.save()
        self.assertTrue(
            self.client.login(username=self.user.username,
                              password=self.password))
        self.staff_url = reverse('staff')
        self.httptester_url = reverse('httptester-index')

    def test_non_production_setting_shows_httptester(self):
        """Only superusers can see the httptester URL"""
        self.user.is_superuser = True
        self.user.save()
        rsp = self.client.get(self.staff_url)
        self.assertContains(rsp, self.httptester_url)

    @override_settings(ENVIRONMENT='production')
    def test_production_settings_no_httptester(self):
        rsp = self.client.get(self.staff_url)
        self.assertNotContains(rsp, self.httptester_url)

    def test_rollgen_visible_for_superuser(self):
        """ensure superusers see the rollgen URL"""
        self.user.is_staff = True
        self.user.is_superuser = True
        self.user.save()
        self.client.logout()
        self.assertTrue(
            self.client.login(username=self.user.username,
                              password=self.password))

        rsp = self.client.get(self.staff_url)
        self.assertContains(rsp, reverse('rollgen:overview'))

    def test_rollgen_visible_for_rollgen_group_member(self):
        """ensure staff users in the rollgen_view_job group see the rollgen URL in the staff view"""
        self.user.is_staff = True
        self.user.groups.add(Group.objects.get(name='rollgen_view_job'))
        self.user.save()
        self.client.logout()
        self.assertTrue(
            self.client.login(username=self.user.username,
                              password=self.password))
        rsp = self.client.get(self.staff_url)
        self.assertContains(rsp, reverse('rollgen:overview'))

    def test_rollgen_not_visible_for_rollgen_group_nonmember(self):
        """ensure staff users not in the rollgen_view_job group don't see the rollgen URL"""
        rsp = self.client.get(self.staff_url)
        self.assertNotContains(rsp, reverse('rollgen:overview'))

    def test_staff_page_redirects_to_login_if_not_logged_in(self):
        self.client.logout()
        rsp = self.client.get(self.staff_url)
        self.assertRedirects(
            rsp,
            reverse(settings.LOGIN_URL) + "?next=" + self.staff_url)

    def test_staff_page_responds_403_for_non_staff(self):
        self.user.is_staff = False
        self.user.save()
        rsp = self.client.get(self.staff_url)
        self.assertEqual(FORBIDDEN, rsp.status_code)
Example #16
0
class TestPhoneTool(TestCase):
    def setUp(self):
        self.staff_user = UserFactory()
        self.staff_user.is_staff = True
        self.staff_user.save()

        assert self.client.login(username=self.staff_user.username,
                                 password=DEFAULT_USER_PASSWORD)

        self.bad_center_id = 'abcd'
        self.bad_phone_number = 'abcd'

        self.good_center_id = RegistrationCenterFactory().center_id
        self.good_looking_center_id = self.good_center_id + 1  # until the next center is created
        self.assertFalse(
            RegistrationCenter.objects.filter(
                center_id=self.good_looking_center_id).exists())

        # these will both get whitelisted by this test class
        self.good_phone_number_1 = 218000000106
        self.good_phone_number_2 = 218000000107

    def test_form_validation(self):
        # no args
        form = PhoneAndMessageQueryForm({})
        self.assertFalse(form.is_valid())

        # too many args
        form = PhoneAndMessageQueryForm({
            'center_id':
            self.good_center_id,
            'phone_number':
            self.good_phone_number_1
        })
        self.assertFalse(form.is_valid())

        # bad center id
        form = PhoneAndMessageQueryForm({
            'center_id': self.bad_center_id,
        })
        self.assertFalse(form.is_valid())

        # bad center id that looks good
        form = PhoneAndMessageQueryForm({
            'center_id':
            self.good_looking_center_id,
        })
        self.assertFalse(form.is_valid())

        # bad phone
        form = PhoneAndMessageQueryForm({
            'phone_number': self.bad_phone_number,
        })
        self.assertFalse(form.is_valid())

        # just right (center id)
        form = PhoneAndMessageQueryForm({
            'center_id': self.good_center_id,
        })
        self.assertTrue(form.is_valid())

        # just right (phone)
        form = PhoneAndMessageQueryForm({
            'phone_number':
            self.good_phone_number_1,
        })
        self.assertTrue(form.is_valid())

    def test_form_view(self):
        rsp = self.client.get(reverse('vr_dashboard:phone-message-tool'))
        self.assertEqual(200, rsp.status_code)

        rsp = self.client.post(reverse('vr_dashboard:phone-message-tool'),
                               {'center_id': self.good_center_id})
        self.assertRedirects(
            rsp,
            reverse('vr_dashboard:search-phones') +
            '?center_id=%s' % self.good_center_id,
            target_status_code=404  # no phones for that center
        )

        rsp = self.client.post(reverse('vr_dashboard:phone-message-tool'),
                               {'phone_number': self.good_phone_number_1})
        print rsp.content
        self.assertRedirects(
            rsp,
            reverse('vr_dashboard:phone-history') +
            '?phone=%s' % self.good_phone_number_1)

    def test_bad_args(self):
        """ Test what happens when bad data gets past the form, or the form is
        bypassed.
        """
        rsp = self.client.get(
            reverse('vr_dashboard:search-phones') +
            '?phone=%s' % self.bad_phone_number)
        self.assertEqual(400, rsp.status_code)
        rsp = self.client.get(
            reverse('vr_dashboard:search-phones') +
            '?center=%s' % self.bad_center_id)
        self.assertEqual(400, rsp.status_code)
        rsp = self.client.get(
            reverse('vr_dashboard:phone-history') +
            '?phone=%s' % self.bad_phone_number)
        self.assertEqual(400, rsp.status_code)

    def test_whitelist(self):  # success, redirect to phone history
        rsp = self.client.post(reverse('vr_dashboard:whitelist-phone'),
                               {'phone': self.good_phone_number_1})
        self.assertRedirects(
            rsp,
            reverse('vr_dashboard:phone-history') +
            '?phone=%s' % self.good_phone_number_1)

        # success, redirect to phone list
        rsp = self.client.post(reverse('vr_dashboard:whitelist-phone'), {
            'phone': self.good_phone_number_2,
            'center_id': self.good_center_id
        })
        self.assertRedirects(
            rsp,
            reverse('vr_dashboard:search-phones') +
            '?center_id=%s' % self.good_center_id,
            target_status_code=404  # no phones for that center
        )

        # failure

        rsp = self.client.post(reverse('vr_dashboard:whitelist-phone'), {
            'phone': self.good_phone_number_1,
            'center_id': self.bad_center_id
        })
        self.assertEqual(rsp.status_code, 400)
Example #17
0
 def setUp(self):
     super(ViewsEmptyTestCase, self).setUp()
     user = UserFactory(password='******')
     user.is_superuser = True
     user.save()
     self.assertTrue(self.client.login(username=user.username, password='******'))
Example #18
0
class TestEndToEnd(TestCase):

    def setUp(self):
        self.staff_user = UserFactory()
        self.staff_user.is_staff = True
        self.staff_user.save()
        assert self.client.login(username=self.staff_user.username, password=DEFAULT_USER_PASSWORD)
        self.reporting_user = test_reports.TEST_USERNAME
        self.reporting_password = test_reports.TEST_PASSWORD
        REPORT_USER_DB[self.reporting_user] = self.reporting_password
        # Pick a start time that represents different days in Libya vs UTC
        tz = timezone(settings.TIME_ZONE)
        polling_start_time = astz(FUTURE_DAY.replace(hour=22), tz)
        polling_end_time = tz.normalize(polling_start_time + timedelta(hours=16))
        self.election = ElectionFactory(
            polling_start_time=polling_start_time,
            polling_end_time=polling_end_time,
        )
        self.election_day_dt = self.election.polling_start_time
        # Create "decoy" election just to confirm that it doesn't break reports.
        decoy_start_time = tz.normalize(polling_start_time - timedelta(days=10))
        decoy_end_time = tz.normalize(decoy_start_time + timedelta(hours=16))
        ElectionFactory(
            polling_start_time=decoy_start_time,
            polling_end_time=decoy_end_time,
        )
        self.all_centers = []
        self.rc_1 = RegistrationCenterFactory()
        self.all_centers.append(self.rc_1)
        self.rc_2 = RegistrationCenterFactory()
        self.all_centers.append(self.rc_2)
        self.rc_3 = RegistrationCenterFactory()
        self.all_centers.append(self.rc_3)
        self.rc_4 = RegistrationCenterFactory()
        self.all_centers.append(self.rc_4)
        self.copy_of_rc_1 = RegistrationCenterFactory(copy_of=self.rc_1, office=self.rc_1.office)
        self.all_centers.append(self.copy_of_rc_1)
        # rc_5 is inactive for this election
        self.rc_5 = RegistrationCenterFactory(office=self.rc_1.office)
        self.all_centers.append(self.rc_5)
        inactive_on_election = CenterClosedForElection(
            registration_center=self.rc_5, election=self.election
        )
        inactive_on_election.full_clean()
        inactive_on_election.save()
        self.all_office_ids = [center.office_id for center in self.all_centers]
        self.carrier_1 = BackendFactory()
        self.citizen_1 = CitizenFactory()

        # Create registrations on the 4 days leading up to election day
        # Put the registrations at different hours of the day to stress TZ handling.
        self.registration_dates = []
        self.registration_date_strs = []
        hour_of_day = 0
        for delta_days in range(10, 4, -1):
            assert hour_of_day < 24
            reg_date = astz(self.election_day_dt - timedelta(days=delta_days), tz)\
                .replace(hour=hour_of_day)
            hour_of_day += 4
            self.registration_dates.append(reg_date)
            self.registration_date_strs.append(reg_date.strftime('%Y-%m-%d'))
        self.yesterday_date, _ = calc_yesterday(self.registration_date_strs)
        self.yesterday_date_dm = self.yesterday_date.strftime('%d/%m')
        # yesterday_date is a date; get a datetime form
        self.yesterday_date_dt = tz.localize(datetime(self.yesterday_date.year,
                                                      self.yesterday_date.month,
                                                      self.yesterday_date.day,
                                                      0, 0, 0))
        self.staff_phone_number = STAFF_PHONE_NUMBER_PATTERN % 12345

    def _describe_infra(self):
        logger.info("Registration Centers:")
        for center in self.all_centers:
            logger.info('  %s' % center)
            logger.info('    office id %d' % center.office.id)
            if center.copy_of:
                logger.info('    copy of center %d' % center.center_id)

    def _request(self, url, **extra):
        """ Request the specified URL using self.client, perform any common
        processing like checking the status code and logging the response.
        """
        logger.info(url)
        if extra:
            logger.info(extra)
        rsp = self.client.get(url, **extra)
        self.assertEqual(200, rsp.status_code)
        logger.info(rsp.content)
        return rsp

    def _request_csv(self, url, **extra):
        """ Like _request() above, but also parse the response body as a CSV
        as created by the voter registration dashboard and log in parsed form.

        This adds the query arg which specifies CSV rendering.
        """
        url += '?format=csv'
        rsp = self._request(url, **extra)
        content = rsp.content[2:]  # skip BOM
        reader = UnicodeReader(StringIO(content), encoding="utf-16-le", delimiter='\t')
        rows = []
        for row in reader:
            rows.append(row)
            logger.info(row)
        return rows

    def _msg_type_to_str(self, t):
        """ Get string form of the provided SMS message type.
        """
        # unicode() resolves the lazy translation object
        return unicode([x for x in SMS.MESSAGE_TYPES if x[0] == t][0][1])

    def _create_registrations(self, expected_stats):
        """ Create different numbers of registrations on each of the chosen registration dates
        so we can be sure that a count was assigned to the correct date.
        """
        msg_type_str = self._msg_type_to_str(SMS.REGISTRATION)
        expected_stats['message_stats'][msg_type_str] = dict()
        # Accumulators for by-center and total number of registrations
        expected_stats['by_center'][self.rc_1.center_id]['registrations'] = 0
        expected_stats['by_center'][self.rc_2.center_id]['registrations'] = 0
        expected_stats['by_center'][self.rc_3.center_id]['registrations'] = 0
        expected_stats['by_center'][self.rc_4.center_id]['registrations'] = 0
        expected_stats['by_center'][self.copy_of_rc_1.center_id]['registrations'] = ''
        expected_stats['by_center'][self.rc_5.center_id]['registrations'] = 0
        expected_stats['message_stats'][msg_type_str]['total'] = 0
        for i, reg_date in enumerate(self.registration_dates):
            regs_on_date = i + 1
            for j in range(regs_on_date):
                citizen = CitizenFactory()
                s = SMS(from_number='12345', to_number='12345', citizen=citizen,
                        direction=INCOMING, message='my reg message',
                        msg_type=SMS.REGISTRATION, message_code=MESSAGE_1,
                        carrier=self.carrier_1, creation_date=reg_date)
                s.full_clean()
                s.save()
                r = Registration(citizen=citizen, registration_center=self.rc_1,
                                 archive_time=None,
                                 sms=s, creation_date=reg_date, modification_date=reg_date)
                r.full_clean()
                r.save()
                expected_stats['by_center'][self.rc_1.center_id]['registrations'] += 1
            expected_stats['message_stats'][msg_type_str]['total'] += regs_on_date
            # Capture the count on "yesterday" (reported on SMS page)
            if self.yesterday_date == reg_date.date():
                expected_stats['message_stats'][msg_type_str][self.yesterday_date_dm] = \
                    regs_on_date

    @classmethod
    def _max_report_time(cls, time1, time2):
        """ Return the greater of the two times.  time1 can be None, which
        means long, long ago.
        """
        return max(time1 or time2, time2)

    def _create_election_day_data(self, expected_stats):
        """Create various types of election data for testing of the election
        day dashboard."""

        # Pick open times that could vary by date based on time zone.
        rc_1_open_time = self.election_day_dt.replace(hour=1, minute=23)
        rc_2_open_time = self.election_day_dt.replace(hour=10, minute=23)
        # This center open time is before the election time really starts,
        # so it will be reported under the corresponding office as an
        # unopened center.
        open_time_3 = self.election.start_time - timedelta(hours=6)

        # configure election day activities by registration center
        center_activities = []

        center_activities.append({
            'center': self.rc_1,
            'open_time': rc_1_open_time,
            'phone_number': STAFF_PHONE_NUMBER_PATTERN % 1,
        })

        center_activities.append({
            'center': self.rc_2,
            'open_time': rc_2_open_time,
            'phone_number': STAFF_PHONE_NUMBER_PATTERN % 1,
            'prelim_time': self.election_day_dt,
            'prelim_option': 9,
            'prelim_votes': 7312,  # four digits to test intcomma formatting
            'period_4_time': rc_2_open_time + timedelta(hours=6),
            'period_4_count': 79,
            # period "5" is a report for period 4 sent on following day
            'period_5_time': self.election_day_dt + timedelta(days=1),
            'period_5_count': 82,
        })

        center_activities.append({
            'center': self.rc_3,
            'open_time': open_time_3,
            'phone_number': STAFF_PHONE_NUMBER_PATTERN % 2,
        })

        center_activities.append({
            'center': self.rc_4,
            # DOES NOT SEND CenterOpen or anything else
        })

        center_activities.append({
            'center': self.copy_of_rc_1,
            # The copy center opened, coincidentally at the same time as the copied center.
            'open_time': rc_1_open_time,
            'phone_number': STAFF_PHONE_NUMBER_PATTERN % 3,
            # vote report for period 2
            'period_2_time': self.election_day_dt,
            'period_2_count': 4321,  # four digits to test intcomma formatting
        })

        center_activities.append({
            'center': self.rc_5,
            # DOES NOT SEND CenterOpen or anything else
            # This shares an office id with rc_1, and is also marked as
            # inactive for this particular election.
        })

        # shortcuts into dictionaries
        expected_center_stats = expected_stats['by_center']
        expected_office_stats = expected_stats['by_office']
        expected_summary_stats = expected_stats['summary']

        # Clear office-level summaries
        # (Some offices will be repeated, but it doesn't matter.)
        for activity in center_activities:
            office_id = activity['center'].office_id
            for key in ('opened', 'closed', 'not_reported_1', 'not_reported_2', 'not_reported_3',
                        'not_reported_4', 'unopened'):
                expected_office_stats[office_id][key] = 0
            expected_office_stats[office_id]['summary'] = deepcopy(EMPTY_SUMMARY)

        # Create the messages, increment/set counters/fields to represent
        # expected dashboard data.
        for activity in center_activities:
            # shortcuts specific to this center
            expected_for_this_center = expected_center_stats[activity['center'].center_id]
            expected_for_this_office = expected_office_stats[activity['center'].office_id]
            expected_summary_for_this_office = expected_for_this_office['summary']

            last_report_dt = None  # track the last report from this center

            open_time = activity.get('open_time', None)
            if open_time:
                open_msg = CenterOpen(election=self.election,
                                      phone_number=activity['phone_number'],
                                      registration_center=activity['center'],
                                      creation_date=activity['open_time'])
                open_msg.full_clean()
                open_msg.save()
                last_report_dt = self._max_report_time(last_report_dt, activity['open_time'])

            # It does not count as an open if it happened too early
            if open_time and open_time >= self.election.start_time:
                expected_for_this_center['ed_open'] = open_time.strftime('%d/%m %H:%M')
                expected_for_this_center['opened_hm'] = open_time.strftime('%H:%M')
                expected_for_this_office['opened'] += 1
                expected_summary_stats['opened'] += 1
                expected_summary_for_this_office['opened'] += 1
            else:
                expected_for_this_center['ed_open'] = None
                expected_for_this_center['opened_hm'] = None
                expected_for_this_office['unopened'] += 1
                expected_summary_stats['unopened'] += 1
                expected_summary_for_this_office['unopened'] += 1

            for period in ('1', '2', '3', '4'):
                report_time, report_count = \
                    activity.get('period_' + period + '_time', None), \
                    activity.get('period_' + period + '_count', None)

                if report_time:
                    r = PollingReport(election=self.election,
                                      phone_number=activity['phone_number'],
                                      registration_center=activity['center'],
                                      period_number=int(period),
                                      num_voters=report_count,
                                      creation_date=report_time)
                    r.full_clean()
                    r.save()
                    last_report_dt = self._max_report_time(last_report_dt, report_time)

                    expected_for_this_center['votes_reported_' + period] = report_count
                    expected_for_this_center['reported_period_' + period] = 'has_reported'
                    expected_for_this_center['reported_period_' + period + '_count'] = report_count
                    expected_for_this_office['votes_reported_' + period] = report_count
                    expected_summary_stats['votes_reported_' + period] += report_count
                    expected_summary_for_this_office['votes_reported_' + period] += report_count
                    if period == '4':  # got period 4 report, so didn't close
                        expected_for_this_center['is_closed'] = 'Yes'
                        expected_for_this_office['closed'] += 1
                else:
                    if open_time and open_time >= self.election.start_time:
                        # The effective time of the reports was just after period 2, so
                        # if this is the period 1 or 2 report then it is overdue, and
                        # if this is the period 3 or 4 report then it is not due yet.
                        flag = 'has_not_reported' if period in ('1', '2') else 'not_due'
                        expected_for_this_center['reported_period_' + period] = flag
                    else:
                        expected_for_this_center['reported_period_' + period] = 'no_data'
                    expected_for_this_center['reported_period_' + period + '_count'] = 0
                    expected_for_this_office['not_reported_' + period] += 1
                    if period == '4':  # no period 4 report, so didn't close
                        expected_for_this_center['is_closed'] = 'No'

            # Very basic support for sending period 4 report on day after election
            #
            # It assumes that a period 4 report was also sent on election day, which
            # simplifies handling of votes_reported_4 counters and information on
            # closing.
            #
            # Period "5" is period 4 on the following day.
            period_5_time = activity.get('period_5_time', None)
            if period_5_time:
                period_5_count = activity['period_5_count']
                period_4_count = activity['period_4_count']

                r = PollingReport(election=self.election,
                                  phone_number=activity['phone_number'],
                                  registration_center=activity['center'],
                                  period_number=4,
                                  num_voters=period_5_count,
                                  creation_date=period_5_time)
                r.full_clean()
                r.save()
                last_report_dt = self._max_report_time(last_report_dt, period_5_time)

                # Add in delta to prior period 4 report
                delta = period_5_count - period_4_count
                expected_for_this_center['votes_reported_4'] += delta
                expected_for_this_center['reported_period_4_count'] += delta
                expected_for_this_office['votes_reported_4'] += delta
                expected_summary_stats['votes_reported_4'] += delta
                expected_summary_for_this_office['votes_reported_4'] += delta

            prelim_time = activity.get('prelim_time', None)
            if prelim_time:
                prelim = PreliminaryVoteCount(election=self.election,
                                              phone_number=activity['phone_number'],
                                              registration_center=activity['center'],
                                              option=activity['prelim_option'],
                                              num_votes=activity['prelim_votes'],
                                              creation_date=prelim_time)
                prelim.full_clean()
                prelim.save()
                last_report_dt = self._max_report_time(last_report_dt, prelim_time)

                expected_for_this_office['prelim'] = {
                    str(activity['prelim_option']): intcomma(activity['prelim_votes'])
                }

            expected_for_this_center['last_report'] = \
                'Not Reported' if not last_report_dt else \
                last_report_dt.strftime('%d/%m %H:%M')

        # rc_5 is inactive for this election
        # (CenterClosedForElection created when center was created)
        # Now that the office 'summary' has been set up, note where inactive should show up.
        expected_center_stats[self.rc_5.center_id]['inactive'] = True
        expected_office_stats[self.rc_5.office.id]['summary']['inactive'] += 1

    def _create_sms_messages(self, expected_stats):
        """ Create SMS messages of a certain type at different times "yesterday".
        The times should have the same date in local TZ but different dates when
        the TZ is bungled somewhere.  By making them "yesterday", the counts will show
        up in the SMS page in the yesterday column.
        """
        msg_type = SMS.INVALID_CENTRE_CODE_LENGTH
        msg_type_str = self._msg_type_to_str(msg_type)
        expected_stats['message_stats'][msg_type_str] = dict()
        num_staff_messages = 8
        for msg_hour in range(num_staff_messages):
            msg_time = self.yesterday_date_dt.replace(hour=msg_hour, minute=23)
            s = SMS(from_number=self.staff_phone_number, to_number='12345', citizen=self.citizen_1,
                    direction=INCOMING, message='my message',
                    msg_type=msg_type, message_code=MESSAGE_1,
                    carrier=self.carrier_1, creation_date=msg_time)
            s.full_clean()
            s.save()
        expected_stats['message_stats'][msg_type_str][self.yesterday_date_dt.strftime('%d/%m')] = \
            num_staff_messages
        expected_stats['message_stats'][msg_type_str]['total'] = num_staff_messages
        expected_stats['phone_history'][self.staff_phone_number] = {
            'message_count': num_staff_messages,
        }

    @classmethod
    def _str_to_int(cls, s):
        """ Convert the input numeric string, which may contain comma
        separators, to an int.
        """
        return int(s.replace(',', ''))

    @classmethod
    def _extract_int_from_span(cls, s):
        """Input s contains a number we need to convert to an int, embedded
        in a <span...></span>."""
        m = re.search('<span.*>(.+)</span>', s)
        if m:
            return cls._str_to_int(m.group(1))
        else:
            raise ValueError('Argument "%" does not contain <span></span>')

    def _parse_headline(self, headline, has_inactive=False):
        return {
            # Grab the number from u'3 centers have opened' and similar for unopened and inactive
            'opened': self._extract_int_from_span(headline['open_centers']),
            'unopened': self._extract_int_from_span(headline['unopen_centers']),
            'inactive':
                self._extract_int_from_span(headline['inactive_centers']) if has_inactive else 0,
            # Grab the number from u'Votes reported period 1: 0'
            'votes_reported_1': self._extract_int_from_span(headline['period1']),
            'votes_reported_2': self._extract_int_from_span(headline['period2']),
            'votes_reported_3': self._extract_int_from_span(headline['period3']),
            'votes_reported_4': self._extract_int_from_span(headline['period4']),
        }

    def _read_dashboard(self, actual_stats):
        """ Read parts of the dashboard that contain data we're testing and fill in the
        provided dictionary with the stats we observe.

        This only supports a small subset of the stats on the dashboard.
        """

        # Process the office-specific election day screen
        for office_id in self.all_office_ids:
            url = reverse('vr_dashboard:election-day-office-n', args=(office_id,))
            rsp = self._request(url)
            actual_stats['by_office'][office_id]['summary'] = \
                self._parse_headline(rsp.context['headline'], has_inactive=True)
            for row in rsp.context['office_centers_table']:
                center_id = row['polling_center_code']
                assert center_id in actual_stats['by_center'], \
                    'Center id %s is unexpected (not one of %s)' % \
                    (center_id, actual_stats['by_center'].keys())
                if 'opened_today' in row:
                    open_dt = row['opened_today']
                    actual_stats['by_center'][center_id]['ed_open'] = open_dt
                if 'inactive_for_election' in row:
                    actual_stats['by_center'][center_id]['inactive'] = True
                    self.assertEqual(row['tr_class'], 'inactive_for_election')
                for period in ['1', '2', '3', '4']:
                    votes_reported_period = 'votes_reported_' + period
                    if votes_reported_period in row:
                        actual_stats['by_center'][center_id][votes_reported_period] = \
                            row[votes_reported_period]
                        actual_stats['by_office'][office_id][votes_reported_period] = \
                            row[votes_reported_period]
                    actual_stats['by_center'][center_id]['reported_period_' + period] = \
                        row['reported_period_' + period]

        # prelim vote counts
        url = reverse('vr_dashboard:election-day-preliminary')
        rsp = self._request(url)
        for office in rsp.context['offices']:
            if PRELIMINARY_VOTE_COUNTS in office:
                actual_stats['by_office'][office['office_id']]['prelim'] = {
                    key: intcomma(value)
                    for key, value in office[PRELIMINARY_VOTE_COUNTS].iteritems()
                }

        # process the sms screen
        url = reverse('vr_dashboard:sms')
        rsp = self._request(url)
        yesterday_str = rsp.context['sms_stats']['last_date']
        for stats in rsp.context['message_stats_by_type']:
            msg_type = stats['translated_sms_type']
            msg_yesterday_count = int(stats['last'])
            msg_total_count = int(stats['total'])
            actual_stats['message_stats'][msg_type] = {
                yesterday_str: msg_yesterday_count,
                'total': msg_total_count
            }

        # look at summary stats from main election_day page
        url = reverse('vr_dashboard:election-day')
        rsp = self._request(url)
        actual_stats['summary'] = self._parse_headline(rsp.context['headline'])
        for row in rsp.context['offices']:
            actual_office_stats = actual_stats['by_office'][row['office_id']]
            actual_office_stats['opened'] = row['opened']
            actual_office_stats['unopened'] = row['not_opened']
            actual_office_stats['closed'] = row['closed']
            actual_office_stats['not_reported_1'] = row['not_reported_1']
            actual_office_stats['not_reported_2'] = row['not_reported_2']
            actual_office_stats['not_reported_3'] = row['not_reported_3']
            actual_office_stats['not_reported_4'] = row['not_reported_4']

        # process the election day CSVs, testing something from each data row
        csv = self._request_csv(reverse('vr_dashboard:election-day'))
        # office data starts in 4th row, country-wide is last row
        for row in csv[3:-1]:
            office_id = int(row[0].split()[0])
            opened = int(row[2])
            # already grabbed from normal view, so make sure it matches
            self.assertEquals(actual_stats['by_office'][office_id]['opened'], opened)

        csv = self._request_csv(reverse('vr_dashboard:election-day-center'))
        # center data starts in 4th row
        for row in csv[3:]:
            center_id = int(row[1])
            total_regs = '' if not row[3] else int(row[3])  # '' for copy center
            actual_stats['by_center'][center_id]['registrations'] = total_regs
            # inactive already read from election-day-office-n; make sure it matches
            active_flag = 'No' if 'inactive' in actual_stats['by_center'][center_id] else 'Yes'
            self.assertEqual(row[5], active_flag)
            actual_stats['by_center'][center_id]['opened_hm'] = row[6] if row[6] else None
            actual_stats['by_center'][center_id]['is_closed'] = row[11]
            # votes_reported_N already read from election-day-office-n; make sure it matches
            for period, where_in_row in (('1', 7), ('2', 8), ('3', 9), ('4', 10)):
                period_key = 'votes_reported_' + period
                if row[where_in_row]:
                    self.assertEquals(actual_stats['by_center'][center_id][period_key],
                                      int(row[where_in_row]))
                else:
                    self.assertNotIn(period_key, actual_stats['by_center'][center_id])

        for office_id in self.all_office_ids:
            csv = self._request_csv(reverse('vr_dashboard:election-day-office-n',
                                            args=[office_id]))
            # The center open time has already been read from the HTML, so just
            # make sure that it is consistent in the CSV.
            # Note that if there's no CenterOpen, the context will have None but
            # the screen and CSV will have '-'.
            center_id = int(csv[3][1])
            open_dt = csv[3][3]
            self.assertEqual(
                actual_stats['by_center'][center_id]['ed_open'] or '-',
                open_dt,
                'Open for center %d different between HTML and CSV (%s, %s)' % (
                    center_id, actual_stats['by_center'][center_id]['ed_open'], open_dt
                )
            )
            for row in csv[3:]:
                center_id = int(row[1])
                # inactive already read from election-day-office-n; make sure it matches
                active_flag = 'No' if 'inactive' in actual_stats['by_center'][center_id] else 'Yes'
                self.assertEqual(row[2], active_flag)

        for center in self.all_centers:
            center_id = center.center_id
            rsp = self._request(reverse('vr_dashboard:election-day-center-n',
                                        args=[center_id]))
            stats = rsp.context['stats']
            actual_center_stats = actual_stats['by_center'][center_id]
            # The last opened time was already read (set to None if it didn't open);
            # make sure it matches the form on this page, which uses 'Not Opened'
            # instead of None for unopened.
            self.assertEqual(
                actual_center_stats['ed_open'] or 'Not Opened',
                stats['last_opened'],
                'Open for center %d different between by-office and by-center pages (%s, %s)' % (
                    center_id, actual_center_stats['ed_open'], stats['last_opened']
                )
            )
            actual_center_stats['last_report'] = stats['last_report']
            for period in ('1', '2', '3', '4'):
                # votes for period is either '' or string form of number
                orig_key = 'reported_period_' + period
                new_key = 'reported_period_' + period + '_count'
                actual_center_stats[new_key] = \
                    self._extract_int_from_span(stats[orig_key]) if stats[orig_key] else 0
            # consistency between what was already extracted for 'inactive' and this response?
            self.assertEqual(
                'inactive' in actual_stats['by_center'][center_id],
                'inactive_for_election' in rsp.context['center']
            )

        # messages from staff phone
        history = self._request(
            reverse('vr_dashboard:phone-history') + '?phone=%s' % self.staff_phone_number
        )
        actual_stats['phone_history'][self.staff_phone_number] = {
            'message_count': len(history.context['sms_messages']),
        }

    def test(self):
        """ This is the one test case in this class; it performs end-to-end
        testing on reporting-api and vr-dashboard, using the following
        steps:

        1. create basic objects like RegistrationCenter (in setUp())
        2. initialize expected and actual stats dictionaries
        3. create different types of data, updating the expected stats
           dictionary to indicate what should appear on the dashboard
        4. run Celery tasks (directly) to regenerate reports based on the
           data created
        5. fetch and log the JSON reports
        6. scrape dashboard screens to get the actual stats reported
        7. compare expected and actual stats
        """
        expected_stats = {
            'by_center': {
                self.rc_1.center_id: {
                    # nothing yet
                },
                self.rc_2.center_id: {
                    # nothing yet
                },
                self.rc_3.center_id: {
                    # nothing yet
                },
                self.rc_4.center_id: {
                    # nothing yet
                },
                self.copy_of_rc_1.center_id: {
                    # nothing yet
                },
                self.rc_5.center_id: {
                    # nothing yet
                }
            },
            'by_office': {
                self.rc_1.office_id: {
                    # nothing yet
                },
                self.rc_2.office_id: {
                    # nothing yet
                },
                self.rc_3.office_id: {
                    # nothing yet
                },
                self.rc_4.office_id: {
                    # nothing yet
                },
                self.copy_of_rc_1.office_id: {
                    # nothing yet
                },
            },
            'summary': deepcopy(EMPTY_SUMMARY),
            'message_stats': {},
            'phone_history': {},
        }
        actual_stats = deepcopy(expected_stats)

        self._describe_infra()
        self._create_election_day_data(expected_stats)
        self._create_registrations(expected_stats)
        self._create_sms_messages(expected_stats)

        # Regenerate reports based on current database contents

        # Make the election day report code think it is now 3:35 p.m. on election day (just
        # after the period 2 time) so that it will flag missing period 1 and 2 reports.
        middle_of_election_day = self.election.polling_start_time.replace(hour=15, minute=35)
        with patch.object(reports, 'get_effective_reminder_time') as mock_reminder_time:
            mock_reminder_time.return_value = middle_of_election_day
            tasks.election_day()
            mock_reminder_time.assert_called()

        tasks.registrations()

        # Log the JSON reports to help with debugging
        credentials = base64.b64encode(self.reporting_user + ':' + self.reporting_password)
        auth_headers = {
            'HTTP_AUTHORIZATION': 'Basic ' + credentials
        }
        for report_rel_url in [test_reports.REGISTRATIONS_REL_URI,
                               test_reports.ELECTION_DAY_LOG_REL_URI,
                               test_reports.ELECTION_DAY_REPORT_REL_URI]:
            url = test_reports.BASE_URI + report_rel_url
            self._request(url, **auth_headers)

        # Scrape the dashboard screens
        self._read_dashboard(actual_stats)

        # Compare expected and actual stats
        logger.info('Expected:')
        logger.info(expected_stats)
        logger.info('Actual:')
        logger.info(actual_stats)
        # Compare some slices of dictionary first to narrow in on the problem
        self.assertDictEqual(expected_stats['summary'], actual_stats['summary'])
        self.assertDictEqual(expected_stats['by_center'], actual_stats['by_center'])
        self.assertDictEqual(expected_stats['by_office'], actual_stats['by_office'])
        # Everything
        self.assertDictEqual(expected_stats, actual_stats)
Example #19
0
class ViewsFailedJobTestCase(ResponseCheckerMixin, TestJobBase):
    """Exercise views when a job has failed"""
    @property
    def faux_output_dir(self):
        return os.path.normpath(os.path.join(self.output_path, '..'))

    def setUp(self):
        super(ViewsFailedJobTestCase, self).setUp()
        self.user = UserFactory(password='******')
        self.user.is_superuser = True
        self.user.save()
        self.assertTrue(
            self.client.login(username=self.user.username,
                              password='******'))

        # Generate a center with no voters to force an error when the job runs.
        self.no_voters_center = RegistrationCenterFactory(
            name=generate_arabic_place_name())

        phase = 'in-person'

        self.input_arguments['phase'] = phase
        self.input_arguments['center_ids'] = [self.no_voters_center.center_id]

        with override_settings(ROLLGEN_OUTPUT_DIR=self.faux_output_dir):
            self.job = Job(phase, [self.no_voters_center],
                           self.input_arguments, self.user.username,
                           self.output_path)
            try:
                self.job.generate_rolls()
            except NoVotersError as exception:
                # This is expected. (In fact, it's the whole point of the test.)
                handle_job_exception(exception, self.job.output_path)

        self.dirname = os.path.basename(self.job.output_path)

    def test_browse_job_offices_view(self):
        """Generate a job offices view and test the context it passes to the template"""
        with override_settings(ROLLGEN_OUTPUT_DIR=self.faux_output_dir):
            response = self.client.get(
                reverse('rollgen:browse_job_offices', args=(self.dirname, )))

        self.assertResponseOK(response)
        self.assertTemplateUsed(response, 'rollgen/job_failed_view.html')
        context = response.context
        expected_keys = ('job', )
        self.assertTrue(set(expected_keys) < set(context.keys()))
        self.assertEqual(
            JobOverview(self.output_path).raw_metadata,
            context['job'].raw_metadata)

    def test_browse_job_centers_view(self):
        """Generate a job centers view and test the context it passes to the template"""
        with override_settings(ROLLGEN_OUTPUT_DIR=self.faux_output_dir):
            response = self.client.get(
                reverse('rollgen:browse_job_centers', args=(self.dirname, )))

        self.assertResponseOK(response)
        self.assertTemplateUsed(response, 'rollgen/job_failed_view.html')
        context = response.context
        expected_keys = ('job', )
        self.assertTrue(set(expected_keys) < set(context.keys()))
        self.assertEqual(
            JobOverview(self.output_path).raw_metadata,
            context['job'].raw_metadata)
Example #20
0
class BulkUploadViewTest(ResponseCheckerMixin, LibyaTest):
    def setUp(self):
        self.home_url = reverse('upload_broadcast')
        self.staff_user = UserFactory(username='******', password='******')
        self.staff_user.is_staff = True
        self.staff_user.user_permissions.add(
            Permission.objects.get(codename='add_broadcast'))
        self.staff_user.user_permissions.add(
            Permission.objects.get(codename='read_broadcast'))
        self.staff_user.user_permissions.add(
            Permission.objects.get(codename='browse_broadcast'))
        self.staff_user.user_permissions.add(
            Permission.objects.get(codename='approve_broadcast'))
        self.staff_user.save()
        self.client.login(username='******', password='******')

    def test_get_home_view_no_user(self):
        # get home view, not_logged_in -> redirects to login
        self.client.logout()
        self.assertRedirectsToLogin(self.client.get(self.home_url))

    def test_get_home_view_not_staff(self):
        # get view not_staff -> 403
        self.client.logout()
        self.user = UserFactory(username='******', password='******')
        self.client.login(username='******', password='******')
        self.assertForbidden(self.client.get(self.home_url))

    def test_get_home_view_staff(self):
        # get view staff -> shows form
        rsp = self.client.get(self.home_url)
        self.assertEqual(200, rsp.status_code)
        self.assertIn('form', rsp.context)

    def test_post_upload_invalid(self):
        # post view invalid -> shows form
        data = {}
        rsp = self.client.post(self.home_url, data=data)
        self.assertEqual(200, rsp.status_code)
        self.assertIn('form', rsp.context)
        self.assertTrue(rsp.context['form'].errors)

    def test_post_upload_valid(self):
        # post view valid -> no form, contains status about batch
        f = SimpleUploadedFile("batch_test.csv", CSV_DATA)
        data = {'name': u'A name', 'csv': f, 'message': u'Upload'}
        rsp = self.client.post(reverse('upload_broadcast'),
                               data=data,
                               follow=True)
        self.assertNotIn('form', rsp.context)
        self.assertContains(rsp, u'Messages are uploading in the background')

    def test_uploaded_broadcast_same_user_cant_review(self):
        # uploaded batch, same user -> can't approve/reject
        batch = BatchFactory(status=Batch.PENDING, created_by=self.staff_user)
        BatchFactory(status=Batch.PENDING,
                     created_by=self.staff_user,
                     deleted=True)
        broadcast = Broadcast.objects.create(created_by=batch.created_by,
                                             batch=batch,
                                             audience=Broadcast.CUSTOM,
                                             message=batch.description)
        # can see detail page
        rsp = self.client.get(
            reverse('read_broadcast', kwargs=dict(pk=broadcast.id)))
        self.assertEqual(rsp.status_code, 200)
        self.assertContains(rsp, 'Approve')
        self.assertContains(rsp, 'Reject')
        # can't approve broadcast
        kwargs = dict(broadcast_id=broadcast.id)
        review_url = reverse('approve_reject_broadcast', kwargs=kwargs)
        data = {'approve': True}
        rsp = self.client.post(review_url, data=data)
        self.assertRedirects(
            rsp, reverse('read_broadcast', kwargs=dict(pk=broadcast.id)))

    def test_uploaded_broadcast_different_user_can_review(self):
        # uploaded batch, different user -> approve/reject button
        batch = BatchFactory(status=Batch.PENDING)
        BatchFactory(status=Batch.PENDING,
                     created_by=self.staff_user,
                     deleted=True)
        broadcast = Broadcast.objects.create(created_by=batch.created_by,
                                             batch=batch,
                                             audience=Broadcast.CUSTOM,
                                             message=batch.description)
        # can see detail page
        rsp = self.client.get(
            reverse('read_broadcast', kwargs=dict(pk=broadcast.id)))
        self.assertEqual(rsp.status_code, 200)
        self.assertContains(rsp, 'Approve')
        self.assertContains(rsp, 'Reject')
        # they can see approve broadcast
        kwargs = dict(broadcast_id=broadcast.id)
        review_url = reverse('approve_reject_broadcast', kwargs=kwargs)
        data = {'approve': True}
        rsp = self.client.post(review_url, data=data, follow=True)
        self.assertEqual(200, rsp.status_code)
        self.assertContains(rsp, 'You have approved the broadcast.')

    def test_rejected_batch_get_home_view(self):
        # rejected batch, get home view -> show form
        BatchFactory(status=Batch.REJECTED)
        BatchFactory(status=Batch.PENDING, deleted=True)
        rsp = self.client.get(self.home_url)
        self.assertEqual(200, rsp.status_code)
        self.assertIn('form', rsp.context)

    def test_completed_batch_get_home_view(self):
        # completed batch, get home view -> show form
        BatchFactory(status=Batch.COMPLETED)
        BatchFactory(status=Batch.PENDING, deleted=True)
        rsp = self.client.get(self.home_url)
        self.assertEqual(200, rsp.status_code)
        self.assertIn('form', rsp.context)

    def test_can_reject_batch(self):
        batch = BatchFactory(status=Batch.APPROVED)
        broadcast = Broadcast.objects.create(created_by=batch.created_by,
                                             batch=batch,
                                             audience=Broadcast.CUSTOM,
                                             message=batch.description)
        review_url = reverse('approve_reject_broadcast',
                             kwargs={'broadcast_id': broadcast.id})
        data = {'reject': True}
        self.client.post(review_url, data=data)
        self.assertEqual(Batch.objects.get(id=batch.id).status, Batch.REJECTED)
Example #21
0
 def create_staff_user(self):
     user = UserFactory(username=self.username, email=self.email,
                        password=self.password)
     user.is_staff = True
     user.save()
     return user
Example #22
0
class ViewsInProgressJobTestCase(ResponseCheckerMixin, TestJobBase):
    """Exercise views when a job is in progress"""
    @property
    def faux_output_dir(self):
        return os.path.normpath(os.path.join(self.output_path, '..'))

    def setUp(self):
        super(ViewsInProgressJobTestCase, self).setUp()
        self.user = UserFactory(password='******')
        self.user.is_superuser = True
        self.user.save()
        self.assertTrue(
            self.client.login(username=self.user.username,
                              password='******'))

        # I would like to create an in-progress job "organically", but that's hard to do under
        # test conditions. Instead I simulate the conditions of in-progress job.
        with open(os.path.join(self.output_path, ROLLGEN_FLAG_FILENAME),
                  'w') as f:
            f.write(' ')

        self.dirname = os.path.basename(self.output_path)

    def test_overview_view(self):
        """Generate a job view and test the context it passes to the template"""
        with override_settings(ROLLGEN_OUTPUT_DIR=self.faux_output_dir):
            response = self.client.get(reverse('rollgen:overview'))

        self.assertResponseOK(response)
        self.assertTemplateUsed(response, 'rollgen/overview.html')

        context = response.context
        expected_keys = ('jobs', )
        self.assertTrue(set(expected_keys) < set(context.keys()))
        self.assertEqual(1, len(context['jobs']))
        self.assertTrue(context['jobs'][0].in_progress)
        # There should not be a link to the job page.
        self.assertNotContains(
            response,
            reverse('rollgen:browse_job_offices', args=(self.dirname, )))

    def test_browse_job_offices_view(self):
        """Generate a job offices view and test the context it passes to the template"""
        with override_settings(ROLLGEN_OUTPUT_DIR=self.faux_output_dir):
            response = self.client.get(
                reverse('rollgen:browse_job_offices', args=(self.dirname, )))

        self.assertResponseOK(response)
        self.assertTemplateUsed(response, 'rollgen/job_in_progress_view.html')
        context = response.context
        expected_keys = ('job', )
        self.assertTrue(set(expected_keys) < set(context.keys()))
        self.assertTrue(context['job'].in_progress)

    def test_browse_job_centers_view(self):
        """Generate a job centers view and test the context it passes to the template"""
        with override_settings(ROLLGEN_OUTPUT_DIR=self.faux_output_dir):
            response = self.client.get(
                reverse('rollgen:browse_job_centers', args=(self.dirname, )))

        self.assertResponseOK(response)
        self.assertTemplateUsed(response, 'rollgen/job_in_progress_view.html')
        context = response.context
        expected_keys = ('job', )
        self.assertTrue(set(expected_keys) < set(context.keys()))
        self.assertTrue(context['job'].in_progress)
class TestRegistrationData(TestCase):

    def setUp(self):
        create_test_data.create(num_registrations=NUM_REGISTRATIONS)
        self.unused_center = RegistrationCenterFactory()
        self.unused_office = OfficeFactory()
        tasks.election_day()
        tasks.registrations()
        self.staff_user = UserFactory()
        self.staff_user.is_staff = True
        self.staff_user.save()

    def test_auth(self):
        """ Ensure that we get a redirect to the login page for non-public pages. """
        for uri_name in ALL_URI_NAMES:
            uri = reverse(URI_NAMESPACE + uri_name)
            rsp = self.client.get(uri)
            if uri_name in PUBLIC_URI_NAMES:
                self.assertEqual(200, rsp.status_code,
                                 'Request to %s failed with status %d' % (uri, rsp.status_code))
            else:
                self.assertRedirects(rsp, reverse(settings.LOGIN_URL) + "?next=" + uri,
                                     msg_prefix='Path %s not handled properly' % uri)

    def test_basic_operation(self):
        """  For the time being, simply ensure that the VR dashboard pages don't blow up. """
        assert self.client.login(username=self.staff_user.username, password=DEFAULT_USER_PASSWORD)
        for uri_name in ALL_URI_NAMES:
            uri = reverse(URI_NAMESPACE + uri_name)
            rsp = self.client.get(uri)
            self.assertEqual(200, rsp.status_code,
                             'Request to %s failed with status %d' % (uri, rsp.status_code))
            if uri_name in SUPPORT_CSV_FORMAT:
                rsp = self.client.get(uri + '?format=csv')
                self.assertEqual(200, rsp.status_code,
                                 'CSV request to %s failed with status %d' % (uri, rsp.status_code))
        # pages without fixed paths
        # test election-day-office-n with both default and CSV renderings
        # First, we must find an office that actually has registrations
        some_valid_office_id = Registration.objects.first().registration_center.office.id
        uri = reverse(URI_NAMESPACE + 'election-day-office-n', args=[some_valid_office_id])
        rsp = self.client.get(uri)
        self.assertEqual(200, rsp.status_code,
                         'Request to %s failed with status %d' % (uri, rsp.status_code))
        rsp = self.client.get(uri + '?format=csv')
        self.assertEqual(200, rsp.status_code,
                         'Request to %s failed with status %d' % (uri, rsp.status_code))

    def test_invalid_office_center(self):
        assert self.client.login(username=self.staff_user.username, password=DEFAULT_USER_PASSWORD)
        # We should get 404 from truly bogus ids as well as from centers or offices that
        # exist but aren't used.
        for input_uri_name, invalid_id in [
            ('vr_dashboard:election-day-center-n', self.unused_center.id),
            ('vr_dashboard:election-day-center-n', 999999),
            ('vr_dashboard:election-day-office-n', self.unused_office.id),
            ('vr_dashboard:election-day-office-n', 999999)
        ]:
            uri = reverse(input_uri_name, args=[invalid_id])
            rsp = self.client.get(uri)
            self.assertContains(rsp, str(invalid_id), status_code=404)
Example #24
0
class TestPhoneTool(TestCase):

    def setUp(self):
        self.staff_user = UserFactory()
        self.staff_user.is_staff = True
        self.staff_user.save()

        assert self.client.login(username=self.staff_user.username,
                                 password=DEFAULT_USER_PASSWORD)

        self.bad_center_id = 'abcd'
        self.bad_phone_number = 'abcd'

        self.good_center_id = RegistrationCenterFactory().center_id
        self.good_looking_center_id = self.good_center_id + 1  # until the next center is created
        self.assertFalse(
            RegistrationCenter.objects.filter(center_id=self.good_looking_center_id).exists()
        )

        # these will both get whitelisted by this test class
        self.good_phone_number_1 = 218000000106
        self.good_phone_number_2 = 218000000107

    def test_form_validation(self):
        # no args
        form = PhoneAndMessageQueryForm({
        })
        self.assertFalse(form.is_valid())

        # too many args
        form = PhoneAndMessageQueryForm({
            'center_id': self.good_center_id,
            'phone_number': self.good_phone_number_1
        })
        self.assertFalse(form.is_valid())

        # bad center id
        form = PhoneAndMessageQueryForm({
            'center_id': self.bad_center_id,
        })
        self.assertFalse(form.is_valid())

        # bad center id that looks good
        form = PhoneAndMessageQueryForm({
            'center_id': self.good_looking_center_id,
        })
        self.assertFalse(form.is_valid())

        # bad phone
        form = PhoneAndMessageQueryForm({
            'phone_number': self.bad_phone_number,
        })
        self.assertFalse(form.is_valid())

        # just right (center id)
        form = PhoneAndMessageQueryForm({
            'center_id': self.good_center_id,
        })
        self.assertTrue(form.is_valid())

        # just right (phone)
        form = PhoneAndMessageQueryForm({
            'phone_number': self.good_phone_number_1,
        })
        self.assertTrue(form.is_valid())

    def test_form_view(self):
        rsp = self.client.get(reverse('vr_dashboard:phone-message-tool'))
        self.assertEqual(200, rsp.status_code)

        rsp = self.client.post(reverse('vr_dashboard:phone-message-tool'), {
            'center_id': self.good_center_id
        })
        self.assertRedirects(
            rsp,
            reverse('vr_dashboard:search-phones') + '?center_id=%s' % self.good_center_id,
            target_status_code=404  # no phones for that center
        )

        rsp = self.client.post(reverse('vr_dashboard:phone-message-tool'), {
            'phone_number': self.good_phone_number_1
        })
        print rsp.content
        self.assertRedirects(
            rsp,
            reverse('vr_dashboard:phone-history') + '?phone=%s' % self.good_phone_number_1
        )

    def test_bad_args(self):
        """ Test what happens when bad data gets past the form, or the form is
        bypassed.
        """
        rsp = self.client.get(reverse('vr_dashboard:search-phones') +
                              '?phone=%s' % self.bad_phone_number)
        self.assertEqual(400, rsp.status_code)
        rsp = self.client.get(reverse('vr_dashboard:search-phones') +
                              '?center=%s' % self.bad_center_id)
        self.assertEqual(400, rsp.status_code)
        rsp = self.client.get(reverse('vr_dashboard:phone-history') +
                              '?phone=%s' % self.bad_phone_number)
        self.assertEqual(400, rsp.status_code)

    def test_whitelist(self):        # success, redirect to phone history
        rsp = self.client.post(reverse('vr_dashboard:whitelist-phone'), {
            'phone': self.good_phone_number_1
        })
        self.assertRedirects(
            rsp,
            reverse('vr_dashboard:phone-history') + '?phone=%s' % self.good_phone_number_1
        )

        # success, redirect to phone list
        rsp = self.client.post(reverse('vr_dashboard:whitelist-phone'), {
            'phone': self.good_phone_number_2,
            'center_id': self.good_center_id
        })
        self.assertRedirects(
            rsp,
            reverse('vr_dashboard:search-phones') + '?center_id=%s' % self.good_center_id,
            target_status_code=404  # no phones for that center
        )

        # failure

        rsp = self.client.post(reverse('vr_dashboard:whitelist-phone'), {
            'phone': self.good_phone_number_1,
            'center_id': self.bad_center_id
        })
        self.assertEqual(rsp.status_code, 400)