42 Commits

Author SHA1 Message Date
c7fa54e90e Rename roster methods in portal view 2023-10-17 22:46:55 +02:00
9164ba494f Update examples to implement caching 2023-10-17 22:40:31 +02:00
97989c3384 Improve UX and filtering 2023-10-17 13:44:30 +02:00
7865bf04f0 Add consists view in rolling stock and them in company filter 2023-10-16 22:48:46 +02:00
e6f1480894 Change login menu icon on mobile 2023-10-12 22:33:55 +02:00
8d8ede4c06 Improve page layout on mobile 2023-10-11 22:39:29 +02:00
87e1107156 Bugfixing (#27)
* Enforce ordering on some metadata models
* Fix a 500 error while accessing flat pages
* Clean up HTML and fix cards (missing class)
* Make the "driver" app optional and disabled by default
2023-10-10 22:17:21 +02:00
448ecae070 Add Python 3.12 flow 2023-10-09 23:17:00 +02:00
2b0fdc4487 Workaround for python 3.12 on Fedora 39 2023-10-09 23:16:06 +02:00
764240d67a Fix bookshelf default sorting 2023-10-09 23:09:05 +02:00
424b17ae58 Bug fixing for consists 2023-10-08 09:52:38 +02:00
c73efb01e4 Introduce private docs and flatpages preview (#26)
* Add support for private documents
* Fix migrations after merge
* Rebase fixtures
* Filter private decoder docs
* Enable preview of unpublished pages
2023-10-07 22:38:20 +02:00
a21baac10c Fix a dependency on solo during bootstrap 2023-10-06 21:37:24 +02:00
4b0361acc1 Fix the consists search 2023-10-05 23:21:52 +02:00
425eed3d83 Bookshelf reloaded (#25)
* Navbar refactoring
* Fix coming soon SVG fonts
* Overhaul templating and extend search to consists and books
2023-10-05 23:13:42 +02:00
2d48463474 Change model default sort for Book 2023-10-03 23:08:11 +02:00
08226247c7 Extend ISBN to include dashes 2023-10-03 22:43:20 +02:00
4f52736d97 Fix a copy-paste issue in bookshelf model 2023-10-03 22:26:51 +02:00
bf8c2331c0 Add link to bookshelf in admin menu 2023-10-03 22:23:22 +02:00
1de4938ae7 Merge pull request #24 from daniviga/bookshelf
Implement a bookshelf
2023-10-03 22:18:11 +02:00
817d53d39a Anchor renaming 2023-10-03 22:13:39 +02:00
12ac33f4a2 Fix books default ordering 2023-10-03 21:58:45 +02:00
cbd76e4f66 Add a book details page in bookshelf 2023-10-03 21:54:47 +02:00
bcfed3534c Minor improvements 2023-10-02 23:27:57 +02:00
22bee7d95d Show booshelf menu 2023-10-02 23:16:54 +02:00
98c696b2d9 Add Books in the main menu 2023-10-02 23:01:43 +02:00
996ddd67ea Web bookshelf first draft 2023-10-02 22:58:15 +02:00
3f905877e7 Extend the bookshelf implementation 2023-10-02 22:19:04 +02:00
968ebeb0b6 First bookshelf implementation 2023-10-02 00:02:24 +02:00
b8572c1701 Rename SKU to 'Item number' 2023-10-01 21:35:14 +02:00
124f3c2a8b Do not show the coming soon image on mobile
To save some extra scrolling...
2023-10-01 19:26:04 +02:00
f4023f105f Add a default card image when no custom one exists (#23)
* Add a default card image when no custom one exists
* Add coming_soon.png source
* Use directly the svg source instead of the png raster
2023-10-01 16:36:30 +02:00
a189646aa5 More minor UX fixes 2023-10-01 11:14:17 +02:00
7a103cca56 Minor fix 2023-10-01 11:10:21 +02:00
2fe221d0f4 Add .table-group-divider to tbody with a custom color 2023-10-01 10:56:09 +02:00
6355460e01 Fix decoder document visualization 2023-10-01 10:39:09 +02:00
75074d5e90 Fix columns size 2023-10-01 10:22:03 +02:00
5d536ce568 Add documents to decoders (#22)
* Add decoder documents support
* Use abstract model for Documents
* Increase version
* Code cleanup
2023-10-01 00:03:41 +02:00
9483648a1f Update arduino/esp32 dependencies 2023-09-26 17:30:13 +02:00
8c15441fe5 More html cleanup to match W3c 2023-09-22 14:30:45 +02:00
5ebce9480e Cleanup html tags 2023-09-21 22:22:06 +02:00
64eefe43aa Update README.md 2023-09-18 22:02:13 +02:00
76 changed files with 2000 additions and 673 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'] python-version: ['3.9', '3.10', '3.11', '3.12']
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3

View File

@@ -2,7 +2,7 @@
[![Django CI](https://github.com/daniviga/django-rma/actions/workflows/django.yml/badge.svg)](https://github.com/daniviga/django-rma/actions/workflows/django.yml) [![Django CI](https://github.com/daniviga/django-rma/actions/workflows/django.yml/badge.svg)](https://github.com/daniviga/django-rma/actions/workflows/django.yml)
![Screenshot 2022-07-23 at 22-40-17 Railroad Assets Manager](https://user-images.githubusercontent.com/1818657/180622177-a4bba00e-47da-42b3-a7f6-b24773e69936.png) ![Screenshot 2023-09-18 at 21-57-33 Company RGS - Railroad Assets Manager](https://github.com/daniviga/django-ram/assets/1818657/d20fbe27-1192-4ab1-a19f-8d2ae50cf781)
A `jff` (just for fun) project that aims to create a A `jff` (just for fun) project that aims to create a
model railroad assets manager that allows to: model railroad assets manager that allows to:
@@ -140,15 +140,16 @@ To be continued ...
## Screenshots ## Screenshots
### Frontend ### Frontend
![Screenshot 2022-07-23 at 22-41-44 Railroad Assets Manager](https://user-images.githubusercontent.com/1818657/180622406-760774a9-f028-44fc-b332-fa74e43307df.png)
![Screenshot 2023-09-18 at 22-00-39 RGS C-19 #40 - Railroad Assets Manager](https://github.com/daniviga/django-ram/assets/1818657/94834b89-5b17-46e7-9494-a1651d72c072)
--- ---
![Screenshot 2022-07-23 at 22-44-35 Railroad Assets Manager](https://user-images.githubusercontent.com/1818657/180622342-40586d75-239a-400c-93a1-1cb9583a7d17.png) ![Screenshot 2023-09-18 at 21-59-30 RGS 1930s short train - Railroad Assets Manager](https://github.com/daniviga/django-ram/assets/1818657/77f9b7c9-27b3-4a65-bad0-26e9cf77e623)
---
![Screenshot 2022-07-23 at 22-44-46 Railroad Assets Manager](https://user-images.githubusercontent.com/1818657/180622321-1ab76440-9c6e-4667-9247-dbbcf6c6055c.png)
#### Dark mode #### Dark mode
![Screenshot 2022-07-23 at 22-53-43 Railroad Assets Manager](https://user-images.githubusercontent.com/1818657/180622629-65d81eaf-cca4-4f44-b39b-3b0077b43a34.png) ![Screenshot 2023-09-18 at 21-58-22 Company RGS - Railroad Assets Manager](https://github.com/daniviga/django-ram/assets/1818657/c95697c9-0897-46f4-941c-6092271e4743)
--- ---

View File

52
ram/bookshelf/admin.py Normal file
View File

@@ -0,0 +1,52 @@
from django.contrib import admin
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
from bookshelf.models import BookProperty, BookImage, Book, Author, Publisher
class BookImageInline(SortableInlineAdminMixin, admin.TabularInline):
model = BookImage
min_num = 0
extra = 0
readonly_fields = ("image_thumbnail",)
classes = ["collapse"]
class BookPropertyInline(admin.TabularInline):
model = BookProperty
min_num = 0
extra = 0
@admin.register(Book)
class BookAdmin(SortableAdminBase, admin.ModelAdmin):
inlines = (BookImageInline, BookPropertyInline,)
list_display = (
"title",
"get_authors",
"get_publisher",
"publication_year",
"number_of_pages"
)
search_fields = ("title", "publisher__name", "authors__last_name")
list_filter = ("publisher__name", "authors")
@admin.display(description="Publisher")
def get_publisher(self, obj):
return obj.publisher.name
@admin.display(description="Authors")
def get_authors(self, obj):
return ", ".join(a.short_name() for a in obj.authors.all())
@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
search_fields = ("first_name", "last_name",)
list_filter = ("last_name",)
@admin.register(Publisher)
class PublisherAdmin(admin.ModelAdmin):
list_display = ("name", "country")
search_fields = ("name",)

6
ram/bookshelf/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class BookshelfConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "bookshelf"

View File

@@ -0,0 +1,119 @@
# Generated by Django 4.2.5 on 2023-10-01 20:16
import ckeditor_uploader.fields
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
("metadata", "0012_alter_decoder_manufacturer_decoderdocument"),
]
operations = [
migrations.CreateModel(
name="Author",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("first_name", models.CharField(max_length=100)),
("last_name", models.CharField(max_length=100)),
],
),
migrations.CreateModel(
name="Book",
fields=[
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("title", models.CharField(max_length=200)),
("ISBN", models.CharField(max_length=13, unique=True)),
("publication_year", models.SmallIntegerField(blank=True, null=True)),
("purchase_date", models.DateField(blank=True, null=True)),
("notes", ckeditor_uploader.fields.RichTextUploadingField(blank=True)),
("creation_time", models.DateTimeField(auto_now_add=True)),
("updated_time", models.DateTimeField(auto_now=True)),
("authors", models.ManyToManyField(to="bookshelf.author")),
],
),
migrations.CreateModel(
name="Publisher",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=200)),
("website", models.URLField()),
],
),
migrations.CreateModel(
name="BookProperty",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("value", models.CharField(max_length=256)),
(
"book",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="property",
to="bookshelf.book",
),
),
(
"property",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="metadata.property",
),
),
],
options={
"verbose_name_plural": "Properties",
},
),
migrations.AddField(
model_name="book",
name="publisher",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="bookshelf.publisher"
),
),
migrations.AddField(
model_name="book",
name="tags",
field=models.ManyToManyField(
blank=True, related_name="bookshelf", to="metadata.tag"
),
),
]

View File

@@ -0,0 +1,142 @@
# Generated by Django 4.2.5 on 2023-10-01 21:33
from django.db import migrations, models
import django_countries.fields
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="book",
name="language",
field=models.CharField(
choices=[
("af", "Afrikaans"),
("ar", "Arabic"),
("ar-dz", "Algerian Arabic"),
("ast", "Asturian"),
("az", "Azerbaijani"),
("bg", "Bulgarian"),
("be", "Belarusian"),
("bn", "Bengali"),
("br", "Breton"),
("bs", "Bosnian"),
("ca", "Catalan"),
("ckb", "Central Kurdish (Sorani)"),
("cs", "Czech"),
("cy", "Welsh"),
("da", "Danish"),
("de", "German"),
("dsb", "Lower Sorbian"),
("el", "Greek"),
("en", "English"),
("en-au", "Australian English"),
("en-gb", "British English"),
("eo", "Esperanto"),
("es", "Spanish"),
("es-ar", "Argentinian Spanish"),
("es-co", "Colombian Spanish"),
("es-mx", "Mexican Spanish"),
("es-ni", "Nicaraguan Spanish"),
("es-ve", "Venezuelan Spanish"),
("et", "Estonian"),
("eu", "Basque"),
("fa", "Persian"),
("fi", "Finnish"),
("fr", "French"),
("fy", "Frisian"),
("ga", "Irish"),
("gd", "Scottish Gaelic"),
("gl", "Galician"),
("he", "Hebrew"),
("hi", "Hindi"),
("hr", "Croatian"),
("hsb", "Upper Sorbian"),
("hu", "Hungarian"),
("hy", "Armenian"),
("ia", "Interlingua"),
("id", "Indonesian"),
("ig", "Igbo"),
("io", "Ido"),
("is", "Icelandic"),
("it", "Italian"),
("ja", "Japanese"),
("ka", "Georgian"),
("kab", "Kabyle"),
("kk", "Kazakh"),
("km", "Khmer"),
("kn", "Kannada"),
("ko", "Korean"),
("ky", "Kyrgyz"),
("lb", "Luxembourgish"),
("lt", "Lithuanian"),
("lv", "Latvian"),
("mk", "Macedonian"),
("ml", "Malayalam"),
("mn", "Mongolian"),
("mr", "Marathi"),
("ms", "Malay"),
("my", "Burmese"),
("nb", "Norwegian Bokmål"),
("ne", "Nepali"),
("nl", "Dutch"),
("nn", "Norwegian Nynorsk"),
("os", "Ossetic"),
("pa", "Punjabi"),
("pl", "Polish"),
("pt", "Portuguese"),
("pt-br", "Brazilian Portuguese"),
("ro", "Romanian"),
("ru", "Russian"),
("sk", "Slovak"),
("sl", "Slovenian"),
("sq", "Albanian"),
("sr", "Serbian"),
("sr-latn", "Serbian Latin"),
("sv", "Swedish"),
("sw", "Swahili"),
("ta", "Tamil"),
("te", "Telugu"),
("tg", "Tajik"),
("th", "Thai"),
("tk", "Turkmen"),
("tr", "Turkish"),
("tt", "Tatar"),
("udm", "Udmurt"),
("uk", "Ukrainian"),
("ur", "Urdu"),
("uz", "Uzbek"),
("vi", "Vietnamese"),
("zh-hans", "Simplified Chinese"),
("zh-hant", "Traditional Chinese"),
],
default="en",
max_length=7,
),
),
migrations.AddField(
model_name="book",
name="numbers_of_pages",
field=models.SmallIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name="publisher",
name="country",
field=django_countries.fields.CountryField(blank=True, max_length=2),
),
migrations.AlterField(
model_name="book",
name="ISBN",
field=models.CharField(blank=True, max_length=13),
),
migrations.AlterField(
model_name="publisher",
name="website",
field=models.URLField(blank=True),
),
]

View File

@@ -0,0 +1,51 @@
# Generated by Django 4.2.5 on 2023-10-02 10:36
from django.db import migrations, models
import django.db.models.deletion
import ram.utils
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0002_book_language_book_numbers_of_pages_and_more"),
]
operations = [
migrations.CreateModel(
name="BookImage",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("order", models.PositiveIntegerField(default=0)),
(
"image",
models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/books/",
),
),
(
"book",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="image",
to="bookshelf.book",
),
),
],
options={
"ordering": ["order"],
"abstract": False,
},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.2.5 on 2023-10-02 20:38
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0003_bookimage"),
]
operations = [
migrations.RenameField(
model_name="book",
old_name="numbers_of_pages",
new_name="number_of_pages",
),
]

View File

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

View File

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

View File

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

View File

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

View File

93
ram/bookshelf/models.py Normal file
View File

@@ -0,0 +1,93 @@
from uuid import uuid4
from django.db import models
from django.conf import settings
from django.urls import reverse
from django_countries.fields import CountryField
from ckeditor_uploader.fields import RichTextUploadingField
from metadata.models import Tag
from ram.utils import DeduplicatedStorage
from ram.models import Image, PropertyInstance
class Publisher(models.Model):
name = models.CharField(max_length=200)
country = CountryField(blank=True)
website = models.URLField(blank=True)
class Meta:
ordering = ["name"]
def __str__(self):
return self.name
class Author(models.Model):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
class Meta:
ordering = ["last_name", "first_name"]
def __str__(self):
return f"{self.last_name}, {self.first_name}"
def short_name(self):
return f"{self.last_name} {self.first_name[0]}."
class Book(models.Model):
uuid = models.UUIDField(primary_key=True, default=uuid4, editable=False)
title = models.CharField(max_length=200)
authors = models.ManyToManyField(Author)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
ISBN = models.CharField(max_length=17, blank=True) # 13 + dashes
language = models.CharField(
max_length=7,
choices=settings.LANGUAGES,
default='en'
)
number_of_pages = models.SmallIntegerField(null=True, blank=True)
publication_year = models.SmallIntegerField(null=True, blank=True)
purchase_date = models.DateField(null=True, blank=True)
tags = models.ManyToManyField(
Tag, related_name="bookshelf", blank=True
)
notes = RichTextUploadingField(blank=True)
creation_time = models.DateTimeField(auto_now_add=True)
updated_time = models.DateTimeField(auto_now=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})
class BookImage(Image):
book = models.ForeignKey(
Book, on_delete=models.CASCADE, related_name="image"
)
image = models.ImageField(
upload_to="images/books/", # FIXME, find a better way to replace this
storage=DeduplicatedStorage,
null=True,
blank=True
)
class BookProperty(PropertyInstance):
book = models.ForeignKey(
Book,
on_delete=models.CASCADE,
null=False,
blank=False,
related_name="property",
)

View File

@@ -0,0 +1,26 @@
from rest_framework import serializers
from bookshelf.models import Book, Author, Publisher
from metadata.serializers import TagSerializer
class AuthorSerializer(serializers.ModelSerializer):
class Meta:
model = Author
fields = "__all__"
class PublisherSerializer(serializers.ModelSerializer):
class Meta:
model = Publisher
fields = "__all__"
class BookSerializer(serializers.ModelSerializer):
authors = AuthorSerializer(many=True)
publisher = PublisherSerializer()
tags = TagSerializer(many=True)
class Meta:
model = Book
fields = "__all__"
read_only_fields = ("creation_time", "updated_time")

3
ram/bookshelf/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

7
ram/bookshelf/urls.py Normal file
View File

@@ -0,0 +1,7 @@
from django.urls import path
from bookshelf.views import BookList, BookGet
urlpatterns = [
path("book/list", BookList.as_view()),
path("book/get/<str:uuid>", BookGet.as_view()),
]

18
ram/bookshelf/views.py Normal file
View File

@@ -0,0 +1,18 @@
from rest_framework.generics import ListAPIView, RetrieveAPIView
from rest_framework.schemas.openapi import AutoSchema
from bookshelf.models import Book
from bookshelf.serializers import BookSerializer
class BookList(ListAPIView):
queryset = Book.objects.all()
serializer_class = BookSerializer
class BookGet(RetrieveAPIView):
queryset = Book.objects.all()
serializer_class = BookSerializer
lookup_field = "uuid"
schema = AutoSchema(operation_id_base="retrieveBookByUUID")

View File

@@ -4,6 +4,7 @@ from adminsortable2.admin import SortableAdminMixin
from metadata.models import ( from metadata.models import (
Property, Property,
Decoder, Decoder,
DecoderDocument,
Scale, Scale,
Manufacturer, Manufacturer,
Company, Company,
@@ -14,11 +15,20 @@ from metadata.models import (
@admin.register(Property) @admin.register(Property)
class PropertyAdmin(admin.ModelAdmin): class PropertyAdmin(admin.ModelAdmin):
list_display = ("name", "private")
search_fields = ("name",) search_fields = ("name",)
class DecoderDocInline(admin.TabularInline):
model = DecoderDocument
min_num = 0
extra = 0
classes = ["collapse"]
@admin.register(Decoder) @admin.register(Decoder)
class DecoderAdmin(admin.ModelAdmin): class DecoderAdmin(admin.ModelAdmin):
inlines = (DecoderDocInline,)
readonly_fields = ("image_thumbnail",) readonly_fields = ("image_thumbnail",)
list_display = ("__str__", "sound") list_display = ("__str__", "sound")
list_filter = ("manufacturer", "sound") list_filter = ("manufacturer", "sound")

View File

@@ -0,0 +1,59 @@
# Generated by Django 4.2 on 2023-09-30 21:54
from django.db import migrations, models
import django.db.models.deletion
import ram.utils
class Migration(migrations.Migration):
dependencies = [
("metadata", "0011_company_slug_and_more"),
]
operations = [
migrations.AlterField(
model_name="decoder",
name="manufacturer",
field=models.ForeignKey(
limit_choices_to={"category": "accessory"},
on_delete=django.db.models.deletion.CASCADE,
to="metadata.manufacturer",
),
),
migrations.CreateModel(
name="DecoderDocument",
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(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage(),
upload_to="files/",
),
),
(
"decoder",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="document",
to="metadata.decoder",
),
),
],
options={
"unique_together": {("decoder", "file")},
},
),
]

View File

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

View File

@@ -0,0 +1,20 @@
# Generated by Django 4.2.6 on 2023-10-10 12:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("metadata", "0013_decoderdocument_private"),
]
operations = [
migrations.AlterModelOptions(
name="decoder",
options={"ordering": ["manufacturer", "name"]},
),
migrations.AlterModelOptions(
name="tag",
options={"ordering": ["name"]},
),
]

View File

@@ -1,11 +1,10 @@
from urllib.parse import quote
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.conf import settings from django.conf import settings
from django.dispatch.dispatcher import receiver from django.dispatch.dispatcher import receiver
from django_countries.fields import CountryField from django_countries.fields import CountryField
from ram.models import Document
from ram.utils import DeduplicatedStorage, get_image_preview, slugify from ram.utils import DeduplicatedStorage, get_image_preview, slugify
@@ -88,7 +87,7 @@ class Decoder(models.Model):
manufacturer = models.ForeignKey( manufacturer = models.ForeignKey(
Manufacturer, Manufacturer,
on_delete=models.CASCADE, on_delete=models.CASCADE,
limit_choices_to={"category": "model"}, limit_choices_to={"category": "accessory"},
) )
version = models.CharField(max_length=64, blank=True) version = models.CharField(max_length=64, blank=True)
sound = models.BooleanField(default=False) sound = models.BooleanField(default=False)
@@ -96,6 +95,9 @@ class Decoder(models.Model):
upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True
) )
class Meta(object):
ordering = ["manufacturer", "name"]
def __str__(self): def __str__(self):
return "{0} - {1}".format(self.manufacturer, self.name) return "{0} - {1}".format(self.manufacturer, self.name)
@@ -105,6 +107,15 @@ class Decoder(models.Model):
image_thumbnail.short_description = "Preview" image_thumbnail.short_description = "Preview"
class DecoderDocument(Document):
decoder = models.ForeignKey(
Decoder, on_delete=models.CASCADE, related_name="document"
)
class Meta:
unique_together = ("decoder", "file")
class Scale(models.Model): class Scale(models.Model):
scale = models.CharField(max_length=32, unique=True) scale = models.CharField(max_length=32, unique=True)
slug = models.CharField(max_length=32, unique=True, editable=False) slug = models.CharField(max_length=32, unique=True, editable=False)
@@ -155,6 +166,9 @@ class Tag(models.Model):
name = models.CharField(max_length=128, unique=True) name = models.CharField(max_length=128, unique=True)
slug = models.CharField(max_length=128, unique=True) slug = models.CharField(max_length=128, unique=True)
class Meta(object):
ordering = ["name"]
def __str__(self): def __str__(self):
return self.name return self.name
@@ -167,7 +181,6 @@ class Tag(models.Model):
) )
@receiver(models.signals.pre_save, sender=Manufacturer) @receiver(models.signals.pre_save, sender=Manufacturer)
@receiver(models.signals.pre_save, sender=Company) @receiver(models.signals.pre_save, sender=Company)
@receiver(models.signals.pre_save, sender=Scale) @receiver(models.signals.pre_save, sender=Scale)

View File

@@ -3,6 +3,7 @@ from solo.admin import SingletonModelAdmin
from portal.models import SiteConfiguration, Flatpage from portal.models import SiteConfiguration, Flatpage
@admin.register(SiteConfiguration) @admin.register(SiteConfiguration)
class SiteConfigurationAdmin(SingletonModelAdmin): class SiteConfigurationAdmin(SingletonModelAdmin):
fieldsets = ( fieldsets = (

View File

@@ -0,0 +1,5 @@
from django.conf import settings
def default_card_image(request):
return {"DEFAULT_CARD_IMAGE": settings.DEFAULT_CARD_IMAGE}

View File

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

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="420"
height="226"
viewBox="0 0 5.8333333 3.1388889"
version="1.1"
id="svg1"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1">
<linearGradient
id="linearGradient1">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0.59606808"
id="stop2" />
<stop
style="stop-color:#f5f5f5;stop-opacity:1;"
offset="1"
id="stop1" />
</linearGradient>
<linearGradient
xlink:href="#linearGradient1"
id="linearGradient2"
x1="0"
y1="1.5694444"
x2="5.8333335"
y2="1.5694444"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.53809522,0,0,1.8584071,-3.1388889,0)"
spreadMethod="pad" />
</defs>
<g
id="layer1">
<rect
style="mix-blend-mode:normal;fill:url(#linearGradient2);stroke:none;stroke-width:0.801535"
id="rect1"
width="3.1388888"
height="5.8333335"
x="-3.1388888"
y="-2.220446e-16"
transform="rotate(-90)" />
<text
xml:space="preserve"
style="font-weight:bold;font-size:0.444444px;line-height:1.25;font-family:system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue','Noto Sans','Liberation Sans',Arial,sans-serif;-inkscape-font-specification:'Noto Sans Bold';letter-spacing:0px;word-spacing:0px;stroke-width:0.0138889"
x="1.5366687"
y="1.6798887"
id="text1"><tspan
id="tspan1"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:0.444444px;font-family:system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue','Noto Sans','Liberation Sans',Arial,sans-serif;-inkscape-font-specification:'Noto Sans';fill:#dee2e6;fill-opacity:1;stroke-width:0.0138889"
x="1.5366687"
y="1.6798887">Coming soon</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -7,6 +7,16 @@ html[data-bs-theme='dark'] .navbar svg {
width: 100%; width: 100%;
} }
td > img.logo {
max-width: 200px;
max-height: 48px;
}
td > img.logo-xl {
max-width: 400px;
max-height: 96px;
}
.btn > span { .btn > span {
display: inline-block; display: inline-block;
} }
@@ -20,6 +30,14 @@ a.badge, a.badge:hover {
padding: 0; padding: 0;
} }
.w-33 {
width: 33% !important;
}
.table-group-divider {
border-top: calc(var(--bs-border-width) * 3) solid var(--bs-border-color);
}
#nav-notes > p { #nav-notes > p {
padding: .5rem; padding: .5rem;
} }

View File

@@ -121,12 +121,12 @@
</head> </head>
<body> <body>
<header> <header>
<nav class="navbar navbar-expand-lg bg-body-tertiary shadow-sm"> <nav class="navbar navbar-expand-sm bg-body-tertiary shadow-sm">
<div class="container d-flex"> <div class="container d-flex">
<div class="me-auto"> <div class="me-auto">
<a href="{% url 'index' %}" class="navbar-brand d-flex align-items-center"> <a href="{% url 'index' %}" class="navbar-brand d-flex align-items-center">
<svg class="me-2" width="26" height="16" enable-background="new 0 0 26 26" version="1" viewBox="0 0 26 16" xmlns="http://www.w3.org/2000/svg"> <svg class="me-2" width="26" height="16" viewBox="0 0 26 16" xmlns="http://www.w3.org/2000/svg">
<path d="m2.8125 0.0010991a1.0001 1.0001 0 0 0-0.8125 1c0 0.55455-0.44545 1-1 1a1.0001 1.0001 0 0 0-1 1v10a1.0001 1.0001 0 0 0 1 1c0.55455 0 1 0.44546 1 1a1.0001 1.0001 0 0 0 1 1h20a1.0001 1.0001 0 0 0 1-1c0-0.55454 0.44546-1 1-1a1.0001 1.0001 0 0 0 1-1v-10a1.0001 1.0001 0 0 0-1-1c-0.55454 0-1-0.44545-1-1a1.0001 1.0001 0 0 0-1-1h-20a1.0001 1.0001 0 0 0-0.09375 0 1.0001 1.0001 0 0 0-0.09375 0zm0.78125 2h14.406v1h2v-1h2.4062c0.30628 0.76906 0.82469 1.2875 1.5938 1.5938v8.8125c-0.76906 0.30628-1.2875 0.82469-1.5938 1.5938h-2.4062v-1h-2v1h-14.406c-0.30628-0.76906-0.82469-1.2875-1.5938-1.5938v-8.8125c0.76906-0.30628 1.2875-0.82469 1.5938-1.5938zm14.406 2v2h2v-2zm0 3v2h2v-2zm0 3v2h2v-2z" enable-background="accumulate" overflow="visible" stroke-width="2" /> <path d="m2.8125 0.0010991a1.0001 1.0001 0 0 0-0.8125 1c0 0.55455-0.44545 1-1 1a1.0001 1.0001 0 0 0-1 1v10a1.0001 1.0001 0 0 0 1 1c0.55455 0 1 0.44546 1 1a1.0001 1.0001 0 0 0 1 1h20a1.0001 1.0001 0 0 0 1-1c0-0.55454 0.44546-1 1-1a1.0001 1.0001 0 0 0 1-1v-10a1.0001 1.0001 0 0 0-1-1c-0.55454 0-1-0.44545-1-1a1.0001 1.0001 0 0 0-1-1h-20a1.0001 1.0001 0 0 0-0.09375 0 1.0001 1.0001 0 0 0-0.09375 0zm0.78125 2h14.406v1h2v-1h2.4062c0.30628 0.76906 0.82469 1.2875 1.5938 1.5938v8.8125c-0.76906 0.30628-1.2875 0.82469-1.5938 1.5938h-2.4062v-1h-2v1h-14.406c-0.30628-0.76906-0.82469-1.2875-1.5938-1.5938v-8.8125c0.76906-0.30628 1.2875-0.82469 1.5938-1.5938zm14.406 2v2h2v-2zm0 3v2h2v-2zm0 3v2h2v-2z" stroke-width="2" />
<style> <style>
path { path {
text-indent:0; text-indent:0;
@@ -146,32 +146,34 @@
<nav class="navbar navbar-expand-lg"> <nav class="navbar navbar-expand-lg">
<div class="container-fluid g-0"> <div class="container-fluid g-0">
<a class="navbar-brand" href="{% url 'index' %}">Home</a> <a class="navbar-brand" href="{% url 'index' %}">Home</a>
<div class="navbar-collapse" id="navbarSupportedContent"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0"> <ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item dropdown"> <li class="nav-item">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <a class="nav-link" href="{% url 'roster' %}">Roster</a>
Roster
</a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<li><a class="dropdown-item" href="{% url 'roster' %}">Rolling stock</a></li>
<li><a class="dropdown-item" href="{% url 'companies' %}">Companies</a></li>
<li><a class="dropdown-item" href="{% url 'types' %}">Types</a></li>
<li><a class="dropdown-item" href="{% url 'scales' %}">Scales</a></li>
</ul>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'consists' %}">Consists</a> <a class="nav-link" href="{% url 'consists' %}">Consists</a>
</li> </li>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" id="filterDropdownMenu" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Manufacturers Search by
</a> </a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink"> <ul class="dropdown-menu" aria-labelledby="filterDropdownMenu">
<li><a class="dropdown-item" href="{% url 'manufacturers' category='model' %}">Models</a></li> <li class="ps-2 text-secondary">Model</li>
<li><a class="dropdown-item" href="{% url 'manufacturers' category='real' %}">Real</a></li> <li><a class="dropdown-item" href="{% url 'scales' %}">Scale</a></li>
<li><a class="dropdown-item" href="{% url 'manufacturers' category='model' %}">Manufacturer</a></li>
<li><hr class="dropdown-divider"></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 'companies' %}">Company</a></li>
<li><a class="dropdown-item" href="{% url 'manufacturers' category='real' %}">Manufacturer</a></li>
</ul> </ul>
</li> </li>
{% show_menu %} {% show_bookshelf_menu %}
{% show_flatpages_menu %}
</ul> </ul>
{% include 'includes/search.html' %} {% include 'includes/search.html' %}
</div> </div>
@@ -191,7 +193,7 @@
<div class="container"> <div class="container">
{% block carousel %} {% block carousel %}
{% endblock %} {% endblock %}
<a id="rolling-stock"></a> <a id="main-content"></a>
{% block cards_layout %} {% block cards_layout %}
{% endblock %} {% endblock %}
</div> </div>

View File

@@ -0,0 +1,123 @@
{% extends 'base.html' %}
{% block header %}
{% if book.tags.all %}
<p><small>Tags:</small>
{% for t in book.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
{{ t.name }}</a>{# new line is required #}
{% endfor %}
</p>
{% endif %}
<small class="text-muted">Updated {{ book.updated_time | date:"M d, Y H:i" }}</small>
{% endblock %}
{% block carousel %}
<div class="row">
<div id="carouselControls" class="carousel carousel-dark slide" data-bs-ride="carousel" data-bs-interval="10000">
<div class="carousel-inner">
{% for t in book.image.all %}
{% if forloop.first %}
<div class="carousel-item active">
{% else %}
<div class="carousel-item">
{% endif %}
<img src="{{ t.image.url }}" class="d-block w-100 rounded img-thumbnail" alt="...">
</div>
{% endfor %}
</div>
{% if book.image.count > 1 %}
<button class="carousel-control-prev" type="button" data-bs-target="#carouselControls" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Previous</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#carouselControls" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Next</span>
</button>
{% endif %}
</div>
</div>
{% endblock %}
{% block cards %}
{% endblock %}
{% block extra_content %}
<section class="py-4 text-start container">
<div class="row">
<div class="mx-auto">
<nav class="nav nav-tabs flex-column flex-md-row mb-2" id="nav-tab">
<button class="nav-link active" id="nav-summary-tab" data-bs-toggle="tab" data-bs-target="#nav-summary" type="button" role="tab" aria-controls="nav-summary" aria-selected="true">Summary</button>
{% if book.notes %}<button class="nav-link" id="nav-notes-tab" data-bs-toggle="tab" data-bs-target="#nav-notes" type="button" role="tab" aria-controls="nav-notes" aria-selected="false">Notes</button>{% endif %}
</nav>
<div class="tab-content" id="nav-tabContent">
<div class="tab-pane show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab">
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Book</th>
</tr>
</thead>
<tbody class="table-group-divider">
<tr>
<th class="w-33" scope="row">Title</th>
<td>{{ book.title }}</td>
</tr>
<tr>
<th scope="row">Authors</th>
<td>
<ul class="mb-0 list-unstyled">{% for a in book.authors.all %}<li>{{ a }}</li>{% endfor %}</ul>
</td>
</tr>
<tr>
<th scope="row">Publisher</th>
<td>{{ book.publisher }}</td>
</tr>
<tr>
<th scope="row">ISBN</th>
<td>{{ book.ISBN|default:"-" }}</td>
</tr>
<tr>
<th scope="row">Language</th>
<td>{{ book.get_language_display }}</td>
</tr>
<tr>
<th scope="row">Number of pages</th>
<td>{{ book.number_of_pages|default:"-" }}</td>
</tr>
<tr>
<th scope="row">Publication year</th>
<td>{{ book.publication_year|default:"-" }}</td>
</tr>
<tr>
<th scope="row">Purchase date</th>
<td>{{ book.purchase_date|default:"-" }}</td>
</tr>
</tbody>
</table>
{% if book_properties %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Properties</th>
</tr>
</thead>
<tbody class="table-group-divider">
{% for p in book_properties %}
<tr>
<th class="w-33" scope="row">{{ p.property }}</th>
<td>{{ p.value }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
<div class="tab-pane" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab">
{{ book.notes | safe }}
</div>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:bookshelf_book_change' book.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</section>
{% endblock %}

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 'books_pagination' 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 '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">Next</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">Next</span>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,10 @@
{% if bookshelf_menu %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="bookshelfDropdownMenuLink" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Bookshelf
</a>
<ul class="dropdown-menu" aria-labelledby="bookshelfDropdownMenuLink">
<li><a class="dropdown-item" href="{% url 'books' %}">Books</a></li>
</ul>
</li>
{% endif %}

View File

@@ -1,100 +1,26 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block header %} {% block header %}
<p class="lead text-muted">Results found: {{ matches }}</p> <p class="lead text-muted">Results found: {{ matches }}</p>
{% endblock %} {% endblock %}
{% block cards_layout %} {% block cards_layout %}
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-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 %}
<div class="col"> {% if d.type == "rolling_stock" %}
<div class="card shadow-sm"> {% include "cards/roster.html" %}
{% for i in d.image.all %} {% elif d.type == "company" %}
{% if forloop.first %}<a href="{{d.get_absolute_url}}"><img class="card-img-top" src="{{ i.image.url }}" alt="Card image cap"></a>{% endif %} {% include "cards/company.html" %}
{% endfor %} {% elif d.type == "rolling_stock_type" %}
<div class="card-body"> {% include "cards/rolling_stock_type.html" %}
<p class="card-text" style="position: relative;"> {% elif d.type == "scale" %}
<strong>{{ d }}</strong> {% include "cards/scale.html" %}
<a class="stretched-link" href="{{ d.get_absolute_url }}"></a> {% elif d.type == "consist" %}
</p> {% include "cards/consist.html" %}
{% if d.tags.all %} {% elif d.type == "manufacturer" %}
<p class="card-text"><small>Tags:</small> {% include "cards/manufacturer.html" %}
{% for t in d.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary"> {% elif d.type == "book" %}
{{ t.name }}</a>{# new line is required #} {% include "cards/book.html" %}
{% endfor %}
</p>
{% endif %} {% endif %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Data</th>
</tr>
</thead>
<tbody>
<tr>
<th width="35%" scope="row">Type</th>
<td>{{ d.rolling_class.type }}</td>
</tr>
<tr>
<th scope="row">Company</th>
<td>
<a href="{% url 'filtered' _filter="company" search=d.rolling_class.company.slug %}"><abbr title="{{ d.rolling_class.company.extended_name }}">{{ d.rolling_class.company }}</abbr></a>
</td>
</tr>
<tr>
<th scope="row">Class</th>
<td>{{ d.rolling_class.identifier }}</td>
</tr>
<tr>
<th scope="row">Road number</th>
<td>{{ d.road_number }}</td>
</tr>
<tr>
<th scope="row">Era</th>
<td>{{ d.era }}</td>
</tr>
<tr>
<th width="35%" scope="row">Manufacturer</th>
<td>{%if d.manufacturer %}
<a href="{% url 'filtered' _filter="manufacturer" search=d.manufacturer.slug %}">{{ d.manufacturer }}{% if d.manufacturer.website %}</a> <a href="{{ d.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
{% endif %}</td>
</tr>
<tr>
<th scope="row">Scale</th>
<td><a href="{% url 'filtered' _filter="scale" search=d.scale.slug %}"><abbr title="{{ d.scale.ratio }} - {{ d.scale.tracks }}">{{ d.scale }}</abbr></a></td>
</tr>
<tr>
<th scope="row">SKU</th>
<td>{{ d.sku }}</td>
</tr>
</tbody>
</table>
{% if d.decoder %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">DCC data</th>
</tr>
</thead>
<tbody>
<tr>
<th width="35%" scope="row">Decoder</th>
<td>{{ d.decoder }}</td>
</tr>
<tr>
<th scope="row">Address</th>
<td>{{ d.address }}</td>
</tr>
</tbody>
</table>
{% endif %}
<div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{{d.get_absolute_url}}">Show all data</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:roster_rollingstock_change' d.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}
</div> </div>

View File

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

View File

@@ -0,0 +1,46 @@
<div class="col">
<div class="card shadow-sm">
<div class="card-body">
<p class="card-text" style="position: relative;">
<strong>{{ d.item.name }}</strong>
</p>
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Company</th>
</tr>
</thead>
<tbody class="table-group-divider">
{% if d.item.logo %}
<tr>
<th class="w-33" scope="row">Logo</th>
<td><img class="logo" src="{{ d.item.logo.url }}" /></td>
</tr>
{% endif %}
<tr>
<th class="w-33" scope="row">Name</th>
<td>{{ d.item.extended_name }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Abbreviation</th>
<td>{{ d.item.name }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Country</th>
<td>{{ d.item.country.name }} <img src="{{ d.item.country.flag }}" alt="{{ d.item.country }}" />
</tr>
{% if d.item.freelance %}
<tr>
<th class="w-33" scope="row">Notes</th>
<td>A <em>freelance</em> company</td>
</tr>
{% endif %}
</tbody>
</table>
<div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="company" search=d.item.slug %}">Show all rolling stock</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_company_change' d.item.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>

View File

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

View File

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

View File

@@ -0,0 +1,29 @@
<div class="col">
<div class="card shadow-sm">
<div class="card-body">
<p class="card-text"><strong>{{ d.item }}</strong></p>
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Type</th>
</tr>
</thead>
<tbody class="table-group-divider">
<tr>
<th class="w-33" scope="row">Type</th>
<td>{{ d.item.type }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Category</th>
<td>{{ d.item.category | title}}</td>
</tr>
</tbody>
</table>
<div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="type" search=d.item.slug %}">Show all rolling stock</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_scale_change' d.item.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>

View File

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

View File

@@ -0,0 +1,36 @@
<div class="col">
<div class="card shadow-sm">
<div class="card-body">
<p class="card-text"><strong>{{ d.item }}</strong></p>
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Scale</th>
</tr>
</thead>
<tbody>
<tr>
<th class="w-33" scope="row">Name</th>
<td>{{ d.item.scale }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Ratio</th>
<td>{{ d.item.ratio }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Gauge</th>
<td>{{ d.item.gauge }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Tracks</th>
<td>{{ d.item.tracks }}</td>
</tr>
</tbody>
</table>
<div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="scale" search=d.item.slug %}">Show all rolling stock</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_scale_change' d.item.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>

View File

@@ -1,62 +1,11 @@
{% extends "cards.html" %} {% extends "cards.html" %}
{% block cards %}
{% for d in data %}
<div class="col">
<div class="card shadow-sm">
<div class="card-body">
<p class="card-text" style="position: relative;">
<strong>{{ d.name }}</strong>
</p>
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Company</th>
</tr>
</thead>
<tbody>
{% if d.logo %}
<tr>
<th width="35%" scope="row">Logo</th>
<td><img style="max-height: 48px" src="{{ d.logo.url }}" /></td>
</tr>
{% endif %}
<tr>
<th width="35%" scope="row">Name</th>
<td>{{ d.extended_name }}</td>
</tr>
<tr>
<th width="35%" scope="row">Abbreviation</th>
<td>{{ d.name }}</td>
</tr>
<tr>
<th width="35%" scope="row">Country</th>
<td>{{ d.country.name }} <img src="{{ d.country.flag }}" alt="{{ d.country }}" />
</tr>
{% if d.freelance %}
<tr>
<th width="35%" scope="row">Notes</th>
<td>A <em>freelance</em> company</td>
</tr>
{% endif %}
</tbody>
</table>
<div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="company" search=d.slug %}">Show all rolling stock</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_company_change' d.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}
{% block pagination %} {% block pagination %}
{% if data.has_other_pages %} {% if data.has_other_pages %}
<nav aria-label="Page navigation example"> <nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mt-4 mb-0"> <ul class="pagination justify-content-center mt-4 mb-0">
{% if data.has_previous %} {% if data.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'companies_pagination' page=data.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a> <a class="page-link" href="{% url 'companies_pagination' page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -66,19 +15,19 @@
{% for i in page_range %} {% for i in page_range %}
{% if data.number == i %} {% if data.number == i %}
<li class="page-item active"> <li class="page-item active">
<span class="page-link">{{ i }}</span></span> <span class="page-link">{{ i }}</span>
</li> </li>
{% else %} {% else %}
{% 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 'companies_pagination' page=i %}#rolling-stock">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'companies_pagination' page=i %}#main-content">{{ i }}</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if data.has_next %} {% if data.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'companies_pagination' page=data.next_page_number %}#rolling-stock" tabindex="-1">Next</a> <a class="page-link" href="{% url 'companies_pagination' page=data.next_page_number %}#main-content" tabindex="-1">Next</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">

View File

@@ -29,7 +29,7 @@
<ul class="pagination justify-content-center mt-4 mb-0"> <ul class="pagination justify-content-center mt-4 mb-0">
{% if data.has_previous %} {% if data.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'consist_pagination' uuid=consist.uuid page=data.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a> <a class="page-link" href="{% url 'consist_pagination' uuid=consist.uuid page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -39,19 +39,19 @@
{% for i in page_range %} {% for i in page_range %}
{% if data.number == i %} {% if data.number == i %}
<li class="page-item active"> <li class="page-item active">
<span class="page-link">{{ i }}</span></span> <span class="page-link">{{ i }}</span>
</li> </li>
{% else %} {% else %}
{% 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 'consist_pagination' uuid=consist.uuid page=i %}#rolling-stock">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'consist_pagination' uuid=consist.uuid 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 'consist_pagination' uuid=consist.uuid page=data.next_page_number %}#rolling-stock" tabindex="-1">Next</a> <a class="page-link" href="{% url 'consist_pagination' uuid=consist.uuid page=data.next_page_number %}#main-content" tabindex="-1">Next</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -66,24 +66,24 @@
<section class="py-4 text-start container"> <section class="py-4 text-start container">
<div class="row"> <div class="row">
<div class="mx-auto"> <div class="mx-auto">
<nav> <nav class="nav nav-tabs flex-column flex-md-row mb-2" id="nav-tab">
<div class="nav nav-tabs" id="nav-tab" role="tablist">
<button class="nav-link active" id="nav-summary-tab" data-bs-toggle="tab" data-bs-target="#nav-summary" type="button" role="tab" aria-controls="nav-summary" aria-selected="true">Summary</button> <button class="nav-link active" id="nav-summary-tab" data-bs-toggle="tab" data-bs-target="#nav-summary" type="button" role="tab" aria-controls="nav-summary" aria-selected="true">Summary</button>
{% if consist.notes %}<button class="nav-link" id="nav-notes-tab" data-bs-toggle="tab" data-bs-target="#nav-notes" type="button" role="tab" aria-controls="nav-notes" aria-selected="false">Notes</button>{% endif %} {% if consist.notes %}<button class="nav-link" id="nav-notes-tab" data-bs-toggle="tab" data-bs-target="#nav-notes" type="button" role="tab" aria-controls="nav-notes" aria-selected="false">Notes</button>{% endif %}
</div>
</nav> </nav>
<div class="tab-content" id="nav-tabContent"> <div class="tab-content" id="nav-tabContent">
<div class="tab-pane fade show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab"> <div class="tab-pane show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th colspan="2" scope="row">Data</th> <th colspan="2" scope="row">Data</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody class="table-group-divider">
<tr> <tr>
<th width="35%" scope="row">Company</th> <th class="w-33" scope="row">Company</th>
<td><abbr title="{{ consist.company.extended_name }}">{{ consist.company }}</abbr></td> <td>
<a href="{% url 'filtered' _filter="company" search=consist.company.slug %}">{{ consist.company }}</a> ({{ consist.company.extended_name }})
</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Era</th> <th scope="row">Era</th>
@@ -96,14 +96,14 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="tab-pane fade" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab"> <div class="tab-pane" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab">
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th scope="row">Notes</th> <th scope="row">Notes</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody class="table-group-divider">
<tr> <tr>
<td>{{ consist.notes | safe }}</td> <td>{{ consist.notes | safe }}</td>
</tr> </tr>

View File

@@ -1,75 +1,11 @@
{% extends "cards.html" %} {% extends "cards.html" %}
{% block cards %}
{% for d in data %}
<div class="col">
<div class="card shadow-sm">
<a href="{{ d.get_absolute_url }}">
{% if d.image %}
<img src="{{ d.image.url }}" alt="Card image cap">
{% else %}
{% with d.consist_item.first.rolling_stock as r %}
{% for i in r.image.all %}
{% if forloop.first %}<img src="{{ i.image.url }}" alt="Card image cap">{% endif %}
{% endfor %}
{% endwith %}
{% endif %}
</a>
<div class="card-body">
<p class="card-text" style="position: relative;">
<strong>{{ d }}</strong>
<a class="stretched-link" href="{{ d.get_absolute_url }}"></a>
</p>
{% if d.tags.all %}
<p class="card-text"><small>Tags:</small>
{% for t in d.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
{{ t.name }}</a>{# new line is required #}
{% endfor %}
</p>
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Consist data</th>
</tr>
</thead>
<tbody>
{% if d.address %}
<tr>
<th width="35%" scope="row">Address</th>
<td>{{ d.address }}</td>
</tr>
{% endif %}
<tr>
<th width="35%" scope="row">Company</th>
<td><abbr title="{{ d.company.extended_name }}">{{ d.company }}</abbr></td>
</tr>
<tr>
<th scope="row">Era</th>
<td>{{ d.era }}</td>
</tr>
<tr>
<th scope="row">Length</th>
<td>{{ d.consist_item.all | length }}</td>
</tr>
</tbody>
</table>
<div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{{ d.get_absolute_url }}">Show all data</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:consist_consist_change' d.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}
{% block pagination %} {% block pagination %}
{% if data.has_other_pages %} {% if data.has_other_pages %}
<nav aria-label="Page navigation example"> <nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mt-4 mb-0"> <ul class="pagination justify-content-center mt-4 mb-0">
{% if data.has_previous %} {% if data.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'consists_pagination' page=data.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a> <a class="page-link" href="{% url 'consists_pagination' page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -79,19 +15,19 @@
{% for i in page_range %} {% for i in page_range %}
{% if data.number == i %} {% if data.number == i %}
<li class="page-item active"> <li class="page-item active">
<span class="page-link">{{ i }}</span></span> <span class="page-link">{{ i }}</span>
</li> </li>
{% else %} {% else %}
{% 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 'consists_pagination' page=i %}#rolling-stock">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'consists_pagination' page=i %}#main-content">{{ i }}</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if data.has_next %} {% if data.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'consists_pagination' page=data.next_page_number %}#rolling-stock" tabindex="-1">Next</a> <a class="page-link" href="{% url 'consists_pagination' page=data.next_page_number %}#main-content" tabindex="-1">Next</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">

View File

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

View File

@@ -1,10 +1,10 @@
{% if menu %} {% if flatpages_menu %}
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" id="flatpageDropdownMenuLink" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Articles Articles
</a> </a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink"> <ul class="dropdown-menu" aria-labelledby="flatpageDropdownMenuLink">
{% for m in menu %} {% for m in flatpages_menu %}
<li><a class="dropdown-item" href="{{ m.get_absolute_url }}">{{ m.name }}</a></li> <li><a class="dropdown-item" href="{{ m.get_absolute_url }}">{{ m.name }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>

View File

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

View File

@@ -13,6 +13,6 @@
<div class="container"> <div class="container">
<p class="small text-muted">Made with ❤️ for 🚂 and <a href="https://github.com/daniviga/django-ram">django-ram</a> <p class="small text-muted">Made with ❤️ for 🚂 and <a href="https://github.com/daniviga/django-ram">django-ram</a>
{% if site_conf.show_version %}<br/>Version {{ site_conf.version }}{% endif %} {% if site_conf.show_version %}<br>Version {{ site_conf.version }}{% endif %}
</div> </div>
</footer> </footer>

View File

@@ -1,4 +1,7 @@
<div class="navbar-collapse justify-content-end" id="loginNavbar"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#loginNavbar" aria-controls="loginNavbar" aria-expanded="false" aria-label="Toggle navigation">
<span class="bi bi-person-fill-gear"></span>
</button>
<div class="collapse navbar-collapse justify-content-end" id="loginNavbar">
<ul class="navbar-nav"> <ul class="navbar-nav">
<li class="nav-item dropdown"> <li class="nav-item dropdown">
{% if request.user.is_staff %} {% if request.user.is_staff %}
@@ -10,10 +13,10 @@
<li><a class="dropdown-item" href="{% url 'admin:consist_consist_changelist' %}">Consists</a></li> <li><a class="dropdown-item" href="{% url 'admin:consist_consist_changelist' %}">Consists</a></li>
<li><a class="dropdown-item" href="{% url 'admin:app_list' 'metadata' %}">Metadata</a></li> <li><a class="dropdown-item" href="{% url 'admin:app_list' 'metadata' %}">Metadata</a></li>
<li><a class="dropdown-item" href="{% url 'admin:portal_flatpage_changelist' %}">Pages</a></li> <li><a class="dropdown-item" href="{% url 'admin:portal_flatpage_changelist' %}">Pages</a></li>
<li><a class="dropdown-item" href="{% url 'admin:app_list' 'bookshelf' %}">Bookshelf</a></li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{% url 'admin:index' %}">Admin</a></li> <li><a class="dropdown-item" href="{% url 'admin:index' %}">Admin</a></li>
<li><a class="dropdown-item" href="{% url 'admin:portal_siteconfiguration_changelist' %}">Site configuration</a></li> <li><a class="dropdown-item" href="{% url 'admin:portal_siteconfiguration_changelist' %}">Site configuration</a></li>
<li><a class="dropdown-item" href="{% url 'admin:driver_driverconfiguration_changelist' %}">DCC configuration</a></li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="{% url 'admin:logout' %}?next={{ request.path }}">Logout</a></li> <li><a class="dropdown-item text-danger" href="{% url 'admin:logout' %}?next={{ request.path }}">Logout</a></li>
</ul> </ul>
@@ -22,7 +25,7 @@
{% endif %} {% endif %}
</li> </li>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="theme-icon-active nav-link dropdown-toggle" href="#" type="button" id="bd-theme" data-bs-toggle="dropdown" aria-expanded="false"> <a class="theme-icon-active nav-link dropdown-toggle" href="#" role="button" id="bd-theme" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-circle-half"></i> <i class="bi bi-circle-half"></i>
</a> </a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="bd-theme"> <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="bd-theme">

View File

@@ -1,55 +1,12 @@
{% extends "cards.html" %} {% extends "cards.html" %}
{% block cards %}
{% for d in data %}
<div class="col">
<div class="card shadow-sm">
<div class="card-body">
<p class="card-text" style="position: relative;">
<strong>{{ d.name }}</strong>
</p>
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Manufacturer</th>
</tr>
</thead>
<tbody>
{% if d.logo %}
<tr>
<th width="35%" scope="row">Logo</th>
<td><img style="max-height: 48px" src="{{ d.logo.url }}" /></td>
</tr>
{% endif %}
{% if d.website %}
<tr>
<th width="35%" scope="row">Website</th>
<td><a href="{{ d.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a></td>
</tr>
{% endif %}
<tr>
<th width="35%" scope="row">Category</th>
<td>{{ d.category | title }}</td>
</tr>
</tbody>
</table>
<div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="manufacturer" search=d.slug %}">Show all rolling stock</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_manufacturer_change' d.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}
{% block pagination %} {% block pagination %}
{% if data.has_other_pages %} {% if data.has_other_pages %}
{% with data.0.category as c %} {% with data.0.item.category as c %}
<nav aria-label="Page navigation example"> <nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mt-4 mb-0"> <ul class="pagination justify-content-center mt-4 mb-0">
{% if data.has_previous %} {% if data.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'manufacturers_pagination' category=c page=data.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a> <a class="page-link" href="{% url 'manufacturers_pagination' category=c page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -59,19 +16,19 @@
{% for i in page_range %} {% for i in page_range %}
{% if data.number == i %} {% if data.number == i %}
<li class="page-item active"> <li class="page-item active">
<span class="page-link">{{ i }}</span></span> <span class="page-link">{{ i }}</span>
</li> </li>
{% else %} {% else %}
{% 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 'manufacturers_pagination' category=c page=i %}#rolling-stock">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'manufacturers_pagination' category=c 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 'manufacturers_pagination' category=c page=data.next_page_number %}#rolling-stock" tabindex="-1">Next</a> <a class="page-link" href="{% url 'manufacturers_pagination' category=c page=data.next_page_number %}#main-content" tabindex="-1">Next</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">

View File

@@ -43,39 +43,40 @@
<section class="py-4 text-start container"> <section class="py-4 text-start container">
<div class="row"> <div class="row">
<div class="mx-auto"> <div class="mx-auto">
<nav> <nav class="nav nav-tabs flex-column flex-md-row mb-2" id="nav-tab">
<div class="nav nav-tabs" id="nav-tab" role="tablist">
<button class="nav-link active" id="nav-summary-tab" data-bs-toggle="tab" data-bs-target="#nav-summary" type="button" role="tab" aria-controls="nav-summary" aria-selected="true">Summary</button> <button class="nav-link active" id="nav-summary-tab" data-bs-toggle="tab" data-bs-target="#nav-summary" type="button" role="tab" aria-controls="nav-summary" aria-selected="true">Summary</button>
<button class="nav-link" id="nav-model-tab" data-bs-toggle="tab" data-bs-target="#nav-model" type="button" role="tab" aria-controls="nav-model" aria-selected="false">Model data</button> <button class="nav-link" id="nav-model-tab" data-bs-toggle="tab" data-bs-target="#nav-model" type="button" role="tab" aria-controls="nav-model" aria-selected="false">Model</button>
<button class="nav-link" id="nav-class-tab" data-bs-toggle="tab" data-bs-target="#nav-class" type="button" role="tab" aria-controls="nav-class" aria-selected="false">Class data</button> <button class="nav-link" id="nav-class-tab" data-bs-toggle="tab" data-bs-target="#nav-class" type="button" role="tab" aria-controls="nav-class" aria-selected="false">Class</button>
<button class="nav-link" id="nav-company-tab" data-bs-toggle="tab" data-bs-target="#nav-company" type="button" role="tab" aria-controls="nav-company" aria-selected="false">Company</button>
{% if rolling_stock.decoder %}<button class="nav-link" id="nav-dcc-tab" data-bs-toggle="tab" data-bs-target="#nav-dcc" type="button" role="tab" aria-controls="nav-dcc" aria-selected="false">DCC</button>{% endif %} {% if rolling_stock.decoder %}<button class="nav-link" id="nav-dcc-tab" data-bs-toggle="tab" data-bs-target="#nav-dcc" type="button" role="tab" aria-controls="nav-dcc" aria-selected="false">DCC</button>{% endif %}
{% if 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 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 rolling_stock.document.count > 0 %}<button class="nav-link" id="nav-documents-tab" data-bs-toggle="tab" data-bs-target="#nav-documents" type="button" role="tab" aria-controls="nav-documents" aria-selected="false">Documents</button>{% endif %} {% if 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 rolling_stock_journal.count > 0 %}<button class="nav-link" id="nav-journal-tab" data-bs-toggle="tab" data-bs-target="#nav-journal" type="button" role="tab" aria-controls="nav-journal" aria-selected="false">Journal</button>{% endif %}
</div>
</nav> </nav>
{% with class=rolling_stock.rolling_class company=rolling_stock.rolling_class.company %}
<div class="tab-content" id="nav-tabContent"> <div class="tab-content" id="nav-tabContent">
<div class="tab-pane fade show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab"> <div class="tab-pane show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th colspan="2" scope="row">Data</th> <th colspan="2" scope="row">Data</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody class="table-group-divider">
<tr> <tr>
<th width="35%" scope="row">Type</th> <th class="w-33" scope="row">Type</th>
<td>{{ rolling_stock.rolling_class.type }}</td> <td>{{ class.type }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Company</th> <th scope="row">Company</th>
<td> <td>
<a href="{% url 'filtered' _filter="company" search=rolling_stock.rolling_class.company.slug %}"><abbr title="{{ rolling_stock.rolling_class.company.extended_name }}">{{ rolling_stock.rolling_class.company }}</abbr></a> <a href="{% url 'filtered' _filter="company" search=company.slug %}">{{ company }}</a> ({{ company.extended_name }})
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Class</th> <th scope="row">Class</th>
<td>{{ rolling_stock.rolling_class.identifier }}</td> <td>{{ class.identifier }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Road number</th> <th scope="row">Road number</th>
@@ -93,9 +94,9 @@
<th colspan="2" scope="row">Model data</th> <th colspan="2" scope="row">Model data</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody class="table-group-divider">
<tr> <tr>
<th width="35%" scope="row">Manufacturer</th> <th class="w-33" scope="row">Manufacturer</th>
<td>{%if rolling_stock.manufacturer %} <td>{%if rolling_stock.manufacturer %}
<a href="{% url 'filtered' _filter="manufacturer" search=rolling_stock.manufacturer.slug %}">{{ rolling_stock.manufacturer }}{% if rolling_stock.manufacturer.website %}</a> <a href="{{ rolling_stock.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %} <a href="{% url 'filtered' _filter="manufacturer" search=rolling_stock.manufacturer.slug %}">{{ rolling_stock.manufacturer }}{% if rolling_stock.manufacturer.website %}</a> <a href="{{ rolling_stock.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
{% endif %}</td> {% endif %}</td>
@@ -105,8 +106,8 @@
<td><a href="{% url 'filtered' _filter="scale" search=rolling_stock.scale %}"><abbr title="{{ rolling_stock.scale.ratio }} - {{ rolling_stock.scale.tracks }}">{{ rolling_stock.scale }}</abbr></a></td> <td><a href="{% url 'filtered' _filter="scale" search=rolling_stock.scale %}"><abbr title="{{ rolling_stock.scale.ratio }} - {{ rolling_stock.scale.tracks }}">{{ rolling_stock.scale }}</abbr></a></td>
</tr> </tr>
<tr> <tr>
<th scope="row">SKU</th> <th scope="row">Item number</th>
<td>{{ rolling_stock.sku }}</td> <td>{{ rolling_stock.item_number }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -117,9 +118,9 @@
<th colspan="2" scope="row">DCC data</th> <th colspan="2" scope="row">DCC data</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody class="table-group-divider">
<tr> <tr>
<th width="35%" scope="row">Interface</th> <th class="w-33" scope="row">Interface</th>
<td>{{ rolling_stock.get_decoder_interface_display }}</td> <td>{{ rolling_stock.get_decoder_interface_display }}</td>
</tr> </tr>
{% if rolling_stock.decoder %} {% if rolling_stock.decoder %}
@@ -136,16 +137,16 @@
</table> </table>
{% endif %} {% endif %}
</div> </div>
<div class="tab-pane fade" id="nav-model" role="tabpanel" aria-labelledby="nav-model-tab"> <div class="tab-pane" id="nav-model" role="tabpanel" aria-labelledby="nav-model-tab">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th colspan="2" scope="row">Model data</th> <th colspan="2" scope="row">Model data</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody class="table-group-divider">
<tr> <tr>
<th width="35%" scope="row">Manufacturer</th> <th class="w-33" scope="row">Manufacturer</th>
<td>{%if rolling_stock.manufacturer %} <td>{%if rolling_stock.manufacturer %}
<a href="{% url 'filtered' _filter="manufacturer" search=rolling_stock.manufacturer.slug %}">{{ rolling_stock.manufacturer }}{% if rolling_stock.manufacturer.website %}</a> <a href="{{ rolling_stock.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %} <a href="{% url 'filtered' _filter="manufacturer" search=rolling_stock.manufacturer.slug %}">{{ rolling_stock.manufacturer }}{% if rolling_stock.manufacturer.website %}</a> <a href="{{ rolling_stock.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
{% endif %}</td> {% endif %}</td>
@@ -155,8 +156,8 @@
<td><a href="{% url 'filtered' _filter="scale" search=rolling_stock.scale %}"><abbr title="{{ rolling_stock.scale.ratio }} - {{ rolling_stock.scale.tracks }}">{{ rolling_stock.scale }}</abbr></a></td> <td><a href="{% url 'filtered' _filter="scale" search=rolling_stock.scale %}"><abbr title="{{ rolling_stock.scale.ratio }} - {{ rolling_stock.scale.tracks }}">{{ rolling_stock.scale }}</abbr></a></td>
</tr> </tr>
<tr> <tr>
<th scope="row">SKU</th> <th scope="row">Item number</th>
<td>{{ rolling_stock.sku }}</td> <td>{{ rolling_stock.item_number }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Era</th> <th scope="row">Era</th>
@@ -164,25 +165,25 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Production year</th> <th scope="row">Production year</th>
<td>{{ rolling_stock.production_year | default_if_none:"" }}</td> <td>{{ rolling_stock.production_year|default:"-" }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Purchase date</th> <th scope="row">Purchase date</th>
<td>{{ rolling_stock.purchase_date | default_if_none:"" }}</td> <td>{{ rolling_stock.purchase_date|default:"-" }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
{% if rolling_stock_properties %} {% if properties %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th colspan="2" scope="row">Properties</th> <th colspan="2" scope="row">Properties</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody class="table-group-divider">
{% for p in rolling_stock_properties %} {% for p in properties %}
<tr> <tr>
<th width="35%" scope="row">{{ p.property }}</th> <th class="w-33" scope="row">{{ p.property }}</th>
<td>{{ p.value }}</td> <td>{{ p.value }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
@@ -190,36 +191,26 @@
</table> </table>
{% endif %} {% endif %}
</div> </div>
<div class="tab-pane fade" id="nav-class" role="tabpanel" aria-labelledby="nav-class-tab"> <div class="tab-pane" id="nav-class" role="tabpanel" aria-labelledby="nav-class-tab">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th colspan="2" scope="row">Class data</th> <th colspan="2" scope="row">Class data</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody class="table-group-divider">
<tr> <tr>
<th width="35%" scope="row">Class</th> <th class="w-33" scope="row">Class</th>
<td>{{ rolling_stock.rolling_class.identifier }}</td> <td>{{ class.identifier }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Type</th> <th scope="row">Type</th>
<td>{{ rolling_stock.rolling_class.type }}</td> <td>{{ class.type }}</td>
</tr>
<tr>
<th scope="row">Company</th>
<td>
<a href="{% url 'filtered' _filter="company" search=rolling_stock.rolling_class.company.slug %}">{{ rolling_stock.rolling_class.company }}</a> ({{ rolling_stock.rolling_class.company.extended_name }})
</td>
</tr>
<tr>
<th scope="row">Country</th>
<td>{{ rolling_stock.rolling_class.company.country.name }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Manufacturer</th> <th scope="row">Manufacturer</th>
<td>{%if rolling_stock.rolling_class.manufacturer %} <td>{%if class.manufacturer %}
<a href="{% url 'filtered' _filter="manufacturer" search=rolling_stock.rolling_class.manufacturer.slug %}">{{ rolling_stock.rolling_class.manufacturer }}{% if rolling_stock.rolling_class.manufacturer.website %}</a> <a href="{{ rolling_stock.rolling_class.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %} <a href="{% url 'filtered' _filter="manufacturer" search=class.manufacturer.slug %}">{{ class.manufacturer }}{% if class.manufacturer.website %}</a> <a href="{{ class.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
{% endif %}</td> {% endif %}</td>
</tr> </tr>
</tbody> </tbody>
@@ -231,10 +222,10 @@
<th colspan="2" scope="row">Properties</th> <th colspan="2" scope="row">Properties</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody class="table-group-divider">
{% for p in class_properties %} {% for p in class_properties %}
<tr> <tr>
<th width="35%" scope="row">{{ p.property }}</th> <th class="w-33" scope="row">{{ p.property }}</th>
<td>{{ p.value }}</td> <td>{{ p.value }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
@@ -242,20 +233,51 @@
</table> </table>
{% endif %} {% endif %}
</div> </div>
<div class="tab-pane fade" id="nav-dcc" role="tabpanel" aria-labelledby="nav-dcc-tab"> <div class="tab-pane" id="nav-company" role="tabpanel" aria-labelledby="nav-company-tab">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th colspan="2" scope="row">DCC data</th> <th colspan="2" scope="row">Company data</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody class="table-group-divider">
{% if company.logo %}
<tr>
<th class="w-33" scope="row">Logo</th>
<td><img class="logo-xl" src="{{ company.logo.url }}" /></td>
</tr>
{% endif %}
<tr>
<th class="w-33" scope="row">Name</th>
<td><a href="{% url 'filtered' _filter="company" search=company.slug %}">{{ company.name }}</a> ({{ company.extended_name }})</td>
</tr>
<tr>
<th class="w-33" scope="row">Country</th>
<td>{{ company.country.name }} <img src="{{ company.country.flag }}" alt="{{ company.country }}" />
</tr>
{% if company.freelance %}
<tr>
<th class="w-33" scope="row">Notes</th>
<td>A <em>freelance</em> company</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<div class="tab-pane" id="nav-dcc" role="tabpanel" aria-labelledby="nav-dcc-tab">
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Decoder data</th>
</tr>
</thead>
<tbody class="table-group-divider">
<tr> <tr>
<th scope="row">Interface</th> <th scope="row">Interface</th>
<td>{{ rolling_stock.get_decoder_interface_display }}</td> <td>{{ rolling_stock.get_decoder_interface_display }}</td>
</tr> </tr>
<tr> <tr>
<th width="35%" scope="row">Address</th> <th class="w-33" scope="row">Address</th>
<td>{{ rolling_stock.address }}</td> <td>{{ rolling_stock.address }}</td>
</tr> </tr>
<tr> <tr>
@@ -264,7 +286,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Manufacturer</th> <th scope="row">Manufacturer</th>
<td>{{ rolling_stock.decoder.manufacturer|default_if_none:"" }}</td> <td>{{ rolling_stock.decoder.manufacturer|default:"-" }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Version</th> <th scope="row">Version</th>
@@ -277,56 +299,73 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="tab-pane fade" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab"> <div class="tab-pane" id="nav-documents" role="tabpanel" aria-labelledby="nav-documents-tab">
<table class="table"> {% if documents %}
<thead>
<tr>
<th scope="row">Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ rolling_stock.notes | safe }}</td>
</tr>
</tbody>
</table>
</div>
<div class="tab-pane fade" id="nav-documents" role="tabpanel" aria-labelledby="nav-documents-tab">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th colspan="3" scope="row">Documents</th> <th colspan="3" scope="row">Documents</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody class="table-group-divider">
{% for d in rolling_stock.document.all %} {% for d in documents.all %}
<tr> <tr>
<td>{{ d.description }}</td> <td class="w-33">{{ d.description }}</td>
<td><a href="{{ d.file.url }}" target="_blank">{{ d.filename }}</a></td> <td><a href="{{ d.file.url }}" target="_blank">{{ d.filename }}</a></td>
<td class="text-end">{{ d.file.size | filesizeformat }}</td> <td class="text-end">{{ d.file.size | filesizeformat }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% endif %}
{% if decoder_documents %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="3" scope="row">Decoder documents</th>
</tr>
</thead>
<tbody class="table-group-divider">
{% for d in decoder_documents.all %}
<tr>
<td class="w-33">{{ d.description }}</td>
<td><a href="{{ d.file.url }}" target="_blank">{{ d.filename }}</a></td>
<td class="text-end">{{ d.file.size | filesizeformat }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div> </div>
<div class="tab-pane fade" id="nav-journal" role="tabpanel" aria-labelledby="nav-journal-tab"> <div class="tab-pane" id="nav-journal" role="tabpanel" aria-labelledby="nav-journal-tab">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th colspan="3" scope="row">Journal</th> <th colspan="3" scope="row">Journal</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody class="table-group-divider">
{% for j in rolling_stock_journal %} {% for j in journal %}
<tr> <tr>
<th width="35%" scope="row">{{ j.date }}</th> <th class="w-33" scope="row">{{ j.date }}</th>
<td>{{ j.log | safe }}</a></td> <td>{{ j.log | safe }}</a></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="tab-pane" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab">
{{ rolling_stock.notes | safe }}
</div> </div>
<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">
{% for d in consists %}
{% include "cards/consist.html" %}
{% endfor %}
</div>
</div>
</div>
{% endwith %}
<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:roster_rollingstock_change' rolling_stock.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:roster_rollingstock_change' rolling_stock.pk %}">Edit</a>{% endif %}
</div> </div>

View File

@@ -6,7 +6,7 @@
<ul class="pagination justify-content-center mt-4 mb-0"> <ul class="pagination justify-content-center mt-4 mb-0">
{% if data.has_previous %} {% if data.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'roster_pagination' page=data.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a> <a class="page-link" href="{% url 'roster_pagination' page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -16,19 +16,19 @@
{% for i in page_range %} {% for i in page_range %}
{% if data.number == i %} {% if data.number == i %}
<li class="page-item active"> <li class="page-item active">
<span class="page-link">{{ i }}</span></span> <span class="page-link">{{ i }}</span>
</li> </li>
{% else %} {% else %}
{% 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 %}#rolling-stock">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'roster_pagination' page=i %}#main-content">{{ i }}</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if data.has_next %} {% if data.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'roster_pagination' page=data.next_page_number %}#rolling-stock" tabindex="-1">Next</a> <a class="page-link" href="{% url 'roster_pagination' page=data.next_page_number %}#main-content" tabindex="-1">Next</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">

View File

@@ -1,52 +1,11 @@
{% extends "cards.html" %} {% extends "cards.html" %}
{% block cards %}
{% for d in data %}
<div class="col">
<div class="card shadow-sm">
<div class="card-body">
<p class="card-text"><strong>{{ d }}</strong></p>
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Scale</th>
</tr>
</thead>
<tbody>
<tr>
<th width="35%" scope="row">Name</th>
<td>{{ d.scale }}</td>
</tr>
<tr>
<th width="35%" scope="row">Ratio</th>
<td>{{ d.ratio }}</td>
</tr>
<tr>
<th width="35%" scope="row">Gauge</th>
<td>{{ d.gauge }}</td>
</tr>
<tr>
<th width="35%" scope="row">Tracks</th>
<td>{{ d.tracks }}</td>
</tr>
</tbody>
</table>
<div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="scale" search=d.slug %}">Show all rolling stock</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_scale_change' d.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}
{% block pagination %} {% block pagination %}
{% if data.has_other_pages %} {% if data.has_other_pages %}
<nav aria-label="Page navigation example"> <nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mt-4 mb-0"> <ul class="pagination justify-content-center mt-4 mb-0">
{% if data.has_previous %} {% if data.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'scales_pagination' page=data.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a> <a class="page-link" href="{% url 'scales_pagination' page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -56,19 +15,19 @@
{% for i in page_range %} {% for i in page_range %}
{% if data.number == i %} {% if data.number == i %}
<li class="page-item active"> <li class="page-item active">
<span class="page-link">{{ i }}</span></span> <span class="page-link">{{ i }}</span>
</li> </li>
{% else %} {% else %}
{% if i == data.paginator.ELLIPSIS %} {% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li> <li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %} {% else %}
<li class="page-item"><a class="page-link" href="{% url 'scales_pagination' page=i %}#rolling-stock">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'scales_pagination' page=i %}#main-content">{{ i }}</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if data.has_next %} {% if data.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'scales_pagination' page=data.next_page_number %}#rolling-stock" tabindex="-1">Next</a> <a class="page-link" href="{% url 'scales_pagination' page=data.next_page_number %}#main-content" tabindex="-1">Next</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">

View File

@@ -6,7 +6,7 @@
<ul class="pagination justify-content-center mt-4 mb-0"> <ul class="pagination justify-content-center mt-4 mb-0">
{% if data.has_previous %} {% if data.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'search_pagination' search=encoded_search page=data.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a> <a class="page-link" href="{% url 'search_pagination' search=encoded_search page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -16,19 +16,19 @@
{% for i in page_range %} {% for i in page_range %}
{% if data.number == i %} {% if data.number == i %}
<li class="page-item active"> <li class="page-item active">
<span class="page-link">{{ i }}</span></span> <span class="page-link">{{ i }}</span>
</li> </li>
{% else %} {% else %}
{% 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 'search_pagination' search=encoded_search page=i %}#rolling-stock">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'search_pagination' search=encoded_search 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 'search_pagination' search=encoded_search page=data.next_page_number %}#rolling-stock" tabindex="-1">Next</a> <a class="page-link" href="{% url 'search_pagination' search=encoded_search page=data.next_page_number %}#main-content" tabindex="-1">Next</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">

View File

@@ -1,44 +1,11 @@
{% extends "cards.html" %} {% extends "cards.html" %}
{% block cards %}
{% for d in data %}
<div class="col">
<div class="card shadow-sm">
<div class="card-body">
<p class="card-text"><strong>{{ d }}</strong></p>
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Type</th>
</tr>
</thead>
<tbody>
<tr>
<th width="35%" scope="row">Type</th>
<td>{{ d.type }}</td>
</tr>
<tr>
<th width="35%" scope="row">Category</th>
<td>{{ d.category | title}}</td>
</tr>
</tbody>
</table>
<div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="type" search=d.slug %}">Show all rolling stock</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_scale_change' d.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}
{% block pagination %} {% block pagination %}
{% if data.has_other_pages %} {% if data.has_other_pages %}
<nav aria-label="Page navigation example"> <nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mt-4 mb-0"> <ul class="pagination justify-content-center mt-4 mb-0">
{% if data.has_previous %} {% if data.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'scales_pagination' page=data.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a> <a class="page-link" href="{% url 'types_pagination' page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -48,19 +15,19 @@
{% for i in page_range %} {% for i in page_range %}
{% if data.number == i %} {% if data.number == i %}
<li class="page-item active"> <li class="page-item active">
<span class="page-link">{{ i }}</span></span> <span class="page-link">{{ i }}</span>
</li> </li>
{% else %} {% else %}
{% if i == data.paginator.ELLIPSIS %} {% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li> <li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %} {% else %}
<li class="page-item"><a class="page-link" href="{% url 'scales_pagination' page=i %}#rolling-stock">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'types_pagination' page=i %}#main-content">{{ i }}</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if data.has_next %} {% if data.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'scales_pagination' page=data.next_page_number %}#rolling-stock" tabindex="-1">Next</a> <a class="page-link" href="{% url 'types_pagination' page=data.next_page_number %}#main-content" tabindex="-1">Next</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">

View File

@@ -1,10 +1,16 @@
from django import template from django import template
from portal.views import Flatpage from portal.models import Flatpage
from bookshelf.models import Book
register = template.Library() register = template.Library()
@register.inclusion_tag('flatpage_menu.html') @register.inclusion_tag('bookshelf/bookshelf_menu.html')
def show_menu(): def show_bookshelf_menu():
return {"bookshelf_menu": Book.objects.exists()}
@register.inclusion_tag('flatpages/flatpages_menu.html')
def show_flatpages_menu():
menu = Flatpage.objects.filter(published=True).order_by("name") menu = Flatpage.objects.filter(published=True).order_by("name")
return {"menu": menu} return {"flatpages_menu": menu}

View File

@@ -3,7 +3,7 @@ from django.urls import path
from portal.views import ( from portal.views import (
GetData, GetData,
GetRoster, GetRoster,
GetRosterFiltered, GetObjectsFiltered,
GetFlatpage, GetFlatpage,
GetRollingStock, GetRollingStock,
GetConsist, GetConsist,
@@ -12,11 +12,13 @@ from portal.views import (
Manufacturers, Manufacturers,
Scales, Scales,
Types, Types,
SearchRoster, Books,
GetBook,
SearchObjects,
) )
urlpatterns = [ urlpatterns = [
path("", GetData.as_view(), name="index"), path("", GetData.as_view(template="home.html"), name="index"),
path("roster", GetRoster.as_view(), name="roster"), path("roster", GetRoster.as_view(), name="roster"),
path("roster/<int:page>", GetRoster.as_view(), name="roster_pagination"), path("roster/<int:page>", GetRoster.as_view(), name="roster_pagination"),
path( path(
@@ -24,9 +26,15 @@ urlpatterns = [
GetFlatpage.as_view(), GetFlatpage.as_view(),
name="flatpage", name="flatpage",
), ),
path("consists", Consists.as_view(), name="consists"),
path( path(
"consists/<int:page>", Consists.as_view(), name="consists_pagination" "consists",
Consists.as_view(template="consists.html"),
name="consists"
),
path(
"consists/<int:page>",
Consists.as_view(template="consists.html"),
name="consists_pagination"
), ),
path("consist/<uuid:uuid>", GetConsist.as_view(), name="consist"), path("consist/<uuid:uuid>", GetConsist.as_view(), name="consist"),
path( path(
@@ -34,44 +42,75 @@ urlpatterns = [
GetConsist.as_view(), GetConsist.as_view(),
name="consist_pagination", name="consist_pagination",
), ),
path("companies", Companies.as_view(), name="companies"), path(
"companies",
Companies.as_view(template="companies.html"),
name="companies"
),
path( path(
"companies/<int:page>", "companies/<int:page>",
Companies.as_view(), Companies.as_view(template="companies.html"),
name="companies_pagination", name="companies_pagination",
), ),
path( path(
"manufacturers/<str:category>", "manufacturers/<str:category>",
Manufacturers.as_view(), Manufacturers.as_view(template="manufacturers.html"),
name="manufacturers" name="manufacturers"
), ),
path( path(
"manufacturers/<str:category>/<int:page>", "manufacturers/<str:category>/<int:page>",
Manufacturers.as_view(), Manufacturers.as_view(template="manufacturers.html"),
name="manufacturers_pagination", name="manufacturers_pagination",
), ),
path("scales", Scales.as_view(), name="scales"), path(
path("scales/<int:page>", Types.as_view(), name="scales_pagination"), "scales",
path("types", Types.as_view(), name="types"), Scales.as_view(template="scales.html"),
path("types/<int:page>", Types.as_view(), name="types_pagination"), name="scales"
),
path(
"scales/<int:page>",
Scales.as_view(template="scales.html"),
name="scales_pagination"
),
path(
"types",
Types.as_view(template="types.html"),
name="types"
),
path(
"types/<int:page>",
Types.as_view(template="types.html"),
name="types_pagination"
),
path(
"bookshelf/books",
Books.as_view(template="bookshelf/books.html"),
name="books"
),
path(
"bookshelf/books/<int:page>",
Books.as_view(template="bookshelf/books.html"),
name="books_pagination"
),
path("bookshelf/book/<uuid:uuid>", GetBook.as_view(), name="book"),
path( path(
"search", "search",
SearchRoster.as_view(http_method_names=["post"]), SearchObjects.as_view(http_method_names=["post"]),
name="search", name="search",
), ),
path( path(
"search/<str:search>/<int:page>", "search/<str:search>/<int:page>",
SearchRoster.as_view(), SearchObjects.as_view(),
name="search_pagination", name="search_pagination",
), ),
path( path(
"<str:_filter>/<str:search>", "<str:_filter>/<str:search>",
GetRosterFiltered.as_view(), GetObjectsFiltered.as_view(),
name="filtered", name="filtered",
), ),
path( path(
"<str:_filter>/<str:search>/<int:page>", "<str:_filter>/<str:search>/<int:page>",
GetRosterFiltered.as_view(), GetObjectsFiltered.as_view(),
name="filtered_pagination", name="filtered_pagination",
), ),
path("<uuid:uuid>", GetRollingStock.as_view(), name="rolling_stock"), path("<uuid:uuid>", GetRollingStock.as_view(), name="rolling_stock"),

View File

@@ -5,6 +5,7 @@ from urllib.parse import unquote
from django.views import View from django.views import View
from django.http import Http404, HttpResponseBadRequest from django.http import Http404, HttpResponseBadRequest
from django.db.utils import OperationalError, ProgrammingError
from django.db.models import Q from django.db.models import Q
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
@@ -14,11 +15,18 @@ 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 metadata.models import Company, Manufacturer, Scale, RollingStockType, Tag from bookshelf.models import Book
from metadata.models import (
Company, Manufacturer, Scale, DecoderDocument, RollingStockType, Tag
)
def order_by_fields(): def order_by_fields():
try:
order_by = get_site_conf().items_ordering order_by = get_site_conf().items_ordering
except (OperationalError, ProgrammingError):
order_by = "type"
fields = [ fields = [
"rolling_class__type", "rolling_class__type",
"rolling_class__company", "rolling_class__company",
@@ -35,15 +43,22 @@ def order_by_fields():
class GetData(View): class GetData(View):
def __init__(self): title = "Home"
self.title = "Home" template = "roster.html"
self.template = "home.html" item_type = "rolling_stock"
self.data = RollingStock.objects.order_by(*order_by_fields()) queryset = RollingStock.objects.order_by(*order_by_fields())
def get(self, request, page=1): def get(self, request, page=1):
site_conf = get_site_conf() site_conf = get_site_conf()
paginator = Paginator(self.data, site_conf.items_per_page) data = []
for item in self.queryset:
data.append({
"type": self.item_type,
"item": item
})
paginator = Paginator(data, site_conf.items_per_page)
data = paginator.get_page(page) data = paginator.get_page(page)
page_range = paginator.get_elided_page_range( page_range = paginator.get_elided_page_range(
data.number, on_each_side=2, on_ends=1 data.number, on_each_side=2, on_ends=1
@@ -54,6 +69,7 @@ class GetData(View):
self.template, self.template,
{ {
"title": self.title, "title": self.title,
"type": self.item_type,
"data": data, "data": data,
"matches": paginator.count, "matches": paginator.count,
"page_range": page_range, "page_range": page_range,
@@ -62,13 +78,12 @@ class GetData(View):
class GetRoster(GetData): class GetRoster(GetData):
def __init__(self): title = "Roster"
self.title = "Rolling stock" item_type = "rolling_stock"
self.template = "roster.html" queryset = RollingStock.objects.order_by(*order_by_fields())
self.data = RollingStock.objects.order_by(*order_by_fields())
class SearchRoster(View): class 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() site_conf = get_site_conf()
if _filter is None: if _filter is None:
@@ -80,7 +95,7 @@ class SearchRoster(View):
| Q(rolling_class__description__icontains=s) | Q(rolling_class__description__icontains=s)
| Q(rolling_class__type__type__icontains=s) | Q(rolling_class__type__type__icontains=s)
| Q(road_number__icontains=s) | Q(road_number__icontains=s)
| Q(sku=s) | Q(item_number=s)
| Q(rolling_class__company__name__icontains=s) | Q(rolling_class__company__name__icontains=s)
| Q(rolling_class__company__country__icontains=s) | Q(rolling_class__company__country__icontains=s)
| Q(manufacturer__name__icontains=s) | Q(manufacturer__name__icontains=s)
@@ -110,20 +125,50 @@ class SearchRoster(View):
else: else:
raise Http404 raise Http404
# FIXME duplicated code!
data = []
rolling_stock = ( rolling_stock = (
RollingStock.objects.filter(query) RollingStock.objects.filter(query)
.distinct() .distinct()
.order_by(*order_by_fields()) .order_by(*order_by_fields())
) )
matches = rolling_stock.count() for item in rolling_stock:
data.append({
"type": "rolling_stock",
"item": item
})
if _filter is None:
consists = (
Consist.objects.filter(
Q(
Q(identifier__icontains=search)
| Q(company__name__icontains=search)
)
)
.distinct()
)
for item in consists:
data.append({
"type": "consist",
"item": item
})
books = (
Book.objects.filter(title__icontains=search)
.distinct()
)
for item in books:
data.append({
"type": "book",
"item": item
})
paginator = Paginator(rolling_stock, site_conf.items_per_page) paginator = Paginator(data, site_conf.items_per_page)
rolling_stock = paginator.get_page(page) data = paginator.get_page(page)
page_range = paginator.get_elided_page_range( page_range = paginator.get_elided_page_range(
rolling_stock.number, on_each_side=2, on_ends=1 data.number, on_each_side=2, on_ends=1
) )
return rolling_stock, matches, page_range return data, paginator.count, page_range
def split_search(self, search): def split_search(self, search):
search = search.strip().split(":") search = search.strip().split(":")
@@ -148,7 +193,7 @@ class SearchRoster(View):
encoded_search = base64.b64encode( encoded_search = base64.b64encode(
search.encode()).decode() search.encode()).decode()
_filter, keyword = self.split_search(search) _filter, keyword = self.split_search(search)
rolling_stock, matches, page_range = self.run_search( data, matches, page_range = self.run_search(
request, keyword, _filter, page request, keyword, _filter, page
) )
@@ -159,8 +204,8 @@ class SearchRoster(View):
"title": "Search: \"{}\"".format(search), "title": "Search: \"{}\"".format(search),
"search": search, "search": search,
"encoded_search": encoded_search, "encoded_search": encoded_search,
"data": data,
"matches": matches, "matches": matches,
"data": rolling_stock,
"page_range": page_range, "page_range": page_range,
}, },
) )
@@ -170,15 +215,17 @@ class SearchRoster(View):
return self.get(request, search, page) return self.get(request, search, page)
class GetRosterFiltered(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()
if _filter == "type": if _filter == "type":
title = RollingStockType.objects.get(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)
elif _filter == "company": elif _filter == "company":
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)
elif _filter == "manufacturer": elif _filter == "manufacturer":
title = get_object_or_404(Manufacturer, slug__iexact=search) title = get_object_or_404(Manufacturer, slug__iexact=search)
query = Q( query = Q(
@@ -188,9 +235,13 @@ class GetRosterFiltered(View):
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)
query_2nd = Q(
consist_item__rolling_stock__scale__slug__iexact=search
)
elif _filter == "tag": elif _filter == "tag":
title = get_object_or_404(Tag, slug__iexact=search) title = get_object_or_404(Tag, slug__iexact=search)
query = Q(tags__slug__iexact=search) query = Q(tags__slug__iexact=search)
query_2nd = query # For tags the 2nd level query doesn't change
else: else:
raise Http404 raise Http404
@@ -199,15 +250,44 @@ class GetRosterFiltered(View):
.distinct() .distinct()
.order_by(*order_by_fields()) .order_by(*order_by_fields())
) )
matches = rolling_stock.count()
paginator = Paginator(rolling_stock, site_conf.items_per_page) data = []
rolling_stock = paginator.get_page(page) for item in rolling_stock:
data.append({
"type": "rolling_stock",
"item": item
})
try: # Execute only if query_2nd is defined
consists = (
Consist.objects.filter(query_2nd)
.distinct()
)
for item in consists:
data.append({
"type": "consist",
"item": item
})
if _filter == "tag": # Books can be filtered only by tag
books = (
Book.objects.filter(query_2nd)
.distinct()
)
for item in books:
data.append({
"type": "book",
"item": item
})
except NameError:
pass
paginator = Paginator(data, site_conf.items_per_page)
data = paginator.get_page(page)
page_range = paginator.get_elided_page_range( page_range = paginator.get_elided_page_range(
rolling_stock.number, on_each_side=2, on_ends=1 data.number, on_each_side=2, on_ends=1
) )
return rolling_stock, title, matches, page_range return data, title, paginator.count, page_range
def get(self, request, search, _filter, page=1): def get(self, request, search, _filter, page=1):
data, title, matches, page_range = self.run_filter( data, title, matches, page_range = self.run_filter(
@@ -222,8 +302,8 @@ class GetRosterFiltered(View):
_filter.capitalize(), title), _filter.capitalize(), title),
"search": search, "search": search,
"filter": _filter, "filter": _filter,
"matches": matches,
"data": data, "data": data,
"matches": matches,
"page_range": page_range, "page_range": page_range,
}, },
) )
@@ -236,24 +316,36 @@ class GetRollingStock(View):
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise Http404 raise Http404
class_properties = ( # FIXME there's likely a better and more efficient way of doing this
rolling_stock.rolling_class.property.all() # but keeping KISS for now
if request.user.is_authenticated decoder_documents = []
else rolling_stock.rolling_class.property.filter( if request.user.is_authenticated:
class_properties = rolling_stock.rolling_class.property.all()
properties = rolling_stock.property.all()
documents = rolling_stock.document.all()
journal = rolling_stock.journal.all()
if rolling_stock.decoder:
decoder_documents = rolling_stock.decoder.document.all()
else:
class_properties = rolling_stock.rolling_class.property.filter(
property__private=False property__private=False
) )
properties = rolling_stock.property.filter(
property__private=False
) )
rolling_stock_properties = ( documents = rolling_stock.document.filter(private=False)
rolling_stock.property.all() journal = rolling_stock.journal.filter(private=False)
if request.user.is_authenticated if rolling_stock.decoder:
else rolling_stock.property.filter(property__private=False) decoder_documents = rolling_stock.decoder.document.filter(
private=False
) )
rolling_stock_journal = ( consists = [{
rolling_stock.journal.all() "type": "consist",
if request.user.is_authenticated "item": c
else rolling_stock.journal.filter(private=False) } for c in Consist.objects.filter(
) consist_item__rolling_stock=rolling_stock
)] # A dict with "item" is required by the consists card
return render( return render(
request, request,
@@ -262,17 +354,19 @@ class GetRollingStock(View):
"title": rolling_stock, "title": rolling_stock,
"rolling_stock": rolling_stock, "rolling_stock": rolling_stock,
"class_properties": class_properties, "class_properties": class_properties,
"rolling_stock_properties": rolling_stock_properties, "properties": properties,
"rolling_stock_journal": rolling_stock_journal, "decoder_documents": decoder_documents,
"documents": documents,
"journal": journal,
"consists": consists,
}, },
) )
class Consists(GetData): class Consists(GetData):
def __init__(self): title = "Consists"
self.title = "Consists" item_type = "consist"
self.template = "consists.html" queryset = Consist.objects.all()
self.data = Consist.objects.all()
class GetConsist(View): class GetConsist(View):
@@ -282,15 +376,15 @@ class GetConsist(View):
consist = Consist.objects.get(uuid=uuid) consist = Consist.objects.get(uuid=uuid)
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise Http404 raise Http404
rolling_stock = [ data = [{
RollingStock.objects.get(uuid=r.rolling_stock_id) for r in "type": "rolling_stock",
consist.consist_item.all() "item": RollingStock.objects.get(uuid=r.rolling_stock_id)
] } for r in consist.consist_item.all()]
paginator = Paginator(rolling_stock, site_conf.items_per_page) paginator = Paginator(data, site_conf.items_per_page)
rolling_stock = paginator.get_page(page) data = paginator.get_page(page)
page_range = paginator.get_elided_page_range( page_range = paginator.get_elided_page_range(
rolling_stock.number, on_each_side=2, on_ends=1 data.number, on_each_side=2, on_ends=1
) )
return render( return render(
@@ -299,58 +393,85 @@ class GetConsist(View):
{ {
"title": consist, "title": consist,
"consist": consist, "consist": consist,
"data": rolling_stock, "data": data,
"page_range": page_range, "page_range": page_range,
}, },
) )
class Manufacturers(GetData): class Manufacturers(GetData):
def __init__(self): title = "Manufacturers"
self.title = "Manufacturers" item_type = "manufacturer"
self.template = "manufacturers.html" queryset = None # Set via method get
self.data = None # Set via method get
# overload get method to filter by category # overload get method to filter by category
def get(self, request, category, page=1): def get(self, request, category, page=1):
if category not in ("real", "model"): if category not in ("real", "model"):
raise Http404 raise Http404
self.data = Manufacturer.objects.filter(category=category) self.queryset = Manufacturer.objects.filter(category=category)
return super().get(request, page) return super().get(request, page)
class Companies(GetData): class Companies(GetData):
def __init__(self): title = "Companies"
self.title = "Companies" item_type = "company"
self.template = "companies.html" queryset = Company.objects.all()
self.data = Company.objects.all()
class Scales(GetData): class Scales(GetData):
def __init__(self): title = "Scales"
self.title = "Scales" item_type = "scale"
self.template = "scales.html" queryset = Scale.objects.all()
self.data = Scale.objects.all()
class Types(GetData): class Types(GetData):
def __init__(self): title = "Types"
self.title = "Types" item_type = "rolling_stock_type"
self.template = "types.html" queryset = RollingStockType.objects.all()
self.data = RollingStockType.objects.all()
class Books(GetData):
title = "Books"
item_type = "book"
queryset = Book.objects.all()
class GetBook(View):
def get(self, request, uuid):
try:
book = Book.objects.get(uuid=uuid)
except ObjectDoesNotExist:
raise Http404
book_properties = (
book.property.all()
if request.user.is_authenticated
else book.property.filter(property__private=False)
)
return render(
request,
"bookshelf/book.html",
{
"title": book,
"book_properties": book_properties,
"book": book,
},
)
class GetFlatpage(View): class GetFlatpage(View):
def get(self, request, flatpage): def get(self, request, flatpage):
_filter = Q(published=True) # Show only published pages
if request.user.is_authenticated:
_filter = Q() # Reset the filter if user is authenticated
try: try:
flatpage = Flatpage.objects.get( flatpage = Flatpage.objects.filter(_filter).get(path=flatpage)
Q(Q(path=flatpage) & Q(published=True))
)
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise Http404 raise Http404
return render( return render(
request, request,
"flatpage.html", "flatpages/flatpage.html",
{"title": flatpage.name, "flatpage": flatpage}, {"title": flatpage.name, "flatpage": flatpage},
) )

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
# vim: syntax=python # vim: syntax=python
from django.conf import settings
""" """
Django local_settings for ram project. Example of changes suitable for production
""" """
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
@@ -12,9 +13,23 @@ SECRET_KEY = (
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False DEBUG = False
# SECURITY WARNING: cache middlewares must be loaded before cookies one
MIDDLEWARE = [
"django.middleware.cache.UpdateCacheMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.cache.FetchFromCacheMiddleware",
] + settings.MIDDLEWARE
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "redis://127.0.0.1:6379",
}
}
CACHE_MIDDLEWARE_SECONDS = 300
STORAGE_DIR = BASE_DIR / "storage" STORAGE_DIR = BASE_DIR / "storage"
ALLOWED_HOSTS = ["127.0.0.1"] ALLOWED_HOSTS = ["127.0.0.1", "myhost"]
CSRF_TRUSTED_ORIGINS = ["https://myhost"] CSRF_TRUSTED_ORIGINS = ["https://myhost"]
ROOT_URLCONF = "ram.urls"
STATIC_URL = "static/" STATIC_URL = "static/"
MEDIA_URL = "media/" MEDIA_URL = "media/"

65
ram/ram/models.py Normal file
View File

@@ -0,0 +1,65 @@
import os
from django.db import models
from django.utils.safestring import mark_safe
from ram.utils import DeduplicatedStorage, get_image_preview
class Document(models.Model):
description = models.CharField(max_length=128, blank=True)
file = models.FileField(
upload_to="files/",
storage=DeduplicatedStorage(),
null=True,
blank=True,
)
private = models.BooleanField(default=False)
class Meta:
abstract = True
def __str__(self):
return "{0}".format(os.path.basename(self.file.name))
def filename(self):
return self.__str__()
def download(self):
return mark_safe(
'<a href="{0}" target="_blank">Link</a>'.format(self.file.url)
)
class Image(models.Model):
order = models.PositiveIntegerField(default=0, blank=False, null=False)
image = models.ImageField(
upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True
)
def image_thumbnail(self):
return get_image_preview(self.image.url)
image_thumbnail.short_description = "Preview"
def __str__(self):
return "{0}".format(os.path.basename(self.image.name))
class Meta:
abstract = True
ordering = ["order"]
class PropertyInstance(models.Model):
property = models.ForeignKey(
"metadata.Property", # To avoid circular dependencies
on_delete=models.CASCADE
)
value = models.CharField(max_length=256)
def __str__(self):
return self.property.name
class Meta:
abstract = True
verbose_name_plural = "Properties"

View File

@@ -51,10 +51,11 @@ INSTALLED_APPS = [
"rest_framework", "rest_framework",
"ram", "ram",
"portal", "portal",
"driver", # "driver",
"metadata", "metadata",
"roster", "roster",
"consist", "consist",
"bookshelf",
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@@ -80,6 +81,7 @@ TEMPLATES = [
"django.template.context_processors.request", "django.template.context_processors.request",
"django.contrib.auth.context_processors.auth", "django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages", "django.contrib.messages.context_processors.messages",
"portal.context_processors.default_card_image",
], ],
}, },
}, },
@@ -148,6 +150,10 @@ COUNTRIES_OVERRIDE = {
"ZZ": "Freelance", "ZZ": "Freelance",
} }
# Image used on cards without a custom image uploaded.
# The file must be placed in the root of the 'static' folder
DEFAULT_CARD_IMAGE = "coming_soon.svg"
DECODER_INTERFACES = [ DECODER_INTERFACES = [
(1, "NEM651"), (1, "NEM651"),
(2, "NEM652"), (2, "NEM652"),

View File

@@ -13,6 +13,7 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path 1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.apps import apps
from django.conf import settings from django.conf import settings
from django.shortcuts import redirect from django.shortcuts import redirect
from django.conf.urls.static import static from django.conf.urls.static import static
@@ -27,9 +28,15 @@ urlpatterns = [
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("api/v1/consist/", include("consist.urls")), path("api/v1/consist/", include("consist.urls")),
path("api/v1/roster/", include("roster.urls")), path("api/v1/roster/", include("roster.urls")),
path("api/v1/dcc/", include("driver.urls")), path("api/v1/bookshelf/", include("bookshelf.urls")),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# Enable the "/dcc" routing only if the "driver" app is active
if apps.is_installed("driver"):
urlpatterns += [
path("api/v1/dcc/", include("driver.urls")),
]
if settings.DEBUG: if settings.DEBUG:
from django.views.generic import TemplateView from django.views.generic import TemplateView
from rest_framework.schemas import get_schema_view from rest_framework.schemas import get_schema_view
@@ -53,3 +60,7 @@ if settings.DEBUG:
name="openapi-schema", name="openapi-schema",
), ),
] ]
if apps.is_installed("debug_toolbar"):
urlpatterns += [
path("__debug__/", include("debug_toolbar.urls")),
]

View File

@@ -16,7 +16,7 @@ class DeduplicatedStorage(FileSystemStorage):
def save(self, name, content, max_length=None): def save(self, name, content, max_length=None):
if super().exists(name): if super().exists(name):
new = hashlib.sha256(content.file.getbuffer()).hexdigest() new = hashlib.sha256(content.read()).hexdigest()
with open(super().path(name), "rb") as file: with open(super().path(name), "rb") as file:
file_binary = file.read() file_binary = file.read()
old = hashlib.sha256(file_binary).hexdigest() old = hashlib.sha256(file_binary).hexdigest()

View File

@@ -64,11 +64,12 @@ class RollingStockDocumentAdmin(admin.ModelAdmin):
"__str__", "__str__",
"rolling_stock", "rolling_stock",
"description", "description",
"private",
"download", "download",
) )
search_fields = ( search_fields = (
"rolling_stock__rolling_class__identifier", "rolling_stock__rolling_class__identifier",
"rolling_stock__sku", "rolling_stock__item_number",
"description", "description",
"file", "file",
) )
@@ -89,7 +90,7 @@ class RollingJournalDocumentAdmin(admin.ModelAdmin):
search_fields = ( search_fields = (
"rolling_stock__rolling_class__identifier", "rolling_stock__rolling_class__identifier",
"rolling_stock__road_number", "rolling_stock__road_number",
"rolling_stock__sku", "rolling_stock__item_number",
"log", "log",
) )
@@ -108,7 +109,7 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
"address", "address",
"manufacturer", "manufacturer",
"scale", "scale",
"sku", "item_number",
"company", "company",
"country", "country",
) )
@@ -125,7 +126,7 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
"manufacturer__name", "manufacturer__name",
"road_number", "road_number",
"address", "address",
"sku", "item_number",
) )
save_as = True save_as = True
@@ -138,7 +139,7 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
"road_number", "road_number",
"scale", "scale",
"manufacturer", "manufacturer",
"sku", "item_number",
"era", "era",
"production_year", "production_year",
"purchase_date", "purchase_date",

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.2.5 on 2023-10-01 19:32
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("roster", "0017_remove_rollingstockimage_is_thumbnail"),
]
operations = [
migrations.RenameField(
model_name="rollingstock",
old_name="sku",
new_name="item_number",
),
]

View File

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

View File

@@ -1,17 +1,15 @@
import os
import re import re
from uuid import uuid4 from uuid import uuid4
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.conf import settings from django.conf import settings
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.safestring import mark_safe
from ckeditor_uploader.fields import RichTextUploadingField from ckeditor_uploader.fields import RichTextUploadingField
from ram.utils import DeduplicatedStorage, get_image_preview from ram.models import Document, Image, PropertyInstance
from ram.utils import get_image_preview
from metadata.models import ( from metadata.models import (
Property,
Scale, Scale,
Manufacturer, Manufacturer,
Decoder, Decoder,
@@ -43,7 +41,7 @@ class RollingClass(models.Model):
return "{0} {1}".format(self.company, self.identifier) return "{0} {1}".format(self.company, self.identifier)
class RollingClassProperty(models.Model): class RollingClassProperty(PropertyInstance):
rolling_class = models.ForeignKey( rolling_class = models.ForeignKey(
RollingClass, RollingClass,
on_delete=models.CASCADE, on_delete=models.CASCADE,
@@ -52,14 +50,6 @@ class RollingClassProperty(models.Model):
related_name="property", related_name="property",
verbose_name="Class", verbose_name="Class",
) )
property = models.ForeignKey(Property, on_delete=models.CASCADE)
value = models.CharField(max_length=256)
def __str__(self):
return self.property.name
class Meta:
verbose_name_plural = "Properties"
class RollingStock(models.Model): class RollingStock(models.Model):
@@ -82,7 +72,7 @@ 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)
sku = models.CharField(max_length=32, blank=True) item_number = models.CharField(max_length=32, blank=True)
decoder_interface = models.PositiveSmallIntegerField( decoder_interface = models.PositiveSmallIntegerField(
choices=settings.DECODER_INTERFACES, null=True, blank=True choices=settings.DECODER_INTERFACES, null=True, blank=True
) )
@@ -127,55 +117,22 @@ def pre_save_running_number(sender, instance, *args, **kwargs):
pass pass
class RollingStockDocument(models.Model): class RollingStockDocument(Document):
rolling_stock = models.ForeignKey( rolling_stock = models.ForeignKey(
RollingStock, on_delete=models.CASCADE, related_name="document" RollingStock, on_delete=models.CASCADE, related_name="document"
) )
description = models.CharField(max_length=128, blank=True)
file = models.FileField(
upload_to="files/",
storage=DeduplicatedStorage(),
null=True,
blank=True,
)
class Meta(object): class Meta(object):
unique_together = ("rolling_stock", "file") unique_together = ("rolling_stock", "file")
def __str__(self):
return "{0}".format(os.path.basename(self.file.name))
def filename(self): class RollingStockImage(Image):
return self.__str__()
def download(self):
return mark_safe(
'<a href="{0}" target="_blank">Link</a>'.format(self.file.url)
)
class RollingStockImage(models.Model):
order = models.PositiveIntegerField(default=0, blank=False, null=False)
rolling_stock = models.ForeignKey( rolling_stock = models.ForeignKey(
RollingStock, on_delete=models.CASCADE, related_name="image" RollingStock, on_delete=models.CASCADE, related_name="image"
) )
image = models.ImageField(
upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True
)
def image_thumbnail(self):
return get_image_preview(self.image.url)
image_thumbnail.short_description = "Preview"
def __str__(self):
return "{0}".format(os.path.basename(self.image.name))
class Meta:
ordering = ["order"]
class RollingStockProperty(models.Model): class RollingStockProperty(PropertyInstance):
rolling_stock = models.ForeignKey( rolling_stock = models.ForeignKey(
RollingStock, RollingStock,
on_delete=models.CASCADE, on_delete=models.CASCADE,
@@ -183,14 +140,6 @@ class RollingStockProperty(models.Model):
null=False, null=False,
blank=False, blank=False,
) )
property = models.ForeignKey(Property, on_delete=models.CASCADE)
value = models.CharField(max_length=256)
def __str__(self):
return self.property.name
class Meta:
verbose_name_plural = "Properties"
class RollingStockJournal(models.Model): class RollingStockJournal(models.Model):

View File

@@ -1 +1,2 @@
gunicorn gunicorn
pylibmc

View File

@@ -8,5 +8,8 @@ django-countries
django-health-check django-health-check
django-admin-sortable2 django-admin-sortable2
django-ckeditor django-ckeditor
# psycopg2-binary # Optional: # psycopg2-binary
pySerial # Optional: # pySerial
# Required by django-countries and not always installed
# by default on modern venvs (like Python 3.12 on Fedora 39)
setuptools