From 718f4407203ccbb253dd3879536d7d5a3781132f Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Thu, 4 Apr 2019 11:18:22 +0100 Subject: [PATCH] Get info about organisations from database table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is the first step of replacing the `domains.yml` file. In order to replicate the same functionality we get from the `domains.yml` file and its associated code this commit adds a `Organisation` model. This model copies a lot of methods from the `AgreementInfo` class which wrapped the `domains.yml` file. It factors out some stuff that would otherwise be duplicated between the `Organisation` and `Service` model, in such a way that could be reused for making other models in the future. This commit doesn’t change other parts of the code to make use of this new model yet – that will come in subsequent commits. --- app/models/__init__.py | 27 ++++ app/models/organisation.py | 140 ++++++++++++++++++ app/models/service.py | 31 ++-- app/models/user.py | 17 +++ app/notify_client/organisations_api_client.py | 12 ++ 5 files changed, 209 insertions(+), 18 deletions(-) create mode 100644 app/models/__init__.py create mode 100644 app/models/organisation.py 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