def gen_prelim_count_cells(options, counts):
    """
    :param options: list of possible vote options
    :param counts: either a dict of vote counts or a dict that contains one
    :return: table cells with each vote count for the region, office, whatever,
      substituting '-' when no votes were reported by the entity for a vote
      option
    """
    counts = counts.get(PRELIMINARY_VOTE_COUNTS, counts)
    output = []
    for option in options:
        number = counts.get(option)
        formatted_number = intcomma(number) if number else '-'
        output.append('<td>%s</td>' % formatted_number)
    return ''.join(output)
def lookup(current, *args):
    """ Look up data in a single hash or a series of nested hashes, using
    keys (possibly variables) specified in args.  Return the final value.

    The caller can add the additional arg 'intcomma' to format an integer result
    like the intcomma filter of humanize.

    Django templates don't support hash lookups with variable keys (such
    as when iterating over different age groups in table columns).

    In the following example, age and gender are template variables, and the
    dict data with an age key yields another dict keyed by gender.

    <p>Count by age and gender: {% lookup data age gender %}</p>

    The following variation will format the count like the intcomma filter of
    humanize:

    <p>Count by age and gender: {% lookup data age gender intcomma %}</p>

    If the keys were constant, the normal Django template support could be used:

    <p>Count by age and gender: {{ data.19.F }}</p>
    <p>Count by age and gender: {{ data.19.F|intcomma }}</p>
    """
    int_comma = False
    for arg in args:
        if arg == '':  # trailing empty string args passed when invocation uses ' as <var>'
            break
        if arg == 'intcomma':
            int_comma = True
            break  # force 'intcomma' to be the last arg, when used
        current = current[arg]
    if int_comma:
        return intcomma(current)
    else:
        return current
Beispiel #3
0
    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']),
        }
Beispiel #4
0
    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 _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 _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