11 Commits

32 changed files with 246 additions and 58 deletions

View File

@@ -31,6 +31,7 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
"consist_address", "consist_address",
"company", "company",
"era", "era",
"image",
"notes", "notes",
"tags", "tags",
) )

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.6 on 2022-07-12 12:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('consist', '0002_alter_consist_options'),
]
operations = [
migrations.AddField(
model_name='consist',
name='image',
field=models.ImageField(blank=True, null=True, upload_to='images/'),
),
]

View File

@@ -17,6 +17,7 @@ class Consist(models.Model):
Company, on_delete=models.CASCADE, null=True, blank=True Company, on_delete=models.CASCADE, null=True, blank=True
) )
era = models.CharField(max_length=32, blank=True) era = models.CharField(max_length=32, blank=True)
image = models.ImageField(upload_to="images/", null=True, blank=True)
notes = models.TextField(blank=True) notes = models.TextField(blank=True)
creation_time = models.DateTimeField(auto_now_add=True) creation_time = models.DateTimeField(auto_now_add=True)
updated_time = models.DateTimeField(auto_now=True) updated_time = models.DateTimeField(auto_now=True)

View File

@@ -7,6 +7,14 @@ from driver.models import DriverConfiguration
@admin.register(DriverConfiguration) @admin.register(DriverConfiguration)
class DriverConfigurationAdmin(SingletonModelAdmin): class DriverConfigurationAdmin(SingletonModelAdmin):
fieldsets = ( fieldsets = (
(
"General configuration",
{
"fields": (
"enabled",
)
},
),
( (
"Remote DCC-EX configuration", "Remote DCC-EX configuration",
{ {

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.6 on 2022-07-11 18:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('driver', '0004_alter_driverconfiguration_remote_host'),
]
operations = [
migrations.AddField(
model_name='driverconfiguration',
name='enabled',
field=models.BooleanField(default=False),
),
]

View File

@@ -1,10 +1,11 @@
from django.db import models from django.db import models
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from ipaddress import IPv4Address, IPv4Network from ipaddress import IPv4Network
from solo.models import SingletonModel from solo.models import SingletonModel
class DriverConfiguration(SingletonModel): class DriverConfiguration(SingletonModel):
enabled = models.BooleanField(default=False)
remote_host = models.GenericIPAddressField( remote_host = models.GenericIPAddressField(
protocol="IPv4", default="192.168.4.1" protocol="IPv4", default="192.168.4.1"
) )

View File

@@ -34,9 +34,22 @@ def addresschecker(f):
return addresslookup return addresslookup
class IsEnabled(BasePermission):
def has_permission(self, request, view):
config = DriverConfiguration.get_solo()
# if driver is disabled, block all connections
if not config.enabled:
raise Http404
return True
class Firewall(BasePermission): class Firewall(BasePermission):
def has_permission(self, request, view): def has_permission(self, request, view):
config = DriverConfiguration.get_solo() config = DriverConfiguration.get_solo()
# if network is not configured, accept only read ops
if not config.network: if not config.network:
return request.method in SAFE_METHODS return request.method in SAFE_METHODS
@@ -61,7 +74,7 @@ class Test(APIView):
""" """
parser_classes = [PlainTextParser] parser_classes = [PlainTextParser]
permission_classes = [IsAuthenticated | Firewall] permission_classes = [IsEnabled & IsAuthenticated | Firewall]
def get(self, request): def get(self, request):
response = Connector().passthrough("<s>") response = Connector().passthrough("<s>")
@@ -76,7 +89,7 @@ class SendCommand(APIView):
""" """
parser_classes = [PlainTextParser] parser_classes = [PlainTextParser]
permission_classes = [IsAuthenticated | Firewall] permission_classes = [IsEnabled & IsAuthenticated | Firewall]
def put(self, request): def put(self, request):
data = request.data data = request.data
@@ -101,7 +114,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 = [IsEnabled & IsAuthenticated | Firewall]
def put(self, request, address): def put(self, request, address):
serializer = FunctionSerializer(data=request.data) serializer = FunctionSerializer(data=request.data)
@@ -118,7 +131,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 = [IsEnabled & IsAuthenticated | Firewall]
def put(self, request, address): def put(self, request, address):
serializer = CabSerializer(data=request.data) serializer = CabSerializer(data=request.data)
@@ -134,7 +147,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 = [IsEnabled & IsAuthenticated | Firewall]
def put(self, request): def put(self, request):
serializer = InfraSerializer(data=request.data) serializer = InfraSerializer(data=request.data)
@@ -150,7 +163,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 = [IsEnabled & IsAuthenticated | Firewall]
def put(self, request): def put(self, request):
Connector().emergency() Connector().emergency()

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.6 on 2022-07-12 10:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('metadata', '0002_alter_decoder_manufacturer'),
]
operations = [
migrations.AddField(
model_name='property',
name='private',
field=models.BooleanField(default=False),
),
]

View File

@@ -8,6 +8,7 @@ from ram.utils import get_image_preview, slugify
class Property(models.Model): class Property(models.Model):
name = models.CharField(max_length=128, unique=True) name = models.CharField(max_length=128, unique=True)
private = models.BooleanField(default=False)
class Meta: class Meta:
verbose_name_plural = "Properties" verbose_name_plural = "Properties"

View File

@@ -10,6 +10,10 @@
min-height: 300px; min-height: 300px;
} }
.img-thumbnail {
padding: 0;
}
#nav-notes > p { #nav-notes > p {
padding: .5rem; padding: .5rem;
} }

View File

@@ -49,7 +49,7 @@
<nav class="navbar navbar-expand-lg navbar-light"> <nav class="navbar navbar-expand-lg navbar-light">
<div class="container-fluid g-0"> <div class="container-fluid g-0">
<a class="navbar-brand" href="{% url 'index' %}">Home</a> <a class="navbar-brand" href="{% url 'index' %}">Home</a>
<div class="collapse navbar-collapse" id="navbarSupportedContent"> <div class="navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0"> <ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'index' %}">Roster</a> <a class="nav-link" href="{% url 'index' %}">Roster</a>

View File

@@ -8,6 +8,7 @@
{% 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 %}
<div class="card-body"> <div class="card-body">
<p class="card-text"><strong>{{ c.identifier }}</strong></p> <p class="card-text"><strong>{{ c.identifier }}</strong></p>
{% if c.tags.all %} {% if c.tags.all %}
@@ -46,7 +47,7 @@
</table> </table>
<div class="btn-group mb-4"> <div class="btn-group mb-4">
<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"> <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> <small class="text-muted">Updated {{ c.updated_time | date:"M d, Y H:m" }}</small>

View File

@@ -2,7 +2,7 @@
{% load markdown %} {% load markdown %}
{% block header %} {% block header %}
<h1 class="fw-light">About</h1> {% if site_conf.about %}<h1 class="fw-light">About</h1>{% endif %}
<p class="lead text-muted">{{ site_conf.about | markdown | safe }}</p> <p class="lead text-muted">{{ site_conf.about | markdown | safe }}</p>
{% endblock %} {% endblock %}

View File

@@ -12,9 +12,9 @@
{{ site_conf.footer_extended | markdown | safe }} {{ site_conf.footer_extended | markdown | safe }}
</div> </div>
</div> </div>
{% if site_conf.show_version %}
<div class="container"> <div class="container">
<p class="small text-muted">Made with ❤️ and <a href="https://github.com/daniviga/django-ram">django-ram</a> version {{ site_conf.version }}</p> <p class="small text-muted">Made with ❤️ for 🚂 and <a href="https://github.com/daniviga/django-ram">django-ram</a>
{% if site_conf.show_version %}<br/>Version {{ site_conf.version }}{% endif %}
</div> </div>
{% endif %}
</footer> </footer>

View File

@@ -0,0 +1,7 @@
<section class="py-4 text-center container">
<div class="row">
<div class="mx-auto">
{% block header_content %}{% endblock %}
</div>
</div>
</section>

View File

@@ -29,7 +29,6 @@
<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 data</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 data</button>
{% if rolling_stock.decoder %}<button class="nav-link" id="nav-dcc-tab" data-bs-toggle="tab" data-bs-target="#nav-dcc" type="button" role="tab" aria-controls="nav-dcc" aria-selected="false">DCC</button>{% endif %} {% if rolling_stock.decoder %}<button class="nav-link" id="nav-dcc-tab" data-bs-toggle="tab" data-bs-target="#nav-dcc" type="button" role="tab" aria-controls="nav-dcc" aria-selected="false">DCC</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 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.property.count > 0 %}<button class="nav-link" id="nav-properties-tab" data-bs-toggle="tab" data-bs-target="#nav-properties" type="button" role="tab" aria-controls="nav-properties" aria-selected="false">Properties</button>{% endif %}
{% if rolling_stock.document.count > 0 %}<button class="nav-link" id="nav-documents-tab" data-bs-toggle="tab" data-bs-target="#nav-documents" type="button" role="tab" aria-controls="nav-documents" aria-selected="false">Documents</button>{% endif %} {% if rolling_stock.document.count > 0 %}<button class="nav-link" id="nav-documents-tab" data-bs-toggle="tab" data-bs-target="#nav-documents" type="button" role="tab" aria-controls="nav-documents" aria-selected="false">Documents</button>{% endif %}
</div> </div>
</nav> </nav>
@@ -139,6 +138,23 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
{% if rolling_stock_properties %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Properties</th>
</tr>
</thead>
<tbody>
{% for p in rolling_stock_properties %}
<tr>
<th width="35%" scope="row">{{ p.property }}</th>
<td>{{ p.value }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div> </div>
<div class="tab-pane fade" id="nav-class" role="tabpanel" aria-labelledby="nav-class-tab"> <div class="tab-pane fade" id="nav-class" role="tabpanel" aria-labelledby="nav-class-tab">
<table class="table table-striped"> <table class="table table-striped">
@@ -170,6 +186,23 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
{% if class_properties %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Properties</th>
</tr>
</thead>
<tbody>
{% for p in class_properties %}
<tr>
<th width="35%" scope="row">{{ p.property }}</th>
<td>{{ p.value }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div> </div>
<div class="tab-pane fade" id="nav-dcc" role="tabpanel" aria-labelledby="nav-dcc-tab"> <div class="tab-pane fade" id="nav-dcc" role="tabpanel" aria-labelledby="nav-dcc-tab">
<table class="table table-striped"> <table class="table table-striped">
@@ -209,23 +242,6 @@
<div class="tab-pane fade" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab"> <div class="tab-pane fade" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab">
{{ rolling_stock.notes | markdown | safe }} {{ rolling_stock.notes | markdown | safe }}
</div> </div>
<div class="tab-pane fade" id="nav-properties" role="tabpanel" aria-labelledby="nav-properties-tab">
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Properties</th>
</tr>
</thead>
<tbody>
{% for p in rolling_stock.property.all %}
<tr>
<th scope="row">{{ p.property }}</th>
<td>{{ p.value }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="tab-pane fade" id="nav-documents" role="tabpanel" aria-labelledby="nav-documents-tab"> <div class="tab-pane fade" id="nav-documents" role="tabpanel" aria-labelledby="nav-documents-tab">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>

View File

@@ -113,11 +113,25 @@ class GetRollingStock(View):
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise Http404 raise Http404
class_properties = (
rolling_stock.rolling_class.property.all() if
request.user.is_authenticated else
rolling_stock.rolling_class.property.filter(
property__private=False)
)
rolling_stock_properties = (
rolling_stock.property.all() if
request.user.is_authenticated else
rolling_stock.property.filter(property__private=False)
)
return render( return render(
request, request,
"page.html", "page.html",
{ {
"rolling_stock": rolling_stock, "rolling_stock": rolling_stock,
"class_properties": class_properties,
"rolling_stock_properties": rolling_stock_properties,
}, },
) )

View File

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

View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<title>Swagger</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="//unpkg.com/swagger-ui-dist@3/swagger-ui.css" />
</head>
<body>
<div id="swagger-ui"></div>
<script src="//unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js"></script>
<script>
const ui = SwaggerUIBundle({
url: "{% url schema_url %}",
dom_id: '#swagger-ui',
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
layout: "BaseLayout"
})
</script>
</body>
</html>

View File

@@ -29,18 +29,18 @@ urlpatterns = [
path("api/v1/dcc/", include("driver.urls")), path("api/v1/dcc/", include("driver.urls")),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# if settings.DEBUG: if settings.DEBUG:
# 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('swagger/', TemplateView.as_view( path('swagger/', TemplateView.as_view(
# template_name='swagger.html', template_name='swagger.html',
# extra_context={'schema_url': 'openapi-schema'} extra_context={'schema_url': 'openapi-schema'}
# ), name='swagger'), ), name='swagger'),
# path('openapi', get_schema_view( path('openapi', get_schema_view(
# title="BITE - A Basic/IoT/Example", title="RAM - Railroad Assets Manager",
# description="BITE API for IoT", description="RAM API",
# version="1.0.0" version="1.0.0"
# ), name='openapi-schema'), ), name='openapi-schema'),
# ] ]

View File

@@ -0,0 +1,24 @@
# Generated by Django 4.0.6 on 2022-07-13 17:58
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('roster', '0005_alter_rollingstockproperty_rolling_stock'),
]
operations = [
migrations.AlterField(
model_name='rollingclassproperty',
name='rolling_class',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='property', to='roster.rollingclass', verbose_name='Class'),
),
migrations.AlterField(
model_name='rollingstock',
name='rolling_class',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rolling_class', to='roster.rollingclass', verbose_name='Class'),
),
]

View File

@@ -55,6 +55,7 @@ class RollingClassProperty(models.Model):
on_delete=models.CASCADE, on_delete=models.CASCADE,
null=False, null=False,
blank=False, blank=False,
related_name="property",
verbose_name="Class", verbose_name="Class",
) )
property = models.ForeignKey(Property, on_delete=models.CASCADE) property = models.ForeignKey(Property, on_delete=models.CASCADE)
@@ -74,6 +75,7 @@ class RollingStock(models.Model):
on_delete=models.CASCADE, on_delete=models.CASCADE,
null=False, null=False,
blank=False, blank=False,
related_name="rolling_class",
verbose_name="Class", verbose_name="Class",
) )
road_number = models.CharField(max_length=128, unique=False) road_number = models.CharField(max_length=128, unique=False)

View File

@@ -1,9 +1,9 @@
from django.urls import path from django.urls import path
from roster.views import RosterList, RosterGet, RosterAddress, RosterIdentifier from roster.views import RosterList, RosterGet, RosterAddress, RosterClass
urlpatterns = [ urlpatterns = [
path("list", RosterList.as_view()), path("list", RosterList.as_view()),
path("get/<str:uuid>", RosterGet.as_view()), path("get/<str:uuid>", RosterGet.as_view()),
path("address/<int:address>", RosterAddress.as_view()), path("address/<int:address>", RosterAddress.as_view()),
path("identifier/<str:identifier>", RosterIdentifier.as_view()), path("class/<str:class>", RosterClass.as_view()),
] ]

View File

@@ -1,4 +1,5 @@
from rest_framework.generics import ListAPIView, RetrieveAPIView from rest_framework.generics import ListAPIView, RetrieveAPIView
from rest_framework.schemas.openapi import AutoSchema
from roster.models import RollingStock from roster.models import RollingStock
from roster.serializers import RollingStockSerializer from roster.serializers import RollingStockSerializer
@@ -14,14 +15,30 @@ class RosterGet(RetrieveAPIView):
serializer_class = RollingStockSerializer serializer_class = RollingStockSerializer
lookup_field = "uuid" lookup_field = "uuid"
schema = AutoSchema(
operation_id_base="retrieveRollingStockByUUID"
)
class RosterAddress(ListAPIView): class RosterAddress(ListAPIView):
queryset = RollingStock.objects.all()
serializer_class = RollingStockSerializer serializer_class = RollingStockSerializer
lookup_field = "address"
schema = AutoSchema(
operation_id_base="retrieveRollingStockByAddress"
)
def get_queryset(self):
address = self.kwargs["address"]
return RollingStock.objects.filter(address=address)
class RosterIdentifier(RetrieveAPIView): class RosterClass(ListAPIView):
queryset = RollingStock.objects.all()
serializer_class = RollingStockSerializer serializer_class = RollingStockSerializer
lookup_field = "identifier"
schema = AutoSchema(
operation_id_base="retrieveRollingStockByClass"
)
def get_queryset(self):
_class = self.kwargs["class"]
return RollingStock.objects.filter(rolling_class__identifier=_class)

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
sample_data/images/scrr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -20,7 +20,7 @@
"name": "Roco", "name": "Roco",
"category": "model", "category": "model",
"website": "https://www.roco.cc/", "website": "https://www.roco.cc/",
"logo": "images/Roco_logo_ABtacmm.png" "logo": "images/roco_logo.png"
} }
}, },
{ {
@@ -60,7 +60,7 @@
"name": "Bachmann", "name": "Bachmann",
"category": "model", "category": "model",
"website": "https://bachmanntrains.com/", "website": "https://bachmanntrains.com/",
"logo": "images/Bachmann_bros_logo.png" "logo": "images/bachmann_logo.png"
} }
}, },
{ {
@@ -161,7 +161,7 @@
"extended_name": "Österreichische Bundesbahnen", "extended_name": "Österreichische Bundesbahnen",
"country": "AT", "country": "AT",
"freelance": false, "freelance": false,
"logo": "images/ÖBB_Logo_Pflatsch_neu_vQraZ8r.png" "logo": "images/oebb_logo.png"
} }
}, },
{ {
@@ -172,7 +172,7 @@
"extended_name": "Sand Creek Railroad", "extended_name": "Sand Creek Railroad",
"country": "US", "country": "US",
"freelance": true, "freelance": true,
"logo": "images/scrr_Ikh9VQ4.png" "logo": "images/scrr.png"
} }
}, },
{ {