10 Commits

29 changed files with 255 additions and 43 deletions

View File

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

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

@@ -4,6 +4,7 @@ from django.urls import reverse
from ckeditor_uploader.fields import RichTextUploadingField
from ram.utils import DeduplicatedStorage
from metadata.models import Company, Tag
from roster.models import RollingStock
@@ -17,7 +18,9 @@ class Consist(models.Model):
)
company = models.ForeignKey(Company, on_delete=models.CASCADE)
era = models.CharField(max_length=32, blank=True)
image = models.ImageField(upload_to="images/", null=True, blank=True)
image = models.ImageField(
upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True
)
notes = RichTextUploadingField(blank=True)
creation_time = models.DateTimeField(auto_now_add=True)
updated_time = models.DateTimeField(auto_now=True)

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_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):
@@ -24,7 +24,9 @@ class Manufacturer(models.Model):
max_length=64, choices=settings.MANUFACTURER_TYPES
)
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:
ordering = ["category", "name"]
@@ -43,7 +45,9 @@ class Company(models.Model):
extended_name = models.CharField(max_length=128, blank=True)
country = CountryField()
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:
verbose_name_plural = "Companies"
@@ -67,7 +71,9 @@ class Decoder(models.Model):
)
version = models.CharField(max_length=64, blank=True)
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):
return "{0} - {1}".format(self.manufacturer, self.name)

View File

@@ -3,7 +3,34 @@ from solo.admin import SingletonModelAdmin
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)

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

@@ -35,6 +35,7 @@ class SiteConfiguration(SingletonModel):
footer = RichTextField(blank=True)
footer_extended = RichTextField(blank=True)
show_version = models.BooleanField(default=True)
extra_head = models.TextField(blank=True)
class Meta:
verbose_name = "Site Configuration"

View File

@@ -12,7 +12,7 @@
<meta name="description" content="{{ site_conf.about}}">
<meta name="author" content="{{ site_conf.site_author }}">
<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="{% static "css/main.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-dark-inline { display: inline !important; }
</style>
{% block extra_head %}
{{ site_conf.extra_head | safe }}
{% endblock %}
</head>
<body>
<header>
@@ -81,7 +85,9 @@
<section class="py-4 text-center container">
<div class="row">
<div class="mx-auto">
{% block header %}{% endblock %}
<h1 class="fw-light">{{ title }}</h1>
{% block header %}
{% endblock %}
</div>
</div>
</section>

View File

@@ -1,8 +1,5 @@
{% extends "base.html" %}
{% block header %}
<h1 class="fw-light">Companies</h1>
{% endblock %}
{% block cards %}
{% for c in company %}
<div class="col">

View File

@@ -1,7 +1,6 @@
{% extends "base.html" %}
{% block header %}
<h1 class="fw-light">{{ consist }}</h1>
{% if consist.tags.all %}
<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">

View File

@@ -1,8 +1,5 @@
{% extends "base.html" %}
{% block header %}
<h1 class="fw-light">Consists</h1>
{% endblock %}
{% block cards %}
{% for c in consist %}
<div class="col">

View File

@@ -1,7 +1,6 @@
{% 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 %}

View File

@@ -1,7 +1,6 @@
{% extends "base.html" %}
{% block header %}
{% if site_conf.about %}<h1 class="fw-light">About</h1>{% endif %}
<p class="lead text-muted">{{ site_conf.about | safe }}</p>
{% endblock %}

View File

@@ -1,7 +1,6 @@
{% extends 'base.html' %}
{% block header %}
<h1 class="fw-light">{{ rolling_stock }}</h1>
{% if rolling_stock.tags.all %}
<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">

View File

@@ -1,8 +1,5 @@
{% extends "base.html" %}
{% block header %}
<h1 class="fw-light">Scales</h1>
{% endblock %}
{% block cards %}
{% for s in scale %}
<div class="col">

View File

@@ -1,7 +1,6 @@
{% extends "base.html" %}
{% block header %}
<h1 class="fw-light">{{ filter | default_if_none:"Search" | title }}: {{ search }}</h1>
<p class="lead text-muted">Results found: {{ matches }}</p>
{% endblock %}
{% block pagination %}

View File

@@ -46,7 +46,11 @@ class GetHome(View):
return render(
request,
"home.html",
{"rolling_stock": rolling_stock, "page_range": page_range},
{
"title": "Home",
"rolling_stock": rolling_stock,
"page_range": page_range,
},
)
@@ -83,8 +87,10 @@ class GetHomeFiltered(View):
query = Q(tags__slug__iexact=search)
else:
raise Http404
rolling_stock = RollingStock.objects.filter(query).order_by(
*order_by_fields()
rolling_stock = (
RollingStock.objects.filter(query)
.distinct()
.order_by(*order_by_fields())
)
matches = len(rolling_stock)
@@ -105,6 +111,7 @@ class GetHomeFiltered(View):
request,
"search.html",
{
"title": "{0}: {1}".format(_filter.capitalize(), search),
"search": search,
"filter": _filter,
"matches": matches,
@@ -125,6 +132,7 @@ class GetHomeFiltered(View):
request,
"search.html",
{
"title": "{0}: {1}".format(_filter.capitalize(), search),
"search": search,
"filter": _filter,
"matches": matches,
@@ -164,6 +172,7 @@ class GetRollingStock(View):
request,
"page.html",
{
"title": rolling_stock,
"rolling_stock": rolling_stock,
"class_properties": class_properties,
"rolling_stock_properties": rolling_stock_properties,
@@ -186,7 +195,11 @@ class Consists(View):
return render(
request,
"consists.html",
{"consist": consist, "page_range": page_range},
{
"title": "Consists",
"consist": consist,
"page_range": page_range,
},
)
@@ -209,6 +222,7 @@ class GetConsist(View):
request,
"consist.html",
{
"title": consist,
"consist": consist,
"rolling_stock": rolling_stock,
"page_range": page_range,
@@ -230,7 +244,11 @@ class Companies(View):
return render(
request,
"companies.html",
{"company": company, "page_range": page_range},
{
"title": "Companies",
"company": company,
"page_range": page_range,
},
)
@@ -248,7 +266,7 @@ class Scales(View):
return render(
request,
"scales.html",
{"scale": scale, "page_range": page_range},
{"title": "Scales", "scale": scale, "page_range": page_range},
)
@@ -264,5 +282,5 @@ class GetFlatpage(View):
return render(
request,
"flatpage.html",
{"flatpage": flatpage},
{"title": flatpage.name, "flatpage": flatpage},
)

View File

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

View File

@@ -1,8 +1,29 @@
import os
import hashlib
import subprocess
from django.utils.html import format_html
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):

View File

@@ -125,6 +125,7 @@ class RollingStockAdmin(admin.ModelAdmin):
"address",
"sku",
)
save_as = True
fieldsets = (
(

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

@@ -9,7 +9,7 @@ from django.utils.safestring import mark_safe
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 (
Property,
Scale,
@@ -20,11 +20,6 @@ from metadata.models import (
RollingStockType,
)
# class OverwriteMixin(FileSystemStorage):
# def get_available_name(self, name, max_length):
# self.delete(name)
# return name
class RollingClass(models.Model):
identifier = models.CharField(max_length=128, unique=False)
@@ -137,7 +132,12 @@ class RollingStockDocument(models.Model):
RollingStock, on_delete=models.CASCADE, related_name="document"
)
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):
unique_together = ("rolling_stock", "file")
@@ -158,7 +158,9 @@ class RollingStockImage(models.Model):
rolling_stock = models.ForeignKey(
RollingStock, on_delete=models.CASCADE, related_name="image"
)
image = models.ImageField(upload_to="images/", null=True, blank=True)
image = models.ImageField(
upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True
)
is_thumbnail = models.BooleanField()
def image_thumbnail(self):
@@ -176,6 +178,9 @@ class RollingStockImage(models.Model):
).update(is_thumbnail=False)
super().save(**kwargs)
class Meta:
ordering = ["-is_thumbnail"]
class RollingStockProperty(models.Model):
rolling_stock = models.ForeignKey(