Files
notifications-admin/app/models/template_list.py
Ben Thorner 1c4a2b4790 Cache templates and folders in TemplateList
This gives us the performance gains identified in [^1] for the test
service described in the spike:

- user_template_folders - from 10s to a little above 3s on its own

- get_templates_and_folders - from 10s to below 6s on its own

In combination, these two uses of caching reduce the test page load
time from 10s to a little above 3s. This is slightly higher than in
the spike PR due to all the extra work we're doing to generate the
"move to" list of folders, as described in a previous commit.

The render time is unchanged for services with few folders. We start
to see the benefit of this change at around 200 templates + folders,
with no evidence that any service will experience worse render times,
despite the extra work we're doing from previous commits.

[^1]: https://github.com/alphagov/notifications-admin/pull/4251
2022-06-07 11:05:36 +01:00

308 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.folders = folders
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,
)