Files
notifications-admin/app/models/broadcast_message.py
Chris Hill-Scott 8ea3f0141c Give estimates of the number of phones in a broadcast area
We need to give people a better feel for the consequences of
broadcasting an alert. We’ve seen in research that some users will
assume it is subscription based, or opt-in, rather than going to every
phone in the area.

I reckon that the most effective way to communicate this is to put some
numbers next to the areas, to give people an idea of how many people
will get alerted.

We can estimate how many phones are in an area by:
- taking the population of all electoral wards in that area
- multiplying it by the percentage of people who own an internet
  connected phone[1]

The Office for National Statistics publish both these datasets.

The number of people who own an intenet connected phone varies a lot by
age. Since the population data for each ward is broken down by age we
can factor this in. Simplified, the calculation looks like this:
- take the _Abbey_ ward of _Barking and Dagenham_
- in this ward there are 26 people aged 80
- 40% of people over 65 have an internet-connected phone
- therefore 10 of these 80-year-olds would be likely to receive a
  broadcast
- (repeat for all other ages)

These numbers won’t be exact, but should be enough to give people a feel
for the severity of what they’re about to do. We can see if they acheive
this aim in user research.

1. This is a proxy for the number of people who are likely to have a 4G
   capable phone, because only 4G capable phones will be receiving
   broadcasts to begin with
2020-09-14 16:26:09 +01:00

212 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import itertools
from datetime import datetime, timedelta
from math import floor, log10
from notifications_utils.template import BroadcastPreviewTemplate
from orderedset import OrderedSet
from werkzeug.utils import cached_property
from app.broadcast_areas import broadcast_area_libraries
from app.broadcast_areas.polygons import Polygons
from app.models import JSONModel, ModelList
from app.models.user import User
from app.notify_client.broadcast_message_api_client import (
broadcast_message_api_client,
)
from app.notify_client.service_api_client import service_api_client
def round_to_significant_figures(x, n):
return int(round(
x,
-int(floor(log10(abs(x)))) + (n - 1)
))
class BroadcastMessage(JSONModel):
ALLOWED_PROPERTIES = {
'id',
'service_id',
'template_id',
'template_name',
'template_version',
'service_id',
'created_by',
'personalisation',
'starts_at',
'finishes_at',
'created_at',
'approved_at',
'cancelled_at',
'updated_at',
'created_by_id',
'approved_by_id',
'cancelled_by_id',
}
libraries = broadcast_area_libraries
def __lt__(self, other):
return (
self.cancelled_at or self.finishes_at or self.created_at
) < (
other.cancelled_at or other.finishes_at or self.created_at
)
@classmethod
def create(cls, *, service_id, template_id):
return cls(broadcast_message_api_client.create_broadcast_message(
service_id=service_id,
template_id=template_id,
))
@classmethod
def from_id(cls, broadcast_message_id, *, service_id):
return cls(broadcast_message_api_client.get_broadcast_message(
service_id=service_id,
broadcast_message_id=broadcast_message_id,
))
@property
def areas(self):
return self.get_areas(areas=self._dict['areas'])
@property
def initial_area_names(self):
return [
area.name for area in self.areas
][:10]
@cached_property
def polygons(self):
return Polygons(
list(itertools.chain(*(
area.polygons for area in self.areas
)))
)
@cached_property
def simple_polygons(self):
return self.get_simple_polygons(areas=self.areas)
@property
def template(self):
response = service_api_client.get_service_template(
self.service_id,
self.template_id,
version=self.template_version,
)
return BroadcastPreviewTemplate(response['data'])
@property
def status(self):
if (
self._dict['status']
and self._dict['status'] == 'broadcasting'
and self.finishes_at < datetime.utcnow().isoformat()
):
return 'completed'
return self._dict['status']
@cached_property
def created_by(self):
return User.from_id(self.created_by_id)
@cached_property
def approved_by(self):
return User.from_id(self.approved_by_id)
@cached_property
def cancelled_by(self):
return User.from_id(self.cancelled_by_id)
@property
def count_of_phones(self):
return round_to_significant_figures(
sum(area.count_of_phones or 0 for area in self.areas),
2
)
@property
def count_of_phones_likely(self):
area_estimate = self.simple_polygons.estimated_area
bleed_area_estimate = self.simple_polygons.bleed.estimated_area - area_estimate
return round_to_significant_figures(
self.count_of_phones * bleed_area_estimate / area_estimate,
1
)
def get_areas(self, areas):
return broadcast_area_libraries.get_areas(
*areas
)
def get_simple_polygons(self, areas):
polygons = Polygons(
list(itertools.chain(*(
area.simple_polygons for area in areas
)))
)
# If weve added multiple areas then we need to re-simplify the
# combined shapes to keep the point count down
return polygons.smooth.simplify if len(areas) > 1 else polygons
def add_areas(self, *new_areas):
areas = list(OrderedSet(
self._dict['areas'] + list(new_areas)
))
simple_polygons = self.get_simple_polygons(areas=self.get_areas(areas=areas))
self._update(areas=areas, simple_polygons=simple_polygons.as_coordinate_pairs_lat_long)
def remove_area(self, area_to_remove):
areas = [
area for area in self._dict['areas']
if area != area_to_remove
]
simple_polygons = self.get_simple_polygons(areas=self.get_areas(areas=areas))
self._update(areas=areas, simple_polygons=simple_polygons.as_coordinate_pairs_lat_long)
def _set_status_to(self, status):
broadcast_message_api_client.update_broadcast_message_status(
status,
broadcast_message_id=self.id,
service_id=self.service_id,
)
def _update(self, **kwargs):
broadcast_message_api_client.update_broadcast_message(
broadcast_message_id=self.id,
service_id=self.service_id,
data=kwargs,
)
def request_approval(self):
self._set_status_to('pending-approval')
def approve_broadcast(self):
self._update(
starts_at=datetime.utcnow().isoformat(),
finishes_at=(
datetime.utcnow() + timedelta(hours=23, minutes=59)
).isoformat(),
)
self._set_status_to('broadcasting')
def reject_broadcast(self):
self._set_status_to('rejected')
def cancel_broadcast(self):
self._set_status_to('cancelled')
class BroadcastMessages(ModelList):
model = BroadcastMessage
client_method = broadcast_message_api_client.get_broadcast_messages
def with_status(self, *statuses):
return [
broadcast for broadcast in self if broadcast.status in statuses
]