Implement support for sets and other improvements (#34)

* Add a boolean to define item as part of a set
* Add contextual help in admin
* Introduce support to sets and to item code lookup
Also review the url path for pagination
This commit is contained in:
2024-04-21 00:31:52 +02:00
committed by GitHub
parent b37f5420c5
commit e4ad98fa38
14 changed files with 275 additions and 28 deletions

View File

@@ -0,0 +1,30 @@
# Generated by Django 5.0.4 on 2024-04-20 12:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("consist", "0010_alter_consist_notes"),
]
operations = [
migrations.AlterField(
model_name="consist",
name="consist_address",
field=models.SmallIntegerField(
blank=True,
default=None,
help_text="DCC consist address if enabled",
null=True,
),
),
migrations.AlterField(
model_name="consist",
name="era",
field=models.CharField(
blank=True, help_text="Era or epoch of the consist", max_length=32
),
),
]

View File

@@ -16,10 +16,17 @@ class Consist(models.Model):
identifier = models.CharField(max_length=128, unique=False) identifier = models.CharField(max_length=128, unique=False)
tags = models.ManyToManyField(Tag, related_name="consist", blank=True) tags = models.ManyToManyField(Tag, related_name="consist", blank=True)
consist_address = models.SmallIntegerField( consist_address = models.SmallIntegerField(
default=None, null=True, blank=True default=None,
null=True,
blank=True,
help_text="DCC consist address if enabled",
) )
company = models.ForeignKey(Company, on_delete=models.CASCADE) company = models.ForeignKey(Company, on_delete=models.CASCADE)
era = models.CharField(max_length=32, blank=True) era = models.CharField(
max_length=32,
blank=True,
help_text="Era or epoch of the consist",
)
image = models.ImageField( image = models.ImageField(
upload_to=os.path.join("images", "consists"), upload_to=os.path.join("images", "consists"),
storage=DeduplicatedStorage, storage=DeduplicatedStorage,

View File

@@ -0,0 +1,20 @@
# Generated by Django 5.0.4 on 2024-04-20 12:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("metadata", "0016_alter_decoderdocument_file"),
]
operations = [
migrations.AlterField(
model_name="property",
name="private",
field=models.BooleanField(
default=False, help_text="Property will be only visible to logged users"
),
),
]

View File

@@ -11,7 +11,10 @@ from ram.utils import DeduplicatedStorage, get_image_preview, slugify
class Property(models.Model): class Property(models.Model):
name = models.CharField(max_length=128, unique=True) name = models.CharField(max_length=128, unique=True)
private = models.BooleanField(default=False) private = models.BooleanField(
default=False,
help_text="Property will be only visible to logged users",
)
class Meta: class Meta:
verbose_name_plural = "Properties" verbose_name_plural = "Properties"

View File

@@ -60,7 +60,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Item number</th> <th scope="row">Item number</th>
<td>{{ d.item.item_number }}</td> <td>{{ d.item.item_number }}{%if d.item.set %} | <a class="badge text-bg-primary" href="{% url 'manufacturer' manufacturer=d.item.manufacturer search=d.item.item_number %}">SET</a>{% endif %}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -0,0 +1,40 @@
{% extends "cards.html" %}
{% block pagination %}
{% if data.has_other_pages %}
<nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mt-4 mb-0">
{% if data.has_previous %}
<li class="page-item">
<a class="page-link" href="{% url 'manufacturer_pagination' manufacturer=manufacturer search=search page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">Previous</span>
</li>
{% endif %}
{% for i in page_range %}
{% if data.number == i %}
<li class="page-item active">
<span class="page-link">{{ i }}</span>
</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 'manufacturer_pagination' manufacturer=manufacturer search=search page=i %}#main-content">{{ i }}</a></li>
{% endif %}
{% endif %}
{% endfor %}
{% if data.has_next %}
<li class="page-item">
<a class="page-link" href="{% url 'manufacturer_pagination' manufacturer=manufacturer search=search page=data.next_page_number %}#main-content" tabindex="-1">Next</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">Next</span>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% endblock %}

View File

@@ -52,6 +52,7 @@
{% if documents or decoder_documents %}<button class="nav-link" id="nav-documents-tab" data-bs-toggle="tab" data-bs-target="#nav-documents" type="button" role="tab" aria-controls="nav-documents" aria-selected="false">Documents</button>{% endif %} {% if documents or decoder_documents %}<button class="nav-link" id="nav-documents-tab" data-bs-toggle="tab" data-bs-target="#nav-documents" type="button" role="tab" aria-controls="nav-documents" aria-selected="false">Documents</button>{% endif %}
{% if journal %}<button class="nav-link" id="nav-journal-tab" data-bs-toggle="tab" data-bs-target="#nav-journal" type="button" role="tab" aria-controls="nav-journal" aria-selected="false">Journal</button>{% endif %} {% if journal %}<button class="nav-link" id="nav-journal-tab" data-bs-toggle="tab" data-bs-target="#nav-journal" type="button" role="tab" aria-controls="nav-journal" aria-selected="false">Journal</button>{% endif %}
{% if rolling_stock.notes %}<button class="nav-link" id="nav-notes-tab" data-bs-toggle="tab" data-bs-target="#nav-notes" type="button" role="tab" aria-controls="nav-notes" aria-selected="false">Notes</button>{% endif %} {% if rolling_stock.notes %}<button class="nav-link" id="nav-notes-tab" data-bs-toggle="tab" data-bs-target="#nav-notes" type="button" role="tab" aria-controls="nav-notes" aria-selected="false">Notes</button>{% endif %}
{% if set %}<button class="nav-link" id="nav-set-tab" data-bs-toggle="tab" data-bs-target="#nav-set" type="button" role="tab" aria-controls="nav-set" aria-selected="false">Set</button>{% endif %}
{% if consists %}<button class="nav-link" id="nav-consists-tab" data-bs-toggle="tab" data-bs-target="#nav-consists" type="button" role="tab" aria-controls="nav-consists" aria-selected="false">Consists</button>{% endif %} {% if consists %}<button class="nav-link" id="nav-consists-tab" data-bs-toggle="tab" data-bs-target="#nav-consists" type="button" role="tab" aria-controls="nav-consists" aria-selected="false">Consists</button>{% endif %}
</nav> </nav>
<select class="form-select d-lg-none mb-2" id="tabSelector" aria-label="Tab selector"> <select class="form-select d-lg-none mb-2" id="tabSelector" aria-label="Tab selector">
@@ -63,6 +64,7 @@
{% if documents or decoder_documents %}<option value="nav-documents">Documents</option>{% endif %} {% if documents or decoder_documents %}<option value="nav-documents">Documents</option>{% endif %}
{% if journal %}<option value="nav-journal">Journal</option>{% endif %} {% if journal %}<option value="nav-journal">Journal</option>{% endif %}
{% if rolling_stock.notes %}<option value="nav-notes">Notes</option>{% endif %} {% if rolling_stock.notes %}<option value="nav-notes">Notes</option>{% endif %}
{% if set %}<option value="nav-set">Set</option>{% endif %}
{% if consists %}<option value="nav-consists">Consists</option>{% endif %} {% if consists %}<option value="nav-consists">Consists</option>{% endif %}
</select> </select>
{% with class=rolling_stock.rolling_class company=rolling_stock.rolling_class.company %} {% with class=rolling_stock.rolling_class company=rolling_stock.rolling_class.company %}
@@ -118,7 +120,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Item number</th> <th scope="row">Item number</th>
<td>{{ rolling_stock.item_number }}</td> <td>{{ rolling_stock.item_number }}{%if rolling_stock.set %} | <a class="badge text-bg-primary" href="{% url 'manufacturer' manufacturer=rolling_stock.manufacturer search=rolling_stock.item_number %}">SET</a>{% endif %}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -171,7 +173,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Item number</th> <th scope="row">Item number</th>
<td>{{ rolling_stock.item_number }}</td> <td>{{ rolling_stock.item_number }}{%if rolling_stock.set %} | <a class="badge text-bg-primary" href="{% url 'manufacturer' manufacturer=rolling_stock.manufacturer search=rolling_stock.item_number %}">SET</a>{% endif %}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Era</th> <th scope="row">Era</th>
@@ -374,6 +376,13 @@
<div class="tab-pane" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab"> <div class="tab-pane" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab">
{{ rolling_stock.notes | safe }} {{ rolling_stock.notes | safe }}
</div> </div>
<div class="tab-pane" id="nav-set" role="tabpanel" aria-labelledby="nav-set-tab">
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3 mb-3">
{% for d in set %}
{% include "cards/roster.html" %}
{% endfor %}
</div>
</div>
<div class="tab-pane" id="nav-consists" role="tabpanel" aria-labelledby="nav-cosists-tab"> <div class="tab-pane" id="nav-consists" role="tabpanel" aria-labelledby="nav-cosists-tab">
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3 mb-3"> <div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3 mb-3">
{% for d in consists %} {% for d in consists %}

View File

@@ -4,6 +4,7 @@ from portal.views import (
GetData, GetData,
GetRoster, GetRoster,
GetObjectsFiltered, GetObjectsFiltered,
GetManufacturerItem,
GetFlatpage, GetFlatpage,
GetRollingStock, GetRollingStock,
GetConsist, GetConsist,
@@ -20,7 +21,11 @@ from portal.views import (
urlpatterns = [ urlpatterns = [
path("", GetData.as_view(template="home.html"), 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/page/<int:page>",
GetRoster.as_view(),
name="roster_pagination"
),
path( path(
"page/<str:flatpage>", "page/<str:flatpage>",
GetFlatpage.as_view(), GetFlatpage.as_view(),
@@ -32,13 +37,13 @@ urlpatterns = [
name="consists" name="consists"
), ),
path( path(
"consists/<int:page>", "consists/page/<int:page>",
Consists.as_view(template="consists.html"), Consists.as_view(template="consists.html"),
name="consists_pagination" name="consists_pagination"
), ),
path("consist/<uuid:uuid>", GetConsist.as_view(), name="consist"), path("consist/<uuid:uuid>", GetConsist.as_view(), name="consist"),
path( path(
"consist/<uuid:uuid>/<int:page>", "consist/<uuid:uuid>/page/<int:page>",
GetConsist.as_view(), GetConsist.as_view(),
name="consist_pagination", name="consist_pagination",
), ),
@@ -48,7 +53,7 @@ urlpatterns = [
name="companies" name="companies"
), ),
path( path(
"companies/<int:page>", "companies/page/<int:page>",
Companies.as_view(template="companies.html"), Companies.as_view(template="companies.html"),
name="companies_pagination", name="companies_pagination",
), ),
@@ -58,7 +63,7 @@ urlpatterns = [
name="manufacturers" name="manufacturers"
), ),
path( path(
"manufacturers/<str:category>/<int:page>", "manufacturers/<str:category>/page/<int:page>",
Manufacturers.as_view(template="manufacturers.html"), Manufacturers.as_view(template="manufacturers.html"),
name="manufacturers_pagination", name="manufacturers_pagination",
), ),
@@ -68,7 +73,7 @@ urlpatterns = [
name="scales" name="scales"
), ),
path( path(
"scales/<int:page>", "scales/page/<int:page>",
Scales.as_view(template="scales.html"), Scales.as_view(template="scales.html"),
name="scales_pagination" name="scales_pagination"
), ),
@@ -78,7 +83,7 @@ urlpatterns = [
name="types" name="types"
), ),
path( path(
"types/<int:page>", "types/page/<int:page>",
Types.as_view(template="types.html"), Types.as_view(template="types.html"),
name="types_pagination" name="types_pagination"
), ),
@@ -88,7 +93,7 @@ urlpatterns = [
name="books" name="books"
), ),
path( path(
"bookshelf/books/<int:page>", "bookshelf/books/page/<int:page>",
Books.as_view(template="bookshelf/books.html"), Books.as_view(template="bookshelf/books.html"),
name="books_pagination" name="books_pagination"
), ),
@@ -99,17 +104,37 @@ urlpatterns = [
name="search", name="search",
), ),
path( path(
"search/<str:search>/<int:page>", "search/<str:search>/page/<int:page>",
SearchObjects.as_view(), SearchObjects.as_view(),
name="search_pagination", name="search_pagination",
), ),
path(
"manufacturer/<str:manufacturer>",
GetManufacturerItem.as_view(),
name="manufacturer",
),
path(
"manufacturer/<str:manufacturer>/page/<int:page>",
GetManufacturerItem.as_view(),
name="manufacturer_pagination",
),
path(
"manufacturer/<str:manufacturer>/<str:search>",
GetManufacturerItem.as_view(),
name="manufacturer",
),
path(
"manufacturer/<str:manufacturer>/<str:search>/page/<int:page>",
GetManufacturerItem.as_view(),
name="manufacturer_pagination",
),
path( path(
"<str:_filter>/<str:search>", "<str:_filter>/<str:search>",
GetObjectsFiltered.as_view(), GetObjectsFiltered.as_view(),
name="filtered", name="filtered",
), ),
path( path(
"<str:_filter>/<str:search>/<int:page>", "<str:_filter>/<str:search>/page/<int:page>",
GetObjectsFiltered.as_view(), GetObjectsFiltered.as_view(),
name="filtered_pagination", name="filtered_pagination",
), ),

View File

@@ -7,7 +7,7 @@ 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.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, get_list_or_404
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator from django.core.paginator import Paginator
@@ -215,6 +215,58 @@ class SearchObjects(View):
return self.get(request, search, page) return self.get(request, search, page)
class GetManufacturerItem(View):
def get(self, request, manufacturer, search="all", page=1):
site_conf = get_site_conf()
if search != "all":
rolling_stock = get_list_or_404(
RollingStock.objects.order_by(*order_by_fields()),
Q(
Q(manufacturer__name__iexact=manufacturer)
& Q(item_number__exact=search)
)
)
title = "{0}: {1}".format(
rolling_stock[0].manufacturer,
search
)
else:
rolling_stock = get_list_or_404(
RollingStock.objects.order_by(*order_by_fields()),
Q(rolling_class__manufacturer__slug__iexact=manufacturer)
| Q(manufacturer__slug__iexact=manufacturer)
)
title = "Manufacturer: {0}".format(
get_object_or_404(Manufacturer, slug__iexact=manufacturer)
)
data = []
for item in rolling_stock:
data.append({
"type": "rolling_stock",
"item": item
})
paginator = Paginator(data, site_conf.items_per_page)
data = paginator.get_page(page)
page_range = paginator.get_elided_page_range(
data.number, on_each_side=2, on_ends=1
)
return render(
request,
"manufacturer.html",
{
"title": title,
"manufacturer": manufacturer,
"search": search,
"data": data,
"matches": paginator.count,
"page_range": page_range,
},
)
class GetObjectsFiltered(View): class GetObjectsFiltered(View):
def run_filter(self, request, search, _filter, page=1): def run_filter(self, request, search, _filter, page=1):
site_conf = get_site_conf() site_conf = get_site_conf()
@@ -226,12 +278,6 @@ class GetObjectsFiltered(View):
title = get_object_or_404(Company, slug__iexact=search) title = get_object_or_404(Company, slug__iexact=search)
query = Q(rolling_class__company__slug__iexact=search) query = Q(rolling_class__company__slug__iexact=search)
query_2nd = Q(company__slug__iexact=search) query_2nd = Q(company__slug__iexact=search)
elif _filter == "manufacturer":
title = get_object_or_404(Manufacturer, slug__iexact=search)
query = Q(
Q(rolling_class__manufacturer__slug__iexact=search)
| Q(manufacturer__slug__iexact=search)
)
elif _filter == "scale": elif _filter == "scale":
title = get_object_or_404(Scale, slug__iexact=search) title = get_object_or_404(Scale, slug__iexact=search)
query = Q(scale__slug__iexact=search) query = Q(scale__slug__iexact=search)
@@ -347,6 +393,16 @@ class GetRollingStock(View):
consist_item__rolling_stock=rolling_stock consist_item__rolling_stock=rolling_stock
)] # A dict with "item" is required by the consists card )] # A dict with "item" is required by the consists card
set = [{
"type": "set",
"item": s
} for s in RollingStock.objects.filter(
Q(
Q(item_number__exact=rolling_stock.item_number)
& Q(set=True)
)
)]
return render( return render(
request, request,
"rollingstock.html", "rollingstock.html",
@@ -358,6 +414,7 @@ class GetRollingStock(View):
"decoder_documents": decoder_documents, "decoder_documents": decoder_documents,
"documents": documents, "documents": documents,
"journal": journal, "journal": journal,
"set": set,
"consists": consists, "consists": consists,
}, },
) )

View File

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

View File

@@ -141,6 +141,7 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
"scale", "scale",
"manufacturer", "manufacturer",
"item_number", "item_number",
"set",
"era", "era",
"description", "description",
"production_year", "production_year",

View File

@@ -0,0 +1,40 @@
# Generated by Django 5.0.4 on 2024-04-20 12:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("roster", "0024_rollingstock_description"),
]
operations = [
migrations.AddField(
model_name="rollingstock",
name="set",
field=models.BooleanField(default=False, help_text="Part of a set"),
),
migrations.AlterField(
model_name="rollingstock",
name="era",
field=models.CharField(
blank=True, help_text="Era or epoch of the model", max_length=32
),
),
migrations.AlterField(
model_name="rollingstock",
name="item_number",
field=models.CharField(
blank=True, help_text="Catalog item number or code", max_length=32
),
),
migrations.AlterField(
model_name="rollingstockjournal",
name="private",
field=models.BooleanField(
default=False,
help_text="Journal log will be visible only to logged users",
),
),
]

View File

@@ -74,7 +74,15 @@ class RollingStock(models.Model):
limit_choices_to={"category": "model"}, limit_choices_to={"category": "model"},
) )
scale = models.ForeignKey(Scale, on_delete=models.CASCADE) scale = models.ForeignKey(Scale, on_delete=models.CASCADE)
item_number = models.CharField(max_length=32, blank=True) item_number = models.CharField(
max_length=32,
blank=True,
help_text="Catalog item number or code",
)
set = models.BooleanField(
default=False,
help_text="Part of a set",
)
decoder_interface = models.PositiveSmallIntegerField( decoder_interface = models.PositiveSmallIntegerField(
choices=settings.DECODER_INTERFACES, null=True, blank=True choices=settings.DECODER_INTERFACES, null=True, blank=True
) )
@@ -82,7 +90,11 @@ class RollingStock(models.Model):
Decoder, on_delete=models.CASCADE, null=True, blank=True Decoder, on_delete=models.CASCADE, null=True, blank=True
) )
address = models.SmallIntegerField(default=None, null=True, blank=True) address = models.SmallIntegerField(default=None, null=True, blank=True)
era = models.CharField(max_length=32, blank=True) era = models.CharField(
max_length=32,
blank=True,
help_text="Era or epoch of the model",
)
production_year = models.SmallIntegerField(null=True, blank=True) production_year = models.SmallIntegerField(null=True, blank=True)
purchase_date = models.DateField(null=True, blank=True) purchase_date = models.DateField(null=True, blank=True)
description = tinymce.HTMLField(blank=True) description = tinymce.HTMLField(blank=True)
@@ -177,7 +189,10 @@ class RollingStockJournal(models.Model):
) )
date = models.DateField() date = models.DateField()
log = tinymce.HTMLField() log = tinymce.HTMLField()
private = models.BooleanField(default=False) private = models.BooleanField(
default=False,
help_text="Journal log will be visible only to logged users",
)
creation_time = models.DateTimeField(auto_now_add=True) creation_time = models.DateTimeField(auto_now_add=True)
updated_time = models.DateTimeField(auto_now=True) updated_time = models.DateTimeField(auto_now=True)