Files
notifications-admin/app/models/template_list.py
Ben Thorner 5db2581669 Simplify tests for TemplateList class
In response to: [^1][^2][^3].

Originally there were three tests:

- One was testing the "get_user_template_folders" method directly.

- The other two were testing "get_template_folders", which calls
the "_user_" method if a user is passed - this is what the tests
were primarily checking, by passing or not passing a User object.

The two tests of "get_template_folders" also implicitly checked
that it filtered folders based on a "parent_folder_id". I wanted
to emulate the tests but it makes more sense to simplify them, as
the methods are now only used by TemplateList internally.

To simplify, we just keep just two tests to check that the overall
set of folders is filtered based on the presence of a User. We do
lose the implicit check of filtering by "parent_folder_id", but:

- We are still checking this implicitly in the sense that the loop
to generate the overall set of folders terminates. If the method
didn't do any filtering, the loop would be infinite.

- We need to acknowledge that these tests were incomplete to begin
with. There is also lots of coverage in higher level tests [^4],
although it's harder to reason exactly what is covered.

[^1]: https://github.com/alphagov/notifications-admin/pull/4258#discussion_r890144076
[^2]: https://github.com/alphagov/notifications-admin/pull/4258#discussion_r890151220
[^3]: https://github.com/alphagov/notifications-admin/pull/4258#discussion_r890147506
[^4]: 1787f9f42f/tests/app/main/views/test_template_folders.py
2022-06-07 11:31:09 +01:00

307 lines
9.2 KiB
Python

from werkzeug.utils import cached_property
from app import format_notification_type
class TemplateList():
def __init__(
self,
service,
template_type='all',
template_folder_id=None,
user=None,
):
self.service = service
self.template_type = template_type
self.template_folder_id = template_folder_id
self.user = user
def __iter__(self):
yield from self.items
@cached_property
def items(self):
return list(self.get_templates_and_folders(
self.template_type, self.template_folder_id, ancestors=[]
))
def get_templates_and_folders(self, template_type, template_folder_id, ancestors):
for item in self.get_template_folders(
template_type, template_folder_id,
):
yield TemplateListFolder(
item,
folders=self.get_template_folders(
template_type, item['id'],
),
templates=self.get_templates(
template_type, item['id']
),
ancestors=ancestors,
service_id=self.service.id,
)
for sub_item in self.get_templates_and_folders(
template_type, item['id'], ancestors + [item]
):
yield sub_item
for item in self.get_templates(
template_type, template_folder_id,
):
yield TemplateListTemplate(
item,
ancestors=ancestors,
service_id=self.service.id,
)
def get_templates(self, template_type='all', template_folder_id=None):
if self.user and template_folder_id:
folder = self.service.get_template_folder(template_folder_id)
if not self.user.has_template_folder_permission(folder):
return []
if isinstance(template_type, str):
template_type = [template_type]
if template_folder_id:
template_folder_id = str(template_folder_id)
return [
template for template in self.service.all_templates
if (set(template_type) & {'all', template['template_type']})
and template.get('folder') == template_folder_id
]
@cached_property
def user_template_folders(self):
"""Returns a modified list of folders a user has permission to view
For each folder, we do the following:
- if user has no permission to view the folder, skip it
- if folder is visible and its parent is visible, we add it to the list of folders
we later return without modifying anything
- if folder is visible, but the parent is not, we iterate through the parent until we
either find a visible parent or reach root folder. On each iteration we concatenate
invisible parent folder name to the front of our folder name, modifying the name, and we
change parent_folder_id attribute to a higher level parent. This flattens the path to the
folder making sure it displays in the closest visible parent.
"""
user_folders = []
for folder in self.service.all_template_folders:
if not self.user.has_template_folder_permission(folder, service=self.service):
continue
parent = self.service.get_template_folder(folder["parent_id"])
if self.user.has_template_folder_permission(parent, service=self.service):
user_folders.append(folder)
else:
folder_attrs = {
"id": folder["id"], "name": folder["name"], "parent_id": folder["parent_id"],
"users_with_permission": folder["users_with_permission"]
}
while folder_attrs["parent_id"] is not None:
folder_attrs["name"] = [
parent["name"],
folder_attrs["name"],
]
if parent["parent_id"] is None:
folder_attrs["parent_id"] = None
else:
parent = self.service.get_template_folder(parent["parent_id"])
folder_attrs["parent_id"] = parent.get("id", None)
if self.user.has_template_folder_permission(parent, service=self.service):
break
user_folders.append(folder_attrs)
return user_folders
def get_template_folders(self, template_type='all', parent_folder_id=None):
if self.user:
folders = self.user_template_folders
else:
folders = self.service.all_template_folders
if parent_folder_id:
parent_folder_id = str(parent_folder_id)
return [
folder for folder in folders
if (
folder['parent_id'] == parent_folder_id
and self.is_folder_visible(folder['id'], template_type)
)
]
def is_folder_visible(self, template_folder_id, template_type='all'):
if template_type == 'all':
return True
if self.get_templates(template_type, template_folder_id):
return True
if any(
self.is_folder_visible(child_folder['id'], template_type)
for child_folder in self.get_template_folders(template_type, template_folder_id)
):
return True
return False
@property
def as_id_and_name(self):
return [(item.id, item.name) for item in self]
@property
def templates_to_show(self):
return any(self)
@property
def folder_is_empty(self):
return not any(self.get_templates_and_folders(
'all', self.template_folder_id, []
))
class ServiceTemplateList(TemplateList):
def __iter__(self):
template_list_service = TemplateListService(
self.service,
templates=self.get_templates(
template_folder_id=None,
),
folders=self.get_template_folders(
parent_folder_id=None,
),
)
yield template_list_service
yield from self.get_templates_and_folders(
self.template_type,
self.template_folder_id,
ancestors=[template_list_service]
)
class TemplateLists():
def __init__(self, user):
self.services = sorted(
user.services,
key=lambda service: service.name.lower(),
)
self.user = user
def __iter__(self):
if len(self.services) == 1:
yield from TemplateList(
service=self.services[0],
user=self.user,
)
return
for service in self.services:
yield from ServiceTemplateList(
service=service,
user=self.user,
)
@property
def templates_to_show(self):
return bool(self.services)
class TemplateListItem():
is_service = False
def __init__(
self,
template_or_folder,
ancestors,
):
self.id = template_or_folder['id']
self.name = template_or_folder['name']
self.ancestors = ancestors
class TemplateListTemplate(TemplateListItem):
is_folder = False
def __init__(
self,
template,
ancestors,
service_id,
):
super().__init__(template, ancestors)
self.service_id = service_id
self.template_type = template['template_type']
self.content = template.get('content')
@property
def hint(self):
if self.template_type == 'broadcast':
max_length_in_chars = 40
if len(self.content) > (max_length_in_chars + 2):
return self.content[:max_length_in_chars].strip() + ''
return self.content
return format_notification_type(self.template_type) + ' template'
class TemplateListFolder(TemplateListItem):
is_folder = True
def __init__(
self,
folder,
templates,
folders,
ancestors,
service_id,
):
super().__init__(folder, ancestors)
self.folder = folder
self.service_id = service_id
self.number_of_templates = len(templates)
self.number_of_folders = len(folders)
@property
def _hint_parts(self):
if self.number_of_folders == self.number_of_templates == 0:
yield 'Empty'
if self.number_of_templates == 1:
yield '1 template'
elif self.number_of_templates > 1:
yield '{} templates'.format(self.number_of_templates)
if self.number_of_folders == 1:
yield '1 folder'
elif self.number_of_folders > 1:
yield '{} folders'.format(self.number_of_folders)
@property
def hint(self):
return ', '.join(self._hint_parts)
class TemplateListService(TemplateListFolder):
is_service = True
def __init__(
self,
service,
templates,
folders,
):
super().__init__(
folder=service._dict,
templates=templates,
folders=folders,
ancestors=[],
service_id=service.id,
)