Merge pull request #40 from daniviga/catalogue

Introduce the concept of catalogs, improve books and code refactoring
This commit is contained in:
2024-12-22 22:13:30 +01:00
committed by GitHub
27 changed files with 560 additions and 300 deletions

View File

@@ -13,7 +13,7 @@ jobs:
strategy: strategy:
max-parallel: 2 max-parallel: 2
matrix: matrix:
python-version: ['3.10', '3.11', '3.12'] python-version: ['3.12', '3.13']
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3

View File

@@ -1,35 +1,58 @@
from django.contrib import admin from django.contrib import admin
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
from bookshelf.models import BookProperty, BookImage, Book, Author, Publisher from bookshelf.models import (
BaseBookProperty,
BaseBookImage,
BaseBookDocument,
Book,
Author,
Publisher,
Catalog,
)
class BookImageInline(SortableInlineAdminMixin, admin.TabularInline): class BookImageInline(SortableInlineAdminMixin, admin.TabularInline):
model = BookImage model = BaseBookImage
min_num = 0 min_num = 0
extra = 0 extra = 0
readonly_fields = ("image_thumbnail",) readonly_fields = ("image_thumbnail",)
classes = ["collapse"] classes = ["collapse"]
verbose_name = "Image"
class BookDocInline(admin.TabularInline):
model = BaseBookDocument
min_num = 0
extra = 0
classes = ["collapse"]
class BookPropertyInline(admin.TabularInline): class BookPropertyInline(admin.TabularInline):
model = BookProperty model = BaseBookProperty
min_num = 0 min_num = 0
extra = 0 extra = 0
autocomplete_fields = ("property",) autocomplete_fields = ("property",)
verbose_name = "Property"
verbose_name_plural = "Properties"
@admin.register(Book) @admin.register(Book)
class BookAdmin(SortableAdminBase, admin.ModelAdmin): class BookAdmin(SortableAdminBase, admin.ModelAdmin):
inlines = (BookImageInline, BookPropertyInline,) inlines = (
BookPropertyInline,
BookImageInline,
BookDocInline,
)
list_display = ( list_display = (
"title", "title",
"published",
"get_authors", "get_authors",
"get_publisher", "get_publisher",
"publication_year", "publication_year",
"number_of_pages" "number_of_pages",
"published",
) )
autocomplete_fields = ("authors", "publisher")
readonly_fields = ("creation_time", "updated_time") readonly_fields = ("creation_time", "updated_time")
search_fields = ("title", "publisher__name", "authors__last_name") search_fields = ("title", "publisher__name", "authors__last_name")
list_filter = ("publisher__name", "authors") list_filter = ("publisher__name", "authors")
@@ -77,7 +100,10 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin):
@admin.register(Author) @admin.register(Author)
class AuthorAdmin(admin.ModelAdmin): class AuthorAdmin(admin.ModelAdmin):
search_fields = ("first_name", "last_name",) search_fields = (
"first_name",
"last_name",
)
list_filter = ("last_name",) list_filter = ("last_name",)
@@ -85,3 +111,58 @@ class AuthorAdmin(admin.ModelAdmin):
class PublisherAdmin(admin.ModelAdmin): class PublisherAdmin(admin.ModelAdmin):
list_display = ("name", "country") list_display = ("name", "country")
search_fields = ("name",) search_fields = ("name",)
@admin.register(Catalog)
class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
inlines = (
BookPropertyInline,
BookImageInline,
BookDocInline,
)
list_display = (
"manufacturer",
"years",
"get_scales",
"published",
)
autocomplete_fields = ("manufacturer",)
readonly_fields = ("creation_time", "updated_time")
search_fields = ("manufacturer__name", "years", "scales__scale")
list_filter = ("manufacturer__name", "publication_year", "scales__scale")
fieldsets = (
(
None,
{
"fields": (
"published",
"manufacturer",
"years",
"scales",
"ISBN",
"language",
"number_of_pages",
"publication_year",
"description",
"purchase_date",
"notes",
"tags",
)
},
),
(
"Audit",
{
"classes": ("collapse",),
"fields": (
"creation_time",
"updated_time",
),
},
),
)
@admin.display(description="Scales")
def get_scales(self, obj):
return "/".join(s.scale for s in obj.scales.all())

View File

@@ -12,7 +12,7 @@ from django.conf import settings
def move_images(apps, schema_editor): def move_images(apps, schema_editor):
sys.stdout.write("\n Processing files. Please await...") sys.stdout.write("\n Processing files. Please await...")
for r in bookshelf.models.BookImage.objects.all(): for r in bookshelf.models.BaseBookImage.objects.all():
fname = os.path.basename(r.image.path) fname = os.path.basename(r.image.path)
new_image = bookshelf.models.book_image_upload(r, fname) new_image = bookshelf.models.book_image_upload(r, fname)
new_path = os.path.join(settings.MEDIA_ROOT, new_image) new_path = os.path.join(settings.MEDIA_ROOT, new_image)
@@ -31,19 +31,21 @@ class Migration(migrations.Migration):
("bookshelf", "0008_alter_author_options_alter_publisher_options"), ("bookshelf", "0008_alter_author_options_alter_publisher_options"),
] ]
# Migration is stale and shouldn't be used since model hes been heavily
# modified since then. Leaving it here for reference.
operations = [ operations = [
migrations.AlterField( # migrations.AlterField(
model_name="bookimage", # model_name="bookimage",
name="image", # name="image",
field=models.ImageField( # field=models.ImageField(
blank=True, # blank=True,
null=True, # null=True,
storage=ram.utils.DeduplicatedStorage, # storage=ram.utils.DeduplicatedStorage,
upload_to=bookshelf.models.book_image_upload, # upload_to=bookshelf.models.book_image_upload,
), # ),
), # ),
migrations.RunPython( # migrations.RunPython(
move_images, # move_images,
reverse_code=migrations.RunPython.noop # reverse_code=migrations.RunPython.noop
), # ),
] ]

View File

@@ -0,0 +1,141 @@
# Generated by Django 5.1.2 on 2024-11-27 16:35
import django.db.models.deletion
from django.db import migrations, models
def basebook_to_book(apps, schema_editor):
basebook = apps.get_model("bookshelf", "BaseBook")
book = apps.get_model("bookshelf", "Book")
for row in basebook.objects.all():
b = book.objects.create(
basebook_ptr=row,
title=row.old_title,
publisher=row.old_publisher,
)
b.authors.set(row.old_authors.all())
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0015_alter_book_authors"),
("metadata", "0019_alter_scale_gauge"),
]
operations = [
migrations.AlterModelOptions(
name="Book",
options={"ordering": ["creation_time"]},
),
migrations.RenameModel(
old_name="BookImage",
new_name="BaseBookImage",
),
migrations.RenameModel(
old_name="BookProperty",
new_name="BaseBookProperty",
),
migrations.RenameModel(
old_name="Book",
new_name="BaseBook",
),
migrations.RenameField(
model_name="basebook",
old_name="title",
new_name="old_title",
),
migrations.RenameField(
model_name="basebook",
old_name="authors",
new_name="old_authors",
),
migrations.RenameField(
model_name="basebook",
old_name="publisher",
new_name="old_publisher",
),
migrations.AlterModelOptions(
name="basebookimage",
options={"ordering": ["order"], "verbose_name_plural": "Images"},
),
migrations.CreateModel(
name="Book",
fields=[
(
"basebook_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="bookshelf.basebook",
),
),
("title", models.CharField(max_length=200)),
(
"authors",
models.ManyToManyField(
blank=True,
to="bookshelf.author"
),
),
(
"publisher",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="bookshelf.publisher"
),
),
],
options={
"ordering": ["title"],
},
),
migrations.RunPython(
basebook_to_book,
reverse_code=migrations.RunPython.noop
),
migrations.RemoveField(
model_name="basebook",
name="old_title",
),
migrations.RemoveField(
model_name="basebook",
name="old_authors",
),
migrations.RemoveField(
model_name="basebook",
name="old_publisher",
),
migrations.CreateModel(
name="Catalog",
fields=[
(
"basebook_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="bookshelf.basebook",
),
),
("years", models.CharField(max_length=12)),
(
"manufacturer",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="metadata.manufacturer",
),
),
("scales", models.ManyToManyField(to="metadata.scale")),
],
options={
"ordering": ["manufacturer", "publication_year"],
},
bases=("bookshelf.basebook",),
),
]

View File

@@ -0,0 +1,52 @@
# Generated by Django 5.1.2 on 2024-12-22 20:38
import django.db.models.deletion
import ram.utils
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0016_basebook_book_catalogue"),
]
operations = [
migrations.AlterModelOptions(
name="basebook",
options={},
),
migrations.CreateModel(
name="BaseBookDocument",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("description", models.CharField(blank=True, max_length=128)),
(
"file",
models.FileField(
storage=ram.utils.DeduplicatedStorage(), upload_to="files/"
),
),
("private", models.BooleanField(default=False)),
(
"book",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="document",
to="bookshelf.basebook",
),
),
],
options={
"unique_together": {("book", "file")},
},
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.1.4 on 2024-12-22 20:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0017_alter_basebook_options_basebookdocument"),
]
operations = [
migrations.AlterModelOptions(
name="basebookdocument",
options={"verbose_name_plural": "Documents"},
),
]

View File

@@ -9,7 +9,8 @@ from tinymce import models as tinymce
from metadata.models import Tag from metadata.models import Tag
from ram.utils import DeduplicatedStorage from ram.utils import DeduplicatedStorage
from ram.models import BaseModel, Image, PropertyInstance from ram.models import BaseModel, Image, Document, PropertyInstance
from metadata.models import Scale, Manufacturer
class Publisher(models.Model): class Publisher(models.Model):
@@ -38,10 +39,7 @@ class Author(models.Model):
return f"{self.last_name} {self.first_name[0]}." return f"{self.last_name} {self.first_name[0]}."
class Book(BaseModel): class BaseBook(BaseModel):
title = models.CharField(max_length=200)
authors = models.ManyToManyField(Author, blank=True)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
ISBN = models.CharField(max_length=17, blank=True) # 13 + dashes ISBN = models.CharField(max_length=17, blank=True) # 13 + dashes
language = models.CharField( language = models.CharField(
max_length=7, max_length=7,
@@ -56,18 +54,6 @@ class Book(BaseModel):
Tag, related_name="bookshelf", blank=True Tag, related_name="bookshelf", blank=True
) )
class Meta:
ordering = ["title"]
def __str__(self):
return self.title
def publisher_name(self):
return self.publisher.name
def get_absolute_url(self):
return reverse("book", kwargs={"uuid": self.uuid})
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
shutil.rmtree( shutil.rmtree(
os.path.join( os.path.join(
@@ -75,7 +61,7 @@ class Book(BaseModel):
), ),
ignore_errors=True ignore_errors=True
) )
super(Book, self).delete(*args, **kwargs) super(BaseBook, self).delete(*args, **kwargs)
def book_image_upload(instance, filename): def book_image_upload(instance, filename):
@@ -87,9 +73,9 @@ def book_image_upload(instance, filename):
) )
class BookImage(Image): class BaseBookImage(Image):
book = models.ForeignKey( book = models.ForeignKey(
Book, on_delete=models.CASCADE, related_name="image" BaseBook, on_delete=models.CASCADE, related_name="image"
) )
image = models.ImageField( image = models.ImageField(
upload_to=book_image_upload, upload_to=book_image_upload,
@@ -97,11 +83,68 @@ class BookImage(Image):
) )
class BookProperty(PropertyInstance): class BaseBookDocument(Document):
book = models.ForeignKey( book = models.ForeignKey(
Book, BaseBook, on_delete=models.CASCADE, related_name="document"
)
class Meta:
verbose_name_plural = "Documents"
unique_together = ("book", "file")
class BaseBookProperty(PropertyInstance):
book = models.ForeignKey(
BaseBook,
on_delete=models.CASCADE, on_delete=models.CASCADE,
null=False, null=False,
blank=False, blank=False,
related_name="property", related_name="property",
) )
class Book(BaseBook):
title = models.CharField(max_length=200)
authors = models.ManyToManyField(Author, blank=True)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
class Meta:
ordering = ["title"]
def __str__(self):
return self.title
def publisher_name(self):
return self.publisher.name
def get_absolute_url(self):
return reverse(
"bookshelf_item",
kwargs={"selector": "book", "uuid": self.uuid}
)
class Catalog(BaseBook):
manufacturer = models.ForeignKey(
Manufacturer,
on_delete=models.CASCADE,
)
years = models.CharField(max_length=12)
scales = models.ManyToManyField(Scale)
class Meta:
ordering = ["manufacturer", "publication_year"]
def __str__(self):
scales = self.get_scales
return "%s %s %s" % (self.manufacturer.name, self.years, scales)
def get_absolute_url(self):
return reverse(
"bookshelf_item",
kwargs={"selector": "catalog", "uuid": self.uuid}
)
@property
def get_scales(self):
return "/".join([s.scale for s in self.scales.all()])

View File

@@ -180,7 +180,7 @@
<li><a class="dropdown-item" href="{% url 'manufacturers' category='model' %}">Manufacturer</a></li> <li><a class="dropdown-item" href="{% url 'manufacturers' category='model' %}">Manufacturer</a></li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li class="ps-2 text-secondary">Prototype</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 'rolling_stock_types' %}">Type</a></li>
<li><a class="dropdown-item" href="{% url 'companies' %}">Company</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> <li><a class="dropdown-item" href="{% url 'manufacturers' category='real' %}">Manufacturer</a></li>
</ul> </ul>

View File

@@ -1,4 +1,5 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load dynamic_url %}
{% block header %} {% block header %}
{% if book.tags.all %} {% if book.tags.all %}
@@ -57,24 +58,39 @@
{{ book.description | safe }} {{ book.description | safe }}
<thead> <thead>
<tr> <tr>
{% if type == "catalog" %}
<th colspan="2" scope="row">Catalog</th>
{% elif type == "book" %}
<th colspan="2" scope="row">Book</th> <th colspan="2" scope="row">Book</th>
{% endif %}
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
{% if type == "catalog" %}
<tr>
<th class="w-33" scope="row">Manufacturer</th>
<td>{{ book.manufacturer }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Scales</th>
<td>{{ book.get_scales }}</td>
</tr>
{% elif type == "book" %}
<tr> <tr>
<th class="w-33" scope="row">Title</th> <th class="w-33" scope="row">Title</th>
<td>{{ book.title }}</td> <td>{{ book.title }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Authors</th> <th class="w-33" scope="row">Authors</th>
<td> <td>
<ul class="mb-0 list-unstyled">{% for a in book.authors.all %}<li>{{ a }}</li>{% endfor %}</ul> <ul class="mb-0 list-unstyled">{% for a in book.authors.all %}<li>{{ a }}</li>{% endfor %}</ul>
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Publisher</th> <th class="w-33" scope="row">Publisher</th>
<td>{{ book.publisher }}</td> <td>{{ book.publisher }}</td>
</tr> </tr>
{% endif %}
<tr> <tr>
<th scope="row">ISBN</th> <th scope="row">ISBN</th>
<td>{{ book.ISBN|default:"-" }}</td> <td>{{ book.ISBN|default:"-" }}</td>
@@ -120,7 +136,7 @@
</div> </div>
</div> </div>
<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:bookshelf_book_change' book.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% dynamic_admin_url 'bookshelf' type book.pk %}">Edit</a>{% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,40 +0,0 @@
{% extends "cards.html" %}
{% block pagination %}
{% if data.has_other_pages %}
<nav aria-label="Page navigation example">
<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 'books_pagination' 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 'books_pagination' page=i %}#main-content">{{ i }}</a></li>
{% endif %}
{% endif %}
{% endfor %}
{% if data.has_next %}
<li class="page-item">
<a class="page-link" href="{% url 'books_pagination' 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 %}

View File

@@ -4,7 +4,12 @@
Bookshelf Bookshelf
</a> </a>
<ul class="dropdown-menu" aria-labelledby="bookshelfDropdownMenuLink"> <ul class="dropdown-menu" aria-labelledby="bookshelfDropdownMenuLink">
{% if books_menu %}
<li><a class="dropdown-item" href="{% url 'books' %}">Books</a></li> <li><a class="dropdown-item" href="{% url 'books' %}">Books</a></li>
{% endif %}
{% if catalogs_menu %}
<li><a class="dropdown-item" href="{% url 'catalogs' %}">Catalogs</a></li>
{% endif %}
</ul> </ul>
</li> </li>
{% endif %} {% endif %}

View File

@@ -6,7 +6,7 @@
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3"> <div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3">
{% block cards %} {% block cards %}
{% for d in data %} {% for d in data %}
{% if d.type == "rolling_stock" %} {% if d.type == "roster" %}
{% include "cards/roster.html" %} {% include "cards/roster.html" %}
{% elif d.type == "company" %} {% elif d.type == "company" %}
{% include "cards/company.html" %} {% include "cards/company.html" %}
@@ -18,7 +18,7 @@
{% include "cards/consist.html" %} {% include "cards/consist.html" %}
{% elif d.type == "manufacturer" %} {% elif d.type == "manufacturer" %}
{% include "cards/manufacturer.html" %} {% include "cards/manufacturer.html" %}
{% elif d.type == "book" %} {% elif d.type == "book" or d.type == "catalog" %}
{% include "cards/book.html" %} {% include "cards/book.html" %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}

View File

@@ -1,3 +1,4 @@
{% load dynamic_url %}
<div class="col"> <div class="col">
<div class="card shadow-sm"> <div class="card shadow-sm">
{% if d.item.image.exists %} {% if d.item.image.exists %}
@@ -18,10 +19,24 @@
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
{% if d.type == "catalog" %}
<th colspan="2" scope="row">Catalog</th>
{% elif d.type == "book" %}
<th colspan="2" scope="row">Book</th> <th colspan="2" scope="row">Book</th>
{% endif %}
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
{% if d.type == "catalog" %}
<tr>
<th class="w-33" scope="row">Manufacturer</th>
<td>{{ d.item.manufacturer }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Scales</th>
<td>{{ d.item.get_scales }}</td>
</tr>
{% elif d.type == "book" %}
<tr> <tr>
<th class="w-33" scope="row">Authors</th> <th class="w-33" scope="row">Authors</th>
<td> <td>
@@ -32,6 +47,7 @@
<th class="w-33" scope="row">Publisher</th> <th class="w-33" scope="row">Publisher</th>
<td>{{ d.item.publisher }}</td> <td>{{ d.item.publisher }}</td>
</tr> </tr>
{% endif %}
<tr> <tr>
<th scope="row">Language</th> <th scope="row">Language</th>
<td>{{ d.item.get_language_display }}</td> <td>{{ d.item.get_language_display }}</td>
@@ -48,7 +64,7 @@
</table> </table>
<div class="d-grid gap-2 mb-1 d-md-block"> <div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{{ d.item.get_absolute_url }}">Show all data</a> <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 %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% dynamic_admin_url 'bookshelf' d.type d.item.pk %}">Edit</a>{% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,40 +0,0 @@
{% extends "cards.html" %}
{% block pagination %}
{% if data.has_other_pages %}
<nav aria-label="Page navigation example">
<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 'companies_pagination' 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 'companies_pagination' page=i %}#main-content">{{ i }}</a></li>
{% endif %}
{% endif %}
{% endfor %}
{% if data.has_next %}
<li class="page-item">
<a class="page-link" href="{% url 'companies_pagination' 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 %}

View File

@@ -1,40 +0,0 @@
{% extends "cards.html" %}
{% block pagination %}
{% if data.has_other_pages %}
<nav aria-label="Page navigation example">
<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 'consists_pagination' 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 'consists_pagination' page=i %}#main-content">{{ i }}</a></li>
{% endif %}
{% endif %}
{% endfor %}
{% if data.has_next %}
<li class="page-item">
<a class="page-link" href="{% url 'consists_pagination' 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 %}

View File

@@ -1,4 +1,4 @@
{% extends "roster.html" %} {% extends "pagination.html" %}
{% block header %} {% block header %}
<div class="text-muted">{{ site_conf.about | safe }}</div> <div class="text-muted">{{ site_conf.about | safe }}</div>

View File

@@ -1,4 +1,5 @@
{% extends "cards.html" %} {% extends "cards.html" %}
{% load dynamic_url %}
{% block pagination %} {% block pagination %}
{% if data.has_other_pages %} {% if data.has_other_pages %}
@@ -6,7 +7,7 @@
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0"> <ul class="pagination flex-wrap 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 'roster_pagination' page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a> <a class="page-link" href="{% dynamic_pagination type page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -22,13 +23,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 'roster_pagination' page=i %}#main-content">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% dynamic_pagination type 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 'roster_pagination' page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a> <a class="page-link" href="{% dynamic_pagination type page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">

View File

@@ -1,40 +0,0 @@
{% extends "cards.html" %}
{% block pagination %}
{% if data.has_other_pages %}
<nav aria-label="Page navigation example">
<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 'scales_pagination' 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 'scales_pagination' page=i %}#main-content">{{ i }}</a></li>
{% endif %}
{% endif %}
{% endfor %}
{% if data.has_next %}
<li class="page-item">
<a class="page-link" href="{% url 'scales_pagination' page=data.next_page_number %}#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 %}

View File

@@ -1,40 +0,0 @@
{% extends "cards.html" %}
{% block pagination %}
{% if data.has_other_pages %}
<nav aria-label="Page navigation example">
<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 'types_pagination' 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 'types_pagination' page=i %}#main-content">{{ i }}</a></li>
{% endif %}
{% endif %}
{% endfor %}
{% if data.has_next %}
<li class="page-item">
<a class="page-link" href="{% url 'types_pagination' 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 %}

View File

@@ -0,0 +1,21 @@
from django import template
from django.urls import reverse
register = template.Library()
@register.simple_tag
def dynamic_admin_url(app_name, model_name, object_id=None):
if object_id:
return reverse(
f'admin:{app_name}_{model_name}_change',
args=[object_id]
)
return reverse(f'admin:{app_name}_{model_name}_changelist')
@register.simple_tag
def dynamic_pagination(reverse_name, page):
if reverse_name.endswith('y'):
return reverse(f'{reverse_name[:-1]}ies_pagination', args=[page])
return reverse(f'{reverse_name}s_pagination', args=[page])

View File

@@ -1,13 +1,18 @@
from django import template from django import template
from portal.models import Flatpage from portal.models import Flatpage
from bookshelf.models import Book from bookshelf.models import Book, Catalog
register = template.Library() register = template.Library()
@register.inclusion_tag('bookshelf/bookshelf_menu.html') @register.inclusion_tag('bookshelf/bookshelf_menu.html')
def show_bookshelf_menu(): def show_bookshelf_menu():
return {"bookshelf_menu": Book.objects.exists()} # FIXME: Filter out unpublished books and catalogs?
return {
"bookshelf_menu": (Book.objects.exists() or Catalog.objects.exists()),
"books_menu": Book.objects.exists(),
"catalogs_menu": Catalog.objects.exists(),
}
@register.inclusion_tag('flatpages/flatpages_menu.html') @register.inclusion_tag('flatpages/flatpages_menu.html')

View File

@@ -14,7 +14,8 @@ from portal.views import (
Scales, Scales,
Types, Types,
Books, Books,
GetBook, Catalogs,
GetBookCatalog,
SearchObjects, SearchObjects,
) )
@@ -24,7 +25,7 @@ urlpatterns = [
path( path(
"roster/page/<int:page>", "roster/page/<int:page>",
GetRoster.as_view(), GetRoster.as_view(),
name="roster_pagination" name="rosters_pagination"
), ),
path( path(
"page/<str:flatpage>", "page/<str:flatpage>",
@@ -33,12 +34,12 @@ urlpatterns = [
), ),
path( path(
"consists", "consists",
Consists.as_view(template="consists.html"), Consists.as_view(),
name="consists" name="consists"
), ),
path( path(
"consists/page/<int:page>", "consists/page/<int:page>",
Consists.as_view(template="consists.html"), Consists.as_view(),
name="consists_pagination" name="consists_pagination"
), ),
path("consist/<uuid:uuid>", GetConsist.as_view(), name="consist"), path("consist/<uuid:uuid>", GetConsist.as_view(), name="consist"),
@@ -49,55 +50,69 @@ urlpatterns = [
), ),
path( path(
"companies", "companies",
Companies.as_view(template="companies.html"), Companies.as_view(),
name="companies" name="companies"
), ),
path( path(
"companies/page/<int:page>", "companies/page/<int:page>",
Companies.as_view(template="companies.html"), Companies.as_view(),
name="companies_pagination", name="companies_pagination",
), ),
path( path(
"manufacturers/<str:category>", "manufacturers/<str:category>",
Manufacturers.as_view(template="manufacturers.html"), Manufacturers.as_view(template="pagination_manufacturers.html"),
name="manufacturers" name="manufacturers"
), ),
path( path(
"manufacturers/<str:category>/page/<int:page>", "manufacturers/<str:category>/page/<int:page>",
Manufacturers.as_view(template="manufacturers.html"), Manufacturers.as_view(template="pagination_manufacturers.html"),
name="manufacturers_pagination", name="manufacturers_pagination",
), ),
path( path(
"scales", "scales",
Scales.as_view(template="scales.html"), Scales.as_view(),
name="scales" name="scales"
), ),
path( path(
"scales/page/<int:page>", "scales/page/<int:page>",
Scales.as_view(template="scales.html"), Scales.as_view(),
name="scales_pagination" name="scales_pagination"
), ),
path( path(
"types", "types",
Types.as_view(template="types.html"), Types.as_view(),
name="types" name="rolling_stock_types"
), ),
path( path(
"types/page/<int:page>", "types/page/<int:page>",
Types.as_view(template="types.html"), Types.as_view(),
name="types_pagination" name="rolling_stock_types_pagination"
), ),
path( path(
"bookshelf/books", "bookshelf/books",
Books.as_view(template="bookshelf/books.html"), Books.as_view(),
name="books" name="books"
), ),
path( path(
"bookshelf/books/page/<int:page>", "bookshelf/books/page/<int:page>",
Books.as_view(template="bookshelf/books.html"), Books.as_view(),
name="books_pagination" name="books_pagination"
), ),
path("bookshelf/book/<uuid:uuid>", GetBook.as_view(), name="book"), path(
"bookshelf/<str:selector>/<uuid:uuid>",
GetBookCatalog.as_view(),
name="bookshelf_item"
),
path(
"bookshelf/catalogs",
Catalogs.as_view(),
name="catalogs"
),
path(
"bookshelf/catalogs/page/<int:page>",
Catalogs.as_view(),
name="catalogs_pagination"
),
path( path(
"search", "search",
SearchObjects.as_view(http_method_names=["post"]), SearchObjects.as_view(http_method_names=["post"]),

View File

@@ -1,5 +1,6 @@
import base64 import base64
import operator import operator
from itertools import chain
from functools import reduce from functools import reduce
from urllib.parse import unquote from urllib.parse import unquote
@@ -15,7 +16,7 @@ from portal.utils import get_site_conf
from portal.models import Flatpage from portal.models import Flatpage
from roster.models import RollingStock from roster.models import RollingStock
from consist.models import Consist from consist.models import Consist
from bookshelf.models import Book from bookshelf.models import Book, Catalog
from metadata.models import ( from metadata.models import (
Company, Company,
Manufacturer, Manufacturer,
@@ -61,8 +62,8 @@ class Render404(View):
class GetData(View): class GetData(View):
title = "Home" title = "Home"
template = "roster.html" template = "pagination.html"
item_type = "rolling_stock" item_type = "roster"
filter = Q() # empty filter by default filter = Q() # empty filter by default
def get_data(self, request): def get_data(self, request):
@@ -97,8 +98,8 @@ class GetData(View):
class GetRoster(GetData): class GetRoster(GetData):
title = "Roster" title = "The Roster"
item_type = "rolling_stock" item_type = "roster"
def get_data(self, request): def get_data(self, request):
return RollingStock.objects.get_published(request.user).order_by( return RollingStock.objects.get_published(request.user).order_by(
@@ -148,15 +149,18 @@ class SearchObjects(View):
raise Http404 raise Http404
# FIXME duplicated code! # FIXME duplicated code!
# FIXME see if it makes sense to filter calatogs and books by scale
# and manufacturer as well
data = [] data = []
rolling_stock = ( roster = (
RollingStock.objects.get_published(request.user) RollingStock.objects.get_published(request.user)
.filter(query) .filter(query)
.distinct() .distinct()
.order_by(*get_order_by_field()) .order_by(*get_order_by_field())
) )
for item in rolling_stock: for item in roster:
data.append({"type": "rolling_stock", "item": item}) data.append({"type": "roster", "item": item})
if _filter is None: if _filter is None:
consists = ( consists = (
Consist.objects.get_published(request.user) Consist.objects.get_published(request.user)
@@ -175,7 +179,12 @@ class SearchObjects(View):
.filter(title__icontains=search) .filter(title__icontains=search)
.distinct() .distinct()
) )
for item in books: catalogs = (
Catalog.objects.get_published(request.user)
.filter(manufacturer__name__icontains=search)
.distinct()
)
for item in list(chain(books, catalogs)):
data.append({"type": "book", "item": item}) data.append({"type": "book", "item": item})
paginator = Paginator(data, get_items_per_page()) paginator = Paginator(data, get_items_per_page())
@@ -237,7 +246,7 @@ class GetManufacturerItem(View):
) )
if search != "all": if search != "all":
rolling_stock = get_list_or_404( roster = get_list_or_404(
RollingStock.objects.get_published(request.user).order_by( RollingStock.objects.get_published(request.user).order_by(
*get_order_by_field() *get_order_by_field()
), ),
@@ -250,10 +259,10 @@ class GetManufacturerItem(View):
manufacturer, manufacturer,
# all returned records must have the same `item_number``; # all returned records must have the same `item_number``;
# just pick it up the first result, otherwise `search` # just pick it up the first result, otherwise `search`
rolling_stock[0].item_number if rolling_stock else search, roster.first.item_number if roster else search,
) )
else: else:
rolling_stock = ( roster = (
RollingStock.objects.get_published(request.user) RollingStock.objects.get_published(request.user)
.order_by(*get_order_by_field()) .order_by(*get_order_by_field())
.filter( .filter(
@@ -264,8 +273,8 @@ class GetManufacturerItem(View):
title = "Manufacturer: {0}".format(manufacturer) title = "Manufacturer: {0}".format(manufacturer)
data = [] data = []
for item in rolling_stock: for item in roster:
data.append({"type": "rolling_stock", "item": item}) data.append({"type": "roster", "item": item})
paginator = Paginator(data, get_items_per_page()) paginator = Paginator(data, get_items_per_page())
data = paginator.get_page(page) data = paginator.get_page(page)
@@ -308,7 +317,7 @@ class GetObjectsFiltered(View):
else: else:
raise Http404 raise Http404
rolling_stock = ( roster = (
RollingStock.objects.get_published(request.user) RollingStock.objects.get_published(request.user)
.filter(query) .filter(query)
.distinct() .distinct()
@@ -316,8 +325,8 @@ class GetObjectsFiltered(View):
) )
data = [] data = []
for item in rolling_stock: for item in roster:
data.append({"type": "rolling_stock", "item": item}) data.append({"type": "roster", "item": item})
try: # Execute only if query_2nd is defined try: # Execute only if query_2nd is defined
consists = ( consists = (
@@ -442,7 +451,7 @@ class GetConsist(View):
raise Http404 raise Http404
data = [ data = [
{ {
"type": "rolling_stock", "type": "roster",
"item": RollingStock.objects.get_published(request.user).get( "item": RollingStock.objects.get_published(request.user).get(
uuid=r.rolling_stock_id uuid=r.rolling_stock_id
), ),
@@ -517,10 +526,26 @@ class Books(GetData):
return Book.objects.get_published(request.user).all() return Book.objects.get_published(request.user).all()
class GetBook(View): class Catalogs(GetData):
def get(self, request, uuid): title = "Catalogs"
item_type = "catalog"
def get_data(self, request):
return Catalog.objects.get_published(request.user).all()
class GetBookCatalog(View):
def get_object(self, request, uuid, selector):
if selector == "book":
return Book.objects.get_published(request.user).get(uuid=uuid)
elif selector == "catalog":
return Catalog.objects.get_published(request.user).get(uuid=uuid)
else:
raise Http404
def get(self, request, uuid, selector):
try: try:
book = Book.objects.get_published(request.user).get(uuid=uuid) book = self.get_object(request, uuid, selector)
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise Http404 raise Http404
@@ -532,6 +557,7 @@ class GetBook(View):
"title": book, "title": book,
"book_properties": book_properties, "book_properties": book_properties,
"book": book, "book": book,
"type": selector
}, },
) )

View File

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

View File

@@ -32,6 +32,7 @@ class Document(models.Model):
class Meta: class Meta:
abstract = True abstract = True
verbose_name_plural = "Documents"
def __str__(self): def __str__(self):
return "{0}".format(os.path.basename(self.file.name)) return "{0}".format(os.path.basename(self.file.name))
@@ -65,6 +66,7 @@ class Image(models.Model):
class Meta: class Meta:
abstract = True abstract = True
ordering = ["order"] ordering = ["order"]
verbose_name_plural = "Images"
objects = PublicManager() objects = PublicManager()

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.1.2 on 2024-12-22 20:38
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("roster", "0028_rollingstock_published"),
]
operations = [
migrations.AlterModelOptions(
name="rollingstockimage",
options={"ordering": ["order"], "verbose_name_plural": "Images"},
),
]