mirror of
https://github.com/daniviga/django-ram.git
synced 2026-02-04 18:10:38 +01:00
Compare commits
3 Commits
2ab2d00585
...
v0.19.7
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a469378df | |||
| ede8741473 | |||
|
49c8d804d6
|
18
ram/consist/migrations/0019_consistitem_load.py
Normal file
18
ram/consist/migrations/0019_consistitem_load.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0 on 2026-01-03 12:31
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("consist", "0018_alter_consist_scale"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="consistitem",
|
||||||
|
name="load",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -43,10 +43,10 @@ class Consist(BaseModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def length(self):
|
def length(self):
|
||||||
return self.consist_item.count()
|
return self.consist_item.filter(load=False).count()
|
||||||
|
|
||||||
def get_type_count(self):
|
def get_type_count(self):
|
||||||
return self.consist_item.annotate(
|
return self.consist_item.filter(load=False).annotate(
|
||||||
type=models.F("rolling_stock__rolling_class__type__type")
|
type=models.F("rolling_stock__rolling_class__type__type")
|
||||||
).values(
|
).values(
|
||||||
"type"
|
"type"
|
||||||
@@ -69,6 +69,7 @@ class ConsistItem(models.Model):
|
|||||||
Consist, on_delete=models.CASCADE, related_name="consist_item"
|
Consist, on_delete=models.CASCADE, related_name="consist_item"
|
||||||
)
|
)
|
||||||
rolling_stock = models.ForeignKey(RollingStock, on_delete=models.CASCADE)
|
rolling_stock = models.ForeignKey(RollingStock, on_delete=models.CASCADE)
|
||||||
|
load = models.BooleanField(default=False)
|
||||||
order = models.PositiveIntegerField(blank=False, null=False)
|
order = models.PositiveIntegerField(blank=False, null=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -92,10 +93,15 @@ class ConsistItem(models.Model):
|
|||||||
# because the consist is not saved yet and it must be moved
|
# because the consist is not saved yet and it must be moved
|
||||||
# to the admin form validation via InlineFormSet.clean()
|
# to the admin form validation via InlineFormSet.clean()
|
||||||
consist = self.consist
|
consist = self.consist
|
||||||
if rolling_stock.scale != consist.scale:
|
# Scale must match, but allow loads of any scale
|
||||||
|
if rolling_stock.scale != consist.scale and not self.load:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
"The rolling stock and consist must be of the same scale."
|
"The rolling stock and consist must be of the same scale."
|
||||||
)
|
)
|
||||||
|
if self.load and rolling_stock.scale.ratio != consist.scale.ratio:
|
||||||
|
raise ValidationError(
|
||||||
|
"The load and consist must be of the same scale ratio."
|
||||||
|
)
|
||||||
if self.consist.published and not rolling_stock.published:
|
if self.consist.published and not rolling_stock.published:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
"You must unpublish the the consist before using this item."
|
"You must unpublish the the consist before using this item."
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
{{ t.name }}</a>{# new line is required #}
|
{{ t.name }}</a>{# new line is required #}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</p>
|
</p>
|
||||||
|
{% endif %}
|
||||||
{% if not consist.published %}
|
{% if not consist.published %}
|
||||||
<span class="badge text-bg-warning">Unpublished</span> |
|
<span class="badge text-bg-warning">Unpublished</span> |
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<small class="text-body-secondary">Updated {{ consist.updated_time | date:"M d, Y H:i" }}</small>
|
<small class="text-body-secondary">Updated {{ consist.updated_time | date:"M d, Y H:i" }}</small>
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block carousel %}
|
{% block carousel %}
|
||||||
{% if consist.image %}
|
{% if consist.image %}
|
||||||
@@ -26,6 +26,33 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
{% block cards_layout %}
|
||||||
|
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3">
|
||||||
|
{% block cards %}
|
||||||
|
{% for d in data %}
|
||||||
|
{% include "cards/roster.html" %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
<div class="accordion shadow-sm mt-4" id="accordionLoads">
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseLoads" aria-expanded="false" aria-controls="collapseLoads">
|
||||||
|
<i class="bi bi-download"></i> Rolling Stock loaded on freight cars
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapseLoads" class="accordion-collapse collapse" data-bs-parent="#accordionLoads">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3">
|
||||||
|
{% for l in loads %}
|
||||||
|
{% include "cards/roster.html" with d=l %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
{% block pagination %}
|
{% block pagination %}
|
||||||
{% if data.has_other_pages %}
|
{% if data.has_other_pages %}
|
||||||
<nav aria-label="Page navigation">
|
<nav aria-label="Page navigation">
|
||||||
@@ -113,7 +140,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Composition</th>
|
<th scope="row">Composition</th>
|
||||||
<td>{% for t in consist.get_type_count %}{{ t.count }}x {{ t.type }} {{t.category }}{% if not forloop.last %} » {% endif %}{% endfor %}</td>
|
<td>{% for t in consist.get_type_count %}{{ t.count }}x {{ t.type }} {{t.category }}{% if not forloop.last %} » {% endif %}{% endfor %}{% if loads %} | <i class="bi bi-download"></i> {{ loads|length }}x Load{{ loads|pluralize }}{% endif %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -526,7 +526,13 @@ class GetConsist(View):
|
|||||||
RollingStock.objects.get_published(request.user).get(
|
RollingStock.objects.get_published(request.user).get(
|
||||||
uuid=r.rolling_stock_id
|
uuid=r.rolling_stock_id
|
||||||
)
|
)
|
||||||
for r in consist.consist_item.all()
|
for r in consist.consist_item.filter(load=False)
|
||||||
|
)
|
||||||
|
loads = list(
|
||||||
|
RollingStock.objects.get_published(request.user).get(
|
||||||
|
uuid=r.rolling_stock_id
|
||||||
|
)
|
||||||
|
for r in consist.consist_item.filter(load=True)
|
||||||
)
|
)
|
||||||
paginator = Paginator(data, get_items_per_page())
|
paginator = Paginator(data, get_items_per_page())
|
||||||
data = paginator.get_page(page)
|
data = paginator.get_page(page)
|
||||||
@@ -541,6 +547,7 @@ class GetConsist(View):
|
|||||||
"title": consist,
|
"title": consist,
|
||||||
"consist": consist,
|
"consist": consist,
|
||||||
"data": data,
|
"data": data,
|
||||||
|
"loads": loads,
|
||||||
"page_range": page_range,
|
"page_range": page_range,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from ram.utils import git_suffix
|
from ram.utils import git_suffix
|
||||||
|
|
||||||
__version__ = "0.19.5"
|
__version__ = "0.19.7"
|
||||||
__version__ += git_suffix(__file__)
|
__version__ += git_suffix(__file__)
|
||||||
|
|||||||
@@ -206,6 +206,9 @@ ROLLING_STOCK_TYPES = [
|
|||||||
|
|
||||||
FEATURED_ITEMS_MAX = 6
|
FEATURED_ITEMS_MAX = 6
|
||||||
|
|
||||||
|
# If True, use X-Accel-Redirect (Nginx)
|
||||||
|
USE_X_ACCEL_REDIRECT = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from ram.local_settings import *
|
from ram.local_settings import *
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|||||||
@@ -21,17 +21,22 @@ from django.conf.urls.static import static
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
|
|
||||||
from ram.views import UploadImage
|
from ram.views import UploadImage, DownloadFile
|
||||||
from portal.views import Render404
|
from portal.views import Render404
|
||||||
|
|
||||||
handler404 = Render404.as_view()
|
handler404 = Render404.as_view()
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", lambda r: redirect("portal/")),
|
path("", lambda r: redirect("portal/")),
|
||||||
|
path("admin/", admin.site.urls),
|
||||||
path("tinymce/", include("tinymce.urls")),
|
path("tinymce/", include("tinymce.urls")),
|
||||||
path("tinymce/upload_image", UploadImage.as_view(), name="upload_image"),
|
path("tinymce/upload_image", UploadImage.as_view(), name="upload_image"),
|
||||||
|
path(
|
||||||
|
"media/files/<path:filename>",
|
||||||
|
DownloadFile.as_view(),
|
||||||
|
name="download_file",
|
||||||
|
),
|
||||||
path("portal/", include("portal.urls")),
|
path("portal/", include("portal.urls")),
|
||||||
path("admin/", admin.site.urls),
|
|
||||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
||||||
# Enable the "/dcc" routing only if the "driver" app is active
|
# Enable the "/dcc" routing only if the "driver" app is active
|
||||||
@@ -55,6 +60,7 @@ if settings.DEBUG:
|
|||||||
if settings.REST_ENABLED:
|
if settings.REST_ENABLED:
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
from rest_framework.schemas import get_schema_view
|
from rest_framework.schemas import get_schema_view
|
||||||
|
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
path(
|
path(
|
||||||
"swagger/",
|
"swagger/",
|
||||||
|
|||||||
@@ -5,19 +5,26 @@ import posixpath
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from PIL import Image, UnidentifiedImageError
|
from PIL import Image, UnidentifiedImageError
|
||||||
|
|
||||||
from django.views import View
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import (
|
from django.http import (
|
||||||
|
Http404,
|
||||||
|
HttpResponse,
|
||||||
HttpResponseBadRequest,
|
HttpResponseBadRequest,
|
||||||
HttpResponseForbidden,
|
HttpResponseForbidden,
|
||||||
|
FileResponse,
|
||||||
JsonResponse,
|
JsonResponse,
|
||||||
)
|
)
|
||||||
|
from django.views import View
|
||||||
from django.utils.text import slugify as slugify
|
from django.utils.text import slugify as slugify
|
||||||
|
from django.utils.encoding import smart_str
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
||||||
from rest_framework.pagination import LimitOffsetPagination
|
from rest_framework.pagination import LimitOffsetPagination
|
||||||
|
|
||||||
|
from ram.models import PrivateDocument
|
||||||
|
|
||||||
|
|
||||||
class CustomLimitOffsetPagination(LimitOffsetPagination):
|
class CustomLimitOffsetPagination(LimitOffsetPagination):
|
||||||
default_limit = 10
|
default_limit = 10
|
||||||
@@ -67,3 +74,43 @@ class UploadImage(View):
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadFile(View):
|
||||||
|
def get(self, request, filename, disposition="inline"):
|
||||||
|
# Clean up the filename to prevent directory traversal attacks
|
||||||
|
filename = os.path.basename(filename)
|
||||||
|
|
||||||
|
# Find a document where the stored file name matches
|
||||||
|
# Find all models inheriting from PublishableFile
|
||||||
|
for model in apps.get_models():
|
||||||
|
if issubclass(model, PrivateDocument) and not model._meta.abstract:
|
||||||
|
try:
|
||||||
|
doc = model.objects.get(file__endswith=filename)
|
||||||
|
if doc.private and not request.user.is_staff:
|
||||||
|
break
|
||||||
|
|
||||||
|
file = doc.file
|
||||||
|
if not os.path.exists(file.path):
|
||||||
|
break
|
||||||
|
|
||||||
|
if getattr(settings, "USE_X_ACCEL_REDIRECT", False):
|
||||||
|
response = HttpResponse()
|
||||||
|
response["Content-Type"] = ""
|
||||||
|
response["X-Accel-Redirect"] = file.url
|
||||||
|
else:
|
||||||
|
response = FileResponse(
|
||||||
|
open(file.path, "rb"), as_attachment=True
|
||||||
|
)
|
||||||
|
|
||||||
|
response["Content-Disposition"] = (
|
||||||
|
'{}; filename="{}"'.format(
|
||||||
|
disposition,
|
||||||
|
smart_str(os.path.basename(file.path))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
except model.DoesNotExist:
|
||||||
|
continue
|
||||||
|
|
||||||
|
raise Http404("File not found")
|
||||||
|
|||||||
Reference in New Issue
Block a user