5 Commits

Author SHA1 Message Date
54a68d9b1f Fix data retreival issue on GetData (#35) 2024-04-21 15:34:16 +02:00
aa02404dfe Fix an ordering issue on items in a set query 2024-04-21 09:56:10 +02:00
e4ad98fa38 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
2024-04-21 00:31:52 +02:00
b37f5420c5 Update to Bootstrap 5.3.3 (#33)
* Update to Bootstrap 5.3.3
* Remove support for python 3.9
2024-04-09 23:45:58 +02:00
4b74a69f3f Add the possbility to provide descriptions (#32)
to class, rolling stock, book
2024-03-02 15:45:42 +01:00
30 changed files with 410 additions and 65 deletions

View File

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

1
.gitignore vendored
View File

@@ -10,7 +10,6 @@ __pycache__/
.Python .Python
build/ build/
develop-eggs/ develop-eggs/
dist/
downloads/ downloads/
eggs/ eggs/
.eggs/ .eggs/

View File

@@ -49,7 +49,7 @@ It has been developed with:
## Requirements ## Requirements
- Python 3.9+ - Python 3.10+
- A USB port when running Arduino hardware (and adaptors if you have a Mac) - A USB port when running Arduino hardware (and adaptors if you have a Mac)
## Web portal installation ## Web portal installation

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.0.2 on 2024-03-02 14:31
import tinymce.models
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0012_alter_book_notes"),
]
operations = [
migrations.AddField(
model_name="book",
name="description",
field=tinymce.models.HTMLField(blank=True),
),
]

View File

@@ -52,6 +52,7 @@ class Book(models.Model):
) )
number_of_pages = models.SmallIntegerField(null=True, blank=True) number_of_pages = models.SmallIntegerField(null=True, blank=True)
publication_year = models.SmallIntegerField(null=True, blank=True) publication_year = models.SmallIntegerField(null=True, blank=True)
description = tinymce.HTMLField(blank=True)
purchase_date = models.DateField(null=True, blank=True) purchase_date = models.DateField(null=True, blank=True)
tags = models.ManyToManyField( tags = models.ManyToManyField(
Tag, related_name="bookshelf", blank=True Tag, related_name="bookshelf", blank=True

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

@@ -1,14 +1,14 @@
/*! /*!
* Bootstrap Icons v1.11.1 (https://icons.getbootstrap.com/) * Bootstrap Icons v1.11.3 (https://icons.getbootstrap.com/)
* Copyright 2019-2023 The Bootstrap Authors * Copyright 2019-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE) * Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE)
*/ */
@font-face { @font-face {
font-display: block; font-display: block;
font-family: "bootstrap-icons"; font-family: "bootstrap-icons";
src: url("./fonts/bootstrap-icons.woff2?2820a3852bdb9a5832199cc61cec4e65") format("woff2"), src: url("./fonts/bootstrap-icons.woff2?dd67030699838ea613ee6dbda90effa6") format("woff2"),
url("./fonts/bootstrap-icons.woff?2820a3852bdb9a5832199cc61cec4e65") format("woff"); url("./fonts/bootstrap-icons.woff?dd67030699838ea613ee6dbda90effa6") format("woff");
} }
.bi::before, .bi::before,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -16,11 +16,11 @@
<link rel="icon" href="{% static "favicon.png" %}" sizes="any"> <link rel="icon" href="{% static "favicon.png" %}" sizes="any">
<link rel="icon" href="{% static "favicon.svg" %}" type="image/svg+xml"> <link rel="icon" href="{% static "favicon.svg" %}" type="image/svg+xml">
{% if site_conf.use_cdn %} {% if site_conf.use_cdn %}
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
{% else %} {% else %}
<link href="{% static "bootstrap@5.3.2/dist/css/bootstrap.min.css" %}" rel="stylesheet"> <link href="{% static "bootstrap@5.3.3/dist/css/bootstrap.min.css" %}" rel="stylesheet">
<link href="{% static "bootstrap-icons@1.11.1/font/bootstrap-icons.css" %}" rel="stylesheet"> <link href="{% static "bootstrap-icons@1.11.3/font/bootstrap-icons.css" %}" rel="stylesheet">
{% endif %} {% endif %}
<link href="{% static "css/main.css" %}?v={{ site_conf.version }}" rel="stylesheet"> <link href="{% static "css/main.css" %}?v={{ site_conf.version }}" rel="stylesheet">
<style> <style>
@@ -216,9 +216,9 @@
</main> </main>
{% include 'includes/footer.html' %} {% include 'includes/footer.html' %}
{% if site_conf.use_cdn %} {% if site_conf.use_cdn %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
{% else %} {% else %}
<script src="{% static "bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" %}"></script> <script src="{% static "bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" %}"></script>
{% endif %} {% endif %}
</body> </body>
</html> </html>

View File

@@ -54,6 +54,7 @@
<div class="tab-content" id="nav-tabContent"> <div class="tab-content" id="nav-tabContent">
<div class="tab-pane show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab"> <div class="tab-pane show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab">
<table class="table table-striped"> <table class="table table-striped">
{{ book.description | safe }}
<thead> <thead>
<tr> <tr>
<th colspan="2" scope="row">Book</th> <th colspan="2" scope="row">Book</th>

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>
@@ -149,6 +151,7 @@
{% endif %} {% endif %}
</div> </div>
<div class="tab-pane" id="nav-model" role="tabpanel" aria-labelledby="nav-model-tab"> <div class="tab-pane" id="nav-model" role="tabpanel" aria-labelledby="nav-model-tab">
{{ rolling_stock.description | safe }}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@@ -170,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>
@@ -205,6 +208,7 @@
{% endif %} {% endif %}
</div> </div>
<div class="tab-pane" id="nav-class" role="tabpanel" aria-labelledby="nav-class-tab"> <div class="tab-pane" id="nav-class" role="tabpanel" aria-labelledby="nav-class-tab">
{{ class.description | safe }}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@@ -372,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
@@ -21,7 +21,15 @@ from metadata.models import (
) )
def order_by_fields(): def get_items_per_page():
try:
items_per_page = get_site_conf().items_per_page
except (OperationalError, ProgrammingError):
items_per_page = 6
return items_per_page
def get_order_by_field():
try: try:
order_by = get_site_conf().items_ordering order_by = get_site_conf().items_ordering
except (OperationalError, ProgrammingError): except (OperationalError, ProgrammingError):
@@ -46,19 +54,22 @@ class GetData(View):
title = "Home" title = "Home"
template = "roster.html" template = "roster.html"
item_type = "rolling_stock" item_type = "rolling_stock"
queryset = RollingStock.objects.order_by(*order_by_fields()) filter = Q() # empty filter by default
def get_data(self):
return RollingStock.objects.order_by(
*get_order_by_field()
).filter(self.filter)
def get(self, request, page=1): def get(self, request, page=1):
site_conf = get_site_conf()
data = [] data = []
for item in self.queryset: for item in self.get_data():
data.append({ data.append({
"type": self.item_type, "type": self.item_type,
"item": item "item": item
}) })
paginator = Paginator(data, site_conf.items_per_page) paginator = Paginator(data, get_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
@@ -80,12 +91,13 @@ class GetData(View):
class GetRoster(GetData): class GetRoster(GetData):
title = "Roster" title = "Roster"
item_type = "rolling_stock" item_type = "rolling_stock"
queryset = RollingStock.objects.order_by(*order_by_fields())
def get_data(self):
return RollingStock.objects.order_by(*get_order_by_field())
class SearchObjects(View): class SearchObjects(View):
def run_search(self, request, search, _filter, page=1): def run_search(self, request, search, _filter, page=1):
site_conf = get_site_conf()
if _filter is None: if _filter is None:
query = reduce( query = reduce(
operator.or_, operator.or_,
@@ -130,7 +142,7 @@ class SearchObjects(View):
rolling_stock = ( rolling_stock = (
RollingStock.objects.filter(query) RollingStock.objects.filter(query)
.distinct() .distinct()
.order_by(*order_by_fields()) .order_by(*get_order_by_field())
) )
for item in rolling_stock: for item in rolling_stock:
data.append({ data.append({
@@ -162,7 +174,7 @@ class SearchObjects(View):
"item": item "item": item
}) })
paginator = Paginator(data, site_conf.items_per_page) paginator = Paginator(data, get_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
@@ -215,10 +227,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):
if search != "all":
rolling_stock = get_list_or_404(
RollingStock.objects.order_by(*get_order_by_field()),
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(*get_order_by_field()),
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, get_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()
if _filter == "type": if _filter == "type":
title = get_object_or_404(RollingStockType, slug__iexact=search) title = get_object_or_404(RollingStockType, slug__iexact=search)
query = Q(rolling_class__type__slug__iexact=search) query = Q(rolling_class__type__slug__iexact=search)
@@ -226,12 +286,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)
@@ -248,7 +302,7 @@ class GetObjectsFiltered(View):
rolling_stock = ( rolling_stock = (
RollingStock.objects.filter(query) RollingStock.objects.filter(query)
.distinct() .distinct()
.order_by(*order_by_fields()) .order_by(*get_order_by_field())
) )
data = [] data = []
@@ -281,7 +335,7 @@ class GetObjectsFiltered(View):
except NameError: except NameError:
pass pass
paginator = Paginator(data, site_conf.items_per_page) paginator = Paginator(data, get_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
@@ -347,6 +401,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)
)
).order_by(*get_order_by_field())]
return render( return render(
request, request,
"rollingstock.html", "rollingstock.html",
@@ -358,6 +422,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,
}, },
) )
@@ -366,12 +431,13 @@ class GetRollingStock(View):
class Consists(GetData): class Consists(GetData):
title = "Consists" title = "Consists"
item_type = "consist" item_type = "consist"
queryset = Consist.objects.all()
def get_data(self):
return Consist.objects.all()
class GetConsist(View): class GetConsist(View):
def get(self, request, uuid, page=1): def get(self, request, uuid, page=1):
site_conf = get_site_conf()
try: try:
consist = Consist.objects.get(uuid=uuid) consist = Consist.objects.get(uuid=uuid)
except ObjectDoesNotExist: except ObjectDoesNotExist:
@@ -381,7 +447,7 @@ class GetConsist(View):
"item": RollingStock.objects.get(uuid=r.rolling_stock_id) "item": RollingStock.objects.get(uuid=r.rolling_stock_id)
} for r in consist.consist_item.all()] } for r in consist.consist_item.all()]
paginator = Paginator(data, site_conf.items_per_page) paginator = Paginator(data, get_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
@@ -402,20 +468,25 @@ class GetConsist(View):
class Manufacturers(GetData): class Manufacturers(GetData):
title = "Manufacturers" title = "Manufacturers"
item_type = "manufacturer" item_type = "manufacturer"
queryset = None # Set via method get
def get_data(self):
return Manufacturer.objects.filter(self.filter)
# 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.queryset = Manufacturer.objects.filter(category=category) self.filter = Q(category=category)
return super().get(request, page) return super().get(request, page)
class Companies(GetData): class Companies(GetData):
title = "Companies" title = "Companies"
item_type = "company" item_type = "company"
queryset = Company.objects.all()
def get_data(self):
return Company.objects.all()
class Scales(GetData): class Scales(GetData):
@@ -423,17 +494,24 @@ class Scales(GetData):
item_type = "scale" item_type = "scale"
queryset = Scale.objects.all() queryset = Scale.objects.all()
def get_data(self):
return Scale.objects.all()
class Types(GetData): class Types(GetData):
title = "Types" title = "Types"
item_type = "rolling_stock_type" item_type = "rolling_stock_type"
queryset = RollingStockType.objects.all()
def get_data(self):
return RollingStockType.objects.all()
class Books(GetData): class Books(GetData):
title = "Books" title = "Books"
item_type = "book" item_type = "book"
queryset = Book.objects.all()
def get_data(self):
return Book.objects.all()
class GetBook(View): class GetBook(View):

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.0.2 on 2024-03-02 13:30
import tinymce.models
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("roster", "0022_alter_rollingstock_notes_and_more"),
]
operations = [
migrations.AlterField(
model_name="rollingclass",
name="description",
field=tinymce.models.HTMLField(blank=True),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.0.2 on 2024-03-02 14:30
import tinymce.models
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("roster", "0023_alter_rollingclass_description"),
]
operations = [
migrations.AddField(
model_name="rollingstock",
name="description",
field=tinymce.models.HTMLField(blank=True),
),
]

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

@@ -25,7 +25,7 @@ class RollingClass(models.Model):
identifier = models.CharField(max_length=128, unique=False) identifier = models.CharField(max_length=128, unique=False)
type = models.ForeignKey(RollingStockType, on_delete=models.CASCADE) type = models.ForeignKey(RollingStockType, on_delete=models.CASCADE)
company = models.ForeignKey(Company, on_delete=models.CASCADE) company = models.ForeignKey(Company, on_delete=models.CASCADE)
description = models.CharField(max_length=256, blank=True) description = tinymce.HTMLField(blank=True)
manufacturer = models.ForeignKey( manufacturer = models.ForeignKey(
Manufacturer, Manufacturer,
on_delete=models.CASCADE, on_delete=models.CASCADE,
@@ -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,13 +90,18 @@ 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)
notes = tinymce.HTMLField(blank=True) description = tinymce.HTMLField(blank=True)
tags = models.ManyToManyField( tags = models.ManyToManyField(
Tag, related_name="rolling_stock", blank=True Tag, related_name="rolling_stock", blank=True
) )
notes = tinymce.HTMLField(blank=True)
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)
@@ -176,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)