Add inventory search with haystack

Also give a coherent look to all inventory lists
This commit is contained in:
Guillaume Bertholon 2020-11-28 00:33:33 +01:00
parent 68dea4002c
commit 1850746975
19 changed files with 217 additions and 45 deletions

1
.gitignore vendored
View file

@ -63,6 +63,7 @@ venv
# Project specific # Project specific
db.sqlite3 db.sqlite3
whoosh_index/
public/ public/
# Vim recover files # Vim recover files

View file

@ -23,6 +23,15 @@ DATABASES = {
} }
} }
# Search engine
# https://django-haystack.readthedocs.io/en/latest/tutorial.html#configuration
HAYSTACK_CONNECTIONS = {
"default": {
"ENGINE": "haystack.backends.whoosh_backend.WhooshEngine",
"PATH": os.path.join(BASE_DIR, "whoosh_index"),
},
}
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/3.0/topics/i18n/ # https://docs.djangoproject.com/en/3.0/topics/i18n/

View file

@ -25,6 +25,7 @@ INSTALLED_APPS = [
"django.contrib.sessions", "django.contrib.sessions",
"django.contrib.messages", "django.contrib.messages",
"django.contrib.staticfiles", "django.contrib.staticfiles",
"haystack",
"mainsite", "mainsite",
"inventory", "inventory",
"django_cas_ng", "django_cas_ng",
@ -79,6 +80,9 @@ AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
] ]
# Update the search database on save
HAYSTACK_SIGNAL_PROCESSOR = "haystack.signals.RealtimeSignalProcessor"
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.0/howto/static-files/ # https://docs.djangoproject.com/en/3.0/howto/static-files/

View file

@ -0,0 +1,27 @@
from haystack import indexes
from .models import Category, Tag, Game
class CategoryIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, model_attr="name")
def get_model(self):
return Category
class TagIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, model_attr="name")
def get_model(self):
return Tag
class GameIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(
document=True,
use_template=True,
template_name="inventory/search_indexes/game.txt",
)
def get_model(self):
return Game

View file

@ -7,7 +7,7 @@
Il y a {{ game_list|length }} jeu{{ game_list|pluralize:"x" }} dans cette catégorie : Il y a {{ game_list|length }} jeu{{ game_list|pluralize:"x" }} dans cette catégorie :
<ul> <ul>
{% for game in game_list %} {% for game in game_list %}
<li><a href="{{ game.get_absolute_url }}">{{ game.title }}</a></li> {% include "./partials/game_item.html" %}
{% endfor %} {% endfor %}
</ul> </ul>
{% endwith %} {% endwith %}

View file

@ -1,12 +1,12 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block "content" %} {% block "content" %}
<h1><i class="fa fa-bookmark" aria-hidden="true"></i> Liste des catégories</h1> <h1>Liste des catégories</h1>
Il y a {{ category_list|length }} catégorie{{ category_list|pluralize }} de jeux&nbsp;: Il y a {{ category_list|length }} catégorie{{ category_list|pluralize }} de jeux&nbsp;:
<ul> <ul>
{% for category in category_list %} {% for category in category_list %}
<li><a href="{{ category.get_absolute_url }}">{{ category.name }}</a></li> <li><a href="{{ category.get_absolute_url }}"><i class="fa fa-bookmark" aria-hidden="true"></i> {{ category.name }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endblock %} {% endblock %}

View file

@ -6,7 +6,7 @@
Il y a {{ game_list|length }} jeu{{ game_list|pluralize:"x" }} en salle jeux&nbsp;: Il y a {{ game_list|length }} jeu{{ game_list|pluralize:"x" }} en salle jeux&nbsp;:
<ul> <ul>
{% for game in game_list %} {% for game in game_list %}
<li><a href="{{ game.get_absolute_url }}">{{game.title}}</a></li> {% include "./partials/game_item.html" %}
{% endfor %} {% endfor %}
</ul> </ul>
{% endblock %} {% endblock %}

View file

@ -3,7 +3,16 @@
{% block "content" %} {% block "content" %}
<h1>Inventaire du club Jeux</h1> <h1>Inventaire du club Jeux</h1>
Rechercher dans la ludothèque:
<form class="search" method="get" action="{% url "inventory:search" %}">
<input type="search" name="q" />
<button type="submit"><i class="fa fa-fw fa-search" aria-hidden="true"></i></button>
</form>
<hr/>
<div class="btn_row"> <div class="btn_row">
<a href="{% url "inventory:category_list" %}"> <a href="{% url "inventory:category_list" %}">
Liste des catégories Liste des catégories
<p class="helptext"> <p class="helptext">

View file

@ -0,0 +1,19 @@
<li>
<a href="{{ game.get_absolute_url }}">
{{game.title}}
<div class="details">
<p><i class="fa fa-fw fa-users" aria-hidden="true"></i> {{ game.player_range }}</p>
<p><i class="fa fa-fw fa-clock-o" aria-hidden="true"></i> {{ game.duration }}
<p><i class="fa fa-fw fa-bookmark"></i> {{ game.category }}</p>
{% for tag in game.tags.all %}
<p><i class="fa fa-fw fa-tag" aria-hidden="true"></i> {{ tag }}</p>
{% endfor %}
{% if game.game_designer %}
<p><i class="fa fa-fw fa-pencil" aria-hidden="true"></i> {{ game.game_designer }}
{% endif %}
{% if game.editor %}
<p><i class="fa fa-fw fa-cogs" aria-hidden="true"></i> {{ game.editor }}</p>
{% endif %}
</div>
</a>
</li>

View file

@ -0,0 +1,39 @@
{% extends "base.html" %}
{% block "content" %}
<h1>Recherche</h1>
<form class="search" method="get" action=".">
{{ form.q }}
<button type="submit"><i class="fa fa-fw fa-search" aria-hidden="true"></i></button>
</form>
{% if query %}
<hr/>
<ul>
{% for result in page_obj.object_list %}
{% if result.model_name == "game" %}
{% include "./partials/game_item.html" with game=result.object %}
{% elif result.model_name == "category" %}
<li>
<a href="{{ result.object.get_absolute_url }}"><i class="fa fa-bookmark" aria-hidden="true"></i> {{ result.object.name }}</a>
</li>
{% elif result.model_name == "tag" %}
<li>
<a href="{{ result.object.get_absolute_url }}"><i class="fa fa-tag" aria-hidden="true"></i> {{ result.object.name }}</a>
</li>
{% endif %}
{% empty %}
<li>Aucun résultat trouvé</li>
{% endfor %}
</ul>
{% if page_obj.has_previous or page_obj.has_next %}
<div>
{% if page_obj.has_previous %}<a href="?q={{ query }}&amp;page={{ page_obj.previous_page_number }}">{% endif %}&laquo; Précédent{% if page_obj.has_previous %}</a>{% endif %}
|
{% if page_obj.has_next %}<a href="?q={{ query }}&amp;page={{ page_obj.next_page_number }}">{% endif %}Suivant &raquo;{% if page_obj.has_next %}</a>{% endif %}
</div>
{% endif %}
{% endif %}
{% endblock %}

View file

@ -0,0 +1,4 @@
{{ object.title }}
{{ object.editor }}
{{ object.game_designer }}
{{ object.description }}

View file

@ -7,7 +7,7 @@
Il y a {{ game_list|length }} jeu{{ game_list|pluralize:"x" }} marqué{{ game_list|pluralize }} avec ce tag&nbsp;: Il y a {{ game_list|length }} jeu{{ game_list|pluralize:"x" }} marqué{{ game_list|pluralize }} avec ce tag&nbsp;:
<ul> <ul>
{% for game in game_list %} {% for game in game_list %}
<li><a href="{{ game.get_absolute_url }}">{{ game.title }}</a></li> {% include "./partials/game_item.html" %}
{% endfor %} {% endfor %}
</ul> </ul>
{% endwith %} {% endwith %}

View file

@ -1,12 +1,12 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block "content" %} {% block "content" %}
<h1><i class="fa fa-tags" aria-hidden="true"></i> Liste des tags</h1> <h1>Liste des tags</h1>
Il y a {{ tag_list|length }} tag{{ tag_list|pluralize }} dans la ludothèque&nbsp;: Il y a {{ tag_list|length }} tag{{ tag_list|pluralize }} dans la ludothèque&nbsp;:
<ul> <ul>
{% for tag in tag_list %} {% for tag in tag_list %}
<li><a href="{{ tag.get_absolute_url }}">{{ tag.name }}</a></li> <li><a href="{{ tag.get_absolute_url }}"><i class="fa fa-tag" aria-hidden="true"></i> {{ tag.name }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endblock %} {% endblock %}

View file

@ -7,6 +7,7 @@ from .views import (
TagView, TagView,
GameListView, GameListView,
GameView, GameView,
InventorySearchView,
) )
app_name = "inventory" app_name = "inventory"
@ -19,4 +20,5 @@ urlpatterns = [
path("tag/<slug>/", TagView.as_view(), name="tag"), path("tag/<slug>/", TagView.as_view(), name="tag"),
path("game/", GameListView.as_view(), name="game_list"), path("game/", GameListView.as_view(), name="game_list"),
path("game/<slug>/", GameView.as_view(), name="game"), path("game/<slug>/", GameView.as_view(), name="game"),
path("search/", InventorySearchView.as_view(), name="search"),
] ]

View file

@ -1,4 +1,7 @@
from django.views.generic import TemplateView, ListView, DetailView from django.views.generic import TemplateView, ListView, DetailView
from haystack.generic_views import SearchView
from haystack.forms import SearchForm
from haystack.query import SearchQuerySet
from .models import Category, Tag, Game from .models import Category, Tag, Game
@ -34,3 +37,11 @@ class GameListView(ListView):
class GameView(DetailView): class GameView(DetailView):
model = Game model = Game
template_name = "inventory/game.html" template_name = "inventory/game.html"
class InventorySearchView(SearchView):
form_class = SearchForm
template_name = "inventory/search.html"
def get_queryset(self):
return SearchQuerySet().models(Category, Tag, Game)

View file

@ -97,20 +97,11 @@ select {
background-color: rgba($error_box_color, .4); background-color: rgba($error_box_color, .4);
} }
.checkbox_input { form.search {
display: flex; flex-direction: row;
justify-content: space-evenly; gap: 10px;
align-items: center;
.label_line { button {
order: 1; margin: 0;
flex: 1 1 500px;
}
input {
flex: 0 1 50px;
} }
} }
.fieldgroup {
margin: 15px 0;
}

View file

@ -44,7 +44,7 @@ main {
text-align: justify; text-align: justify;
&.small_page { &.small_page {
width: $small_page_width; width: $small_page_width;
} }
} }
@ -136,13 +136,13 @@ button, .btn_row a {
border-radius: 3px; border-radius: 3px;
.tooltiptext { .tooltiptext {
visibility: hidden; visibility: hidden;
display: block; display: block;
background-color: black; background-color: black;
color: rgba(white, 0.80); color: rgba(white, 0.80);
text-align: justify; text-align: justify;
padding: 10px; padding: 10px;
border-radius: 6px; border-radius: 6px;
font-size: 0.8em; font-size: 0.8em;
width: 250px; width: 250px;
@ -152,10 +152,10 @@ button, .btn_row a {
left: -75px; left: -75px;
} }
/* Position the tooltip text - see examples below! */ /* Position the tooltip text - see examples below! */
position: absolute; position: absolute;
left: -75px; left: -75px;
z-index: 1; z-index: 1;
ul { ul {
margin: 0; margin: 0;
@ -276,3 +276,38 @@ iframe {
} }
} }
ul {
padding: 0 20px;
list-style-type: none;
li>a {
display: block;
text-decoration: none;
padding: 15px;
margin: 10px 5px;
border-radius: 10px;
border: 1px solid transparent;
&:hover {
border-color: $header_border_color;
background-color: lighten($header_border_color, 70%);
}
&:focus {
border-color: $header_border_color;
background-color: lighten($header_border_color, 70%);
box-shadow: 0 0 1.5px 1px $header_bg_color;
}
.details {
font-size: 0.7em;
display: flex;
flex-wrap: wrap;
margin: 5px;
gap: 5px 20px;
p {
margin: 0;
}
}
}
}

View file

@ -151,18 +151,11 @@ select {
border-radius: 10px; border-radius: 10px;
background-color: rgba(255, 205, 221, 0.4); } background-color: rgba(255, 205, 221, 0.4); }
.checkbox_input { form.search {
display: flex; flex-direction: row;
justify-content: space-evenly; gap: 10px; }
align-items: center; } form.search button {
.checkbox_input .label_line { margin: 0; }
order: 1;
flex: 1 1 500px; }
.checkbox_input input {
flex: 0 1 50px; }
.fieldgroup {
margin: 15px 0; }
html { html {
box-sizing: border-box; } box-sizing: border-box; }
@ -413,3 +406,29 @@ iframe {
margin: 1ex; } margin: 1ex; }
#game_infos #details hr { #game_infos #details hr {
margin: 1ex; } margin: 1ex; }
ul {
padding: 0 20px;
list-style-type: none; }
ul li > a {
display: block;
text-decoration: none;
padding: 15px;
margin: 10px 5px;
border-radius: 10px;
border: 1px solid transparent; }
ul li > a:hover {
border-color: #51808c;
background-color: white; }
ul li > a:focus {
border-color: #51808c;
background-color: white;
box-shadow: 0 0 1.5px 1px #6bb8c4; }
ul li > a .details {
font-size: 0.7em;
display: flex;
flex-wrap: wrap;
margin: 5px;
gap: 5px 20px; }
ul li > a .details p {
margin: 0; }

View file

@ -2,3 +2,5 @@ Django
django-autoslug django-autoslug
Pillow Pillow
django-cas-ng django-cas-ng
django-haystack
Whoosh