Add documents to decoders (#22)

* Add decoder documents support
* Use abstract model for Documents
* Increase version
* Code cleanup
This commit is contained in:
2023-10-01 00:03:41 +02:00
committed by GitHub
parent 9483648a1f
commit 5d536ce568
16 changed files with 161 additions and 60 deletions

View File

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

View File

@@ -0,0 +1,59 @@
# Generated by Django 4.2 on 2023-09-30 21:54
from django.db import migrations, models
import django.db.models.deletion
import ram.utils
class Migration(migrations.Migration):
dependencies = [
("metadata", "0011_company_slug_and_more"),
]
operations = [
migrations.AlterField(
model_name="decoder",
name="manufacturer",
field=models.ForeignKey(
limit_choices_to={"category": "accessory"},
on_delete=django.db.models.deletion.CASCADE,
to="metadata.manufacturer",
),
),
migrations.CreateModel(
name="DecoderDocument",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("description", models.CharField(blank=True, max_length=128)),
(
"file",
models.FileField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage(),
upload_to="files/",
),
),
(
"decoder",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="document",
to="metadata.decoder",
),
),
],
options={
"unique_together": {("decoder", "file")},
},
),
]

View File

@@ -1,11 +1,10 @@
from urllib.parse import quote
from django.db import models
from django.urls import reverse
from django.conf import settings
from django.dispatch.dispatcher import receiver
from django_countries.fields import CountryField
from ram.models import Document
from ram.utils import DeduplicatedStorage, get_image_preview, slugify
@@ -88,7 +87,7 @@ class Decoder(models.Model):
manufacturer = models.ForeignKey(
Manufacturer,
on_delete=models.CASCADE,
limit_choices_to={"category": "model"},
limit_choices_to={"category": "accessory"},
)
version = models.CharField(max_length=64, blank=True)
sound = models.BooleanField(default=False)
@@ -105,6 +104,15 @@ class Decoder(models.Model):
image_thumbnail.short_description = "Preview"
class DecoderDocument(Document):
decoder = models.ForeignKey(
Decoder, on_delete=models.CASCADE, related_name="document"
)
class Meta:
unique_together = ("decoder", "file")
class Scale(models.Model):
scale = models.CharField(max_length=32, unique=True)
slug = models.CharField(max_length=32, unique=True, editable=False)
@@ -167,7 +175,6 @@ class Tag(models.Model):
)
@receiver(models.signals.pre_save, sender=Manufacturer)
@receiver(models.signals.pre_save, sender=Company)
@receiver(models.signals.pre_save, sender=Scale)

View File

@@ -36,7 +36,3 @@ a.badge, a.badge:hover {
#footer > p {
display: inline;
}
th.th-35 {
width: 35%;
}

View File

@@ -32,7 +32,7 @@
</thead>
<tbody>
<tr>
<th class="th-35" scope="row">Type</th>
<th class="w-25" scope="row">Type</th>
<td>{{ d.rolling_class.type }}</td>
</tr>
<tr>
@@ -54,7 +54,7 @@
<td>{{ d.era }}</td>
</tr>
<tr>
<th class="th-35" scope="row">Manufacturer</th>
<th class="w-25" scope="row">Manufacturer</th>
<td>{%if d.manufacturer %}
<a href="{% url 'filtered' _filter="manufacturer" search=d.manufacturer.slug %}">{{ d.manufacturer }}{% if d.manufacturer.website %}</a> <a href="{{ d.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
{% endif %}</td>
@@ -78,7 +78,7 @@
</thead>
<tbody>
<tr>
<th class="th-35" scope="row">Decoder</th>
<th class="w-25" scope="row">Decoder</th>
<td>{{ d.decoder }}</td>
</tr>
<tr>

View File

@@ -17,25 +17,25 @@
<tbody>
{% if d.logo %}
<tr>
<th class="th-35" scope="row">Logo</th>
<th class="w-25" scope="row">Logo</th>
<td><img style="max-height: 48px" src="{{ d.logo.url }}" /></td>
</tr>
{% endif %}
<tr>
<th class="th-35" scope="row">Name</th>
<th class="w-25" scope="row">Name</th>
<td>{{ d.extended_name }}</td>
</tr>
<tr>
<th class="th-35" scope="row">Abbreviation</th>
<th class="w-25" scope="row">Abbreviation</th>
<td>{{ d.name }}</td>
</tr>
<tr>
<th class="th-35" scope="row">Country</th>
<th class="w-25" scope="row">Country</th>
<td>{{ d.country.name }} <img src="{{ d.country.flag }}" alt="{{ d.country }}" />
</tr>
{% if d.freelance %}
<tr>
<th class="th-35" scope="row">Notes</th>
<th class="w-25" scope="row">Notes</th>
<td>A <em>freelance</em> company</td>
</tr>
{% endif %}

View File

@@ -82,7 +82,7 @@
</thead>
<tbody>
<tr>
<th class="th-35" scope="row">Company</th>
<th class="w-25" scope="row">Company</th>
<td><abbr title="{{ consist.company.extended_name }}">{{ consist.company }}</abbr></td>
</tr>
<tr>

View File

@@ -36,12 +36,12 @@
<tbody>
{% if d.address %}
<tr>
<th class="th-35" scope="row">Address</th>
<th class="w-25" scope="row">Address</th>
<td>{{ d.address }}</td>
</tr>
{% endif %}
<tr>
<th class="th-35" scope="row">Company</th>
<th class="w-25" scope="row">Company</th>
<td><abbr title="{{ d.company.extended_name }}">{{ d.company }}</abbr></td>
</tr>
<tr>

View File

@@ -17,18 +17,18 @@
<tbody>
{% if d.logo %}
<tr>
<th class="th-35" scope="row">Logo</th>
<th class="w-25" scope="row">Logo</th>
<td><img style="max-height: 48px" src="{{ d.logo.url }}" /></td>
</tr>
{% endif %}
{% if d.website %}
<tr>
<th class="th-35" scope="row">Website</th>
<th class="w-25" scope="row">Website</th>
<td><a href="{{ d.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a></td>
</tr>
{% endif %}
<tr>
<th class="th-35" scope="row">Category</th>
<th class="w-25" scope="row">Category</th>
<td>{{ d.category | title }}</td>
</tr>
</tbody>

View File

@@ -64,7 +64,7 @@
</thead>
<tbody>
<tr>
<th class="th-35" scope="row">Type</th>
<th class="w-25" scope="row">Type</th>
<td>{{ rolling_stock.rolling_class.type }}</td>
</tr>
<tr>
@@ -95,7 +95,7 @@
</thead>
<tbody>
<tr>
<th class="th-35" scope="row">Manufacturer</th>
<th class="w-25" scope="row">Manufacturer</th>
<td>{%if rolling_stock.manufacturer %}
<a href="{% url 'filtered' _filter="manufacturer" search=rolling_stock.manufacturer.slug %}">{{ rolling_stock.manufacturer }}{% if rolling_stock.manufacturer.website %}</a> <a href="{{ rolling_stock.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
{% endif %}</td>
@@ -119,7 +119,7 @@
</thead>
<tbody>
<tr>
<th class="th-35" scope="row">Interface</th>
<th class="w-25" scope="row">Interface</th>
<td>{{ rolling_stock.get_decoder_interface_display }}</td>
</tr>
{% if rolling_stock.decoder %}
@@ -145,7 +145,7 @@
</thead>
<tbody>
<tr>
<th class="th-35" scope="row">Manufacturer</th>
<th class="w-25" scope="row">Manufacturer</th>
<td>{%if rolling_stock.manufacturer %}
<a href="{% url 'filtered' _filter="manufacturer" search=rolling_stock.manufacturer.slug %}">{{ rolling_stock.manufacturer }}{% if rolling_stock.manufacturer.website %}</a> <a href="{{ rolling_stock.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
{% endif %}</td>
@@ -182,7 +182,7 @@
<tbody>
{% for p in rolling_stock_properties %}
<tr>
<th class="th-35" scope="row">{{ p.property }}</th>
<th class="w-25" scope="row">{{ p.property }}</th>
<td>{{ p.value }}</td>
</tr>
{% endfor %}
@@ -199,7 +199,7 @@
</thead>
<tbody>
<tr>
<th class="th-35" scope="row">Class</th>
<th class="w-25" scope="row">Class</th>
<td>{{ rolling_stock.rolling_class.identifier }}</td>
</tr>
<tr>
@@ -234,7 +234,7 @@
<tbody>
{% for p in class_properties %}
<tr>
<th class="th-35" scope="row">{{ p.property }}</th>
<th class="w-25" scope="row">{{ p.property }}</th>
<td>{{ p.value }}</td>
</tr>
{% endfor %}
@@ -255,7 +255,7 @@
<td>{{ rolling_stock.get_decoder_interface_display }}</td>
</tr>
<tr>
<th class="th-35" scope="row">Address</th>
<th class="w-25" scope="row">Address</th>
<td>{{ rolling_stock.address }}</td>
</tr>
<tr>
@@ -301,13 +301,31 @@
<tbody>
{% for d in rolling_stock.document.all %}
<tr>
<td>{{ d.description }}</td>
<td class="w-25">{{ d.description }}</td>
<td><a href="{{ d.file.url }}" target="_blank">{{ d.filename }}</a></td>
<td class="text-end">{{ d.file.size | filesizeformat }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if rolling_stock.decoder.document.count > 0 %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="3" scope="row">Decoder documents</th>
</tr>
</thead>
<tbody>
{% for d in rolling_stock.decoder.document.all %}
<tr>
<td class="w-25">{{ d.description }}</td>
<td><a href="{{ d.file.url }}" target="_blank">{{ d.filename }}</a></td>
<td class="text-end">{{ d.file.size | filesizeformat }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
<div class="tab-pane fade" id="nav-journal" role="tabpanel" aria-labelledby="nav-journal-tab">
<table class="table table-striped">
@@ -319,7 +337,7 @@
<tbody>
{% for j in rolling_stock_journal %}
<tr>
<th class="th-35" scope="row">{{ j.date }}</th>
<th class="w-25" scope="row">{{ j.date }}</th>
<td>{{ j.log | safe }}</a></td>
</tr>
{% endfor %}

View File

@@ -14,19 +14,19 @@
</thead>
<tbody>
<tr>
<th class="th-35" scope="row">Name</th>
<th class="w-25" scope="row">Name</th>
<td>{{ d.scale }}</td>
</tr>
<tr>
<th class="th-35" scope="row">Ratio</th>
<th class="w-25" scope="row">Ratio</th>
<td>{{ d.ratio }}</td>
</tr>
<tr>
<th class="th-35" scope="row">Gauge</th>
<th class="w-25" scope="row">Gauge</th>
<td>{{ d.gauge }}</td>
</tr>
<tr>
<th class="th-35" scope="row">Tracks</th>
<th class="w-25" scope="row">Tracks</th>
<td>{{ d.tracks }}</td>
</tr>
</tbody>

View File

@@ -14,11 +14,11 @@
</thead>
<tbody>
<tr>
<th class="th-35" scope="row">Type</th>
<th class="w-25" scope="row">Type</th>
<td>{{ d.type }}</td>
</tr>
<tr>
<th class="th-35" scope="row">Category</th>
<th class="w-25" scope="row">Category</th>
<td>{{ d.category | title}}</td>
</tr>
</tbody>

View File

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

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

@@ -0,0 +1,30 @@
import os
from django.db import models
from django.utils.safestring import mark_safe
from ram.utils import DeduplicatedStorage
class Document(models.Model):
description = models.CharField(max_length=128, blank=True)
file = models.FileField(
upload_to="files/",
storage=DeduplicatedStorage(),
null=True,
blank=True,
)
class Meta:
abstract = True
def __str__(self):
return "{0}".format(os.path.basename(self.file.name))
def filename(self):
return self.__str__()
def download(self):
return mark_safe(
'<a href="{0}" target="_blank">Link</a>'.format(self.file.url)
)

View File

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

View File

@@ -5,11 +5,11 @@ from django.db import models
from django.urls import reverse
from django.conf import settings
from django.dispatch import receiver
from django.utils.safestring import mark_safe
from ckeditor_uploader.fields import RichTextUploadingField
from ram.utils import DeduplicatedStorage, get_image_preview
from ram.models import Document
from metadata.models import (
Property,
Scale,
@@ -127,32 +127,14 @@ def pre_save_running_number(sender, instance, *args, **kwargs):
pass
class RollingStockDocument(models.Model):
class RollingStockDocument(Document):
rolling_stock = models.ForeignKey(
RollingStock, on_delete=models.CASCADE, related_name="document"
)
description = models.CharField(max_length=128, blank=True)
file = models.FileField(
upload_to="files/",
storage=DeduplicatedStorage(),
null=True,
blank=True,
)
class Meta(object):
unique_together = ("rolling_stock", "file")
def __str__(self):
return "{0}".format(os.path.basename(self.file.name))
def filename(self):
return self.__str__()
def download(self):
return mark_safe(
'<a href="{0}" target="_blank">Link</a>'.format(self.file.url)
)
class RollingStockImage(models.Model):
order = models.PositiveIntegerField(default=0, blank=False, null=False)