diff --git a/pillar/web/nodes/finders.py b/pillar/web/nodes/finders.py index 45ac08e5..b1a72dd6 100644 --- a/pillar/web/nodes/finders.py +++ b/pillar/web/nodes/finders.py @@ -94,6 +94,16 @@ def find_for_post(project, node): url=node.properties.url) +@register_node_finder('page') +def find_for_page(project, node): + """Returns the URL for a page.""" + + project_id = project['_id'] + + the_project = project_url(project_id, project=project) + return url_for('projects.view_node', project_url=the_project.url, node_id=node.properties.url) + + def find_for_other(project, node): """Fallback: Assets, textures, and other node types. diff --git a/pillar/web/projects/routes.py b/pillar/web/projects/routes.py index b66965b1..eb2cbaf8 100644 --- a/pillar/web/projects/routes.py +++ b/pillar/web/projects/routes.py @@ -24,6 +24,7 @@ from pillar import current_app from pillar.api.utils import utcnow from pillar.web import system_util from pillar.web import utils +from pillar.web.nodes import finders from pillar.web.utils.jstree import jstree_get_children import pillar.extension @@ -302,6 +303,52 @@ def view(project_url): 'header_video_node': header_video_node}) +def project_navigation_links(project, api) -> list: + """Returns a list of nodes for the project, for top navigation display. + + Args: + project: A Project object. + api: the api client credential. + + Returns: + A list of links for the Project. + For example we display a link to the project blog if present, as well + as pages. The list is structured as follows: + + [{'url': '/p/spring/about', 'label': 'About'}, + {'url': '/p/spring/blog', 'label': 'Blog'}] + """ + + links = [] + + # Fetch the blog + blog = Node.find_first({ + 'where': {'project': project._id, 'node_type': 'blog', '_deleted': {'$ne': True}}, + 'projection': { + 'name': 1, + } + }, api=api) + + if blog: + links.append({'url': finders.find_url_for_node(blog), 'label': blog.name}) + + # Fetch pages + pages = Node.all({ + 'where': {'project': project._id, 'node_type': 'page', '_deleted': {'$ne': True}}, + 'projection': { + 'name': 1, + 'properties.url': 1 + } + }, api=api) + + # Process the results and append the links to the list + for p in pages._items: + + links.append({'url': finders.find_url_for_node(p), 'label': p.name}) + + return links + + def render_project(project, api, extra_context=None, template_name=None): project.picture_square = utils.get_file(project.picture_square, api=api) project.picture_header = utils.get_file(project.picture_header, api=api) @@ -370,13 +417,7 @@ def render_project(project, api, extra_context=None, template_name=None): extension_sidebar_links = current_app.extension_sidebar_links(project) - pages = Node.all({ - 'where': {'project': project._id, 'node_type': 'page', '_deleted': False}, - 'projection': { - 'name': 1, - 'url': 1 - } - }, api=api) + navigation_links = project_navigation_links(project, api) return render_template(template_name, api=api, @@ -386,7 +427,7 @@ def render_project(project, api, extra_context=None, template_name=None): show_project=True, og_picture=project.picture_header, activity_stream=activity_stream, - pages=pages._items, + navigation_links=navigation_links, extension_sidebar_links=extension_sidebar_links, **extra_context) @@ -456,16 +497,14 @@ def view_node(project_url, node_id): # Append _theatre to load the proper template theatre = '_theatre' if theatre_mode else '' + navigation_links = project_navigation_links(project, api) if node.node_type == 'page': - pages = Node.all({ - 'where': {'project': project._id, 'node_type': 'page'}, - 'projection': {'name': 1}}, api=api) return render_template('nodes/custom/page/view_embed.html', api=api, node=node, project=project, - pages=pages._items, + navigation_links=navigation_links, og_picture=og_picture,) extension_sidebar_links = current_app.extension_sidebar_links(project) @@ -477,6 +516,7 @@ def view_node(project_url, node_id): show_node=True, show_project=False, og_picture=og_picture, + navigation_links=navigation_links, extension_sidebar_links=extension_sidebar_links) diff --git a/tests/test_web/test_projects.py b/tests/test_web/test_projects.py new file mode 100644 index 00000000..a428c4f1 --- /dev/null +++ b/tests/test_web/test_projects.py @@ -0,0 +1,107 @@ +from bson import ObjectId + +from pillar.tests import AbstractPillarTest +from pillar.tests import common_test_data as ctd +from pillar.web import system_util +from pillar.web.projects.routes import project_navigation_links, find_project_or_404 + + +class ProjectTest(AbstractPillarTest): + def setUp(self, **kwargs): + super().setUp(**kwargs) + + self.pid, self.project = self.ensure_project_exists() + self.owner_uid = self.create_user(24 * 'a', + groups=[ctd.EXAMPLE_ADMIN_GROUP_ID], + token='admin-token') + + # Create a Node of type page. + self.node_id = self.create_node({ + '_id': ObjectId('572761099837730efe8e120d'), + 'description': 'This the about page', + 'node_type': 'page', + 'user': self.owner_uid, + 'properties': { + 'status': 'published', + 'url': 'about', + }, + 'name': 'About', + 'project': self.pid, + }) + + self.user_uid = self.create_user(24 * 'b', groups=[ctd.EXAMPLE_ADMIN_GROUP_ID], + token='user-token') + + def test_project_navigation_links_one_page(self): + """Test link generation for project navigation.""" + with self.app.test_request_context(): + api = system_util.pillar_api() + project = find_project_or_404(self.project['url'], api=api) + navigation_links = project_navigation_links(project, api=api) + + # We expect only one link to be in the list + links = [{'url': '/p/default-project/about', 'label': 'About'}] + self.assertListEqual(links, navigation_links) + + def test_project_navigation_links_pages_and_blog(self): + """Test link generation for a project with two Pages and one Blog.""" + # Add one more page to the project + self.create_node({ + '_id': ObjectId(), + 'description': 'This the awards page', + 'node_type': 'page', + 'user': self.owner_uid, + 'properties': { + 'status': 'published', + 'url': 'awards', + }, + 'name': 'Awards', + 'project': self.pid, + }) + # Create a Node of type blog. + self.create_node({ + '_id': ObjectId(), + 'description': 'This the blog page', + 'node_type': 'blog', + 'user': self.owner_uid, + 'properties': { + 'status': 'published', + }, + 'name': 'Blog', + 'project': self.pid, + }) + # Create a Node of type asset + self.create_node({ + '_id': ObjectId(), + 'description': 'This is an asset without file', + 'node_type': 'asset', + 'user': self.owner_uid, + 'properties': { + 'status': 'published', + 'content_type': 'image', + }, + 'name': 'Image test', + 'project': self.pid, + }) + + with self.app.test_request_context(): + api = system_util.pillar_api() + project = find_project_or_404(self.project['url'], api=api) + navigation_links = project_navigation_links(project, api=api) + expected_list = [ + {'url': '/blog/', 'label': 'Blog'}, # Blog is the first element of the list (since it's added first) + {'url': '/p/default-project/about', 'label': 'About'}, + {'url': '/p/default-project/awards', 'label': 'Awards'}] + + self.assertListEqual(expected_list, navigation_links) + + def test_project_no_navigation_links(self): + """Test link generation in a project with no Pages or Blog.""" + with self.app.test_request_context(): + # Delete the existing page from the database + self.app.db('nodes').delete_one({'_id': ObjectId('572761099837730efe8e120d')}) + api = system_util.pillar_api() + project = find_project_or_404(self.project['url'], api=api) + navigation_links = project_navigation_links(project, api=api) + # This should result in an empty list + self.assertListEqual([], navigation_links)