2020-08-18 16:36:40 +01:00
|
|
|
from datetime import datetime, timedelta
|
2020-07-09 10:33:50 +01:00
|
|
|
|
|
|
|
|
from notifications_utils.template import BroadcastPreviewTemplate
|
|
|
|
|
from orderedset import OrderedSet
|
Show how a broadcast will overspill selected area
Broadcasting is not a precise technology, because:
- cell towers are directional
- their range varies depending on whether they are 2, 3, 4, or 5G
(the higher the bandwidth the shorter the range)
- in urban areas the towers are more densely packed, so a phone is
likely to have a greater choice of tower to connect to, and will
favour a closer one (which has a stronger signal)
- topography and even weather can affect the range of a tower
So it’s good for us to visually indicate that the broadcast is not as
precise as the boundaries of the area, because it gives the person
sending the message an indication of how the technology works.
At the same time we have a restriction on the number of polygons we
think and area can have, so we’ve done some work to make versions of
polygons which are simplified and buffered (see
https://github.com/alphagov/notifications-utils/pull/769 for context).
Serendipitously, the simplified and buffered polygons are larger and
smoother than the detailed polygons we’ve got from the GeoJSON files. So
they naturally give the impression of covering an area which is wider
and less precise.
So this commit takes those simple polygons and uses them to render the
blue fill. This makes the blue fill extend outside the black stroke,
which is still using the detailed polygons direct from the GeoJSON.
2020-08-06 13:38:15 +01:00
|
|
|
from shapely.geometry import MultiPolygon, Polygon
|
|
|
|
|
from shapely.ops import unary_union
|
2020-08-05 16:01:21 +01:00
|
|
|
from werkzeug.utils import cached_property
|
2020-07-09 10:33:50 +01:00
|
|
|
|
2020-08-06 13:33:52 +01:00
|
|
|
from app.broadcast_areas import broadcast_area_libraries
|
2020-07-09 15:48:56 +01:00
|
|
|
from app.models import JSONModel, ModelList
|
2020-07-10 14:06:00 +01:00
|
|
|
from app.models.user import User
|
2020-07-09 10:33:50 +01:00
|
|
|
from app.notify_client.broadcast_message_api_client import (
|
|
|
|
|
broadcast_message_api_client,
|
|
|
|
|
)
|
|
|
|
|
from app.notify_client.service_api_client import service_api_client
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2020-07-10 09:26:47 +01:00
|
|
|
def __lt__(self, other):
|
|
|
|
|
return (
|
2020-08-19 15:10:44 +01:00
|
|
|
self.cancelled_at or self.finishes_at or self.created_at
|
2020-07-10 09:26:47 +01:00
|
|
|
) < (
|
2020-08-19 15:10:44 +01:00
|
|
|
other.cancelled_at or other.finishes_at or self.created_at
|
2020-07-10 09:26:47 +01:00
|
|
|
)
|
|
|
|
|
|
2020-07-09 10:33:50 +01:00
|
|
|
@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 broadcast_area_libraries.get_areas(
|
|
|
|
|
*self._dict['areas']
|
|
|
|
|
)
|
|
|
|
|
|
2020-07-09 15:48:56 +01:00
|
|
|
@property
|
|
|
|
|
def initial_area_names(self):
|
|
|
|
|
return [
|
|
|
|
|
area.name for area in self.areas
|
|
|
|
|
][:10]
|
|
|
|
|
|
2020-07-09 10:33:50 +01:00
|
|
|
@property
|
|
|
|
|
def polygons(self):
|
|
|
|
|
return broadcast_area_libraries.get_polygons_for_areas_lat_long(
|
|
|
|
|
*self._dict['areas']
|
|
|
|
|
)
|
|
|
|
|
|
Show how a broadcast will overspill selected area
Broadcasting is not a precise technology, because:
- cell towers are directional
- their range varies depending on whether they are 2, 3, 4, or 5G
(the higher the bandwidth the shorter the range)
- in urban areas the towers are more densely packed, so a phone is
likely to have a greater choice of tower to connect to, and will
favour a closer one (which has a stronger signal)
- topography and even weather can affect the range of a tower
So it’s good for us to visually indicate that the broadcast is not as
precise as the boundaries of the area, because it gives the person
sending the message an indication of how the technology works.
At the same time we have a restriction on the number of polygons we
think and area can have, so we’ve done some work to make versions of
polygons which are simplified and buffered (see
https://github.com/alphagov/notifications-utils/pull/769 for context).
Serendipitously, the simplified and buffered polygons are larger and
smoother than the detailed polygons we’ve got from the GeoJSON files. So
they naturally give the impression of covering an area which is wider
and less precise.
So this commit takes those simple polygons and uses them to render the
blue fill. This makes the blue fill extend outside the black stroke,
which is still using the detailed polygons direct from the GeoJSON.
2020-08-06 13:38:15 +01:00
|
|
|
@property
|
|
|
|
|
def simple_polygons(self):
|
|
|
|
|
simple_polygons = broadcast_area_libraries.get_simple_polygons_for_areas_lat_long(
|
|
|
|
|
*self._dict['areas']
|
|
|
|
|
)
|
|
|
|
|
unioned_polygons = unary_union([
|
|
|
|
|
Polygon(i) for i in simple_polygons
|
|
|
|
|
])
|
|
|
|
|
if isinstance(unioned_polygons, MultiPolygon):
|
|
|
|
|
return [
|
|
|
|
|
[
|
|
|
|
|
[x, y] for x, y in p.exterior.coords
|
|
|
|
|
]
|
|
|
|
|
for p in unioned_polygons
|
|
|
|
|
]
|
|
|
|
|
return [[
|
|
|
|
|
[x, y] for x, y in unioned_polygons.exterior.coords
|
|
|
|
|
]]
|
|
|
|
|
|
2020-07-09 10:33:50 +01:00
|
|
|
@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'])
|
|
|
|
|
|
2020-07-09 15:48:56 +01:00
|
|
|
@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']
|
|
|
|
|
|
2020-08-05 16:01:21 +01:00
|
|
|
@cached_property
|
2020-07-10 14:06:00 +01:00
|
|
|
def created_by(self):
|
|
|
|
|
return User.from_id(self.created_by_id)
|
|
|
|
|
|
2020-08-05 16:01:21 +01:00
|
|
|
@cached_property
|
2020-07-10 14:06:00 +01:00
|
|
|
def approved_by(self):
|
|
|
|
|
return User.from_id(self.approved_by_id)
|
|
|
|
|
|
2020-08-05 16:01:21 +01:00
|
|
|
@cached_property
|
2020-07-10 14:06:00 +01:00
|
|
|
def cancelled_by(self):
|
|
|
|
|
return User.from_id(self.cancelled_by_id)
|
|
|
|
|
|
2020-07-09 10:33:50 +01:00
|
|
|
def add_areas(self, *new_areas):
|
2020-07-17 08:07:42 +01:00
|
|
|
self._update(areas=list(OrderedSet(
|
|
|
|
|
self._dict['areas'] + list(new_areas)
|
|
|
|
|
)))
|
2020-07-09 10:33:50 +01:00
|
|
|
|
|
|
|
|
def remove_area(self, area_to_remove):
|
2020-07-17 08:07:42 +01:00
|
|
|
self._update(areas=[
|
|
|
|
|
area for area in self._dict['areas']
|
|
|
|
|
if area != area_to_remove
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
def _set_status_to(self, status):
|
|
|
|
|
broadcast_message_api_client.update_broadcast_message_status(
|
|
|
|
|
status,
|
2020-07-09 10:33:50 +01:00
|
|
|
broadcast_message_id=self.id,
|
|
|
|
|
service_id=self.service_id,
|
|
|
|
|
)
|
|
|
|
|
|
2020-07-17 08:07:42 +01:00
|
|
|
def _update(self, **kwargs):
|
2020-07-09 10:33:50 +01:00
|
|
|
broadcast_message_api_client.update_broadcast_message(
|
|
|
|
|
broadcast_message_id=self.id,
|
|
|
|
|
service_id=self.service_id,
|
2020-07-17 08:07:42 +01:00
|
|
|
data=kwargs,
|
2020-07-09 10:33:50 +01:00
|
|
|
)
|
2020-07-17 08:07:42 +01:00
|
|
|
|
2020-08-18 16:36:40 +01:00
|
|
|
def request_approval(self):
|
2020-07-17 08:07:43 +01:00
|
|
|
self._set_status_to('pending-approval')
|
2020-07-09 15:48:56 +01:00
|
|
|
|
2020-07-17 08:07:44 +01:00
|
|
|
def approve_broadcast(self):
|
|
|
|
|
self._update(
|
|
|
|
|
starts_at=datetime.utcnow().isoformat(),
|
2020-08-18 16:36:40 +01:00
|
|
|
finishes_at=(
|
|
|
|
|
datetime.utcnow() + timedelta(hours=23, minutes=59)
|
|
|
|
|
).isoformat(),
|
2020-07-17 08:07:44 +01:00
|
|
|
)
|
|
|
|
|
self._set_status_to('broadcasting')
|
|
|
|
|
|
2020-07-17 08:07:44 +01:00
|
|
|
def reject_broadcast(self):
|
|
|
|
|
self._set_status_to('rejected')
|
|
|
|
|
|
2020-07-10 09:16:36 +01:00
|
|
|
def cancel_broadcast(self):
|
2020-07-17 08:07:42 +01:00
|
|
|
self._set_status_to('cancelled')
|
2020-07-10 09:16:36 +01:00
|
|
|
|
2020-07-09 15:48:56 +01:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
]
|