diff --git a/ram/consist/models.py b/ram/consist/models.py index 4db824b..bb9b7ab 100644 --- a/ram/consist/models.py +++ b/ram/consist/models.py @@ -26,9 +26,7 @@ class Consist(models.Model): class ConsistItem(models.Model): consist = models.ForeignKey( - 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) order = models.PositiveIntegerField(default=0, blank=False, null=False) diff --git a/ram/driver/models.py b/ram/driver/models.py index ddb7838..18fd6eb 100644 --- a/ram/driver/models.py +++ b/ram/driver/models.py @@ -6,23 +6,16 @@ from solo.models import SingletonModel class DriverConfiguration(SingletonModel): remote_host = models.GenericIPAddressField( - protocol="IPv4", - default="192.168.4.1" + protocol="IPv4", default="192.168.4.1" ) remote_port = models.SmallIntegerField(default=2560) timeout = models.SmallIntegerField(default=250) network = models.GenericIPAddressField( - protocol="IPv4", - default="192.168.4.0", - blank=True, - null=True + protocol="IPv4", default="192.168.4.0", blank=True, null=True ) subnet_mask = models.GenericIPAddressField( - protocol="IPv4", - default="255.255.255.0", - blank=True, - null=True + protocol="IPv4", default="255.255.255.0", blank=True, null=True ) def __str__(self): @@ -31,8 +24,7 @@ class DriverConfiguration(SingletonModel): def clean(self, *args, **kwargs): if self.network: try: - IPv4Network( - "{0}/{1}".format(self.network, self.subnet_mask)) + IPv4Network("{0}/{1}".format(self.network, self.subnet_mask)) except ValueError as e: raise ValidationError(e) super().clean(*args, **kwargs) diff --git a/ram/driver/views.py b/ram/driver/views.py index 1e8cfdc..a23133b 100644 --- a/ram/driver/views.py +++ b/ram/driver/views.py @@ -7,7 +7,7 @@ from rest_framework.response import Response from rest_framework.permissions import ( IsAuthenticated, BasePermission, - SAFE_METHODS + SAFE_METHODS, ) from ram.parsers import PlainTextParser @@ -40,16 +40,15 @@ class Firewall(BasePermission): if not config.network: 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: - ip = IPv4Address(x_forwarded_for.split(',')[0]) + ip = IPv4Address(x_forwarded_for.split(",")[0]) else: ip = IPv4Address(request.META.get("REMOTE_ADDR")) - network = IPv4Network("{0}/{1}".format( - config.network, - config.subnet_mask - )) + network = IPv4Network( + "{0}/{1}".format(config.network, config.subnet_mask) + ) # accept IP configured is settings or localhost 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 """ + permission_classes = [IsAuthenticated | Firewall] def put(self, request, address): @@ -117,6 +117,7 @@ class Cab(APIView): """ Send "Cab" commands to a valid DCC address """ + permission_classes = [IsAuthenticated | Firewall] def put(self, request, address): @@ -132,6 +133,7 @@ class Infra(APIView): """ Send "Infra" commands to a valid DCC address """ + permission_classes = [IsAuthenticated | Firewall] def put(self, request): @@ -147,6 +149,7 @@ class Emergency(APIView): """ Send an "Emergency" stop, no matter the HTTP method used """ + permission_classes = [IsAuthenticated | Firewall] def put(self, request): diff --git a/ram/portal/apps.py b/ram/portal/apps.py index 781ac2f..d912bf9 100644 --- a/ram/portal/apps.py +++ b/ram/portal/apps.py @@ -2,5 +2,5 @@ from django.apps import AppConfig class PortalConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'portal' + default_auto_field = "django.db.models.BigAutoField" + name = "portal" diff --git a/ram/portal/models.py b/ram/portal/models.py index 583e911..8732a33 100644 --- a/ram/portal/models.py +++ b/ram/portal/models.py @@ -7,14 +7,14 @@ from solo.models import SingletonModel class SiteConfiguration(SingletonModel): site_name = models.CharField( - max_length=256, - default="Railroad Assets Manager") + max_length=256, default="Railroad Assets Manager" + ) site_author = models.CharField(max_length=256, blank=True) about = models.TextField(blank=True) items_per_page = models.CharField( - max_length=2, choices=[ - (str(x * 3), str(x * 3)) for x in range(2, 11)], - default='6' + max_length=2, + choices=[(str(x * 3), str(x * 3)) for x in range(2, 11)], + default="6", ) footer = models.TextField(blank=True) footer_extended = models.TextField(blank=True) diff --git a/ram/portal/templates/base.html b/ram/portal/templates/base.html new file mode 100644 index 0000000..8b62c38 --- /dev/null +++ b/ram/portal/templates/base.html @@ -0,0 +1,166 @@ +{% load static %} +{% load solo_tags %} +{% load markdown %} +{% get_solo 'portal.SiteConfiguration' as site_conf %} + + + + + + + + + + {{ site_conf.site_name }} + + + + + +
+ +
+
+
+
+ + +
+
+
+ {% block header %}{% endblock %} +
+
+
+
+
+ +
+ {% block cards %} + {% for r in rolling_stock %} +
+
+ {% for i in r.image.all %} + {% if i.is_thumbnail %}Card image cap{% endif %} + {% endfor %} +
+

{{ r }}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Data
Type{{ r.rolling_class.type }}
Company{{ r.rolling_class.company }}
Class{{ r.rolling_class.identifier }}
Road number{{ r.road_number }}
Era{{ r.era }}
Manufacturer{% if r.manufacturer.website %}{% endif %}{{ r.manufacturer }}{% if r.manufacturer.website %}{% endif %} +
Scale{{ r.scale }}
SKU{{ r.sku }}
+ {% if r.decoder %} + + + + + + + + + + + + + + + + +
DCC data
Decoder{{ r.decoder }}
Address{{ r.address }}
+ {% endif %} +
+ Show all data + {% if request.user.is_staff %}Edit{% endif %} +
+ {% if r.tags.all %} +

Tags: + {% for t in r.tags.all %} + {{ t.name }}{# new line is required #} + {% endfor %} +

+ {% endif %} +
+ Updated {{ r.updated_time | date:"M d, Y H:m" }} +
+
+
+
+ {% endfor %} + {% endblock %} +
+
+
{% block pagination %}{% endblock %}
+
+ {% block extra_content %}{% endblock %} +
+ {% include 'includes/footer.html' %} + + + + diff --git a/ram/portal/templates/footer.html b/ram/portal/templates/footer.html deleted file mode 100644 index 18301f1..0000000 --- a/ram/portal/templates/footer.html +++ /dev/null @@ -1,20 +0,0 @@ -{% load markdown %} - - diff --git a/ram/portal/templates/home.html b/ram/portal/templates/home.html index a6316d0..f249b6d 100644 --- a/ram/portal/templates/home.html +++ b/ram/portal/templates/home.html @@ -1,175 +1,12 @@ -{% load static %} -{% load solo_tags %} +{% extends "base.html" %} {% load markdown %} -{% get_solo 'portal.SiteConfiguration' as site_conf %} - - - - - - - - - {{ site_conf.site_name }} - - - - - - - - - - -
- -
- -
-
-
-
+ {% block header %}

About

{{ site_conf.about | markdown | safe }}

-
-
-
+ {% endblock %} -
-
- -
- {% for r in rolling_stock %} -
-
- {% for i in r.image.all %} - {% if i.is_thumbnail %}Card image cap{% endif %} - {% endfor %} -
-

{{ r }}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Data
Type{{ r.rolling_class.type }}
Company{{ r.rolling_class.company }}
Class{{ r.rolling_class.identifier }}
Road number{{ r.road_number }}
Era{{ r.era }}
-
- - - - - - - - - - - - - - - - - - - -
Model data
Manufacturer{% if r.manufacturer.website %}{% endif %}{{ r.manufacturer }}{% if r.manufacturer.website %}{% endif %} -
Scale{{ r.scale }}
SKU{{ r.sku }}
-
- {% if r.decoder %} -
- - - - - - - - - - - - - - - - -
DCC data
Decoder{{ r.decoder }}
Address{{ r.address }}
-
- {% endif %} -
- - {% if r.decoder %} - - {% endif %} - Show all data - {% if request.user.is_staff %}Edit{% endif %} -
- {% if r.tags.all %} -

Tags: - {% for t in r.tags.all %} - {{ t.name }}{# new line is required #} - {% endfor %} -

- {% endif %} -
- Updated {{ r.updated_time | date:"M d, Y H:m" }} -
-
-
-
- {% endfor %} -
-
-
+ {% block pagination %} {% if rolling_stock.has_other_pages %} {% endif %} -
-
- - -
-{% include 'footer.html' %} - - - + {% endblock %} diff --git a/ram/portal/templates/includes/footer.html b/ram/portal/templates/includes/footer.html new file mode 100644 index 0000000..2334d13 --- /dev/null +++ b/ram/portal/templates/includes/footer.html @@ -0,0 +1,20 @@ +{% load markdown %} + + diff --git a/ram/portal/templates/login.html b/ram/portal/templates/includes/login.html similarity index 100% rename from ram/portal/templates/login.html rename to ram/portal/templates/includes/login.html diff --git a/ram/portal/templates/includes/search.html b/ram/portal/templates/includes/search.html new file mode 100644 index 0000000..37baf01 --- /dev/null +++ b/ram/portal/templates/includes/search.html @@ -0,0 +1,5 @@ +
+ + +
+ diff --git a/ram/portal/templates/page.html b/ram/portal/templates/page.html index 7a501d1..4e2c054 100644 --- a/ram/portal/templates/page.html +++ b/ram/portal/templates/page.html @@ -1,318 +1,250 @@ -{% load static %} -{% load solo_tags %} +{% extends 'base.html' %} {% load markdown %} -{% get_solo 'portal.SiteConfiguration' as site_conf %} - - - - - - - - - {{ site_conf.site_name }} - - - - - - - - - - -
- -
- -
-
-
-
- + {% block header %}

{{ rolling_stock }}

- {% if rolling_stock.tags.all %} -

Tags: - {% for t in rolling_stock.tags.all %} - {{ t.name }}{# new line is required #} - {% endfor %} -

- {% endif %} -
-
-
-
-
-
- - -
- {% if request.user.is_staff %}Edit{% endif %} -
-
-
-
-
-
- -
+ {% if rolling_stock.tags.all %} +

Tags: + {% for t in rolling_stock.tags.all %} + {{ t.name }}{# new line is required #} + {% endfor %} +

+ {% endif %} + {% endblock %} + {% block cards %} {% for t in rolling_stock.image.all %}
Rolling stock image
{% endfor %} -
-
-
-
-{% include 'footer.html' %} - - - + {% endblock %} + {% block extra_content %} +
+
+
+ + +
+ {% if request.user.is_staff %}Edit{% endif %} +
+
+
+
+ {% endblock %} diff --git a/ram/portal/templates/search.html b/ram/portal/templates/search.html new file mode 100644 index 0000000..bda6965 --- /dev/null +++ b/ram/portal/templates/search.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} + + {% block header %} +

Search: {{ search }}

+

Results found: {{ rolling_stock | length }}

+ {% endblock %} + {% block pagination %} + {% if rolling_stock.has_other_pages %} + + {% endif %} + {% endblock %} diff --git a/ram/portal/templatetags/markdown.py b/ram/portal/templatetags/markdown.py index 690b818..00dcee6 100644 --- a/ram/portal/templatetags/markdown.py +++ b/ram/portal/templatetags/markdown.py @@ -9,4 +9,4 @@ register = template.Library() @register.filter @stringfilter def markdown(value): - return md.markdown(value, extensions=['markdown.extensions.fenced_code']) + return md.markdown(value, extensions=["markdown.extensions.fenced_code"]) diff --git a/ram/portal/urls.py b/ram/portal/urls.py index 9991f27..c106df8 100644 --- a/ram/portal/urls.py +++ b/ram/portal/urls.py @@ -1,8 +1,22 @@ from django.urls import path -from portal.views import GetHome, GetRollingStock +from portal.views import GetHome, GetHomeFiltered, GetRollingStock urlpatterns = [ - path("", GetHome.as_view(), name='index_pagination'), - path("", GetRollingStock.as_view(), name='rolling_stock'), + path("", GetHome.as_view(), name="index"), + path("", GetHome.as_view(), name="index_pagination"), + path( + "search", + GetHomeFiltered.as_view(http_method_names=["post"]), + name="index_filtered", + ), + path( + "search/", GetHomeFiltered.as_view(), name="index_filtered" + ), + path( + "search//", + GetHomeFiltered.as_view(), + name="index_filtered_pagination", + ), + path("", GetRollingStock.as_view(), name="rolling_stock"), ] diff --git a/ram/portal/utils.py b/ram/portal/utils.py index b14b91b..0c2b6cb 100644 --- a/ram/portal/utils.py +++ b/ram/portal/utils.py @@ -2,5 +2,5 @@ from django.apps import apps def get_site_conf(): - SiteConfiguration = apps.get_model('portal', 'SiteConfiguration') + SiteConfiguration = apps.get_model("portal", "SiteConfiguration") return SiteConfiguration.get_solo() diff --git a/ram/portal/views.py b/ram/portal/views.py index 94b34e6..5f393af 100644 --- a/ram/portal/views.py +++ b/ram/portal/views.py @@ -1,16 +1,20 @@ +import operator +from functools import reduce + from django.views import View +from django.http import Http404 +from django.db.models import Q from django.shortcuts import render from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from portal.utils import get_site_conf -from roster.models import RollingStock, RollingStockImage +from roster.models import RollingStock class GetHome(View): def get(self, request, page=1): site_conf = get_site_conf() rolling_stock = RollingStock.objects.all() - thumbnails = RollingStockImage.objects.filter(is_thumbnail=True) paginator = Paginator(rolling_stock, site_conf.items_per_page) try: @@ -20,16 +24,76 @@ class GetHome(View): except EmptyPage: rolling_stock = paginator.page(paginator.num_pages) - return render(request, 'home.html', { - 'rolling_stock': rolling_stock, - 'thumbnails': thumbnails - }) + return render(request, "home.html", {"rolling_stock": rolling_stock}) + + +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): def get(self, request, uuid): rolling_stock = RollingStock.objects.get(uuid=uuid) - return render(request, 'page.html', { - 'rolling_stock': rolling_stock, - }) + return render( + request, + "page.html", + { + "rolling_stock": rolling_stock, + }, + ) diff --git a/ram/ram/__init__.py b/ram/ram/__init__.py index 9db4f2e..d7eb4b5 100644 --- a/ram/ram/__init__.py +++ b/ram/ram/__init__.py @@ -1,4 +1,4 @@ from ram.utils import git_suffix -__version__ = '0.0.1' +__version__ = "0.0.1" __version__ += git_suffix(__file__) diff --git a/ram/ram/settings.py b/ram/ram/settings.py index e2fdf24..74dd8bf 100644 --- a/ram/ram/settings.py +++ b/ram/ram/settings.py @@ -28,7 +28,7 @@ SECRET_KEY = ( # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ["*"] # Application definition @@ -152,10 +152,7 @@ DECODER_INTERFACES = [ (5, "Next18/Next18S"), ] -MANUFACTURER_TYPES = [ - ("model", "Model"), - ("real", "Real") -] +MANUFACTURER_TYPES = [("model", "Model"), ("real", "Real")] ROLLING_STOCK_TYPES = [ ("engine", "Engine"), diff --git a/ram/ram/urls.py b/ram/ram/urls.py index 493a099..3f27eab 100644 --- a/ram/ram/urls.py +++ b/ram/ram/urls.py @@ -14,18 +14,18 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.conf import settings +from django.shortcuts import redirect from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path from portal.utils import get_site_conf -from portal.views import GetHome site_conf = get_site_conf() admin.site.site_header = site_conf.site_name urlpatterns = [ - path("", GetHome.as_view(), name="index"), + path("", lambda r: redirect("/portal/")), path("portal/", include("portal.urls")), path("ht/", include("health_check.urls")), path("admin/", admin.site.urls), diff --git a/ram/ram/utils.py b/ram/ram/utils.py index 47266fb..98a9fb9 100644 --- a/ram/ram/utils.py +++ b/ram/ram/utils.py @@ -11,13 +11,14 @@ def git_suffix(fname): """ try: gh = subprocess.check_output( - ['git', 'rev-parse', '--short', 'HEAD'], - stderr=open(os.devnull, 'w')).strip() - gh = "-git" + gh.decode() if gh else '' + ["git", "rev-parse", "--short", "HEAD"], + stderr=open(os.devnull, "w"), + ).strip() + gh = "-git" + gh.decode() if gh else "" except Exception: # trapping everything on purpose; git may not be installed or it # may not work properly - gh = '' + gh = "" return gh diff --git a/ram/roster/models.py b/ram/roster/models.py index 15cea70..27fc471 100644 --- a/ram/roster/models.py +++ b/ram/roster/models.py @@ -2,6 +2,7 @@ import os from uuid import uuid4 from django.db import models from django.urls import reverse + # from django.core.files.storage import FileSystemStorage # from django.dispatch import receiver @@ -32,8 +33,11 @@ class RollingClass(models.Model): ) description = models.CharField(max_length=256, blank=True) manufacturer = models.ForeignKey( - Manufacturer, on_delete=models.CASCADE, null=True, blank=True, - limit_choices_to={"category": "real"} + Manufacturer, + on_delete=models.CASCADE, + null=True, + blank=True, + limit_choices_to={"category": "real"}, ) class Meta: @@ -74,8 +78,11 @@ class RollingStock(models.Model): ) road_number = models.CharField(max_length=128, unique=False) manufacturer = models.ForeignKey( - Manufacturer, on_delete=models.CASCADE, null=True, blank=True, - limit_choices_to={"category": "model"} + Manufacturer, + on_delete=models.CASCADE, + null=True, + blank=True, + limit_choices_to={"category": "model"}, ) scale = models.ForeignKey(Scale, on_delete=models.CASCADE) sku = models.CharField(max_length=32, blank=True) @@ -112,9 +119,7 @@ class RollingStock(models.Model): class RollingStockDocument(models.Model): rolling_stock = models.ForeignKey( - RollingStock, - on_delete=models.CASCADE, - related_name="document" + RollingStock, on_delete=models.CASCADE, related_name="document" ) description = models.CharField(max_length=128, blank=True) file = models.FileField(upload_to="files/", null=True, blank=True) @@ -131,9 +136,7 @@ class RollingStockDocument(models.Model): class RollingStockImage(models.Model): 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="images/", null=True, blank=True) is_thumbnail = models.BooleanField() @@ -149,7 +152,8 @@ class RollingStockImage(models.Model): def save(self, **kwargs): if self.is_thumbnail: RollingStockImage.objects.filter( - rolling_stock=self.rolling_stock).update(is_thumbnail=False) + rolling_stock=self.rolling_stock + ).update(is_thumbnail=False) super().save(**kwargs) @@ -159,7 +163,7 @@ class RollingStockProperty(models.Model): on_delete=models.CASCADE, related_name="property", null=False, - blank=False + blank=False, ) property = models.ForeignKey(Property, on_delete=models.CASCADE) value = models.CharField(max_length=256)