5 Commits

15 changed files with 138 additions and 64 deletions

View File

@@ -12,6 +12,11 @@ td > img.logo {
max-height: 48px; max-height: 48px;
} }
td > img.logo-xl {
max-width: 400px;
max-height: 96px;
}
.btn > span { .btn > span {
display: inline-block; display: inline-block;
} }

View File

@@ -151,27 +151,25 @@
</button> </button>
<div class="collapse navbar-collapse" id="navbarSupportedContent"> <div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0"> <ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item dropdown"> <li class="nav-item">
<a class="nav-link dropdown-toggle" href="#" id="rosterDropdownMenu" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <a class="nav-link" href="{% url 'roster' %}">Roster</a>
Roster
</a>
<ul class="dropdown-menu" aria-labelledby="rosterDropdownMenu">
<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> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'consists' %}">Consists</a> <a class="nav-link" href="{% url 'consists' %}">Consists</a>
</li> </li>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="manufacturersDropdownMenu" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" id="filterDropdownMenu" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Manufacturers Search by
</a> </a>
<ul class="dropdown-menu" aria-labelledby="manufacturersDropdownMenu"> <ul class="dropdown-menu" aria-labelledby="filterDropdownMenu">
<li><a class="dropdown-item" href="{% url 'manufacturers' category='model' %}">Models</a></li> <li class="ps-2 text-secondary">Model</li>
<li><a class="dropdown-item" href="{% url 'manufacturers' category='real' %}">Real</a></li> <li><a class="dropdown-item" href="{% url 'scales' %}">Scale</a></li>
<li><a class="dropdown-item" href="{% url 'manufacturers' category='model' %}">Manufacturer</a></li>
<li><hr class="dropdown-divider"></li>
<li class="ps-2 text-secondary">Prototype</li>
<li><a class="dropdown-item" href="{% url 'types' %}">Type</a></li>
<li><a class="dropdown-item" href="{% url 'companies' %}">Company</a></li>
<li><a class="dropdown-item" href="{% url 'manufacturers' category='real' %}">Manufacturer</a></li>
</ul> </ul>
</li> </li>
{% show_bookshelf_menu %} {% show_bookshelf_menu %}

View File

@@ -7,7 +7,7 @@
{% block cards %} {% block cards %}
{% for d in data %} {% for d in data %}
{% if d.type == "rolling_stock" %} {% if d.type == "rolling_stock" %}
{% include "cards/rolling_stock.html" %} {% include "cards/roster.html" %}
{% elif d.type == "company" %} {% elif d.type == "company" %}
{% include "cards/company.html" %} {% include "cards/company.html" %}
{% elif d.type == "rolling_stock_type" %} {% elif d.type == "rolling_stock_type" %}

View File

@@ -14,7 +14,7 @@
{% if d.item.logo %} {% if d.item.logo %}
<tr> <tr>
<th class="w-33" scope="row">Logo</th> <th class="w-33" scope="row">Logo</th>
<td><img style="max-height: 48px" src="{{ d.item.logo.url }}" /></td> <td><img class="logo" src="{{ d.item.logo.url }}" /></td>
</tr> </tr>
{% endif %} {% endif %}
<tr> <tr>

View File

@@ -44,7 +44,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Length</th> <th scope="row">Length</th>
<td>{{ d.item.consist_item.all | length }}</td> <td>{{ d.item.consist_item.count }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -81,7 +81,9 @@
<tbody class="table-group-divider"> <tbody class="table-group-divider">
<tr> <tr>
<th class="w-33" scope="row">Company</th> <th class="w-33" scope="row">Company</th>
<td><abbr title="{{ consist.company.extended_name }}">{{ consist.company }}</abbr></td> <td>
<a href="{% url 'filtered' _filter="company" search=consist.company.slug %}">{{ consist.company }}</a> ({{ consist.company.extended_name }})
</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Era</th> <th scope="row">Era</th>

View File

@@ -1,5 +1,5 @@
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#loginNavbar" aria-controls="loginNavbar" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#loginNavbar" aria-controls="loginNavbar" aria-expanded="false" aria-label="Toggle navigation">
<span class="bi bi-box-arrow-in-right"></span> <span class="bi bi-person-fill-gear"></span>
</button> </button>
<div class="collapse navbar-collapse justify-content-end" id="loginNavbar"> <div class="collapse navbar-collapse justify-content-end" id="loginNavbar">
<ul class="navbar-nav"> <ul class="navbar-nav">

View File

@@ -45,13 +45,16 @@
<div class="mx-auto"> <div class="mx-auto">
<nav class="nav nav-tabs flex-column flex-md-row mb-2" id="nav-tab"> <nav class="nav nav-tabs flex-column flex-md-row mb-2" id="nav-tab">
<button class="nav-link active" id="nav-summary-tab" data-bs-toggle="tab" data-bs-target="#nav-summary" type="button" role="tab" aria-controls="nav-summary" aria-selected="true">Summary</button> <button class="nav-link active" id="nav-summary-tab" data-bs-toggle="tab" data-bs-target="#nav-summary" type="button" role="tab" aria-controls="nav-summary" aria-selected="true">Summary</button>
<button class="nav-link" id="nav-model-tab" data-bs-toggle="tab" data-bs-target="#nav-model" type="button" role="tab" aria-controls="nav-model" aria-selected="false">Model data</button> <button class="nav-link" id="nav-model-tab" data-bs-toggle="tab" data-bs-target="#nav-model" type="button" role="tab" aria-controls="nav-model" aria-selected="false">Model</button>
<button class="nav-link" id="nav-class-tab" data-bs-toggle="tab" data-bs-target="#nav-class" type="button" role="tab" aria-controls="nav-class" aria-selected="false">Class data</button> <button class="nav-link" id="nav-class-tab" data-bs-toggle="tab" data-bs-target="#nav-class" type="button" role="tab" aria-controls="nav-class" aria-selected="false">Class</button>
<button class="nav-link" id="nav-company-tab" data-bs-toggle="tab" data-bs-target="#nav-company" type="button" role="tab" aria-controls="nav-company" aria-selected="false">Company</button>
{% if rolling_stock.decoder %}<button class="nav-link" id="nav-dcc-tab" data-bs-toggle="tab" data-bs-target="#nav-dcc" type="button" role="tab" aria-controls="nav-dcc" aria-selected="false">DCC</button>{% endif %} {% if rolling_stock.decoder %}<button class="nav-link" id="nav-dcc-tab" data-bs-toggle="tab" data-bs-target="#nav-dcc" type="button" role="tab" aria-controls="nav-dcc" aria-selected="false">DCC</button>{% endif %}
{% if documents or decoder_documents %}<button class="nav-link" id="nav-documents-tab" data-bs-toggle="tab" data-bs-target="#nav-documents" type="button" role="tab" aria-controls="nav-documents" aria-selected="false">Documents</button>{% endif %} {% if documents or decoder_documents %}<button class="nav-link" id="nav-documents-tab" data-bs-toggle="tab" data-bs-target="#nav-documents" type="button" role="tab" aria-controls="nav-documents" aria-selected="false">Documents</button>{% endif %}
{% if journal %}<button class="nav-link" id="nav-journal-tab" data-bs-toggle="tab" data-bs-target="#nav-journal" type="button" role="tab" aria-controls="nav-journal" aria-selected="false">Journal</button>{% endif %} {% if journal %}<button class="nav-link" id="nav-journal-tab" data-bs-toggle="tab" data-bs-target="#nav-journal" type="button" role="tab" aria-controls="nav-journal" aria-selected="false">Journal</button>{% endif %}
{% if rolling_stock.notes %}<button class="nav-link" id="nav-notes-tab" data-bs-toggle="tab" data-bs-target="#nav-notes" type="button" role="tab" aria-controls="nav-notes" aria-selected="false">Notes</button>{% endif %} {% if rolling_stock.notes %}<button class="nav-link" id="nav-notes-tab" data-bs-toggle="tab" data-bs-target="#nav-notes" type="button" role="tab" aria-controls="nav-notes" aria-selected="false">Notes</button>{% endif %}
{% if consists %}<button class="nav-link" id="nav-consists-tab" data-bs-toggle="tab" data-bs-target="#nav-consists" type="button" role="tab" aria-controls="nav-consists" aria-selected="false">Consists</button>{% endif %}
</nav> </nav>
{% with class=rolling_stock.rolling_class company=rolling_stock.rolling_class.company %}
<div class="tab-content" id="nav-tabContent"> <div class="tab-content" id="nav-tabContent">
<div class="tab-pane show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab"> <div class="tab-pane show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab">
<table class="table table-striped"> <table class="table table-striped">
@@ -63,17 +66,17 @@
<tbody class="table-group-divider"> <tbody class="table-group-divider">
<tr> <tr>
<th class="w-33" scope="row">Type</th> <th class="w-33" scope="row">Type</th>
<td>{{ rolling_stock.rolling_class.type }}</td> <td>{{ class.type }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Company</th> <th scope="row">Company</th>
<td> <td>
<a href="{% url 'filtered' _filter="company" search=rolling_stock.rolling_class.company.slug %}"><abbr title="{{ rolling_stock.rolling_class.company.extended_name }}">{{ rolling_stock.rolling_class.company }}</abbr></a> <a href="{% url 'filtered' _filter="company" search=company.slug %}">{{ company }}</a> ({{ company.extended_name }})
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Class</th> <th scope="row">Class</th>
<td>{{ rolling_stock.rolling_class.identifier }}</td> <td>{{ class.identifier }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Road number</th> <th scope="row">Road number</th>
@@ -198,26 +201,16 @@
<tbody class="table-group-divider"> <tbody class="table-group-divider">
<tr> <tr>
<th class="w-33" scope="row">Class</th> <th class="w-33" scope="row">Class</th>
<td>{{ rolling_stock.rolling_class.identifier }}</td> <td>{{ class.identifier }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Type</th> <th scope="row">Type</th>
<td>{{ rolling_stock.rolling_class.type }}</td> <td>{{ class.type }}</td>
</tr>
<tr>
<th scope="row">Company</th>
<td>
<a href="{% url 'filtered' _filter="company" search=rolling_stock.rolling_class.company.slug %}">{{ rolling_stock.rolling_class.company }}</a> ({{ rolling_stock.rolling_class.company.extended_name }})
</td>
</tr>
<tr>
<th scope="row">Country</th>
<td>{{ rolling_stock.rolling_class.company.country.name }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Manufacturer</th> <th scope="row">Manufacturer</th>
<td>{%if rolling_stock.rolling_class.manufacturer %} <td>{%if class.manufacturer %}
<a href="{% url 'filtered' _filter="manufacturer" search=rolling_stock.rolling_class.manufacturer.slug %}">{{ rolling_stock.rolling_class.manufacturer }}{% if rolling_stock.rolling_class.manufacturer.website %}</a> <a href="{{ rolling_stock.rolling_class.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %} <a href="{% url 'filtered' _filter="manufacturer" search=class.manufacturer.slug %}">{{ class.manufacturer }}{% if class.manufacturer.website %}</a> <a href="{{ class.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
{% endif %}</td> {% endif %}</td>
</tr> </tr>
</tbody> </tbody>
@@ -240,6 +233,37 @@
</table> </table>
{% endif %} {% endif %}
</div> </div>
<div class="tab-pane" id="nav-company" role="tabpanel" aria-labelledby="nav-company-tab">
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Company data</th>
</tr>
</thead>
<tbody class="table-group-divider">
{% if company.logo %}
<tr>
<th class="w-33" scope="row">Logo</th>
<td><img class="logo-xl" src="{{ company.logo.url }}" /></td>
</tr>
{% endif %}
<tr>
<th class="w-33" scope="row">Name</th>
<td><a href="{% url 'filtered' _filter="company" search=company.slug %}">{{ company.name }}</a> ({{ company.extended_name }})</td>
</tr>
<tr>
<th class="w-33" scope="row">Country</th>
<td>{{ company.country.name }} <img src="{{ company.country.flag }}" alt="{{ company.country }}" />
</tr>
{% if company.freelance %}
<tr>
<th class="w-33" scope="row">Notes</th>
<td>A <em>freelance</em> company</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<div class="tab-pane" id="nav-dcc" role="tabpanel" aria-labelledby="nav-dcc-tab"> <div class="tab-pane" id="nav-dcc" role="tabpanel" aria-labelledby="nav-dcc-tab">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
@@ -333,7 +357,15 @@
<div class="tab-pane" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab"> <div class="tab-pane" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab">
{{ rolling_stock.notes | safe }} {{ rolling_stock.notes | safe }}
</div> </div>
<div class="tab-pane" id="nav-consists" role="tabpanel" aria-labelledby="nav-cosists-tab">
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3 mb-3">
{% for d in consists %}
{% include "cards/consist.html" %}
{% endfor %}
</div> </div>
</div>
</div>
{% endwith %}
<div class="d-grid gap-2 d-md-flex justify-content-md-end"> <div class="d-grid gap-2 d-md-flex justify-content-md-end">
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:roster_rollingstock_change' rolling_stock.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:roster_rollingstock_change' rolling_stock.pk %}">Edit</a>{% endif %}
</div> </div>

View File

@@ -3,7 +3,7 @@ from django.urls import path
from portal.views import ( from portal.views import (
GetData, GetData,
GetRoster, GetRoster,
GetRosterFiltered, GetObjectsFiltered,
GetFlatpage, GetFlatpage,
GetRollingStock, GetRollingStock,
GetConsist, GetConsist,
@@ -14,7 +14,7 @@ from portal.views import (
Types, Types,
Books, Books,
GetBook, GetBook,
SearchRoster, SearchObjects,
) )
urlpatterns = [ urlpatterns = [
@@ -95,22 +95,22 @@ urlpatterns = [
path("bookshelf/book/<uuid:uuid>", GetBook.as_view(), name="book"), path("bookshelf/book/<uuid:uuid>", GetBook.as_view(), name="book"),
path( path(
"search", "search",
SearchRoster.as_view(http_method_names=["post"]), SearchObjects.as_view(http_method_names=["post"]),
name="search", name="search",
), ),
path( path(
"search/<str:search>/<int:page>", "search/<str:search>/<int:page>",
SearchRoster.as_view(), SearchObjects.as_view(),
name="search_pagination", name="search_pagination",
), ),
path( path(
"<str:_filter>/<str:search>", "<str:_filter>/<str:search>",
GetRosterFiltered.as_view(), GetObjectsFiltered.as_view(),
name="filtered", name="filtered",
), ),
path( path(
"<str:_filter>/<str:search>/<int:page>", "<str:_filter>/<str:search>/<int:page>",
GetRosterFiltered.as_view(), GetObjectsFiltered.as_view(),
name="filtered_pagination", name="filtered_pagination",
), ),
path("<uuid:uuid>", GetRollingStock.as_view(), name="rolling_stock"), path("<uuid:uuid>", GetRollingStock.as_view(), name="rolling_stock"),

View File

@@ -78,12 +78,12 @@ class GetData(View):
class GetRoster(GetData): class GetRoster(GetData):
title = "Rolling stock" title = "Roster"
item_type = "rolling_stock" item_type = "rolling_stock"
queryset = RollingStock.objects.order_by(*order_by_fields()) queryset = RollingStock.objects.order_by(*order_by_fields())
class SearchRoster(View): class SearchObjects(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 is None: if _filter is None:
@@ -215,15 +215,17 @@ class SearchRoster(View):
return self.get(request, search, page) return self.get(request, search, page)
class GetRosterFiltered(View): class GetObjectsFiltered(View):
def run_filter(self, request, search, _filter, page=1): def run_filter(self, request, search, _filter, page=1):
site_conf = get_site_conf() site_conf = get_site_conf()
if _filter == "type": if _filter == "type":
title = RollingStockType.objects.get(slug__iexact=search) title = get_object_or_404(RollingStockType, slug__iexact=search)
query = Q(rolling_class__type__slug__iexact=search) query = Q(rolling_class__type__slug__iexact=search)
elif _filter == "company": elif _filter == "company":
title = get_object_or_404(Company, slug__iexact=search) title = get_object_or_404(Company, slug__iexact=search)
query = Q(rolling_class__company__slug__iexact=search) query = Q(rolling_class__company__slug__iexact=search)
query_2nd = Q(company__slug__iexact=search)
elif _filter == "manufacturer": elif _filter == "manufacturer":
title = get_object_or_404(Manufacturer, slug__iexact=search) title = get_object_or_404(Manufacturer, slug__iexact=search)
query = Q( query = Q(
@@ -233,9 +235,13 @@ class GetRosterFiltered(View):
elif _filter == "scale": elif _filter == "scale":
title = get_object_or_404(Scale, slug__iexact=search) title = get_object_or_404(Scale, slug__iexact=search)
query = Q(scale__slug__iexact=search) query = Q(scale__slug__iexact=search)
query_2nd = Q(
consist_item__rolling_stock__scale__slug__iexact=search
)
elif _filter == "tag": elif _filter == "tag":
title = get_object_or_404(Tag, slug__iexact=search) title = get_object_or_404(Tag, slug__iexact=search)
query = Q(tags__slug__iexact=search) query = Q(tags__slug__iexact=search)
query_2nd = query # For tags the 2nd level query doesn't change
else: else:
raise Http404 raise Http404
@@ -252,9 +258,9 @@ class GetRosterFiltered(View):
"item": item "item": item
}) })
if _filter == "tag": try: # Execute only if query_2nd is defined
consists = ( consists = (
Consist.objects.filter(query) Consist.objects.filter(query_2nd)
.distinct() .distinct()
) )
for item in consists: for item in consists:
@@ -262,8 +268,9 @@ class GetRosterFiltered(View):
"type": "consist", "type": "consist",
"item": item "item": item
}) })
if _filter == "tag": # Books can be filtered only by tag
books = ( books = (
Book.objects.filter(query) Book.objects.filter(query_2nd)
.distinct() .distinct()
) )
for item in books: for item in books:
@@ -271,6 +278,8 @@ class GetRosterFiltered(View):
"type": "book", "type": "book",
"item": item "item": item
}) })
except NameError:
pass
paginator = Paginator(data, site_conf.items_per_page) paginator = Paginator(data, site_conf.items_per_page)
data = paginator.get_page(page) data = paginator.get_page(page)
@@ -331,6 +340,13 @@ class GetRollingStock(View):
private=False private=False
) )
consists = [{
"type": "consist",
"item": c
} for c in Consist.objects.filter(
consist_item__rolling_stock=rolling_stock
)] # A dict with "item" is required by the consists card
return render( return render(
request, request,
"rollingstock.html", "rollingstock.html",
@@ -342,6 +358,7 @@ class GetRollingStock(View):
"decoder_documents": decoder_documents, "decoder_documents": decoder_documents,
"documents": documents, "documents": documents,
"journal": journal, "journal": journal,
"consists": consists,
}, },
) )

View File

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

View File

@@ -1,7 +1,8 @@
# vim: syntax=python # vim: syntax=python
from django.conf import settings
""" """
Django local_settings for ram project. Example of changes suitable for production
""" """
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
@@ -12,9 +13,23 @@ SECRET_KEY = (
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False DEBUG = False
# SECURITY WARNING: cache middlewares must be loaded before cookies one
MIDDLEWARE = [
"django.middleware.cache.UpdateCacheMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.cache.FetchFromCacheMiddleware",
] + settings.MIDDLEWARE
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "redis://127.0.0.1:6379",
}
}
CACHE_MIDDLEWARE_SECONDS = 300
STORAGE_DIR = BASE_DIR / "storage" STORAGE_DIR = BASE_DIR / "storage"
ALLOWED_HOSTS = ["127.0.0.1"] ALLOWED_HOSTS = ["127.0.0.1", "myhost"]
CSRF_TRUSTED_ORIGINS = ["https://myhost"] CSRF_TRUSTED_ORIGINS = ["https://myhost"]
ROOT_URLCONF = "ram.urls"
STATIC_URL = "static/" STATIC_URL = "static/"
MEDIA_URL = "media/" MEDIA_URL = "media/"

View File

@@ -60,3 +60,7 @@ if settings.DEBUG:
name="openapi-schema", name="openapi-schema",
), ),
] ]
if apps.is_installed("debug_toolbar"):
urlpatterns += [
path("__debug__/", include("debug_toolbar.urls")),
]

View File

@@ -1 +1,2 @@
gunicorn gunicorn
pylibmc