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 ( from metadata.models import (
Property, Property,
Decoder, Decoder,
DecoderDocument,
Scale, Scale,
Manufacturer, Manufacturer,
Company, Company,
@@ -17,8 +18,16 @@ class PropertyAdmin(admin.ModelAdmin):
search_fields = ("name",) search_fields = ("name",)
class DecoderDocInline(admin.TabularInline):
model = DecoderDocument
min_num = 0
extra = 0
classes = ["collapse"]
@admin.register(Decoder) @admin.register(Decoder)
class DecoderAdmin(admin.ModelAdmin): class DecoderAdmin(admin.ModelAdmin):
inlines = (DecoderDocInline,)
readonly_fields = ("image_thumbnail",) readonly_fields = ("image_thumbnail",)
list_display = ("__str__", "sound") list_display = ("__str__", "sound")
list_filter = ("manufacturer", "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.db import models
from django.urls import reverse from django.urls import reverse
from django.conf import settings 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.models import Document
from ram.utils import DeduplicatedStorage, get_image_preview, slugify from ram.utils import DeduplicatedStorage, get_image_preview, slugify
@@ -88,7 +87,7 @@ class Decoder(models.Model):
manufacturer = models.ForeignKey( manufacturer = models.ForeignKey(
Manufacturer, Manufacturer,
on_delete=models.CASCADE, on_delete=models.CASCADE,
limit_choices_to={"category": "model"}, limit_choices_to={"category": "accessory"},
) )
version = models.CharField(max_length=64, blank=True) version = models.CharField(max_length=64, blank=True)
sound = models.BooleanField(default=False) sound = models.BooleanField(default=False)
@@ -105,6 +104,15 @@ class Decoder(models.Model):
image_thumbnail.short_description = "Preview" 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): class Scale(models.Model):
scale = models.CharField(max_length=32, unique=True) scale = models.CharField(max_length=32, unique=True)
slug = models.CharField(max_length=32, unique=True, editable=False) 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=Manufacturer)
@receiver(models.signals.pre_save, sender=Company) @receiver(models.signals.pre_save, sender=Company)
@receiver(models.signals.pre_save, sender=Scale) @receiver(models.signals.pre_save, sender=Scale)

View File

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

View File

@@ -32,7 +32,7 @@
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<th class="th-35" scope="row">Type</th> <th class="w-25" scope="row">Type</th>
<td>{{ d.rolling_class.type }}</td> <td>{{ d.rolling_class.type }}</td>
</tr> </tr>
<tr> <tr>
@@ -54,7 +54,7 @@
<td>{{ d.era }}</td> <td>{{ d.era }}</td>
</tr> </tr>
<tr> <tr>
<th class="th-35" scope="row">Manufacturer</th> <th class="w-25" scope="row">Manufacturer</th>
<td>{%if d.manufacturer %} <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 %} <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> {% endif %}</td>
@@ -78,7 +78,7 @@
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<th class="th-35" scope="row">Decoder</th> <th class="w-25" scope="row">Decoder</th>
<td>{{ d.decoder }}</td> <td>{{ d.decoder }}</td>
</tr> </tr>
<tr> <tr>

View File

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

View File

@@ -82,7 +82,7 @@
</thead> </thead>
<tbody> <tbody>
<tr> <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> <td><abbr title="{{ consist.company.extended_name }}">{{ consist.company }}</abbr></td>
</tr> </tr>
<tr> <tr>

View File

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

View File

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

View File

@@ -64,7 +64,7 @@
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<th class="th-35" scope="row">Type</th> <th class="w-25" scope="row">Type</th>
<td>{{ rolling_stock.rolling_class.type }}</td> <td>{{ rolling_stock.rolling_class.type }}</td>
</tr> </tr>
<tr> <tr>
@@ -95,7 +95,7 @@
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<th class="th-35" scope="row">Manufacturer</th> <th class="w-25" scope="row">Manufacturer</th>
<td>{%if rolling_stock.manufacturer %} <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 %} <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> {% endif %}</td>
@@ -119,7 +119,7 @@
</thead> </thead>
<tbody> <tbody>
<tr> <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> <td>{{ rolling_stock.get_decoder_interface_display }}</td>
</tr> </tr>
{% if rolling_stock.decoder %} {% if rolling_stock.decoder %}
@@ -145,7 +145,7 @@
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<th class="th-35" scope="row">Manufacturer</th> <th class="w-25" scope="row">Manufacturer</th>
<td>{%if rolling_stock.manufacturer %} <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 %} <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> {% endif %}</td>
@@ -182,7 +182,7 @@
<tbody> <tbody>
{% for p in rolling_stock_properties %} {% for p in rolling_stock_properties %}
<tr> <tr>
<th class="th-35" scope="row">{{ p.property }}</th> <th class="w-25" scope="row">{{ p.property }}</th>
<td>{{ p.value }}</td> <td>{{ p.value }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
@@ -199,7 +199,7 @@
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<th class="th-35" scope="row">Class</th> <th class="w-25" scope="row">Class</th>
<td>{{ rolling_stock.rolling_class.identifier }}</td> <td>{{ rolling_stock.rolling_class.identifier }}</td>
</tr> </tr>
<tr> <tr>
@@ -234,7 +234,7 @@
<tbody> <tbody>
{% for p in class_properties %} {% for p in class_properties %}
<tr> <tr>
<th class="th-35" scope="row">{{ p.property }}</th> <th class="w-25" scope="row">{{ p.property }}</th>
<td>{{ p.value }}</td> <td>{{ p.value }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
@@ -255,7 +255,7 @@
<td>{{ rolling_stock.get_decoder_interface_display }}</td> <td>{{ rolling_stock.get_decoder_interface_display }}</td>
</tr> </tr>
<tr> <tr>
<th class="th-35" scope="row">Address</th> <th class="w-25" scope="row">Address</th>
<td>{{ rolling_stock.address }}</td> <td>{{ rolling_stock.address }}</td>
</tr> </tr>
<tr> <tr>
@@ -301,13 +301,31 @@
<tbody> <tbody>
{% for d in rolling_stock.document.all %} {% for d in rolling_stock.document.all %}
<tr> <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><a href="{{ d.file.url }}" target="_blank">{{ d.filename }}</a></td>
<td class="text-end">{{ d.file.size | filesizeformat }}</td> <td class="text-end">{{ d.file.size | filesizeformat }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </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>
<div class="tab-pane fade" id="nav-journal" role="tabpanel" aria-labelledby="nav-journal-tab"> <div class="tab-pane fade" id="nav-journal" role="tabpanel" aria-labelledby="nav-journal-tab">
<table class="table table-striped"> <table class="table table-striped">
@@ -319,7 +337,7 @@
<tbody> <tbody>
{% for j in rolling_stock_journal %} {% for j in rolling_stock_journal %}
<tr> <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> <td>{{ j.log | safe }}</a></td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
from ram.utils import git_suffix from ram.utils import git_suffix
__version__ = "0.3.3" __version__ = "0.4.0"
__version__ += git_suffix(__file__) __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): def save(self, name, content, max_length=None):
if super().exists(name): 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: with open(super().path(name), "rb") as file:
file_binary = file.read() file_binary = file.read()
old = hashlib.sha256(file_binary).hexdigest() old = hashlib.sha256(file_binary).hexdigest()

View File

@@ -5,11 +5,11 @@ from django.db import models
from django.urls import reverse from django.urls import reverse
from django.conf import settings from django.conf import settings
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.safestring import mark_safe
from ckeditor_uploader.fields import RichTextUploadingField from ckeditor_uploader.fields import RichTextUploadingField
from ram.utils import DeduplicatedStorage, get_image_preview from ram.utils import DeduplicatedStorage, get_image_preview
from ram.models import Document
from metadata.models import ( from metadata.models import (
Property, Property,
Scale, Scale,
@@ -127,32 +127,14 @@ def pre_save_running_number(sender, instance, *args, **kwargs):
pass pass
class RollingStockDocument(models.Model): class RollingStockDocument(Document):
rolling_stock = models.ForeignKey( rolling_stock = models.ForeignKey(
RollingStock, on_delete=models.CASCADE, related_name="document" 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): class Meta(object):
unique_together = ("rolling_stock", "file") 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): class RollingStockImage(models.Model):
order = models.PositiveIntegerField(default=0, blank=False, null=False) order = models.PositiveIntegerField(default=0, blank=False, null=False)