17 Commits

Author SHA1 Message Date
7dadf23f5f Make pylibmc optional in requirements-prod.txt 2023-11-04 23:58:51 +01:00
4a12201d22 Make Document and Image files not nullable 2023-11-04 23:54:56 +01:00
830da80302 Keep media folder clean (#28)
* Reorg roster, portal and bookshelf media
* Extend media reorg to consists
* Delete roster and bookshelf images on delte.
   Do not delete others data that might be dedup! 
* Bump version
2023-10-31 11:16:55 +01:00
416ca5bbc6 eu.gif is part of dajngo-countries 2023-10-28 14:00:52 +02:00
03fc82c38d Enable csrf protection 2023-10-28 13:56:43 +02:00
ec8684dbc0 Add a "None" country and "Europe" with flags 2023-10-28 13:55:21 +02:00
7ec8baf733 Replace \t with spaces in base.html 2023-10-28 09:29:11 +02:00
86589ad718 More w3c minor fixes 2023-10-27 23:20:36 +02:00
98fed02a40 Fix a table in rollingstock.html 2023-10-27 23:16:23 +02:00
9602f67e0e Remove a spurious tag 2023-10-27 23:14:09 +02:00
5bb6279095 Extend UX improvements on other pages 2023-10-27 23:11:21 +02:00
84cdee42a6 Fix html syntax in rollingstock.html 2023-10-27 22:58:24 +02:00
168b424df7 Bump version 2023-10-27 22:46:19 +02:00
e1400fe720 Remove health page 2023-10-27 22:26:24 +02:00
26dea2fb35 Improve rollingstock page UX on mobile 2023-10-27 22:26:05 +02:00
ef767ec33d Fix a pretty-print on companies 2023-10-23 18:54:57 +02:00
b23801dbf0 Clear cache on save if active 2023-10-21 21:42:03 +02:00
30 changed files with 461 additions and 45 deletions

View File

@@ -0,0 +1,49 @@
# Generated by Django 4.2.6 on 2023-10-30 13:16
import os
import sys
import shutil
import ram.utils
import bookshelf.models
from django.db import migrations, models
from django.conf import settings
def move_images(apps, schema_editor):
sys.stdout.write("\n Processing files. Please await...")
for r in bookshelf.models.BookImage.objects.all():
fname = os.path.basename(r.image.path)
new_image = bookshelf.models.book_image_upload(r, fname)
new_path = os.path.join(settings.MEDIA_ROOT, new_image)
os.makedirs(os.path.dirname(new_path), exist_ok=True)
try:
shutil.move(r.image.path, new_path)
except FileNotFoundError:
sys.stderr.write(" !! FileNotFoundError: {}\n".format(new_image))
pass
r.image.name = new_image
r.save()
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0008_alter_author_options_alter_publisher_options"),
]
operations = [
migrations.AlterField(
model_name="bookimage",
name="image",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to=bookshelf.models.book_image_upload,
),
),
migrations.RunPython(
move_images,
reverse_code=migrations.RunPython.noop
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 4.2.6 on 2023-11-04 22:53
import bookshelf.models
from django.db import migrations, models
import ram.utils
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0009_alter_bookimage_image"),
]
operations = [
migrations.AlterField(
model_name="bookimage",
name="image",
field=models.ImageField(
storage=ram.utils.DeduplicatedStorage,
upload_to=bookshelf.models.book_image_upload,
),
),
]

View File

@@ -1,3 +1,5 @@
import os
import shutil
from uuid import uuid4 from uuid import uuid4
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
@@ -70,16 +72,32 @@ class Book(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return reverse("book", kwargs={"uuid": self.uuid}) return reverse("book", kwargs={"uuid": self.uuid})
def delete(self, *args, **kwargs):
shutil.rmtree(
os.path.join(
settings.MEDIA_ROOT, "images", "books", str(self.uuid)
),
ignore_errors=True
)
super(Book, self).delete(*args, **kwargs)
def book_image_upload(instance, filename):
return os.path.join(
"images",
"books",
str(instance.book.uuid),
filename
)
class BookImage(Image): class BookImage(Image):
book = models.ForeignKey( book = models.ForeignKey(
Book, on_delete=models.CASCADE, related_name="image" Book, on_delete=models.CASCADE, related_name="image"
) )
image = models.ImageField( image = models.ImageField(
upload_to="images/books/", # FIXME, find a better way to replace this upload_to=book_image_upload,
storage=DeduplicatedStorage, storage=DeduplicatedStorage,
null=True,
blank=True
) )

View File

@@ -0,0 +1,51 @@
# Generated by Django 4.2.6 on 2023-10-31 09:41
import os
import sys
import shutil
import ram.utils
from django.conf import settings
from django.db import migrations, models
def move_images(apps, schema_editor):
sys.stdout.write("\n Processing files. Please await...")
model = apps.get_model("consist", "Consist")
for r in model.objects.all():
if not r.image: # exit the loop if there's no image
continue
fname = os.path.basename(r.image.path)
new_image = os.path.join("images", "consists", fname)
new_path = os.path.join(settings.MEDIA_ROOT, new_image)
os.makedirs(os.path.dirname(new_path), exist_ok=True)
try:
shutil.move(r.image.path, new_path)
except FileNotFoundError:
sys.stderr.write(" !! FileNotFoundError: {}\n".format(new_image))
pass
r.image.name = new_image
r.save()
class Migration(migrations.Migration):
dependencies = [
("consist", "0008_alter_consist_options"),
]
operations = [
migrations.AlterField(
model_name="consist",
name="image",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/consists",
),
),
migrations.RunPython(
move_images,
reverse_code=migrations.RunPython.noop
),
]

View File

@@ -1,3 +1,5 @@
import os
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
@@ -19,7 +21,10 @@ 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( image = models.ImageField(
upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True upload_to=os.path.join("images", "consists"),
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)

View File

@@ -0,0 +1,80 @@
# Generated by Django 4.2.6 on 2023-10-30 13:16
import os
import sys
import shutil
import ram.utils
from django.conf import settings
from django.db import migrations, models
def move_images(apps, schema_editor):
fields = {
"Company": ["companies", "logo"],
"Decoder": ["decoders", "image"],
"Manufacturer": ["manufacturers", "logo"],
}
sys.stdout.write("\n Processing files. Please await...")
for m in fields.items():
model = apps.get_model("metadata", m[0])
for r in model.objects.all():
field = getattr(r, m[1][1])
if not field: # exit the loop if there's no image
continue
fname = os.path.basename(field.path)
new_image = os.path.join("images", m[1][0], fname)
new_path = os.path.join(settings.MEDIA_ROOT, new_image)
os.makedirs(os.path.dirname(new_path), exist_ok=True)
try:
shutil.move(field.path, new_path)
except FileNotFoundError:
sys.stderr.write(
" !! FileNotFoundError: {}\n".format(new_image)
)
pass
field.name = new_image
r.save()
class Migration(migrations.Migration):
dependencies = [
("metadata", "0014_alter_decoder_options_alter_tag_options"),
]
operations = [
migrations.AlterField(
model_name="company",
name="logo",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/companies",
),
),
migrations.AlterField(
model_name="decoder",
name="image",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/decoders",
),
),
migrations.AlterField(
model_name="manufacturer",
name="logo",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/manufacturers",
),
),
migrations.RunPython(
move_images,
reverse_code=migrations.RunPython.noop
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 4.2.6 on 2023-11-04 22:53
from django.db import migrations, models
import ram.utils
class Migration(migrations.Migration):
dependencies = [
("metadata", "0015_alter_company_logo_alter_decoder_image_and_more"),
]
operations = [
migrations.AlterField(
model_name="decoderdocument",
name="file",
field=models.FileField(
storage=ram.utils.DeduplicatedStorage(), upload_to="files/"
),
),
]

View File

@@ -1,3 +1,4 @@
import os
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
@@ -28,7 +29,10 @@ class Manufacturer(models.Model):
) )
website = models.URLField(blank=True) website = models.URLField(blank=True)
logo = models.ImageField( logo = models.ImageField(
upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True upload_to=os.path.join("images", "manufacturers"),
storage=DeduplicatedStorage,
null=True,
blank=True,
) )
class Meta: class Meta:
@@ -58,7 +62,10 @@ class Company(models.Model):
country = CountryField() country = CountryField()
freelance = models.BooleanField(default=False) freelance = models.BooleanField(default=False)
logo = models.ImageField( logo = models.ImageField(
upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True upload_to=os.path.join("images", "companies"),
storage=DeduplicatedStorage,
null=True,
blank=True,
) )
class Meta: class Meta:
@@ -76,6 +83,9 @@ class Company(models.Model):
} }
) )
def extended_name_pp(self):
return "({})".format(self.extended_name) if self.extended_name else ""
def logo_thumbnail(self): def logo_thumbnail(self):
return get_image_preview(self.logo.url) return get_image_preview(self.logo.url)
@@ -92,7 +102,10 @@ class Decoder(models.Model):
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)
image = models.ImageField( image = models.ImageField(
upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True upload_to=os.path.join("images", "decoders"),
storage=DeduplicatedStorage,
null=True,
blank=True,
) )
class Meta(object): class Meta(object):

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 B

View File

@@ -115,6 +115,19 @@
}) })
})() })()
</script> </script>
<script>
document.addEventListener('DOMContentLoaded', function () {
var selectElement = document.getElementById('tabSelector');
selectElement.addEventListener('change', function () {
var selectedTabId = this.value;
var tabs = document.querySelectorAll('.tab-pane');
tabs.forEach(function (tab) {
tab.classList.remove('show', 'active');
});
document.getElementById(selectedTabId).classList.add('show', 'active');
});
});
</script>
{% block extra_head %} {% block extra_head %}
{{ site_conf.extra_head | safe }} {{ site_conf.extra_head | safe }}
{% endblock %} {% endblock %}

View File

@@ -43,10 +43,14 @@
<section class="py-4 text-start container"> <section class="py-4 text-start container">
<div class="row"> <div class="row">
<div class="mx-auto"> <div class="mx-auto">
<nav class="nav nav-tabs flex-column flex-md-row mb-2" id="nav-tab"> <nav class="nav nav-tabs d-none d-lg-flex flex-row mb-2" id="nav-tab" role="tablist">
<button class="nav-link active" id="nav-summary-tab" data-bs-toggle="tab" data-bs-target="#nav-summary" type="button" role="tab" aria-controls="nav-summary" aria-selected="true">Summary</button> <button class="nav-link active" id="nav-summary-tab" data-bs-toggle="tab" data-bs-target="#nav-summary" type="button" role="tab" aria-controls="nav-summary" aria-selected="true">Summary</button>
{% if book.notes %}<button class="nav-link" id="nav-notes-tab" data-bs-toggle="tab" data-bs-target="#nav-notes" type="button" role="tab" aria-controls="nav-notes" aria-selected="false">Notes</button>{% endif %} {% if book.notes %}<button class="nav-link" id="nav-notes-tab" data-bs-toggle="tab" data-bs-target="#nav-notes" type="button" role="tab" aria-controls="nav-notes" aria-selected="false">Notes</button>{% endif %}
</nav> </nav>
<select class="form-select d-lg-none mb-2" id="tabSelector" aria-label="Tab selector">
<option value="nav-summary" selected>Summary</option>
{% if book.notes %}<option value="nav-notes">Notes</option>{% endif %}
</select>
<div class="tab-content" id="nav-tabContent"> <div class="tab-content" id="nav-tabContent">
<div class="tab-pane show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab"> <div class="tab-pane show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab">
<table class="table table-striped"> <table class="table table-striped">

View File

@@ -1,7 +1,7 @@
<div class="col"> <div class="col">
<div class="card shadow-sm"> <div class="card shadow-sm">
{% if d.item.image.exists %} {% if d.item.image.exists %}
<a href="{{d.item.get_absolute_url}}"><img class="card-img-top" src="{{ d.item.image.first.image.url }}" alt="{{ d }}"></a> <a href="{{d.item.get_absolute_url}}"><img class="card-img-top" src="{{ d.item.image.first.image.url }}" alt="{{ d.item }}"></a>
{% endif %} {% endif %}
<div class="card-body"> <div class="card-body">
<p class="card-text" style="position: relative;"> <p class="card-text" style="position: relative;">

View File

@@ -14,7 +14,7 @@
{% if d.item.logo %} {% if d.item.logo %}
<tr> <tr>
<th class="w-33" scope="row">Logo</th> <th class="w-33" scope="row">Logo</th>
<td><img class="logo" src="{{ d.item.logo.url }}" /></td> <td><img class="logo" src="{{ d.item.logo.url }}" alt="{{ d.item.name }} logo"></td>
</tr> </tr>
{% endif %} {% endif %}
<tr> <tr>
@@ -27,7 +27,7 @@
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Country</th> <th class="w-33" scope="row">Country</th>
<td>{{ d.item.country.name }} <img src="{{ d.item.country.flag }}" alt="{{ d.item.country }}" /> <td>{{ d.item.country.name }} <img src="{{ d.item.country.flag }}" alt="{{ d.item.country }}">
</tr> </tr>
{% if d.item.freelance %} {% if d.item.freelance %}
<tr> <tr>

View File

@@ -2,10 +2,10 @@
<div class="card shadow-sm"> <div class="card shadow-sm">
<a href="{{ d.item.get_absolute_url }}"> <a href="{{ d.item.get_absolute_url }}">
{% if d.item.image %} {% if d.item.image %}
<img class="card-img-top" src="{{ d.item.image.url }}" alt="{{ d }}"> <img class="card-img-top" src="{{ d.item.image.url }}" alt="{{ d.item }}">
{% else %} {% else %}
{% with d.item.consist_item.first.rolling_stock as r %} {% with d.item.consist_item.first.rolling_stock as r %}
<img class="card-img-top" src="{{ r.image.first.image.url }}" alt="{{ d }}"></a> <img class="card-img-top" src="{{ r.image.first.image.url }}" alt="{{ d.item }}">
{% endwith %} {% endwith %}
{% endif %} {% endif %}
</a> </a>

View File

@@ -14,7 +14,7 @@
{% if d.item.logo %} {% if d.item.logo %}
<tr> <tr>
<th class="w-33" scope="row">Logo</th> <th class="w-33" scope="row">Logo</th>
<td><img class="logo" src="{{ d.item.logo.url }}" /></td> <td><img class="logo" src="{{ d.item.logo.url }}" alt="{{ d.item.name }} logo"></td>
</tr> </tr>
{% endif %} {% endif %}
{% if d.item.website %} {% if d.item.website %}

View File

@@ -2,10 +2,10 @@
<div class="col"> <div class="col">
<div class="card shadow-sm"> <div class="card shadow-sm">
{% if d.item.image.exists %} {% if d.item.image.exists %}
<a href="{{d.item.get_absolute_url}}"><img class="card-img-top" src="{{ d.item.image.first.image.url }}" alt="{{ d }}"></a> <a href="{{d.item.get_absolute_url}}"><img class="card-img-top" src="{{ d.item.image.first.image.url }}" alt="{{ d.item }}"></a>
{% else %} {% else %}
<!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) --> <!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) -->
<a href="{{d.item.get_absolute_url}}"><img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d }}"></a> <a href="{{d.item.get_absolute_url}}"><img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d.item }}"></a>
{% endif %} {% endif %}
<div class="card-body"> <div class="card-body">
<p class="card-text" style="position: relative;"> <p class="card-text" style="position: relative;">

View File

@@ -16,7 +16,7 @@
<div id="carouselControls" class="carousel carousel-dark slide" data-bs-ride="carousel"> <div id="carouselControls" class="carousel carousel-dark slide" data-bs-ride="carousel">
<div class="carousel-inner"> <div class="carousel-inner">
<div class="carousel-item active"> <div class="carousel-item active">
<img src="{{ consist.image.url }}" class="d-block w-100 rounded img-thumbnail" alt="..."> <img src="{{ consist.image.url }}" class="d-block w-100 rounded img-thumbnail" alt="Consist cover">
</div> </div>
</div> </div>
</div> </div>
@@ -66,10 +66,14 @@
<section class="py-4 text-start container"> <section class="py-4 text-start container">
<div class="row"> <div class="row">
<div class="mx-auto"> <div class="mx-auto">
<nav class="nav nav-tabs flex-column flex-md-row mb-2" id="nav-tab"> <nav class="nav nav-tabs d-none d-lg-flex flex-row mb-2" id="nav-tab" role="tablist">
<button class="nav-link active" id="nav-summary-tab" data-bs-toggle="tab" data-bs-target="#nav-summary" type="button" role="tab" aria-controls="nav-summary" aria-selected="true">Summary</button> <button class="nav-link active" id="nav-summary-tab" data-bs-toggle="tab" data-bs-target="#nav-summary" type="button" role="tab" aria-controls="nav-summary" aria-selected="true">Summary</button>
{% if consist.notes %}<button class="nav-link" id="nav-notes-tab" data-bs-toggle="tab" data-bs-target="#nav-notes" type="button" role="tab" aria-controls="nav-notes" aria-selected="false">Notes</button>{% endif %} {% if consist.notes %}<button class="nav-link" id="nav-notes-tab" data-bs-toggle="tab" data-bs-target="#nav-notes" type="button" role="tab" aria-controls="nav-notes" aria-selected="false">Notes</button>{% endif %}
</nav> </nav>
<select class="form-select d-lg-none mb-2" id="tabSelector" aria-label="Tab selector">
<option value="nav-summary" selected>Summary</option>
{% if consist.notes %}<option value="nav-notes">Notes</option>{% endif %}
</select>
<div class="tab-content" id="nav-tabContent"> <div class="tab-content" id="nav-tabContent">
<div class="tab-pane show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab"> <div class="tab-pane show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab">
<table class="table table-striped"> <table class="table table-striped">

View File

@@ -1,4 +1,4 @@
<form class="d-flex needs-validation" action="{% url 'search' %}" method="post" novalidate> <form class="d-flex needs-validation" action="{% url 'search' %}" method="post" novalidate>{% csrf_token %}
<div class="input-group has-validation"> <div class="input-group has-validation">
<input class="form-control" type="search" list="datalistOptions" placeholder="Search" aria-label="Search" name="search" id="searchValidation" required> <input class="form-control" type="search" list="datalistOptions" placeholder="Search" aria-label="Search" name="search" id="searchValidation" required>
<datalist id="datalistOptions"> <datalist id="datalistOptions">

View File

@@ -43,7 +43,7 @@
<section class="py-4 text-start container"> <section class="py-4 text-start container">
<div class="row"> <div class="row">
<div class="mx-auto"> <div class="mx-auto">
<nav class="nav nav-tabs flex-column flex-md-row mb-2" id="nav-tab"> <nav class="nav nav-tabs d-none d-lg-flex flex-row mb-2" id="nav-tab" role="tablist">
<button class="nav-link active" id="nav-summary-tab" data-bs-toggle="tab" data-bs-target="#nav-summary" type="button" role="tab" aria-controls="nav-summary" aria-selected="true">Summary</button> <button class="nav-link active" id="nav-summary-tab" data-bs-toggle="tab" data-bs-target="#nav-summary" type="button" role="tab" aria-controls="nav-summary" aria-selected="true">Summary</button>
<button class="nav-link" id="nav-model-tab" data-bs-toggle="tab" data-bs-target="#nav-model" type="button" role="tab" aria-controls="nav-model" aria-selected="false">Model</button> <button class="nav-link" id="nav-model-tab" data-bs-toggle="tab" data-bs-target="#nav-model" type="button" role="tab" aria-controls="nav-model" aria-selected="false">Model</button>
<button class="nav-link" id="nav-class-tab" data-bs-toggle="tab" data-bs-target="#nav-class" type="button" role="tab" aria-controls="nav-class" aria-selected="false">Class</button> <button class="nav-link" id="nav-class-tab" data-bs-toggle="tab" data-bs-target="#nav-class" type="button" role="tab" aria-controls="nav-class" aria-selected="false">Class</button>
@@ -54,6 +54,17 @@
{% if rolling_stock.notes %}<button class="nav-link" id="nav-notes-tab" data-bs-toggle="tab" data-bs-target="#nav-notes" type="button" role="tab" aria-controls="nav-notes" aria-selected="false">Notes</button>{% endif %} {% if rolling_stock.notes %}<button class="nav-link" id="nav-notes-tab" data-bs-toggle="tab" data-bs-target="#nav-notes" type="button" role="tab" aria-controls="nav-notes" aria-selected="false">Notes</button>{% endif %}
{% if consists %}<button class="nav-link" id="nav-consists-tab" data-bs-toggle="tab" data-bs-target="#nav-consists" type="button" role="tab" aria-controls="nav-consists" aria-selected="false">Consists</button>{% endif %} {% if consists %}<button class="nav-link" id="nav-consists-tab" data-bs-toggle="tab" data-bs-target="#nav-consists" type="button" role="tab" aria-controls="nav-consists" aria-selected="false">Consists</button>{% endif %}
</nav> </nav>
<select class="form-select d-lg-none mb-2" id="tabSelector" aria-label="Tab selector">
<option value="nav-summary" selected>Summary</option>
<option value="nav-model">Model</option>
<option value="nav-class">Class</option>
<option value="nav-company">Company</option>
{% if rolling_stock.decoder %}<option value="nav-dcc">DCC</option>{% endif %}
{% if documents or decoder_documents %}<option value="nav-documents">Documents</option>{% endif %}
{% if journal %}<option value="nav-journal">Journal</option>{% endif %}
{% if rolling_stock.notes %}<option value="nav-notes">Notes</option>{% endif %}
{% if consists %}<option value="nav-consists">Consists</option>{% endif %}
</select>
{% with class=rolling_stock.rolling_class company=rolling_stock.rolling_class.company %} {% with class=rolling_stock.rolling_class company=rolling_stock.rolling_class.company %}
<div class="tab-content" id="nav-tabContent"> <div class="tab-content" id="nav-tabContent">
<div class="tab-pane show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab"> <div class="tab-pane show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab">
@@ -71,7 +82,7 @@
<tr> <tr>
<th scope="row">Company</th> <th scope="row">Company</th>
<td> <td>
<a href="{% url 'filtered' _filter="company" search=company.slug %}">{{ company }}</a> ({{ company.extended_name }}) <a href="{% url 'filtered' _filter="company" search=company.slug %}">{{ company }}</a> {{ company.extended_name_pp }}
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -98,7 +109,7 @@
<tr> <tr>
<th class="w-33" scope="row">Manufacturer</th> <th class="w-33" 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 }}</a>{% if rolling_stock.manufacturer.website %} <a href="{{ rolling_stock.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
{% endif %}</td> {% endif %}</td>
</tr> </tr>
<tr> <tr>
@@ -147,9 +158,11 @@
<tbody class="table-group-divider"> <tbody class="table-group-divider">
<tr> <tr>
<th class="w-33" scope="row">Manufacturer</th> <th class="w-33" scope="row">Manufacturer</th>
<td>{%if rolling_stock.manufacturer %} <td>
<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 %} {%if rolling_stock.manufacturer %}
{% endif %}</td> <a href="{% url 'filtered' _filter="manufacturer" search=rolling_stock.manufacturer.slug %}">{{ rolling_stock.manufacturer }}</a>{% if rolling_stock.manufacturer.website %} <a href="{{ rolling_stock.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
{% else %}-{% endif %}
</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Scale</th> <th scope="row">Scale</th>
@@ -209,9 +222,11 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Manufacturer</th> <th scope="row">Manufacturer</th>
<td>{%if class.manufacturer %} <td>
<a href="{% url 'filtered' _filter="manufacturer" search=class.manufacturer.slug %}">{{ class.manufacturer }}{% if class.manufacturer.website %}</a> <a href="{{ class.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %} {%if class.manufacturer %}
{% endif %}</td> <a href="{% url 'filtered' _filter="manufacturer" search=class.manufacturer.slug %}">{{ class.manufacturer }}</a>{% if class.manufacturer.website %} <a href="{{ class.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
{% else %}-{% endif %}
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -244,16 +259,16 @@
{% if company.logo %} {% if company.logo %}
<tr> <tr>
<th class="w-33" scope="row">Logo</th> <th class="w-33" scope="row">Logo</th>
<td><img class="logo-xl" src="{{ company.logo.url }}" /></td> <td><img class="logo-xl" src="{{ company.logo.url }}" alt="{{ company }} logo"></td>
</tr> </tr>
{% endif %} {% endif %}
<tr> <tr>
<th class="w-33" scope="row">Name</th> <th class="w-33" scope="row">Name</th>
<td><a href="{% url 'filtered' _filter="company" search=company.slug %}">{{ company.name }}</a> ({{ company.extended_name }})</td> <td><a href="{% url 'filtered' _filter="company" search=company.slug %}">{{ company.name }}</a> {{ company.extended_name_pp }}</td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Country</th> <th class="w-33" scope="row">Country</th>
<td>{{ company.country.name }} <img src="{{ company.country.flag }}" alt="{{ company.country }}" /> <td>{{ company.country.name }} <img src="{{ company.country.flag }}" alt="{{ company.country }}">
</tr> </tr>
{% if company.freelance %} {% if company.freelance %}
<tr> <tr>
@@ -341,14 +356,14 @@
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th colspan="3" scope="row">Journal</th> <th colspan="2" scope="row">Journal</th>
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
{% for j in journal %} {% for j in journal %}
<tr> <tr>
<th class="w-33" scope="row">{{ j.date }}</th> <th class="w-33" scope="row">{{ j.date }}</th>
<td>{{ j.log | safe }}</a></td> <td>{{ j.log | safe }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

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

14
ram/ram/apps.py Normal file
View File

@@ -0,0 +1,14 @@
from django.conf import settings
from django.apps import AppConfig
class RamConfig(AppConfig):
name = "ram"
def ready(self):
cache_middleware = set([
"django.middleware.cache.UpdateCacheMiddleware",
"django.middleware.cache.FetchFromCacheMiddleware",
])
if cache_middleware.issubset(settings.MIDDLEWARE):
from ram.signals import clear_cache # noqa: F401

View File

@@ -11,8 +11,6 @@ class Document(models.Model):
file = models.FileField( file = models.FileField(
upload_to="files/", upload_to="files/",
storage=DeduplicatedStorage(), storage=DeduplicatedStorage(),
null=True,
blank=True,
) )
private = models.BooleanField(default=False) private = models.BooleanField(default=False)
@@ -34,7 +32,8 @@ class Document(models.Model):
class Image(models.Model): class Image(models.Model):
order = models.PositiveIntegerField(default=0, blank=False, null=False) order = models.PositiveIntegerField(default=0, blank=False, null=False)
image = models.ImageField( image = models.ImageField(
upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True upload_to="images/",
storage=DeduplicatedStorage,
) )
def image_thumbnail(self): def image_thumbnail(self):

View File

@@ -41,8 +41,6 @@ INSTALLED_APPS = [
"django.contrib.sessions", "django.contrib.sessions",
"django.contrib.messages", "django.contrib.messages",
"django.contrib.staticfiles", "django.contrib.staticfiles",
"health_check",
"health_check.db",
"adminsortable2", "adminsortable2",
"django_countries", "django_countries",
"solo", "solo",
@@ -51,7 +49,7 @@ INSTALLED_APPS = [
"rest_framework", "rest_framework",
"ram", "ram",
"portal", "portal",
# "driver", # "driver", # uncomment this to enable the "driver" API
"metadata", "metadata",
"roster", "roster",
"consist", "consist",
@@ -62,7 +60,7 @@ MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware", "django.middleware.common.CommonMiddleware",
# 'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
@@ -147,7 +145,8 @@ MEDIA_ROOT = STORAGE_DIR / "media"
CKEDITOR_UPLOAD_PATH = "uploads/" CKEDITOR_UPLOAD_PATH = "uploads/"
COUNTRIES_OVERRIDE = { COUNTRIES_OVERRIDE = {
"ZZ": "Freelance", "EU": "Europe",
"XX": "None",
} }
# Image used on cards without a custom image uploaded. # Image used on cards without a custom image uploaded.

8
ram/ram/signals.py Normal file
View File

@@ -0,0 +1,8 @@
from django.core.cache import cache
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save)
def clear_cache(sender, **kwargs):
cache.clear()

View File

@@ -24,7 +24,6 @@ urlpatterns = [
path("", lambda r: redirect("portal/")), path("", lambda r: redirect("portal/")),
path("ckeditor/", include("ckeditor_uploader.urls")), path("ckeditor/", include("ckeditor_uploader.urls")),
path("portal/", include("portal.urls")), path("portal/", include("portal.urls")),
path("ht/", include("health_check.urls")),
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("api/v1/consist/", include("consist.urls")), path("api/v1/consist/", include("consist.urls")),
path("api/v1/roster/", include("roster.urls")), path("api/v1/roster/", include("roster.urls")),

View File

@@ -28,6 +28,7 @@ class RollingClass(admin.ModelAdmin):
"company__name", "company__name",
"type__type", "type__type",
) )
save_as = True
class RollingStockDocInline(admin.TabularInline): class RollingStockDocInline(admin.TabularInline):

View File

@@ -0,0 +1,49 @@
# Generated by Django 4.2.6 on 2023-10-30 13:16
import os
import sys
import shutil
import ram.utils
import roster.models
from django.db import migrations, models
from django.conf import settings
def move_images(apps, schema_editor):
sys.stdout.write("\n Processing files. Please await...")
for r in roster.models.RollingStockImage.objects.all():
fname = os.path.basename(r.image.path)
new_image = roster.models.rolling_stock_image_upload(r, fname)
new_path = os.path.join(settings.MEDIA_ROOT, new_image)
os.makedirs(os.path.dirname(new_path), exist_ok=True)
try:
shutil.move(r.image.path, new_path)
except FileNotFoundError:
sys.stderr.write(" !! FileNotFoundError: {}\n".format(new_image))
pass
r.image.name = new_image
r.save()
class Migration(migrations.Migration):
dependencies = [
("roster", "0019_rollingstockdocument_private"),
]
operations = [
migrations.AlterField(
model_name="rollingstockimage",
name="image",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to=roster.models.rolling_stock_image_upload,
),
),
migrations.RunPython(
move_images,
reverse_code=migrations.RunPython.noop
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 4.2.6 on 2023-11-04 22:53
from django.db import migrations, models
import ram.utils
import roster.models
class Migration(migrations.Migration):
dependencies = [
("roster", "0020_alter_rollingstockimage_image"),
]
operations = [
migrations.AlterField(
model_name="rollingstockdocument",
name="file",
field=models.FileField(
storage=ram.utils.DeduplicatedStorage(), upload_to="files/"
),
),
migrations.AlterField(
model_name="rollingstockimage",
name="image",
field=models.ImageField(
storage=ram.utils.DeduplicatedStorage,
upload_to=roster.models.rolling_stock_image_upload,
),
),
]

View File

@@ -1,4 +1,6 @@
import os
import re import re
import shutil
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
@@ -8,7 +10,7 @@ from django.dispatch import receiver
from ckeditor_uploader.fields import RichTextUploadingField from ckeditor_uploader.fields import RichTextUploadingField
from ram.models import Document, Image, PropertyInstance from ram.models import Document, Image, PropertyInstance
from ram.utils import get_image_preview from ram.utils import DeduplicatedStorage
from metadata.models import ( from metadata.models import (
Scale, Scale,
Manufacturer, Manufacturer,
@@ -106,6 +108,15 @@ class RollingStock(models.Model):
def company(self): def company(self):
return str(self.rolling_class.company) return str(self.rolling_class.company)
def delete(self, *args, **kwargs):
shutil.rmtree(
os.path.join(
settings.MEDIA_ROOT, "images", "rollingstock", str(self.uuid)
),
ignore_errors=True
)
super(RollingStock, self).delete(*args, **kwargs)
@receiver(models.signals.pre_save, sender=RollingStock) @receiver(models.signals.pre_save, sender=RollingStock)
def pre_save_running_number(sender, instance, *args, **kwargs): def pre_save_running_number(sender, instance, *args, **kwargs):
@@ -126,10 +137,23 @@ class RollingStockDocument(Document):
unique_together = ("rolling_stock", "file") unique_together = ("rolling_stock", "file")
def rolling_stock_image_upload(instance, filename):
return os.path.join(
"images",
"rollingstock",
str(instance.rolling_stock.uuid),
filename
)
class RollingStockImage(Image): class RollingStockImage(Image):
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=rolling_stock_image_upload,
storage=DeduplicatedStorage,
)
class RollingStockProperty(PropertyInstance): class RollingStockProperty(PropertyInstance):

View File

@@ -1,2 +1,2 @@
gunicorn gunicorn
pylibmc # Optional: # pylibmc