Add inventory search with haystack
Also give a coherent look to all inventory lists
This commit is contained in:
parent
68dea4002c
commit
1850746975
19 changed files with 217 additions and 45 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -63,6 +63,7 @@ venv
|
||||||
|
|
||||||
# Project specific
|
# Project specific
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
|
whoosh_index/
|
||||||
public/
|
public/
|
||||||
|
|
||||||
# Vim recover files
|
# Vim recover files
|
||||||
|
|
|
@ -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/
|
||||||
|
|
|
@ -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/
|
||||||
|
|
||||||
|
|
27
inventory/search_indexes.py
Normal file
27
inventory/search_indexes.py
Normal 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
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 :
|
Il y a {{ category_list|length }} catégorie{{ category_list|pluralize }} de jeux :
|
||||||
<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 %}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
Il y a {{ game_list|length }} jeu{{ game_list|pluralize:"x" }} en salle jeux :
|
Il y a {{ game_list|length }} jeu{{ game_list|pluralize:"x" }} en salle jeux :
|
||||||
<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 %}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
19
inventory/templates/inventory/partials/game_item.html
Normal file
19
inventory/templates/inventory/partials/game_item.html
Normal 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>
|
39
inventory/templates/inventory/search.html
Normal file
39
inventory/templates/inventory/search.html
Normal 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 }}&page={{ page_obj.previous_page_number }}">{% endif %}« Précédent{% if page_obj.has_previous %}</a>{% endif %}
|
||||||
|
|
|
||||||
|
{% if page_obj.has_next %}<a href="?q={{ query }}&page={{ page_obj.next_page_number }}">{% endif %}Suivant »{% if page_obj.has_next %}</a>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
4
inventory/templates/inventory/search_indexes/game.txt
Normal file
4
inventory/templates/inventory/search_indexes/game.txt
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{{ object.title }}
|
||||||
|
{{ object.editor }}
|
||||||
|
{{ object.game_designer }}
|
||||||
|
{{ object.description }}
|
|
@ -7,7 +7,7 @@
|
||||||
Il y a {{ game_list|length }} jeu{{ game_list|pluralize:"x" }} marqué{{ game_list|pluralize }} avec ce tag :
|
Il y a {{ game_list|length }} jeu{{ game_list|pluralize:"x" }} marqué{{ game_list|pluralize }} avec ce tag :
|
||||||
<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 %}
|
||||||
|
|
|
@ -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 :
|
Il y a {{ tag_list|length }} tag{{ tag_list|pluralize }} dans la ludothèque :
|
||||||
<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 %}
|
||||||
|
|
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -242,7 +242,7 @@ iframe {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
@media (max-width: 500px) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
@ -261,7 +261,7 @@ iframe {
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
max-width: 50%;
|
max-width: 50%;
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
@media (max-width: 500px) {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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; }
|
||||||
|
|
|
@ -2,3 +2,5 @@ Django
|
||||||
django-autoslug
|
django-autoslug
|
||||||
Pillow
|
Pillow
|
||||||
django-cas-ng
|
django-cas-ng
|
||||||
|
django-haystack
|
||||||
|
Whoosh
|
||||||
|
|
Loading…
Reference in a new issue