Merge pull request #7 from daniviga/search

Add search bar and refactor templates
This commit is contained in:
2022-04-19 14:52:55 +02:00
committed by GitHub
22 changed files with 619 additions and 573 deletions

View File

@@ -26,9 +26,7 @@ class Consist(models.Model):
class ConsistItem(models.Model): class ConsistItem(models.Model):
consist = models.ForeignKey( consist = models.ForeignKey(
Consist, Consist, on_delete=models.CASCADE, related_name="consist_item"
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)
order = models.PositiveIntegerField(default=0, blank=False, null=False) order = models.PositiveIntegerField(default=0, blank=False, null=False)

View File

@@ -6,23 +6,16 @@ from solo.models import SingletonModel
class DriverConfiguration(SingletonModel): class DriverConfiguration(SingletonModel):
remote_host = models.GenericIPAddressField( remote_host = models.GenericIPAddressField(
protocol="IPv4", protocol="IPv4", default="192.168.4.1"
default="192.168.4.1"
) )
remote_port = models.SmallIntegerField(default=2560) remote_port = models.SmallIntegerField(default=2560)
timeout = models.SmallIntegerField(default=250) timeout = models.SmallIntegerField(default=250)
network = models.GenericIPAddressField( network = models.GenericIPAddressField(
protocol="IPv4", protocol="IPv4", default="192.168.4.0", blank=True, null=True
default="192.168.4.0",
blank=True,
null=True
) )
subnet_mask = models.GenericIPAddressField( subnet_mask = models.GenericIPAddressField(
protocol="IPv4", protocol="IPv4", default="255.255.255.0", blank=True, null=True
default="255.255.255.0",
blank=True,
null=True
) )
def __str__(self): def __str__(self):
@@ -31,8 +24,7 @@ class DriverConfiguration(SingletonModel):
def clean(self, *args, **kwargs): def clean(self, *args, **kwargs):
if self.network: if self.network:
try: try:
IPv4Network( IPv4Network("{0}/{1}".format(self.network, self.subnet_mask))
"{0}/{1}".format(self.network, self.subnet_mask))
except ValueError as e: except ValueError as e:
raise ValidationError(e) raise ValidationError(e)
super().clean(*args, **kwargs) super().clean(*args, **kwargs)

View File

@@ -7,7 +7,7 @@ from rest_framework.response import Response
from rest_framework.permissions import ( from rest_framework.permissions import (
IsAuthenticated, IsAuthenticated,
BasePermission, BasePermission,
SAFE_METHODS SAFE_METHODS,
) )
from ram.parsers import PlainTextParser from ram.parsers import PlainTextParser
@@ -40,16 +40,15 @@ class Firewall(BasePermission):
if not config.network: if not config.network:
return request.method in SAFE_METHODS return request.method in SAFE_METHODS
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
if x_forwarded_for: if x_forwarded_for:
ip = IPv4Address(x_forwarded_for.split(',')[0]) ip = IPv4Address(x_forwarded_for.split(",")[0])
else: else:
ip = IPv4Address(request.META.get("REMOTE_ADDR")) ip = IPv4Address(request.META.get("REMOTE_ADDR"))
network = IPv4Network("{0}/{1}".format( network = IPv4Network(
config.network, "{0}/{1}".format(config.network, config.subnet_mask)
config.subnet_mask )
))
# accept IP configured is settings or localhost # accept IP configured is settings or localhost
if ip in network or ip in IPv4Network("127.0.0.0/8"): if ip in network or ip in IPv4Network("127.0.0.0/8"):
@@ -101,6 +100,7 @@ class Function(APIView):
""" """
Send "Function" commands to a valid DCC address Send "Function" commands to a valid DCC address
""" """
permission_classes = [IsAuthenticated | Firewall] permission_classes = [IsAuthenticated | Firewall]
def put(self, request, address): def put(self, request, address):
@@ -117,6 +117,7 @@ class Cab(APIView):
""" """
Send "Cab" commands to a valid DCC address Send "Cab" commands to a valid DCC address
""" """
permission_classes = [IsAuthenticated | Firewall] permission_classes = [IsAuthenticated | Firewall]
def put(self, request, address): def put(self, request, address):
@@ -132,6 +133,7 @@ class Infra(APIView):
""" """
Send "Infra" commands to a valid DCC address Send "Infra" commands to a valid DCC address
""" """
permission_classes = [IsAuthenticated | Firewall] permission_classes = [IsAuthenticated | Firewall]
def put(self, request): def put(self, request):
@@ -147,6 +149,7 @@ class Emergency(APIView):
""" """
Send an "Emergency" stop, no matter the HTTP method used Send an "Emergency" stop, no matter the HTTP method used
""" """
permission_classes = [IsAuthenticated | Firewall] permission_classes = [IsAuthenticated | Firewall]
def put(self, request): def put(self, request):

View File

@@ -2,5 +2,5 @@ from django.apps import AppConfig
class PortalConfig(AppConfig): class PortalConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = "django.db.models.BigAutoField"
name = 'portal' name = "portal"

View File

@@ -7,14 +7,14 @@ from solo.models import SingletonModel
class SiteConfiguration(SingletonModel): class SiteConfiguration(SingletonModel):
site_name = models.CharField( site_name = models.CharField(
max_length=256, max_length=256, default="Railroad Assets Manager"
default="Railroad Assets Manager") )
site_author = models.CharField(max_length=256, blank=True) site_author = models.CharField(max_length=256, blank=True)
about = models.TextField(blank=True) about = models.TextField(blank=True)
items_per_page = models.CharField( items_per_page = models.CharField(
max_length=2, choices=[ max_length=2,
(str(x * 3), str(x * 3)) for x in range(2, 11)], choices=[(str(x * 3), str(x * 3)) for x in range(2, 11)],
default='6' default="6",
) )
footer = models.TextField(blank=True) footer = models.TextField(blank=True)
footer_extended = models.TextField(blank=True) footer_extended = models.TextField(blank=True)

View File

@@ -0,0 +1,166 @@
{% load static %}
{% load solo_tags %}
{% load markdown %}
{% get_solo 'portal.SiteConfiguration' as site_conf %}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="{{ site_conf.site_author }}">
<meta name="generator" content="Django Framework">
<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="{% static "css/main.css" %}" rel="stylesheet">
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
</head>
<body>
<header>
<div class="navbar navbar-dark bg-dark shadow-sm">
<div class="container">
<a href="/" class="navbar-brand d-flex align-items-center">
<svg class="me-2" width="26" height="16" enable-background="new 0 0 26 26" version="1" viewBox="0 0 26 16" xmlns="http://www.w3.org/2000/svg">
<path d="m2.8125 0.0010991a1.0001 1.0001 0 0 0-0.8125 1c0 0.55455-0.44545 1-1 1a1.0001 1.0001 0 0 0-1 1v10a1.0001 1.0001 0 0 0 1 1c0.55455 0 1 0.44546 1 1a1.0001 1.0001 0 0 0 1 1h20a1.0001 1.0001 0 0 0 1-1c0-0.55454 0.44546-1 1-1a1.0001 1.0001 0 0 0 1-1v-10a1.0001 1.0001 0 0 0-1-1c-0.55454 0-1-0.44545-1-1a1.0001 1.0001 0 0 0-1-1h-20a1.0001 1.0001 0 0 0-0.09375 0 1.0001 1.0001 0 0 0-0.09375 0zm0.78125 2h14.406v1h2v-1h2.4062c0.30628 0.76906 0.82469 1.2875 1.5938 1.5938v8.8125c-0.76906 0.30628-1.2875 0.82469-1.5938 1.5938h-2.4062v-1h-2v1h-14.406c-0.30628-0.76906-0.82469-1.2875-1.5938-1.5938v-8.8125c0.76906-0.30628 1.2875-0.82469 1.5938-1.5938zm14.406 2v2h2v-2zm0 3v2h2v-2zm0 3v2h2v-2z" enable-background="accumulate" fill="#fff" overflow="visible" stroke-width="2" style="text-indent:0;text-transform:none"/>
</svg>
<strong>{{ site_conf.site_name }}</strong>
</a>
{% include 'includes/login.html' %}
</div>
</div>
</header>
<main>
<div class="py-2 container">
<nav class="navbar navbar-expand-lg navbar-light">
<div class="container-fluid g-0">
<a class="navbar-brand" href="/">Home</a>
{% include 'includes/search.html' %}
</div>
</div>
</nav>
</div>
<section class="py-4 text-center container">
<div class="row">
<div class="mx-auto">
{% block header %}{% endblock %}
</div>
</div>
</section>
<div class="album py-5 bg-light">
<div class="container">
<a id="rolling-stock"></a>
<div data-masonry='{"percentPosition": true }' class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
{% block cards %}
{% for r in rolling_stock %}
<div class="col">
<div class="card shadow-sm">
{% for i in r.image.all %}
{% if i.is_thumbnail %}<a href="/portal/{{ r.uuid }}"><img src="{{ i.image.url }}" alt="Card image cap"></a>{% endif %}
{% endfor %}
<div class="card-body">
<p class="card-text"><strong>{{ r }}</strong></p>
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Data</th>
</tr>
</thead>
<tbody>
<tr>
<th width="35%" scope="row">Type</th>
<td>{{ r.rolling_class.type }}</td>
</tr>
<tr>
<th scope="row">Company</th>
<td><abbr title="{{ r.rolling_class.company.extended_name }}">{{ r.rolling_class.company }}</abbr></td>
</tr>
<tr>
<th scope="row">Class</th>
<td>{{ r.rolling_class.identifier }}</td>
</tr>
<tr>
<th scope="row">Road number</th>
<td>{{ r.road_number }}</td>
</tr>
<tr>
<th scope="row">Era</th>
<td>{{ r.era }}</td>
</tr>
<tr>
<th width="35%" scope="row">Manufacturer</th>
<td>{% if r.manufacturer.website %}<a href="{{ r.manufacturer.website }}">{% endif %}{{ r.manufacturer }}{% if r.manufacturer.website %}</a>{% endif %}</th>
</tr>
<tr>
<th scope="row">Scale</th>
<td><abbr title="{{ r.scale.ratio }} - {{ r.scale.gauge }}">{{ r.scale }}</abbr></td>
</tr>
<tr>
<th scope="row">SKU</th>
<td>{{ r.sku }}</td>
</tr>
</tbody>
</table>
{% if r.decoder %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">DCC data</th>
</tr>
</thead>
<tbody>
<tr>
<th width="35%" scope="row">Decoder</th>
<td>{{ r.decoder }}</td>
</tr>
<tr>
<th scope="row">Address</th>
<td>{{ r.address }}</td>
</tr>
</tbody>
</table>
{% endif %}
<div class="btn-group mb-4">
<a class="btn btn-sm btn-outline-primary" href="/portal/{{ r.uuid }}">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 %}
</div>
{% if r.tags.all %}
<p class="card-text"><small>Tags:</small>
{% for t in r.tags.all %}<span class="badge bg-primary">
{{ t.name }}</span>{# new line is required #}
{% endfor %}
</p>
{% endif %}
<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>
{% endfor %}
{% endblock %}
</div>
</div>
<div class="container">{% block pagination %}{% endblock %}</div>
</div>
{% block extra_content %}{% endblock %}
</main>
{% 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/masonry-layout@4.2.2/dist/masonry.pkgd.min.js" integrity="sha384-GNFwBvfVxBkLMJpYMOABq3c+d3KnQxudP/mGPkzpZSTYykLBNsZEnG2D9G/X/+7D" crossorigin="anonymous" async></script>
</body>
</html>

View File

@@ -1,20 +0,0 @@
{% load markdown %}
<footer class="text-muted py-4">
<div class="container">
<p class="float-end mb-1">
<a href="#">Back to top</a>
</p>
<div id="footer" class="mb-1">
<p>&copy; {% now "Y" %}</p> {{ site_conf.footer | markdown | safe }}
</div>
<div id="footer_extended" class="mb-0">
{{ site_conf.footer_extended | markdown | safe }}
</div>
</div>
{% if site_conf.show_version %}
<div class="container">
<p class="small text-muted">Version: {{ site_conf.version }}</p>
</div>
{% endif %}
</footer>

View File

@@ -1,175 +1,12 @@
{% load static %} {% extends "base.html" %}
{% load solo_tags %}
{% load markdown %} {% load markdown %}
{% get_solo 'portal.SiteConfiguration' as site_conf %}
<!doctype html> {% block header %}
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
<meta name="generator" content="Hugo 0.88.1">
<title>{{ site_conf.site_name }}</title>
<link rel="canonical" href="https://getbootstrap.com/docs/5.1/examples/album/">
<!-- Bootstrap core CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="{% static "css/main.css" %}" rel="stylesheet">
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
</head>
<body>
<header>
<div class="navbar navbar-dark bg-dark shadow-sm">
<div class="container">
<a href="/" class="navbar-brand d-flex align-items-center">
<svg class="me-2" width="26" height="16" enable-background="new 0 0 26 26" version="1" viewBox="0 0 26 16" xmlns="http://www.w3.org/2000/svg">
<path d="m2.8125 0.0010991a1.0001 1.0001 0 0 0-0.8125 1c0 0.55455-0.44545 1-1 1a1.0001 1.0001 0 0 0-1 1v10a1.0001 1.0001 0 0 0 1 1c0.55455 0 1 0.44546 1 1a1.0001 1.0001 0 0 0 1 1h20a1.0001 1.0001 0 0 0 1-1c0-0.55454 0.44546-1 1-1a1.0001 1.0001 0 0 0 1-1v-10a1.0001 1.0001 0 0 0-1-1c-0.55454 0-1-0.44545-1-1a1.0001 1.0001 0 0 0-1-1h-20a1.0001 1.0001 0 0 0-0.09375 0 1.0001 1.0001 0 0 0-0.09375 0zm0.78125 2h14.406v1h2v-1h2.4062c0.30628 0.76906 0.82469 1.2875 1.5938 1.5938v8.8125c-0.76906 0.30628-1.2875 0.82469-1.5938 1.5938h-2.4062v-1h-2v1h-14.406c-0.30628-0.76906-0.82469-1.2875-1.5938-1.5938v-8.8125c0.76906-0.30628 1.2875-0.82469 1.5938-1.5938zm14.406 2v2h2v-2zm0 3v2h2v-2zm0 3v2h2v-2z" enable-background="accumulate" fill="#fff" overflow="visible" stroke-width="2" style="text-indent:0;text-transform:none"/>
</svg>
<strong>{{ site_conf.site_name }}</strong>
</a>
{% include 'login.html' %}
</div>
</div>
</header>
<main>
<section class="py-1 text-center container">
<div class="row py-lg-4">
<div class="mx-auto">
<h1 class="fw-light">About</h1> <h1 class="fw-light">About</h1>
<p class="lead text-muted">{{ site_conf.about | markdown | safe }}</p> <p class="lead text-muted">{{ site_conf.about | markdown | safe }}</p>
</div> {% endblock %}
</div>
</section>
<div class="album py-5 bg-light"> {% block pagination %}
<div class="container">
<a id="rolling-stock"></a>
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
{% for r in rolling_stock %}
<div class="col">
<div class="card shadow-sm">
{% for i in r.image.all %}
{% if i.is_thumbnail %}<a href="/portal/{{ r.uuid }}"><img src="{{ i.image.url }}" alt="Card image cap"></a>{% endif %}
{% endfor %}
<div class="card-body">
<p class="card-text"><strong>{{ r }}</strong></p>
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Data</th>
</tr>
</thead>
<tbody>
<tr>
<th width="35%" scope="row">Type</th>
<td>{{ r.rolling_class.type }}</td>
</tr>
<tr>
<th scope="row">Company</th>
<td><abbr title="{{ r.rolling_class.company.extended_name }}">{{ r.rolling_class.company }}</abbr></td>
</tr>
<tr>
<th scope="row">Class</th>
<td>{{ r.rolling_class.identifier }}</td>
</tr>
<tr>
<th scope="row">Road number</th>
<td>{{ r.road_number }}</td>
</tr>
<tr>
<th scope="row">Era</th>
<td>{{ r.era }}</td>
</tr>
</tbody>
</table>
<div class="collapse" id="collapseModel{{ r.pk }}">
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Model data</th>
</tr>
</thead>
<tbody>
<tr>
<th width="35%" scope="row">Manufacturer</th>
<td>{% if r.manufacturer.website %}<a href="{{ r.manufacturer.website }}">{% endif %}{{ r.manufacturer }}{% if r.manufacturer.website %}</a>{% endif %}</th>
</tr>
<tr>
<th scope="row">Scale</th>
<td><abbr title="{{ r.scale.ratio }} - {{ r.scale.gauge }}">{{ r.scale }}</abbr></td>
</tr>
<tr>
<th scope="row">SKU</th>
<td>{{ r.sku }}</td>
</tr>
</tbody>
</table>
</div>
{% if r.decoder %}
<div class="collapse" id="collapseDCC{{ r.pk }}">
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">DCC data</th>
</tr>
</thead>
<tbody>
<tr>
<th width="35%" scope="row">Decoder</th>
<td>{{ r.decoder }}</td>
</tr>
<tr>
<th scope="row">Address</th>
<td>{{ r.address }}</td>
</tr>
</tbody>
</table>
</div>
{% endif %}
<div class="btn-group mb-4">
<button class="btn btn-outline-secondary btn-sm" type="button" data-bs-toggle="button"><span data-bs-toggle="collapse" data-bs-target="#collapseModel{{ r.pk }}" aria-expanded="false" aria-controls="collapseModel{{ r.pk }}">Model data</span></button>
{% if r.decoder %}
<button class="btn btn-outline-secondary btn-sm" type="button" data-bs-toggle="button"> <span data-bs-toggle="collapse" data-bs-target="#collapseDCC{{ r.pk }}" aria-expanded="false" aria-controls="collapseDCC{{ r.pk }}">DCC data</span></button>
{% endif %}
<a class="btn btn-sm btn-outline-primary" href="/portal/{{ r.uuid }}">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 %}
</div>
{% if r.tags.all %}
<p class="card-text"><small>Tags:</small>
{% for t in r.tags.all %}<span class="badge bg-primary">
{{ t.name }}</span>{# new line is required #}
{% endfor %}
</p>
{% endif %}
<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>
{% endfor %}
</div>
</div>
<div class="container">
{% if rolling_stock.has_other_pages %} {% if rolling_stock.has_other_pages %}
<nav aria-label="Page navigation example"> <nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mt-4"> <ul class="pagination justify-content-center mt-4">
@@ -203,12 +40,4 @@
</ul> </ul>
</nav> </nav>
{% endif %} {% endif %}
</div> {% endblock %}
</div>
</div>
</div>
</main>
{% include 'footer.html' %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,20 @@
{% load markdown %}
<footer class="text-muted py-4">
<div class="container">
<p class="float-end mb-1">
<a href="#">Back to top</a>
</p>
<div id="footer" class="mb-1">
<p>&copy; {% now "Y" %}</p> {{ site_conf.footer | markdown | safe }}
</div>
<div id="footer_extended" class="mb-0">
{{ site_conf.footer_extended | markdown | safe }}
</div>
</div>
{% if site_conf.show_version %}
<div class="container">
<p class="small text-muted">Version: {{ site_conf.version }}</p>
</div>
{% endif %}
</footer>

View File

@@ -0,0 +1,5 @@
<form class="d-flex" action="/portal/search" method="POST">
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search" value="{{ search }}" name="search">
<button class="btn btn-outline-success" type="submit">Search</button>
</form>

View File

@@ -1,65 +1,7 @@
{% load static %} {% extends 'base.html' %}
{% load solo_tags %}
{% load markdown %} {% load markdown %}
{% get_solo 'portal.SiteConfiguration' as site_conf %}
<!doctype html> {% block header %}
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
<meta name="generator" content="Hugo 0.88.1">
<title>{{ site_conf.site_name }}</title>
<link rel="canonical" href="https://getbootstrap.com/docs/5.1/examples/album/">
<!-- Bootstrap core CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="{% static "css/main.css" %}" rel="stylesheet">
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
</head>
<body>
<header>
<div class="navbar navbar-dark bg-dark shadow-sm">
<div class="container">
<a href="/" class="navbar-brand d-flex align-items-center">
<svg class="me-2" width="26" height="16" enable-background="new 0 0 26 26" version="1" viewBox="0 0 26 16" xmlns="http://www.w3.org/2000/svg">
<path d="m2.8125 0.0010991a1.0001 1.0001 0 0 0-0.8125 1c0 0.55455-0.44545 1-1 1a1.0001 1.0001 0 0 0-1 1v10a1.0001 1.0001 0 0 0 1 1c0.55455 0 1 0.44546 1 1a1.0001 1.0001 0 0 0 1 1h20a1.0001 1.0001 0 0 0 1-1c0-0.55454 0.44546-1 1-1a1.0001 1.0001 0 0 0 1-1v-10a1.0001 1.0001 0 0 0-1-1c-0.55454 0-1-0.44545-1-1a1.0001 1.0001 0 0 0-1-1h-20a1.0001 1.0001 0 0 0-0.09375 0 1.0001 1.0001 0 0 0-0.09375 0zm0.78125 2h14.406v1h2v-1h2.4062c0.30628 0.76906 0.82469 1.2875 1.5938 1.5938v8.8125c-0.76906 0.30628-1.2875 0.82469-1.5938 1.5938h-2.4062v-1h-2v1h-14.406c-0.30628-0.76906-0.82469-1.2875-1.5938-1.5938v-8.8125c0.76906-0.30628 1.2875-0.82469 1.5938-1.5938zm14.406 2v2h2v-2zm0 3v2h2v-2zm0 3v2h2v-2z" enable-background="accumulate" fill="#fff" overflow="visible" stroke-width="2" style="text-indent:0;text-transform:none"/>
</svg>
<strong>{{ site_conf.site_name }}</strong>
</a>
{% include 'login.html' %}
</div>
</div>
</header>
<main>
<section class="py-1 text-start container">
<div class="row py-lg-4">
<div class="mx-auto">
<nav style="--bs-breadcrumb-divider: '>';" aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">Home</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ rolling_stock }}</li>
</ol>
</nav>
<h1 class="fw-light">{{ rolling_stock }}</h1> <h1 class="fw-light">{{ rolling_stock }}</h1>
{% if rolling_stock.tags.all %} {% if rolling_stock.tags.all %}
<p><small>Tags:</small> <p><small>Tags:</small>
@@ -68,11 +10,17 @@
{% endfor %} {% endfor %}
</p> </p>
{% endif %} {% endif %}
{% endblock %}
{% block cards %}
{% for t in rolling_stock.image.all %}
<div class="col">
<img class="img-thumbnail" src="{{ t.image.url }}" alt="Rolling stock image">
</div> </div>
</div> {% endfor %}
</section> {% endblock %}
<section class="py-1 text-start container"> {% block extra_content %}
<div class="row py-lg-2"> <section class="py-4 text-start container">
<div class="row">
<div class="mx-auto"> <div class="mx-auto">
<nav> <nav>
<div class="nav nav-tabs" id="nav-tab" role="tablist"> <div class="nav nav-tabs" id="nav-tab" role="tablist">
@@ -299,20 +247,4 @@
</div> </div>
</div> </div>
</section> </section>
<div class="album py-5 bg-light"> {% endblock %}
<div class="container">
<a id="rolling-stock"></a>
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
{% for t in rolling_stock.image.all %}
<div class="col">
<img class="img-thumbnail" src="{{ t.image.url }}" alt="Rolling stock image">
</div>
{% endfor %}
</div>
</div>
</div>
</main>
{% include 'footer.html' %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,41 @@
{% extends "base.html" %}
{% block header %}
<h1 class="fw-light">Search: {{ search }}</h1>
<p class="lead text-muted">Results found: {{ rolling_stock | length }}</p>
{% endblock %}
{% block pagination %}
{% if rolling_stock.has_other_pages %}
<nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mt-4">
{% if rolling_stock.has_previous %}
<li class="page-item">
<a class="page-link" href="/portal/search/{{ search }}/{{ rolling_stock.previous_page_number }}#rolling_stock" tabindex="-1">Previous</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">Previous</span>
</li>
{% endif %}
{% for i in rolling_stock.paginator.page_range %}
{% if rolling_stock.number == i %}
<li class="page-item active">
<span class="page-link">{{ i }}</span></span>
</li>
{% else %}
<li class="page-item"><a class="page-link" href="/portal/search/{{ search }}/{{ i }}#rolling-stock">{{ i }}</a></li>
{% endif %}
{% endfor %}
{% if rolling_stock.has_next %}
<li class="page-item">
<a class="page-link" href="/portal/search/{{ search }}/{{ rolling_stock.next_page_number }}#rolling-stock" tabindex="-1">Next</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">Next</span>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% endblock %}

View File

@@ -9,4 +9,4 @@ register = template.Library()
@register.filter @register.filter
@stringfilter @stringfilter
def markdown(value): def markdown(value):
return md.markdown(value, extensions=['markdown.extensions.fenced_code']) return md.markdown(value, extensions=["markdown.extensions.fenced_code"])

View File

@@ -1,8 +1,22 @@
from django.urls import path from django.urls import path
from portal.views import GetHome, GetRollingStock from portal.views import GetHome, GetHomeFiltered, GetRollingStock
urlpatterns = [ urlpatterns = [
path("<int:page>", GetHome.as_view(), name='index_pagination'), path("", GetHome.as_view(), name="index"),
path("<uuid:uuid>", GetRollingStock.as_view(), name='rolling_stock'), path("<int:page>", GetHome.as_view(), name="index_pagination"),
path(
"search",
GetHomeFiltered.as_view(http_method_names=["post"]),
name="index_filtered",
),
path(
"search/<str:search>", GetHomeFiltered.as_view(), name="index_filtered"
),
path(
"search/<str:search>/<int:page>",
GetHomeFiltered.as_view(),
name="index_filtered_pagination",
),
path("<uuid:uuid>", GetRollingStock.as_view(), name="rolling_stock"),
] ]

View File

@@ -2,5 +2,5 @@ from django.apps import apps
def get_site_conf(): def get_site_conf():
SiteConfiguration = apps.get_model('portal', 'SiteConfiguration') SiteConfiguration = apps.get_model("portal", "SiteConfiguration")
return SiteConfiguration.get_solo() return SiteConfiguration.get_solo()

View File

@@ -1,16 +1,20 @@
import operator
from functools import reduce
from django.views import View from django.views import View
from django.http import Http404
from django.db.models import Q
from django.shortcuts import render from django.shortcuts import render
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from portal.utils import get_site_conf from portal.utils import get_site_conf
from roster.models import RollingStock, RollingStockImage from roster.models import RollingStock
class GetHome(View): class GetHome(View):
def get(self, request, page=1): def get(self, request, page=1):
site_conf = get_site_conf() site_conf = get_site_conf()
rolling_stock = RollingStock.objects.all() rolling_stock = RollingStock.objects.all()
thumbnails = RollingStockImage.objects.filter(is_thumbnail=True)
paginator = Paginator(rolling_stock, site_conf.items_per_page) paginator = Paginator(rolling_stock, site_conf.items_per_page)
try: try:
@@ -20,16 +24,76 @@ class GetHome(View):
except EmptyPage: except EmptyPage:
rolling_stock = paginator.page(paginator.num_pages) rolling_stock = paginator.page(paginator.num_pages)
return render(request, 'home.html', { return render(request, "home.html", {"rolling_stock": rolling_stock})
'rolling_stock': rolling_stock,
'thumbnails': thumbnails
}) class GetHomeFiltered(View):
def run_search(self, request, search, page=1):
# if not hasattr(RollingStock, _filter):
# raise Http404
site_conf = get_site_conf()
# query = {
# _filter: _value
# }
query = reduce(
operator.or_,
(
Q(
Q(rolling_class__identifier__icontains=s)
| Q(rolling_class__description__icontains=s)
| Q(rolling_class__type__type__icontains=s)
| Q(road_number__icontains=s)
| Q(rolling_class__company__name__icontains=s)
| Q(rolling_class__company__country__icontains=s)
| Q(manufacturer__name__icontains=s)
| Q(scale__scale__icontains=s)
| Q(tags__name__icontains=s)
)
for s in search.split()
),
)
rolling_stock = RollingStock.objects.filter(query)
paginator = Paginator(rolling_stock, site_conf.items_per_page)
try:
rolling_stock = paginator.page(page)
except PageNotAnInteger:
rolling_stock = paginator.page(1)
except EmptyPage:
rolling_stock = paginator.page(paginator.num_pages)
return rolling_stock
def get(self, request, search, page=1):
rolling_stock = self.run_search(request, search, page)
return render(
request,
"search.html",
{"search": search, "rolling_stock": rolling_stock},
)
def post(self, request, page=1):
search = request.POST.get("search")
if not search:
raise Http404
rolling_stock = self.run_search(request, search, page)
return render(
request,
"search.html",
{"search": search, "rolling_stock": rolling_stock},
)
class GetRollingStock(View): class GetRollingStock(View):
def get(self, request, uuid): def get(self, request, uuid):
rolling_stock = RollingStock.objects.get(uuid=uuid) rolling_stock = RollingStock.objects.get(uuid=uuid)
return render(request, 'page.html', { return render(
'rolling_stock': rolling_stock, request,
}) "page.html",
{
"rolling_stock": rolling_stock,
},
)

View File

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

View File

@@ -28,7 +28,7 @@ SECRET_KEY = (
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
ALLOWED_HOSTS = [] ALLOWED_HOSTS = ["*"]
# Application definition # Application definition
@@ -152,10 +152,7 @@ DECODER_INTERFACES = [
(5, "Next18/Next18S"), (5, "Next18/Next18S"),
] ]
MANUFACTURER_TYPES = [ MANUFACTURER_TYPES = [("model", "Model"), ("real", "Real")]
("model", "Model"),
("real", "Real")
]
ROLLING_STOCK_TYPES = [ ROLLING_STOCK_TYPES = [
("engine", "Engine"), ("engine", "Engine"),

View File

@@ -14,18 +14,18 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.conf import settings from django.conf import settings
from django.shortcuts import redirect
from django.conf.urls.static import static 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 portal.utils import get_site_conf from portal.utils import get_site_conf
from portal.views import GetHome
site_conf = get_site_conf() site_conf = get_site_conf()
admin.site.site_header = site_conf.site_name admin.site.site_header = site_conf.site_name
urlpatterns = [ urlpatterns = [
path("", GetHome.as_view(), name="index"), path("", lambda r: redirect("/portal/")),
path("portal/", include("portal.urls")), path("portal/", include("portal.urls")),
path("ht/", include("health_check.urls")), path("ht/", include("health_check.urls")),
path("admin/", admin.site.urls), path("admin/", admin.site.urls),

View File

@@ -11,13 +11,14 @@ def git_suffix(fname):
""" """
try: try:
gh = subprocess.check_output( gh = subprocess.check_output(
['git', 'rev-parse', '--short', 'HEAD'], ["git", "rev-parse", "--short", "HEAD"],
stderr=open(os.devnull, 'w')).strip() stderr=open(os.devnull, "w"),
gh = "-git" + gh.decode() if gh else '' ).strip()
gh = "-git" + gh.decode() if gh else ""
except Exception: except Exception:
# trapping everything on purpose; git may not be installed or it # trapping everything on purpose; git may not be installed or it
# may not work properly # may not work properly
gh = '' gh = ""
return gh return gh

View File

@@ -2,6 +2,7 @@ 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
# from django.core.files.storage import FileSystemStorage # from django.core.files.storage import FileSystemStorage
# from django.dispatch import receiver # from django.dispatch import receiver
@@ -32,8 +33,11 @@ class RollingClass(models.Model):
) )
description = models.CharField(max_length=256, blank=True) description = models.CharField(max_length=256, blank=True)
manufacturer = models.ForeignKey( manufacturer = models.ForeignKey(
Manufacturer, on_delete=models.CASCADE, null=True, blank=True, Manufacturer,
limit_choices_to={"category": "real"} on_delete=models.CASCADE,
null=True,
blank=True,
limit_choices_to={"category": "real"},
) )
class Meta: class Meta:
@@ -74,8 +78,11 @@ class RollingStock(models.Model):
) )
road_number = models.CharField(max_length=128, unique=False) road_number = models.CharField(max_length=128, unique=False)
manufacturer = models.ForeignKey( manufacturer = models.ForeignKey(
Manufacturer, on_delete=models.CASCADE, null=True, blank=True, Manufacturer,
limit_choices_to={"category": "model"} on_delete=models.CASCADE,
null=True,
blank=True,
limit_choices_to={"category": "model"},
) )
scale = models.ForeignKey(Scale, on_delete=models.CASCADE) scale = models.ForeignKey(Scale, on_delete=models.CASCADE)
sku = models.CharField(max_length=32, blank=True) sku = models.CharField(max_length=32, blank=True)
@@ -112,9 +119,7 @@ class RollingStock(models.Model):
class RollingStockDocument(models.Model): class RollingStockDocument(models.Model):
rolling_stock = models.ForeignKey( rolling_stock = models.ForeignKey(
RollingStock, RollingStock, on_delete=models.CASCADE, related_name="document"
on_delete=models.CASCADE,
related_name="document"
) )
description = models.CharField(max_length=128, blank=True) description = models.CharField(max_length=128, blank=True)
file = models.FileField(upload_to="files/", null=True, blank=True) file = models.FileField(upload_to="files/", null=True, blank=True)
@@ -131,9 +136,7 @@ class RollingStockDocument(models.Model):
class RollingStockImage(models.Model): class RollingStockImage(models.Model):
rolling_stock = models.ForeignKey( rolling_stock = models.ForeignKey(
RollingStock, RollingStock, on_delete=models.CASCADE, related_name="image"
on_delete=models.CASCADE,
related_name="image"
) )
image = models.ImageField(upload_to="images/", null=True, blank=True) image = models.ImageField(upload_to="images/", null=True, blank=True)
is_thumbnail = models.BooleanField() is_thumbnail = models.BooleanField()
@@ -149,7 +152,8 @@ class RollingStockImage(models.Model):
def save(self, **kwargs): def save(self, **kwargs):
if self.is_thumbnail: if self.is_thumbnail:
RollingStockImage.objects.filter( RollingStockImage.objects.filter(
rolling_stock=self.rolling_stock).update(is_thumbnail=False) rolling_stock=self.rolling_stock
).update(is_thumbnail=False)
super().save(**kwargs) super().save(**kwargs)
@@ -159,7 +163,7 @@ class RollingStockProperty(models.Model):
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="property", related_name="property",
null=False, null=False,
blank=False blank=False,
) )
property = models.ForeignKey(Property, on_delete=models.CASCADE) property = models.ForeignKey(Property, on_delete=models.CASCADE)
value = models.CharField(max_length=256) value = models.CharField(max_length=256)