76 Commits

Author SHA1 Message Date
c1a45ad4c9 Bump version 2022-08-27 14:58:13 +02:00
29180572c1 Add a journal for rolling stock 2022-08-27 14:57:26 +02:00
d30d9fc9ed Various improvements for flatpages 2022-08-25 12:44:04 +02:00
4ed95d0edf Fix migrations 2022-08-25 00:49:10 +02:00
24bd2aa53c Add migrations md to html 2022-08-24 17:56:59 +02:00
5ef51cb9b7 cleanup 2022-08-24 14:55:26 +02:00
65493ba068 Merge pull request #10 from daniviga/flat-pages
Introduce support for Flatpages
2022-08-24 14:54:14 +02:00
ca459c467b Replace md editor with ckeditor 2022-08-23 17:54:58 +02:00
575c938205 Hotfix for document filename 2022-08-22 18:23:38 +02:00
7cc917d9f7 Use a markdown editor 2022-08-22 18:16:59 +02:00
0fe0644d1b Bump version 2022-08-22 17:14:32 +02:00
f7987f06d5 Merge branch 'master' into flat-pages 2022-08-22 17:13:39 +02:00
2af772a722 Black'ed 2022-08-22 17:13:10 +02:00
f580bcffc5 Documents section in admin 2022-08-22 17:12:22 +02:00
6accb66006 Enable search by sku 2022-08-21 16:53:18 +02:00
602c8359e9 Black'ed 2022-08-07 18:46:33 +02:00
46477c4576 Introduce support for Flatpages
Markdown support only
2022-08-07 18:43:58 +02:00
f56accb4ff Use lead unit thumbnail if not provided in consist 2022-07-23 23:45:11 +02:00
5a7b7fd79e Update README.md 2022-07-23 22:55:58 +02:00
dcdad71b1b Update README.md 2022-07-23 22:54:46 +02:00
321ae1065e Update README.md 2022-07-23 22:51:24 +02:00
e8efa5d87a Update README.md 2022-07-23 22:50:58 +02:00
97254b302c Fix a typo 2022-07-23 16:15:56 +02:00
b8aa34ce1d Add modal for pictures 2022-07-23 11:58:17 +02:00
e023edbeeb Add support for dark mode 2022-07-22 22:39:02 +02:00
c9c8976c60 UX improvements 2022-07-21 23:01:34 +02:00
5765472704 Fix to scale abbr 2022-07-21 22:11:17 +02:00
4fb9d1903f Reduce elided_page_range 2022-07-20 21:51:18 +02:00
63379c9673 Expose tracks 2022-07-18 23:45:13 +02:00
be6a685f55 Gauge vs track 2022-07-18 23:41:47 +02:00
ad33731913 Fix filtered pagination 2022-07-18 22:48:04 +02:00
503a214a4d Minor fixes 2022-07-18 17:07:01 +02:00
1528d1ba56 Make card title a stretched link 2022-07-17 20:46:00 +02:00
5b04abb262 Add countries and scales pages 2022-07-17 12:25:09 +02:00
9fa70ae656 Add filtering by scale 2022-07-16 21:24:36 +02:00
49b7aac807 Run black 2022-07-16 21:00:17 +02:00
24af738ad4 Fix page range 2022-07-16 20:57:29 +02:00
8136a180ab Try another fix for ellipsis 2022-07-16 19:56:08 +02:00
908790c3e0 Try a fix for ellipsis 2022-07-16 19:46:33 +02:00
44cdb8b09f Refactor paginator and add ellipsis 2022-07-16 18:57:46 +02:00
7d3f29e734 Use int sort for road numbers 2022-07-16 17:48:04 +02:00
65b615ae63 Validate search 2022-07-15 22:26:37 +02:00
66a85b4ed7 Add tag based filtering 2022-07-15 21:31:40 +02:00
70c12c69b2 Improve road number sorting and enforce company on consists 2022-07-15 18:10:18 +02:00
e55f953c8a Add sorting, enforce foreign keys 2022-07-15 17:28:44 +02:00
273225f919 Default if none in template 2022-07-13 21:43:50 +02:00
1296c4e663 Default if none in template 2022-07-13 21:42:33 +02:00
bbecdd54bb Remove a pdb leftover 2022-07-13 20:41:48 +02:00
899e33da61 Move properties under model and class sections 2022-07-13 20:40:42 +02:00
1f4966ad49 Add missing swagger template 2022-07-13 20:39:49 +02:00
c7dfb84632 Hide 'About' when no text is provided 2022-07-12 21:56:07 +02:00
4e3a13162a Fix navbar on mobile 2022-07-12 21:17:29 +02:00
74431c5d94 Remove thumbnail padding 2022-07-12 21:01:17 +02:00
cb2ff90d8a Review roster API 2022-07-12 19:00:50 +02:00
2dbe01d8bd Add private properties and consist thumbnail 2022-07-12 19:00:29 +02:00
47b4b2915b Add an option to completely disable driver 2022-07-11 20:56:31 +02:00
49d28b176e Rename a logo file 2022-07-11 18:14:24 +02:00
0e30740366 Fix sample data 2022-07-11 18:12:45 +02:00
7c3fee4127 Limit decoders selection to model manufacturers 2022-07-10 23:48:10 +02:00
1f96ff8833 Metadata pretty print 2022-07-10 23:44:18 +02:00
e76a1ea6b2 Add some sample metadata 2022-07-10 23:42:13 +02:00
565028de72 Add support for local_settings.py 2022-07-10 23:31:37 +02:00
7c88983cba More url generation fixes 2022-07-10 23:19:21 +02:00
51eb9ba2a2 Fix main redirection 2022-07-10 22:56:27 +02:00
b19ea23fa6 Update footer and version 2022-07-10 22:52:54 +02:00
353da224be Fix consists pagination and remove hardcoded urls 2022-07-10 22:48:45 +02:00
d39cd47280 Update arduino-cli to 0.24.0 2022-07-10 22:41:54 +02:00
a67ea83068 Update README.md 2022-06-25 22:48:04 +02:00
410fcd8626 Update README.md 2022-06-25 22:47:34 +02:00
05eb0909c8 Update README.md 2022-06-25 22:46:16 +02:00
8369ffce46 Update README.md 2022-05-20 21:16:11 +02:00
818db8c6e3 Update README.md 2022-05-20 21:15:55 +02:00
9267d78815 Update README.md 2022-05-11 22:21:23 +02:00
de9910936a Update README.md 2022-04-24 00:12:02 +02:00
ea78c81f79 Update README.md 2022-04-24 00:10:53 +02:00
cb7a86c977 Update README 2022-04-24 00:02:06 +02:00
74 changed files with 2059 additions and 246 deletions

View File

@@ -2,27 +2,31 @@
[![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)
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:
- Create a database of assets (model trains) and consits with their metadata - Create a database of assets (model trains) and consists with their metadata
- Manage the database via a simple but rationale backoffice - Manage the database via a simple but rationale backoffice
- Expose main data via an HTML interface to show how beautiful is your collection - Expose main data via an HTML interface to show how beautiful is your collection
to the outside world to the outside world
- Act as a DCC++ EX REST API gateway to control assets remotely via DCC. - Act as a DCC++ EX REST API gateway to control assets remotely via DCC.
By anyone, if you'd like (really?). By anyone, if you'd like (seriously?).
## Preface ## Preface
Project is intended to have fun only and it has been developed with a **This project is work in progress**. It is intended for fun only and
commitment of few minutes a day; it lacks any kind of documentation, code it has been developed with a commitment of few minutes a day;
review, architectural review, security assesment, pentest, ISO certification, it lacks any kind of documentation, code review, architectural review,
etc. security assesment, pentest, ISO certification, etc.
This project probably doesn't match you needs nor expectations. Be aware. This project probably doesn't match you needs nor expectations. Be aware.
Your model train may also catch fire while using this software. Your model train may also catch fire while using this software.
Check out [my own instance](https://daniele.mynarrowgauge.org).
## Components ## Components
Project is based on the following technologies and components: Project is based on the following technologies and components:
@@ -30,20 +34,20 @@ Project is based on the following technologies and components:
- [Django](https://www.djangoproject.com/): *the* web framework - [Django](https://www.djangoproject.com/): *the* web framework
- [Django REST](https://www.django-rest-framework.org/): API for the lazy - [Django REST](https://www.django-rest-framework.org/): API for the lazy
- [Bootstrap](https://getbootstrap.com/): for the web frontend - [Bootstrap](https://getbootstrap.com/): for the web frontend
- [Arduino](https://arduino.cc): DCC hardware - [Arduino](https://arduino.cc): DCC hardware; you must get one, really
- [DCC++ EX Command Station](https://dcc-ex.com/): DCC firmware; an amazing project - [DCC++ EX Command Station](https://dcc-ex.com/): DCC firmware; an amazing project
- [DCC++ EX WebThrottle](https://github.com/DCC-EX/WebThrottle-EX): a slighly modified version of the DCC++ EX web throttle - [DCC++ EX WebThrottle](https://github.com/DCC-EX/WebThrottle-EX): the DCC++ EX web throttle, a slightly modified version
It has been developed with: It has been developed with:
- [vim](https://www.vim.org/): because it rocks - [vim](https://www.vim.org/): because it rocks
- [arduino-cli](https://github.com/arduino/arduino-cli/): a mouse? What the hack? - [arduino-cli](https://github.com/arduino/arduino-cli/): a mouse? What the heck?
- [vim-arduino](https://github.com/stevearc/vim-arduino): another IDE? No thanks - [vim-arduino](https://github.com/stevearc/vim-arduino): another IDE? No thanks
- [podman](https://podman.io/): because containers are fancy - [podman](https://podman.io/): because containers are fancy
- [QEMU (avr)](https://qemu-project.gitlab.io/qemu/system/target-avr.html): QEMU can even make toast! - [QEMU (avr)](https://qemu-project.gitlab.io/qemu/system/target-avr.html): QEMU can even make toast!
## Requirments ## Requirements
- Python 3.8+ - Python 3.8+
- A USB port when running Arduino hardware (and adaptors if you have a Mac) - A USB port when running Arduino hardware (and adaptors if you have a Mac)
@@ -132,11 +136,34 @@ $ podman run --init --cpus 0.1 -d -p 2560:2560 dcc/net-to-serial:sim
To be continued ... To be continued ...
## Screenshots
### 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 2022-07-23 at 22-44-35 Railroad Assets Manager](https://user-images.githubusercontent.com/1818657/180622342-40586d75-239a-400c-93a1-1cb9583a7d17.png)
---
![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
![Screenshot 2022-07-23 at 22-53-43 Railroad Assets Manager](https://user-images.githubusercontent.com/1818657/180622629-65d81eaf-cca4-4f44-b39b-3b0077b43a34.png)
---
### Backoffice
![image](https://user-images.githubusercontent.com/1818657/175789937-3e4970a2-b37d-44c3-8605-62dabe209c65.png)
---
![image](https://user-images.githubusercontent.com/1818657/175789946-d7ce882c-1ba6-49b2-8e0a-1144e5c6bc35.png)
---
![image](https://user-images.githubusercontent.com/1818657/175789954-0735a4ea-bcaf-4a45-adbc-64105091b051.png)
### Rest API
![image](https://user-images.githubusercontent.com/1818657/180622471-ade06c84-c73b-41d5-a2a7-02a95b2ffc02.png)

View File

@@ -1,6 +1,6 @@
# AVR Simulator # AVR Simulator
`qemu-system-avr` tries to use all the CPU cicles (leaving a CPU core stuck at 100%. Limit CPU core usage to 10%. It may be adjusted on slower machines. `qemu-system-avr` tries to use all the CPU cicles (leaving a CPU core stuck at 100%; limit CPU core usage to 10% via `--cpus 0.1`. It can be adjusted on slower machines.
```bash ```bash
$ podman build -t dcc/net-to-serial:sim . $ podman build -t dcc/net-to-serial:sim .

View File

@@ -31,6 +31,7 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
"consist_address", "consist_address",
"company", "company",
"era", "era",
"image",
"notes", "notes",
"tags", "tags",
) )

View File

@@ -0,0 +1,17 @@
# Generated by Django 4.0.6 on 2022-07-10 13:19
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('consist', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='consist',
options={'ordering': ['creation_time']},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.6 on 2022-07-12 12:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('consist', '0002_alter_consist_options'),
]
operations = [
migrations.AddField(
model_name='consist',
name='image',
field=models.ImageField(blank=True, null=True, upload_to='images/'),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 4.0.6 on 2022-07-15 16:08
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('metadata', '0004_alter_rollingstocktype_options_and_more'),
('consist', '0003_consist_image'),
]
operations = [
migrations.AlterField(
model_name='consist',
name='company',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='metadata.company'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 4.1 on 2022-08-23 15:54
import ckeditor_uploader.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("consist", "0004_alter_consist_company"),
]
operations = [
migrations.AlterField(
model_name="consist",
name="notes",
field=ckeditor_uploader.fields.RichTextUploadingField(blank=True),
),
]

View File

@@ -0,0 +1,34 @@
# Generated by Django 4.1 on 2022-08-24 15:30
import markdown
from django.db import migrations
def md_to_html(apps, schema_editor):
fields = {
"Consist": ["notes"],
}
for m in fields.items():
model = apps.get_model("consist", m[0])
for row in model.objects.all():
for field in m[1]:
html = markdown.markdown(getattr(row, field))
row.__dict__[field] = html
row.save(update_fields=m[1])
class Migration(migrations.Migration):
dependencies = [
("consist", "0005_alter_consist_notes"),
]
operations = [
migrations.RunPython(
md_to_html,
reverse_code=migrations.RunPython.noop
),
]

View File

@@ -2,6 +2,8 @@ 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 ckeditor_uploader.fields import RichTextUploadingField
from metadata.models import Company, Tag from metadata.models import Company, Tag
from roster.models import RollingStock from roster.models import RollingStock
@@ -13,20 +15,22 @@ class Consist(models.Model):
consist_address = models.SmallIntegerField( consist_address = models.SmallIntegerField(
default=None, null=True, blank=True default=None, null=True, blank=True
) )
company = models.ForeignKey( company = models.ForeignKey(Company, on_delete=models.CASCADE)
Company, on_delete=models.CASCADE, null=True, blank=True
)
era = models.CharField(max_length=32, blank=True) era = models.CharField(max_length=32, blank=True)
notes = models.TextField(blank=True) image = models.ImageField(upload_to="images/", null=True, blank=True)
notes = RichTextUploadingField(blank=True)
creation_time = models.DateTimeField(auto_now_add=True) creation_time = models.DateTimeField(auto_now_add=True)
updated_time = models.DateTimeField(auto_now=True) updated_time = models.DateTimeField(auto_now=True)
def __str__(self): def __str__(self):
return "{0}".format(self.identifier) return "{0} {1}".format(self.company, self.identifier)
def get_absolute_url(self): def get_absolute_url(self):
return reverse("consist", kwargs={"uuid": self.uuid}) return reverse("consist", kwargs={"uuid": self.uuid})
class Meta:
ordering = ["creation_time"]
class ConsistItem(models.Model): class ConsistItem(models.Model):
consist = models.ForeignKey( consist = models.ForeignKey(

View File

@@ -7,6 +7,10 @@ from driver.models import DriverConfiguration
@admin.register(DriverConfiguration) @admin.register(DriverConfiguration)
class DriverConfigurationAdmin(SingletonModelAdmin): class DriverConfigurationAdmin(SingletonModelAdmin):
fieldsets = ( fieldsets = (
(
"General configuration",
{"fields": ("enabled",)},
),
( (
"Remote DCC-EX configuration", "Remote DCC-EX configuration",
{ {

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.6 on 2022-07-11 18:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('driver', '0004_alter_driverconfiguration_remote_host'),
]
operations = [
migrations.AddField(
model_name='driverconfiguration',
name='enabled',
field=models.BooleanField(default=False),
),
]

View File

@@ -1,10 +1,11 @@
from django.db import models from django.db import models
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from ipaddress import IPv4Address, IPv4Network from ipaddress import IPv4Network
from solo.models import SingletonModel from solo.models import SingletonModel
class DriverConfiguration(SingletonModel): class DriverConfiguration(SingletonModel):
enabled = models.BooleanField(default=False)
remote_host = models.GenericIPAddressField( remote_host = models.GenericIPAddressField(
protocol="IPv4", default="192.168.4.1" protocol="IPv4", default="192.168.4.1"
) )

View File

@@ -34,9 +34,22 @@ def addresschecker(f):
return addresslookup return addresslookup
class IsEnabled(BasePermission):
def has_permission(self, request, view):
config = DriverConfiguration.get_solo()
# if driver is disabled, block all connections
if not config.enabled:
raise Http404
return True
class Firewall(BasePermission): class Firewall(BasePermission):
def has_permission(self, request, view): def has_permission(self, request, view):
config = DriverConfiguration.get_solo() config = DriverConfiguration.get_solo()
# if network is not configured, accept only read ops
if not config.network: if not config.network:
return request.method in SAFE_METHODS return request.method in SAFE_METHODS
@@ -61,7 +74,7 @@ class Test(APIView):
""" """
parser_classes = [PlainTextParser] parser_classes = [PlainTextParser]
permission_classes = [IsAuthenticated | Firewall] permission_classes = [IsEnabled & IsAuthenticated | Firewall]
def get(self, request): def get(self, request):
response = Connector().passthrough("<s>") response = Connector().passthrough("<s>")
@@ -76,7 +89,7 @@ class SendCommand(APIView):
""" """
parser_classes = [PlainTextParser] parser_classes = [PlainTextParser]
permission_classes = [IsAuthenticated | Firewall] permission_classes = [IsEnabled & IsAuthenticated | Firewall]
def put(self, request): def put(self, request):
data = request.data data = request.data
@@ -101,7 +114,7 @@ class Function(APIView):
Send "Function" commands to a valid DCC address Send "Function" commands to a valid DCC address
""" """
permission_classes = [IsAuthenticated | Firewall] permission_classes = [IsEnabled & IsAuthenticated | Firewall]
def put(self, request, address): def put(self, request, address):
serializer = FunctionSerializer(data=request.data) serializer = FunctionSerializer(data=request.data)
@@ -118,7 +131,7 @@ class Cab(APIView):
Send "Cab" commands to a valid DCC address Send "Cab" commands to a valid DCC address
""" """
permission_classes = [IsAuthenticated | Firewall] permission_classes = [IsEnabled & IsAuthenticated | Firewall]
def put(self, request, address): def put(self, request, address):
serializer = CabSerializer(data=request.data) serializer = CabSerializer(data=request.data)
@@ -134,7 +147,7 @@ class Infra(APIView):
Send "Infra" commands to a valid DCC address Send "Infra" commands to a valid DCC address
""" """
permission_classes = [IsAuthenticated | Firewall] permission_classes = [IsEnabled & IsAuthenticated | Firewall]
def put(self, request): def put(self, request):
serializer = InfraSerializer(data=request.data) serializer = InfraSerializer(data=request.data)
@@ -150,7 +163,7 @@ class Emergency(APIView):
Send an "Emergency" stop, no matter the HTTP method used Send an "Emergency" stop, no matter the HTTP method used
""" """
permission_classes = [IsAuthenticated | Firewall] permission_classes = [IsEnabled & IsAuthenticated | Firewall]
def put(self, request): def put(self, request):
Connector().emergency() Connector().emergency()

View File

@@ -1,4 +1,6 @@
from django.contrib import admin from django.contrib import admin
from adminsortable2.admin import SortableAdminMixin
from metadata.models import ( from metadata.models import (
Property, Property,
Decoder, Decoder,
@@ -20,13 +22,13 @@ class DecoderAdmin(admin.ModelAdmin):
readonly_fields = ("image_thumbnail",) readonly_fields = ("image_thumbnail",)
list_display = ("__str__", "interface") list_display = ("__str__", "interface")
list_filter = ("manufacturer", "interface") list_filter = ("manufacturer", "interface")
search_fields = ("__str__",) search_fields = ("name", "manufacturer__name")
@admin.register(Scale) @admin.register(Scale)
class ScaleAdmin(admin.ModelAdmin): class ScaleAdmin(admin.ModelAdmin):
list_display = ("scale", "ratio", "gauge") list_display = ("scale", "ratio", "gauge", "tracks")
list_filter = ("ratio", "gauge") list_filter = ("ratio", "gauge", "tracks")
search_fields = list_display search_fields = list_display
@@ -54,7 +56,7 @@ class TagAdmin(admin.ModelAdmin):
@admin.register(RollingStockType) @admin.register(RollingStockType)
class RollingStockTypeAdmin(admin.ModelAdmin): class RollingStockTypeAdmin(SortableAdminMixin, admin.ModelAdmin):
list_display = ("__str__",) list_display = ("__str__",)
list_filter = ("type", "category") list_filter = ("type", "category")
search_fields = list_display search_fields = ("type", "category")

View File

@@ -0,0 +1,19 @@
# Generated by Django 4.0.6 on 2022-07-10 21:46
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('metadata', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='decoder',
name='manufacturer',
field=models.ForeignKey(limit_choices_to={'category': 'model'}, on_delete=django.db.models.deletion.CASCADE, to='metadata.manufacturer'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.6 on 2022-07-12 10:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('metadata', '0002_alter_decoder_manufacturer'),
]
operations = [
migrations.AddField(
model_name='property',
name='private',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 4.0.6 on 2022-07-14 14:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('metadata', '0003_property_private'),
]
operations = [
migrations.AlterModelOptions(
name='rollingstocktype',
options={'ordering': ['order']},
),
migrations.AddField(
model_name='rollingstocktype',
name='order',
field=models.PositiveSmallIntegerField(default=0),
preserve_default=False,
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.6 on 2022-07-18 21:40
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('metadata', '0004_alter_rollingstocktype_options_and_more'),
]
operations = [
migrations.RenameField(
model_name='scale',
old_name='gauge',
new_name='track',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.6 on 2022-07-18 21:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('metadata', '0005_rename_gauge_scale_track'),
]
operations = [
migrations.AddField(
model_name='scale',
name='gauge',
field=models.CharField(blank=True, max_length=16),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.6 on 2022-07-18 21:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('metadata', '0006_scale_gauge'),
]
operations = [
migrations.RenameField(
model_name='scale',
old_name='track',
new_name='tracks',
),
]

View File

@@ -8,6 +8,7 @@ from ram.utils import get_image_preview, slugify
class Property(models.Model): class Property(models.Model):
name = models.CharField(max_length=128, unique=True) name = models.CharField(max_length=128, unique=True)
private = models.BooleanField(default=False)
class Meta: class Meta:
verbose_name_plural = "Properties" verbose_name_plural = "Properties"
@@ -59,7 +60,11 @@ class Company(models.Model):
class Decoder(models.Model): class Decoder(models.Model):
name = models.CharField(max_length=128, unique=True) name = models.CharField(max_length=128, unique=True)
manufacturer = models.ForeignKey(Manufacturer, on_delete=models.CASCADE) manufacturer = models.ForeignKey(
Manufacturer,
on_delete=models.CASCADE,
limit_choices_to={"category": "model"},
)
version = models.CharField(max_length=64, blank=True) version = models.CharField(max_length=64, blank=True)
interface = models.PositiveSmallIntegerField( interface = models.PositiveSmallIntegerField(
choices=settings.DECODER_INTERFACES, null=True, blank=True choices=settings.DECODER_INTERFACES, null=True, blank=True
@@ -80,6 +85,7 @@ class Scale(models.Model):
scale = models.CharField(max_length=32, unique=True) scale = models.CharField(max_length=32, unique=True)
ratio = models.CharField(max_length=16, blank=True) ratio = models.CharField(max_length=16, blank=True)
gauge = models.CharField(max_length=16, blank=True) gauge = models.CharField(max_length=16, blank=True)
tracks = models.CharField(max_length=16, blank=True)
class Meta: class Meta:
ordering = ["scale"] ordering = ["scale"]
@@ -103,12 +109,14 @@ def tag_pre_save(sender, instance, **kwargs):
class RollingStockType(models.Model): class RollingStockType(models.Model):
type = models.CharField(max_length=64) type = models.CharField(max_length=64)
order = models.PositiveSmallIntegerField()
category = models.CharField( category = models.CharField(
max_length=64, choices=settings.ROLLING_STOCK_TYPES max_length=64, choices=settings.ROLLING_STOCK_TYPES
) )
class Meta(object): class Meta(object):
unique_together = ("category", "type") unique_together = ("category", "type")
ordering = ["order"]
def __str__(self): def __str__(self):
return "{0} {1}".format(self.type, self.category) return "{0} {1}".format(self.type, self.category)

View File

@@ -1,6 +1,38 @@
from django.contrib import admin from django.contrib import admin
from solo.admin import SingletonModelAdmin from solo.admin import SingletonModelAdmin
from portal.models import SiteConfiguration from portal.models import SiteConfiguration, Flatpage
admin.site.register(SiteConfiguration, SingletonModelAdmin) admin.site.register(SiteConfiguration, SingletonModelAdmin)
@admin.register(Flatpage)
class FlatpageAdmin(admin.ModelAdmin):
readonly_fields = ("path", "creation_time", "updated_time")
list_display = ("name", "path", "published", "get_link")
list_filter = ("published",)
search_fields = ("name",)
fieldsets = (
(
None,
{
"fields": (
"name",
"path",
"content",
"published",
)
},
),
(
"Audit",
{
"classes": ("collapse",),
"fields": (
"creation_time",
"updated_time",
),
},
),
)

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.6 on 2022-07-15 12:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal', '0006_alter_siteconfiguration_site_name'),
]
operations = [
migrations.AddField(
model_name='siteconfiguration',
name='items_ordering',
field=models.CharField(choices=[('type', 'By rolling stock type'), ('company', 'By company name'), ('identifier', 'By rolling stock class')], default='type', max_length=10),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 4.0.6 on 2022-08-07 15:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal', '0007_siteconfiguration_items_ordering'),
]
operations = [
migrations.CreateModel(
name='Flatpage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=256, unique=True)),
('draft', models.BooleanField(default=True)),
('content', models.TextField(blank=True)),
],
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 4.0.6 on 2022-08-07 15:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal', '0008_flatpage'),
]
operations = [
migrations.AddField(
model_name='flatpage',
name='path',
field=models.CharField(default='', max_length=256, unique=True),
preserve_default=False,
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 4.0.6 on 2022-08-07 15:46
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('portal', '0009_flatpage_path'),
]
operations = [
migrations.AddField(
model_name='flatpage',
name='creation_time',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='flatpage',
name='updated_time',
field=models.DateTimeField(auto_now=True),
),
]

View File

@@ -0,0 +1,35 @@
# Generated by Django 4.1 on 2022-08-23 15:54
import ckeditor.fields
import ckeditor_uploader.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("portal", "0010_flatpage_creation_time_flatpage_updated_time"),
]
operations = [
migrations.AlterField(
model_name="flatpage",
name="content",
field=ckeditor_uploader.fields.RichTextUploadingField(),
),
migrations.AlterField(
model_name="siteconfiguration",
name="about",
field=ckeditor.fields.RichTextField(blank=True),
),
migrations.AlterField(
model_name="siteconfiguration",
name="footer",
field=ckeditor.fields.RichTextField(blank=True),
),
migrations.AlterField(
model_name="siteconfiguration",
name="footer_extended",
field=ckeditor.fields.RichTextField(blank=True),
),
]

View File

@@ -0,0 +1,38 @@
# Generated by Django 4.1 on 2022-08-24 15:00
import markdown
from django.db import migrations
def md_to_html(apps, schema_editor):
fields = {
"SiteConfiguration": ["about", "footer", "footer_extended"],
"Flatpage": ["content"]
}
for m in fields.items():
model = apps.get_model("portal", m[0])
for row in model.objects.all():
for field in m[1]:
html = markdown.markdown(getattr(row, field))
row.__dict__[field] = html
row.save(update_fields=m[1])
class Migration(migrations.Migration):
dependencies = [
(
"portal",
"0011_alter_flatpage_content_alter_siteconfiguration_about_and_more",
),
]
operations = [
migrations.RunPython(
md_to_html,
reverse_code=migrations.RunPython.noop
),
]

View File

@@ -0,0 +1,42 @@
# Generated by Django 4.1 on 2022-08-25 10:18
from django.db import migrations, models
def reverse_bool(apps, schema_editor):
model = apps.get_model("portal", "Flatpage")
for row in model.objects.all():
row.published = not row.draft
row.save(update_fields=["published"])
def reverse_bool_back(apps, schema_editor):
model = apps.get_model("portal", "Flatpage")
for row in model.objects.all():
row.draft = not row.published
row.save(update_fields=["draft"])
class Migration(migrations.Migration):
dependencies = [
("portal", "0012_md_to_html"),
]
operations = [
migrations.AddField(
model_name="flatpage",
name="published",
field=models.BooleanField(default=False),
),
migrations.RunPython(
reverse_bool,
reverse_code=reverse_bool_back
),
migrations.RemoveField(
model_name="flatpage",
name="draft",
),
]

View File

@@ -1,8 +1,15 @@
import django import django
from django.db import models from django.db import models
from django.urls import reverse
from django.dispatch.dispatcher import receiver
from django.utils.safestring import mark_safe
from solo.models import SingletonModel
from ckeditor.fields import RichTextField
from ckeditor_uploader.fields import RichTextUploadingField
from ram import __version__ as app_version from ram import __version__ as app_version
from solo.models import SingletonModel from ram.utils import slugify
class SiteConfiguration(SingletonModel): class SiteConfiguration(SingletonModel):
@@ -10,14 +17,23 @@ class SiteConfiguration(SingletonModel):
max_length=256, default="Railroad Assets Manager" max_length=256, default="Railroad Assets Manager"
) )
site_author = models.CharField(max_length=256, blank=True) site_author = models.CharField(max_length=256, blank=True)
about = models.TextField(blank=True) about = RichTextField(blank=True)
items_per_page = models.CharField( items_per_page = models.CharField(
max_length=2, max_length=2,
choices=[(str(x * 3), str(x * 3)) for x in range(2, 11)], choices=[(str(x * 3), str(x * 3)) for x in range(2, 11)],
default="6", default="6",
) )
footer = models.TextField(blank=True) items_ordering = models.CharField(
footer_extended = models.TextField(blank=True) max_length=10,
choices=[
("type", "By rolling stock type"),
("company", "By company name"),
("identifier", "By rolling stock class"),
],
default="type",
)
footer = RichTextField(blank=True)
footer_extended = RichTextField(blank=True)
show_version = models.BooleanField(default=True) show_version = models.BooleanField(default=True)
class Meta: class Meta:
@@ -31,3 +47,31 @@ class SiteConfiguration(SingletonModel):
def django_version(self): def django_version(self):
return django.get_version() return django.get_version()
class Flatpage(models.Model):
name = models.CharField(max_length=256, unique=True)
path = models.CharField(max_length=256, unique=True)
published = models.BooleanField(default=False)
content = RichTextUploadingField()
creation_time = models.DateTimeField(auto_now_add=True)
updated_time = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse("flatpage", kwargs={"flatpage": self.path})
def get_link(self):
if self.published:
return mark_safe(
'<a href="{0}" target="_blank">Link</a>'.format(
self.get_absolute_url()
)
)
@receiver(models.signals.pre_save, sender=Flatpage)
def tag_pre_save(sender, instance, **kwargs):
instance.path = slugify(instance.name)

View File

@@ -6,10 +6,19 @@
display: inline-block; display: inline-block;
} }
a.badge, a.badge:hover {
text-decoration: none;
color: #fff;
}
.tab-pane { .tab-pane {
min-height: 300px; min-height: 300px;
} }
.img-thumbnail {
padding: 0;
}
#nav-notes > p { #nav-notes > p {
padding: .5rem; padding: .5rem;
} }

View File

@@ -1,6 +1,6 @@
{% load static %} {% load static %}
{% load solo_tags %} {% load solo_tags %}
{% load markdown %} {% load show_menu %}
{% get_solo 'portal.SiteConfiguration' as site_conf %} {% get_solo 'portal.SiteConfiguration' as site_conf %}
<!doctype html> <!doctype html>
@@ -8,12 +8,14 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content=""> <meta name="color-scheme" content="light dark">
<meta name="description" content="{{ site_conf.about}}">
<meta name="author" content="{{ site_conf.site_author }}"> <meta name="author" content="{{ site_conf.site_author }}">
<meta name="generator" content="Django Framework"> <meta name="generator" content="Django Framework">
<title>{{ site_conf.site_name }}</title> <title>{{ site_conf.site_name }}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/css/bootstrap-nightshade.min.css" rel="stylesheet">
<link href="{% static "css/main.css" %}" rel="stylesheet"> <link href="{% static "css/main.css" %}" rel="stylesheet">
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<style> <style>
.bd-placeholder-img { .bd-placeholder-img {
font-size: 1.125rem; font-size: 1.125rem;
@@ -22,25 +24,31 @@
-moz-user-select: none; -moz-user-select: none;
user-select: none; user-select: none;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.bd-placeholder-img-lg { .bd-placeholder-img-lg {
font-size: 3.5rem; font-size: 3.5rem;
} }
} }
.d-light-inline { display: inline !important; }
.d-dark-inline { display: none !important; }
html.dark .d-light-inline { display: none !important; }
html.dark .d-dark-inline { display: inline !important; }
</style> </style>
</head> </head>
<body> <body>
<header> <header>
<div class="navbar navbar-light bg-light shadow-sm"> <div class="navbar navbar-light bg-light shadow-sm">
<div class="container"> <div class="container">
<a href="/" 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" enable-background="new 0 0 26 26" version="1" 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" fill="#000" overflow="visible" stroke-width="2" style="text-indent:0;text-transform:none"/> <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" fill="#000" overflow="visible" stroke-width="2" style="text-indent:0;text-transform:none"/>
</svg> </svg>
<strong>{{ site_conf.site_name }}</strong> <strong>{{ site_conf.site_name }}</strong>
</a> </a>
{% include 'includes/login.html' %} <div class="btn-group" role="group" aria-label="Basic example">
{% include 'includes/login.html' %}
<a id="darkmode-button" class="btn btn-sm btn-outline-dark"><i class="fa fa-moon-o fa-fw d-none d-light-inline" title="Switch to dark mode"></i><i class="fa fa-sun-o fa-fw d-none d-dark-inline" title="Switch to light mode"></i></a>
</div>
</div> </div>
</div> </div>
</header> </header>
@@ -48,17 +56,24 @@
<div class="container py-2"> <div class="container py-2">
<nav class="navbar navbar-expand-lg navbar-light"> <nav class="navbar navbar-expand-lg navbar-light">
<div class="container-fluid g-0"> <div class="container-fluid g-0">
<a class="navbar-brand" href="/">Home</a> <a class="navbar-brand" href="{% url 'index' %}">Home</a>
<div class="collapse navbar-collapse" id="navbarSupportedContent"> <div class="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"> <li class="nav-item">
<a class="nav-link" href="/portal">Roster</a> <a class="nav-link" href="{% url 'index' %}">Roster</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/portal/consist">Consists</a> <a class="nav-link" href="{% url 'consists' %}">Consists</a>
</li> </li>
<li class="nav-item">
<a class="nav-link" href="{% url 'companies' %}">Companies</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'scales' %}">Scales</a>
</li>
{% show_menu %}
</ul> </ul>
{% include 'includes/search.html' %} {% include 'includes/search.html' %}
</div> </div>
</div> </div>
</nav> </nav>
@@ -70,7 +85,7 @@
</div> </div>
</div> </div>
</section> </section>
<div class="album py-5 bg-light"> <div class="album py-4 bg-light">
<div class="container"> <div class="container">
<a id="rolling-stock"></a> <a id="rolling-stock"></a>
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3"> <div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
@@ -79,14 +94,17 @@
<div class="col"> <div class="col">
<div class="card shadow-sm"> <div class="card shadow-sm">
{% for i in r.image.all %} {% for i in r.image.all %}
{% if i.is_thumbnail %}<a href="/portal/{{ r.uuid }}"><img src="{{ i.image.url }}" alt="Card image cap"></a>{% endif %} {% if i.is_thumbnail %}<a href="{{r.get_absolute_url}}"><img src="{{ i.image.url }}" alt="Card image cap"></a>{% endif %}
{% endfor %} {% endfor %}
<div class="card-body"> <div class="card-body">
<p class="card-text"><strong>{{ r }}</strong></p> <p class="card-text" style="position: relative;">
<strong>{{ r }}</strong>
<a class="stretched-link" href="{{ r.get_absolute_url }}"></a>
</p>
{% if r.tags.all %} {% if r.tags.all %}
<p class="card-text"><small>Tags:</small> <p class="card-text"><small>Tags:</small>
{% for t in r.tags.all %}<span class="badge bg-primary"> {% for t in r.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
{{ t.name }}</span>{# new line is required #} {{ t.name }}</a>{# new line is required #}
{% endfor %} {% endfor %}
</p> </p>
{% endif %} {% endif %}
@@ -123,7 +141,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Scale</th> <th scope="row">Scale</th>
<td><abbr title="{{ r.scale.ratio }} - {{ r.scale.gauge }}">{{ r.scale }}</abbr></td> <td><a href="{% url 'filtered' _filter="scale" search=r.scale %}"><abbr title="{{ r.scale.ratio }} - {{ r.scale.tracks }}">{{ r.scale }}</abbr></a></td>
</tr> </tr>
<tr> <tr>
<th scope="row">SKU</th> <th scope="row">SKU</th>
@@ -150,13 +168,10 @@
</tbody> </tbody>
</table> </table>
{% endif %} {% endif %}
<div class="btn-group mb-4"> <div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="/portal/{{ r.uuid }}">Show all data</a> <a class="btn btn-sm btn-outline-primary" href="{{r.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' r.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:roster_rollingstock_change' r.pk %}">Edit</a>{% endif %}
</div> </div>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">Updated {{ r.updated_time | date:"M d, Y H:m" }}</small>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -170,6 +185,12 @@
</main> </main>
{% include 'includes/footer.html' %} {% include 'includes/footer.html' %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/masonry-layout@4.2.2/dist/masonry.pkgd.min.js" integrity="sha384-GNFwBvfVxBkLMJpYMOABq3c+d3KnQxudP/mGPkzpZSTYykLBNsZEnG2D9G/X/+7D" crossorigin="anonymous" async></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/js/darkmode.min.js"></script>
<!-- script src="https://cdn.jsdelivr.net/npm/masonry-layout@4.2.2/dist/masonry.pkgd.min.js" integrity="sha384-GNFwBvfVxBkLMJpYMOABq3c+d3KnQxudP/mGPkzpZSTYykLBNsZEnG2D9G/X/+7D" crossorigin="anonymous" async></script -->
<script>
document.querySelector("#darkmode-button").onclick = function(e){
darkmode.toggleDarkMode();
}
</script>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,94 @@
{% extends "base.html" %}
{% block header %}
<h1 class="fw-light">Companies</h1>
{% endblock %}
{% block cards %}
{% for c in company %}
<div class="col">
<div class="card shadow-sm">
<div class="card-body">
<p class="card-text" style="position: relative;">
<strong>{{ c.name }}</strong>
</p>
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Company</th>
</tr>
</thead>
<tbody>
{% if c.logo %}
<tr>
<th width="35%" scope="row">Logo</th>
<td><img style="max-height: 48px" src="{{ c.logo.url }}" /></td>
</tr>
{% endif %}
<tr>
<th width="35%" scope="row">Name</th>
<td>{{ c.extended_name }}</td>
</tr>
<tr>
<th width="35%" scope="row">Abbreviation</th>
<td>{{ c }}</td>
</tr>
<tr>
<th width="35%" scope="row">Country</th>
<td>{{ c.country.name }} <img src="{{ c.country.flag }}" alt="{{ c.country }}" />
</tr>
{% if c.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=c %}">Show all rolling stock</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_company_change' c.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}
{% block pagination %}
{% if company.has_other_pages %}
<nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mt-4 mb-0">
{% if company.has_previous %}
<li class="page-item">
<a class="page-link" href="{% url 'company_pagination' page=company.previous_page_number %}#rolling-stock" 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 company.number == i %}
<li class="page-item active">
<span class="page-link">{{ i }}</span></span>
</li>
{% else %}
{% if i == company.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %}
<li class="page-item"><a class="page-link" href="{% url 'company_pagination' page=i %}#rolling-stock">{{ i }}</a></li>
{% endif %}
{% endif %}
{% endfor %}
{% if company.has_next %}
<li class="page-item">
<a class="page-link" href="{% url 'company_pagination' page=company.next_page_number %}#rolling-stock" 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

@@ -1,29 +1,32 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load markdown %}
{% block header %} {% block header %}
<h1 class="fw-light">{{ consist }}</h1> <h1 class="fw-light">{{ consist }}</h1>
{% if consist.tags.all %} {% if consist.tags.all %}
<p><small>Tags:</small> <p><small>Tags:</small>
{% for t in consist.tags.all %}<span class="badge bg-primary"> {% for t in consist.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
{{ t.name }}</span>{# new line is required #} {{ t.name }}</a>{# new line is required #}
{% endfor %} {% endfor %}
</p> </p>
{% endif %} <small class="text-muted">Updated {{ consist.updated_time | date:"M d, Y H:i" }}</small>
{% endif %}
{% endblock %} {% endblock %}
{% block cards %} {% block cards %}
{% for r in rolling_stock %} {% for r in rolling_stock %}
<div class="col"> <div class="col">
<div class="card shadow-sm"> <div class="card shadow-sm">
{% for i in r.rolling_stock.image.all %} {% for i in r.rolling_stock.image.all %}
{% if i.is_thumbnail %}<a href="/portal/{{ r.rolling_stock.uuid }}"><img src="{{ i.image.url }}" alt="Card image cap"></a>{% endif %} {% if i.is_thumbnail %}<a href="{{r.rolling_stock.get_absolute_url}}"><img src="{{ i.image.url }}" alt="Card image cap"></a>{% endif %}
{% endfor %} {% endfor %}
<div class="card-body"> <div class="card-body">
<p class="card-text"><strong>{{ r }}</strong></p> <p class="card-text" style="position: relative;">
<strong>{{ r }}</strong>
<a class="stretched-link" href="{{ r.rolling_stock.get_absolute_url }}"></a>
</p>
{% if r.rolling_stock.tags.all %} {% if r.rolling_stock.tags.all %}
<p class="card-text"><small>Tags:</small> <p class="card-text"><small>Tags:</small>
{% for t in r.rolling_stock.tags.all %}<span class="badge bg-primary"> {% for t in r.rolling_stock.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
{{ t.name }}</span>{# new line is required #} {{ t.name }}</a>{# new line is required #}
{% endfor %} {% endfor %}
</p> </p>
{% endif %} {% endif %}
@@ -60,7 +63,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Scale</th> <th scope="row">Scale</th>
<td><abbr title="{{ r.rolling_stock.scale.ratio }} - {{ r.rolling_stock.scale.gauge }}">{{ r.rolling_stock.scale }}</abbr></td> <td><a href="{% url 'filtered' _filter="scale" search=r.rolling_stock.scale %}"><abbr title="{{ r.rolling_stock.scale.ratio }} - {{ r.rolling_stock.scale.tracks }}">{{ r.rolling_stock.scale }}</abbr></a></td>
</tr> </tr>
<tr> <tr>
<th scope="row">SKU</th> <th scope="row">SKU</th>
@@ -87,13 +90,10 @@
</tbody> </tbody>
</table> </table>
{% endif %} {% endif %}
<div class="btn-group mb-4"> <div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="/portal/{{ r.rolling_stock.uuid }}">Show all data</a> <a class="btn btn-sm btn-outline-primary" href="{{r.rolling_stock.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' r.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' r.rolling_stock.pk %}">Edit</a>{% endif %}
</div> </div>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">Updated {{ r.rolling_stock.updated_time | date:"M d, Y H:m" }}</small>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -102,28 +102,32 @@
{% block pagination %} {% block pagination %}
{% if rolling_stock.has_other_pages %} {% if rolling_stock.has_other_pages %}
<nav aria-label="Page navigation example"> <nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mt-4"> <ul class="pagination justify-content-center mt-4 mb-0">
{% if rolling_stock.has_previous %} {% if rolling_stock.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="/portal/consist/{{ consist.uuid }}/{{ rolling_stock.previous_page_number }}#rolling_stock" tabindex="-1">Previous</a> <a class="page-link" href="{% url 'consist_pagination' uuid=consist.uuid page=rolling_stock.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
<span class="page-link">Previous</span> <span class="page-link">Previous</span>
</li> </li>
{% endif %} {% endif %}
{% for i in rolling_stock.paginator.page_range %} {% for i in page_range %}
{% if rolling_stock.number == i %} {% if rolling_stock.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></span>
</li> </li>
{% else %} {% else %}
<li class="page-item"><a class="page-link" href="/portal/consist/{{ consist.uuid }}/{{ i }}#rolling-stock">{{ i }}</a></li> {% if i == rolling_stock.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %}
<li class="page-item"><a class="page-link" href="{% url 'consist_pagination' uuid=consist.uuid page=i %}#rolling-stock">{{ i }}</a></li>
{% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if rolling_stock.has_next %} {% if rolling_stock.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="/portal/consist/{{ consist.uuid }}/{{ rolling_stock.next_page_number }}#rolling-stock" tabindex="-1">Next</a> <a class="page-link" href="{% url 'consist_pagination' uuid=consist.uuid page=rolling_stock.next_page_number %}#rolling-stock" tabindex="-1">Next</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">

View File

@@ -1,5 +1,4 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load markdown %}
{% block header %} {% block header %}
<h1 class="fw-light">Consists</h1> <h1 class="fw-light">Consists</h1>
@@ -8,12 +7,26 @@
{% for c in consist %} {% for c in consist %}
<div class="col"> <div class="col">
<div class="card shadow-sm"> <div class="card shadow-sm">
<a href="{{ c.get_absolute_url }}">
{% if c.image %}
<img src="{{ c.image.url }}" alt="Card image cap">
{% else %}
{% with c.consist_item.first.rolling_stock as r %}
{% for i in r.image.all %}
{% if i.is_thumbnail %}<img src="{{ i.image.url }}" alt="Card image cap">{% endif %}
{% endfor %}
{% endwith %}
{% endif %}
</a>
<div class="card-body"> <div class="card-body">
<p class="card-text"><strong>{{ c.identifier }}</strong></p> <p class="card-text" style="position: relative;">
<strong>{{ c }}</strong>
<a class="stretched-link" href="{{ c.get_absolute_url }}"></a>
</p>
{% if c.tags.all %} {% if c.tags.all %}
<p class="card-text"><small>Tags:</small> <p class="card-text"><small>Tags:</small>
{% for t in c.tags.all %}<span class="badge bg-primary"> {% for t in c.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
{{ t.name }}</span>{# new line is required #} {{ t.name }}</a>{# new line is required #}
{% endfor %} {% endfor %}
</p> </p>
{% endif %} {% endif %}
@@ -44,12 +57,9 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="btn-group mb-4"> <div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="/portal/consist/{{ c.uuid }}">Show all data</a> <a class="btn btn-sm btn-outline-primary" href="{{ c.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' c.pk #}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:consist_consist_change' c.pk %}">Edit</a>{% endif %}
</div>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">Updated {{ c.updated_time | date:"M d, Y H:m" }}</small>
</div> </div>
</div> </div>
</div> </div>
@@ -57,30 +67,34 @@
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}
{% block pagination %} {% block pagination %}
{% if rolling_stock.has_other_pages %} {% if consist.has_other_pages %}
<nav aria-label="Page navigation example"> <nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mt-4"> <ul class="pagination justify-content-center mt-4 mb-0">
{% if rolling_stock.has_previous %} {% if consist.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="/portal/consist/{{ consist.uuid }}/{{ rolling_stock.previous_page_number }}#rolling_stock" tabindex="-1">Previous</a> <a class="page-link" href="{% url 'consists_pagination' page=consist.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
<span class="page-link">Previous</span> <span class="page-link">Previous</span>
</li> </li>
{% endif %} {% endif %}
{% for i in rolling_stock.paginator.page_range %} {% for i in page_range %}
{% if rolling_stock.number == i %} {% if consist.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></span>
</li> </li>
{% else %} {% else %}
<li class="page-item"><a class="page-link" href="/portal/consist/{{ consist.uuid }}/{{ i }}#rolling-stock">{{ i }}</a></li> {% if i == consist.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %}
<li class="page-item"><a class="page-link" href="{% url 'consists_pagination' page=i %}#rolling-stock">{{ i }}</a></li>
{% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if rolling_stock.has_next %} {% if consist.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="/portal/consist/{{ consist.uuid }}/{{ rolling_stock.next_page_number }}#rolling-stock" tabindex="-1">Next</a> <a class="page-link" href="{% url 'consists_pagination' page=consist.next_page_number %}#rolling-stock" tabindex="-1">Next</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">

View File

@@ -0,0 +1,18 @@
{% extends 'base.html' %}
{% block header %}
<h1 class="fw-light">{{ flatpage.name }}</h1>
<small class="text-muted">Updated {{ flatpage.updated_time | date:"M d, Y H:i" }}</small>
{% endblock %}
{% block extra_content %}
<section class="py-4 text-start container">
<div class="row">
<div class="mx-auto">
<div>{{ flatpage.content | safe }} </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:portal_flatpage_change' flatpage.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</section>
{% endblock %}

View File

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

View File

@@ -1,36 +1,39 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load markdown %}
{% block header %} {% block header %}
<h1 class="fw-light">About</h1> {% if site_conf.about %}<h1 class="fw-light">About</h1>{% endif %}
<p class="lead text-muted">{{ site_conf.about | markdown | safe }}</p> <p class="lead text-muted">{{ site_conf.about | safe }}</p>
{% endblock %} {% endblock %}
{% block pagination %} {% block pagination %}
{% if rolling_stock.has_other_pages %} {% if rolling_stock.has_other_pages %}
<nav aria-label="Page navigation example"> <nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mt-4"> <ul class="pagination justify-content-center mt-4 mb-0">
{% if rolling_stock.has_previous %} {% if rolling_stock.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="/portal/{{ rolling_stock.previous_page_number }}#rolling_stock" tabindex="-1">Previous</a> <a class="page-link" href="{% url 'index_pagination' page=rolling_stock.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
<span class="page-link">Previous</span> <span class="page-link">Previous</span>
</li> </li>
{% endif %} {% endif %}
{% for i in rolling_stock.paginator.page_range %} {% for i in page_range %}
{% if rolling_stock.number == i %} {% if rolling_stock.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></span>
</li> </li>
{% else %} {% else %}
<li class="page-item"><a class="page-link" href="/portal/{{ i }}#rolling-stock">{{ i }}</a></li> {% if i == rolling_stock.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %}
<li class="page-item"><a class="page-link" href="{% url 'index_pagination' page=i %}#rolling-stock">{{ i }}</a></li>
{% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if rolling_stock.has_next %} {% if rolling_stock.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="/portal/{{ rolling_stock.next_page_number }}#rolling-stock" tabindex="-1">Next</a> <a class="page-link" href="{% url 'index_pagination' page=rolling_stock.next_page_number %}#rolling-stock" tabindex="-1">Next</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">

View File

@@ -1,20 +1,18 @@
{% load markdown %}
<footer class="text-muted py-4"> <footer class="text-muted py-4">
<div class="container"> <div class="container">
<p class="float-end mb-1"> <p class="float-end mb-1">
<a href="#">Back to top</a> <a href="#">Back to top</a>
</p> </p>
<div id="footer" class="mb-1"> <div id="footer" class="mb-1">
<p>&copy; {% now "Y" %}</p> {{ site_conf.footer | markdown | safe }} <p>&copy; {% now "Y" %}</p> {{ site_conf.footer | safe }}
</div> </div>
<div id="footer_extended" class="mb-0"> <div id="footer_extended" class="mb-0">
{{ site_conf.footer_extended | markdown | safe }} {{ site_conf.footer_extended | safe }}
</div> </div>
</div> </div>
{% if site_conf.show_version %}
<div class="container"> <div class="container">
<p class="small text-muted">Version: {{ site_conf.version }}</p> <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 %}
</div> </div>
{% endif %}
</footer> </footer>

View File

@@ -0,0 +1,7 @@
<section class="py-4 text-center container">
<div class="row">
<div class="mx-auto">
{% block header_content %}{% endblock %}
</div>
</div>
</section>

View File

@@ -7,6 +7,7 @@
<li><a class="dropdown-item" href="{% url 'admin:roster_rollingstock_changelist' %}">Rolling stock</a></li> <li><a class="dropdown-item" href="{% url 'admin:roster_rollingstock_changelist' %}">Rolling stock</a></li>
<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><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>

View File

@@ -1,5 +1,24 @@
<form class="d-flex" action="/portal/search" method="post"> <form class="d-flex needs-validation" action="{% url 'search' %}" method="post" novalidate>
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search" name="search"> <div class="input-group has-validation">
<button class="btn btn-outline-primary" type="submit">Search</button> <input class="form-control me-2" type="search" placeholder="Search" aria-label="Search" name="search" id="searchValidation" required>
<button class="btn btn-outline-primary" type="submit">Search</button>
</div>
</form> </form>
<script>
(function () {
'use strict'
// Fetch all the forms we want to apply custom Bootstrap validation styles to
var forms = document.querySelectorAll('.needs-validation')
// Loop over them and prevent submission
Array.prototype.slice.call(forms)
.forEach(function (form) {
form.addEventListener('submit', function (event) {
if (!form.checkValidity()) {
form.classList.add('was-validated')
event.preventDefault()
event.stopPropagation()
}
}, false)
})
})()
</script>

View File

@@ -1,20 +1,37 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load markdown %}
{% block header %} {% block header %}
<h1 class="fw-light">{{ rolling_stock }}</h1> <h1 class="fw-light">{{ rolling_stock }}</h1>
{% if rolling_stock.tags.all %} {% if rolling_stock.tags.all %}
<p><small>Tags:</small> <p><small>Tags:</small>
{% for t in rolling_stock.tags.all %}<span class="badge bg-primary"> {% for t in rolling_stock.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
{{ t.name }}</span>{# new line is required #} {{ t.name }}</a>{# new line is required #}
{% endfor %} {% endfor %}
</p> </p>
{% endif %} {% endif %}
<small class="text-muted">Updated {{ rolling_stock.updated_time | date:"M d, Y H:i" }}</small>
{% endblock %} {% endblock %}
{% block cards %} {% block cards %}
{% for t in rolling_stock.image.all %} {% for t in rolling_stock.image.all %}
<div class="col"> <div class="col">
<img class="img-thumbnail" src="{{ t.image.url }}" alt="Rolling stock image"> <a href="" data-bs-toggle="modal" data-bs-target="#pictureModal{{ forloop.counter }}"><img class="img-thumbnail" src="{{ t.image.url }}" alt="Rolling stock image"></a>
</div>
<!-- Modal -->
<div class="modal fade" id="pictureModal{{ forloop.counter }}" tabindex="-1" aria-labelledby="pictureModalLabel{{ forloop.counter }}" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="pictureModalLabel{{ forloop.counter }}">{{ rolling_stock }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center">
<img class="rounded img-fluid" src="{{ t.image.url }}" alt="Rolling stock image">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div> </div>
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}
@@ -29,8 +46,8 @@
<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 data</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 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.property.count > 0 %}<button class="nav-link" id="nav-properties-tab" data-bs-toggle="tab" data-bs-target="#nav-properties" type="button" role="tab" aria-controls="nav-properties" aria-selected="false">Properties</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 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 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> </div>
</nav> </nav>
<div class="tab-content" id="nav-tabContent"> <div class="tab-content" id="nav-tabContent">
@@ -73,11 +90,11 @@
<tbody> <tbody>
<tr> <tr>
<th width="35%" scope="row">Manufacturer</th> <th width="35%" scope="row">Manufacturer</th>
<td>{% if rolling_stock.manufacturer.website %}<a href="{{ rolling_stock.manufacturer.website }}">{% endif %}{{ rolling_stock.manufacturer }}{% if rolling_stock.manufacturer.website %}</a>{% endif %}</th> <td>{% if rolling_stock.manufacturer.website %}<a href="{{ rolling_stock.manufacturer.website }}">{% endif %}{{ rolling_stock.manufacturer|default_if_none:"" }}{% if rolling_stock.manufacturer.website %}</a>{% endif %}</th>
</tr> </tr>
<tr> <tr>
<th scope="row">Scale</th> <th scope="row">Scale</th>
<td><abbr title="{{ rolling_stock.scale.ratio }} - {{ rolling_stock.scale.gauge }}">{{ rolling_stock.scale }}</abbr></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">SKU</th>
@@ -115,18 +132,18 @@
<tbody> <tbody>
<tr> <tr>
<th width="35%" scope="row">Manufacturer</th> <th width="35%" scope="row">Manufacturer</th>
<td>{% if rolling_stock.manufacturer.website %}<a href="{{ rolling_stock.manufacturer.website }}">{% endif %}{{ rolling_stock.manufacturer }}{% if rolling_stock.manufacturer.website %}</a>{% endif %}</th> <td>{% if rolling_stock.manufacturer.website %}<a href="{{ rolling_stock.manufacturer.website }}">{% endif %}{{ rolling_stock.manufacturer|default_if_none:"" }}{% if rolling_stock.manufacturer.website %}</a>{% endif %}</th>
</tr> </tr>
<tr> <tr>
<th scope="row">Scale</th> <th scope="row">Scale</th>
<td><abbr title="{{ rolling_stock.scale.ratio }} - {{ rolling_stock.scale.gauge }}">{{ rolling_stock.scale }}</abbr></td> <td><abbr title="{{ rolling_stock.scale.ratio }} - {{ rolling_stock.scale.tracks }}">{{ rolling_stock.scale }}</abbr></td>
</tr> </tr>
<tr> <tr>
<th scope="row">SKU</th> <th scope="row">SKU</th>
<td>{{ rolling_stock.sku }}</td> <td>{{ rolling_stock.sku }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">ERA</th> <th scope="row">Era</th>
<td>{{ rolling_stock.era }}</td> <td>{{ rolling_stock.era }}</td>
</tr> </tr>
<tr> <tr>
@@ -139,6 +156,23 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
{% if rolling_stock_properties %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Properties</th>
</tr>
</thead>
<tbody>
{% for p in rolling_stock_properties %}
<tr>
<th width="35%" scope="row">{{ p.property }}</th>
<td>{{ p.value }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div> </div>
<div class="tab-pane fade" id="nav-class" role="tabpanel" aria-labelledby="nav-class-tab"> <div class="tab-pane fade" id="nav-class" role="tabpanel" aria-labelledby="nav-class-tab">
<table class="table table-striped"> <table class="table table-striped">
@@ -150,7 +184,7 @@
<tbody> <tbody>
<tr> <tr>
<th width="35%" scope="row">Class</th> <th width="35%" scope="row">Class</th>
<td>{{ rolling_stock.rolling_class }}</td> <td>{{ rolling_stock.rolling_class.identifier }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Type</th> <th scope="row">Type</th>
@@ -166,10 +200,27 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Manufacturer</th> <th scope="row">Manufacturer</th>
<td>{{ rolling_stock.rolling_class.manufacturer }}</td> <td>{{ rolling_stock.rolling_class.manufacturer|default_if_none:"" }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
{% if class_properties %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Properties</th>
</tr>
</thead>
<tbody>
{% for p in class_properties %}
<tr>
<th width="35%" scope="row">{{ p.property }}</th>
<td>{{ p.value }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div> </div>
<div class="tab-pane fade" id="nav-dcc" role="tabpanel" aria-labelledby="nav-dcc-tab"> <div class="tab-pane fade" id="nav-dcc" role="tabpanel" aria-labelledby="nav-dcc-tab">
<table class="table table-striped"> <table class="table table-striped">
@@ -189,7 +240,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Manufacturer</th> <th scope="row">Manufacturer</th>
<td>{{ rolling_stock.decoder.manufacturer }}</td> <td>{{ rolling_stock.decoder.manufacturer|default_if_none:"" }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Version</th> <th scope="row">Version</th>
@@ -207,24 +258,7 @@
</table> </table>
</div> </div>
<div class="tab-pane fade" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab"> <div class="tab-pane fade" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab">
{{ rolling_stock.notes | markdown | safe }} {{ rolling_stock.notes | safe }}
</div>
<div class="tab-pane fade" id="nav-properties" role="tabpanel" aria-labelledby="nav-properties-tab">
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Properties</th>
</tr>
</thead>
<tbody>
{% for p in rolling_stock.property.all %}
<tr>
<th scope="row">{{ p.property }}</th>
<td>{{ p.value }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
<div class="tab-pane fade" id="nav-documents" role="tabpanel" aria-labelledby="nav-documents-tab"> <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">
@@ -244,6 +278,23 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="tab-pane fade" id="nav-journal" role="tabpanel" aria-labelledby="nav-journal-tab">
<table class="table table-striped">
<thead>
<tr>
<th colspan="3" scope="row">Journal</th>
</tr>
</thead>
<tbody>
{% for j in rolling_stock_journal %}
<tr>
<th width="35%" scope="row">{{ j.date }}</th>
<td>{{ j.log | safe }}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div> </div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end"> <div class="d-grid gap-2 d-md-flex justify-content-md-end">
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin: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 %}

View File

@@ -0,0 +1,84 @@
{% extends "base.html" %}
{% block header %}
<h1 class="fw-light">Scales</h1>
{% endblock %}
{% block cards %}
{% for s in scale %}
<div class="col">
<div class="card shadow-sm">
<div class="card-body">
<p class="card-text"><strong>{{ s }}</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>{{ s.scale }}</td>
</tr>
<tr>
<th width="35%" scope="row">Ratio</th>
<td>{{ s.ratio }}</td>
</tr>
<tr>
<th width="35%" scope="row">Gauge</th>
<td>{{ s.gauge }}</td>
</tr>
<tr>
<th width="35%" scope="row">Tracks</th>
<td>{{ s.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=s %}">Show all rolling stock</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_scale_change' s.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}
{% block pagination %}
{% if scale.has_other_pages %}
<nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mt-4 mb-0">
{% if scale.has_previous %}
<li class="page-item">
<a class="page-link" href="{% url 'scale_pagination' page=scale.previous_page_number %}#rolling-stock" 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 scale.number == i %}
<li class="page-item active">
<span class="page-link">{{ i }}</span></span>
</li>
{% else %}
{% if i == scale.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %}
<li class="page-item"><a class="page-link" href="{% url 'scale_pagination' page=i %}#rolling-stock">{{ i }}</a></li>
{% endif %}
{% endif %}
{% endfor %}
{% if scale.has_next %}
<li class="page-item">
<a class="page-link" href="{% url 'scale_pagination' page=scale.next_page_number %}#rolling-stock" 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

@@ -7,28 +7,32 @@
{% block pagination %} {% block pagination %}
{% if rolling_stock.has_other_pages %} {% if rolling_stock.has_other_pages %}
<nav aria-label="Page navigation example"> <nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mt-4"> <ul class="pagination justify-content-center mt-4 mb-0">
{% if rolling_stock.has_previous %} {% if rolling_stock.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="/portal/search/{{ search }}/{{ rolling_stock.previous_page_number }}#rolling_stock" tabindex="-1">Previous</a> <a class="page-link" href="{% url 'filtered_pagination' _filter=filter search=search page=rolling_stock.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
<span class="page-link">Previous</span> <span class="page-link">Previous</span>
</li> </li>
{% endif %} {% endif %}
{% for i in rolling_stock.paginator.page_range %} {% for i in page_range %}
{% if rolling_stock.number == i %} {% if rolling_stock.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></span>
</li> </li>
{% else %} {% else %}
<li class="page-item"><a class="page-link" href="/portal/search/{{ search }}/{{ i }}#rolling-stock">{{ i }}</a></li> {% if i == rolling_stock.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %}
<li class="page-item"><a class="page-link" href="{% url 'filtered_pagination' _filter=filter search=search page=i %}#rolling-stock">{{ i }}</a></li>
{% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if rolling_stock.has_next %} {% if rolling_stock.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="/portal/search/{{ search }}/{{ rolling_stock.next_page_number }}#rolling-stock" tabindex="-1">Next</a> <a class="page-link" href="{% url 'filtered_pagination' _filter=filter search=search page=rolling_stock.next_page_number %}#rolling-stock" tabindex="-1">Next</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">

View File

@@ -0,0 +1,10 @@
from django import template
from portal.views import Flatpage
register = template.Library()
@register.inclusion_tag('flatpage_menu.html')
def show_menu():
menu = Flatpage.objects.filter(published=True).order_by("name")
return {"menu": menu}

View File

@@ -3,32 +3,45 @@ from django.urls import path
from portal.views import ( from portal.views import (
GetHome, GetHome,
GetHomeFiltered, GetHomeFiltered,
GetFlatpage,
GetRollingStock, GetRollingStock,
GetConsist, GetConsist,
Consists, Consists,
Companies,
Scales,
) )
urlpatterns = [ urlpatterns = [
path("", GetHome.as_view(), name="index"), path("", GetHome.as_view(), name="index"),
path("<int:page>", GetHome.as_view(), name="index_pagination"), path("<int:page>", GetHome.as_view(), name="index_pagination"),
path(
"page/<str:flatpage>",
GetFlatpage.as_view(),
name="flatpage",
),
path( path(
"search", "search",
GetHomeFiltered.as_view(http_method_names=["post"]), GetHomeFiltered.as_view(http_method_names=["post"]),
name="search", name="search",
), ),
path("search/<str:search>", GetHomeFiltered.as_view(), name="search"), path("consists", Consists.as_view(), name="consists"),
path( path(
"search/<str:search>/<int:page>", "consists/<int:page>", Consists.as_view(), name="consists_pagination"
GetHomeFiltered.as_view(),
name="search_pagination",
), ),
path("consist", Consists.as_view(), name="consists"),
path("consist/<uuid:uuid>", GetConsist.as_view(), name="consist"), path("consist/<uuid:uuid>", GetConsist.as_view(), name="consist"),
path( path(
"consist/<uuid:uuid>/<int:page>", "consist/<uuid:uuid>/<int:page>",
GetConsist.as_view(), GetConsist.as_view(),
name="consist_pagination", name="consist_pagination",
), ),
path("companies", Companies.as_view(), name="companies"),
path(
"companies/<int:page>",
Companies.as_view(),
name="companies_pagination",
),
path("scales", Scales.as_view(), name="scales"),
path("scales/<int:page>", Scales.as_view(), name="scales_pagination"),
path( path(
"<str:_filter>/<str:search>", "<str:_filter>/<str:search>",
GetHomeFiltered.as_view(), GetHomeFiltered.as_view(),

View File

@@ -6,33 +6,54 @@ from django.http import Http404
from django.db.models import Q from django.db.models import Q
from django.shortcuts import render from django.shortcuts import render
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.core.paginator import Paginator, PageNotAnInteger
from portal.utils import get_site_conf from portal.utils import get_site_conf
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, Scale
def order_by_fields():
order_by = get_site_conf().items_ordering
fields = [
"rolling_class__type",
"rolling_class__company",
"rolling_class__identifier",
"road_number_int",
]
if order_by == "type":
return (fields[0], fields[1], fields[2], fields[3])
elif order_by == "company":
return (fields[1], fields[0], fields[2], fields[3])
elif order_by == "identifier":
return (fields[2], fields[0], fields[1], fields[3])
class GetHome(View): class GetHome(View):
def get(self, request, page=1): def get(self, request, page=1):
site_conf = get_site_conf() site_conf = get_site_conf()
rolling_stock = RollingStock.objects.all() rolling_stock = RollingStock.objects.order_by(*order_by_fields())
paginator = Paginator(rolling_stock, site_conf.items_per_page) paginator = Paginator(rolling_stock, site_conf.items_per_page)
rolling_stock = paginator.get_page(page)
page_range = paginator.get_elided_page_range(
rolling_stock.number, on_each_side=2, on_ends=1
)
try: return render(
rolling_stock = paginator.page(page) request,
except PageNotAnInteger: "home.html",
rolling_stock = paginator.page(1) {"rolling_stock": rolling_stock, "page_range": page_range},
except EmptyPage: )
rolling_stock = paginator.page(paginator.num_pages)
return render(request, "home.html", {"rolling_stock": rolling_stock})
class GetHomeFiltered(View): class GetHomeFiltered(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 == "search":
query = reduce( query = reduce(
operator.or_, operator.or_,
( (
@@ -41,6 +62,7 @@ class GetHomeFiltered(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(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)
@@ -56,25 +78,28 @@ class GetHomeFiltered(View):
| Q(rolling_class__company__extended_name__icontains=search) | Q(rolling_class__company__extended_name__icontains=search)
) )
elif _filter == "scale": elif _filter == "scale":
query = Q(scale__scale__icontains=search) query = Q(scale__scale__iexact=search)
elif _filter == "tag":
query = Q(tags__slug__iexact=search)
else: else:
raise Http404 raise Http404
rolling_stock = RollingStock.objects.filter(query) rolling_stock = RollingStock.objects.filter(query).order_by(
*order_by_fields()
)
matches = len(rolling_stock) matches = len(rolling_stock)
paginator = Paginator(rolling_stock, site_conf.items_per_page) paginator = Paginator(rolling_stock, site_conf.items_per_page)
rolling_stock = paginator.get_page(page)
page_range = paginator.get_elided_page_range(
rolling_stock.number, on_each_side=2, on_ends=1
)
try: return rolling_stock, matches, page_range
rolling_stock = paginator.page(page)
except PageNotAnInteger:
rolling_stock = paginator.page(1)
except EmptyPage:
rolling_stock = paginator.page(paginator.num_pages)
return rolling_stock, matches def get(self, request, search, _filter="search", page=1):
rolling_stock, matches, page_range = self.run_search(
def get(self, request, search, _filter=None, page=1): request, search, _filter, page
rolling_stock, matches = self.run_search( )
request, search, _filter, page)
return render( return render(
request, request,
@@ -84,15 +109,17 @@ class GetHomeFiltered(View):
"filter": _filter, "filter": _filter,
"matches": matches, "matches": matches,
"rolling_stock": rolling_stock, "rolling_stock": rolling_stock,
"page_range": page_range,
}, },
) )
def post(self, request, _filter=None, page=1): def post(self, request, _filter="search", page=1):
search = request.POST.get("search") search = request.POST.get("search")
if not search: if not search:
raise Http404 raise Http404
rolling_stock, matches = self.run_search( rolling_stock, matches, page_range = self.run_search(
request, search, _filter, page) request, search, _filter, page
)
return render( return render(
request, request,
@@ -102,6 +129,7 @@ class GetHomeFiltered(View):
"filter": _filter, "filter": _filter,
"matches": matches, "matches": matches,
"rolling_stock": rolling_stock, "rolling_stock": rolling_stock,
"page_range": page_range,
}, },
) )
@@ -113,11 +141,33 @@ class GetRollingStock(View):
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise Http404 raise Http404
class_properties = (
rolling_stock.rolling_class.property.all()
if request.user.is_authenticated
else rolling_stock.rolling_class.property.filter(
property__private=False
)
)
rolling_stock_properties = (
rolling_stock.property.all()
if request.user.is_authenticated
else rolling_stock.property.filter(property__private=False)
)
rolling_stock_journal = (
rolling_stock.journal.all()
if request.user.is_authenticated
else rolling_stock.journal.filter(private=False)
)
return render( return render(
request, request,
"page.html", "page.html",
{ {
"rolling_stock": rolling_stock, "rolling_stock": rolling_stock,
"class_properties": class_properties,
"rolling_stock_properties": rolling_stock_properties,
"rolling_stock_journal": rolling_stock_journal,
}, },
) )
@@ -126,16 +176,18 @@ class Consists(View):
def get(self, request, page=1): def get(self, request, page=1):
site_conf = get_site_conf() site_conf = get_site_conf()
consist = Consist.objects.all() consist = Consist.objects.all()
paginator = Paginator(consist, site_conf.items_per_page) paginator = Paginator(consist, site_conf.items_per_page)
consist = paginator.get_page(page)
page_range = paginator.get_elided_page_range(
consist.number, on_each_side=2, on_ends=1
)
try: return render(
consist = paginator.page(page) request,
except PageNotAnInteger: "consists.html",
consist = paginator.page(1) {"consist": consist, "page_range": page_range},
except EmptyPage: )
consist = paginator.page(paginator.num_pages)
return render(request, "consists.html", {"consist": consist})
class GetConsist(View): class GetConsist(View):
@@ -146,17 +198,71 @@ class GetConsist(View):
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise Http404 raise Http404
rolling_stock = consist.consist_item.all() rolling_stock = consist.consist_item.all()
paginator = Paginator(rolling_stock, site_conf.items_per_page)
try: paginator = Paginator(rolling_stock, site_conf.items_per_page)
rolling_stock = paginator.page(page) rolling_stock = paginator.get_page(page)
except PageNotAnInteger: page_range = paginator.get_elided_page_range(
rolling_stock = paginator.page(1) rolling_stock.number, on_each_side=2, on_ends=1
except EmptyPage: )
rolling_stock = paginator.page(paginator.num_pages)
return render( return render(
request, request,
"consist.html", "consist.html",
{"consist": consist, "rolling_stock": rolling_stock}, {
"consist": consist,
"rolling_stock": rolling_stock,
"page_range": page_range,
},
)
class Companies(View):
def get(self, request, page=1):
site_conf = get_site_conf()
company = Company.objects.all()
paginator = Paginator(company, site_conf.items_per_page)
company = paginator.get_page(page)
page_range = paginator.get_elided_page_range(
company.number, on_each_side=2, on_ends=1
)
return render(
request,
"companies.html",
{"company": company, "page_range": page_range},
)
class Scales(View):
def get(self, request, page=1):
site_conf = get_site_conf()
scale = Scale.objects.all()
paginator = Paginator(scale, site_conf.items_per_page)
scale = paginator.get_page(page)
page_range = paginator.get_elided_page_range(
scale.number, on_each_side=2, on_ends=1
)
return render(
request,
"scales.html",
{"scale": scale, "page_range": page_range},
)
class GetFlatpage(View):
def get(self, request, flatpage):
try:
flatpage = Flatpage.objects.get(
Q(Q(path=flatpage) & Q(published=True))
)
except ObjectDoesNotExist:
raise Http404
return render(
request,
"flatpage.html",
{"flatpage": flatpage},
) )

View File

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

View File

@@ -0,0 +1,20 @@
# vim: syntax=python
"""
Django local_settings for ram project.
"""
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = (
"django-insecure-1fgtf05rwp0qp05@ef@a7%x#o+t6vk6063py=vhdmut0j!8s4u"
)
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
STORAGE_DIR = BASE_DIR / "storage"
ALLOWED_HOSTS = ["127.0.0.1"]
CSRF_TRUSTED_ORIGINS = ["https://myhost"]
ROOT_URLCONF = "ram.urls"
STATIC_URL = "static/"
MEDIA_URL = "media/"

View File

@@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/4.0/ref/settings/
""" """
import os import os
import time
from pathlib import Path from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
@@ -45,6 +46,8 @@ INSTALLED_APPS = [
"adminsortable2", "adminsortable2",
"django_countries", "django_countries",
"solo", "solo",
"ckeditor",
"ckeditor_uploader",
"rest_framework", "rest_framework",
"ram", "ram",
"portal", "portal",
@@ -139,6 +142,7 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
MEDIA_URL = "media/" MEDIA_URL = "media/"
MEDIA_ROOT = STORAGE_DIR / "media" MEDIA_ROOT = STORAGE_DIR / "media"
CKEDITOR_UPLOAD_PATH = "uploads/"
COUNTRIES_OVERRIDE = { COUNTRIES_OVERRIDE = {
"ZZ": "Freelance", "ZZ": "Freelance",
@@ -161,3 +165,10 @@ ROLLING_STOCK_TYPES = [
("equipment", "Equipment"), ("equipment", "Equipment"),
("other", "Other"), ("other", "Other"),
] ]
try:
from ram.local_settings import *
except ImportError:
# If a local_setting.py does not exist
# settings in this file only will be used
pass

View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<title>Swagger</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="//unpkg.com/swagger-ui-dist@3/swagger-ui.css" />
</head>
<body>
<div id="swagger-ui"></div>
<script src="//unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js"></script>
<script>
const ui = SwaggerUIBundle({
url: "{% url schema_url %}",
dom_id: '#swagger-ui',
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
layout: "BaseLayout"
})
</script>
</body>
</html>

View File

@@ -20,7 +20,8 @@ from django.contrib import admin
from django.urls import include, path from django.urls import include, path
urlpatterns = [ urlpatterns = [
path("", lambda r: redirect("/portal/")), path("", lambda r: redirect("portal/")),
path("ckeditor/", include("ckeditor_uploader.urls")),
path("portal/", include("portal.urls")), path("portal/", include("portal.urls")),
path("ht/", include("health_check.urls")), path("ht/", include("health_check.urls")),
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
@@ -29,18 +30,26 @@ urlpatterns = [
path("api/v1/dcc/", include("driver.urls")), path("api/v1/dcc/", include("driver.urls")),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# 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
#
# urlpatterns += [ urlpatterns += [
# path('swagger/', TemplateView.as_view( path(
# template_name='swagger.html', "swagger/",
# extra_context={'schema_url': 'openapi-schema'} TemplateView.as_view(
# ), name='swagger'), template_name="swagger.html",
# path('openapi', get_schema_view( extra_context={"schema_url": "openapi-schema"},
# title="BITE - A Basic/IoT/Example", ),
# description="BITE API for IoT", name="swagger",
# version="1.0.0" ),
# ), name='openapi-schema'), path(
# ] "openapi",
get_schema_view(
title="RAM - Railroad Assets Manager",
description="RAM API",
version="1.0.0",
),
name="openapi-schema",
),
]

View File

@@ -6,6 +6,7 @@ from roster.models import (
RollingStockImage, RollingStockImage,
RollingStockDocument, RollingStockDocument,
RollingStockProperty, RollingStockProperty,
RollingStockJournal,
) )
@@ -20,13 +21,18 @@ class RollingClass(admin.ModelAdmin):
inlines = (RollingClassPropertyInline,) inlines = (RollingClassPropertyInline,)
list_display = ("__str__", "type", "company") list_display = ("__str__", "type", "company")
list_filter = ("company", "type__category", "type") list_filter = ("company", "type__category", "type")
search_fields = list_display search_fields = (
"identifier",
"company__name",
"type__type",
)
class RollingStockDocInline(admin.TabularInline): class RollingStockDocInline(admin.TabularInline):
model = RollingStockDocument model = RollingStockDocument
min_num = 0 min_num = 0
extra = 0 extra = 0
classes = ["collapse"]
class RollingStockImageInline(admin.TabularInline): class RollingStockImageInline(admin.TabularInline):
@@ -34,6 +40,7 @@ class RollingStockImageInline(admin.TabularInline):
min_num = 0 min_num = 0
extra = 0 extra = 0
readonly_fields = ("image_thumbnail",) readonly_fields = ("image_thumbnail",)
classes = ["collapse"]
class RollingStockPropertyInline(admin.TabularInline): class RollingStockPropertyInline(admin.TabularInline):
@@ -42,12 +49,56 @@ class RollingStockPropertyInline(admin.TabularInline):
extra = 0 extra = 0
class RollingStockJournalInline(admin.TabularInline):
model = RollingStockJournal
min_num = 0
extra = 0
classes = ["collapse"]
@admin.register(RollingStockDocument)
class RollingStockDocumentAdmin(admin.ModelAdmin):
list_display = (
"__str__",
"rolling_stock",
"description",
"download",
)
search_fields = (
"rolling_stock__rolling_class__identifier",
"rolling_stock__sku",
"description",
"file",
)
@admin.register(RollingStockJournal)
class RollingJournalDocumentAdmin(admin.ModelAdmin):
list_display = (
"__str__",
"date",
"rolling_stock",
"private",
)
list_filter = (
"date",
"private",
)
search_fields = (
"rolling_stock__rolling_class__identifier",
"rolling_stock__road_number",
"rolling_stock__sku",
"log",
)
@admin.register(RollingStock) @admin.register(RollingStock)
class RollingStockAdmin(admin.ModelAdmin): class RollingStockAdmin(admin.ModelAdmin):
inlines = ( inlines = (
RollingStockPropertyInline, RollingStockPropertyInline,
RollingStockImageInline, RollingStockImageInline,
RollingStockDocInline, RollingStockDocInline,
RollingStockJournalInline,
) )
readonly_fields = ("creation_time", "updated_time") readonly_fields = ("creation_time", "updated_time")
list_display = ( list_display = (
@@ -62,10 +113,18 @@ class RollingStockAdmin(admin.ModelAdmin):
list_filter = ( list_filter = (
"rolling_class__type__category", "rolling_class__type__category",
"rolling_class__type", "rolling_class__type",
"rolling_class__company__name",
"scale", "scale",
"manufacturer", "manufacturer",
) )
search_fields = list_display search_fields = (
"rolling_class__identifier",
"rolling_class__company__name",
"manufacturer__name",
"road_number",
"address",
"sku",
)
fieldsets = ( fieldsets = (
( (

View File

@@ -0,0 +1,24 @@
# Generated by Django 4.0.6 on 2022-07-13 17:58
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('roster', '0005_alter_rollingstockproperty_rolling_stock'),
]
operations = [
migrations.AlterField(
model_name='rollingclassproperty',
name='rolling_class',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='property', to='roster.rollingclass', verbose_name='Class'),
),
migrations.AlterField(
model_name='rollingstock',
name='rolling_class',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rolling_class', to='roster.rollingclass', verbose_name='Class'),
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 4.0.6 on 2022-07-15 15:24
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('metadata', '0004_alter_rollingstocktype_options_and_more'),
('roster', '0006_alter_rollingclassproperty_rolling_class_and_more'),
]
operations = [
migrations.AlterField(
model_name='rollingclass',
name='company',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='metadata.company'),
),
migrations.AlterField(
model_name='rollingclass',
name='type',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='metadata.rollingstocktype'),
),
]

View File

@@ -0,0 +1,34 @@
# Generated by Django 4.0.6 on 2022-07-15 15:55
from django.db import migrations, models
def gen_road_number_cleaned(apps, schema_editor):
RollingStock = apps.get_model('roster', 'RollingStock')
for row in RollingStock.objects.all():
row.road_number_cleaned = row.road_number.lstrip('#').lstrip('0')
row.save(update_fields=['road_number_cleaned'])
class Migration(migrations.Migration):
dependencies = [
('roster', '0007_alter_rollingclass_company_alter_rollingclass_type'),
]
operations = [
migrations.AddField(
model_name='rollingstock',
name='road_number_cleaned',
field=models.CharField(default='', max_length=128),
preserve_default=False,
),
migrations.RunPython(
gen_road_number_cleaned,
reverse_code=migrations.RunPython.noop
),
migrations.AlterModelOptions(
name='rollingstock',
options={'ordering': ['rolling_class', 'road_number_cleaned'], 'verbose_name_plural': 'Rolling stock'},
),
]

View File

@@ -0,0 +1,41 @@
# Generated by Django 4.0.6 on 2022-07-16 15:38
import re
from django.db import migrations, models
def gen_road_number_cleaned(apps, schema_editor):
RollingStock = apps.get_model('roster', 'RollingStock')
for row in RollingStock.objects.all():
try:
row.road_number_int = int(re.findall(r"\d+", row.road_number)[0])
row.save(update_fields=['road_number_int'])
except IndexError:
pass
class Migration(migrations.Migration):
dependencies = [
('roster', '0008_rollingstock_road_number_cleaned'),
]
operations = [
migrations.AlterModelOptions(
name='rollingstock',
options={'ordering': ['rolling_class', 'road_number_int'], 'verbose_name_plural': 'Rolling stock'},
),
migrations.RemoveField(
model_name='rollingstock',
name='road_number_cleaned',
),
migrations.AddField(
model_name='rollingstock',
name='road_number_int',
field=models.PositiveSmallIntegerField(default=0),
),
migrations.RunPython(
gen_road_number_cleaned,
reverse_code=migrations.RunPython.noop
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 4.1 on 2022-08-23 15:54
import ckeditor_uploader.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("roster", "0009_alter_rollingstock_options_and_more"),
]
operations = [
migrations.AlterField(
model_name="rollingstock",
name="notes",
field=ckeditor_uploader.fields.RichTextUploadingField(blank=True),
),
]

View File

@@ -0,0 +1,34 @@
# Generated by Django 4.1 on 2022-08-24 15:30
import markdown
from django.db import migrations
def md_to_html(apps, schema_editor):
fields = {
"RollingStock": ["notes"],
}
for m in fields.items():
model = apps.get_model("roster", m[0])
for row in model.objects.all():
for field in m[1]:
html = markdown.markdown(getattr(row, field))
row.__dict__[field] = html
row.save(update_fields=m[1])
class Migration(migrations.Migration):
dependencies = [
("roster", "0010_alter_rollingstock_notes"),
]
operations = [
migrations.RunPython(
md_to_html,
reverse_code=migrations.RunPython.noop
),
]

View File

@@ -0,0 +1,45 @@
# Generated by Django 4.1 on 2022-08-27 12:43
import ckeditor_uploader.fields
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("roster", "0011_md_to_html"),
]
operations = [
migrations.CreateModel(
name="RollingStockJournal",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("date", models.DateField()),
("log", ckeditor_uploader.fields.RichTextUploadingField()),
("private", models.BooleanField(default=False)),
("creation_time", models.DateTimeField(auto_now_add=True)),
("updated_time", models.DateTimeField(auto_now=True)),
(
"rolling_stock",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="journal",
to="roster.rollingstock",
),
),
],
options={
"ordering": ["date", "rolling_stock"],
},
),
]

View File

@@ -1,10 +1,12 @@
import os import os
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.dispatch import receiver
from django.utils.safestring import mark_safe
# from django.core.files.storage import FileSystemStorage from ckeditor_uploader.fields import RichTextUploadingField
# from django.dispatch import receiver
from ram.utils import get_image_preview from ram.utils import get_image_preview
from metadata.models import ( from metadata.models import (
@@ -25,12 +27,8 @@ from metadata.models import (
class RollingClass(models.Model): class RollingClass(models.Model):
identifier = models.CharField(max_length=128, unique=False) identifier = models.CharField(max_length=128, unique=False)
type = models.ForeignKey( type = models.ForeignKey(RollingStockType, on_delete=models.CASCADE)
RollingStockType, on_delete=models.CASCADE, null=True, blank=True company = models.ForeignKey(Company, on_delete=models.CASCADE)
)
company = models.ForeignKey(
Company, on_delete=models.CASCADE, null=True, blank=True
)
description = models.CharField(max_length=256, blank=True) description = models.CharField(max_length=256, blank=True)
manufacturer = models.ForeignKey( manufacturer = models.ForeignKey(
Manufacturer, Manufacturer,
@@ -55,6 +53,7 @@ class RollingClassProperty(models.Model):
on_delete=models.CASCADE, on_delete=models.CASCADE,
null=False, null=False,
blank=False, blank=False,
related_name="property",
verbose_name="Class", verbose_name="Class",
) )
property = models.ForeignKey(Property, on_delete=models.CASCADE) property = models.ForeignKey(Property, on_delete=models.CASCADE)
@@ -74,9 +73,11 @@ class RollingStock(models.Model):
on_delete=models.CASCADE, on_delete=models.CASCADE,
null=False, null=False,
blank=False, blank=False,
related_name="rolling_class",
verbose_name="Class", verbose_name="Class",
) )
road_number = models.CharField(max_length=128, unique=False) road_number = models.CharField(max_length=128, unique=False)
road_number_int = models.PositiveSmallIntegerField(default=0, unique=False)
manufacturer = models.ForeignKey( manufacturer = models.ForeignKey(
Manufacturer, Manufacturer,
on_delete=models.CASCADE, on_delete=models.CASCADE,
@@ -93,7 +94,7 @@ class RollingStock(models.Model):
era = models.CharField(max_length=32, blank=True) era = models.CharField(max_length=32, blank=True)
production_year = models.SmallIntegerField(null=True, blank=True) production_year = models.SmallIntegerField(null=True, blank=True)
purchase_date = models.DateField(null=True, blank=True) purchase_date = models.DateField(null=True, blank=True)
notes = models.TextField(blank=True) notes = RichTextUploadingField(blank=True)
tags = models.ManyToManyField( tags = models.ManyToManyField(
Tag, related_name="rolling_stock", blank=True Tag, related_name="rolling_stock", blank=True
) )
@@ -101,7 +102,7 @@ class RollingStock(models.Model):
updated_time = models.DateTimeField(auto_now=True) updated_time = models.DateTimeField(auto_now=True)
class Meta: class Meta:
ordering = ["rolling_class", "road_number"] ordering = ["rolling_class", "road_number_int"]
verbose_name_plural = "Rolling stock" verbose_name_plural = "Rolling stock"
def __str__(self): def __str__(self):
@@ -117,6 +118,16 @@ class RollingStock(models.Model):
return str(self.rolling_class.company) return str(self.rolling_class.company)
@receiver(models.signals.pre_save, sender=RollingStock)
def pre_save_running_number(sender, instance, *args, **kwargs):
try:
instance.road_number_int = int(
re.findall(r"\d+", instance.road_number)[0]
)
except IndexError:
pass
class RollingStockDocument(models.Model): class RollingStockDocument(models.Model):
rolling_stock = models.ForeignKey( rolling_stock = models.ForeignKey(
RollingStock, on_delete=models.CASCADE, related_name="document" RollingStock, on_delete=models.CASCADE, related_name="document"
@@ -131,7 +142,12 @@ class RollingStockDocument(models.Model):
return "{0}".format(os.path.basename(self.file.name)) return "{0}".format(os.path.basename(self.file.name))
def filename(self): def filename(self):
return os.path.basename(self.file.name) return self.__str__()
def download(self):
return mark_safe(
'<a href="{0}" target="_blank">Link</a>'.format(self.file.url)
)
class RollingStockImage(models.Model): class RollingStockImage(models.Model):
@@ -175,6 +191,27 @@ class RollingStockProperty(models.Model):
verbose_name_plural = "Properties" verbose_name_plural = "Properties"
class RollingStockJournal(models.Model):
rolling_stock = models.ForeignKey(
RollingStock,
on_delete=models.CASCADE,
related_name="journal",
null=False,
blank=False,
)
date = models.DateField()
log = RichTextUploadingField()
private = models.BooleanField(default=False)
creation_time = models.DateTimeField(auto_now_add=True)
updated_time = models.DateTimeField(auto_now=True)
def __str__(self):
return "{0} - {1}".format(self.rolling_stock, self.date)
class Meta:
ordering = ["date", "rolling_stock"]
# @receiver(models.signals.post_delete, sender=Cab) # @receiver(models.signals.post_delete, sender=Cab)
# def post_save_image(sender, instance, *args, **kwargs): # def post_save_image(sender, instance, *args, **kwargs):
# try: # try:

View File

@@ -1,9 +1,9 @@
from django.urls import path from django.urls import path
from roster.views import RosterList, RosterGet, RosterAddress, RosterIdentifier from roster.views import RosterList, RosterGet, RosterAddress, RosterClass
urlpatterns = [ urlpatterns = [
path("list", RosterList.as_view()), path("list", RosterList.as_view()),
path("get/<str:uuid>", RosterGet.as_view()), path("get/<str:uuid>", RosterGet.as_view()),
path("address/<int:address>", RosterAddress.as_view()), path("address/<int:address>", RosterAddress.as_view()),
path("identifier/<str:identifier>", RosterIdentifier.as_view()), path("class/<str:class>", RosterClass.as_view()),
] ]

View File

@@ -1,4 +1,5 @@
from rest_framework.generics import ListAPIView, RetrieveAPIView from rest_framework.generics import ListAPIView, RetrieveAPIView
from rest_framework.schemas.openapi import AutoSchema
from roster.models import RollingStock from roster.models import RollingStock
from roster.serializers import RollingStockSerializer from roster.serializers import RollingStockSerializer
@@ -14,14 +15,24 @@ class RosterGet(RetrieveAPIView):
serializer_class = RollingStockSerializer serializer_class = RollingStockSerializer
lookup_field = "uuid" lookup_field = "uuid"
schema = AutoSchema(operation_id_base="retrieveRollingStockByUUID")
class RosterAddress(ListAPIView): class RosterAddress(ListAPIView):
queryset = RollingStock.objects.all()
serializer_class = RollingStockSerializer serializer_class = RollingStockSerializer
lookup_field = "address"
schema = AutoSchema(operation_id_base="retrieveRollingStockByAddress")
def get_queryset(self):
address = self.kwargs["address"]
return RollingStock.objects.filter(address=address)
class RosterIdentifier(RetrieveAPIView): class RosterClass(ListAPIView):
queryset = RollingStock.objects.all()
serializer_class = RollingStockSerializer serializer_class = RollingStockSerializer
lookup_field = "identifier"
schema = AutoSchema(operation_id_base="retrieveRollingStockByClass")
def get_queryset(self):
_class = self.kwargs["class"]
return RollingStock.objects.filter(rolling_class__identifier=_class)

View File

@@ -7,5 +7,6 @@ django-solo
django-countries django-countries
django-health-check django-health-check
django-admin-sortable2 django-admin-sortable2
django-ckeditor
# psycopg2-binary # psycopg2-binary
pySerial pySerial

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
sample_data/images/scrr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

376
sample_data/metadata.json Normal file
View File

@@ -0,0 +1,376 @@
[
{
"model": "metadata.property",
"pk": 1,
"fields": {
"name": "Wheel arrangement"
}
},
{
"model": "metadata.property",
"pk": 2,
"fields": {
"name": "Trucks"
}
},
{
"model": "metadata.manufacturer",
"pk": 1,
"fields": {
"name": "Roco",
"category": "model",
"website": "https://www.roco.cc/",
"logo": "images/roco_logo.png"
}
},
{
"model": "metadata.manufacturer",
"pk": 2,
"fields": {
"name": "Liliput",
"category": "model",
"website": "https://liliput.de/",
"logo": "images/liliput_logo.png"
}
},
{
"model": "metadata.manufacturer",
"pk": 3,
"fields": {
"name": "Stängl",
"category": "model",
"website": "https://www.halling.at/",
"logo": "images/staengl_logo.png"
}
},
{
"model": "metadata.manufacturer",
"pk": 4,
"fields": {
"name": "Ferro-Train",
"category": "model",
"website": "https://www.halling.at/",
"logo": "images/ferro_logo.png"
}
},
{
"model": "metadata.manufacturer",
"pk": 5,
"fields": {
"name": "Bachmann",
"category": "model",
"website": "https://bachmanntrains.com/",
"logo": "images/bachmann_logo.png"
}
},
{
"model": "metadata.manufacturer",
"pk": 6,
"fields": {
"name": "ESU",
"category": "model",
"website": "https://www.esu.eu/",
"logo": ""
}
},
{
"model": "metadata.manufacturer",
"pk": 7,
"fields": {
"name": "Soundtraxx",
"category": "model",
"website": "https://soundtraxx.com/",
"logo": ""
}
},
{
"model": "metadata.manufacturer",
"pk": 8,
"fields": {
"name": "Lima Locomotive Works",
"category": "real",
"website": "",
"logo": ""
}
},
{
"model": "metadata.manufacturer",
"pk": 9,
"fields": {
"name": "Baldwin Locomotive Works",
"category": "real",
"website": "",
"logo": ""
}
},
{
"model": "metadata.manufacturer",
"pk": 10,
"fields": {
"name": "Rogers Locomotive and Machine Works",
"category": "real",
"website": "",
"logo": ""
}
},
{
"model": "metadata.manufacturer",
"pk": 11,
"fields": {
"name": "Porter Locomotive Works",
"category": "real",
"website": "",
"logo": ""
}
},
{
"model": "metadata.manufacturer",
"pk": 12,
"fields": {
"name": "Krauss & Comp. / Österreichische Siemens-Schuckert Werke",
"category": "real",
"website": "",
"logo": ""
}
},
{
"model": "metadata.manufacturer",
"pk": 13,
"fields": {
"name": "Simmering-Graz-Pauker",
"category": "real",
"website": "",
"logo": ""
}
},
{
"model": "metadata.manufacturer",
"pk": 14,
"fields": {
"name": "Davenport Locomotive Works",
"category": "real",
"website": "",
"logo": ""
}
},
{
"model": "metadata.company",
"pk": 1,
"fields": {
"name": "ÖBB",
"extended_name": "Österreichische Bundesbahnen",
"country": "AT",
"freelance": false,
"logo": "images/oebb_logo.png"
}
},
{
"model": "metadata.company",
"pk": 2,
"fields": {
"name": "SCRR",
"extended_name": "Sand Creek Railroad",
"country": "US",
"freelance": true,
"logo": "images/scrr.png"
}
},
{
"model": "metadata.company",
"pk": 3,
"fields": {
"name": "RhB",
"extended_name": "Rhätische Bahn",
"country": "CH",
"freelance": false,
"logo": ""
}
},
{
"model": "metadata.company",
"pk": 4,
"fields": {
"name": "FO",
"extended_name": "Furka-Oberalp",
"country": "CH",
"freelance": false,
"logo": ""
}
},
{
"model": "metadata.decoder",
"pk": 1,
"fields": {
"name": "Basic",
"manufacturer": 5,
"version": "",
"interface": 2,
"sound": false,
"image": ""
}
},
{
"model": "metadata.decoder",
"pk": 2,
"fields": {
"name": "Tsunami",
"manufacturer": 7,
"version": "1",
"interface": 2,
"sound": true,
"image": ""
}
},
{
"model": "metadata.decoder",
"pk": 3,
"fields": {
"name": "LokPilot Micro",
"manufacturer": 6,
"version": "v4",
"interface": 1,
"sound": false,
"image": ""
}
},
{
"model": "metadata.scale",
"pk": 1,
"fields": {
"scale": "H0e",
"ratio": "1:87",
"gauge": "9 mm"
}
},
{
"model": "metadata.scale",
"pk": 2,
"fields": {
"scale": "H0",
"ratio": "1:87",
"gauge": "16.5 mm"
}
},
{
"model": "metadata.scale",
"pk": 3,
"fields": {
"scale": "0n30",
"ratio": "1:48",
"gauge": "16.5 mm"
}
},
{
"model": "metadata.scale",
"pk": 4,
"fields": {
"scale": "H0m",
"ratio": "1:87",
"gauge": "11 mm"
}
},
{
"model": "metadata.tag",
"pk": 1,
"fields": {
"name": "Mariazellerbahn",
"slug": "mariazellerbahn"
}
},
{
"model": "metadata.tag",
"pk": 2,
"fields": {
"name": "Colorado",
"slug": "colorado"
}
},
{
"model": "metadata.tag",
"pk": 3,
"fields": {
"name": "Narrow gauge",
"slug": "narrow-gauge"
}
},
{
"model": "metadata.rollingstocktype",
"pk": 1,
"fields": {
"type": "Electric",
"category": "engine"
}
},
{
"model": "metadata.rollingstocktype",
"pk": 2,
"fields": {
"type": "Diesel",
"category": "engine"
}
},
{
"model": "metadata.rollingstocktype",
"pk": 3,
"fields": {
"type": "Steam",
"category": "engine"
}
},
{
"model": "metadata.rollingstocktype",
"pk": 4,
"fields": {
"type": "EMU",
"category": "railcar"
}
},
{
"model": "metadata.rollingstocktype",
"pk": 5,
"fields": {
"type": "DMU",
"category": "railcar"
}
},
{
"model": "metadata.rollingstocktype",
"pk": 6,
"fields": {
"type": "Electric",
"category": "railcar"
}
},
{
"model": "metadata.rollingstocktype",
"pk": 7,
"fields": {
"type": "Diesel",
"category": "railcar"
}
},
{
"model": "metadata.rollingstocktype",
"pk": 8,
"fields": {
"type": "Passenger",
"category": "car"
}
},
{
"model": "metadata.rollingstocktype",
"pk": 9,
"fields": {
"type": "Freight",
"category": "car"
}
},
{
"model": "metadata.rollingstocktype",
"pk": 10,
"fields": {
"type": "Caboose",
"category": "equipment"
}
}
]