15 Commits

Author SHA1 Message Date
575c938205 Hotfix for document filename 2022-08-22 18:23:38 +02:00
2af772a722 Black'ed 2022-08-22 17:13:10 +02:00
f580bcffc5 Documents section in admin 2022-08-22 17:12:22 +02:00
6accb66006 Enable search by sku 2022-08-21 16:53:18 +02:00
f56accb4ff Use lead unit thumbnail if not provided in consist 2022-07-23 23:45:11 +02:00
5a7b7fd79e Update README.md 2022-07-23 22:55:58 +02:00
dcdad71b1b Update README.md 2022-07-23 22:54:46 +02:00
321ae1065e Update README.md 2022-07-23 22:51:24 +02:00
e8efa5d87a Update README.md 2022-07-23 22:50:58 +02:00
97254b302c Fix a typo 2022-07-23 16:15:56 +02:00
b8aa34ce1d Add modal for pictures 2022-07-23 11:58:17 +02:00
e023edbeeb Add support for dark mode 2022-07-22 22:39:02 +02:00
c9c8976c60 UX improvements 2022-07-21 23:01:34 +02:00
5765472704 Fix to scale abbr 2022-07-21 22:11:17 +02:00
4fb9d1903f Reduce elided_page_range 2022-07-20 21:51:18 +02:00
11 changed files with 123 additions and 50 deletions

View File

@@ -2,8 +2,7 @@
[![Django CI](https://github.com/daniviga/django-rma/actions/workflows/django.yml/badge.svg)](https://github.com/daniviga/django-rma/actions/workflows/django.yml) [![Django CI](https://github.com/daniviga/django-rma/actions/workflows/django.yml/badge.svg)](https://github.com/daniviga/django-rma/actions/workflows/django.yml)
![image](https://user-images.githubusercontent.com/1818657/175789825-9a03f0ff-a95e-42a2-9611-e14d2817e22f.png) ![Screenshot 2022-07-23 at 22-40-17 Railroad Assets Manager](https://user-images.githubusercontent.com/1818657/180622177-a4bba00e-47da-42b3-a7f6-b24773e69936.png)
A `jff` (just for fun) project that aims to create a A `jff` (just for fun) project that aims to create a
model railroad assets manager that allows to: model railroad assets manager that allows to:
@@ -26,6 +25,8 @@ This project probably doesn't match you needs nor expectations. Be aware.
Your model train may also catch fire while using this software. Your model train may also catch fire while using this software.
Check out [my own instance](https://daniele.mynarrowgauge.org).
## Components ## Components
Project is based on the following technologies and components: Project is based on the following technologies and components:
@@ -138,14 +139,18 @@ To be continued ...
## Screenshots ## Screenshots
### Frontend ### Frontend
![Screenshot 2022-07-23 at 22-41-44 Railroad Assets Manager](https://user-images.githubusercontent.com/1818657/180622406-760774a9-f028-44fc-b332-fa74e43307df.png)
---
![Screenshot 2022-07-23 at 22-44-35 Railroad Assets Manager](https://user-images.githubusercontent.com/1818657/180622342-40586d75-239a-400c-93a1-1cb9583a7d17.png)
---
![Screenshot 2022-07-23 at 22-44-46 Railroad Assets Manager](https://user-images.githubusercontent.com/1818657/180622321-1ab76440-9c6e-4667-9247-dbbcf6c6055c.png)
#### Dark mode
![Screenshot 2022-07-23 at 22-53-43 Railroad Assets Manager](https://user-images.githubusercontent.com/1818657/180622629-65d81eaf-cca4-4f44-b39b-3b0077b43a34.png)
![image](https://user-images.githubusercontent.com/1818657/175789897-9ec4a9bb-9c65-48ef-9b57-ae94e094e6a7.png)
--- ---
![image](https://user-images.githubusercontent.com/1818657/175789901-ef50acd7-8c05-4788-92a2-1bb1280d598c.png)
---
![image](https://user-images.githubusercontent.com/1818657/175790004-18926d23-28f9-45bb-b279-6c26575ae3a5.png)
---
![image](https://user-images.githubusercontent.com/1818657/175790008-62eea2cc-1c41-42df-9026-4cf6e8ef712c.png)
### Backoffice ### Backoffice
@@ -158,8 +163,7 @@ To be continued ...
### Rest API ### Rest API
![image](https://user-images.githubusercontent.com/1818657/175790064-23ec038e-e8bf-4c39-964c-3118e4295b59.png) ![image](https://user-images.githubusercontent.com/1818657/180622471-ade06c84-c73b-41d5-a2a7-02a95b2ffc02.png)

View File

@@ -8,12 +8,14 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<meta name="description" content="{{ site_conf.about}}"> <meta name="description" content="{{ site_conf.about}}">
<meta name="author" content="{{ site_conf.site_author }}"> <meta name="author" content="{{ site_conf.site_author }}">
<meta name="generator" content="Django Framework"> <meta name="generator" content="Django Framework">
<title>{{ site_conf.site_name }}</title> <title>{{ site_conf.site_name }}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/css/bootstrap-nightshade.min.css" rel="stylesheet">
<link href="{% static "css/main.css" %}" rel="stylesheet"> <link href="{% static "css/main.css" %}" rel="stylesheet">
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<style> <style>
.bd-placeholder-img { .bd-placeholder-img {
font-size: 1.125rem; font-size: 1.125rem;
@@ -22,12 +24,15 @@
-moz-user-select: none; -moz-user-select: none;
user-select: none; user-select: none;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.bd-placeholder-img-lg { .bd-placeholder-img-lg {
font-size: 3.5rem; font-size: 3.5rem;
} }
} }
.d-light-inline { display: inline !important; }
.d-dark-inline { display: none !important; }
html.dark .d-light-inline { display: none !important; }
html.dark .d-dark-inline { display: inline !important; }
</style> </style>
</head> </head>
<body> <body>
@@ -40,7 +45,10 @@
</svg> </svg>
<strong>{{ site_conf.site_name }}</strong> <strong>{{ site_conf.site_name }}</strong>
</a> </a>
{% include 'includes/login.html' %} <div class="btn-group" role="group" aria-label="Basic example">
{% include 'includes/login.html' %}
<a id="darkmode-button" class="btn btn-sm btn-outline-dark"><i class="fa fa-moon-o fa-fw d-none d-light-inline" title="Switch to dark mode"></i><i class="fa fa-sun-o fa-fw d-none d-dark-inline" title="Switch to light mode"></i></a>
</div>
</div> </div>
</div> </div>
</header> </header>
@@ -132,7 +140,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Scale</th> <th scope="row">Scale</th>
<td><a href="{% url 'filtered' _filter="scale" search=r.scale %}"><abbr title="{{ r.scale.ratio }} - {{ r.scale.gauge }}">{{ r.scale }}</abbr></a></td> <td><a href="{% url 'filtered' _filter="scale" search=r.scale %}"><abbr title="{{ r.scale.ratio }} - {{ r.scale.tracks }}">{{ r.scale }}</abbr></a></td>
</tr> </tr>
<tr> <tr>
<th scope="row">SKU</th> <th scope="row">SKU</th>
@@ -159,13 +167,10 @@
</tbody> </tbody>
</table> </table>
{% endif %} {% endif %}
<div class="btn-group mb-4"> <div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{{r.get_absolute_url}}">Show all data</a> <a class="btn btn-sm btn-outline-primary" href="{{r.get_absolute_url}}">Show all data</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:roster_rollingstock_change' r.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:roster_rollingstock_change' r.pk %}">Edit</a>{% endif %}
</div> </div>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">Updated {{ r.updated_time | date:"M d, Y H:m" }}</small>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -179,6 +184,12 @@
</main> </main>
{% include 'includes/footer.html' %} {% include 'includes/footer.html' %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/masonry-layout@4.2.2/dist/masonry.pkgd.min.js" integrity="sha384-GNFwBvfVxBkLMJpYMOABq3c+d3KnQxudP/mGPkzpZSTYykLBNsZEnG2D9G/X/+7D" crossorigin="anonymous" async></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/js/darkmode.min.js"></script>
<!-- script src="https://cdn.jsdelivr.net/npm/masonry-layout@4.2.2/dist/masonry.pkgd.min.js" integrity="sha384-GNFwBvfVxBkLMJpYMOABq3c+d3KnQxudP/mGPkzpZSTYykLBNsZEnG2D9G/X/+7D" crossorigin="anonymous" async></script -->
<script>
document.querySelector("#darkmode-button").onclick = function(e){
darkmode.toggleDarkMode();
}
</script>
</body> </body>
</html> </html>

View File

@@ -45,7 +45,7 @@
{% endif %} {% endif %}
</tbody> </tbody>
</table> </table>
<div class="btn-group mb-4"> <div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="company" search=c %}">Show all rolling stock</a> <a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="company" search=c %}">Show all rolling stock</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_company_change' c.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_company_change' c.pk %}">Edit</a>{% endif %}
</div> </div>

View File

@@ -2,13 +2,14 @@
{% load markdown %} {% load markdown %}
{% block header %} {% block header %}
<h1 class="fw-light">{{ consist }}</h1> <h1 class="fw-light">{{ consist }}</h1>
{% if consist.tags.all %} {% if consist.tags.all %}
<p><small>Tags:</small> <p><small>Tags:</small>
{% for t in consist.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary"> {% for t in consist.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
{{ t.name }}</a>{# new line is required #} {{ t.name }}</a>{# new line is required #}
{% endfor %} {% endfor %}
</p> </p>
<small class="text-muted">Updated {{ consist.updated_time | date:"M d, Y H:i" }}</small>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block cards %} {% block cards %}
@@ -63,7 +64,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Scale</th> <th scope="row">Scale</th>
<td><a href="{% url 'filtered' _filter="scale" search=r.rolling_stock.scale %}"><abbr title="{{ r.rolling_stock.scale.ratio }} - {{ r.rolling_stock.scale.gauge }}">{{ r.rolling_stock.scale }}</abbr></a></td> <td><a href="{% url 'filtered' _filter="scale" search=r.rolling_stock.scale %}"><abbr title="{{ r.rolling_stock.scale.ratio }} - {{ r.rolling_stock.scale.tracks }}">{{ r.rolling_stock.scale }}</abbr></a></td>
</tr> </tr>
<tr> <tr>
<th scope="row">SKU</th> <th scope="row">SKU</th>
@@ -90,13 +91,10 @@
</tbody> </tbody>
</table> </table>
{% endif %} {% endif %}
<div class="btn-group mb-4"> <div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{{r.rolling_stock.get_absolute_url}}">Show all data</a> <a class="btn btn-sm btn-outline-primary" href="{{r.rolling_stock.get_absolute_url}}">Show all data</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:roster_rollingstock_change' r.rolling_stock.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:roster_rollingstock_change' r.rolling_stock.pk %}">Edit</a>{% endif %}
</div> </div>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">Updated {{ r.rolling_stock.updated_time | date:"M d, Y H:m" }}</small>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -8,7 +8,17 @@
{% for c in consist %} {% for c in consist %}
<div class="col"> <div class="col">
<div class="card shadow-sm"> <div class="card shadow-sm">
{% if c.image %}<a href="{{ c.get_absolute_url }}"><img src="{{ c.image.url }}" alt="Card image cap"></a>{% endif %} <a href="{{ c.get_absolute_url }}">
{% if c.image %}
<img src="{{ c.image.url }}" alt="Card image cap">
{% else %}
{% with c.consist_item.first.rolling_stock as r %}
{% for i in r.image.all %}
{% if i.is_thumbnail %}<img src="{{ i.image.url }}" alt="Card image cap">{% endif %}
{% endfor %}
{% endwith %}
{% endif %}
</a>
<div class="card-body"> <div class="card-body">
<p class="card-text" style="position: relative;"> <p class="card-text" style="position: relative;">
<strong>{{ c }}</strong> <strong>{{ c }}</strong>
@@ -48,13 +58,10 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="btn-group mb-4"> <div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{{ c.get_absolute_url }}">Show all data</a> <a class="btn btn-sm btn-outline-primary" href="{{ c.get_absolute_url }}">Show all data</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:consist_consist_change' c.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:consist_consist_change' c.pk %}">Edit</a>{% endif %}
</div> </div>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">Updated {{ c.updated_time | date:"M d, Y H:m" }}</small>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -10,11 +10,29 @@
{% endfor %} {% endfor %}
</p> </p>
{% endif %} {% endif %}
<small class="text-muted">Updated {{ rolling_stock.updated_time | date:"M d, Y H:i" }}</small>
{% endblock %} {% endblock %}
{% block cards %} {% block cards %}
{% for t in rolling_stock.image.all %} {% for t in rolling_stock.image.all %}
<div class="col"> <div class="col">
<img class="img-thumbnail" src="{{ t.image.url }}" alt="Rolling stock image"> <a href="" data-bs-toggle="modal" data-bs-target="#pictureModal{{ forloop.counter }}"><img class="img-thumbnail" src="{{ t.image.url }}" alt="Rolling stock image"></a>
</div>
<!-- Modal -->
<div class="modal fade" id="pictureModal{{ forloop.counter }}" tabindex="-1" aria-labelledby="pictureModalLabel{{ forloop.counter }}" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="pictureModalLabel{{ forloop.counter }}">{{ rolling_stock }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center">
<img class="rounded img-fluid" src="{{ t.image.url }}" alt="Rolling stock image">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div> </div>
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}
@@ -76,7 +94,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Scale</th> <th scope="row">Scale</th>
<td><a href="{% url 'filtered' _filter="scale" search=rolling_stock.scale %}"><abbr title="{{ rolling_stock.scale.ratio }} - {{ rolling_stock.scale.gauge }}">{{ rolling_stock.scale }}</abbr></a></td> <td><a href="{% url 'filtered' _filter="scale" search=rolling_stock.scale %}"><abbr title="{{ rolling_stock.scale.ratio }} - {{ rolling_stock.scale.tracks }}">{{ rolling_stock.scale }}</abbr></a></td>
</tr> </tr>
<tr> <tr>
<th scope="row">SKU</th> <th scope="row">SKU</th>
@@ -118,7 +136,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Scale</th> <th scope="row">Scale</th>
<td><abbr title="{{ rolling_stock.scale.ratio }} - {{ rolling_stock.scale.gauge }}">{{ rolling_stock.scale }}</abbr></td> <td><abbr title="{{ rolling_stock.scale.ratio }} - {{ rolling_stock.scale.tracks }}">{{ rolling_stock.scale }}</abbr></td>
</tr> </tr>
<tr> <tr>
<th scope="row">SKU</th> <th scope="row">SKU</th>
@@ -166,7 +184,7 @@
<tbody> <tbody>
<tr> <tr>
<th width="35%" scope="row">Class</th> <th width="35%" scope="row">Class</th>
<td>{{ rolling_stock.rolling_class }}</td> <td>{{ rolling_stock.rolling_class.identifier }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Type</th> <th scope="row">Type</th>

View File

@@ -35,7 +35,7 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="btn-group mb-4"> <div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="scale" search=s %}">Show all rolling stock</a> <a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="scale" search=s %}">Show all rolling stock</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_scale_change' s.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_scale_change' s.pk %}">Edit</a>{% endif %}
</div> </div>

View File

@@ -38,7 +38,9 @@ class GetHome(View):
paginator = Paginator(rolling_stock, site_conf.items_per_page) paginator = Paginator(rolling_stock, site_conf.items_per_page)
rolling_stock = paginator.get_page(page) rolling_stock = paginator.get_page(page)
page_range = paginator.get_elided_page_range(rolling_stock.number) page_range = paginator.get_elided_page_range(
rolling_stock.number, on_each_side=2, on_ends=1
)
return render( return render(
request, request,
@@ -59,6 +61,7 @@ class GetHomeFiltered(View):
| Q(rolling_class__description__icontains=s) | Q(rolling_class__description__icontains=s)
| Q(rolling_class__type__type__icontains=s) | Q(rolling_class__type__type__icontains=s)
| Q(road_number__icontains=s) | Q(road_number__icontains=s)
| Q(sku=s)
| Q(rolling_class__company__name__icontains=s) | Q(rolling_class__company__name__icontains=s)
| Q(rolling_class__company__country__icontains=s) | Q(rolling_class__company__country__icontains=s)
| Q(manufacturer__name__icontains=s) | Q(manufacturer__name__icontains=s)
@@ -86,7 +89,9 @@ class GetHomeFiltered(View):
paginator = Paginator(rolling_stock, site_conf.items_per_page) paginator = Paginator(rolling_stock, site_conf.items_per_page)
rolling_stock = paginator.get_page(page) rolling_stock = paginator.get_page(page)
page_range = paginator.get_elided_page_range(rolling_stock.number) page_range = paginator.get_elided_page_range(
rolling_stock.number, on_each_side=2, on_ends=1
)
return rolling_stock, matches, page_range return rolling_stock, matches, page_range
@@ -166,7 +171,9 @@ class Consists(View):
paginator = Paginator(consist, site_conf.items_per_page) paginator = Paginator(consist, site_conf.items_per_page)
consist = paginator.get_page(page) consist = paginator.get_page(page)
page_range = paginator.get_elided_page_range(consist.number) page_range = paginator.get_elided_page_range(
consist.number, on_each_side=2, on_ends=1
)
return render( return render(
request, request,
@@ -186,7 +193,9 @@ class GetConsist(View):
paginator = Paginator(rolling_stock, site_conf.items_per_page) paginator = Paginator(rolling_stock, site_conf.items_per_page)
rolling_stock = paginator.get_page(page) rolling_stock = paginator.get_page(page)
page_range = paginator.get_elided_page_range(rolling_stock.number) page_range = paginator.get_elided_page_range(
rolling_stock.number, on_each_side=2, on_ends=1
)
return render( return render(
request, request,
@@ -206,7 +215,9 @@ class Companies(View):
paginator = Paginator(company, site_conf.items_per_page) paginator = Paginator(company, site_conf.items_per_page)
company = paginator.get_page(page) company = paginator.get_page(page)
page_range = paginator.get_elided_page_range(company.number) page_range = paginator.get_elided_page_range(
company.number, on_each_side=2, on_ends=1
)
return render( return render(
request, request,
@@ -222,7 +233,9 @@ class Scales(View):
paginator = Paginator(scale, site_conf.items_per_page) paginator = Paginator(scale, site_conf.items_per_page)
scale = paginator.get_page(page) scale = paginator.get_page(page)
page_range = paginator.get_elided_page_range(scale.number) page_range = paginator.get_elided_page_range(
scale.number, on_each_side=2, on_ends=1
)
return render( return render(
request, request,

View File

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

View File

@@ -46,6 +46,22 @@ class RollingStockPropertyInline(admin.TabularInline):
extra = 0 extra = 0
@admin.register(RollingStockDocument)
class RollingStockDocumentAdmin(admin.ModelAdmin):
list_display = (
"__str__",
"rolling_stock",
"description",
"download",
)
search_fields = (
"rolling_stock__rolling_class__identifier",
"rolling_stock__sku",
"description",
"file",
)
@admin.register(RollingStock) @admin.register(RollingStock)
class RollingStockAdmin(admin.ModelAdmin): class RollingStockAdmin(admin.ModelAdmin):
inlines = ( inlines = (

View File

@@ -4,6 +4,7 @@ from uuid import uuid4
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.safestring import mark_safe
# from django.core.files.storage import FileSystemStorage # from django.core.files.storage import FileSystemStorage
@@ -141,7 +142,12 @@ class RollingStockDocument(models.Model):
return "{0}".format(os.path.basename(self.file.name)) return "{0}".format(os.path.basename(self.file.name))
def filename(self): def filename(self):
return os.path.basename(self.file.name) 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):