Merge pull request #19 from daniviga/more-filters

Add more filters
This commit is contained in:
2023-01-09 00:12:55 +01:00
committed by GitHub
12 changed files with 267 additions and 51 deletions

View File

@@ -1,4 +1,4 @@
from urllib.parse import quote_plus from urllib.parse import quote
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
@@ -37,7 +37,7 @@ class Manufacturer(models.Model):
return self.name return self.name
def safe_name(self): def safe_name(self):
return quote_plus(self.name, safe="&") return quote(self.__str__().lower(), safe="& ")
def logo_thumbnail(self): def logo_thumbnail(self):
return get_image_preview(self.logo.url) return get_image_preview(self.logo.url)
@@ -62,7 +62,7 @@ class Company(models.Model):
return self.name return self.name
def safe_name(self): def safe_name(self):
return quote_plus(self.name, safe="&") return quote(self.__str__().lower(), safe="& ")
def logo_thumbnail(self): def logo_thumbnail(self):
return get_image_preview(self.logo.url) return get_image_preview(self.logo.url)
@@ -104,6 +104,9 @@ class Scale(models.Model):
def __str__(self): def __str__(self):
return str(self.scale) return str(self.scale)
def safe_name(self):
return quote(self.__str__(), safe="& ")
class Tag(models.Model): class Tag(models.Model):
name = models.CharField(max_length=128, unique=True) name = models.CharField(max_length=128, unique=True)
@@ -112,9 +115,12 @@ class Tag(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
def safe_name(self):
return self.slug
@receiver(models.signals.pre_save, sender=Tag) @receiver(models.signals.pre_save, sender=Tag)
def tag_pre_save(sender, instance, **kwargs): def slug_pre_save(sender, instance, **kwargs):
instance.slug = slugify(instance.name) instance.slug = slugify(instance.name)
@@ -131,3 +137,6 @@ class RollingStockType(models.Model):
def __str__(self): def __str__(self):
return "{0} {1}".format(self.type, self.category) return "{0} {1}".format(self.type, self.category)
def safe_name(self):
return quote(self.__str__().lower(), safe="& ")

View File

@@ -81,8 +81,9 @@
Roster Roster
</a> </a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink"> <ul class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<li><a class="dropdown-item" href="{% url 'roster' %}">All roster</a></li> <li><a class="dropdown-item" href="{% url 'roster' %}">Rolling stock</a></li>
<li><a class="dropdown-item" href="{% url 'companies' %}">Companies</a></li> <li><a class="dropdown-item" href="{% url 'companies' %}">Companies</a></li>
<li><a class="dropdown-item" href="{% url 'types' %}">Types</a></li>
<li><a class="dropdown-item" href="{% url 'scales' %}">Scales</a></li> <li><a class="dropdown-item" href="{% url 'scales' %}">Scales</a></li>
</ul> </ul>
</li> </li>

View File

@@ -61,7 +61,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Scale</th> <th scope="row">Scale</th>
<td><a href="{% url 'filtered' _filter="scale" search=d.scale %}"><abbr title="{{ d.scale.ratio }} - {{ d.scale.tracks }}">{{ d.scale }}</abbr></a></td> <td><a href="{% url 'filtered' _filter="scale" search=d.scale.safe_name %}"><abbr title="{{ d.scale.ratio }} - {{ d.scale.tracks }}">{{ d.scale }}</abbr></a></td>
</tr> </tr>
<tr> <tr>
<th scope="row">SKU</th> <th scope="row">SKU</th>

View File

@@ -42,7 +42,7 @@
</tbody> </tbody>
</table> </table>
<div class="d-grid gap-2 mb-1 d-md-block"> <div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="company" search=d %}">Show all rolling stock</a> <a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="company" search=d.safe_name %}">Show all rolling stock</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_company_change' d.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_company_change' d.pk %}">Edit</a>{% endif %}
</div> </div>
</div> </div>

View File

@@ -0,0 +1,41 @@
{% extends "cards.html" %}
{% block pagination %}
{% if data.has_other_pages %}
<nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mt-4 mb-0">
{% if data.has_previous %}
<li class="page-item">
<a class="page-link" href="{% url 'filtered_pagination' _filter=filter search=search page=data.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">Previous</span>
</li>
{% endif %}
{% for i in page_range %}
{% if data.number == i %}
<li class="page-item active">
<span class="page-link">{{ i }}</span></span>
</li>
{% else %}
{% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %}
<li class="page-item"><a class="page-link" href="{% url 'filtered_pagination' _filter=filter search=search page=i %}#rolling-stock">{{ i }}</a></li>
{% endif %}
{% endif %}
{% endfor %}
{% if data.has_next %}
<li class="page-item">
<a class="page-link" href="{% url 'filtered_pagination' _filter=filter search=search page=data.next_page_number %}#rolling-stock" tabindex="-1">Next</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">Next</span>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% endblock %}

View File

@@ -1,6 +1,12 @@
<form class="d-flex needs-validation" action="{% url 'search' %}" method="post" novalidate> <form class="d-flex needs-validation" action="{% url 'search' %}" method="post" novalidate>
<div class="input-group has-validation"> <div class="input-group has-validation">
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search" name="search" id="searchValidation" required> <input class="form-control me-2" type="search" list="datalistOptions" placeholder="Search" aria-label="Search" name="search" id="searchValidation" required>
<datalist id="datalistOptions">
<option value="company: ">
<option value="manufacturer: ">
<option value="scale: ">
<option value="type: ">
</datalist>
<button class="btn btn-outline-primary" type="submit">Search</button> <button class="btn btn-outline-primary" type="submit">Search</button>
</div> </div>
</form> </form>

View File

@@ -32,7 +32,7 @@
</tbody> </tbody>
</table> </table>
<div class="d-grid gap-2 mb-1 d-md-block"> <div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="scale" search=d %}">Show all rolling stock</a> <a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="scale" search=d.safe_name %}">Show all rolling stock</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_scale_change' d.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_scale_change' d.pk %}">Edit</a>{% endif %}
</div> </div>
</div> </div>

View File

@@ -1,15 +1,12 @@
{% extends "cards.html" %} {% extends "cards.html" %}
{% block header %}
<p class="lead text-muted">Results found: {{ matches }}</p>
{% endblock %}
{% block pagination %} {% block pagination %}
{% if data.has_other_pages %} {% if data.has_other_pages %}
<nav aria-label="Page navigation example"> <nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mt-4 mb-0"> <ul class="pagination justify-content-center mt-4 mb-0">
{% if data.has_previous %} {% if data.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'filtered_pagination' _filter=filter search=search page=data.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a> <a class="page-link" href="{% url 'search_pagination' search=encoded_search page=data.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -25,13 +22,13 @@
{% if i == data.paginator.ELLIPSIS %} {% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li> <li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %} {% else %}
<li class="page-item"><a class="page-link" href="{% url 'filtered_pagination' _filter=filter search=search page=i %}#rolling-stock">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'search_pagination' search=encoded_search page=i %}#rolling-stock">{{ i }}</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if data.has_next %} {% if data.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'filtered_pagination' _filter=filter search=search page=data.next_page_number %}#rolling-stock" tabindex="-1">Next</a> <a class="page-link" href="{% url 'search_pagination' search=encoded_search page=data.next_page_number %}#rolling-stock" tabindex="-1">Next</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">

View File

@@ -0,0 +1,73 @@
{% extends "cards.html" %}
{% block cards %}
{% for d in data %}
<div class="col">
<div class="card shadow-sm">
<div class="card-body">
<p class="card-text"><strong>{{ d }}</strong></p>
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Type</th>
</tr>
</thead>
<tbody>
<tr>
<th width="35%" scope="row">Type</th>
<td>{{ d.type }}</td>
</tr>
<tr>
<th width="35%" scope="row">Category</th>
<td>{{ d.category | title}}</td>
</tr>
</tbody>
</table>
<div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="type" search=d.safe_name %}">Show all rolling stock</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_scale_change' d.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}
{% block pagination %}
{% if data.has_other_pages %}
<nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mt-4 mb-0">
{% if data.has_previous %}
<li class="page-item">
<a class="page-link" href="{% url 'scales_pagination' page=data.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">Previous</span>
</li>
{% endif %}
{% for i in page_range %}
{% if data.number == i %}
<li class="page-item active">
<span class="page-link">{{ i }}</span></span>
</li>
{% else %}
{% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %}
<li class="page-item"><a class="page-link" href="{% url 'scales_pagination' page=i %}#rolling-stock">{{ i }}</a></li>
{% endif %}
{% endif %}
{% endfor %}
{% if data.has_next %}
<li class="page-item">
<a class="page-link" href="{% url 'scales_pagination' page=data.next_page_number %}#rolling-stock" tabindex="-1">Next</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">Next</span>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% endblock %}

View File

@@ -11,6 +11,8 @@ from portal.views import (
Companies, Companies,
Manufacturers, Manufacturers,
Scales, Scales,
Types,
SearchRoster,
) )
urlpatterns = [ urlpatterns = [
@@ -22,11 +24,6 @@ urlpatterns = [
GetFlatpage.as_view(), GetFlatpage.as_view(),
name="flatpage", name="flatpage",
), ),
path(
"search",
GetRosterFiltered.as_view(http_method_names=["post"]),
name="search",
),
path("consists", Consists.as_view(), name="consists"), path("consists", Consists.as_view(), name="consists"),
path( path(
"consists/<int:page>", Consists.as_view(), name="consists_pagination" "consists/<int:page>", Consists.as_view(), name="consists_pagination"
@@ -54,7 +51,19 @@ urlpatterns = [
name="manufacturers_pagination", name="manufacturers_pagination",
), ),
path("scales", Scales.as_view(), name="scales"), path("scales", Scales.as_view(), name="scales"),
path("scales/<int:page>", Scales.as_view(), name="scales_pagination"), path("scales/<int:page>", Types.as_view(), name="scales_pagination"),
path("types", Types.as_view(), name="types"),
path("types/<int:page>", Types.as_view(), name="types_pagination"),
path(
"search",
SearchRoster.as_view(http_method_names=["post"]),
name="search",
),
path(
"search/<str:search>/<int:page>",
SearchRoster.as_view(),
name="search_pagination",
),
path( path(
"<str:_filter>/<str:search>", "<str:_filter>/<str:search>",
GetRosterFiltered.as_view(), GetRosterFiltered.as_view(),

View File

@@ -1,19 +1,20 @@
import base64
import operator import operator
from functools import reduce from functools import reduce
from urllib.parse import quote_plus, unquote_plus from urllib.parse import unquote
from django.views import View from django.views import View
from django.http import Http404 from django.http import Http404, HttpResponseBadRequest
from django.db.models import Q from django.db.models import Q
from django.shortcuts import render from django.shortcuts import render, get_object_or_404
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator, PageNotAnInteger from django.core.paginator import Paginator
from portal.utils import get_site_conf from portal.utils import get_site_conf
from portal.models import Flatpage from portal.models import Flatpage
from roster.models import RollingStock from roster.models import RollingStock
from consist.models import Consist from consist.models import Consist
from metadata.models import Company, Manufacturer, Scale from metadata.models import Company, Manufacturer, Scale, RollingStockType, Tag
def order_by_fields(): def order_by_fields():
@@ -62,15 +63,15 @@ class GetData(View):
class GetRoster(GetData): class GetRoster(GetData):
def __init__(self): def __init__(self):
self.title = "Roster" self.title = "Rolling stock"
self.template = "roster.html" self.template = "roster.html"
self.data = RollingStock.objects.order_by(*order_by_fields()) self.data = RollingStock.objects.order_by(*order_by_fields())
class GetRosterFiltered(View): class SearchRoster(View):
def run_search(self, request, search, _filter, page=1): def run_search(self, request, search, _filter, page=1):
site_conf = get_site_conf() site_conf = get_site_conf()
if _filter == "search": if _filter is None:
query = reduce( query = reduce(
operator.or_, operator.or_,
( (
@@ -89,6 +90,11 @@ class GetRosterFiltered(View):
for s in search.split() for s in search.split()
), ),
) )
elif _filter == "type":
query = Q(
Q(rolling_class__type__type__icontains=search)
| Q(rolling_class__type__category__icontains=search)
)
elif _filter == "company": elif _filter == "company":
query = Q( query = Q(
Q(rolling_class__company__name__icontains=search) Q(rolling_class__company__name__icontains=search)
@@ -96,13 +102,11 @@ class GetRosterFiltered(View):
) )
elif _filter == "manufacturer": elif _filter == "manufacturer":
query = Q( query = Q(
Q(manufacturer__name__iexact=search) Q(manufacturer__name__icontains=search)
| Q(rolling_class__manufacturer__name__icontains=search) | Q(rolling_class__manufacturer__name__icontains=search)
) )
elif _filter == "scale": elif _filter == "scale":
query = Q(scale__scale__iexact=search) query = Q(scale__scale__icontains=search)
elif _filter == "tag":
query = Q(tags__slug__iexact=search)
else: else:
raise Http404 raise Http404
@@ -121,47 +125,116 @@ class GetRosterFiltered(View):
return rolling_stock, matches, page_range return rolling_stock, matches, page_range
def get(self, request, search, _filter="search", page=1): def split_search(self, search):
search_unsafe = unquote_plus(search) # expected to be encoded search = search.strip().split(":")
if not search:
raise Http404
elif len(search) == 1: # no filter
_filter = None
search = search[0].strip()
elif len(search) == 2: # filter: search
_filter = search[0].strip().lower()
search = search[1].strip()
else:
return HttpResponseBadRequest
return _filter, search
def get(self, request, search, page=1):
try:
encoded_search = search
search = base64.b64decode(search.encode()).decode()
except Exception:
encoded_search = base64.b64encode(
search.encode()).decode()
_filter, keyword = self.split_search(search)
rolling_stock, matches, page_range = self.run_search( rolling_stock, matches, page_range = self.run_search(
request, search_unsafe, _filter, page request, keyword, _filter, page
) )
return render( return render(
request, request,
"search.html", "search.html",
{ {
"title": "{0}: {1}".format( "title": "Search: \"{}\"".format(search),
_filter.capitalize(), search_unsafe),
"search": search, "search": search,
"search_unsafe": search_unsafe, "encoded_search": encoded_search,
"filter": _filter,
"matches": matches, "matches": matches,
"data": rolling_stock, "data": rolling_stock,
"page_range": page_range, "page_range": page_range,
}, },
) )
def post(self, request, _filter="search", page=1): def post(self, request, page=1):
search = request.POST.get("search") search = request.POST.get("search")
# search = quote_plus(request.POST.get("search"), safe="&") return self.get(request, search, page)
# search_unsafe = unquote_plus(search)
if not search:
class GetRosterFiltered(View):
def run_filter(self, request, search, _filter, page=1):
site_conf = get_site_conf()
if _filter == "type":
type_ = " ".join(search.split()[:-1])
category = search.split()[-1]
try:
title = (
RollingStockType.objects.filter(type__iexact=type_)
.get(category__iexact=category)
)
except ObjectDoesNotExist:
raise Http404
query = Q(
Q(rolling_class__type__type__iexact=type_)
& Q(rolling_class__type__category__iexact=category)
)
elif _filter == "company":
title = get_object_or_404(Company, name__iexact=search)
query = Q(rolling_class__company__name__iexact=search)
elif _filter == "manufacturer":
title = get_object_or_404(Manufacturer, name__iexact=search)
query = Q(
Q(rolling_class__manufacturer__name__iexact=search)
| Q(manufacturer__name__iexact=search)
)
elif _filter == "scale":
title = get_object_or_404(Scale, scale__iexact=search)
query = Q(scale__scale__iexact=search)
elif _filter == "tag":
title = get_object_or_404(Tag, slug=search)
query = Q(tags__slug__iexact=search)
else:
raise Http404 raise Http404
rolling_stock, matches, page_range = self.run_search(
request, search, _filter, page rolling_stock = (
RollingStock.objects.filter(query)
.distinct()
.order_by(*order_by_fields())
)
matches = rolling_stock.count()
paginator = Paginator(rolling_stock, site_conf.items_per_page)
rolling_stock = paginator.get_page(page)
page_range = paginator.get_elided_page_range(
rolling_stock.number, on_each_side=2, on_ends=1
)
return rolling_stock, title, matches, page_range
def get(self, request, search, _filter, page=1):
data, title, matches, page_range = self.run_filter(
request, unquote(search), _filter, page
) )
return render( return render(
request, request,
"search.html", "filter.html",
{ {
"title": "{0}: {1}".format(_filter.capitalize(), search), "title": "{0}: {1}".format(
_filter.capitalize(), title),
"search": search, "search": search,
# "search_unsafe": search_unsafe,
"filter": _filter, "filter": _filter,
"matches": matches, "matches": matches,
"data": rolling_stock, "data": data,
"page_range": page_range, "page_range": page_range,
}, },
) )
@@ -271,6 +344,13 @@ class Scales(GetData):
self.data = Scale.objects.all() self.data = Scale.objects.all()
class Types(GetData):
def __init__(self):
self.title = "Types"
self.template = "types.html"
self.data = RollingStockType.objects.all()
class GetFlatpage(View): class GetFlatpage(View):
def get(self, request, flatpage): def get(self, request, flatpage):
try: try:

View File

@@ -1,4 +1,4 @@
from ram.utils import git_suffix from ram.utils import git_suffix
__version__ = "0.1.1" __version__ = "0.1.2"
__version__ += git_suffix(__file__) __version__ += git_suffix(__file__)