diff --git a/springboard/config.py b/springboard/config.py index 08ba3ef..bd62af6 100644 --- a/springboard/config.py +++ b/springboard/config.py @@ -4,6 +4,7 @@ def includeme(config): config.add_route('home', '/') config.add_route('category', '/category/{uuid}/') config.add_route('page', '/page/{uuid}/') + config.add_route('search', '/search/') config.add_route('locale', '/locale/') config.add_route('locale_change', '/locale/change/') config.add_route('locale_matched', '/locale/{language}/') diff --git a/springboard/defaults.ini b/springboard/defaults.ini index 53de605..0a8a9f3 100644 --- a/springboard/defaults.ini +++ b/springboard/defaults.ini @@ -21,6 +21,7 @@ jinja2.filters = display_language_name = springboard.filters:display_language_name_filter language_direction = springboard.filters:language_direction_filter paginate = springboard.filters:paginate_filter + get_category_title = springboard.filters:get_category_title_filter unicorehub.host = unicorehub.app_id = diff --git a/springboard/filters.py b/springboard/filters.py index 0aed351..11bd6f6 100644 --- a/springboard/filters.py +++ b/springboard/filters.py @@ -59,3 +59,10 @@ def language_direction_filter(locale): def paginate_filter(results, page, results_per_page=10, slider_value=5): return Paginator(results, page, results_per_page, slider_value) + + +@contextfilter +def get_category_title_filter(ctx, primary_category_uuid, all_categories): + for category in all_categories: + if primary_category_uuid == category.uuid: + return category.title diff --git a/springboard/templates/atoms.html b/springboard/templates/atoms.html new file mode 100644 index 0000000..b9f60a2 --- /dev/null +++ b/springboard/templates/atoms.html @@ -0,0 +1,3 @@ +{% macro image(src, srcset, alt) -%} + {{alt}} +{%- endmacro %} diff --git a/springboard/templates/base.jinja2 b/springboard/templates/base.jinja2 index ebeaf17..4d700db 100644 --- a/springboard/templates/base.jinja2 +++ b/springboard/templates/base.jinja2 @@ -1,3 +1,5 @@ +{% import 'molecules.html' as molecules %} + + {% block search %} + {{molecules.search_box(view)}} + {% endblock %} + diff --git a/springboard/templates/molecules.html b/springboard/templates/molecules.html new file mode 100644 index 0000000..9112990 --- /dev/null +++ b/springboard/templates/molecules.html @@ -0,0 +1,123 @@ +{% import 'atoms.html' as atoms %} + +{% macro list_of_languages(view, languages, styling_classes) -%} + +{%- endmacro %} + +{% macro logo(view, src, styling_classes) -%} + +{%- endmacro %} + +{% macro banner_with_image(src) -%} + +{%- endmacro %} + + +{% macro list_of_site_links(view, dict, styling_classes) -%} + +{%- endmacro %} + +{% macro image_and_heading(view, cat, styling_classes) -%} +
+{{atoms.image( view.get_image_url(cat.image_host, cat.image, 45, 45), "" , "" ) }} +

{{cat.title}}

+
+{%- endmacro %} + +{% macro footer(message) -%} + +{%- endmacro %} + +{% macro search_box(view) -%} +
+ + +
+{%- endmacro %} + +{% macro page_number_navigator(view, paginator, p, query) -%} + + {% if paginator.has_previous_page() %} + < Previous + {% endif %} + + {% if paginator.show_start() %} + 1 + {% endif %} + + {% if paginator.needs_start_ellipsis() %} + ... + {% endif %} + + {% if paginator.page_numbers_left() %} + {% for number in paginator.page_numbers_left() %} + {{number + 1}} + {% endfor %} + {% endif %} + + {{p + 1}} + + {% if paginator.page_numbers_right() %} + {% for number in paginator.page_numbers_right() %} + {{number + 1}} + {% endfor %} + {% endif %} + + {% if paginator.needs_end_ellipsis() %} + ... + {% endif %} + + {% if paginator.show_end() %} + {{paginator.total_pages()}} + {% endif %} + + {% if paginator.has_next_page() %} + Next > + {% endif %} + +{%- endmacro %} + +{% macro message_for_no_results(query) -%} + {% if query %} +

No results found for {{query}}

+ {% else %} +

No results found!

+ {% endif %} +{%- endmacro %} + +{% macro search_summary(num_of_results, query) -%} + {% if num_of_results==1 %} +

1 search result for: {{query}}

+ {% elif num_of_results > 1 %} +

{{num_of_results}} search results for: {{query}}

+ {% endif %} +{%- endmacro %} + +{% macro article_summary(result, all_categories) -%} +{% if result.primary_category %} +

{{result.primary_category|get_category_title(all_categories) }}

+{% endif %} + {{result.title}} ... Read More > +{%- endmacro %} diff --git a/springboard/templates/organisms.html b/springboard/templates/organisms.html new file mode 100644 index 0000000..e5d3ab2 --- /dev/null +++ b/springboard/templates/organisms.html @@ -0,0 +1,12 @@ +{% import 'molecules.html' as molecules %} +{% import 'atoms.html' as atoms %} + +{% macro list_of_returned_articles(paginator, all_categories) -%} + +{%- endmacro %} diff --git a/springboard/templates/search_results.jinja2 b/springboard/templates/search_results.jinja2 new file mode 100644 index 0000000..c84b04f --- /dev/null +++ b/springboard/templates/search_results.jinja2 @@ -0,0 +1,23 @@ +{% extends "base.jinja2" %} +{% import 'atoms.html' as atoms %} +{% import 'molecules.html' as molecules %} +{% import 'organisms.html' as organisms %} + +{% block content %} +
+ +{% if paginator %} + + {{molecules.search_summary(paginator.total_count(), query)}} + + {% if paginator.total_pages() > 1 %} + {{molecules.page_number_navigator(view, paginator, p, query)}} + {% endif %} + + {{organisms.list_of_returned_articles(paginator, all_categories)}} +{% else %} + {{molecules.message_for_no_results(query)}} +{% endif %} + +
+{% endblock %} diff --git a/springboard/tests/test_search.py b/springboard/tests/test_search.py new file mode 100644 index 0000000..d2a8bcb --- /dev/null +++ b/springboard/tests/test_search.py @@ -0,0 +1,115 @@ +from springboard.tests import SpringboardTestCase + + +from pyramid import testing +from unicore.content.models import Page + + +class TestSearch(SpringboardTestCase): + + def setUp(self): + self.workspace = self.mk_workspace() + settings = { + 'unicore.repos_dir': self.working_dir, + 'unicore.content_repo_urls': self.workspace.working_dir, + 'available_languages': '\n'.join([ + 'eng_GB', + 'swa_KE', + 'spa_ES', + ]), + 'featured_languages': '\n'.join([ + 'spa_ES', + 'eng_GB', + ]) + } + self.config = testing.setUp(settings=settings) + self.app = self.mk_app(self.workspace, settings=settings) + + def tearDown(self): + testing.tearDown() + + def test_search_no_results(self): + self.app = self.mk_app(self.workspace) + + resp = self.app.get('/search/', params={'q': ''}, status=200) + self.assertTrue('No results found' in resp.body) + + def test_search_blank(self): + self.app = self.mk_app(self.workspace) + self.mk_pages(self.workspace) + + resp = self.app.get('/search/', params={'q': None}, status=200) + self.assertTrue('No results found' in resp.body) + + def test_search_2_results(self): + self.app = self.mk_app(self.workspace) + self.mk_pages(self.workspace, count=2) + resp = self.app.get('/search/', params={'q': 'sample'}, status=200) + + self.assertFalse('No results found' in resp.body) + self.assertTrue('Test Page 0' in resp.body) + self.assertTrue('Test Page 1' in resp.body) + + def test_search_multiple_results(self): + self.app = self.mk_app(self.workspace) + self.mk_pages(self.workspace, count=11) + resp = self.app.get('/search/', params={'q': 'sample'}, status=200) + self.assertTrue( + 'Next >' + in resp.body) + + def test_search_profanity(self): + self.app = self.mk_app(self.workspace) + self.mk_pages(self.workspace, count=2) + + resp = self.app.get('/search/', params={'q': 'kak'}, status=200) + + self.assertTrue('No results found' in resp.body) + + def test_search_added_page(self): + self.app = self.mk_app(self.workspace) + mother_page = Page({ + 'title': 'title for mother', 'language': 'eng_GB', 'position': 2, + 'content': 'Page for mother test page'}) + self.workspace.save(mother_page, 'Add mother page') + + self.workspace.refresh_index() + + resp = self.app.get('/search/', params={'q': 'mother'}, status=200) + + self.assertTrue('mother' in resp.body) + self.assertFalse('No results found' in resp.body) + + def test_pagination(self): + self.app = self.mk_app(self.workspace) + self.mk_pages(self.workspace, count=15, content='baby') + resp = self.app.get( + '/search/', params={'q': 'baby', 'p': '0'}, status=200) + self.assertFalse('Previous' in resp.body) + self.assertTrue('Next' in resp.body) + + def test_search_language_filter(self): + [category_eng] = self.mk_categories( + self.workspace, count=1, language='eng_GB', + title='English Category') + self.mk_pages( + self.workspace, count=1, language='eng_GB', + primary_category=category_eng.uuid, + content='Page for mother test page') + [category_spa] = self.mk_categories( + self.workspace, count=1, language='spa_ES', + title='Spanish Category') + self.mk_pages( + self.workspace, count=1, language='spa_ES', + primary_category=category_spa.uuid, + content='Page for mother test page') + + self.app.get('/locale/?language=eng_GB', status=302) + resp = self.app.get('/search/', params={'q': 'mother'}, status=200) + self.assertTrue('English Category' in resp.body) + self.assertFalse('Spanish Category' in resp.body) + + self.app.get('/locale/?language=spa_ES', status=302) + resp = self.app.get('/search/', params={'q': 'mother'}, status=200) + self.assertTrue('Spanish Category' in resp.body) + self.assertFalse('English Category' in resp.body) diff --git a/springboard/views/base.py b/springboard/views/base.py index 11cb1b7..9c042a1 100644 --- a/springboard/views/base.py +++ b/springboard/views/base.py @@ -40,7 +40,6 @@ def __init__(self, request): self.all_pages = SM(Page, **search_config).es(**self.es_settings) self.all_localisations = SM(Localisation, **search_config).es( **self.es_settings) - self.available_languages = config_list( self.settings.get('available_languages', '')) self.featured_languages = config_list( diff --git a/springboard/views/core.py b/springboard/views/core.py index dd2e4a5..f45fd93 100644 --- a/springboard/views/core.py +++ b/springboard/views/core.py @@ -5,7 +5,7 @@ from pyramid.httpexceptions import HTTPFound from pyramid.response import Response -from springboard.utils import ga_context +from springboard.utils import ga_context, Paginator from springboard.views.base import SpringboardViews from unicore.distribute.tasks import fastforward @@ -29,6 +29,50 @@ def category(self): [category] = self.all_categories.filter(uuid=uuid) return self.context(category=category) + @view_config(route_name='search', + renderer='springboard:templates/search_results.jinja2') + def search(self): + + query = self.request.GET.get('q') + p = int(self.request.GET.get('p', 0)) + + empty_defaults = self.context( + paginator=[], + query=query, + p=p, + ) + + # handle query exception + if not query: + return empty_defaults + + all_results = self.all_pages.query( + content__query_string=query).filter(language=self.language) + + # no results found + if all_results.count() == 0: + return empty_defaults + + paginator = Paginator(all_results, p) + + # requested page number is out of range + total_pages = paginator.total_pages() + # sets the floor to 0 + p = p if p >= 0 else 0 + # sets the roof to `total_pages -1` + p = p if p < total_pages else total_pages - 1 + paginator = Paginator(all_results, p) + + relevant_categories = self.all_categories.query().filter( + language=self.language) + + return self.context( + relevant_categories=relevant_categories, + paginator=paginator, + query=query, + p=p, + ) + @ga_context(lambda context: {'dt': context['page'].title, }) @view_config(route_name='page', renderer='springboard:templates/page.jinja2')