diff --git a/app/models/__init__.py b/app/models/__init__.py
new file mode 100644
index 000000000..f9efbffeb
--- /dev/null
+++ b/app/models/__init__.py
@@ -0,0 +1,27 @@
+from flask import abort
+
+
+class JSONModel():
+
+ ALLOWED_PROPERTIES = set()
+
+ def __init__(self, _dict):
+ # in the case of a bad request _dict may be `None`
+ self._dict = _dict or {}
+
+ def __bool__(self):
+ return self._dict != {}
+
+ def __getattr__(self, attr):
+ if attr in self.ALLOWED_PROPERTIES:
+ return self._dict[attr]
+ raise AttributeError('`{}` is not a {} attribute'.format(
+ attr,
+ self.__class__.__name__.lower(),
+ ))
+
+ def _get_by_id(self, things, id):
+ try:
+ return next(thing for thing in things if thing['id'] == str(id))
+ except StopIteration:
+ abort(404)
diff --git a/app/models/organisation.py b/app/models/organisation.py
new file mode 100644
index 000000000..d3753a431
--- /dev/null
+++ b/app/models/organisation.py
@@ -0,0 +1,140 @@
+from flask import Markup, abort
+
+from app.models import JSONModel
+
+
+class Organisation(JSONModel):
+
+ ALLOWED_PROPERTIES = {
+ 'id',
+ 'name',
+ 'active',
+ 'crown',
+ 'organisation_type',
+ 'letter_branding_id',
+ 'email_branding_id',
+ 'agreement_signed',
+ 'agreement_signed_at',
+ 'agreement_signed_by_id',
+ 'agreement_signed_version',
+ 'domains',
+ }
+
+ def __init__(self, _dict):
+
+ super().__init__(_dict)
+
+ if self._dict == {}:
+ self.name, self.crown, self.agreement_signed = None, None, None
+
+ @property
+ def crown_status(self):
+ return self.crown
+
+ @property
+ def as_human_readable(self):
+ if 'dwp.' in ''.join(self.domains):
+ return 'DWP - Requires OED approval'
+ if self.agreement_signed:
+ return 'Yes, on behalf of {}'.format(self.name)
+ elif self.owner:
+ return '{} (organisation is {}, {})'.format(
+ {
+ False: 'No',
+ None: 'Can’t tell',
+ }.get(self.agreement_signed),
+ self.name,
+ {
+ True: 'a crown body',
+ False: 'a non-crown body',
+ None: 'crown status unknown',
+ }.get(self.crown_status),
+ )
+ else:
+ return 'Can’t tell (domain is {})'.format(self._domain)
+
+ @property
+ def as_info_for_branding_request(self):
+ return self.owner or 'Can’t tell (domain is {})'.format(self._domain)
+
+ @property
+ def as_jinja_template(self):
+ if self.crown_status is None:
+ return 'agreement-choose'
+ if self.agreement_signed:
+ return 'agreement-signed'
+ return 'agreement'
+
+ def as_terms_of_use_paragraph(self, **kwargs):
+ return Markup(self._as_terms_of_use_paragraph(**kwargs))
+
+ def _as_terms_of_use_paragraph(self, terms_link, download_link, support_link, signed_in):
+
+ if not signed_in:
+ return ((
+ '{} Sign in to download a copy '
+ 'or find out if one is already in place.'
+ ).format(self._acceptance_required, terms_link))
+
+ if self.agreement_signed is None:
+ return ((
+ '{} Download the agreement or '
+ 'contact us to find out if we already '
+ 'have one in place with your organisation.'
+ ).format(self._acceptance_required, download_link, support_link))
+
+ if self.agreement_signed is False:
+ return ((
+ '{} Download a copy.'
+ ).format(self._acceptance_required, download_link))
+
+ return (
+ 'Your organisation ({}) has already accepted the '
+ 'GOV.UK Notify data sharing and financial '
+ 'agreement.'.format(self.name)
+ )
+
+ def as_pricing_paragraph(self, **kwargs):
+ return Markup(self._as_pricing_paragraph(**kwargs))
+
+ def _as_pricing_paragraph(self, pricing_link, download_link, support_link, signed_in):
+
+ if not signed_in:
+ return ((
+ 'Sign in to download a copy or find '
+ 'out if one is already in place with your organisation.'
+ ).format(pricing_link))
+
+ if self.agreement_signed is None:
+ return ((
+ 'Download the agreement or '
+ 'contact us to find out if we already '
+ 'have one in place with your organisation.'
+ ).format(download_link, support_link))
+
+ return (
+ 'Download the agreement '
+ '({} {}).'.format(
+ download_link,
+ self.name,
+ {
+ True: 'has already accepted it',
+ False: 'hasn’t accepted it yet'
+ }.get(self.agreement_signed)
+ )
+ )
+
+ @property
+ def _acceptance_required(self):
+ return (
+ 'Your organisation {} must also accept our data sharing '
+ 'and financial agreement.'.format(
+ '({})'.format(self.name) if self.name else '',
+ )
+ )
+
+ @property
+ def crown_status_or_404(self):
+ if self.crown_status is None:
+ abort(404)
+ return self.crown_status
diff --git a/app/models/service.py b/app/models/service.py
index 64805f430..425fceb02 100644
--- a/app/models/service.py
+++ b/app/models/service.py
@@ -2,6 +2,8 @@ from flask import abort, current_app
from notifications_utils.field import Field
from werkzeug.utils import cached_property
+from app.models import JSONModel
+from app.models.organisation import Organisation
from app.notify_client.api_key_api_client import api_key_api_client
from app.notify_client.billing_api_client import billing_api_client
from app.notify_client.email_branding_client import email_branding_client
@@ -18,7 +20,7 @@ from app.notify_client.user_api_client import user_api_client
from app.utils import get_default_sms_sender
-class Service():
+class Service(JSONModel):
ALLOWED_PROPERTIES = {
'active',
@@ -50,25 +52,12 @@ class Service():
)
def __init__(self, _dict):
- # in the case of a bad request current service may be `None`
- self._dict = _dict or {}
+
+ super().__init__(_dict)
+
if 'permissions' not in self._dict:
self.permissions = {'email', 'sms', 'letter'}
- def __bool__(self):
- return self._dict != {}
-
- def __getattr__(self, attr):
- if attr in self.ALLOWED_PROPERTIES:
- return self._dict[attr]
- raise AttributeError('`{}` is not a service attribute'.format(attr))
-
- def _get_by_id(self, things, id):
- try:
- return next(thing for thing in things if thing['id'] == str(id))
- except StopIteration:
- abort(404)
-
def update(self, **kwargs):
return service_api_client.update_service(self.id, **kwargs)
@@ -402,8 +391,14 @@ class Service():
return None
@cached_property
+ def organisation(self):
+ return Organisation(
+ organisations_client.get_service_organisation(self.id)
+ )
+
+ @property
def organisation_name(self):
- return organisations_client.get_service_organisation(self.id).get('name', None)
+ return self.organisation.name
@cached_property
def inbound_number(self):
diff --git a/app/models/user.py b/app/models/user.py
index e54315ed5..e9ec02707 100644
--- a/app/models/user.py
+++ b/app/models/user.py
@@ -2,7 +2,10 @@ from itertools import chain
from flask import abort, request, session
from flask_login import AnonymousUserMixin, UserMixin
+from werkzeug.utils import cached_property
+from app.models.organisation import Organisation
+from app.notify_client.organisations_api_client import organisations_client
from app.utils import is_gov_user
roles = {
@@ -192,6 +195,16 @@ class User(UserMixin):
def is_locked(self):
return self.failed_login_count >= self.max_failed_login_count
+ @property
+ def email_domain(self):
+ return self.email_address.split('@')[-1]
+
+ @cached_property
+ def default_organisation(self):
+ return Organisation(
+ organisations_client.get_organisation_by_domain(self.email_domain)
+ )
+
def serialize(self):
dct = {
"id": self.id,
@@ -322,3 +335,7 @@ class AnonymousUser(AnonymousUserMixin):
# set the anonymous user so that if a new browser hits us we don't error http://stackoverflow.com/a/19275188
def logged_in_elsewhere(self):
return False
+
+ @property
+ def default_organisation(self):
+ return Organisation(None)
diff --git a/app/notify_client/organisations_api_client.py b/app/notify_client/organisations_api_client.py
index d1d082e82..ed54537cc 100644
--- a/app/notify_client/organisations_api_client.py
+++ b/app/notify_client/organisations_api_client.py
@@ -1,3 +1,5 @@
+from notifications_python_client.errors import HTTPError
+
from app.notify_client import NotifyAdminAPIClient, _attach_current_user, cache
@@ -9,6 +11,16 @@ class OrganisationsClient(NotifyAdminAPIClient):
def get_organisation(self, org_id):
return self.get(url='/organisations/{}'.format(org_id))
+ def get_organisation_by_domain(self, domain):
+ try:
+ return self.get(
+ url='/organisations/by-domain?domain={}'.format(domain),
+ )
+ except HTTPError as error:
+ if error.status_code == 404:
+ return None
+ raise error
+
def create_organisation(self, name):
data = {
"name": name