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.conf import settings
@@ -37,7 +37,7 @@ class Manufacturer(models.Model):
return self.name
def safe_name(self):
return quote_plus(self.name, safe="&")
return quote(self.__str__().lower(), safe="& ")
def logo_thumbnail(self):
return get_image_preview(self.logo.url)
@@ -62,7 +62,7 @@ class Company(models.Model):
return self.name
def safe_name(self):
return quote_plus(self.name, safe="&")
return quote(self.__str__().lower(), safe="& ")
def logo_thumbnail(self):
return get_image_preview(self.logo.url)
@@ -104,6 +104,9 @@ class Scale(models.Model):
def __str__(self):
return str(self.scale)
def safe_name(self):
return quote(self.__str__(), safe="& ")
class Tag(models.Model):
name = models.CharField(max_length=128, unique=True)
@@ -112,9 +115,12 @@ class Tag(models.Model):
def __str__(self):
return self.name
def safe_name(self):
return self.slug
@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)
@@ -131,3 +137,6 @@ class RollingStockType(models.Model):
def __str__(self):
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
</a>
<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 'types' %}">Types</a></li>
<li><a class="dropdown-item" href="{% url 'scales' %}">Scales</a></li>
</ul>
</li>

View File

@@ -61,7 +61,7 @@
</tr>
<tr>
<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>
<th scope="row">SKU</th>

View File

@@ -42,7 +42,7 @@
</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="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 %}
</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>
<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>
</div>
</form>

View File

@@ -32,7 +32,7 @@
</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="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 %}
</div>
</div>

View File

@@ -1,15 +1,12 @@
{% extends "cards.html" %}
{% block header %}
<p class="lead text-muted">Results found: {{ matches }}</p>
{% 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 '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>
{% else %}
<li class="page-item disabled">
@@ -25,13 +22,13 @@
{% 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>
<li class="page-item"><a class="page-link" href="{% url 'search_pagination' search=encoded_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>
<a class="page-link" href="{% url 'search_pagination' search=encoded_search page=data.next_page_number %}#rolling-stock" tabindex="-1">Next</a>
</li>
{% else %}
<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,
Manufacturers,
Scales,
Types,
SearchRoster,
)
urlpatterns = [
@@ -22,11 +24,6 @@ urlpatterns = [
GetFlatpage.as_view(),
name="flatpage",
),
path(
"search",
GetRosterFiltered.as_view(http_method_names=["post"]),
name="search",
),
path("consists", Consists.as_view(), name="consists"),
path(
"consists/<int:page>", Consists.as_view(), name="consists_pagination"
@@ -54,7 +51,19 @@ urlpatterns = [
name="manufacturers_pagination",
),
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(
"<str:_filter>/<str:search>",
GetRosterFiltered.as_view(),

View File

@@ -1,19 +1,20 @@
import base64
import operator
from functools import reduce
from urllib.parse import quote_plus, unquote_plus
from urllib.parse import unquote
from django.views import View
from django.http import Http404
from django.http import Http404, HttpResponseBadRequest
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.paginator import Paginator, PageNotAnInteger
from django.core.paginator import Paginator
from portal.utils import get_site_conf
from portal.models import Flatpage
from roster.models import RollingStock
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():
@@ -62,15 +63,15 @@ class GetData(View):
class GetRoster(GetData):
def __init__(self):
self.title = "Roster"
self.title = "Rolling stock"
self.template = "roster.html"
self.data = RollingStock.objects.order_by(*order_by_fields())
class GetRosterFiltered(View):
class SearchRoster(View):
def run_search(self, request, search, _filter, page=1):
site_conf = get_site_conf()
if _filter == "search":
if _filter is None:
query = reduce(
operator.or_,
(
@@ -89,6 +90,11 @@ class GetRosterFiltered(View):
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":
query = Q(
Q(rolling_class__company__name__icontains=search)
@@ -96,13 +102,11 @@ class GetRosterFiltered(View):
)
elif _filter == "manufacturer":
query = Q(
Q(manufacturer__name__iexact=search)
Q(manufacturer__name__icontains=search)
| Q(rolling_class__manufacturer__name__icontains=search)
)
elif _filter == "scale":
query = Q(scale__scale__iexact=search)
elif _filter == "tag":
query = Q(tags__slug__iexact=search)
query = Q(scale__scale__icontains=search)
else:
raise Http404
@@ -121,47 +125,116 @@ class GetRosterFiltered(View):
return rolling_stock, matches, page_range
def get(self, request, search, _filter="search", page=1):
search_unsafe = unquote_plus(search) # expected to be encoded
def split_search(self, search):
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(
request, search_unsafe, _filter, page
request, keyword, _filter, page
)
return render(
request,
"search.html",
{
"title": "{0}: {1}".format(
_filter.capitalize(), search_unsafe),
"title": "Search: \"{}\"".format(search),
"search": search,
"search_unsafe": search_unsafe,
"filter": _filter,
"encoded_search": encoded_search,
"matches": matches,
"data": rolling_stock,
"page_range": page_range,
},
)
def post(self, request, _filter="search", page=1):
def post(self, request, page=1):
search = request.POST.get("search")
# search = quote_plus(request.POST.get("search"), safe="&")
# search_unsafe = unquote_plus(search)
if not search:
return self.get(request, search, page)
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
rolling_stock, matches, page_range = self.run_search(
request, search, _filter, page
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
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(
request,
"search.html",
"filter.html",
{
"title": "{0}: {1}".format(_filter.capitalize(), search),
"title": "{0}: {1}".format(
_filter.capitalize(), title),
"search": search,
# "search_unsafe": search_unsafe,
"filter": _filter,
"matches": matches,
"data": rolling_stock,
"data": data,
"page_range": page_range,
},
)
@@ -271,6 +344,13 @@ class Scales(GetData):
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):
def get(self, request, flatpage):
try:

View File

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