mirror of
https://github.com/daniviga/django-ram.git
synced 2025-08-04 13:17:50 +02:00
Add documents to decoders (#22)
* Add decoder documents support * Use abstract model for Documents * Increase version * Code cleanup
This commit is contained in:
@@ -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")
|
||||
|
@@ -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")},
|
||||
},
|
||||
),
|
||||
]
|
@@ -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)
|
||||
|
@@ -36,7 +36,3 @@ a.badge, a.badge:hover {
|
||||
#footer > p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
th.th-35 {
|
||||
width: 35%;
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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 %}
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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 %}
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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
30
ram/ram/models.py
Normal 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)
|
||||
)
|
@@ -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()
|
||||
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user