Initial gui support

This commit is contained in:
2022-04-09 01:03:22 +02:00
parent d2ad58e323
commit 440c640d9f
28 changed files with 519 additions and 1 deletions

View File

@@ -47,6 +47,7 @@ INSTALLED_APPS = [
"rest_framework", "rest_framework",
"adminsortable2", "adminsortable2",
"dcc", "dcc",
"portal",
"driver", "driver",
"metadata", "metadata",
"roster", "roster",

View File

@@ -18,6 +18,7 @@ from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from portal.views import GetHome
from consist import urls as consist_urls from consist import urls as consist_urls
from roster import urls as roster_urls from roster import urls as roster_urls
from driver import urls as driver_urls from driver import urls as driver_urls
@@ -25,6 +26,7 @@ from driver import urls as driver_urls
admin.site.site_header = "Trains assets manager" admin.site.site_header = "Trains assets manager"
urlpatterns = [ urlpatterns = [
path('', GetHome.as_view(), name='index'),
path("ht/", include("health_check.urls")), path("ht/", include("health_check.urls")),
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("api/v1/consist/", include(consist_urls)), path("api/v1/consist/", include(consist_urls)),

0
dcc/portal/__init__.py Normal file
View File

6
dcc/portal/admin.py Normal file
View File

@@ -0,0 +1,6 @@
from django.contrib import admin
from solo.admin import SingletonModelAdmin
from portal.models import SiteConfiguration
admin.site.register(SiteConfiguration, SingletonModelAdmin)

6
dcc/portal/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class PortalConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'portal'

View File

@@ -0,0 +1,32 @@
# Generated by Django 4.0.3 on 2022-04-08 22:00
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='SiteConfiguration',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('site_name', models.CharField(default='Map viewer', max_length=256)),
('site_author', models.CharField(blank=True, max_length=256)),
('about', models.TextField(blank=True)),
('maps_per_page', models.CharField(choices=[('6', '6'), ('9', '9'), ('12', '12'), ('15', '15'), ('18', '18'), ('21', '21'), ('24', '24'), ('27', '27'), ('30', '30')], default='6', max_length=2)),
('homepage_content', models.TextField(blank=True)),
('footer', models.TextField(blank=True)),
('footer_short', models.TextField(blank=True)),
('show_copyright', models.BooleanField(default=True)),
('show_version', models.BooleanField(default=True)),
],
options={
'verbose_name': 'Site Configuration',
},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.3 on 2022-04-08 22:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='siteconfiguration',
name='site_name',
field=models.CharField(default='Trains assets manager', max_length=256),
),
]

View File

36
dcc/portal/models.py Normal file
View File

@@ -0,0 +1,36 @@
import django
from django.db import models
from solo.models import SingletonModel
class SiteConfiguration(SingletonModel):
site_name = models.CharField(
max_length=256,
default="Trains assets manager")
site_author = models.CharField(max_length=256, blank=True)
about = models.TextField(blank=True)
maps_per_page = models.CharField(
max_length=2, choices=[
(str(x * 3), str(x * 3)) for x in range(2, 11)],
default='6'
)
homepage_content = models.TextField(blank=True)
footer = models.TextField(blank=True)
footer_short = models.TextField(blank=True)
show_copyright = models.BooleanField(default=True)
show_version = models.BooleanField(default=True)
class Meta:
verbose_name = "Site Configuration"
def __str__(self):
return "Site Configuration"
# def version(self):
# return app_version
def django_version(self):
return django.get_version()

View File

@@ -0,0 +1,29 @@
/* titillium-web-regular - latin */
@font-face {
font-family: 'Titillium Web';
font-style: normal;
font-weight: 400;
src: local('Titillium Web Regular'), local('TitilliumWeb-Regular'),
url('../fonts/titillium-web-v6-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('../fonts/titillium-web-v6-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* titillium-web-italic - latin */
@font-face {
font-family: 'Titillium Web';
font-style: italic;
font-weight: 400;
src: local('Titillium Web Italic'), local('TitilliumWeb-Italic'),
url('../fonts/titillium-web-v6-latin-italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('../fonts/titillium-web-v6-latin-italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* titillium-web-700 - latin */
@font-face {
font-family: 'Titillium Web';
font-style: normal;
font-weight: 700;
src: local('Titillium Web Bold'), local('TitilliumWeb-Bold'),
url('../fonts/titillium-web-v6-latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('../fonts/titillium-web-v6-latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}

View File

@@ -0,0 +1,61 @@
body {
font-family: 'Titillium Web','Segoe UI',Arial,sans-serif;
}
:root {
--jumbotron-padding-y: 3rem;
}
#navbarHeader p {
color: #6c757d;
}
.jumbotron {
padding-top: var(--jumbotron-padding-y);
padding-bottom: var(--jumbotron-padding-y);
margin-bottom: 0;
border-radius: 0;
background-color: #fff;
}
.jumbotron p:last-child {
margin-bottom: 0;
}
.jumbotron-heading {
font-weight: 300;
}
.jumbotron .container {
max-width: 40rem;
}
.card > a > img {
width: 100%;
}
.published-info {
text-align: right;
font-size: .7rem;
}
.navbar img#site-logo {
max-width: 200px;
max-height: 100px;
}
.page-item.active .page-link {
background-color: #343a40;
border-color: #343a40;
}
footer {
padding-top: 3rem;
padding-bottom: 3rem;
}
footer a {
color: #fff;
font-weight: bold;
}
footer a:hover {
color: #fff;
}
footer p {
display: inline;
margin-bottom: .25rem;
}
footer p.float-right > a {
font-weight: normal;
}
.box-shadow {
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
}

View File

@@ -0,0 +1 @@
<svg width="356" height="280" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 356 280" preserveAspectRatio="none"><defs><style type="text/css">#holder_1602090c11a text { fill:#eceeef;font-weight:bold;font-family:Arial, Helvetica, Open Sans, sans-serif, monospace;font-size:18pt } </style></defs><g id="holder_1602090c11a"><rect width="356" height="280" fill="#55595c"/><g><text y="147.2" x="84.5">Missing thumbnail</text></g></g></svg>

After

Width:  |  Height:  |  Size: 436 B

View File

@@ -0,0 +1,10 @@
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'index' %}">Home</a></li>
{% for p in pages %}
<li class="breadcrumb-item{% if forloop.last %} active {% endif %}" aria-current="page">{% if forloop.last %}{{ p.name }}{% else %}
<a href="{% url 'portal_page' p.path %}">{{ p.name }}</a>
{% endif %}</li>
{% endfor %}
</ol>
</nav>

View File

@@ -0,0 +1,75 @@
{% load static %}
{% load breadcrumbs %}
{% load solo_tags %}
{% get_solo 'portal.SiteConfiguration' as site_conf %}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="{{ site_conf.about }}">
<meta name="author" content="{{ site_conf.site_author }}">
<link rel="icon" type="image/png" href="{% static 'favicon-196x196.png' %}" sizes="196x196">
<link rel="icon" type="image/png" href="{% static 'favicon-128.png' %}" sizes="128x128">
<link rel="icon" type="image/png" href="{% static 'favicon-96x96.png' %}" sizes="96x96">
<link rel="icon" type="image/png" href="{% static 'favicon-32x32.png' %}" sizes="32x32">
<link rel="icon" type="image/png" href="{% static 'favicon-16x16.png' %}" sizes="16x16">
<title>{{ site_conf.site_name }} - {{ page.name }}</title>
<!-- Bootstrap core CSS -->
<link href="{% static "css/dist/bootstrap.min.css" %}" rel="stylesheet">
<link href="{% static "css/fonts.css" %}" rel="stylesheet">
<link href="{% static "css/main.css" %}" rel="stylesheet">
{% if site_conf.common_css %}<link rel="stylesheet" href="{{ site_conf.common_css.url }}">{% endif %}
</head>
<body class="bg-dark">
<div class="container-fluid h-100 d-flex flex-column">
<header>
{% include 'navbar.html' %}
<div class="row collapse bg-dark shadow-sm" id="navbarHeader">
<div class="container">
<div class="row">
<div class="col-sm-8 col-md-7 py-4">
{% if site_conf.about %}<h4 class="text-white">About</h4>
<p class="text-muted">{{ site_conf.about | safe }}</p>{% endif %}
{% breadcrumbs page.path %}
</div>
<div class="col-sm-4 offset-md-1 py-4 text-right">
{% if request.user.is_staff %}<div class="dropdown btn-group">
<button class="btn btn-sm btn-outline-danger dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Edit
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item" href="{% url 'admin:portal_flatpage_change' page.pk %}">Edit page</a>
<a class="dropdown-item" href="{% url 'admin:portal_flatpage_history' page.pk %}">History</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger" href="{% url 'admin:portal_flatpage_delete' page.pk %}">Delete</a>
</div>
</div>{% endif %}
{% include 'login.html' %}
</div>
</div>
</div>
</div>
</header>
</div>
<main>
<div class="album py-5 bg-white">
<div class="container">
{{ content }}
</div>
</div>
</main>
{% include 'footer.html' %}
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="{% static "js/dist/jquery.min.js" %}"></script>
<script src="{% static "js/dist/bootstrap.bundle.min.js" %}"></script>
</body>
</html>

View File

@@ -0,0 +1,17 @@
<footer class="bg-dark text-white">
<div class="container">
<p class="float-right">
<a href="#">Back to top</a>
</p>
<p>{% if site_conf.show_copyright %}&copy; {% now "Y" %} {% endif %}</p>
{% if rolling_stock and site_conf.footer_short %}
{{ site_conf.footer_short | safe }}
{% else %}
{{ site_conf.footer | safe }}
{% endif %}
</div>
{% if site_conf.show_version and not map %}<div class="container">
<p class="small text-muted">Version: {{ site_conf.version }}</p>
</div>{% endif %}
</footer>

View File

@@ -0,0 +1,147 @@
{% load static %}
{% load solo_tags %}
{% get_solo 'portal.SiteConfiguration' as site_conf %}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="{{ site_conf.about }}">
<meta name="author" content="{{ site_conf.site_author }}">
<link rel="icon" type="image/png" href="{% static 'favicon-196x196.png' %}" sizes="196x196">
<link rel="icon" type="image/png" href="{% static 'favicon-128.png' %}" sizes="128x128">
<link rel="icon" type="image/png" href="{% static 'favicon-96x96.png' %}" sizes="96x96">
<link rel="icon" type="image/png" href="{% static 'favicon-32x32.png' %}" sizes="32x32">
<link rel="icon" type="image/png" href="{% static 'favicon-16x16.png' %}" sizes="16x16">
<title>{{ site_conf.site_name }}</title>
<!-- Bootstrap core CSS -->
<link href="{% static "css/dist/bootstrap.min.css" %}" rel="stylesheet">
<link href="{% static "css/fonts.css" %}" rel="stylesheet">
<link href="{% static "css/main.css" %}" rel="stylesheet">
</head>
<body class="bg-dark">
<div class="container-fluid">
<header>
{% include 'navbar.html' %}
<div class="row collapse bg-dark shadow-sm" id="navbarHeader">
<div class="container">
<div class="row">
<div class="col-sm-8 col-md-7 py-4">
{% if site_conf.about %}<h4 class="text-white">About</h4>
<div class="text-justify text-muted">{{ site_conf.about | safe }}</div>{% endif %}
</div>
<div class="col-sm-4 offset-md-1 py-4 text-right">
{% include 'login.html' %}
</div>
</div>
</div>
</div>
</header>
</div>
<main class="bg-light">
<div class="bg-white py-5">
<div class="container">
{{ site_conf.homepage_content | safe }}
</div>
</div>
<a id="maps"></a>
<div class="album py-5">
<div class="container">
<div class="row text-center">
<div class="col-md-12">
{% if tags %}<p>active filter:
{% for t in tags %}<span class="badge" style="background-color: {{ t.color }}; color: {{ t.label_color }};">{{ t.tag }}</span>{# new line is required #}
{% endfor %}</p>{% endif %}
</div>
</div>
<div class="row">
{% for r in rolling_stock %}
<div class="col-md-4">
<div class="card mb-4 shadow-sm">
<a href="{{ r.get_absolute_url }}">
{% for t in r.thumbnail.all %}{% if t.is_thumbnail %}<img src="{{ t.image.url }}" alt="Card image cap">
{% else %}<img class="card-img-top" src="{% static "img/empty.svg" %}">{% endif %}</a>{% endfor %}
<div class="card-body">
<p class="card-text"><strong>{{ r }}</strong></p>{{ r.description|safe|truncatechars_html:256 }}
<p class="card-text">
{% for t in r.tags.all %}<a class="badge"
style="background-color: {{ t.color }}; color: {{ t.label_color }};" href="{# url 'tags_filtered' t.tag #}">{{ t.tag }}</a>{# new line is required #}
{% endfor %}
</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<a class="btn btn-sm btn-outline-secondary" href="{{ r.get_absolute_url }}">View</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:roster_rollingstock_change' r.pk %}">Edit</a>{% endif %}
</div>
<small class="text-muted published-info">Last update: <em>{{ r.updated_time | date:"M d, Y" }}</em></small>
</div>
</div>
</div>
</div>
{% endfor %}
{% if request.user.is_staff and not rolling_stock.has_next %}
<div class="col-md-4">
<div class="card mb-4 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<a class="btn btn-sm btn-secondary" href="{#% url 'admin:rolling_stock_add' %#}">Add a new map</a>
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% if rolling_stock.has_other_pages %}
<nav aria-label="Page navigation example">
<ul class="pagination justify-content-center">
{% if rolling_stock.has_previous %}
<li class="page-item">
<a class="page-link" href="/page/{{ rolling_stock.previous_page_number }}#rolling_stock" tabindex="-1">Previous</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">Previous</span>
</li>
{% endif %}
{% for i in rolling_stock.paginator.page_range %}
{% if rolling_stock.number == i %}
<li class="page-item active">
<span class="page-link">{{ i }}<span class="sr-only">(current)</span></span>
</li>
{% else %}
<li class="page-item"><a class="page-link" href="/page/{{ i }}#rolling_stock">{{ i }}</a></li>
{% endif %}
{% endfor %}
{% if rolling_stock.has_next %}
<li class="page-item">
<a class="page-link" href="/page/{{ rolling_stock.next_page_number }}#rolling_stock" tabindex="-1">Next</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">Next</span>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
</div>
</main>
{% include 'footer.html' %}
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="{% static "js/dist/jquery.min.js" %}"></script>
<script src="{% static "js/dist/bootstrap.bundle.min.js" %}"></script>
</body>
</html>

View File

@@ -0,0 +1,18 @@
{% if request.user.is_staff %}
<div class="btn-group" role="group" aria-label="User actions">
<button type="button" class="btn btn-sm btn-outline-light dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Welcome back, <strong>{{ request.user }}</strong>
</button>
<div class="dropdown-menu">
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="{% url 'admin:index' %}">Admin</a>
<a class="dropdown-item" href="{% url 'admin:portal_siteconfiguration_changelist' %}">Site configuration</a>
<a class="dropdown-item" href="{% url 'admin:password_change' %}">Change password</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger" href="{% url 'admin:logout' %}?next={{ request.path }}">Logout</a>
</div>
</div>
{% else %}
<a class="btn btn-sm btn-outline-light" href="{% url 'admin:login' %}?next={{ request.path }}">Log in</a>
{% endif %}

View File

@@ -0,0 +1,10 @@
<div class="row navbar navbar-dark">
<div class="container d-flex justify-content-between">
<a href="{% url 'index' %}" class="navbar-brand mr-auto">{% if site_conf.site_logo %}<img class="mr-2 d-inline-block" src="{{ site_conf.site_logo.url }}" alt="website logo" id="site-logo" />{% endif %}<strong class="align-middle">{{ site_conf.site_name }}</strong></a>
{% if rolling_stock %}<span class="navbar-brand">{{ rolling_stock.identifier }}</span>{% endif %}
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarHeader" aria-controls="navbarHeader" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>

3
dcc/portal/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

24
dcc/portal/views.py Normal file
View File

@@ -0,0 +1,24 @@
from django.views import View
from django.shortcuts import render
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from roster.models import RollingStock, RollingStockImage
class GetHome(View):
def get(self, request, page=1):
rolling_stock = RollingStock.objects.all()
thumbnails = RollingStockImage.objects.filter(is_thumbnail=True)
paginator = Paginator(rolling_stock, 6)
try:
rolling_stock = paginator.page(page)
except PageNotAnInteger:
rolling_stock = paginator.page(1)
except EmptyPage:
rolling_stock = paginator.page(paginator.num_pages)
return render(request, 'home.html', {
'rolling_stock': rolling_stock,
'thumbnails': thumbnails
})

View File

@@ -0,0 +1,19 @@
# Generated by Django 4.0.3 on 2022-04-08 22:45
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('roster', '0002_alter_rollingstockimage_unique_together_and_more'),
]
operations = [
migrations.AlterField(
model_name='rollingstockimage',
name='rolling_stock',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='thumbnail', to='roster.rollingstock'),
),
]

View File

@@ -121,7 +121,10 @@ class RollingStockDocument(models.Model):
class RollingStockImage(models.Model): class RollingStockImage(models.Model):
rolling_stock = models.ForeignKey(RollingStock, on_delete=models.CASCADE) rolling_stock = models.ForeignKey(
RollingStock,
on_delete=models.CASCADE,
related_name="thumbnail")
image = models.ImageField(upload_to="images/", null=True, blank=True) image = models.ImageField(upload_to="images/", null=True, blank=True)
is_thumbnail = models.BooleanField() is_thumbnail = models.BooleanField()