10 Commits

Author SHA1 Message Date
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
17 changed files with 242 additions and 29 deletions

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,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 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
@@ -17,7 +18,9 @@ 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(
upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True
)
notes = RichTextUploadingField(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)

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

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

@@ -59,7 +59,7 @@
<ul class="pagination justify-content-center mt-4 mb-0"> <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">
@@ -75,13 +75,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

@@ -102,7 +102,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 +111,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 +236,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 +256,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>

View File

@@ -83,7 +83,7 @@ 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 = RollingStock.objects.filter(query).distinct().order_by(
*order_by_fields() *order_by_fields()
) )
matches = len(rolling_stock) matches = len(rolling_stock)

View File

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

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

@@ -125,6 +125,7 @@ class RollingStockAdmin(admin.ModelAdmin):
"address", "address",
"sku", "sku",
) )
save_as = True
fieldsets = ( fieldsets = (
( (
@@ -148,6 +149,7 @@ class RollingStockAdmin(admin.ModelAdmin):
"DCC", "DCC",
{ {
"fields": ( "fields": (
"decoder_interface",
"decoder", "decoder",
"address", "address",
) )

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

@@ -3,12 +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.utils.safestring import mark_safe
from ckeditor_uploader.fields import RichTextUploadingField 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,
@@ -19,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)
@@ -87,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
) )
@@ -133,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")
@@ -154,7 +158,9 @@ class RollingStockImage(models.Model):
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(
upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True
)
is_thumbnail = models.BooleanField() is_thumbnail = models.BooleanField()
def image_thumbnail(self): def image_thumbnail(self):