38 Commits

Author SHA1 Message Date
32b5522a1e Change how images and consists are sorted (#14) 2023-01-02 16:08:25 +01:00
89b666dab2 Update README.md 2022-12-30 09:28:08 +01:00
ffad964373 Add possibility to inject js in head (analytics) 2022-12-28 23:54:49 +01:00
538dc0bd80 Add page title in html 2022-12-28 23:36:46 +01:00
8bd2635c28 Change image sort, thumbnails first 2022-12-28 22:14:10 +01:00
feda1f6cb4 [auto update] sync submodules 2022-11-28 18:22:05 +01:00
2c851b2822 Add migrations 2022-11-27 01:11:43 +01:00
e5ba2cfaec Merge pull request #13 from daniviga/dedup
Reuse existing file if content is the same
2022-11-27 01:09:50 +01:00
091f426242 Bump version 2022-11-27 01:09:34 +01:00
f603fd3e2d Reuse existing file if content is the same 2022-11-27 01:07:38 +01:00
a3b2112e03 Fix search query 2022-11-24 16:38:07 +01:00
055b0bab59 Enable "Save as" in roster and consist 2022-11-01 12:30:38 +01:00
3aea2ae340 Merge pull request #12 from daniviga/move-dcc-interface
Move decoder interface def into rolling stock
2022-11-01 00:07:18 +01:00
242fe6814d Move decoder interface def into rolling stock 2022-11-01 00:06:30 +01:00
90ffadb2ab Hotfix templates/companies.html pagination 2022-10-22 22:55:31 +02:00
21bf09687a Improve a CSS for journal 2022-08-28 11:32:19 +02:00
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
56 changed files with 1030 additions and 116 deletions

View File

@@ -2,8 +2,7 @@
[![Django CI](https://github.com/daniviga/django-rma/actions/workflows/django.yml/badge.svg)](https://github.com/daniviga/django-rma/actions/workflows/django.yml) [![Django CI](https://github.com/daniviga/django-rma/actions/workflows/django.yml/badge.svg)](https://github.com/daniviga/django-rma/actions/workflows/django.yml)
![image](https://user-images.githubusercontent.com/1818657/175789825-9a03f0ff-a95e-42a2-9611-e14d2817e22f.png) ![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:
@@ -22,10 +21,12 @@ it has been developed with a commitment of few minutes a day;
it lacks any kind of documentation, code review, architectural review, it lacks any kind of documentation, code review, architectural review,
security assesment, pentest, ISO certification, 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 your 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:
@@ -138,14 +139,18 @@ To be continued ...
## Screenshots ## Screenshots
### Frontend ### Frontend
![Screenshot 2022-07-23 at 22-41-44 Railroad Assets Manager](https://user-images.githubusercontent.com/1818657/180622406-760774a9-f028-44fc-b332-fa74e43307df.png)
---
![Screenshot 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)
![image](https://user-images.githubusercontent.com/1818657/175789897-9ec4a9bb-9c65-48ef-9b57-ae94e094e6a7.png)
--- ---
![image](https://user-images.githubusercontent.com/1818657/175789901-ef50acd7-8c05-4788-92a2-1bb1280d598c.png)
---
![image](https://user-images.githubusercontent.com/1818657/175790004-18926d23-28f9-45bb-b279-6c26575ae3a5.png)
---
![image](https://user-images.githubusercontent.com/1818657/175790008-62eea2cc-1c41-42df-9026-4cf6e8ef712c.png)
### Backoffice ### Backoffice
@@ -158,8 +163,7 @@ To be continued ...
### Rest API ### Rest API
![image](https://user-images.githubusercontent.com/1818657/175790064-23ec038e-e8bf-4c39-964c-3118e4295b59.png) ![image](https://user-images.githubusercontent.com/1818657/180622471-ade06c84-c73b-41d5-a2a7-02a95b2ffc02.png)

View File

@@ -21,6 +21,7 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
list_display = ("identifier", "company", "era") list_display = ("identifier", "company", "era")
list_filter = list_display list_filter = list_display
search_fields = list_display search_fields = list_display
save_as = True
fieldsets = ( fieldsets = (
( (

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

@@ -0,0 +1,24 @@
# Generated by Django 4.1.2 on 2022-11-27 00:10
from django.db import migrations, models
import ram.utils
class Migration(migrations.Migration):
dependencies = [
("consist", "0006_md_to_html"),
]
operations = [
migrations.AlterField(
model_name="consist",
name="image",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/",
),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 4.1.3 on 2023-01-02 15:03
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("consist", "0007_alter_consist_image"),
]
operations = [
migrations.AlterModelOptions(
name="consist",
options={"ordering": ["company", "-creation_time"]},
),
]

View File

@@ -2,6 +2,9 @@ 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 ram.utils import DeduplicatedStorage
from metadata.models import Company, Tag from metadata.models import Company, Tag
from roster.models import RollingStock from roster.models import RollingStock
@@ -15,8 +18,10 @@ class Consist(models.Model):
) )
company = models.ForeignKey(Company, on_delete=models.CASCADE) company = models.ForeignKey(Company, on_delete=models.CASCADE)
era = models.CharField(max_length=32, blank=True) era = models.CharField(max_length=32, blank=True)
image = models.ImageField(upload_to="images/", null=True, blank=True) image = models.ImageField(
notes = models.TextField(blank=True) upload_to="images/", storage=DeduplicatedStorage, 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)
@@ -27,7 +32,7 @@ class Consist(models.Model):
return reverse("consist", kwargs={"uuid": self.uuid}) return reverse("consist", kwargs={"uuid": self.uuid})
class Meta: class Meta:
ordering = ["creation_time"] ordering = ["company", "-creation_time"]
class ConsistItem(models.Model): class ConsistItem(models.Model):

View File

@@ -20,8 +20,8 @@ class PropertyAdmin(admin.ModelAdmin):
@admin.register(Decoder) @admin.register(Decoder)
class DecoderAdmin(admin.ModelAdmin): class DecoderAdmin(admin.ModelAdmin):
readonly_fields = ("image_thumbnail",) readonly_fields = ("image_thumbnail",)
list_display = ("__str__", "interface") list_display = ("__str__", "sound")
list_filter = ("manufacturer", "interface") list_filter = ("manufacturer", "sound")
search_fields = ("name", "manufacturer__name") search_fields = ("name", "manufacturer__name")

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.1.2 on 2022-10-31 23:01
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("metadata", "0007_rename_track_scale_tracks"),
("roster", "0013_rollingstock_decoder_interface"),
]
operations = [
migrations.RemoveField(
model_name="decoder",
name="interface",
),
]

View File

@@ -0,0 +1,44 @@
# Generated by Django 4.1.2 on 2022-11-27 00:10
from django.db import migrations, models
import ram.utils
class Migration(migrations.Migration):
dependencies = [
("metadata", "0008_remove_decoder_interface"),
]
operations = [
migrations.AlterField(
model_name="company",
name="logo",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/",
),
),
migrations.AlterField(
model_name="decoder",
name="image",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/",
),
),
migrations.AlterField(
model_name="manufacturer",
name="logo",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/",
),
),
]

View File

@@ -3,7 +3,7 @@ from django.conf import settings
from django.dispatch.dispatcher import receiver from django.dispatch.dispatcher import receiver
from django_countries.fields import CountryField from django_countries.fields import CountryField
from ram.utils import get_image_preview, slugify from ram.utils import DeduplicatedStorage, get_image_preview, slugify
class Property(models.Model): class Property(models.Model):
@@ -24,7 +24,9 @@ class Manufacturer(models.Model):
max_length=64, choices=settings.MANUFACTURER_TYPES max_length=64, choices=settings.MANUFACTURER_TYPES
) )
website = models.URLField(blank=True) website = models.URLField(blank=True)
logo = models.ImageField(upload_to="images/", null=True, blank=True) logo = models.ImageField(
upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True
)
class Meta: class Meta:
ordering = ["category", "name"] ordering = ["category", "name"]
@@ -43,7 +45,9 @@ class Company(models.Model):
extended_name = models.CharField(max_length=128, blank=True) extended_name = models.CharField(max_length=128, blank=True)
country = CountryField() country = CountryField()
freelance = models.BooleanField(default=False) freelance = models.BooleanField(default=False)
logo = models.ImageField(upload_to="images/", null=True, blank=True) logo = models.ImageField(
upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True
)
class Meta: class Meta:
verbose_name_plural = "Companies" verbose_name_plural = "Companies"
@@ -66,11 +70,10 @@ class Decoder(models.Model):
limit_choices_to={"category": "model"}, limit_choices_to={"category": "model"},
) )
version = models.CharField(max_length=64, blank=True) version = models.CharField(max_length=64, blank=True)
interface = models.PositiveSmallIntegerField(
choices=settings.DECODER_INTERFACES, null=True, blank=True
)
sound = models.BooleanField(default=False) sound = models.BooleanField(default=False)
image = models.ImageField(upload_to="images/", null=True, blank=True) image = models.ImageField(
upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True
)
def __str__(self): def __str__(self):
return "{0} - {1}".format(self.manufacturer, self.name) return "{0} - {1}".format(self.manufacturer, self.name)

View File

@@ -1,6 +1,65 @@
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.register(SiteConfiguration)
class SiteConfigurationAdmin(SingletonModelAdmin):
fieldsets = (
(
None,
{
"fields": (
"site_name",
"site_author",
"about",
"items_per_page",
"items_ordering",
"footer",
"footer_extended",
)
},
),
(
"Advanced",
{
"classes": ("collapse",),
"fields": (
"show_version",
"extra_head",
),
},
),
)
@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,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

@@ -0,0 +1,18 @@
# Generated by Django 4.1.3 on 2022-12-28 22:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("portal", "0013_remove_flatpage_draft_flatpage_published"),
]
operations = [
migrations.AddField(
model_name="siteconfiguration",
name="extra_head",
field=models.TextField(blank=True),
),
]

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,7 +17,7 @@ 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)],
@@ -25,9 +32,10 @@ class SiteConfiguration(SingletonModel):
], ],
default="type", default="type",
) )
footer = models.TextField(blank=True) footer = RichTextField(blank=True)
footer_extended = models.TextField(blank=True) footer_extended = RichTextField(blank=True)
show_version = models.BooleanField(default=True) show_version = models.BooleanField(default=True)
extra_head = models.TextField(blank=True)
class Meta: class Meta:
verbose_name = "Site Configuration" verbose_name = "Site Configuration"
@@ -40,3 +48,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

@@ -23,6 +23,15 @@ a.badge, a.badge:hover {
padding: .5rem; padding: .5rem;
} }
#nav-journal ul, #nav-journal ol {
margin: 0;
padding-left: 1rem;
}
#nav-journal p {
margin: 0;
}
#footer > p { #footer > p {
display: inline; display: inline;
} }

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>
@@ -12,7 +12,7 @@
<meta name="description" content="{{ site_conf.about}}"> <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>{% block title %}{{ title }}{% endblock %} - {{ site_conf.site_name }}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/css/bootstrap-nightshade.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"> <link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
@@ -34,6 +34,10 @@
html.dark .d-light-inline { display: none !important; } html.dark .d-light-inline { display: none !important; }
html.dark .d-dark-inline { display: inline !important; } html.dark .d-dark-inline { display: inline !important; }
</style> </style>
{% block extra_head %}
{{ site_conf.extra_head | safe }}
{% endblock %}
</head> </head>
<body> <body>
<header> <header>
@@ -71,8 +75,9 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'scales' %}">Scales</a> <a class="nav-link" href="{% url 'scales' %}">Scales</a>
</li> </li>
{% show_menu %}
</ul> </ul>
{% include 'includes/search.html' %} {% include 'includes/search.html' %}
</div> </div>
</div> </div>
</nav> </nav>
@@ -80,11 +85,13 @@
<section class="py-4 text-center container"> <section class="py-4 text-center container">
<div class="row"> <div class="row">
<div class="mx-auto"> <div class="mx-auto">
{% block header %}{% endblock %} <h1 class="fw-light">{{ title }}</h1>
{% block header %}
{% endblock %}
</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">
@@ -93,7 +100,7 @@
<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="{{r.get_absolute_url}}"><img src="{{ i.image.url }}" alt="Card image cap"></a>{% endif %} {% if forloop.first %}<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" style="position: relative;"> <p class="card-text" style="position: relative;">

View File

@@ -1,9 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load markdown %}
{% block header %}
<h1 class="fw-light">Companies</h1>
{% endblock %}
{% block cards %} {% block cards %}
{% for c in company %} {% for c in company %}
<div class="col"> <div class="col">
@@ -57,10 +53,10 @@
{% block pagination %} {% block pagination %}
{% if company.has_other_pages %} {% if company.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 company.has_previous %} {% if company.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'company_pagination' page=company.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a> <a class="page-link" href="{% url 'companies_pagination' page=company.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -76,13 +72,13 @@
{% if i == company.paginator.ELLIPSIS %} {% if i == company.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li> <li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %} {% else %}
<li class="page-item"><a class="page-link" href="{% url 'company_pagination' page=i %}#rolling-stock">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'companies_pagination' page=i %}#rolling-stock">{{ i }}</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if company.has_next %} {% if company.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'company_pagination' page=company.next_page_number %}#rolling-stock" tabindex="-1">Next</a> <a class="page-link" href="{% url 'companies_pagination' page=company.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,8 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load markdown %}
{% block header %} {% block header %}
<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 %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary"> {% for t in consist.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
@@ -17,7 +15,7 @@
<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="{{r.rolling_stock.get_absolute_url}}"><img src="{{ i.image.url }}" alt="Card image cap"></a>{% endif %} {% if forloop.first %}<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" style="position: relative;"> <p class="card-text" style="position: relative;">
@@ -103,7 +101,7 @@
{% 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="{% url 'consist_pagination' uuid=consist.uuid page=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>

View File

@@ -1,14 +1,20 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load markdown %}
{% block header %}
<h1 class="fw-light">Consists</h1>
{% endblock %}
{% block cards %} {% block cards %}
{% for c in consist %} {% for c in consist %}
<div class="col"> <div class="col">
<div class="card shadow-sm"> <div class="card shadow-sm">
{% if c.image %}<a href="{{ c.get_absolute_url }}"><img src="{{ c.image.url }}" alt="Card image cap"></a>{% endif %} <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 forloop.first %}<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" style="position: relative;"> <p class="card-text" style="position: relative;">
<strong>{{ c }}</strong> <strong>{{ c }}</strong>
@@ -60,7 +66,7 @@
{% block pagination %} {% block pagination %}
{% if consist.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 consist.has_previous %} {% if consist.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'consists_pagination' page=consist.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>

View File

@@ -0,0 +1,17 @@
{% extends 'base.html' %}
{% block header %}
<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,15 +1,13 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load markdown %}
{% block header %} {% block header %}
{% if site_conf.about %}<h1 class="fw-light">About</h1>{% endif %} <p class="lead text-muted">{{ site_conf.about | safe }}</p>
<p class="lead text-muted">{{ site_conf.about | markdown | 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="{% url 'index_pagination' page=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>

View File

@@ -1,15 +1,13 @@
{% 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>

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,8 +1,6 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load markdown %}
{% block header %} {% block header %}
<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 %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary"> {% for t in rolling_stock.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
@@ -48,6 +46,7 @@
{% 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.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">
@@ -102,7 +101,7 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
{% if rolling_stock.decoder %} {% if rolling_stock.decoder or rolling_stock.decoder_interface %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@@ -111,13 +110,19 @@
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<th width="35%" scope="row">Decoder</th> <th width="35%" scope="row">Interface</th>
<td>{{ rolling_stock.get_decoder_interface_display }}</td>
</tr>
{% if rolling_stock.decoder %}
<tr>
<th scope="row">Decoder</th>
<td>{{ rolling_stock.decoder }}</td> <td>{{ rolling_stock.decoder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Address</th> <th scope="row">Address</th>
<td>{{ rolling_stock.address }}</td> <td>{{ rolling_stock.address }}</td>
</tr> </tr>
{% endif %}
</tbody> </tbody>
</table> </table>
{% endif %} {% endif %}
@@ -230,6 +235,10 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr>
<th scope="row">Interface</th>
<td>{{ rolling_stock.get_decoder_interface_display }}</td>
</tr>
<tr> <tr>
<th width="35%" scope="row">Address</th> <th width="35%" scope="row">Address</th>
<td>{{ rolling_stock.address }}</td> <td>{{ rolling_stock.address }}</td>
@@ -246,10 +255,6 @@
<th scope="row">Version</th> <th scope="row">Version</th>
<td>{{ rolling_stock.decoder.version }}</td> <td>{{ rolling_stock.decoder.version }}</td>
</tr> </tr>
<tr>
<th scope="row">Interface</th>
<td>{{ rolling_stock.decoder.get_interface_display }}</td>
</tr>
<tr> <tr>
<th scope="row">Sound</th> <th scope="row">Sound</th>
<td>{{ rolling_stock.decoder.sound | yesno:"Yes,No" }}</td> <td>{{ rolling_stock.decoder.sound | yesno:"Yes,No" }}</td>
@@ -258,7 +263,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>
<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">
@@ -278,6 +283,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

@@ -1,9 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load markdown %}
{% block header %}
<h1 class="fw-light">Scales</h1>
{% endblock %}
{% block cards %} {% block cards %}
{% for s in scale %} {% for s in scale %}
<div class="col"> <div class="col">
@@ -47,7 +43,7 @@
{% block pagination %} {% block pagination %}
{% if scale.has_other_pages %} {% if scale.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 scale.has_previous %} {% if scale.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'scale_pagination' page=scale.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a> <a class="page-link" href="{% url 'scale_pagination' page=scale.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a>

View File

@@ -1,13 +1,12 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block header %} {% block header %}
<h1 class="fw-light">{{ filter | default_if_none:"Search" | title }}: {{ search }}</h1>
<p class="lead text-muted">Results found: {{ matches }}</p> <p class="lead text-muted">Results found: {{ matches }}</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="{% url 'filtered_pagination' _filter=filter search=search page=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>

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,6 +3,7 @@ from django.urls import path
from portal.views import ( from portal.views import (
GetHome, GetHome,
GetHomeFiltered, GetHomeFiltered,
GetFlatpage,
GetRollingStock, GetRollingStock,
GetConsist, GetConsist,
Consists, Consists,
@@ -13,6 +14,11 @@ from portal.views import (
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"]),
@@ -32,14 +38,10 @@ urlpatterns = [
path( path(
"companies/<int:page>", "companies/<int:page>",
Companies.as_view(), Companies.as_view(),
name="companies_pagination" name="companies_pagination",
), ),
path("scales", Scales.as_view(), name="scales"), path("scales", Scales.as_view(), name="scales"),
path( path("scales/<int:page>", Scales.as_view(), name="scales_pagination"),
"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,9 +6,10 @@ 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 from metadata.models import Company, Scale
@@ -45,7 +46,11 @@ class GetHome(View):
return render( return render(
request, request,
"home.html", "home.html",
{"rolling_stock": rolling_stock, "page_range": page_range}, {
"title": "Home",
"rolling_stock": rolling_stock,
"page_range": page_range,
},
) )
@@ -61,6 +66,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)
@@ -81,8 +87,10 @@ class GetHomeFiltered(View):
query = Q(tags__slug__iexact=search) query = Q(tags__slug__iexact=search)
else: else:
raise Http404 raise Http404
rolling_stock = RollingStock.objects.filter(query).order_by( rolling_stock = (
*order_by_fields() RollingStock.objects.filter(query)
.distinct()
.order_by(*order_by_fields())
) )
matches = len(rolling_stock) matches = len(rolling_stock)
@@ -103,6 +111,7 @@ class GetHomeFiltered(View):
request, request,
"search.html", "search.html",
{ {
"title": "{0}: {1}".format(_filter.capitalize(), search),
"search": search, "search": search,
"filter": _filter, "filter": _filter,
"matches": matches, "matches": matches,
@@ -123,6 +132,7 @@ class GetHomeFiltered(View):
request, request,
"search.html", "search.html",
{ {
"title": "{0}: {1}".format(_filter.capitalize(), search),
"search": search, "search": search,
"filter": _filter, "filter": _filter,
"matches": matches, "matches": matches,
@@ -152,13 +162,21 @@ class GetRollingStock(View):
else rolling_stock.property.filter(property__private=False) 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",
{ {
"title": rolling_stock,
"rolling_stock": rolling_stock, "rolling_stock": rolling_stock,
"class_properties": class_properties, "class_properties": class_properties,
"rolling_stock_properties": rolling_stock_properties, "rolling_stock_properties": rolling_stock_properties,
"rolling_stock_journal": rolling_stock_journal,
}, },
) )
@@ -177,7 +195,11 @@ class Consists(View):
return render( return render(
request, request,
"consists.html", "consists.html",
{"consist": consist, "page_range": page_range}, {
"title": "Consists",
"consist": consist,
"page_range": page_range,
},
) )
@@ -200,6 +222,7 @@ class GetConsist(View):
request, request,
"consist.html", "consist.html",
{ {
"title": consist,
"consist": consist, "consist": consist,
"rolling_stock": rolling_stock, "rolling_stock": rolling_stock,
"page_range": page_range, "page_range": page_range,
@@ -221,7 +244,11 @@ class Companies(View):
return render( return render(
request, request,
"companies.html", "companies.html",
{"company": company, "page_range": page_range}, {
"title": "Companies",
"company": company,
"page_range": page_range,
},
) )
@@ -239,5 +266,21 @@ class Scales(View):
return render( return render(
request, request,
"scales.html", "scales.html",
{"scale": scale, "page_range": page_range}, {"title": "Scales", "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",
{"title": flatpage.name, "flatpage": flatpage},
) )

View File

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

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",

View File

@@ -21,6 +21,7 @@ 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),

View File

@@ -1,8 +1,29 @@
import os import os
import hashlib
import subprocess import subprocess
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.text import slugify as django_slugify from django.utils.text import slugify as django_slugify
from django.core.files.storage import FileSystemStorage
class DeduplicatedStorage(FileSystemStorage):
"""
A derived FileSystemStorage class that compares already existing files
(with the same name) with new uploaded ones and stores new file only if
sha256 hash on is content is different
"""
def save(self, name, content, max_length=None):
if super().exists(name):
new = hashlib.sha256(content.file.getbuffer()).hexdigest()
with open(super().path(name), "rb") as file:
file_binary = file.read()
old = hashlib.sha256(file_binary).hexdigest()
if old == new:
return name
return super().save(name, content, max_length)
def git_suffix(fname): def git_suffix(fname):

View File

@@ -1,4 +1,6 @@
from django.contrib import admin from django.contrib import admin
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
from roster.models import ( from roster.models import (
RollingClass, RollingClass,
RollingClassProperty, RollingClassProperty,
@@ -6,6 +8,7 @@ from roster.models import (
RollingStockImage, RollingStockImage,
RollingStockDocument, RollingStockDocument,
RollingStockProperty, RollingStockProperty,
RollingStockJournal,
) )
@@ -31,13 +34,15 @@ 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(SortableInlineAdminMixin, admin.TabularInline):
model = RollingStockImage model = RollingStockImage
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):
@@ -46,12 +51,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(SortableAdminBase, 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 = (
@@ -78,6 +127,7 @@ class RollingStockAdmin(admin.ModelAdmin):
"address", "address",
"sku", "sku",
) )
save_as = True
fieldsets = ( fieldsets = (
( (
@@ -101,6 +151,7 @@ class RollingStockAdmin(admin.ModelAdmin):
"DCC", "DCC",
{ {
"fields": ( "fields": (
"decoder_interface",
"decoder", "decoder",
"address", "address",
) )

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

@@ -0,0 +1,42 @@
# Generated by Django 4.1 on 2022-10-31 22:27
from django.db import migrations, models
def meta_to_roster(apps, schema_editor):
model = apps.get_model("roster", "RollingStock")
for row in model.objects.all():
if row.decoder:
decoder_interface = row.decoder.interface
row.__dict__["decoder_interface"] = decoder_interface
row.save(update_fields=["decoder_interface"])
class Migration(migrations.Migration):
dependencies = [
("roster", "0012_rollingstockjournal"),
]
operations = [
migrations.AddField(
model_name="rollingstock",
name="decoder_interface",
field=models.PositiveSmallIntegerField(
blank=True,
choices=[
(1, "NEM651"),
(2, "NEM652"),
(3, "PluX"),
(4, "21MTC"),
(5, "Next18/Next18S"),
],
null=True,
),
),
migrations.RunPython(
meta_to_roster,
reverse_code=migrations.RunPython.noop
),
]

View File

@@ -0,0 +1,34 @@
# Generated by Django 4.1.2 on 2022-11-27 00:10
from django.db import migrations, models
import ram.utils
class Migration(migrations.Migration):
dependencies = [
("roster", "0013_rollingstock_decoder_interface"),
]
operations = [
migrations.AlterField(
model_name="rollingstockdocument",
name="file",
field=models.FileField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage(),
upload_to="files/",
),
),
migrations.AlterField(
model_name="rollingstockimage",
name="image",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/",
),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 4.1.3 on 2022-12-28 21:07
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("roster", "0014_alter_rollingstockdocument_file_and_more"),
]
operations = [
migrations.AlterModelOptions(
name="rollingstockimage",
options={"ordering": ["-is_thumbnail"]},
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 4.1.3 on 2023-01-02 12:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("roster", "0015_alter_rollingstockimage_options"),
]
operations = [
migrations.AlterModelOptions(
name="rollingstockimage",
options={"ordering": ["order"]},
),
migrations.AddField(
model_name="rollingstockimage",
name="order",
field=models.PositiveIntegerField(default=0),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 4.1.3 on 2023-01-02 15:02
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("roster", "0016_alter_rollingstockimage_options_and_more"),
]
operations = [
migrations.RemoveField(
model_name="rollingstockimage",
name="is_thumbnail",
),
]

View File

@@ -3,11 +3,13 @@ import re
from uuid import uuid4 from uuid import uuid4
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.conf import settings
from django.dispatch import receiver 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 ram.utils import get_image_preview from ram.utils import DeduplicatedStorage, get_image_preview
from metadata.models import ( from metadata.models import (
Property, Property,
Scale, Scale,
@@ -18,11 +20,6 @@ from metadata.models import (
RollingStockType, RollingStockType,
) )
# class OverwriteMixin(FileSystemStorage):
# def get_available_name(self, name, max_length):
# self.delete(name)
# return name
class RollingClass(models.Model): class RollingClass(models.Model):
identifier = models.CharField(max_length=128, unique=False) identifier = models.CharField(max_length=128, unique=False)
@@ -86,6 +83,9 @@ class RollingStock(models.Model):
) )
scale = models.ForeignKey(Scale, on_delete=models.CASCADE) scale = models.ForeignKey(Scale, on_delete=models.CASCADE)
sku = models.CharField(max_length=32, blank=True) sku = models.CharField(max_length=32, blank=True)
decoder_interface = models.PositiveSmallIntegerField(
choices=settings.DECODER_INTERFACES, null=True, blank=True
)
decoder = models.ForeignKey( decoder = models.ForeignKey(
Decoder, on_delete=models.CASCADE, null=True, blank=True Decoder, on_delete=models.CASCADE, null=True, blank=True
) )
@@ -93,7 +93,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
) )
@@ -132,7 +132,12 @@ class RollingStockDocument(models.Model):
RollingStock, on_delete=models.CASCADE, related_name="document" RollingStock, on_delete=models.CASCADE, related_name="document"
) )
description = models.CharField(max_length=128, blank=True) description = models.CharField(max_length=128, blank=True)
file = models.FileField(upload_to="files/", null=True, blank=True) file = models.FileField(
upload_to="files/",
storage=DeduplicatedStorage(),
null=True,
blank=True,
)
class Meta(object): class Meta(object):
unique_together = ("rolling_stock", "file") unique_together = ("rolling_stock", "file")
@@ -141,15 +146,22 @@ 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):
order = models.PositiveIntegerField(default=0, blank=False, null=False)
rolling_stock = models.ForeignKey( rolling_stock = models.ForeignKey(
RollingStock, on_delete=models.CASCADE, related_name="image" RollingStock, on_delete=models.CASCADE, related_name="image"
) )
image = models.ImageField(upload_to="images/", null=True, blank=True) image = models.ImageField(
is_thumbnail = models.BooleanField() upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True
)
def image_thumbnail(self): def image_thumbnail(self):
return get_image_preview(self.image.url) return get_image_preview(self.image.url)
@@ -159,12 +171,8 @@ class RollingStockImage(models.Model):
def __str__(self): def __str__(self):
return "{0}".format(os.path.basename(self.image.name)) return "{0}".format(os.path.basename(self.image.name))
def save(self, **kwargs): class Meta:
if self.is_thumbnail: ordering = ["order"]
RollingStockImage.objects.filter(
rolling_stock=self.rolling_stock
).update(is_thumbnail=False)
super().save(**kwargs)
class RollingStockProperty(models.Model): class RollingStockProperty(models.Model):
@@ -185,6 +193,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

@@ -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