2020-01-21 16:50:44 +00:00
|
|
|
from flask import abort, current_app
|
2018-10-26 15:58:44 +01:00
|
|
|
from werkzeug.utils import cached_property
|
|
|
|
|
|
2021-11-11 14:35:55 +00:00
|
|
|
from app.models import JSONModel, SortByNameMixin
|
2023-08-25 08:57:24 -07:00
|
|
|
from app.models.job import ImmediateJobs, PaginatedJobs, PaginatedUploads, ScheduledJobs
|
2023-07-12 12:09:44 -04:00
|
|
|
from app.models.organization import Organization
|
Make user API client return JSON, not a model
The data flow of other bits of our application looks like this:
```
API (returns JSON)
⬇
API client (returns a built in type, usually `dict`)
⬇
Model (returns an instance, eg of type `Service`)
⬇
View (returns HTML)
```
The user API client was architected weirdly, in that it returned a model
directly, like this:
```
API (returns JSON)
⬇
API client (returns a model, of type `User`, `InvitedUser`, etc)
⬇
View (returns HTML)
```
This mixing of different layers of the application is bad because it
makes it hard to write model code that doesn’t have circular
dependencies. As our application gets more complicated we will be
relying more on models to manage this complexity, so we should make it
easy, not hard to write them.
It also means that most of our mocking was of the User model, not just
the underlying JSON. So it would have been easy to introduce subtle bugs
to the user model, because it wasn’t being comprehensively tested. A lot
of the changed lines of code in this commit mean changing the tests to
mock only the JSON, which means that the model layer gets implicitly
tested.
For those reasons this commit changes the user API client to return
JSON, not an instance of `User` or other models.
2019-05-23 15:27:35 +01:00
|
|
|
from app.models.user import InvitedUsers, User, Users
|
2018-11-07 11:35:24 +00:00
|
|
|
from app.notify_client.api_key_api_client import api_key_api_client
|
2018-10-26 17:31:49 +01:00
|
|
|
from app.notify_client.billing_api_client import billing_api_client
|
|
|
|
|
from app.notify_client.inbound_number_client import inbound_number_client
|
2018-12-03 10:59:36 +00:00
|
|
|
from app.notify_client.invite_api_client import invite_api_client
|
2018-10-26 16:09:59 +01:00
|
|
|
from app.notify_client.job_api_client import job_api_client
|
2023-07-12 12:09:44 -04:00
|
|
|
from app.notify_client.organizations_api_client import organizations_client
|
2018-10-26 16:09:59 +01:00
|
|
|
from app.notify_client.service_api_client import service_api_client
|
2023-08-25 08:57:24 -07:00
|
|
|
from app.notify_client.template_folder_api_client import template_folder_api_client
|
2018-10-26 15:58:44 +01:00
|
|
|
from app.utils import get_default_sms_sender
|
2024-05-16 10:37:37 -04:00
|
|
|
from notifications_utils.serialised_model import SerialisedModelCollection
|
2018-10-26 15:58:44 +01:00
|
|
|
|
|
|
|
|
|
2021-11-11 14:35:55 +00:00
|
|
|
class Service(JSONModel, SortByNameMixin):
|
2018-10-26 15:58:44 +01:00
|
|
|
ALLOWED_PROPERTIES = {
|
2023-08-25 09:12:23 -07:00
|
|
|
"active",
|
|
|
|
|
"billing_contact_email_addresses",
|
|
|
|
|
"billing_contact_names",
|
|
|
|
|
"billing_reference",
|
|
|
|
|
"consent_to_research",
|
|
|
|
|
"contact_link",
|
|
|
|
|
"count_as_live",
|
|
|
|
|
"email_from",
|
2023-12-20 11:27:12 -05:00
|
|
|
"go_live_at",
|
|
|
|
|
"go_live_user",
|
2023-08-25 09:12:23 -07:00
|
|
|
"id",
|
|
|
|
|
"inbound_api",
|
|
|
|
|
"message_limit",
|
|
|
|
|
"rate_limit",
|
|
|
|
|
"name",
|
|
|
|
|
"notes",
|
|
|
|
|
"prefix_sms",
|
|
|
|
|
"purchase_order_number",
|
|
|
|
|
"research_mode",
|
|
|
|
|
"service_callback_api",
|
|
|
|
|
"volume_email",
|
|
|
|
|
"volume_sms",
|
2018-10-26 15:58:44 +01:00
|
|
|
}
|
|
|
|
|
|
2018-11-08 11:54:57 +00:00
|
|
|
TEMPLATE_TYPES = (
|
2023-08-25 09:12:23 -07:00
|
|
|
"email",
|
|
|
|
|
"sms",
|
2018-11-08 11:54:57 +00:00
|
|
|
)
|
|
|
|
|
|
2020-06-03 14:54:37 +01:00
|
|
|
ALL_PERMISSIONS = TEMPLATE_TYPES + (
|
2023-08-25 09:12:23 -07:00
|
|
|
"edit_folder_permissions",
|
|
|
|
|
"email_auth",
|
|
|
|
|
"inbound_sms",
|
|
|
|
|
"international_sms",
|
|
|
|
|
"upload_document",
|
2020-06-03 14:54:37 +01:00
|
|
|
)
|
|
|
|
|
|
Make user API client return JSON, not a model
The data flow of other bits of our application looks like this:
```
API (returns JSON)
⬇
API client (returns a built in type, usually `dict`)
⬇
Model (returns an instance, eg of type `Service`)
⬇
View (returns HTML)
```
The user API client was architected weirdly, in that it returned a model
directly, like this:
```
API (returns JSON)
⬇
API client (returns a model, of type `User`, `InvitedUser`, etc)
⬇
View (returns HTML)
```
This mixing of different layers of the application is bad because it
makes it hard to write model code that doesn’t have circular
dependencies. As our application gets more complicated we will be
relying more on models to manage this complexity, so we should make it
easy, not hard to write them.
It also means that most of our mocking was of the User model, not just
the underlying JSON. So it would have been easy to introduce subtle bugs
to the user model, because it wasn’t being comprehensively tested. A lot
of the changed lines of code in this commit mean changing the tests to
mock only the JSON, which means that the model layer gets implicitly
tested.
For those reasons this commit changes the user API client to return
JSON, not an instance of `User` or other models.
2019-05-23 15:27:35 +01:00
|
|
|
@classmethod
|
|
|
|
|
def from_id(cls, service_id):
|
2023-08-25 09:12:23 -07:00
|
|
|
return cls(service_api_client.get_service(service_id)["data"])
|
Make user API client return JSON, not a model
The data flow of other bits of our application looks like this:
```
API (returns JSON)
⬇
API client (returns a built in type, usually `dict`)
⬇
Model (returns an instance, eg of type `Service`)
⬇
View (returns HTML)
```
The user API client was architected weirdly, in that it returned a model
directly, like this:
```
API (returns JSON)
⬇
API client (returns a model, of type `User`, `InvitedUser`, etc)
⬇
View (returns HTML)
```
This mixing of different layers of the application is bad because it
makes it hard to write model code that doesn’t have circular
dependencies. As our application gets more complicated we will be
relying more on models to manage this complexity, so we should make it
easy, not hard to write them.
It also means that most of our mocking was of the User model, not just
the underlying JSON. So it would have been easy to introduce subtle bugs
to the user model, because it wasn’t being comprehensively tested. A lot
of the changed lines of code in this commit mean changing the tests to
mock only the JSON, which means that the model layer gets implicitly
tested.
For those reasons this commit changes the user API client to return
JSON, not an instance of `User` or other models.
2019-05-23 15:27:35 +01:00
|
|
|
|
2020-06-03 15:04:45 +01:00
|
|
|
@property
|
|
|
|
|
def permissions(self):
|
2023-08-25 09:12:23 -07:00
|
|
|
return self._dict.get("permissions", self.TEMPLATE_TYPES)
|
2020-06-03 15:04:45 +01:00
|
|
|
|
2021-01-25 12:54:23 +00:00
|
|
|
@property
|
|
|
|
|
def billing_details(self):
|
|
|
|
|
billing_details = [
|
2021-01-26 10:41:22 +00:00
|
|
|
self.billing_contact_email_addresses,
|
|
|
|
|
self.billing_contact_names,
|
2021-01-25 12:54:23 +00:00
|
|
|
self.billing_reference,
|
2023-08-25 09:12:23 -07:00
|
|
|
self.purchase_order_number,
|
2021-01-25 12:54:23 +00:00
|
|
|
]
|
|
|
|
|
if any(billing_details):
|
|
|
|
|
return billing_details
|
|
|
|
|
else:
|
|
|
|
|
return None
|
|
|
|
|
|
2018-11-05 16:03:11 +00:00
|
|
|
def update(self, **kwargs):
|
|
|
|
|
return service_api_client.update_service(self.id, **kwargs)
|
|
|
|
|
|
2019-07-08 14:32:18 +01:00
|
|
|
def update_count_as_live(self, count_as_live):
|
2023-08-25 09:12:23 -07:00
|
|
|
return service_api_client.update_count_as_live(
|
|
|
|
|
self.id, count_as_live=count_as_live
|
|
|
|
|
)
|
2019-07-08 14:32:18 +01:00
|
|
|
|
2019-04-10 17:20:51 +01:00
|
|
|
def update_status(self, live):
|
|
|
|
|
return service_api_client.update_status(self.id, live=live)
|
|
|
|
|
|
2018-11-05 17:03:37 +00:00
|
|
|
def switch_permission(self, permission):
|
2018-11-05 16:53:03 +00:00
|
|
|
return self.force_permission(
|
|
|
|
|
permission,
|
|
|
|
|
on=not self.has_permission(permission),
|
|
|
|
|
)
|
|
|
|
|
|
2018-11-05 17:03:37 +00:00
|
|
|
def force_permission(self, permission, on=False):
|
2018-11-05 16:53:03 +00:00
|
|
|
permissions, permission = set(self.permissions), {permission}
|
|
|
|
|
|
|
|
|
|
return self.update_permissions(
|
|
|
|
|
permissions | permission if on else permissions - permission,
|
|
|
|
|
)
|
|
|
|
|
|
2018-11-05 17:03:37 +00:00
|
|
|
def update_permissions(self, permissions):
|
2018-11-05 17:37:50 +00:00
|
|
|
return self.update(permissions=list(permissions))
|
2018-11-05 16:53:03 +00:00
|
|
|
|
2018-11-05 16:17:44 +00:00
|
|
|
def toggle_research_mode(self):
|
2018-11-05 17:37:50 +00:00
|
|
|
self.update(research_mode=not self.research_mode)
|
2018-11-05 16:17:44 +00:00
|
|
|
|
2018-10-26 15:58:44 +01:00
|
|
|
@property
|
|
|
|
|
def trial_mode(self):
|
2023-08-25 09:12:23 -07:00
|
|
|
return self._dict["restricted"]
|
2018-10-26 15:58:44 +01:00
|
|
|
|
2019-06-07 12:38:48 +01:00
|
|
|
@property
|
|
|
|
|
def live(self):
|
|
|
|
|
return not self.trial_mode
|
|
|
|
|
|
2018-10-26 15:58:44 +01:00
|
|
|
def has_permission(self, permission):
|
2020-06-03 14:54:37 +01:00
|
|
|
if permission not in self.ALL_PERMISSIONS:
|
2023-08-25 09:12:23 -07:00
|
|
|
raise KeyError(f"{permission} is not a service permission")
|
2018-10-26 15:58:44 +01:00
|
|
|
return permission in self.permissions
|
|
|
|
|
|
2020-01-08 14:29:56 +00:00
|
|
|
def get_page_of_jobs(self, page):
|
|
|
|
|
return PaginatedJobs(self.id, page=page)
|
|
|
|
|
|
|
|
|
|
def get_page_of_uploads(self, page):
|
|
|
|
|
return PaginatedUploads(self.id, page=page)
|
|
|
|
|
|
2018-10-26 15:58:44 +01:00
|
|
|
@cached_property
|
|
|
|
|
def has_jobs(self):
|
|
|
|
|
return job_api_client.has_jobs(self.id)
|
|
|
|
|
|
2020-01-08 14:29:56 +00:00
|
|
|
@cached_property
|
|
|
|
|
def immediate_jobs(self):
|
|
|
|
|
if not self.has_jobs:
|
|
|
|
|
return []
|
|
|
|
|
return ImmediateJobs(self.id)
|
|
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
|
def scheduled_jobs(self):
|
|
|
|
|
if not self.has_jobs:
|
|
|
|
|
return []
|
|
|
|
|
return ScheduledJobs(self.id)
|
|
|
|
|
|
2020-09-28 14:28:23 +01:00
|
|
|
@cached_property
|
|
|
|
|
def scheduled_job_stats(self):
|
|
|
|
|
if not self.has_jobs:
|
2023-08-25 09:12:23 -07:00
|
|
|
return {"count": 0}
|
2020-09-28 14:28:23 +01:00
|
|
|
return job_api_client.get_scheduled_job_stats(self.id)
|
|
|
|
|
|
2019-02-25 16:51:37 +00:00
|
|
|
@cached_property
|
|
|
|
|
def invited_users(self):
|
Make user API client return JSON, not a model
The data flow of other bits of our application looks like this:
```
API (returns JSON)
⬇
API client (returns a built in type, usually `dict`)
⬇
Model (returns an instance, eg of type `Service`)
⬇
View (returns HTML)
```
The user API client was architected weirdly, in that it returned a model
directly, like this:
```
API (returns JSON)
⬇
API client (returns a model, of type `User`, `InvitedUser`, etc)
⬇
View (returns HTML)
```
This mixing of different layers of the application is bad because it
makes it hard to write model code that doesn’t have circular
dependencies. As our application gets more complicated we will be
relying more on models to manage this complexity, so we should make it
easy, not hard to write them.
It also means that most of our mocking was of the User model, not just
the underlying JSON. So it would have been easy to introduce subtle bugs
to the user model, because it wasn’t being comprehensively tested. A lot
of the changed lines of code in this commit mean changing the tests to
mock only the JSON, which means that the model layer gets implicitly
tested.
For those reasons this commit changes the user API client to return
JSON, not an instance of `User` or other models.
2019-05-23 15:27:35 +01:00
|
|
|
return InvitedUsers(self.id)
|
2019-02-25 16:51:37 +00:00
|
|
|
|
2020-06-08 17:39:14 +01:00
|
|
|
def invite_pending_for(self, email_address):
|
|
|
|
|
return email_address.lower() in (
|
2023-08-25 09:12:23 -07:00
|
|
|
invited_user.email_address.lower() for invited_user in self.invited_users
|
2020-06-08 17:39:14 +01:00
|
|
|
)
|
|
|
|
|
|
2019-02-25 16:51:37 +00:00
|
|
|
@cached_property
|
|
|
|
|
def active_users(self):
|
Make user API client return JSON, not a model
The data flow of other bits of our application looks like this:
```
API (returns JSON)
⬇
API client (returns a built in type, usually `dict`)
⬇
Model (returns an instance, eg of type `Service`)
⬇
View (returns HTML)
```
The user API client was architected weirdly, in that it returned a model
directly, like this:
```
API (returns JSON)
⬇
API client (returns a model, of type `User`, `InvitedUser`, etc)
⬇
View (returns HTML)
```
This mixing of different layers of the application is bad because it
makes it hard to write model code that doesn’t have circular
dependencies. As our application gets more complicated we will be
relying more on models to manage this complexity, so we should make it
easy, not hard to write them.
It also means that most of our mocking was of the User model, not just
the underlying JSON. So it would have been easy to introduce subtle bugs
to the user model, because it wasn’t being comprehensively tested. A lot
of the changed lines of code in this commit mean changing the tests to
mock only the JSON, which means that the model layer gets implicitly
tested.
For those reasons this commit changes the user API client to return
JSON, not an instance of `User` or other models.
2019-05-23 15:27:35 +01:00
|
|
|
return Users(self.id)
|
2019-02-25 16:51:37 +00:00
|
|
|
|
2018-12-03 10:59:36 +00:00
|
|
|
@cached_property
|
|
|
|
|
def team_members(self):
|
2018-12-05 16:39:15 +00:00
|
|
|
return sorted(
|
2019-02-25 16:51:37 +00:00
|
|
|
self.invited_users + self.active_users,
|
2019-02-01 13:09:02 +00:00
|
|
|
key=lambda user: user.email_address.lower(),
|
2018-12-05 16:39:15 +00:00
|
|
|
)
|
2018-12-03 10:59:36 +00:00
|
|
|
|
2018-10-26 15:58:44 +01:00
|
|
|
@cached_property
|
|
|
|
|
def has_team_members(self):
|
2023-08-25 09:12:23 -07:00
|
|
|
return (
|
|
|
|
|
len(
|
|
|
|
|
[
|
|
|
|
|
user
|
|
|
|
|
for user in self.team_members
|
|
|
|
|
if user.has_permission_for_service(self.id, "manage_service")
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
> 1
|
|
|
|
|
)
|
2018-10-26 15:58:44 +01:00
|
|
|
|
2019-02-25 16:51:37 +00:00
|
|
|
def cancel_invite(self, invited_user_id):
|
|
|
|
|
if str(invited_user_id) not in {user.id for user in self.invited_users}:
|
|
|
|
|
abort(404)
|
|
|
|
|
|
|
|
|
|
return invite_api_client.cancel_invited_user(
|
|
|
|
|
service_id=self.id,
|
|
|
|
|
invited_user_id=str(invited_user_id),
|
|
|
|
|
)
|
|
|
|
|
|
2023-12-05 16:08:43 -05:00
|
|
|
def resend_invite(self, invited_user_id):
|
|
|
|
|
if str(invited_user_id) not in {user.id for user in self.invited_users}:
|
|
|
|
|
abort(404)
|
|
|
|
|
|
|
|
|
|
return invite_api_client.resend_invite(
|
|
|
|
|
service_id=self.id,
|
|
|
|
|
invited_user_id=str(invited_user_id),
|
|
|
|
|
)
|
|
|
|
|
|
2019-02-25 16:51:37 +00:00
|
|
|
def get_team_member(self, user_id):
|
|
|
|
|
if str(user_id) not in {user.id for user in self.active_users}:
|
|
|
|
|
abort(404)
|
|
|
|
|
|
Make user API client return JSON, not a model
The data flow of other bits of our application looks like this:
```
API (returns JSON)
⬇
API client (returns a built in type, usually `dict`)
⬇
Model (returns an instance, eg of type `Service`)
⬇
View (returns HTML)
```
The user API client was architected weirdly, in that it returned a model
directly, like this:
```
API (returns JSON)
⬇
API client (returns a model, of type `User`, `InvitedUser`, etc)
⬇
View (returns HTML)
```
This mixing of different layers of the application is bad because it
makes it hard to write model code that doesn’t have circular
dependencies. As our application gets more complicated we will be
relying more on models to manage this complexity, so we should make it
easy, not hard to write them.
It also means that most of our mocking was of the User model, not just
the underlying JSON. So it would have been easy to introduce subtle bugs
to the user model, because it wasn’t being comprehensively tested. A lot
of the changed lines of code in this commit mean changing the tests to
mock only the JSON, which means that the model layer gets implicitly
tested.
For those reasons this commit changes the user API client to return
JSON, not an instance of `User` or other models.
2019-05-23 15:27:35 +01:00
|
|
|
return User.from_id(user_id)
|
2019-02-25 16:51:37 +00:00
|
|
|
|
2018-10-26 15:58:44 +01:00
|
|
|
@cached_property
|
2018-11-05 15:26:59 +00:00
|
|
|
def all_templates(self):
|
2023-08-25 09:12:23 -07:00
|
|
|
templates = service_api_client.get_service_templates(self.id)["data"]
|
2018-10-26 15:58:44 +01:00
|
|
|
|
|
|
|
|
return [
|
2023-08-25 09:12:23 -07:00
|
|
|
template
|
|
|
|
|
for template in templates
|
|
|
|
|
if template["template_type"] in self.available_template_types
|
2018-10-26 15:58:44 +01:00
|
|
|
]
|
|
|
|
|
|
2018-11-08 11:42:01 +00:00
|
|
|
@cached_property
|
|
|
|
|
def all_template_ids(self):
|
2023-08-25 09:12:23 -07:00
|
|
|
return {template["id"] for template in self.all_templates}
|
2018-11-08 11:42:01 +00:00
|
|
|
|
2019-03-19 15:59:59 +00:00
|
|
|
def get_template(self, template_id, version=None):
|
2023-08-25 09:12:23 -07:00
|
|
|
return service_api_client.get_service_template(self.id, template_id, version)[
|
|
|
|
|
"data"
|
|
|
|
|
]
|
2019-03-19 15:59:59 +00:00
|
|
|
|
2019-03-20 17:26:51 +00:00
|
|
|
def get_template_folder_with_user_permission_or_403(self, folder_id, user):
|
|
|
|
|
template_folder = self.get_template_folder(folder_id)
|
2019-03-19 15:59:59 +00:00
|
|
|
|
|
|
|
|
if not user.has_template_folder_permission(template_folder):
|
|
|
|
|
abort(403)
|
|
|
|
|
|
2019-03-20 17:26:51 +00:00
|
|
|
return template_folder
|
|
|
|
|
|
|
|
|
|
def get_template_with_user_permission_or_403(self, template_id, user):
|
|
|
|
|
template = self.get_template(template_id)
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
self.get_template_folder_with_user_permission_or_403(template["folder"], user)
|
2019-03-20 17:26:51 +00:00
|
|
|
|
2019-03-19 15:59:59 +00:00
|
|
|
return template
|
|
|
|
|
|
2018-10-26 15:58:44 +01:00
|
|
|
@property
|
|
|
|
|
def available_template_types(self):
|
2018-11-12 09:16:14 +00:00
|
|
|
return list(filter(self.has_permission, self.TEMPLATE_TYPES))
|
2018-11-08 11:54:57 +00:00
|
|
|
|
2018-10-26 15:58:44 +01:00
|
|
|
@property
|
|
|
|
|
def has_templates(self):
|
2018-11-19 16:52:21 +00:00
|
|
|
return bool(self.all_templates)
|
|
|
|
|
|
|
|
|
|
def has_folders(self):
|
|
|
|
|
return bool(self.all_template_folders)
|
2018-10-26 15:58:44 +01:00
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def has_multiple_template_types(self):
|
2023-08-25 09:12:23 -07:00
|
|
|
return len({template["template_type"] for template in self.all_templates}) > 1
|
2018-10-26 15:58:44 +01:00
|
|
|
|
2019-02-15 11:03:19 +00:00
|
|
|
@property
|
|
|
|
|
def has_estimated_usage(self):
|
2023-08-25 09:12:23 -07:00
|
|
|
return self.consent_to_research is not None and any(
|
|
|
|
|
(
|
2019-02-15 11:03:19 +00:00
|
|
|
self.volume_email,
|
|
|
|
|
self.volume_sms,
|
2023-08-25 09:12:23 -07:00
|
|
|
)
|
2019-02-15 11:03:19 +00:00
|
|
|
)
|
|
|
|
|
|
2022-05-27 10:31:06 +01:00
|
|
|
def has_templates_of_type(self, template_type):
|
|
|
|
|
return any(
|
2023-08-25 09:12:23 -07:00
|
|
|
template
|
|
|
|
|
for template in self.all_templates
|
|
|
|
|
if template["template_type"] == template_type
|
2022-05-27 10:31:06 +01:00
|
|
|
)
|
|
|
|
|
|
2018-10-26 15:58:44 +01:00
|
|
|
@property
|
|
|
|
|
def has_email_templates(self):
|
2023-08-25 09:12:23 -07:00
|
|
|
return self.has_templates_of_type("email")
|
2018-10-26 15:58:44 +01:00
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def has_sms_templates(self):
|
2023-08-25 09:12:23 -07:00
|
|
|
return self.has_templates_of_type("sms")
|
2018-10-26 15:58:44 +01:00
|
|
|
|
2019-03-08 14:26:39 +00:00
|
|
|
@property
|
|
|
|
|
def intending_to_send_email(self):
|
|
|
|
|
if self.volume_email is None:
|
|
|
|
|
return self.has_email_templates
|
|
|
|
|
return self.volume_email > 0
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def intending_to_send_sms(self):
|
|
|
|
|
if self.volume_sms is None:
|
|
|
|
|
return self.has_sms_templates
|
|
|
|
|
return self.volume_sms > 0
|
|
|
|
|
|
2018-10-26 15:58:44 +01:00
|
|
|
@cached_property
|
2018-10-26 17:31:49 +01:00
|
|
|
def email_reply_to_addresses(self):
|
|
|
|
|
return service_api_client.get_reply_to_email_addresses(self.id)
|
|
|
|
|
|
|
|
|
|
@property
|
2018-10-26 15:58:44 +01:00
|
|
|
def has_email_reply_to_address(self):
|
2018-10-26 17:31:49 +01:00
|
|
|
return bool(self.email_reply_to_addresses)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def count_email_reply_to_addresses(self):
|
|
|
|
|
return len(self.email_reply_to_addresses)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def default_email_reply_to_address(self):
|
|
|
|
|
return next(
|
|
|
|
|
(
|
2023-08-25 09:12:23 -07:00
|
|
|
x["email_address"]
|
|
|
|
|
for x in self.email_reply_to_addresses
|
|
|
|
|
if x["is_default"]
|
|
|
|
|
),
|
|
|
|
|
None,
|
2018-10-26 17:31:49 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def get_email_reply_to_address(self, id):
|
|
|
|
|
return service_api_client.get_reply_to_email_address(self.id, id)
|
2018-10-26 15:58:44 +01:00
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def needs_to_add_email_reply_to_address(self):
|
2019-03-08 14:26:39 +00:00
|
|
|
return self.intending_to_send_email and not self.has_email_reply_to_address
|
2018-10-26 15:58:44 +01:00
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def shouldnt_use_govuk_as_sms_sender(self):
|
2023-07-12 12:09:44 -04:00
|
|
|
return self.organization_type != Organization.TYPE_CENTRAL
|
2018-10-26 15:58:44 +01:00
|
|
|
|
|
|
|
|
@cached_property
|
2018-10-26 17:31:49 +01:00
|
|
|
def sms_senders(self):
|
|
|
|
|
return service_api_client.get_sms_senders(self.id)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def sms_senders_with_hints(self):
|
|
|
|
|
def attach_hint(sender):
|
|
|
|
|
hints = []
|
2023-08-25 09:12:23 -07:00
|
|
|
if sender["is_default"]:
|
2018-10-26 17:31:49 +01:00
|
|
|
hints += ["default"]
|
2023-08-25 09:12:23 -07:00
|
|
|
if sender["inbound_number_id"]:
|
2018-10-26 17:31:49 +01:00
|
|
|
hints += ["receives replies"]
|
|
|
|
|
if hints:
|
2023-08-25 09:12:23 -07:00
|
|
|
sender["hint"] = "(" + " and ".join(hints) + ")"
|
2018-10-26 17:31:49 +01:00
|
|
|
return sender
|
|
|
|
|
|
|
|
|
|
return [attach_hint(sender) for sender in self.sms_senders]
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def default_sms_sender(self):
|
|
|
|
|
return get_default_sms_sender(self.sms_senders)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def count_sms_senders(self):
|
|
|
|
|
return len(self.sms_senders)
|
|
|
|
|
|
|
|
|
|
@property
|
2018-10-26 15:58:44 +01:00
|
|
|
def sms_sender_is_govuk(self):
|
2023-08-25 09:12:23 -07:00
|
|
|
return self.default_sms_sender in {"GOVUK", "None"}
|
2018-10-26 17:31:49 +01:00
|
|
|
|
|
|
|
|
def get_sms_sender(self, id):
|
|
|
|
|
return service_api_client.get_sms_sender(self.id, id)
|
2018-10-26 15:58:44 +01:00
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def needs_to_change_sms_sender(self):
|
2023-08-25 09:12:23 -07:00
|
|
|
return all(
|
|
|
|
|
(
|
|
|
|
|
self.intending_to_send_sms,
|
|
|
|
|
self.shouldnt_use_govuk_as_sms_sender,
|
|
|
|
|
self.sms_sender_is_govuk,
|
|
|
|
|
)
|
|
|
|
|
)
|
2018-10-26 15:58:44 +01:00
|
|
|
|
2019-02-27 17:15:09 +00:00
|
|
|
@property
|
|
|
|
|
def volumes(self):
|
2023-08-25 09:12:23 -07:00
|
|
|
return sum(
|
|
|
|
|
filter(
|
|
|
|
|
None,
|
|
|
|
|
(
|
|
|
|
|
self.volume_email,
|
|
|
|
|
self.volume_sms,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
)
|
2019-02-27 17:15:09 +00:00
|
|
|
|
2018-10-26 17:31:49 +01:00
|
|
|
@cached_property
|
|
|
|
|
def free_sms_fragment_limit(self):
|
|
|
|
|
return billing_api_client.get_free_sms_fragment_limit_for_year(self.id) or 0
|
|
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
|
def data_retention(self):
|
|
|
|
|
return service_api_client.get_service_data_retention(self.id)
|
|
|
|
|
|
|
|
|
|
def get_data_retention_item(self, id):
|
2023-08-25 09:12:23 -07:00
|
|
|
return next((dr for dr in self.data_retention if dr["id"] == id), None)
|
2018-12-03 17:19:38 +00:00
|
|
|
|
2018-12-03 17:57:11 +00:00
|
|
|
def get_days_of_retention(self, notification_type):
|
2018-12-03 17:19:38 +00:00
|
|
|
return next(
|
2023-08-25 09:12:23 -07:00
|
|
|
(
|
|
|
|
|
dr
|
|
|
|
|
for dr in self.data_retention
|
|
|
|
|
if dr["notification_type"] == notification_type
|
|
|
|
|
),
|
|
|
|
|
{},
|
|
|
|
|
).get("days_of_retention", current_app.config["ACTIVITY_STATS_LIMIT_DAYS"])
|
2018-10-26 17:31:49 +01:00
|
|
|
|
|
|
|
|
@cached_property
|
2023-07-12 12:09:44 -04:00
|
|
|
def organization(self):
|
|
|
|
|
return Organization.from_id(self.organization_id)
|
2018-10-26 17:31:49 +01:00
|
|
|
|
2019-06-12 12:09:26 +01:00
|
|
|
@property
|
2023-07-12 12:09:44 -04:00
|
|
|
def organization_id(self):
|
2023-08-25 09:12:23 -07:00
|
|
|
return self._dict["organization"]
|
2019-06-12 12:09:26 +01:00
|
|
|
|
2019-07-09 10:38:07 +01:00
|
|
|
@property
|
2023-07-12 12:09:44 -04:00
|
|
|
def organization_type(self):
|
2023-08-25 09:12:23 -07:00
|
|
|
return self.organization.organization_type or self._dict["organization_type"]
|
2019-07-09 10:38:07 +01:00
|
|
|
|
2020-03-20 08:42:33 +00:00
|
|
|
@property
|
2023-07-12 12:09:44 -04:00
|
|
|
def organization_name(self):
|
|
|
|
|
if not self.organization_id:
|
2020-03-20 08:42:33 +00:00
|
|
|
return None
|
2023-07-12 12:09:44 -04:00
|
|
|
return organizations_client.get_organization_name(self.organization_id)
|
2020-03-20 08:42:33 +00:00
|
|
|
|
2019-08-27 16:32:30 +01:00
|
|
|
@property
|
2023-07-12 12:09:44 -04:00
|
|
|
def organization_type_label(self):
|
|
|
|
|
return Organization.TYPE_LABELS.get(self.organization_type)
|
2019-08-27 16:32:30 +01:00
|
|
|
|
2018-10-26 17:31:49 +01:00
|
|
|
@cached_property
|
|
|
|
|
def inbound_number(self):
|
2023-08-25 09:12:23 -07:00
|
|
|
return inbound_number_client.get_inbound_sms_number_for_service(self.id)[
|
|
|
|
|
"data"
|
|
|
|
|
].get("number", "")
|
2018-10-26 17:31:49 +01:00
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def has_inbound_number(self):
|
|
|
|
|
return bool(self.inbound_number)
|
2018-11-01 15:33:09 +00:00
|
|
|
|
2020-02-12 14:03:51 +00:00
|
|
|
@cached_property
|
|
|
|
|
def inbound_sms_summary(self):
|
2023-08-25 09:12:23 -07:00
|
|
|
if not self.has_permission("inbound_sms"):
|
2020-02-12 14:03:51 +00:00
|
|
|
return None
|
|
|
|
|
return service_api_client.get_inbound_sms_summary(self.id)
|
|
|
|
|
|
2018-11-01 15:33:09 +00:00
|
|
|
@cached_property
|
2018-11-05 16:25:19 +00:00
|
|
|
def all_template_folders(self):
|
2018-11-09 16:04:49 +00:00
|
|
|
return sorted(
|
|
|
|
|
template_folder_api_client.get_template_folders(self.id),
|
2023-08-25 09:12:23 -07:00
|
|
|
key=lambda folder: folder["name"].lower(),
|
2018-11-09 16:04:49 +00:00
|
|
|
)
|
2018-11-05 16:25:19 +00:00
|
|
|
|
2018-11-08 11:42:01 +00:00
|
|
|
@cached_property
|
|
|
|
|
def all_template_folder_ids(self):
|
2023-08-25 09:12:23 -07:00
|
|
|
return {folder["id"] for folder in self.all_template_folders}
|
2018-11-08 11:42:01 +00:00
|
|
|
|
2018-11-13 16:05:05 +00:00
|
|
|
def get_template_folder(self, folder_id):
|
2018-11-16 13:41:55 +00:00
|
|
|
if folder_id is None:
|
|
|
|
|
return {
|
2023-08-25 09:12:23 -07:00
|
|
|
"id": None,
|
|
|
|
|
"name": "Templates",
|
|
|
|
|
"parent_id": None,
|
2018-11-16 13:41:55 +00:00
|
|
|
}
|
2018-11-14 10:16:55 +00:00
|
|
|
return self._get_by_id(self.all_template_folders, folder_id)
|
2018-11-13 16:05:05 +00:00
|
|
|
|
2018-11-05 16:25:19 +00:00
|
|
|
def get_template_folder_path(self, template_folder_id):
|
2018-11-16 13:41:55 +00:00
|
|
|
folder = self.get_template_folder(template_folder_id)
|
2018-11-05 16:25:19 +00:00
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
if folder["id"] is None:
|
2018-11-16 13:41:55 +00:00
|
|
|
return [folder]
|
2018-11-05 16:25:19 +00:00
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
return self.get_template_folder_path(folder["parent_id"]) + [
|
|
|
|
|
self.get_template_folder(folder["id"])
|
2018-11-16 13:41:55 +00:00
|
|
|
]
|
2018-11-08 11:56:29 +00:00
|
|
|
|
2018-11-16 14:09:14 +00:00
|
|
|
def get_template_path(self, template):
|
2023-08-25 09:12:23 -07:00
|
|
|
return self.get_template_folder_path(template["folder"]) + [
|
2018-11-16 14:09:14 +00:00
|
|
|
template,
|
|
|
|
|
]
|
|
|
|
|
|
2018-11-20 12:18:18 +00:00
|
|
|
@property
|
|
|
|
|
def count_of_templates_and_folders(self):
|
2018-11-20 16:10:14 +00:00
|
|
|
return len(self.all_templates + self.all_template_folders)
|
2018-11-20 12:18:18 +00:00
|
|
|
|
2018-11-08 14:46:18 +00:00
|
|
|
def move_to_folder(self, ids_to_move, move_to):
|
|
|
|
|
ids_to_move = set(ids_to_move)
|
|
|
|
|
|
|
|
|
|
template_folder_api_client.move_to_folder(
|
|
|
|
|
service_id=self.id,
|
|
|
|
|
folder_id=move_to,
|
|
|
|
|
template_ids=ids_to_move & self.all_template_ids,
|
|
|
|
|
folder_ids=ids_to_move & self.all_template_folder_ids,
|
|
|
|
|
)
|
2018-11-07 11:35:24 +00:00
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
|
def api_keys(self):
|
2018-11-07 12:05:11 +00:00
|
|
|
return sorted(
|
2023-08-25 09:12:23 -07:00
|
|
|
api_key_api_client.get_api_keys(self.id)["apiKeys"],
|
|
|
|
|
key=lambda key: key["name"].lower(),
|
2018-11-07 12:05:11 +00:00
|
|
|
)
|
2018-11-07 11:35:24 +00:00
|
|
|
|
2018-11-07 11:53:29 +00:00
|
|
|
def get_api_key(self, id):
|
2018-11-14 10:16:55 +00:00
|
|
|
return self._get_by_id(self.api_keys, id)
|
2019-04-04 11:27:05 +01:00
|
|
|
|
2021-09-27 15:20:56 +01:00
|
|
|
|
|
|
|
|
class Services(SerialisedModelCollection):
|
|
|
|
|
model = Service
|