9 Commits

Author SHA1 Message Date
764240d67a Fix bookshelf default sorting 2023-10-09 23:09:05 +02:00
424b17ae58 Bug fixing for consists 2023-10-08 09:52:38 +02:00
c73efb01e4 Introduce private docs and flatpages preview (#26)
* Add support for private documents
* Fix migrations after merge
* Rebase fixtures
* Filter private decoder docs
* Enable preview of unpublished pages
2023-10-07 22:38:20 +02:00
a21baac10c Fix a dependency on solo during bootstrap 2023-10-06 21:37:24 +02:00
4b0361acc1 Fix the consists search 2023-10-05 23:21:52 +02:00
425eed3d83 Bookshelf reloaded (#25)
* Navbar refactoring
* Fix coming soon SVG fonts
* Overhaul templating and extend search to consists and books
2023-10-05 23:13:42 +02:00
2d48463474 Change model default sort for Book 2023-10-03 23:08:11 +02:00
08226247c7 Extend ISBN to include dashes 2023-10-03 22:43:20 +02:00
4f52736d97 Fix a copy-paste issue in bookshelf model 2023-10-03 22:26:51 +02:00
36 changed files with 723 additions and 542 deletions

View File

@@ -12,9 +12,6 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name="book", name="book",
options={ options={"ordering": ["authors__last_name", "title"]},
"ordering": ["authors__last_name", "title"],
"verbose_name_plural": "Rolling stock",
},
), ),
] ]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.2.5 on 2023-10-03 20:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0005_alter_book_options"),
]
operations = [
migrations.AlterField(
model_name="book",
name="ISBN",
field=models.CharField(blank=True, max_length=17),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 4.2.5 on 2023-10-03 21:07
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0006_alter_book_isbn"),
]
operations = [
migrations.AlterModelOptions(
name="book",
options={"ordering": ["title"]},
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 4.2.6 on 2023-10-09 21:08
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0007_alter_book_options"),
]
operations = [
migrations.AlterModelOptions(
name="author",
options={"ordering": ["last_name", "first_name"]},
),
migrations.AlterModelOptions(
name="publisher",
options={"ordering": ["name"]},
),
]

View File

@@ -16,6 +16,9 @@ class Publisher(models.Model):
country = CountryField(blank=True) country = CountryField(blank=True)
website = models.URLField(blank=True) website = models.URLField(blank=True)
class Meta:
ordering = ["name"]
def __str__(self): def __str__(self):
return self.name return self.name
@@ -24,6 +27,9 @@ class Author(models.Model):
first_name = models.CharField(max_length=100) first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100) last_name = models.CharField(max_length=100)
class Meta:
ordering = ["last_name", "first_name"]
def __str__(self): def __str__(self):
return f"{self.last_name}, {self.first_name}" return f"{self.last_name}, {self.first_name}"
@@ -36,7 +42,7 @@ class Book(models.Model):
title = models.CharField(max_length=200) title = models.CharField(max_length=200)
authors = models.ManyToManyField(Author) authors = models.ManyToManyField(Author)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE) publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
ISBN = models.CharField(max_length=13, blank=True) ISBN = models.CharField(max_length=17, blank=True) # 13 + dashes
language = models.CharField( language = models.CharField(
max_length=7, max_length=7,
choices=settings.LANGUAGES, choices=settings.LANGUAGES,
@@ -53,8 +59,7 @@ class Book(models.Model):
updated_time = models.DateTimeField(auto_now=True) updated_time = models.DateTimeField(auto_now=True)
class Meta: class Meta:
ordering = ["authors__last_name", "title"] ordering = ["title"]
verbose_name_plural = "Rolling stock"
def __str__(self): def __str__(self):
return self.title return self.title

View File

@@ -15,6 +15,7 @@ from metadata.models import (
@admin.register(Property) @admin.register(Property)
class PropertyAdmin(admin.ModelAdmin): class PropertyAdmin(admin.ModelAdmin):
list_display = ("name", "private")
search_fields = ("name",) search_fields = ("name",)

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.2.5 on 2023-10-06 19:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("metadata", "0012_alter_decoder_manufacturer_decoderdocument"),
]
operations = [
migrations.AddField(
model_name="decoderdocument",
name="private",
field=models.BooleanField(default=False),
),
]

View File

@@ -66,7 +66,6 @@ class Flatpage(models.Model):
return reverse("flatpage", kwargs={"flatpage": self.path}) return reverse("flatpage", kwargs={"flatpage": self.path})
def get_link(self): def get_link(self):
if self.published:
return mark_safe( return mark_safe(
'<a href="{0}" target="_blank">Link</a>'.format( '<a href="{0}" target="_blank">Link</a>'.format(
self.get_absolute_url() self.get_absolute_url()

View File

@@ -46,12 +46,12 @@
transform="rotate(-90)" /> transform="rotate(-90)" />
<text <text
xml:space="preserve" xml:space="preserve"
style="font-weight:bold;font-size:0.444444px;line-height:1.25;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans Bold';letter-spacing:0px;word-spacing:0px;stroke-width:0.0138889" style="font-weight:bold;font-size:0.444444px;line-height:1.25;font-family:system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue','Noto Sans','Liberation Sans',Arial,sans-serif;-inkscape-font-specification:'Noto Sans Bold';letter-spacing:0px;word-spacing:0px;stroke-width:0.0138889"
x="1.5366687" x="1.5366687"
y="1.6798887" y="1.6798887"
id="text1"><tspan id="text1"><tspan
id="tspan1" id="tspan1"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:0.444444px;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans';fill:#dee2e6;fill-opacity:1;stroke-width:0.0138889" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:0.444444px;font-family:system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue','Noto Sans','Liberation Sans',Arial,sans-serif;-inkscape-font-specification:'Noto Sans';fill:#dee2e6;fill-opacity:1;stroke-width:0.0138889"
x="1.5366687" x="1.5366687"
y="1.6798887">Coming soon</tspan></text> y="1.6798887">Coming soon</tspan></text>
</g> </g>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -171,8 +171,8 @@
<li><a class="dropdown-item" href="{% url 'manufacturers' category='real' %}">Real</a></li> <li><a class="dropdown-item" href="{% url 'manufacturers' category='real' %}">Real</a></li>
</ul> </ul>
</li> </li>
{% show_flatpages_menu %}
{% show_bookshelf_menu %} {% show_bookshelf_menu %}
{% show_flatpages_menu %}
</ul> </ul>
{% include 'includes/search.html' %} {% include 'includes/search.html' %}
</div> </div>

View File

@@ -43,14 +43,12 @@
<section class="py-4 text-start container"> <section class="py-4 text-start container">
<div class="row"> <div class="row">
<div class="mx-auto"> <div class="mx-auto">
<nav> <nav class="nav nav-tabs flex-column flex-md-row mb-2" id="nav-tab">
<div class="nav nav-tabs" id="nav-tab" role="tablist">
<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>
{% if book.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 book.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 %}
</div>
</nav> </nav>
<div class="tab-content" id="nav-tabContent"> <div class="tab-content" id="nav-tabContent">
<div class="tab-pane fade 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">
<thead> <thead>
<tr> <tr>
@@ -112,7 +110,7 @@
</table> </table>
{% endif %} {% endif %}
</div> </div>
<div class="tab-pane fade" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab"> <div class="tab-pane" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab">
{{ book.notes | safe }} {{ book.notes | safe }}
</div> </div>
</div> </div>

View File

@@ -1,68 +1,4 @@
{% extends "cards.html" %} {% extends "cards.html" %}
{% block cards %}
{% for d in data %}
<div class="col">
<div class="card shadow-sm">
{% if d.image.all %}
<a href="{{ d.get_absolute_url }}">
{% for r in d.image.all %}
{% if forloop.first %}<img src="{{ r.image.url }}" alt="Card image cap">{% endif %}
{% endfor %}
</a>
{% endif %}
<div class="card-body">
<p class="card-text" style="position: relative;">
<strong>{{ d }}</strong>
<a class="stretched-link" href="{{ d.get_absolute_url }}"></a>
</p>
{% if d.tags.all %}
<p class="card-text"><small>Tags:</small>
{% for t in d.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
{{ t.name }}</a>{# new line is required #}
{% endfor %}
</p>
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Book</th>
</tr>
</thead>
<tbody class="table-group-divider">
<tr>
<th class="w-33" scope="row">Authors</th>
<td>
<ul class="mb-0 list-unstyled">{% for a in d.authors.all %}<li>{{ a }}</li>{% endfor %}</ul>
</td>
</tr>
<tr>
<th class="w-33" scope="row">Publisher</th>
<td>{{ d.publisher }}</td>
</tr>
<tr>
<th scope="row">Language</th>
<td>{{ d.get_language_display }}</td>
</tr>
<tr>
<th scope="row">Pages</th>
<td>{{ d.number_of_pages|default:"-" }}</td>
</tr>
<tr>
<th scope="row">Year</th>
<td>{{ d.publication_year|default:"-" }}</td>
</tr>
</tbody>
</table>
<div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{{ d.get_absolute_url }}">Show all data</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:bookshelf_book_change' d.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
{% 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">

View File

@@ -4,7 +4,7 @@
Bookshelf Bookshelf
</a> </a>
<ul class="dropdown-menu" aria-labelledby="bookshelfDropdownMenuLink"> <ul class="dropdown-menu" aria-labelledby="bookshelfDropdownMenuLink">
<li><a class="nav-link" href="{% url 'books' %}">Books</a></li> <li><a class="dropdown-item" href="{% url 'books' %}">Books</a></li>
</ul> </ul>
</li> </li>
{% endif %} {% endif %}

View File

@@ -1,6 +1,4 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %}
{% block header %} {% block header %}
<p class="lead text-muted">Results found: {{ matches }}</p> <p class="lead text-muted">Results found: {{ matches }}</p>
{% endblock %} {% endblock %}
@@ -8,97 +6,21 @@
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3"> <div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
{% block cards %} {% block cards %}
{% for d in data %} {% for d in data %}
<div class="col"> {% if d.type == "rolling_stock" %}
<div class="card shadow-sm"> {% include "cards/rolling_stock.html" %}
{% if d.image.count > 0 %} {% elif d.type == "company" %}
<a href="{{d.get_absolute_url}}"><img class="card-img-top" src="{{ d.image.first.image.url }}" alt="{{ d }}"></a> {% include "cards/company.html" %}
{% else %} {% elif d.type == "rolling_stock_type" %}
<!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) --> {% include "cards/rolling_stock_type.html" %}
<a href="{{d.get_absolute_url}}"><img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d }}"></a> {% elif d.type == "scale" %}
{% include "cards/scale.html" %}
{% elif d.type == "consist" %}
{% include "cards/consist.html" %}
{% elif d.type == "manufacturer" %}
{% include "cards/manufacturer.html" %}
{% elif d.type == "book" %}
{% include "cards/book.html" %}
{% endif %} {% endif %}
<div class="card-body">
<p class="card-text" style="position: relative;">
<strong>{{ d }}</strong>
<a class="stretched-link" href="{{ d.get_absolute_url }}"></a>
</p>
{% if d.tags.all %}
<p class="card-text"><small>Tags:</small>
{% for t in d.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
{{ t.name }}</a>{# new line is required #}
{% endfor %}
</p>
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Data</th>
</tr>
</thead>
<tbody class="table-group-divider">
<tr>
<th class="w-33" scope="row">Type</th>
<td>{{ d.rolling_class.type }}</td>
</tr>
<tr>
<th scope="row">Company</th>
<td>
<a href="{% url 'filtered' _filter="company" search=d.rolling_class.company.slug %}"><abbr title="{{ d.rolling_class.company.extended_name }}">{{ d.rolling_class.company }}</abbr></a>
</td>
</tr>
<tr>
<th scope="row">Class</th>
<td>{{ d.rolling_class.identifier }}</td>
</tr>
<tr>
<th scope="row">Road number</th>
<td>{{ d.road_number }}</td>
</tr>
<tr>
<th scope="row">Era</th>
<td>{{ d.era }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Manufacturer</th>
<td>{%if d.manufacturer %}
<a href="{% url 'filtered' _filter="manufacturer" search=d.manufacturer.slug %}">{{ d.manufacturer }}{% if d.manufacturer.website %}</a> <a href="{{ d.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
{% endif %}</td>
</tr>
<tr>
<th scope="row">Scale</th>
<td><a href="{% url 'filtered' _filter="scale" search=d.scale.slug %}"><abbr title="{{ d.scale.ratio }} - {{ d.scale.tracks }}">{{ d.scale }}</abbr></a></td>
</tr>
<tr>
<th scope="row">Item number</th>
<td>{{ d.item_number }}</td>
</tr>
</tbody>
</table>
{% if d.decoder %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">DCC data</th>
</tr>
</thead>
<tbody class="table-group-divider">
<tr>
<th class="w-33" scope="row">Decoder</th>
<td>{{ d.decoder }}</td>
</tr>
<tr>
<th scope="row">Address</th>
<td>{{ d.address }}</td>
</tr>
</tbody>
</table>
{% endif %}
<div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{{d.get_absolute_url}}">Show all data</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:roster_rollingstock_change' d.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}
</div> </div>

View File

@@ -0,0 +1,60 @@
<div class="col">
<div class="card shadow-sm">
{% if d.item.image.all %}
<a href="{{ d.item.get_absolute_url }}">
{% for r in d.item.image.all %}
{% if forloop.first %}<img src="{{ r.image.url }}" alt="Card image cap">{% endif %}
{% endfor %}
</a>
{% endif %}
<div class="card-body">
<p class="card-text" style="position: relative;">
<strong>{{ d.item }}</strong>
<a class="stretched-link" href="{{ d.item.get_absolute_url }}"></a>
</p>
{% if d.item.tags.all %}
<p class="card-text"><small>Tags:</small>
{% for t in d.item.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
{{ t.name }}</a>{# new line is required #}
{% endfor %}
</p>
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Book</th>
</tr>
</thead>
<tbody class="table-group-divider">
<tr>
<th class="w-33" scope="row">Authors</th>
<td>
<ul class="mb-0 list-unstyled">{% for a in d.item.authors.all %}<li>{{ a }}</li>{% endfor %}</ul>
</td>
</tr>
<tr>
<th class="w-33" scope="row">Publisher</th>
<td>{{ d.item.publisher }}</td>
</tr>
<tr>
<th scope="row">Language</th>
<td>{{ d.item.get_language_display }}</td>
</tr>
<tr>
<th scope="row">Pages</th>
<td>{{ d.item.number_of_pages|default:"-" }}</td>
</tr>
<tr>
<th scope="row">Year</th>
<td>{{ d.item.publication_year|default:"-" }}</td>
</tr>
</tbody>
</table>
<div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{{ d.item.get_absolute_url }}">Show all data</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:bookshelf_book_change' d.item.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,46 @@
<div class="col">
<div class="card shadow-sm">
<div class="card-body">
<p class="card-text" style="position: relative;">
<strong>{{ d.item.name }}</strong>
</p>
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Company</th>
</tr>
</thead>
<tbody class="table-group-divider">
{% if d.item.logo %}
<tr>
<th class="w-33" scope="row">Logo</th>
<td><img style="max-height: 48px" src="{{ d.item.logo.url }}" /></td>
</tr>
{% endif %}
<tr>
<th class="w-33" scope="row">Name</th>
<td>{{ d.item.extended_name }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Abbreviation</th>
<td>{{ d.item.name }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Country</th>
<td>{{ d.item.country.name }} <img src="{{ d.item.country.flag }}" alt="{{ d.item.country }}" />
</tr>
{% if d.item.freelance %}
<tr>
<th class="w-33" scope="row">Notes</th>
<td>A <em>freelance</em> company</td>
</tr>
{% endif %}
</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.item.slug %}">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.item.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,59 @@
<div class="col">
<div class="card shadow-sm">
<a href="{{ d.item.get_absolute_url }}">
{% if d.item.image %}
<img src="{{ d.item.image.url }}" alt="Card image cap">
{% else %}
{% with d.item.consist_item.first.rolling_stock as r %}
{% for i in r.image.all %}
{% if forloop.first %}<img src="{{ i.image.url }}" alt="Card image cap">{% endif %}
{% endfor %}
{% endwith %}
{% endif %}
</a>
<div class="card-body">
<p class="card-text" style="position: relative;">
<strong>{{ d.item }}</strong>
<a class="stretched-link" href="{{ d.item.get_absolute_url }}"></a>
</p>
{% if d.item.tags.all %}
<p class="card-text"><small>Tags:</small>
{% for t in d.item.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
{{ t.name }}</a>{# new line is required #}
{% endfor %}
</p>
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Consist</th>
</tr>
</thead>
<tbody class="table-group-divider">
{% if d.item.address %}
<tr>
<th class="w-33" scope="row">Address</th>
<td>{{ d.item.address }}</td>
</tr>
{% endif %}
<tr>
<th class="w-33" scope="row">Company</th>
<td><abbr title="{{ d.item.company.extended_name }}">{{ d.item.company }}</abbr></td>
</tr>
<tr>
<th scope="row">Era</th>
<td>{{ d.item.era }}</td>
</tr>
<tr>
<th scope="row">Length</th>
<td>{{ d.item.consist_item.all | length }}</td>
</tr>
</tbody>
</table>
<div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{{ d.item.get_absolute_url }}">Show all data</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:consist_consist_change' d.item.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,39 @@
<div class="col">
<div class="card shadow-sm">
<div class="card-body">
<p class="card-text" style="position: relative;">
<strong>{{ d.item.name }}</strong>
</p>
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Manufacturer</th>
</tr>
</thead>
<tbody class="table-group-divider">
{% if d.item.logo %}
<tr>
<th class="w-33" scope="row">Logo</th>
<td><img style="max-height: 48px" src="{{ d.item.logo.url }}" /></td>
</tr>
{% endif %}
{% if d.item.website %}
<tr>
<th class="w-33" scope="row">Website</th>
<td><a href="{{ d.item.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a></td>
</tr>
{% endif %}
<tr>
<th class="w-33" scope="row">Category</th>
<td>{{ d.item.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="manufacturer" search=d.item.slug %}">Show all rolling stock</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_manufacturer_change' d.item.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,92 @@
{% load static %}
<div class="col">
<div class="card shadow-sm">
{% if d.item.image.exists %}
<a href="{{d.item.get_absolute_url}}"><img class="card-img-top" src="{{ d.item.image.first.image.url }}" alt="{{ d }}"></a>
{% else %}
<!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) -->
<a href="{{d.item.get_absolute_url}}"><img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d }}"></a>
{% endif %}
<div class="card-body">
<p class="card-text" style="position: relative;">
<strong>{{ d.item }}</strong>
<a class="stretched-link" href="{{ d.item.get_absolute_url }}"></a>
</p>
{% if d.item.tags.all %}
<p class="card-text"><small>Tags:</small>
{% for t in d.item.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
{{ t.name }}</a>{# new line is required #}
{% endfor %}
</p>
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Rolling stock</th>
</tr>
</thead>
<tbody class="table-group-divider">
<tr>
<th class="w-33" scope="row">Type</th>
<td>{{ d.item.rolling_class.type }}</td>
</tr>
<tr>
<th scope="row">Company</th>
<td>
<a href="{% url 'filtered' _filter="company" search=d.item.rolling_class.company.slug %}"><abbr title="{{ d.item.rolling_class.company.extended_name }}">{{ d.item.rolling_class.company }}</abbr></a>
</td>
</tr>
<tr>
<th scope="row">Class</th>
<td>{{ d.item.rolling_class.identifier }}</td>
</tr>
<tr>
<th scope="row">Road number</th>
<td>{{ d.item.road_number }}</td>
</tr>
<tr>
<th scope="row">Era</th>
<td>{{ d.item.era }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Manufacturer</th>
<td>{%if d.item.manufacturer %}
<a href="{% url 'filtered' _filter="manufacturer" search=d.item.manufacturer.slug %}">{{ d.item.manufacturer }}{% if d.item.manufacturer.website %}</a> <a href="{{ d.item.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
{% endif %}</td>
</tr>
<tr>
<th scope="row">Scale</th>
<td><a href="{% url 'filtered' _filter="scale" search=d.item.scale.slug %}"><abbr title="{{ d.item.scale.ratio }} - {{ d.item.scale.tracks }}">{{ d.item.scale }}</abbr></a></td>
</tr>
<tr>
<th scope="row">Item number</th>
<td>{{ d.item.item_number }}</td>
</tr>
</tbody>
</table>
{% if d.item.decoder %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">DCC data</th>
</tr>
</thead>
<tbody class="table-group-divider">
<tr>
<th class="w-33" scope="row">Decoder</th>
<td>{{ d.item.decoder }}</td>
</tr>
<tr>
<th scope="row">Address</th>
<td>{{ d.item.address }}</td>
</tr>
</tbody>
</table>
{% endif %}
<div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{{d.item.get_absolute_url}}">Show all data</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:roster_rollingstock_change' d.item.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,29 @@
<div class="col">
<div class="card shadow-sm">
<div class="card-body">
<p class="card-text"><strong>{{ d.item }}</strong></p>
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Type</th>
</tr>
</thead>
<tbody class="table-group-divider">
<tr>
<th class="w-33" scope="row">Type</th>
<td>{{ d.item.type }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Category</th>
<td>{{ d.item.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.item.slug %}">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.item.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,36 @@
<div class="col">
<div class="card shadow-sm">
<div class="card-body">
<p class="card-text"><strong>{{ d.item }}</strong></p>
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Scale</th>
</tr>
</thead>
<tbody>
<tr>
<th class="w-33" scope="row">Name</th>
<td>{{ d.item.scale }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Ratio</th>
<td>{{ d.item.ratio }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Gauge</th>
<td>{{ d.item.gauge }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Tracks</th>
<td>{{ d.item.tracks }}</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="scale" search=d.item.slug %}">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.item.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>

View File

@@ -1,55 +1,4 @@
{% extends "cards.html" %} {% 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" style="position: relative;">
<strong>{{ d.name }}</strong>
</p>
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Company</th>
</tr>
</thead>
<tbody class="table-group-divider">
{% if d.logo %}
<tr>
<th class="w-33" scope="row">Logo</th>
<td><img style="max-height: 48px" src="{{ d.logo.url }}" /></td>
</tr>
{% endif %}
<tr>
<th class="w-33" scope="row">Name</th>
<td>{{ d.extended_name }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Abbreviation</th>
<td>{{ d.name }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Country</th>
<td>{{ d.country.name }} <img src="{{ d.country.flag }}" alt="{{ d.country }}" />
</tr>
{% if d.freelance %}
<tr>
<th class="w-33" scope="row">Notes</th>
<td>A <em>freelance</em> company</td>
</tr>
{% endif %}
</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.slug %}">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>
</div>
</div>
{% endfor %}
{% 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">

View File

@@ -66,14 +66,12 @@
<section class="py-4 text-start container"> <section class="py-4 text-start container">
<div class="row"> <div class="row">
<div class="mx-auto"> <div class="mx-auto">
<nav> <nav class="nav nav-tabs flex-column flex-md-row mb-2" id="nav-tab">
<div class="nav nav-tabs" id="nav-tab" role="tablist">
<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>
{% if consist.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 consist.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 %}
</div>
</nav> </nav>
<div class="tab-content" id="nav-tabContent"> <div class="tab-content" id="nav-tabContent">
<div class="tab-pane fade 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">
<thead> <thead>
<tr> <tr>
@@ -96,7 +94,7 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="tab-pane fade" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab"> <div class="tab-pane" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab">
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>

View File

@@ -1,68 +1,4 @@
{% extends "cards.html" %} {% extends "cards.html" %}
{% block cards %}
{% for d in data %}
<div class="col">
<div class="card shadow-sm">
<a href="{{ d.get_absolute_url }}">
{% if d.image %}
<img src="{{ d.image.url }}" alt="Card image cap">
{% else %}
{% with d.consist_item.first.rolling_stock as r %}
{% for i in r.image.all %}
{% if forloop.first %}<img src="{{ i.image.url }}" alt="Card image cap">{% endif %}
{% endfor %}
{% endwith %}
{% endif %}
</a>
<div class="card-body">
<p class="card-text" style="position: relative;">
<strong>{{ d }}</strong>
<a class="stretched-link" href="{{ d.get_absolute_url }}"></a>
</p>
{% if d.tags.all %}
<p class="card-text"><small>Tags:</small>
{% for t in d.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
{{ t.name }}</a>{# new line is required #}
{% endfor %}
</p>
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Consist data</th>
</tr>
</thead>
<tbody class="table-group-divider">
{% if d.address %}
<tr>
<th class="w-33" scope="row">Address</th>
<td>{{ d.address }}</td>
</tr>
{% endif %}
<tr>
<th class="w-33" scope="row">Company</th>
<td><abbr title="{{ d.company.extended_name }}">{{ d.company }}</abbr></td>
</tr>
<tr>
<th scope="row">Era</th>
<td>{{ d.era }}</td>
</tr>
<tr>
<th scope="row">Length</th>
<td>{{ d.consist_item.all | length }}</td>
</tr>
</tbody>
</table>
<div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{{ d.get_absolute_url }}">Show all data</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:consist_consist_change' d.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
{% 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">

View File

@@ -1,5 +1,4 @@
{% extends "cards.html" %} {% extends "cards.html" %}
{% 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">

View File

@@ -1,50 +1,7 @@
{% extends "cards.html" %} {% 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" style="position: relative;">
<strong>{{ d.name }}</strong>
</p>
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Manufacturer</th>
</tr>
</thead>
<tbody class="table-group-divider">
{% if d.logo %}
<tr>
<th class="w-33" scope="row">Logo</th>
<td><img style="max-height: 48px" src="{{ d.logo.url }}" /></td>
</tr>
{% endif %}
{% if d.website %}
<tr>
<th class="w-33" scope="row">Website</th>
<td><a href="{{ d.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a></td>
</tr>
{% endif %}
<tr>
<th class="w-33" 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="manufacturer" search=d.slug %}">Show all rolling stock</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_manufacturer_change' d.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}
{% block pagination %} {% block pagination %}
{% if data.has_other_pages %} {% if data.has_other_pages %}
{% with data.0.category as c %} {% with data.0.item.category as c %}
<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 %}

View File

@@ -43,19 +43,17 @@
<section class="py-4 text-start container"> <section class="py-4 text-start container">
<div class="row"> <div class="row">
<div class="mx-auto"> <div class="mx-auto">
<nav> <nav class="nav nav-tabs flex-column flex-md-row mb-2" id="nav-tab">
<div class="nav nav-tabs" id="nav-tab" role="tablist">
<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 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 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 data</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 rolling_stock.document.count > 0 or rolling_stock.decoder.document.count > 0 %}<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 rolling_stock_journal.count > 0 %}<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 %}
</div>
</nav> </nav>
<div class="tab-content" id="nav-tabContent"> <div class="tab-content" id="nav-tabContent">
<div class="tab-pane fade 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">
<thead> <thead>
<tr> <tr>
@@ -136,7 +134,7 @@
</table> </table>
{% endif %} {% endif %}
</div> </div>
<div class="tab-pane fade" id="nav-model" role="tabpanel" aria-labelledby="nav-model-tab"> <div class="tab-pane" id="nav-model" role="tabpanel" aria-labelledby="nav-model-tab">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@@ -172,7 +170,7 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
{% if rolling_stock_properties %} {% if properties %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@@ -180,7 +178,7 @@
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
{% for p in rolling_stock_properties %} {% for p in properties %}
<tr> <tr>
<th class="w-33" scope="row">{{ p.property }}</th> <th class="w-33" scope="row">{{ p.property }}</th>
<td>{{ p.value }}</td> <td>{{ p.value }}</td>
@@ -190,7 +188,7 @@
</table> </table>
{% endif %} {% endif %}
</div> </div>
<div class="tab-pane fade" id="nav-class" role="tabpanel" aria-labelledby="nav-class-tab"> <div class="tab-pane" id="nav-class" role="tabpanel" aria-labelledby="nav-class-tab">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@@ -242,7 +240,7 @@
</table> </table>
{% endif %} {% endif %}
</div> </div>
<div class="tab-pane fade" 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>
<tr> <tr>
@@ -277,8 +275,8 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="tab-pane fade" id="nav-documents" role="tabpanel" aria-labelledby="nav-documents-tab"> <div class="tab-pane" id="nav-documents" role="tabpanel" aria-labelledby="nav-documents-tab">
{% if rolling_stock.document.count > 0 %} {% if documents %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@@ -286,7 +284,7 @@
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
{% for d in rolling_stock.document.all %} {% for d in documents.all %}
<tr> <tr>
<td class="w-33">{{ d.description }}</td> <td class="w-33">{{ d.description }}</td>
<td><a href="{{ d.file.url }}" target="_blank">{{ d.filename }}</a></td> <td><a href="{{ d.file.url }}" target="_blank">{{ d.filename }}</a></td>
@@ -296,7 +294,7 @@
</tbody> </tbody>
</table> </table>
{% endif %} {% endif %}
{% if rolling_stock.decoder.document.count > 0 %} {% if decoder_documents %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@@ -304,7 +302,7 @@
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
{% for d in rolling_stock.decoder.document.all %} {% for d in decoder_documents.all %}
<tr> <tr>
<td class="w-33">{{ d.description }}</td> <td class="w-33">{{ d.description }}</td>
<td><a href="{{ d.file.url }}" target="_blank">{{ d.filename }}</a></td> <td><a href="{{ d.file.url }}" target="_blank">{{ d.filename }}</a></td>
@@ -315,7 +313,7 @@
</table> </table>
{% endif %} {% endif %}
</div> </div>
<div class="tab-pane fade" id="nav-journal" role="tabpanel" aria-labelledby="nav-journal-tab"> <div class="tab-pane" id="nav-journal" role="tabpanel" aria-labelledby="nav-journal-tab">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@@ -323,7 +321,7 @@
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
{% for j in rolling_stock_journal %} {% for j in journal %}
<tr> <tr>
<th class="w-33" scope="row">{{ j.date }}</th> <th class="w-33" scope="row">{{ j.date }}</th>
<td>{{ j.log | safe }}</a></td> <td>{{ j.log | safe }}</a></td>
@@ -332,7 +330,7 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="tab-pane fade" 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> </div>

View File

@@ -1,45 +1,4 @@
{% extends "cards.html" %} {% 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">Scale</th>
</tr>
</thead>
<tbody>
<tr>
<th class="w-33" scope="row">Name</th>
<td>{{ d.scale }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Ratio</th>
<td>{{ d.ratio }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Gauge</th>
<td>{{ d.gauge }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Tracks</th>
<td>{{ d.tracks }}</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="scale" search=d.slug %}">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 %} {% block pagination %}
{% if data.has_other_pages %} {% if data.has_other_pages %}
<nav aria-label="Page navigation example"> <nav aria-label="Page navigation example">

View File

@@ -1,44 +1,11 @@
{% extends "cards.html" %} {% 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 class="table-group-divider">
<tr>
<th class="w-33" scope="row">Type</th>
<td>{{ d.type }}</td>
</tr>
<tr>
<th class="w-33" 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.slug %}">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 %} {% 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 'scales_pagination' page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a> <a class="page-link" href="{% url 'types_pagination' page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -54,13 +21,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 'scales_pagination' page=i %}#main-content">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'types_pagination' page=i %}#main-content">{{ 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 'scales_pagination' page=data.next_page_number %}#main-content" tabindex="-1">Next</a> <a class="page-link" href="{% url 'types_pagination' page=data.next_page_number %}#main-content" tabindex="-1">Next</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">

View File

@@ -18,7 +18,7 @@ from portal.views import (
) )
urlpatterns = [ urlpatterns = [
path("", GetData.as_view(), name="index"), path("", GetData.as_view(template="home.html"), name="index"),
path("roster", GetRoster.as_view(), name="roster"), path("roster", GetRoster.as_view(), name="roster"),
path("roster/<int:page>", GetRoster.as_view(), name="roster_pagination"), path("roster/<int:page>", GetRoster.as_view(), name="roster_pagination"),
path( path(
@@ -26,9 +26,15 @@ urlpatterns = [
GetFlatpage.as_view(), GetFlatpage.as_view(),
name="flatpage", name="flatpage",
), ),
path("consists", Consists.as_view(), name="consists"),
path( path(
"consists/<int:page>", Consists.as_view(), name="consists_pagination" "consists",
Consists.as_view(template="consists.html"),
name="consists"
),
path(
"consists/<int:page>",
Consists.as_view(template="consists.html"),
name="consists_pagination"
), ),
path("consist/<uuid:uuid>", GetConsist.as_view(), name="consist"), path("consist/<uuid:uuid>", GetConsist.as_view(), name="consist"),
path( path(
@@ -36,28 +42,56 @@ urlpatterns = [
GetConsist.as_view(), GetConsist.as_view(),
name="consist_pagination", name="consist_pagination",
), ),
path("companies", Companies.as_view(), name="companies"), path(
"companies",
Companies.as_view(template="companies.html"),
name="companies"
),
path( path(
"companies/<int:page>", "companies/<int:page>",
Companies.as_view(), Companies.as_view(template="companies.html"),
name="companies_pagination", name="companies_pagination",
), ),
path( path(
"manufacturers/<str:category>", "manufacturers/<str:category>",
Manufacturers.as_view(), Manufacturers.as_view(template="manufacturers.html"),
name="manufacturers" name="manufacturers"
), ),
path( path(
"manufacturers/<str:category>/<int:page>", "manufacturers/<str:category>/<int:page>",
Manufacturers.as_view(), Manufacturers.as_view(template="manufacturers.html"),
name="manufacturers_pagination", name="manufacturers_pagination",
), ),
path("scales", Scales.as_view(), name="scales"), path(
path("scales/<int:page>", Types.as_view(), name="scales_pagination"), "scales",
path("types", Types.as_view(), name="types"), Scales.as_view(template="scales.html"),
path("types/<int:page>", Types.as_view(), name="types_pagination"), name="scales"
path("bookshelf/books", Books.as_view(), name="books"), ),
path("bookshelf/books/<int:page>", Books.as_view(), name="books_pagination"), path(
"scales/<int:page>",
Scales.as_view(template="scales.html"),
name="scales_pagination"
),
path(
"types",
Types.as_view(template="types.html"),
name="types"
),
path(
"types/<int:page>",
Types.as_view(template="types.html"),
name="types_pagination"
),
path(
"bookshelf/books",
Books.as_view(template="bookshelf/books.html"),
name="books"
),
path(
"bookshelf/books/<int:page>",
Books.as_view(template="bookshelf/books.html"),
name="books_pagination"
),
path("bookshelf/book/<uuid:uuid>", GetBook.as_view(), name="book"), path("bookshelf/book/<uuid:uuid>", GetBook.as_view(), name="book"),
path( path(
"search", "search",

View File

@@ -5,6 +5,7 @@ from urllib.parse import unquote
from django.views import View from django.views import View
from django.http import Http404, HttpResponseBadRequest from django.http import Http404, HttpResponseBadRequest
from django.db.utils import OperationalError, ProgrammingError
from django.db.models import Q from django.db.models import Q
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
@@ -15,11 +16,17 @@ 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 bookshelf.models import Book from bookshelf.models import Book
from metadata.models import Company, Manufacturer, Scale, RollingStockType, Tag from metadata.models import (
Company, Manufacturer, Scale, DecoderDocument, RollingStockType, Tag
)
def order_by_fields(): def order_by_fields():
try:
order_by = get_site_conf().items_ordering order_by = get_site_conf().items_ordering
except (OperationalError, ProgrammingError):
order_by = "type"
fields = [ fields = [
"rolling_class__type", "rolling_class__type",
"rolling_class__company", "rolling_class__company",
@@ -36,15 +43,22 @@ def order_by_fields():
class GetData(View): class GetData(View):
def __init__(self): title = "Home"
self.title = "Home" template = "roster.html"
self.template = "home.html" item_type = "rolling_stock"
self.data = RollingStock.objects.order_by(*order_by_fields()) queryset = RollingStock.objects.order_by(*order_by_fields())
def get(self, request, page=1): def get(self, request, page=1):
site_conf = get_site_conf() site_conf = get_site_conf()
paginator = Paginator(self.data, site_conf.items_per_page) data = []
for item in self.queryset:
data.append({
"type": self.item_type,
"item": item
})
paginator = Paginator(data, site_conf.items_per_page)
data = paginator.get_page(page) data = paginator.get_page(page)
page_range = paginator.get_elided_page_range( page_range = paginator.get_elided_page_range(
data.number, on_each_side=2, on_ends=1 data.number, on_each_side=2, on_ends=1
@@ -55,6 +69,7 @@ class GetData(View):
self.template, self.template,
{ {
"title": self.title, "title": self.title,
"type": self.item_type,
"data": data, "data": data,
"matches": paginator.count, "matches": paginator.count,
"page_range": page_range, "page_range": page_range,
@@ -63,10 +78,9 @@ class GetData(View):
class GetRoster(GetData): class GetRoster(GetData):
def __init__(self): title = "Rolling stock"
self.title = "Rolling stock" item_type = "rolling_stock"
self.template = "roster.html" queryset = RollingStock.objects.order_by(*order_by_fields())
self.data = RollingStock.objects.order_by(*order_by_fields())
class SearchRoster(View): class SearchRoster(View):
@@ -111,20 +125,50 @@ class SearchRoster(View):
else: else:
raise Http404 raise Http404
# FIXME duplicated code!
data = []
rolling_stock = ( rolling_stock = (
RollingStock.objects.filter(query) RollingStock.objects.filter(query)
.distinct() .distinct()
.order_by(*order_by_fields()) .order_by(*order_by_fields())
) )
matches = rolling_stock.count() for item in rolling_stock:
data.append({
"type": "rolling_stock",
"item": item
})
if _filter is None:
consists = (
Consist.objects.filter(
Q(
Q(identifier__icontains=search)
| Q(company__name__icontains=search)
)
)
.distinct()
)
for item in consists:
data.append({
"type": "consist",
"item": item
})
books = (
Book.objects.filter(title__icontains=search)
.distinct()
)
for item in books:
data.append({
"type": "book",
"item": item
})
paginator = Paginator(rolling_stock, site_conf.items_per_page) paginator = Paginator(data, site_conf.items_per_page)
rolling_stock = paginator.get_page(page) data = paginator.get_page(page)
page_range = paginator.get_elided_page_range( page_range = paginator.get_elided_page_range(
rolling_stock.number, on_each_side=2, on_ends=1 data.number, on_each_side=2, on_ends=1
) )
return rolling_stock, matches, page_range return data, paginator.count, page_range
def split_search(self, search): def split_search(self, search):
search = search.strip().split(":") search = search.strip().split(":")
@@ -149,7 +193,7 @@ class SearchRoster(View):
encoded_search = base64.b64encode( encoded_search = base64.b64encode(
search.encode()).decode() search.encode()).decode()
_filter, keyword = self.split_search(search) _filter, keyword = self.split_search(search)
rolling_stock, matches, page_range = self.run_search( data, matches, page_range = self.run_search(
request, keyword, _filter, page request, keyword, _filter, page
) )
@@ -160,8 +204,8 @@ class SearchRoster(View):
"title": "Search: \"{}\"".format(search), "title": "Search: \"{}\"".format(search),
"search": search, "search": search,
"encoded_search": encoded_search, "encoded_search": encoded_search,
"data": data,
"matches": matches, "matches": matches,
"data": rolling_stock,
"page_range": page_range, "page_range": page_range,
}, },
) )
@@ -200,15 +244,41 @@ class GetRosterFiltered(View):
.distinct() .distinct()
.order_by(*order_by_fields()) .order_by(*order_by_fields())
) )
matches = rolling_stock.count()
paginator = Paginator(rolling_stock, site_conf.items_per_page) data = []
rolling_stock = paginator.get_page(page) for item in rolling_stock:
data.append({
"type": "rolling_stock",
"item": item
})
if _filter == "tag":
consists = (
Consist.objects.filter(query)
.distinct()
)
for item in consists:
data.append({
"type": "consist",
"item": item
})
books = (
Book.objects.filter(query)
.distinct()
)
for item in books:
data.append({
"type": "book",
"item": item
})
paginator = Paginator(data, site_conf.items_per_page)
data = paginator.get_page(page)
page_range = paginator.get_elided_page_range( page_range = paginator.get_elided_page_range(
rolling_stock.number, on_each_side=2, on_ends=1 data.number, on_each_side=2, on_ends=1
) )
return rolling_stock, title, matches, page_range return data, title, paginator.count, page_range
def get(self, request, search, _filter, page=1): def get(self, request, search, _filter, page=1):
data, title, matches, page_range = self.run_filter( data, title, matches, page_range = self.run_filter(
@@ -223,8 +293,8 @@ class GetRosterFiltered(View):
_filter.capitalize(), title), _filter.capitalize(), title),
"search": search, "search": search,
"filter": _filter, "filter": _filter,
"matches": matches,
"data": data, "data": data,
"matches": matches,
"page_range": page_range, "page_range": page_range,
}, },
) )
@@ -237,23 +307,28 @@ class GetRollingStock(View):
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise Http404 raise Http404
class_properties = ( # FIXME there's likely a better and more efficient way of doing this
rolling_stock.rolling_class.property.all() # but keeping KISS for now
if request.user.is_authenticated decoder_documents = []
else rolling_stock.rolling_class.property.filter( if request.user.is_authenticated:
class_properties = rolling_stock.rolling_class.property.all()
properties = rolling_stock.property.all()
documents = rolling_stock.document.all()
journal = rolling_stock.journal.all()
if rolling_stock.decoder:
decoder_documents = rolling_stock.decoder.document.all()
else:
class_properties = rolling_stock.rolling_class.property.filter(
property__private=False property__private=False
) )
properties = rolling_stock.property.filter(
property__private=False
) )
rolling_stock_properties = ( documents = rolling_stock.document.filter(private=False)
rolling_stock.property.all() journal = rolling_stock.journal.filter(private=False)
if request.user.is_authenticated if rolling_stock.decoder:
else rolling_stock.property.filter(property__private=False) decoder_documents = rolling_stock.decoder.document.filter(
) private=False
rolling_stock_journal = (
rolling_stock.journal.all()
if request.user.is_authenticated
else rolling_stock.journal.filter(private=False)
) )
return render( return render(
@@ -263,17 +338,18 @@ class GetRollingStock(View):
"title": rolling_stock, "title": rolling_stock,
"rolling_stock": rolling_stock, "rolling_stock": rolling_stock,
"class_properties": class_properties, "class_properties": class_properties,
"rolling_stock_properties": rolling_stock_properties, "properties": properties,
"rolling_stock_journal": rolling_stock_journal, "decoder_documents": decoder_documents,
"documents": documents,
"journal": journal,
}, },
) )
class Consists(GetData): class Consists(GetData):
def __init__(self): title = "Consists"
self.title = "Consists" item_type = "consist"
self.template = "consists.html" queryset = Consist.objects.all()
self.data = Consist.objects.all()
class GetConsist(View): class GetConsist(View):
@@ -283,15 +359,15 @@ class GetConsist(View):
consist = Consist.objects.get(uuid=uuid) consist = Consist.objects.get(uuid=uuid)
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise Http404 raise Http404
rolling_stock = [ data = [{
RollingStock.objects.get(uuid=r.rolling_stock_id) for r in "type": "rolling_stock",
consist.consist_item.all() "item": RollingStock.objects.get(uuid=r.rolling_stock_id)
] } for r in consist.consist_item.all()]
paginator = Paginator(rolling_stock, site_conf.items_per_page) paginator = Paginator(data, site_conf.items_per_page)
rolling_stock = paginator.get_page(page) data = paginator.get_page(page)
page_range = paginator.get_elided_page_range( page_range = paginator.get_elided_page_range(
rolling_stock.number, on_each_side=2, on_ends=1 data.number, on_each_side=2, on_ends=1
) )
return render( return render(
@@ -300,52 +376,47 @@ class GetConsist(View):
{ {
"title": consist, "title": consist,
"consist": consist, "consist": consist,
"data": rolling_stock, "data": data,
"page_range": page_range, "page_range": page_range,
}, },
) )
class Manufacturers(GetData): class Manufacturers(GetData):
def __init__(self): title = "Manufacturers"
self.title = "Manufacturers" item_type = "manufacturer"
self.template = "manufacturers.html" queryset = None # Set via method get
self.data = None # Set via method get
# overload get method to filter by category # overload get method to filter by category
def get(self, request, category, page=1): def get(self, request, category, page=1):
if category not in ("real", "model"): if category not in ("real", "model"):
raise Http404 raise Http404
self.data = Manufacturer.objects.filter(category=category) self.queryset = Manufacturer.objects.filter(category=category)
return super().get(request, page) return super().get(request, page)
class Companies(GetData): class Companies(GetData):
def __init__(self): title = "Companies"
self.title = "Companies" item_type = "company"
self.template = "companies.html" queryset = Company.objects.all()
self.data = Company.objects.all()
class Scales(GetData): class Scales(GetData):
def __init__(self): title = "Scales"
self.title = "Scales" item_type = "scale"
self.template = "scales.html" queryset = Scale.objects.all()
self.data = Scale.objects.all()
class Types(GetData): class Types(GetData):
def __init__(self): title = "Types"
self.title = "Types" item_type = "rolling_stock_type"
self.template = "types.html" queryset = RollingStockType.objects.all()
self.data = RollingStockType.objects.all()
class Books(GetData): class Books(GetData):
def __init__(self): title = "Books"
self.title = "Books" item_type = "book"
self.template = "bookshelf/books.html" queryset = Book.objects.all()
self.data = Book.objects.all()
class GetBook(View): class GetBook(View):
@@ -374,12 +445,13 @@ class GetBook(View):
class GetFlatpage(View): class GetFlatpage(View):
def get(self, request, flatpage): def get(self, request, flatpage):
try: try:
flatpage = Flatpage.objects.get( flatpage = Flatpage.objects.get(path=flatpage)
Q(Q(path=flatpage) & Q(published=True))
)
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise Http404 raise Http404
if not request.user.is_authenticated:
flatpage = flatpage.filter(published=False)
return render( return render(
request, request,
"flatpages/flatpage.html", "flatpages/flatpage.html",

View File

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

View File

@@ -1,10 +1,10 @@
from django.contrib import admin from django.contrib import admin
from django.db.utils import OperationalError from django.db.utils import OperationalError, ProgrammingError
from portal.utils import get_site_conf from portal.utils import get_site_conf
try: try:
site_name = get_site_conf().site_name site_name = get_site_conf().site_name
except OperationalError: except (OperationalError, ProgrammingError):
site_name = "Train Assets Manager" site_name = "Train Assets Manager"
admin.site.site_header = site_name admin.site.site_header = site_name

View File

@@ -14,6 +14,7 @@ class Document(models.Model):
null=True, null=True,
blank=True, blank=True,
) )
private = models.BooleanField(default=False)
class Meta: class Meta:
abstract = True abstract = True

View File

@@ -64,6 +64,7 @@ class RollingStockDocumentAdmin(admin.ModelAdmin):
"__str__", "__str__",
"rolling_stock", "rolling_stock",
"description", "description",
"private",
"download", "download",
) )
search_fields = ( search_fields = (

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.2.5 on 2023-10-06 19:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("roster", "0018_rename_sku_rollingstock_item_number"),
]
operations = [
migrations.AddField(
model_name="rollingstockdocument",
name="private",
field=models.BooleanField(default=False),
),
]