Work in progress to implement magazines and issues UI

This commit is contained in:
2025-12-10 15:41:00 +01:00
parent cd1369e9c9
commit 0688725326
8 changed files with 333 additions and 13 deletions

View File

@@ -81,6 +81,15 @@ def book_image_upload(instance, filename):
)
def magazine_image_upload(instance, filename):
return os.path.join(
"images",
"magazines",
str(instance.uuid),
filename
)
class BaseBookImage(Image):
book = models.ForeignKey(
BaseBook, on_delete=models.CASCADE, related_name="image"
@@ -163,7 +172,7 @@ class Magazine(BaseModel):
ISBN = models.CharField(max_length=17, blank=True) # 13 + dashes
image = models.ImageField(
blank=True,
upload_to=book_image_upload,
upload_to=magazine_image_upload,
storage=DeduplicatedStorage,
)
language = models.CharField(
@@ -192,8 +201,8 @@ class Magazine(BaseModel):
def get_absolute_url(self):
return reverse(
"bookshelf_item",
kwargs={"selector": "magazine", "uuid": self.uuid}
"magazine",
kwargs={"uuid": self.uuid}
)
@@ -224,3 +233,12 @@ class MagazineIssue(BaseBook):
def preview(self):
return self.image.first().image_thumbnail(100)
def get_absolute_url(self):
return reverse(
"issue",
kwargs={
"uuid": self.uuid,
"magazine": self.magazine.uuid
}
)

View File

@@ -18,6 +18,8 @@
{% include "cards/consist.html" %}
{% elif d.type == "manufacturer" %}
{% include "cards/manufacturer.html" %}
{% elif d.type == "magazine" or d.type == "issue" %}
{% include "cards/magazine.html" %}
{% elif d.type == "book" or d.type == "catalog" %}
{% include "cards/book.html" %}
{% endif %}

View File

@@ -24,8 +24,7 @@
<thead>
<tr>
<th colspan="2" scope="row">
{% if d.type == "catalog" %}Catalog
{% elif d.type == "book" %}Book{% endif %}
{{ d.type | capfirst }}
<div class="float-end">
{% if not d.item.published %}
<span class="badge text-bg-warning">Unpublished</span>
@@ -53,7 +52,7 @@
</tr>
<tr>
<th class="w-33" scope="row">Publisher</th>
<td>{{ d.item.publisher }}</td>
<td><img src="{{ d.item.publisher.country.flag }}" alt="{{ d.item.publisher.country }}"> {{ d.item.publisher }}</td>
</tr>
{% endif %}
<tr>

View File

@@ -63,7 +63,7 @@
</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 %}
{% 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>

View File

@@ -0,0 +1,73 @@
{% load static %}
<div class="col">
<div class="card shadow-sm">
{% if d.type == "magazine" %}
<a href="{{ d.item.get_absolute_url }}">
{% if d.item.image and d.type == "magazine" %}
<img class="card-img-top" src="{{ d.item.image.url }}" alt="{{ d.item }}">
{% elif d.item.issue.first.image.exists %}
{% with d.item.issue.first as i %}
<img class="card-img-top" src="{{ i.image.first.image.url }}" alt="{{ d.item }}">
{% endwith %}
{% else %}
<!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) -->
<img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d.item }}">
{% endif %}
</a>
{% endif %}
{% if d.type == "issue" %}
<a href="{{ d.item.get_absolute_url }}">
{% if d.item.image.exists %}
<img class="card-img-top" src="{{ d.item.image.first.image.url }}" alt="{{ d.item }}">
{% else %}
<!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) -->
<img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d.item }}">
{% endif %}
</a>
{% 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">
{{ d.type | capfirst }}
<div class="float-end">
{% if not d.item.published %}
<span class="badge text-bg-warning">Unpublished</span>
{% endif %}
</div>
</th>
</tr>
</thead>
<tbody class="table-group-divider">
<tr>
<th class="w-33" scope="row">Publisher</th>
<td><img src="{{ d.item.publisher.country.flag }}" alt="{{ d.item.publisher.country }}"> {{ d.item.publisher }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Language</th>
<td>{{ d.item.get_language_display }}</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_magazine_change' d.item.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,122 @@
{% extends "cards.html" %}
{% block header %}
{% if magazine.tags.all %}
<p><small>Tags:</small>
{% for t in magazine.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>
{% if not magazine.published %}
<span class="badge text-bg-warning">Unpublished</span> |
{% endif %}
<small class="text-body-secondary">Updated {{ magazine.updated_time | date:"M d, Y H:i" }}</small>
{% endif %}
{% endblock %}
{% block carousel %}
{% if magazine.image %}
<div class="row pb-4">
<div id="carouselControls" class="carousel carousel-dark slide" data-bs-ride="carousel">
<div class="carousel-inner">
<div class="carousel-item active">
<img src="{{ magazine.image.url }}" class="d-block w-100 rounded img-thumbnail" alt="magazine cover">
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block pagination %}
{% if data.has_other_pages %}
<nav aria-label="Page navigation">
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
{% if data.has_previous %}
<li class="page-item">
<a class="page-link" href="{% url 'magazine_pagination' uuid=magazine.uuid page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link"><i class="bi bi-chevron-left"></i></span>
</li>
{% endif %}
{% for i in page_range %}
{% if data.number == i %}
<li class="page-item active">
<span class="page-link">{{ i }}</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 'magazine_pagination' uuid=magazine.uuid page=i %}#main-content">{{ i }}</a></li>
{% endif %}
{% endif %}
{% endfor %}
{% if data.has_next %}
<li class="page-item">
<a class="page-link" href="{% url 'magazine_pagination' uuid=magazine.uuid page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link"><i class="bi bi-chevron-right"></i></span>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% endblock %}
{% block extra_content %}
<section class="py-4 text-start container">
<div class="row">
<div class="mx-auto">
<nav class="nav nav-tabs d-none d-lg-flex flex-row mb-2" 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>
</nav>
<select class="form-select d-lg-none mb-2" id="tabSelector" aria-label="Tab selector">
<option value="nav-summary" selected>Summary</option>
</select>
<div class="tab-content" id="nav-tabContent">
<div class="tab-pane show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab">
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">
Magazine
</th>
</tr>
</thead>
<tbody class="table-group-divider">
<tr>
<th class="w-33" scope="row">Company</th>
<td>
</td>
</tr>
<tr>
<th scope="row">Era</th>
<td>{{ magazine.era }}</td>
</tr>
{% if magazine.description %}
<tr>
<th scope="row">Description</th>
<td>{{ magazine.description | safe }}</td>
</tr>
{% endif %}
<tr>
<th scope="row">Length</th>
<td>{{ magazine.length }}</td>
</tr>
<tr>
<th scope="row">Composition</th>
<td>{% for t in magazine.get_type_count %}{{ t.count }}x {{ t.type }} {{t.category }}{% if not forloop.last %} &raquo; {% endif %}{% endfor %}</td>
</tr>
</tbody>
</table>
</div>
</div>
<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:bookshelf_magazine_change' magazine.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</section>
{% endblock %}

View File

@@ -15,6 +15,9 @@ from portal.views import (
Types,
Books,
Catalogs,
Magazines,
GetMagazine,
GetMagazineIssue,
GetBookCatalog,
SearchObjects,
)
@@ -98,6 +101,31 @@ urlpatterns = [
Books.as_view(),
name="books_pagination"
),
path(
"bookshelf/magazine/<uuid:uuid>",
GetMagazine.as_view(),
name="magazine"
),
path(
"bookshelf/magazine/<uuid:uuid>/page/<int:page>",
GetMagazine.as_view(),
name="magazine_pagination",
),
path(
"bookshelf/magazine/<uuid:magazine>/issue/<uuid:uuid>",
GetMagazineIssue.as_view(),
name="issue",
),
path(
"bookshelf/magazines",
Magazines.as_view(),
name="magazines"
),
path(
"bookshelf/magazines/page/<int:page>",
Magazines.as_view(),
name="magazines_pagination"
),
path(
"bookshelf/<str:selector>/<uuid:uuid>",
GetBookCatalog.as_view(),

View File

@@ -16,7 +16,7 @@ from portal.utils import get_site_conf
from portal.models import Flatpage
from roster.models import RollingStock
from consist.models import Consist
from bookshelf.models import Book, Catalog
from bookshelf.models import Book, Catalog, Magazine, MagazineIssue
from metadata.models import (
Company,
Manufacturer,
@@ -73,7 +73,8 @@ class GetData(View):
.filter(self.filter)
)
def get(self, request, page=1):
def get(self, request, filter=Q(), page=1):
self.filter = filter
data = []
for item in self.get_data(request):
data.append({"type": self.item_type, "item": item})
@@ -491,7 +492,8 @@ class Manufacturers(GetData):
def get_data(self, request):
return (
Manufacturer.objects.filter(self.filter).annotate(
Manufacturer.objects.filter(self.filter)
.annotate(
num_rollingstock=(
Count(
"rollingstock",
@@ -592,9 +594,7 @@ class Scales(GetData):
num_consists=Count(
"consist",
filter=Q(
consist__in=Consist.objects.get_published(
request.user
)
consist__in=Consist.objects.get_published(request.user)
),
distinct=True,
),
@@ -637,6 +637,84 @@ class Catalogs(GetData):
return Catalog.objects.get_published(request.user).all()
class Magazines(GetData):
title = "Magazines"
item_type = "magazine"
def get_data(self, request):
return (
Magazine.objects.get_published(request.user)
.all()
.annotate(
num_issues=Count(
"issue",
filter=Q(
issue__in=(
MagazineIssue.objects.get_published(request.user)
)
),
)
)
)
class GetMagazine(View):
def get(self, request, uuid, page=1):
try:
magazine = Magazine.objects.get_published(request.user).get(
uuid=uuid
)
except ObjectDoesNotExist:
raise Http404
data = [
{
"type": "issue",
"item": i,
}
for i in magazine.issue.get_published(request.user).all()
]
paginator = Paginator(data, get_items_per_page())
data = paginator.get_page(page)
page_range = paginator.get_elided_page_range(
data.number, on_each_side=1, on_ends=1
)
return render(
request,
"magazine.html",
{
"title": magazine,
"magazine": magazine,
"data": data,
"page_range": page_range,
},
)
class GetMagazineIssue(View):
def get(self, request, uuid, magazine, page=1):
try:
issue = MagazineIssue.objects.get_published(request.user).get(
uuid=uuid,
magazine__uuid=magazine,
)
except ObjectDoesNotExist:
raise Http404
properties = issue.property.get_public(request.user)
documents = issue.document.get_public(request.user)
return render(
request,
"bookshelf/book.html",
{
"title": issue,
"book": issue,
"documents": documents,
"properties": properties,
"type": "magazineissue",
},
)
class GetBookCatalog(View):
def get_object(self, request, uuid, selector):
if selector == "book":