diff --git a/arduino/CommandStation-EX b/arduino/CommandStation-EX index bd9a045..c47dd10 160000 --- a/arduino/CommandStation-EX +++ b/arduino/CommandStation-EX @@ -1 +1 @@ -Subproject commit bd9a04572de75744dc2f15ac090ec959d7a09a29 +Subproject commit c47dd101a5587fb0bb1dd5602adc0002aebc2f21 diff --git a/arduino/WebThrottle-EX b/arduino/WebThrottle-EX index 37dcc1e..ca33f6a 160000 --- a/arduino/WebThrottle-EX +++ b/arduino/WebThrottle-EX @@ -1 +1 @@ -Subproject commit 37dcc1e9c118c4b324d0ffeb03c6e60d4bb2a2b4 +Subproject commit ca33f6a1e34b09d1fe872eafc810371f53336ec1 diff --git a/arduino/arduino-cli b/arduino/arduino-cli index 10107d2..0364ce3 160000 --- a/arduino/arduino-cli +++ b/arduino/arduino-cli @@ -1 +1 @@ -Subproject commit 10107d2407c2d9997310fc2e0f22dfd15d48e9a8 +Subproject commit 0364ce35894217a12b0131f1fb2282bb2d2472d9 diff --git a/arduino/dcc-ex.github.io b/arduino/dcc-ex.github.io index ab9ae58..a4367c9 160000 --- a/arduino/dcc-ex.github.io +++ b/arduino/dcc-ex.github.io @@ -1 +1 @@ -Subproject commit ab9ae58cf806a306593f42354c372a261502d452 +Subproject commit a4367c9df082257a34d7cde8a0a1baa118aea143 diff --git a/arduino/vim-arduino b/arduino/vim-arduino index 3933f67..bf371b9 160000 --- a/arduino/vim-arduino +++ b/arduino/vim-arduino @@ -1 +1 @@ -Subproject commit 3933f675698615b137027ad842e326a3bc5888be +Subproject commit bf371b96905958cfa7929ef40fd35a0d6a412d86 diff --git a/daemons/simulator/CommandStation-EX-uno-509014b.elf b/daemons/simulator/CommandStation-EX-uno-509014b.elf deleted file mode 100755 index 2ddf862..0000000 Binary files a/daemons/simulator/CommandStation-EX-uno-509014b.elf and /dev/null differ diff --git a/daemons/simulator/CommandStation-EX-uno-c47dd10.elf b/daemons/simulator/CommandStation-EX-uno-c47dd10.elf new file mode 100755 index 0000000..7e500c2 Binary files /dev/null and b/daemons/simulator/CommandStation-EX-uno-c47dd10.elf differ diff --git a/daemons/simulator/README.md b/daemons/simulator/README.md index 3d9f48e..abfc621 100644 --- a/daemons/simulator/README.md +++ b/daemons/simulator/README.md @@ -4,5 +4,5 @@ ```bash $ podman build -t dcc/net-to-serial:sim . -$ podman run --init --cpu 0.1 -d -p 2560:2560 dcc/net-to-serial:sim +$ podman run --init --cpus 0.1 -d -p 2560:2560 dcc/net-to-serial:sim ``` diff --git a/dcc/consist/__init__.py b/dcc/consist/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dcc/consist/admin.py b/dcc/consist/admin.py new file mode 100644 index 0000000..679caa6 --- /dev/null +++ b/dcc/consist/admin.py @@ -0,0 +1,49 @@ +from django.contrib import admin +from adminsortable2.admin import SortableInlineAdminMixin + +from consist.models import Consist, ConsistItem + + +class ConsistItemInline(SortableInlineAdminMixin, admin.TabularInline): + model = ConsistItem + min_num = 1 + extra = 0 + readonly_fields = ("address", "type", "company", "era") + + +@admin.register(Consist) +class ConsistAdmin(admin.ModelAdmin): + inlines = (ConsistItemInline,) + readonly_fields = ( + "creation_time", + "updated_time", + ) + list_display = ("identifier", "company", "era") + list_filter = list_display + search_fields = list_display + + fieldsets = ( + ( + None, + { + "fields": ( + "identifier", + "consist_address", + "company", + "era", + "notes", + "tags", + ) + }, + ), + ( + "Audit", + { + "classes": ("collapse",), + "fields": ( + "creation_time", + "updated_time", + ), + }, + ), + ) diff --git a/dcc/consist/apps.py b/dcc/consist/apps.py new file mode 100644 index 0000000..1cf6149 --- /dev/null +++ b/dcc/consist/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ConsistConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "consist" diff --git a/dcc/consist/migrations/0001_initial.py b/dcc/consist/migrations/0001_initial.py new file mode 100644 index 0000000..df46d20 --- /dev/null +++ b/dcc/consist/migrations/0001_initial.py @@ -0,0 +1,44 @@ +# Generated by Django 4.0.2 on 2022-04-02 14:25 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('roster', '0001_initial'), + ('metadata', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Consist', + fields=[ + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('identifier', models.CharField(max_length=128)), + ('address', models.SmallIntegerField(blank=True, default=None, null=True)), + ('epoch', models.CharField(blank=True, max_length=32)), + ('notes', models.TextField(blank=True)), + ('creation_time', models.DateTimeField(auto_now_add=True)), + ('updated_time', models.DateTimeField(auto_now=True)), + ('company', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='metadata.company')), + ('tags', models.ManyToManyField(blank=True, related_name='consist', to='metadata.Tag')), + ], + ), + migrations.CreateModel( + name='ConsistItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order', models.PositiveIntegerField(default=0)), + ('consist', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='consist.consist')), + ('rolling_stock', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='roster.rollingstock')), + ], + options={ + 'ordering': ['order'], + }, + ), + ] diff --git a/dcc/consist/migrations/0002_rename_address_consist_consist_address_and_more.py b/dcc/consist/migrations/0002_rename_address_consist_consist_address_and_more.py new file mode 100644 index 0000000..db729ca --- /dev/null +++ b/dcc/consist/migrations/0002_rename_address_consist_consist_address_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.0.2 on 2022-04-02 16:18 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('consist', '0001_initial'), + ] + + operations = [ + migrations.RenameField( + model_name='consist', + old_name='address', + new_name='consist_address', + ), + migrations.RenameField( + model_name='consist', + old_name='epoch', + new_name='era', + ), + ] diff --git a/dcc/consist/migrations/0003_alter_consistitem_consist.py b/dcc/consist/migrations/0003_alter_consistitem_consist.py new file mode 100644 index 0000000..6a2420f --- /dev/null +++ b/dcc/consist/migrations/0003_alter_consistitem_consist.py @@ -0,0 +1,19 @@ +# Generated by Django 4.0.3 on 2022-04-02 21:32 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('consist', '0002_rename_address_consist_consist_address_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='consistitem', + name='consist', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='consist_item', to='consist.consist'), + ), + ] diff --git a/dcc/consist/migrations/__init__.py b/dcc/consist/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dcc/consist/models.py b/dcc/consist/models.py new file mode 100644 index 0000000..4db824b --- /dev/null +++ b/dcc/consist/models.py @@ -0,0 +1,52 @@ +from uuid import uuid4 +from django.db import models + +from metadata.models import Company, Tag +from roster.models import RollingStock + + +class Consist(models.Model): + uuid = models.UUIDField(primary_key=True, default=uuid4, editable=False) + identifier = models.CharField(max_length=128, unique=False) + tags = models.ManyToManyField(Tag, related_name="consist", blank=True) + consist_address = models.SmallIntegerField( + default=None, null=True, blank=True + ) + company = models.ForeignKey( + Company, on_delete=models.CASCADE, null=True, blank=True + ) + era = models.CharField(max_length=32, blank=True) + notes = models.TextField(blank=True) + creation_time = models.DateTimeField(auto_now_add=True) + updated_time = models.DateTimeField(auto_now=True) + + def __str__(self): + return "{0}".format(self.identifier) + + +class ConsistItem(models.Model): + consist = models.ForeignKey( + 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) + + class Meta(object): + ordering = ["order"] + + def __str__(self): + return "{0}".format(self.rolling_stock) + + def type(self): + return self.rolling_stock.rolling_class.type + + def address(self): + return self.rolling_stock.address + + def company(self): + return self.rolling_stock.company() + + def era(self): + return self.rolling_stock.era diff --git a/dcc/consist/serializers.py b/dcc/consist/serializers.py new file mode 100644 index 0000000..947b487 --- /dev/null +++ b/dcc/consist/serializers.py @@ -0,0 +1,24 @@ +from rest_framework import serializers +from consist.models import Consist, ConsistItem + +from metadata.serializers import CompanySerializer, TagSerializer + +# from roster.serializers import RollingStockSerializer + + +class ConsistItemSerializer(serializers.ModelSerializer): + # rolling_stock = RollingStockSerializer() + + class Meta: + model = ConsistItem + fields = ("order", "rolling_stock") + + +class ConsistSerializer(serializers.ModelSerializer): + company = CompanySerializer() + consist_item = ConsistItemSerializer(many=True) + tags = TagSerializer(many=True) + + class Meta: + model = Consist + fields = "__all__" diff --git a/dcc/consist/tests.py b/dcc/consist/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/dcc/consist/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/dcc/consist/urls.py b/dcc/consist/urls.py new file mode 100644 index 0000000..af648ca --- /dev/null +++ b/dcc/consist/urls.py @@ -0,0 +1,6 @@ +from django.urls import path +from consist.views import ConsistList + +urlpatterns = [ + path("list", ConsistList.as_view()), +] diff --git a/dcc/consist/views.py b/dcc/consist/views.py new file mode 100644 index 0000000..e3fdf06 --- /dev/null +++ b/dcc/consist/views.py @@ -0,0 +1,21 @@ +from rest_framework.generics import ListAPIView, RetrieveAPIView + +from consist.models import Consist +from consist.serializers import ConsistSerializer + + +class ConsistList(ListAPIView): + queryset = Consist.objects.all() + serializer_class = ConsistSerializer + + +class ConsistGet(RetrieveAPIView): + queryset = Consist.objects.all() + serializer_class = ConsistSerializer + lookup_field = "uuid" + + +# class RosterIdentifier(RetrieveAPIView): +# queryset = RollingStock.objects.all() +# serializer_class = RollingStockSerializer +# lookup_field = "identifier" diff --git a/dcc/dcc/asgi.py b/dcc/dcc/asgi.py index 0db25d5..b25b7d7 100644 --- a/dcc/dcc/asgi.py +++ b/dcc/dcc/asgi.py @@ -11,6 +11,6 @@ import os from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dcc.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dcc.settings") application = get_asgi_application() diff --git a/dcc/dcc/parsers.py b/dcc/dcc/parsers.py index f46c972..29755d0 100644 --- a/dcc/dcc/parsers.py +++ b/dcc/dcc/parsers.py @@ -2,7 +2,7 @@ from rest_framework.parsers import BaseParser class PlainTextParser(BaseParser): - media_type = 'text/plain' + media_type = "text/plain" def parse(self, stream, media_type=None, parser_context=None): return stream.read() diff --git a/dcc/dcc/settings.py b/dcc/dcc/settings.py index 6eaa721..a87be4f 100644 --- a/dcc/dcc/settings.py +++ b/dcc/dcc/settings.py @@ -21,7 +21,9 @@ BASE_DIR = Path(__file__).resolve().parent.parent # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-1fgtf05rwp0qp05@ef@a7%x#o+t6vk6063py=vhdmut0j!8s4u' +SECRET_KEY = ( + "django-insecure-1fgtf05rwp0qp05@ef@a7%x#o+t6vk6063py=vhdmut0j!8s4u" +) # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -32,61 +34,63 @@ ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'health_check', - 'health_check.db', - 'django_countries', - 'solo', - 'rest_framework', - 'dcc', - 'driver', - 'metadata', - 'roster', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "health_check", + "health_check.db", + "django_countries", + "solo", + "rest_framework", + "adminsortable2", + "dcc", + "driver", + "metadata", + "roster", + "consist", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", # 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'dcc.urls' +ROOT_URLCONF = "dcc.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'dcc.wsgi.application' +WSGI_APPLICATION = "dcc.wsgi.application" # Database # https://docs.djangoproject.com/en/4.0/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", } } @@ -96,16 +100,16 @@ DATABASES = { AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] @@ -113,9 +117,9 @@ AUTH_PASSWORD_VALIDATORS = [ # Internationalization # https://docs.djangoproject.com/en/4.0/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -125,17 +129,32 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.0/howto/static-files/ -STATIC_URL = 'static/' +STATIC_URL = "static/" # Default primary key field type # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" -MEDIA_URL = 'media/' -MEDIA_ROOT = os.path.join(BASE_DIR, 'media') +MEDIA_URL = "media/" +MEDIA_ROOT = os.path.join(BASE_DIR, "media") + +COUNTRIES_OVERRIDE = { + "ZZ": "Freelance", +} + +DECODER_INTERFACES = [ + (1, "NEM651"), + (2, "NEM652"), + (3, "PluX"), + (4, "21MTC"), + (5, "Next18/Next18S"), +] ROLLING_STOCK_TYPES = [ - ("engine", "Engine"), ("car", "Car"), - ("equipment", "Equipment"), ("other", "Other") + ("engine", "Engine"), + ("car", "Car"), + ("railcar", "Railcar"), + ("equipment", "Equipment"), + ("other", "Other"), ] diff --git a/dcc/dcc/urls.py b/dcc/dcc/urls.py index 955fc55..34dce49 100644 --- a/dcc/dcc/urls.py +++ b/dcc/dcc/urls.py @@ -18,20 +18,24 @@ from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path +from consist import urls as consist_urls from roster import urls as roster_urls from driver import urls as driver_urls +admin.site.site_header = "Trains assets manager" + urlpatterns = [ - path('ht/', include('health_check.urls')), - path('admin/', admin.site.urls), - path('api/v1/roster/', include(roster_urls)), - path('api/v1/dcc/', include(driver_urls)), + path("ht/", include("health_check.urls")), + path("admin/", admin.site.urls), + path("api/v1/consist/", include(consist_urls)), + path("api/v1/roster/", include(roster_urls)), + path("api/v1/dcc/", include(driver_urls)), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # if settings.DEBUG: # from django.views.generic import TemplateView # from rest_framework.schemas import get_schema_view -# +# # urlpatterns += [ # path('swagger/', TemplateView.as_view( # template_name='swagger.html', diff --git a/dcc/dcc/utils.py b/dcc/dcc/utils.py index a2b6f59..e97769e 100644 --- a/dcc/dcc/utils.py +++ b/dcc/dcc/utils.py @@ -5,12 +5,13 @@ from django.utils.text import slugify as django_slugify def get_image_preview(url): return format_html( '' % url) + 'background-color: #eee;" />' % url + ) def slugify(string, custom_separator=None): # Make slug 'flat', both '-' and '_' are replaced with '-' - string = django_slugify(string).replace('_', '-') + string = django_slugify(string).replace("_", "-") if custom_separator is not None: - string = string.replace('-', custom_separator) + string = string.replace("-", custom_separator) return string diff --git a/dcc/dcc/wsgi.py b/dcc/dcc/wsgi.py index 8435ef1..4b7e8c5 100644 --- a/dcc/dcc/wsgi.py +++ b/dcc/dcc/wsgi.py @@ -11,6 +11,6 @@ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dcc.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dcc.settings") application = get_wsgi_application() diff --git a/dcc/driver/apps.py b/dcc/driver/apps.py index 27eca5a..0c77fc7 100644 --- a/dcc/driver/apps.py +++ b/dcc/driver/apps.py @@ -3,9 +3,10 @@ from health_check.plugins import plugin_dir class DriverConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'driver' + default_auto_field = "django.db.models.BigAutoField" + name = "driver" def ready(self): from driver.health import DriverHealthCheck + plugin_dir.register(DriverHealthCheck) diff --git a/dcc/driver/connector.py b/dcc/driver/connector.py index d195b41..def3c7e 100644 --- a/dcc/driver/connector.py +++ b/dcc/driver/connector.py @@ -8,7 +8,7 @@ class Connector: self.config = DriverConfiguration.get_solo() def __send_data(self, message): - resp = b'' + resp = b"" # convert to binary if str is received if isinstance(message, str): message = message.encode() @@ -30,10 +30,12 @@ class Connector: def ops(self, address, data, function=False): if function: message = "".format( - address, data['function'], data['state']) + address, data["function"], data["state"] + ) else: message = "".format( - address, data['speed'], data['direction']) + address, data["speed"], data["direction"] + ) self.__send_data(message) def infra(self, data): @@ -43,9 +45,9 @@ class Connector: track = "" if data["power"]: - self.__send_data('<1{}>'.format(track)) + self.__send_data("<1{}>".format(track)) else: - self.__send_data('<0{}>'.format(track)) + self.__send_data("<0{}>".format(track)) def emergency(self): - self.__send_data('') + self.__send_data("") diff --git a/dcc/driver/health.py b/dcc/driver/health.py index 75e09f9..b715b1a 100644 --- a/dcc/driver/health.py +++ b/dcc/driver/health.py @@ -1,6 +1,8 @@ from health_check.backends import BaseHealthCheckBackend -from health_check.exceptions import (ServiceUnavailable, - ServiceReturnedUnexpectedResult) +from health_check.exceptions import ( + ServiceUnavailable, + ServiceReturnedUnexpectedResult, +) from driver.connector import Connector @@ -10,7 +12,7 @@ class DriverHealthCheck(BaseHealthCheckBackend): def check_status(self): try: - Connector().passthrough(b'') + Connector().passthrough(b"") except ConnectionRefusedError as e: self.add_error(ServiceUnavailable("IOError"), e) except Exception as e: diff --git a/dcc/driver/models.py b/dcc/driver/models.py index 972ca19..f5a72df 100644 --- a/dcc/driver/models.py +++ b/dcc/driver/models.py @@ -4,7 +4,8 @@ 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) diff --git a/dcc/driver/serializers.py b/dcc/driver/serializers.py index 19b5ac1..68af6ce 100644 --- a/dcc/driver/serializers.py +++ b/dcc/driver/serializers.py @@ -14,5 +14,6 @@ class CabSerializer(serializers.Serializer): class InfraSerializer(serializers.Serializer): power = serializers.BooleanField(required=True) track = serializers.ChoiceField( - choices=('main', 'prog', 'join', 'MAIN', 'PROG', 'JOIN'), - required=False) + choices=("main", "prog", "join", "MAIN", "PROG", "JOIN"), + required=False, + ) diff --git a/dcc/driver/urls.py b/dcc/driver/urls.py index 64a8e26..d8a52f7 100644 --- a/dcc/driver/urls.py +++ b/dcc/driver/urls.py @@ -2,10 +2,10 @@ from django.urls import path from driver.views import SendCommand, Function, Cab, Emergency, Infra, Test urlpatterns = [ - path('test', Test.as_view()), - path('emergency', Emergency.as_view()), - path('infra', Infra.as_view()), - path('command', SendCommand.as_view()), - path('/cab', Cab.as_view()), - path('/function', Function.as_view()), + path("test", Test.as_view()), + path("emergency", Emergency.as_view()), + path("infra", Infra.as_view()), + path("command", SendCommand.as_view()), + path("/cab", Cab.as_view()), + path("/function", Function.as_view()), ] diff --git a/dcc/driver/views.py b/dcc/driver/views.py index 1b9236b..ed51f7a 100644 --- a/dcc/driver/views.py +++ b/dcc/driver/views.py @@ -7,7 +7,10 @@ from rest_framework.response import Response from dcc.parsers import PlainTextParser from driver.connector import Connector from driver.serializers import ( - FunctionSerializer, CabSerializer, InfraSerializer) + FunctionSerializer, + CabSerializer, + InfraSerializer, +) from roster.models import RollingStock @@ -15,10 +18,12 @@ def addresschecker(f): """ Check if DCC address does exist in the database """ + def addresslookup(request, address, *args): if not RollingStock.objects.filter(address=address): raise Http404 return f(request, address, *args) + return addresslookup @@ -26,32 +31,38 @@ class Test(APIView): """ Send a test command """ + parser_classes = [PlainTextParser] def get(self, request): response = Connector().passthrough("") - return Response({"response": response.decode()}, - status=status.HTTP_202_ACCEPTED) + return Response( + {"response": response.decode()}, status=status.HTTP_202_ACCEPTED + ) class SendCommand(APIView): """ Command passthrough """ + parser_classes = [PlainTextParser] def put(self, request): data = request.data if not data: - raise serializers.ValidationError({ - "error": "a string is expected"}) + raise serializers.ValidationError( + {"error": "a string is expected"} + ) cmd = data.decode().strip() if not (cmd.startswith("<") and cmd.endswith(">")): - raise serializers.ValidationError({ - "error": "please provide a valid command"}) + raise serializers.ValidationError( + {"error": "please provide a valid command"} + ) response = Connector().passthrough(cmd) - return Response({"response": response.decode()}, - status=status.HTTP_202_ACCEPTED) + return Response( + {"response": response.decode()}, status=status.HTTP_202_ACCEPTED + ) @method_decorator(addresschecker, name="put") @@ -59,15 +70,14 @@ class Function(APIView): """ Send "Function" commands to a valid DCC address """ + def put(self, request, address): serializer = FunctionSerializer(data=request.data) if serializer.is_valid(): Connector().ops(address, serializer.data, function=True) - return Response(serializer.data, - status=status.HTTP_202_ACCEPTED) + return Response(serializer.data, status=status.HTTP_202_ACCEPTED) - return Response(serializer.errors, - status=status.HTTP_400_BAD_REQUEST) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @method_decorator(addresschecker, name="put") @@ -75,42 +85,43 @@ class Cab(APIView): """ Send "Cab" commands to a valid DCC address """ + def put(self, request, address): serializer = CabSerializer(data=request.data) if serializer.is_valid(): Connector().ops(address, serializer.data) - return Response(serializer.data, - status=status.HTTP_202_ACCEPTED) + return Response(serializer.data, status=status.HTTP_202_ACCEPTED) - return Response(serializer.errors, - status=status.HTTP_400_BAD_REQUEST) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class Infra(APIView): """ Send "Infra" commands to a valid DCC address """ + def put(self, request): serializer = InfraSerializer(data=request.data) if serializer.is_valid(): Connector().infra(serializer.data) - return Response(serializer.data, - status=status.HTTP_202_ACCEPTED) + return Response(serializer.data, status=status.HTTP_202_ACCEPTED) - return Response(serializer.errors, - status=status.HTTP_400_BAD_REQUEST) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class Emergency(APIView): """ Send an "Emergency" stop, no matter the HTTP method used """ + def put(self, request): Connector().emergency() - return Response({"response": "emergency stop"}, - status=status.HTTP_202_ACCEPTED) + return Response( + {"response": "emergency stop"}, status=status.HTTP_202_ACCEPTED + ) def get(self, request): Connector().emergency() - return Response({"response": "emergency stop"}, - status=status.HTTP_202_ACCEPTED) + return Response( + {"response": "emergency stop"}, status=status.HTTP_202_ACCEPTED + ) diff --git a/dcc/manage.py b/dcc/manage.py index 88b1b9e..20ea1dc 100755 --- a/dcc/manage.py +++ b/dcc/manage.py @@ -6,7 +6,7 @@ import sys def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dcc.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dcc.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +18,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/dcc/metadata/admin.py b/dcc/metadata/admin.py index 98bd15f..fdedd1a 100644 --- a/dcc/metadata/admin.py +++ b/dcc/metadata/admin.py @@ -1,29 +1,46 @@ from django.contrib import admin from metadata.models import ( - Decoder, Manufacturer, Company, Tag, RollingStockType) + Decoder, + Scale, + Manufacturer, + Company, + Tag, + RollingStockType, +) @admin.register(Decoder) class DecoderAdmin(admin.ModelAdmin): - readonly_fields = ('image_thumbnail',) + readonly_fields = ("image_thumbnail",) + list_display = ("__str__", "interface") + list_filter = ("manufacturer", "interface") + + +@admin.register(Scale) +class ScaleAdmin(admin.ModelAdmin): + list_display = ("scale", "ratio", "gauge") + list_filter = ("ratio", "gauge") @admin.register(Company) class CompanyAdmin(admin.ModelAdmin): - readonly_fields = ('logo_thumbnail',) + readonly_fields = ("logo_thumbnail",) + list_display = ("name", "country") + list_filter = list_display @admin.register(Manufacturer) class ManufacturerAdmin(admin.ModelAdmin): - readonly_fields = ('logo_thumbnail',) + readonly_fields = ("logo_thumbnail",) @admin.register(Tag) class TagAdmin(admin.ModelAdmin): - readonly_fields = ('slug',) + readonly_fields = ("slug",) + list_display = ("name", "slug") @admin.register(RollingStockType) class RollingStockTypeAdmin(admin.ModelAdmin): - list_display = ('type', 'category') - list_filter = list_display + list_display = ("__str__",) + list_filter = ("type", "category") diff --git a/dcc/metadata/apps.py b/dcc/metadata/apps.py index 1d731c9..e41215d 100644 --- a/dcc/metadata/apps.py +++ b/dcc/metadata/apps.py @@ -2,5 +2,5 @@ from django.apps import AppConfig class MetadataConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'metadata' + default_auto_field = "django.db.models.BigAutoField" + name = "metadata" diff --git a/dcc/metadata/migrations/0001_initial.py b/dcc/metadata/migrations/0001_initial.py index c3e1d3c..09ced5a 100644 --- a/dcc/metadata/migrations/0001_initial.py +++ b/dcc/metadata/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.0.2 on 2022-04-01 20:25 +# Generated by Django 4.0.2 on 2022-04-02 14:25 from django.db import migrations, models import django.db.models.deletion @@ -17,7 +17,8 @@ class Migration(migrations.Migration): name='Company', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=128, unique=True)), + ('name', models.CharField(max_length=64, unique=True)), + ('extended_name', models.CharField(max_length=128, unique=True)), ('country', django_countries.fields.CountryField(max_length=2)), ('logo', models.ImageField(blank=True, null=True, upload_to='images/')), ], diff --git a/dcc/metadata/migrations/0002_scale_manufacturer_website_and_more.py b/dcc/metadata/migrations/0002_scale_manufacturer_website_and_more.py new file mode 100644 index 0000000..8e289ad --- /dev/null +++ b/dcc/metadata/migrations/0002_scale_manufacturer_website_and_more.py @@ -0,0 +1,32 @@ +# Generated by Django 4.0.2 on 2022-04-02 16:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('metadata', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Scale', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('scale', models.CharField(max_length=32, unique=True)), + ('ratio', models.CharField(blank=True, max_length=16)), + ('gauge', models.CharField(blank=True, max_length=16)), + ], + ), + migrations.AddField( + model_name='manufacturer', + name='website', + field=models.URLField(blank=True), + ), + migrations.AlterField( + model_name='company', + name='extended_name', + field=models.CharField(blank=True, max_length=128), + ), + ] diff --git a/dcc/metadata/migrations/0003_alter_company_options_alter_scale_options_and_more.py b/dcc/metadata/migrations/0003_alter_company_options_alter_scale_options_and_more.py new file mode 100644 index 0000000..9bd9674 --- /dev/null +++ b/dcc/metadata/migrations/0003_alter_company_options_alter_scale_options_and_more.py @@ -0,0 +1,26 @@ +# Generated by Django 4.0.2 on 2022-04-02 17:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('metadata', '0002_scale_manufacturer_website_and_more'), + ] + + operations = [ + migrations.AlterModelOptions( + name='company', + options={'ordering': ['name'], 'verbose_name_plural': 'Companies'}, + ), + migrations.AlterModelOptions( + name='scale', + options={'ordering': ['scale']}, + ), + migrations.AlterField( + model_name='rollingstocktype', + name='category', + field=models.CharField(choices=[('engine', 'Engine'), ('car', 'Car'), ('railcar', 'Railcar'), ('equipment', 'Equipment'), ('other', 'Other')], max_length=64), + ), + ] diff --git a/dcc/metadata/migrations/0004_company_freelance_decoder_sound.py b/dcc/metadata/migrations/0004_company_freelance_decoder_sound.py new file mode 100644 index 0000000..2d32053 --- /dev/null +++ b/dcc/metadata/migrations/0004_company_freelance_decoder_sound.py @@ -0,0 +1,23 @@ +# Generated by Django 4.0.2 on 2022-04-02 17:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('metadata', '0003_alter_company_options_alter_scale_options_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='company', + name='freelance', + field=models.BooleanField(blank=True, default=False, null=True), + ), + migrations.AddField( + model_name='decoder', + name='sound', + field=models.BooleanField(blank=True, default=False, null=True), + ), + ] diff --git a/dcc/metadata/migrations/0005_alter_company_freelance_alter_decoder_sound.py b/dcc/metadata/migrations/0005_alter_company_freelance_alter_decoder_sound.py new file mode 100644 index 0000000..215e18d --- /dev/null +++ b/dcc/metadata/migrations/0005_alter_company_freelance_alter_decoder_sound.py @@ -0,0 +1,23 @@ +# Generated by Django 4.0.2 on 2022-04-02 17:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('metadata', '0004_company_freelance_decoder_sound'), + ] + + operations = [ + migrations.AlterField( + model_name='company', + name='freelance', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='decoder', + name='sound', + field=models.BooleanField(default=False), + ), + ] diff --git a/dcc/metadata/models.py b/dcc/metadata/models.py index 5243bf4..58ed46d 100644 --- a/dcc/metadata/models.py +++ b/dcc/metadata/models.py @@ -8,69 +8,69 @@ from dcc.utils import get_image_preview, slugify class Manufacturer(models.Model): name = models.CharField(max_length=128, unique=True) - logo = models.ImageField( - upload_to='images/', - null=True, - blank=True) + website = models.URLField(blank=True) + logo = models.ImageField(upload_to="images/", null=True, blank=True) def __str__(self): return self.name def logo_thumbnail(self): return get_image_preview(self.logo.url) + logo_thumbnail.short_description = "Preview" class Company(models.Model): - name = models.CharField(max_length=128, unique=True) + name = models.CharField(max_length=64, unique=True) + extended_name = models.CharField(max_length=128, blank=True) country = CountryField() - logo = models.ImageField( - upload_to='images/', - null=True, - blank=True) + freelance = models.BooleanField(default=False) + logo = models.ImageField(upload_to="images/", null=True, blank=True) class Meta: verbose_name_plural = "Companies" + ordering = ["name"] def __str__(self): return self.name def logo_thumbnail(self): return get_image_preview(self.logo.url) + logo_thumbnail.short_description = "Preview" class Decoder(models.Model): - class Interface(models.IntegerChoices): - NEM651 = 1, "NEM651" - NEM652 = 2, "NEM652" - NEM658 = 3, "PluX" - NEM660 = 4, "21MTC" - NEM662 = 5, "Next18/Next18S" - name = models.CharField(max_length=128, unique=True) - manufacturer = models.ForeignKey( - Manufacturer, - on_delete=models.CASCADE) + manufacturer = models.ForeignKey(Manufacturer, on_delete=models.CASCADE) version = models.CharField(max_length=64, blank=True) interface = models.PositiveSmallIntegerField( - choices=Interface.choices, - null=True, - blank=True + choices=settings.DECODER_INTERFACES, null=True, blank=True ) - image = models.ImageField( - upload_to='images/', - null=True, - blank=True) + sound = models.BooleanField(default=False) + image = models.ImageField(upload_to="images/", null=True, blank=True) def __str__(self): return "{0} - {1}".format(self.manufacturer, self.name) def image_thumbnail(self): return get_image_preview(self.image.url) + image_thumbnail.short_description = "Preview" +class Scale(models.Model): + scale = models.CharField(max_length=32, unique=True) + ratio = models.CharField(max_length=16, blank=True) + gauge = models.CharField(max_length=16, blank=True) + + class Meta: + ordering = ["scale"] + + def __str__(self): + return str(self.scale) + + class Tag(models.Model): name = models.CharField(max_length=128, unique=True) slug = models.CharField(max_length=128, unique=True) @@ -87,10 +87,11 @@ def tag_pre_save(sender, instance, **kwargs): class RollingStockType(models.Model): type = models.CharField(max_length=64) category = models.CharField( - max_length=64, choices=settings.ROLLING_STOCK_TYPES) + max_length=64, choices=settings.ROLLING_STOCK_TYPES + ) class Meta(object): - unique_together = ('category', 'type') + unique_together = ("category", "type") def __str__(self): - return "{0}".format(self.type) + return "{0} {1}".format(self.type, self.category) diff --git a/dcc/metadata/serializers.py b/dcc/metadata/serializers.py index d63fb3c..7dd3e0b 100644 --- a/dcc/metadata/serializers.py +++ b/dcc/metadata/serializers.py @@ -1,5 +1,18 @@ from rest_framework import serializers -from metadata.models import Manufacturer, Company, Decoder +from metadata.models import ( + RollingStockType, + Scale, + Manufacturer, + Company, + Decoder, + Tag, +) + + +class RollingStockTypeSerializer(serializers.ModelSerializer): + class Meta: + model = RollingStockType + fields = "__all__" class ManufacturerSerializer(serializers.ModelSerializer): @@ -8,6 +21,12 @@ class ManufacturerSerializer(serializers.ModelSerializer): fields = "__all__" +class ScaleSerializer(serializers.ModelSerializer): + class Meta: + model = Scale + fields = "__all__" + + class CompanySerializer(serializers.ModelSerializer): class Meta: model = Company @@ -20,3 +39,9 @@ class DecoderSerializer(serializers.ModelSerializer): class Meta: model = Decoder fields = "__all__" + + +class TagSerializer(serializers.ModelSerializer): + class Meta: + model = Tag + fields = "__all__" diff --git a/dcc/roster/admin.py b/dcc/roster/admin.py index 40766c4..37aff2d 100644 --- a/dcc/roster/admin.py +++ b/dcc/roster/admin.py @@ -1,7 +1,17 @@ from django.contrib import admin from roster.models import ( - RollingStock, RollingStockImage, RollingStockDocument, Engine, Car, - Equipment, Other) + RollingClass, + RollingStock, + RollingStockImage, + RollingStockDocument, +) + + +@admin.register(RollingClass) +class RollingClass(admin.ModelAdmin): + list_display = ("__str__", "type", "company") + list_filter = ("company", "type__category", "type") + search_fields = list_display class RollingStockDocInline(admin.TabularInline): @@ -14,53 +24,58 @@ class RollingStockImageInline(admin.TabularInline): model = RollingStockImage min_num = 0 extra = 0 - readonly_fields = ('image_thumbnail',) + readonly_fields = ("image_thumbnail",) +@admin.register(RollingStock) class RollingStockAdmin(admin.ModelAdmin): inlines = (RollingStockImageInline, RollingStockDocInline) - readonly_fields = ('creation_time', 'updated_time',) - list_display = ('identifier', 'manufacturer', 'sku', 'company') - list_filter = list_display + readonly_fields = ("creation_time", "updated_time") + list_display = ( + "__str__", + "address", + "manufacturer", + "scale", + "sku", + "company", + "country", + ) + list_filter = ( + "rolling_class__type__category", + "rolling_class__type", + "scale", + "manufacturer", + ) search_fields = list_display fieldsets = ( - (None, { - 'fields': ('identifier', - 'type', - 'tags', - 'manufacturer', - 'sku', - 'decoder', - 'address', - 'company', - 'epoch', - 'production_year', - 'purchase_date', - 'notes') - }), - ('Audit', { - 'classes': ('collapse',), - 'fields': ('creation_time', 'updated_time',) - }), + ( + None, + { + "fields": ( + "rolling_class", + "road_number", + "manufacturer", + "scale", + "sku", + "decoder", + "address", + "era", + "production_year", + "purchase_date", + "notes", + "tags", + ) + }, + ), + ( + "Audit", + { + "classes": ("collapse",), + "fields": ( + "creation_time", + "updated_time", + ), + }, + ), ) - - -@admin.register(Engine) -class Engine(RollingStockAdmin): - list_display = ('identifier', 'address', 'manufacturer', 'sku', 'company') - - -@admin.register(Car) -class Car(RollingStockAdmin): - pass - - -@admin.register(Equipment) -class Equipment(RollingStockAdmin): - pass - - -@admin.register(Other) -class Other(RollingStockAdmin): - pass diff --git a/dcc/roster/apps.py b/dcc/roster/apps.py index 39f52e7..e0172d1 100644 --- a/dcc/roster/apps.py +++ b/dcc/roster/apps.py @@ -2,5 +2,5 @@ from django.apps import AppConfig class RosterConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'roster' + default_auto_field = "django.db.models.BigAutoField" + name = "roster" diff --git a/dcc/roster/migrations/0001_initial.py b/dcc/roster/migrations/0001_initial.py index 2152455..9ad5c6e 100644 --- a/dcc/roster/migrations/0001_initial.py +++ b/dcc/roster/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.0.2 on 2022-04-01 20:25 +# Generated by Django 4.0.2 on 2022-04-02 14:25 from django.db import migrations, models import django.db.models.deletion @@ -14,12 +14,21 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name='Class', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('identifier', models.CharField(max_length=128)), + ('company', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='metadata.company')), + ('type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='metadata.rollingstocktype')), + ], + ), migrations.CreateModel( name='RollingStock', fields=[ ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('identifier', models.CharField(max_length=128)), - ('address', models.SmallIntegerField(blank=True, default=3, null=True)), + ('road_number', models.CharField(max_length=128)), + ('address', models.SmallIntegerField(blank=True, default=None, null=True)), ('sku', models.CharField(blank=True, max_length=32)), ('epoch', models.CharField(blank=True, max_length=32)), ('production_year', models.SmallIntegerField(blank=True, null=True)), @@ -27,14 +36,13 @@ class Migration(migrations.Migration): ('notes', models.TextField(blank=True)), ('creation_time', models.DateTimeField(auto_now_add=True)), ('updated_time', models.DateTimeField(auto_now=True)), - ('company', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='metadata.company')), + ('_class', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='roster.class')), ('decoder', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='metadata.decoder')), ('manufacturer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='metadata.manufacturer')), ('tags', models.ManyToManyField(blank=True, related_name='rolling_stock', to='metadata.Tag')), ], options={ 'verbose_name_plural': 'Rolling stock', - 'ordering': ['address', 'identifier'], }, ), migrations.CreateModel( @@ -60,36 +68,4 @@ class Migration(migrations.Migration): 'unique_together': {('rolling_stock', 'file')}, }, ), - migrations.CreateModel( - name='Other', - fields=[ - ('rollingstock_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='roster.rollingstock')), - ('type', models.ForeignKey(blank=True, limit_choices_to={'category': 'other'}, null=True, on_delete=django.db.models.deletion.CASCADE, to='metadata.rollingstocktype')), - ], - bases=('roster.rollingstock',), - ), - migrations.CreateModel( - name='Equipment', - fields=[ - ('rollingstock_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='roster.rollingstock')), - ('type', models.ForeignKey(blank=True, limit_choices_to={'category': 'equipment'}, null=True, on_delete=django.db.models.deletion.CASCADE, to='metadata.rollingstocktype')), - ], - bases=('roster.rollingstock',), - ), - migrations.CreateModel( - name='Engine', - fields=[ - ('rollingstock_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='roster.rollingstock')), - ('type', models.ForeignKey(blank=True, limit_choices_to={'category': 'engine'}, null=True, on_delete=django.db.models.deletion.CASCADE, to='metadata.rollingstocktype')), - ], - bases=('roster.rollingstock',), - ), - migrations.CreateModel( - name='Car', - fields=[ - ('rollingstock_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='roster.rollingstock')), - ('type', models.ForeignKey(blank=True, limit_choices_to={'category': 'car'}, null=True, on_delete=django.db.models.deletion.CASCADE, to='metadata.rollingstocktype')), - ], - bases=('roster.rollingstock',), - ), ] diff --git a/dcc/roster/migrations/0002_rename_class_rollingclass_alter_rollingclass_options_and_more.py b/dcc/roster/migrations/0002_rename_class_rollingclass_alter_rollingclass_options_and_more.py new file mode 100644 index 0000000..f1cbb85 --- /dev/null +++ b/dcc/roster/migrations/0002_rename_class_rollingclass_alter_rollingclass_options_and_more.py @@ -0,0 +1,48 @@ +# Generated by Django 4.0.2 on 2022-04-02 16:18 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('metadata', '0002_scale_manufacturer_website_and_more'), + ('roster', '0001_initial'), + ] + + operations = [ + migrations.RenameModel( + old_name='Class', + new_name='RollingClass', + ), + migrations.AlterModelOptions( + name='rollingclass', + options={'ordering': ['company', 'identifier'], 'verbose_name': 'Class', 'verbose_name_plural': 'Classes'}, + ), + migrations.AlterModelOptions( + name='rollingstock', + options={'ordering': ['rolling_class', 'road_number'], 'verbose_name_plural': 'Rolling stock'}, + ), + migrations.RenameField( + model_name='rollingstock', + old_name='epoch', + new_name='era', + ), + migrations.RemoveField( + model_name='rollingstock', + name='_class', + ), + migrations.AddField( + model_name='rollingstock', + name='rolling_class', + field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='roster.rollingclass', verbose_name='Class'), + preserve_default=False, + ), + migrations.AddField( + model_name='rollingstock', + name='scale', + field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='metadata.scale'), + preserve_default=False, + ), + ] diff --git a/dcc/roster/migrations/0003_rollingstockimage_description.py b/dcc/roster/migrations/0003_rollingstockimage_description.py new file mode 100644 index 0000000..7d991e1 --- /dev/null +++ b/dcc/roster/migrations/0003_rollingstockimage_description.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.2 on 2022-04-02 17:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('roster', '0002_rename_class_rollingclass_alter_rollingclass_options_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='rollingstockimage', + name='description', + field=models.CharField(blank=True, max_length=256), + ), + ] diff --git a/dcc/roster/migrations/0004_remove_rollingstockimage_description_and_more.py b/dcc/roster/migrations/0004_remove_rollingstockimage_description_and_more.py new file mode 100644 index 0000000..ca7dd75 --- /dev/null +++ b/dcc/roster/migrations/0004_remove_rollingstockimage_description_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 4.0.2 on 2022-04-02 17:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('roster', '0003_rollingstockimage_description'), + ] + + operations = [ + migrations.RemoveField( + model_name='rollingstockimage', + name='description', + ), + migrations.AddField( + model_name='rollingclass', + name='description', + field=models.CharField(blank=True, max_length=256), + ), + ] diff --git a/dcc/roster/models.py b/dcc/roster/models.py index e641df8..5c99b03 100644 --- a/dcc/roster/models.py +++ b/dcc/roster/models.py @@ -1,12 +1,20 @@ 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 from dcc.utils import get_image_preview from metadata.models import ( - Manufacturer, Decoder, Company, Tag, RollingStockType) + Scale, + Manufacturer, + Decoder, + Company, + Tag, + RollingStockType, +) # class OverwriteMixin(FileSystemStorage): # def get_available_name(self, name, max_length): @@ -14,82 +22,75 @@ from metadata.models import ( # return name -class RollingStock(models.Model): - uuid = models.UUIDField( - primary_key=True, default=uuid4, - editable=False) +class RollingClass(models.Model): identifier = models.CharField(max_length=128, unique=False) - tags = models.ManyToManyField( - Tag, - related_name='rolling_stock', - blank=True) - address = models.SmallIntegerField(default=3, null=True, blank=True) + type = models.ForeignKey( + RollingStockType, on_delete=models.CASCADE, null=True, blank=True + ) + description = models.CharField(max_length=256, blank=True) + company = models.ForeignKey( + Company, on_delete=models.CASCADE, null=True, blank=True + ) + + class Meta: + ordering = ["company", "identifier"] + verbose_name = "Class" + verbose_name_plural = "Classes" + + def __str__(self): + return "{0} {1}".format(self.company, self.identifier) + + +class RollingStock(models.Model): + uuid = models.UUIDField(primary_key=True, default=uuid4, editable=False) + rolling_class = models.ForeignKey( + RollingClass, + on_delete=models.CASCADE, + null=False, + blank=False, + verbose_name="Class", + ) + road_number = models.CharField(max_length=128, unique=False) manufacturer = models.ForeignKey( - Manufacturer, on_delete=models.CASCADE, - null=True, blank=True) + Manufacturer, on_delete=models.CASCADE, null=True, blank=True + ) + scale = models.ForeignKey(Scale, on_delete=models.CASCADE) sku = models.CharField(max_length=32, blank=True) decoder = models.ForeignKey( - Decoder, on_delete=models.CASCADE, - null=True, blank=True) - company = models.ForeignKey( - Company, on_delete=models.CASCADE, - null=True, blank=True) - epoch = models.CharField(max_length=32, blank=True) + Decoder, on_delete=models.CASCADE, null=True, blank=True + ) + address = models.SmallIntegerField(default=None, null=True, blank=True) + era = models.CharField(max_length=32, blank=True) production_year = models.SmallIntegerField(null=True, blank=True) purchase_date = models.DateField(null=True, blank=True) - notes = models.TextField(blank=True) - + tags = models.ManyToManyField( + Tag, related_name="rolling_stock", blank=True + ) creation_time = models.DateTimeField(auto_now_add=True) updated_time = models.DateTimeField(auto_now=True) class Meta: - ordering = ['address', 'identifier'] + ordering = ["rolling_class", "road_number"] verbose_name_plural = "Rolling stock" def __str__(self): - return "{0} {1}".format(self.manufacturer, self.identifier) + return "{0} {1}".format(self.rolling_class, self.road_number) + def country(self): + return str(self.rolling_class.company.country) -class Engine(RollingStock): - type = models.ForeignKey( - RollingStockType, on_delete=models.CASCADE, - limit_choices_to={'category': 'engine'}, - null=True, blank=True) - - -class Car(RollingStock): - type = models.ForeignKey( - RollingStockType, on_delete=models.CASCADE, - limit_choices_to={'category': 'car'}, - null=True, blank=True) - - -class Equipment(RollingStock): - type = models.ForeignKey( - RollingStockType, on_delete=models.CASCADE, - limit_choices_to={'category': 'equipment'}, - null=True, blank=True) - - -class Other(RollingStock): - type = models.ForeignKey( - RollingStockType, on_delete=models.CASCADE, - limit_choices_to={'category': 'other'}, - null=True, blank=True) + def company(self): + return str(self.rolling_class.company) class RollingStockDocument(models.Model): - rolling_stock = models.ForeignKey( - RollingStock, on_delete=models.CASCADE) + rolling_stock = models.ForeignKey(RollingStock, on_delete=models.CASCADE) 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) class Meta(object): - unique_together = ('rolling_stock', 'file') + unique_together = ("rolling_stock", "file") def __str__(self): return "{0}".format(os.path.basename(self.file.name)) @@ -97,23 +98,21 @@ class RollingStockDocument(models.Model): class RollingStockImage(models.Model): - rolling_stock = models.ForeignKey( - RollingStock, on_delete=models.CASCADE) - image = models.ImageField( - upload_to='images/', - null=True, - blank=True) + rolling_stock = models.ForeignKey(RollingStock, on_delete=models.CASCADE) + image = models.ImageField(upload_to="images/", null=True, blank=True) def image_thumbnail(self): return get_image_preview(self.image.url) + image_thumbnail.short_description = "Preview" class Meta(object): - unique_together = ('rolling_stock', 'image') + unique_together = ("rolling_stock", "image") def __str__(self): return "{0}".format(os.path.basename(self.image.name)) + # @receiver(models.signals.post_delete, sender=Cab) # def post_save_image(sender, instance, *args, **kwargs): # try: diff --git a/dcc/roster/serializers.py b/dcc/roster/serializers.py index 1d345ee..228e43a 100644 --- a/dcc/roster/serializers.py +++ b/dcc/roster/serializers.py @@ -1,13 +1,30 @@ from rest_framework import serializers -from roster.models import RollingStock +from roster.models import RollingClass, RollingStock from metadata.serializers import ( - ManufacturerSerializer, CompanySerializer, DecoderSerializer) + RollingStockTypeSerializer, + ManufacturerSerializer, + ScaleSerializer, + CompanySerializer, + DecoderSerializer, + TagSerializer, +) + + +class RollingClassSerializer(serializers.ModelSerializer): + company = CompanySerializer() + type = RollingStockTypeSerializer() + + class Meta: + model = RollingClass + fields = "__all__" class RollingStockSerializer(serializers.ModelSerializer): + rolling_class = RollingClassSerializer() manufacturer = ManufacturerSerializer() decoder = DecoderSerializer() - company = CompanySerializer() + scale = ScaleSerializer() + tags = TagSerializer(many=True) class Meta: model = RollingStock diff --git a/dcc/roster/urls.py b/dcc/roster/urls.py index afca8f4..87ccf38 100644 --- a/dcc/roster/urls.py +++ b/dcc/roster/urls.py @@ -1,10 +1,9 @@ from django.urls import path -from roster.views import ( - RosterList, RosterGet, RosterAddress, RosterIdentifier) +from roster.views import RosterList, RosterGet, RosterAddress, RosterIdentifier urlpatterns = [ - path('list', RosterList.as_view()), - path('get/', RosterGet.as_view()), - path('address/', RosterAddress.as_view()), - path('identifier/', RosterIdentifier.as_view()), + path("list", RosterList.as_view()), + path("get/", RosterGet.as_view()), + path("address/", RosterAddress.as_view()), + path("identifier/", RosterIdentifier.as_view()), ] diff --git a/dcc/roster/views.py b/dcc/roster/views.py index 6a29409..a11ac45 100644 --- a/dcc/roster/views.py +++ b/dcc/roster/views.py @@ -12,16 +12,16 @@ class RosterList(ListAPIView): class RosterGet(RetrieveAPIView): queryset = RollingStock.objects.all() serializer_class = RollingStockSerializer - lookup_field = 'uuid' + lookup_field = "uuid" class RosterAddress(ListAPIView): queryset = RollingStock.objects.all() serializer_class = RollingStockSerializer - lookup_field = 'address' + lookup_field = "address" class RosterIdentifier(RetrieveAPIView): queryset = RollingStock.objects.all() serializer_class = RollingStockSerializer - lookup_field = 'identifier' + lookup_field = "identifier" diff --git a/requirements.txt b/requirements.txt index 3c71f53..bf774ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ djangorestframework django-solo django-countries django-health-check +django-admin-sortable2 # psycopg2-binary # paho-mqtt # asyncio-mqtt