Files
notifications-admin/app/models/broadcast_message.py

244 lines
7.4 KiB
Python
Raw Normal View History

import itertools
Remove choice of ‘End time’ from broadcast journey Since we added the end time picker: - we have discovered that broadcasts can’t be longer than 24h - we have observed that most users confuse picking the end time for scheduling the message, or don’t understand exactly what it means for the broadcast to ‘end’ - we’ve developed the concept of ‘training mode’, which you should be going through before sending a real broadcast We also think that, for most scenarios, you won’t necessarily know when a broadcast should end at the time of starting it because the cause of the danger is not within your control. So giving you control of the end time before the broadcast has even been approved is a confusing distraction. Having to pick a time at all also makes the whole process feel more planned and less immediate. Whereas in reality all the phones in the area will be getting the message in seconds. It’s only people coming into the area later to whom the ‘ongoing’ aspect of the broadcast applies. The best place to explain what’s happening with the phones is at the approval stage and once you’ve sent your first (training mode) broadcast. It’s easier to explain what’s happened if it’s in direct response to something you’ve just done. Later on we should add some kind of email reminder after 12 hours to make sure you still want the broadcast live, again after 18 hours, etc. We could let you schedule an end time once the broadcast is live, but don’t think there’s a strong need. Knowing enough that you want to cancel is one thing, but knowing enough to want to cancel but wanting to wait a bit… nah.
2020-08-18 16:36:40 +01:00
from datetime import datetime, timedelta
from notifications_utils.template import BroadcastPreviewTemplate
from orderedset import OrderedSet
from werkzeug.utils import cached_property
from app.broadcast_areas import CustomBroadcastAreas, broadcast_area_libraries
from app.broadcast_areas.polygons import Polygons
from app.formatters import round_to_significant_figures
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,
)
2020-09-09 13:29:45 +01:00
class BroadcastMessage(JSONModel):
ALLOWED_PROPERTIES = {
'id',
'service_id',
'template_id',
'content',
'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):
if self.starts_at and other.starts_at:
return self.starts_at < other.starts_at
if self.starts_at and not other.starts_at:
return True
if not self.starts_at and other.starts_at:
return False
if self.updated_at and not other.updated_at:
return self.updated_at < other.created_at
if not self.updated_at and other.updated_at:
return self.created_at < other.updated_at
if not self.updated_at and not other.updated_at:
return self.created_at < other.created_at
return self.updated_at < other.updated_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,
content=None,
reference=None,
))
@classmethod
def create_from_content(cls, *, service_id, content, reference):
return cls(broadcast_message_api_client.create_broadcast_message(
service_id=service_id,
template_id=None,
content=content,
reference=reference,
))
@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):
library_areas = self.get_areas(areas=self._dict['areas'])
if library_areas:
if len(library_areas) != len(self._dict['areas']):
raise RuntimeError(
f'BroadcastMessage has {len(self._dict["areas"])} areas '
f'but {len(library_areas)} found in the library'
)
return library_areas
return CustomBroadcastAreas(
areas=self._dict['areas'],
polygons=self._dict['simple_polygons'],
)
@property
def parent_areas(self):
return sorted(set(self._parent_areas_iterator))
@property
def _parent_areas_iterator(self):
for area in self.areas:
for parent in area.parents:
yield parent
@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 reference(self):
if self.template_id:
return self._dict['template_name']
return self._dict['reference']
@property
def template(self):
return BroadcastPreviewTemplate({
'template_type': BroadcastPreviewTemplate.template_type,
'name': self.reference,
'content': self.content,
})
@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) if self.created_by_id else None
@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)
2020-09-09 13:29:45 +01:00
@property
def count_of_phones(self):
return round_to_significant_figures(
sum(area.count_of_phones for area in self.areas),
1
2020-09-09 13:29:45 +01:00
)
@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 + (self.count_of_phones * bleed_area_estimate / area_estimate),
2020-09-09 13:29:45 +01:00
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,
)
Remove choice of ‘End time’ from broadcast journey Since we added the end time picker: - we have discovered that broadcasts can’t be longer than 24h - we have observed that most users confuse picking the end time for scheduling the message, or don’t understand exactly what it means for the broadcast to ‘end’ - we’ve developed the concept of ‘training mode’, which you should be going through before sending a real broadcast We also think that, for most scenarios, you won’t necessarily know when a broadcast should end at the time of starting it because the cause of the danger is not within your control. So giving you control of the end time before the broadcast has even been approved is a confusing distraction. Having to pick a time at all also makes the whole process feel more planned and less immediate. Whereas in reality all the phones in the area will be getting the message in seconds. It’s only people coming into the area later to whom the ‘ongoing’ aspect of the broadcast applies. The best place to explain what’s happening with the phones is at the approval stage and once you’ve sent your first (training mode) broadcast. It’s easier to explain what’s happened if it’s in direct response to something you’ve just done. Later on we should add some kind of email reminder after 12 hours to make sure you still want the broadcast live, again after 18 hours, etc. We could let you schedule an end time once the broadcast is live, but don’t think there’s a strong need. Knowing enough that you want to cancel is one thing, but knowing enough to want to cancel but wanting to wait a bit… nah.
2020-08-18 16:36:40 +01:00
def request_approval(self):
self._set_status_to('pending-approval')
def approve_broadcast(self):
self._update(
starts_at=datetime.utcnow().isoformat(),
Remove choice of ‘End time’ from broadcast journey Since we added the end time picker: - we have discovered that broadcasts can’t be longer than 24h - we have observed that most users confuse picking the end time for scheduling the message, or don’t understand exactly what it means for the broadcast to ‘end’ - we’ve developed the concept of ‘training mode’, which you should be going through before sending a real broadcast We also think that, for most scenarios, you won’t necessarily know when a broadcast should end at the time of starting it because the cause of the danger is not within your control. So giving you control of the end time before the broadcast has even been approved is a confusing distraction. Having to pick a time at all also makes the whole process feel more planned and less immediate. Whereas in reality all the phones in the area will be getting the message in seconds. It’s only people coming into the area later to whom the ‘ongoing’ aspect of the broadcast applies. The best place to explain what’s happening with the phones is at the approval stage and once you’ve sent your first (training mode) broadcast. It’s easier to explain what’s happened if it’s in direct response to something you’ve just done. Later on we should add some kind of email reminder after 12 hours to make sure you still want the broadcast live, again after 18 hours, etc. We could let you schedule an end time once the broadcast is live, but don’t think there’s a strong need. Knowing enough that you want to cancel is one thing, but knowing enough to want to cancel but wanting to wait a bit… nah.
2020-08-18 16:36:40 +01:00
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
]