diff --git a/articles/templates/articles/article_list.html b/articles/templates/articles/article_list.html
index b489771..f7aae61 100644
--- a/articles/templates/articles/article_list.html
+++ b/articles/templates/articles/article_list.html
@@ -9,25 +9,28 @@
{% block content %}
- Blog posts
+ {% block main_title %}Blog posts{% endblock %}
+ {% block search_bar %}{% endblock %}
{% for article in articles %}
{% include "articles/snippets/datetime.html" %}
{{ article.title }}
{% empty %}
- No article here. Come back later 🙂
+ {% block empty_results %}
+ No article here. Come back later 🙂
+ {% endblock %}
{% endfor %}
diff --git a/articles/templates/articles/article_search.html b/articles/templates/articles/article_search.html
new file mode 100644
index 0000000..a15d992
--- /dev/null
+++ b/articles/templates/articles/article_search.html
@@ -0,0 +1,21 @@
+{% extends 'articles/article_list.html' %}
+
+{% block main_title %}Search{% if search_expression %} results{% endif %}{% endblock %}
+
+{% block search_bar %}
+
+ This search form is pretty basic and will search for articles matching
+ ALL of your search terms in either the content, the title or the keywords.
+ It returns exact case-insensitive matches.
+
+
+{% endblock %}
+
+{% block empty_results %}
+ {% if search_expression %}
+ No article found matching your criteria.
+ {% endif %}
+{% endblock %}
diff --git a/articles/templates/articles/snippets/navigation.html b/articles/templates/articles/snippets/navigation.html
index b56c455..22df82e 100644
--- a/articles/templates/articles/snippets/navigation.html
+++ b/articles/templates/articles/snippets/navigation.html
@@ -1,5 +1,6 @@
Home
+ Search
RSS
{% if user.is_authenticated %}
Write
diff --git a/articles/urls.py b/articles/urls.py
index 6e802a4..a14d21a 100644
--- a/articles/urls.py
+++ b/articles/urls.py
@@ -5,6 +5,7 @@ from articles.views import api, feeds, html
urlpatterns = [
path("", html.ArticlesListView.as_view(), name="articles-list"),
path("drafts/", html.DraftsListView.as_view(), name="drafts-list"),
+ path("search/", html.SearchArticlesListView.as_view(), name="search"),
path("feed/", feeds.CompleteFeed(), name="complete-feed"),
path("api/render//", api.render_article, name="api-render-article"),
path("/", html.ArticleDetailView.as_view(), name="article-detail"),
diff --git a/articles/views/html.py b/articles/views/html.py
index 2dfbb48..2ddb376 100644
--- a/articles/views/html.py
+++ b/articles/views/html.py
@@ -1,24 +1,47 @@
+import operator
+from functools import reduce
+from typing import Dict
+
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
-from django.db.models import F
+from django.db.models import F, Q
from django.views import generic
from articles.models import Article
class BaseArticleListView(generic.ListView):
+ model = Article
+ context_object_name = "articles"
paginate_by = 10
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["blog_title"] = settings.BLOG["title"]
context["blog_description"] = settings.BLOG["description"]
+ page_obj = context["page_obj"]
+ if page_obj.has_next():
+ querystring = self.build_querystring({"page": page_obj.next_page_number()})
+ context["next_page_querystring"] = querystring
+ if page_obj.has_previous():
+ querystring = self.build_querystring(
+ {"page": page_obj.previous_page_number()}
+ )
+ context["previous_page_querystring"] = querystring
return context
+ def get_additional_querystring_params(self) -> Dict[str, str]:
+ return dict()
+
+ def build_querystring(self, initial_queryparams: Dict[str, str]) -> str:
+ querystring = {
+ **initial_queryparams,
+ **self.get_additional_querystring_params(),
+ }
+ return "&".join(map(lambda item: f"{item[0]}={item[1]}", querystring.items()))
+
class ArticlesListView(BaseArticleListView):
- model = Article
- context_object_name = "articles"
queryset = Article.objects.filter(status=Article.PUBLISHED)
def get_context_data(self, **kwargs):
@@ -30,9 +53,39 @@ class ArticlesListView(BaseArticleListView):
return context
+class SearchArticlesListView(BaseArticleListView):
+ queryset = Article.objects.filter(status=Article.PUBLISHED)
+ template_name = "articles/article_search.html"
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context["search_expression"] = self.request.GET.get("s") or ""
+ return context
+
+ def get_queryset(self):
+ queryset = super().get_queryset()
+ search_expression = self.request.GET.get("s")
+ if not search_expression:
+ return queryset.none()
+ search_terms = search_expression.split()
+ return queryset.filter(
+ reduce(operator.and_, (Q(title__icontains=term) for term in search_terms))
+ | reduce(
+ operator.and_, (Q(content__icontains=term) for term in search_terms)
+ )
+ | reduce(
+ operator.and_, (Q(keywords__icontains=term) for term in search_terms)
+ )
+ )
+
+ def get_additional_querystring_params(self) -> Dict[str, str]:
+ search_expression = self.request.GET.get("s")
+ if search_expression:
+ return {"s": search_expression}
+ return {}
+
+
class DraftsListView(LoginRequiredMixin, BaseArticleListView):
- model = Article
- context_object_name = "articles"
queryset = Article.objects.filter(status=Article.DRAFT)
def get_context_data(self, **kwargs):