From 7422f1147cb69f6bdf2d2cc97fe718e4bea3103e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Thu, 7 Dec 2023 22:28:05 -0300 Subject: [PATCH 001/244] Initial commit --- .gitignore | 160 +++++++++++++++++++++++++++++++++++++++++++++++++++++ LICENSE | 21 +++++++ README.md | 2 + 3 files changed, 183 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..68bc17f --- /dev/null +++ b/.gitignore @@ -0,0 +1,160 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..25adb03 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Fabián Abarca + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7655b2b --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# gtfs-screens +Implementación de pantallas con GTFS From cef08de61523c6f5da039f099a8353387876548c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Fri, 15 Dec 2023 14:30:51 -0300 Subject: [PATCH 002/244] Create ARCHITECTURE.md Esbozo de la estructura --- ARCHITECTURE.md | 69 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 ARCHITECTURE.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..f30c833 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,69 @@ +# Descripción del servidor de aplicaciones para pantallas con información GTFS Realtime + +## Tareas esenciales del servidor + +1. Periódicamente recopilar los `FeedMessage` de GTFS Realtime (este es el servidor "hermano" `gtfs-realtime`). +2. Organizar la información contenida para cada pantalla, según sea relevante (ejemplo: clasificar los viajes que van a pasar por esa parada (asumiendo que las pantallas están en o cerca de una parada), o anuncios de alertas que sean relevantes para el servicio en esa pantalla, etc.). +3. (Opcional) Cargar la plantilla HTML en el navegador de la pantalla cuando se carga el sitio por primera vez (ejemplo: al prender las pantallas). +4. Actualizar la información desplegada en las pantallas utilizando los WebSockets de Django. + +## Aplicaciones y sitios del proyecto de Django + +### Django app: `website` + +- `/`: Página de bienvenida del sistema + +#### Modelos asociados + +`class User`: Información de usuarios del sistema + +### Django app: `screens` + +- `/pantallas/`: Lista de pantallas del sistema +- `/pantallas/crear`: Página de creación de nueva pantalla +- `/pantallas//`: Visualización de la pantalla (contenido) +- `/pantallas//configuracion/`: Sitio de configuración de la pantalla `screen_id` + + +Nota: las pantallas por ahora asumimos que son Raspberry Pi en [modo kiosko](https://www.raspberrypi.com/tutorials/how-to-use-a-raspberry-pi-in-kiosk-mode/) que utilizan [Chromium](https://www.chromium.org/chromium-projects/) para navegar el sitio. + +#### Modelos asociados + +- `class Screen`: Información de cada pantalla + - `screen_id` + - `name` + - `address` + - `location` (ejemplo: 9.93752787687643, -84.04463400265841 con PostGIS y GeoDjango) + - `size` (ejemplo: 32") + - `ratio` (ejemplo: 16:9) + - `orientation` (ejemplo: `VERTICAL`, `HORIZONTAL`) + - `has_sound` (ejemplo: `True`, `False`, booleano) + +### Django app: `gtfs` + +- `gtfs/`: +- `/gtfs/schedule/`: Información y configuración del *feed* GTFS Schedule utilizado +- `/gtfs/realtime/`: Información y configuración del *feed* GTFS Realtime utilizado + + #### Modelos asociados + +- (Todos los modelos de GTFS Schedule, como `Agency`, `Route`, etc.) +- (Todos los modelos de GTFS Realtime, como `VehiclePosition`, etc.) +- `class Company` + - `company_id` + - `name` + - `address` + - `phone` + - `email` + - `website` + - `logo` +- `class Schedule` + - `company_id` (ejemplo: "MBTA") + - `schedule_url`: *feed* estático + - `last_updated` +- `class Realtime` + - `company_id` (ForeignKey) (ejemplo: "MBTA") + - `alerts_url`: *feed message* de alertas del servicio + - `trip_updates_url`: *feed message* de actualizaciones de los viajes + - `vehicle_positions_url`: *feed message* de posición de los vehículos + - `alerts_last_updated` From 347c611aece668795ec7954dae51f228bb56084f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Fri, 15 Dec 2023 14:52:50 -0300 Subject: [PATCH 003/244] Update ARCHITECTURE.md --- ARCHITECTURE.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index f30c833..0aeb51d 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -15,7 +15,8 @@ #### Modelos asociados -`class User`: Información de usuarios del sistema +- `class User`: Información de usuarios del sistema + - `type` ### Django app: `screens` @@ -41,9 +42,10 @@ Nota: las pantallas por ahora asumimos que son Raspberry Pi en [modo kiosko](htt ### Django app: `gtfs` -- `gtfs/`: +- `/gtfs/`: - `/gtfs/schedule/`: Información y configuración del *feed* GTFS Schedule utilizado - `/gtfs/realtime/`: Información y configuración del *feed* GTFS Realtime utilizado +- `/gtfs/company/`: Las pantallas pueden desplegar información de uno o más *feeds* provenientes de una o más compañías #### Modelos asociados From b542f03e05b32643760017a57ec8ee56cccf2b6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Mon, 18 Dec 2023 15:39:39 -0300 Subject: [PATCH 004/244] Update ARCHITECTURE.md --- ARCHITECTURE.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 0aeb51d..2575da2 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -11,7 +11,11 @@ ### Django app: `website` +> Manejo de páginas misceláneas del sitio. + - `/`: Página de bienvenida del sistema +- `/sobre/`: Información del proyecto +- `/perfil/`: Perfil de usuario registrado #### Modelos asociados @@ -20,9 +24,11 @@ ### Django app: `screens` +> Páginas de administración de las pantallas (HTML) y actualización de datos en tiempo real (WebSockets) + - `/pantallas/`: Lista de pantallas del sistema -- `/pantallas/crear`: Página de creación de nueva pantalla -- `/pantallas//`: Visualización de la pantalla (contenido) +- `/pantallas/crear/`: Página de creación de nueva pantalla +- `/pantallas//`: Visualización de la pantalla (**contenido de la pantalla**) - `/pantallas//configuracion/`: Sitio de configuración de la pantalla `screen_id` @@ -42,6 +48,8 @@ Nota: las pantallas por ahora asumimos que son Raspberry Pi en [modo kiosko](htt ### Django app: `gtfs` +> Páginas de administación de información GTFS Schedule y GTFS Realtime. + - `/gtfs/`: - `/gtfs/schedule/`: Información y configuración del *feed* GTFS Schedule utilizado - `/gtfs/realtime/`: Información y configuración del *feed* GTFS Realtime utilizado From 168da34add2a6535c1f3d74735524178a4128ae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Mon, 18 Dec 2023 16:03:00 -0300 Subject: [PATCH 005/244] =?UTF-8?q?Creaci=C3=B3n=20y=20configuraci=C3=B3n?= =?UTF-8?q?=20b=C3=A1sica=20de=20proyecto=20de=20Django?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + gtfs/__init__.py | 0 gtfs/admin.py | 3 + gtfs/apps.py | 6 ++ gtfs/migrations/__init__.py | 0 gtfs/models.py | 3 + gtfs/tests.py | 3 + gtfs/views.py | 3 + gtfs_screens/__init__.py | 0 gtfs_screens/asgi.py | 16 +++++ gtfs_screens/settings.py | 127 +++++++++++++++++++++++++++++++++ gtfs_screens/urls.py | 22 ++++++ gtfs_screens/wsgi.py | 16 +++++ manage.py | 22 ++++++ screens/__init__.py | 0 screens/admin.py | 3 + screens/apps.py | 6 ++ screens/migrations/__init__.py | 0 screens/models.py | 3 + screens/tests.py | 3 + screens/views.py | 3 + website/__init__.py | 0 website/admin.py | 3 + website/apps.py | 6 ++ website/migrations/__init__.py | 0 website/models.py | 3 + website/tests.py | 3 + website/views.py | 3 + 28 files changed, 260 insertions(+) create mode 100644 gtfs/__init__.py create mode 100644 gtfs/admin.py create mode 100644 gtfs/apps.py create mode 100644 gtfs/migrations/__init__.py create mode 100644 gtfs/models.py create mode 100644 gtfs/tests.py create mode 100644 gtfs/views.py create mode 100644 gtfs_screens/__init__.py create mode 100644 gtfs_screens/asgi.py create mode 100644 gtfs_screens/settings.py create mode 100644 gtfs_screens/urls.py create mode 100644 gtfs_screens/wsgi.py create mode 100755 manage.py create mode 100644 screens/__init__.py create mode 100644 screens/admin.py create mode 100644 screens/apps.py create mode 100644 screens/migrations/__init__.py create mode 100644 screens/models.py create mode 100644 screens/tests.py create mode 100644 screens/views.py create mode 100644 website/__init__.py create mode 100644 website/admin.py create mode 100644 website/apps.py create mode 100644 website/migrations/__init__.py create mode 100644 website/models.py create mode 100644 website/tests.py create mode 100644 website/views.py diff --git a/.gitignore b/.gitignore index 68bc17f..d2fd289 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,9 @@ local_settings.py db.sqlite3 db.sqlite3-journal +# macOS stuff: +.DS_Store + # Flask stuff: instance/ .webassets-cache diff --git a/gtfs/__init__.py b/gtfs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gtfs/admin.py b/gtfs/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/gtfs/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/gtfs/apps.py b/gtfs/apps.py new file mode 100644 index 0000000..2db4f65 --- /dev/null +++ b/gtfs/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class GtfsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "gtfs" diff --git a/gtfs/migrations/__init__.py b/gtfs/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gtfs/models.py b/gtfs/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/gtfs/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/gtfs/tests.py b/gtfs/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/gtfs/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/gtfs/views.py b/gtfs/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/gtfs/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/gtfs_screens/__init__.py b/gtfs_screens/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gtfs_screens/asgi.py b/gtfs_screens/asgi.py new file mode 100644 index 0000000..e338716 --- /dev/null +++ b/gtfs_screens/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for gtfs_screens project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gtfs_screens.settings") + +application = get_asgi_application() diff --git a/gtfs_screens/settings.py b/gtfs_screens/settings.py new file mode 100644 index 0000000..1e090ef --- /dev/null +++ b/gtfs_screens/settings.py @@ -0,0 +1,127 @@ +""" +Django settings for gtfs_screens project. + +Generated by 'django-admin startproject' using Django 5.0. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.0/ref/settings/ +""" + +from pathlib import Path +from decouple import config, Csv + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = config("SECRET_KEY") + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = config("DEBUG", cast=bool) + +ALLOWED_HOSTS = config("ALLOWED_HOSTS", cast=Csv()) + + +# Application definition + +INSTALLED_APPS = [ + "website.apps.WebsiteConfig", + "gtfs.apps.GtfsConfig", + "screens.apps.ScreensConfig", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", +] + +MIDDLEWARE = [ + "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", +] + +ROOT_URLCONF = "gtfs_screens.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", + ], + }, + }, +] + +WSGI_APPLICATION = "gtfs_screens.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/5.0/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.0/topics/i18n/ + +LANGUAGE_CODE = "es-cr" + +TIME_ZONE = "America/Costa_Rica" + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.0/howto/static-files/ + +STATIC_URL = "static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/gtfs_screens/urls.py b/gtfs_screens/urls.py new file mode 100644 index 0000000..f0e9588 --- /dev/null +++ b/gtfs_screens/urls.py @@ -0,0 +1,22 @@ +""" +URL configuration for gtfs_screens project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path("admin/", admin.site.urls), +] diff --git a/gtfs_screens/wsgi.py b/gtfs_screens/wsgi.py new file mode 100644 index 0000000..27a6989 --- /dev/null +++ b/gtfs_screens/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for gtfs_screens project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gtfs_screens.settings") + +application = get_wsgi_application() diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..b122dc7 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gtfs_screens.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/screens/__init__.py b/screens/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/screens/admin.py b/screens/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/screens/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/screens/apps.py b/screens/apps.py new file mode 100644 index 0000000..de60361 --- /dev/null +++ b/screens/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ScreensConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "screens" diff --git a/screens/migrations/__init__.py b/screens/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/screens/models.py b/screens/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/screens/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/screens/tests.py b/screens/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/screens/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/screens/views.py b/screens/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/screens/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/website/__init__.py b/website/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/website/admin.py b/website/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/website/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/website/apps.py b/website/apps.py new file mode 100644 index 0000000..bc26c09 --- /dev/null +++ b/website/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class WebsiteConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "website" diff --git a/website/migrations/__init__.py b/website/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/website/models.py b/website/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/website/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/website/tests.py b/website/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/website/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/website/views.py b/website/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/website/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From 772951c1b1df8fe223932aee42d4ed668eec1b43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Mon, 18 Dec 2023 17:16:49 -0300 Subject: [PATCH 006/244] =?UTF-8?q?Creaci=C3=B3n=20de=20URLs=20y=20plantil?= =?UTF-8?q?las=20para=20mapa=20del=20sitio=20en=20cada=20app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gtfs/templates/company.html | 1 + gtfs/templates/gtfs.html | 10 ++++++++++ gtfs/templates/realtime.html | 14 ++++++++++++++ gtfs/templates/schedule.html | 9 +++++++++ gtfs/urls.py | 10 ++++++++++ gtfs/views.py | 16 ++++++++++++++++ gtfs_screens/urls.py | 5 ++++- screens/templates/create_screen.html | 20 ++++++++++++++++++++ screens/templates/edit_screen.html | 5 +++++ screens/templates/screen.html | 12 ++++++++++++ screens/templates/screens.html | 5 +++++ screens/urls.py | 10 ++++++++++ screens/views.py | 18 ++++++++++++++++++ website/templates/about.html | 1 + website/templates/index.html | 4 ++++ website/templates/profile.html | 1 + website/urls.py | 9 +++++++++ website/views.py | 10 ++++++++++ 18 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 gtfs/templates/company.html create mode 100644 gtfs/templates/gtfs.html create mode 100644 gtfs/templates/realtime.html create mode 100644 gtfs/templates/schedule.html create mode 100644 gtfs/urls.py create mode 100644 screens/templates/create_screen.html create mode 100644 screens/templates/edit_screen.html create mode 100644 screens/templates/screen.html create mode 100644 screens/templates/screens.html create mode 100644 screens/urls.py create mode 100644 website/templates/about.html create mode 100644 website/templates/index.html create mode 100644 website/templates/profile.html create mode 100644 website/urls.py diff --git a/gtfs/templates/company.html b/gtfs/templates/company.html new file mode 100644 index 0000000..8a9faae --- /dev/null +++ b/gtfs/templates/company.html @@ -0,0 +1 @@ +

Información sobre la compañía/institución/agencia que publica el GTFS

\ No newline at end of file diff --git a/gtfs/templates/gtfs.html b/gtfs/templates/gtfs.html new file mode 100644 index 0000000..a0345a2 --- /dev/null +++ b/gtfs/templates/gtfs.html @@ -0,0 +1,10 @@ +

Información general sobre GTFS

+ +

+ El General Transit Feed Specification (GTFS) define un formato común para la representación de información de transporte público. + El formato es usado por cientos de agencias de transporte público y aplicaciones de viaje en todo el mundo. +

+ +

+ Aquí también está la explicación de las versiones Schedule y Realtime de GTFS. +

\ No newline at end of file diff --git a/gtfs/templates/realtime.html b/gtfs/templates/realtime.html new file mode 100644 index 0000000..0af864a --- /dev/null +++ b/gtfs/templates/realtime.html @@ -0,0 +1,14 @@ +

Sitio de configuración de GTFS Realtime

+ +

+ Un formulario con los campos necesarios para configurar el sitio GTFS Realtime. +

+ +

+ Los más importantes: +

    +
  • alerts_url
  • +
  • trip_updates_url
  • +
  • vehicle_positions_url
  • +
+

\ No newline at end of file diff --git a/gtfs/templates/schedule.html b/gtfs/templates/schedule.html new file mode 100644 index 0000000..4167dc7 --- /dev/null +++ b/gtfs/templates/schedule.html @@ -0,0 +1,9 @@ +

Sitio de configuración GTFS Schedule

+ +

+ Un formulario con los campos necesarios para configurar el sitio GTFS Schedule. +

+ +

El más importante: schedule_url

+ +

Cosas para hacer por aquí: validad URL, hacer un request de prueba, también para ver datos históricos del feed, etc.

\ No newline at end of file diff --git a/gtfs/urls.py b/gtfs/urls.py new file mode 100644 index 0000000..f11dc8e --- /dev/null +++ b/gtfs/urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + path("", views.gtfs), + path("schedule/", views.schedule, name="schedule"), + path("realtime/", views.realtime, name="realtime"), + path("compania/", views.company, name="company"), +] \ No newline at end of file diff --git a/gtfs/views.py b/gtfs/views.py index 91ea44a..0203f3f 100644 --- a/gtfs/views.py +++ b/gtfs/views.py @@ -1,3 +1,19 @@ from django.shortcuts import render # Create your views here. + + +def gtfs(request): + return render(request, "gtfs.html") + + +def schedule(request): + return render(request, "schedule.html") + + +def realtime(request): + return render(request, "realtime.html") + + +def company(request): + return render(request, "company.html") \ No newline at end of file diff --git a/gtfs_screens/urls.py b/gtfs_screens/urls.py index f0e9588..c23322f 100644 --- a/gtfs_screens/urls.py +++ b/gtfs_screens/urls.py @@ -15,8 +15,11 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import path, include urlpatterns = [ path("admin/", admin.site.urls), + path("", include("website.urls"), name="index"), + path("gtfs/", include("gtfs.urls"), name="gtfs_page"), + path("pantallas/", include("screens.urls"), name="screens_page"), ] diff --git a/screens/templates/create_screen.html b/screens/templates/create_screen.html new file mode 100644 index 0000000..e323277 --- /dev/null +++ b/screens/templates/create_screen.html @@ -0,0 +1,20 @@ +

Página de creación/registro/configuración de una nueva pantalla

+ +

+ Formulario con toda la información necesaria para la pantalla. +

+ +
    +
  • screen_id
  • +
  • name
  • +
  • address
  • +
  • location (ejemplo: 9.93752787687643, -84.04463400265841 con PostGIS y GeoDjango)
  • +
  • size (ejemplo: 32")
  • +
  • ratio (ejemplo: 16:9)
  • +
  • orientation (ejemplo: VERTICAL, HORIZONTAL)
  • +
  • has_sound
  • +
+ +

+ Aquí hay que hacer una validación de los datos ingresados por el usuario. +

\ No newline at end of file diff --git a/screens/templates/edit_screen.html b/screens/templates/edit_screen.html new file mode 100644 index 0000000..017e2f1 --- /dev/null +++ b/screens/templates/edit_screen.html @@ -0,0 +1,5 @@ +

Página de edición de los datos de configuración de cada pantalla

+ +

+ Pantalla: {{ screen_id }} +

\ No newline at end of file diff --git a/screens/templates/screen.html b/screens/templates/screen.html new file mode 100644 index 0000000..68972da --- /dev/null +++ b/screens/templates/screen.html @@ -0,0 +1,12 @@ +

Ruta UCR

+ +

Faltan 5 minutos para el bus

+ +

+ Pantalla: {{ screen_id }} +

+ + \ No newline at end of file diff --git a/screens/templates/screens.html b/screens/templates/screens.html new file mode 100644 index 0000000..6e5710c --- /dev/null +++ b/screens/templates/screens.html @@ -0,0 +1,5 @@ +

Lista administrativa de todas las pantallas

+ +

+ Aquí pueden haber tablas con la lista, mapas con la ubicación de las pantallas, etc. +

\ No newline at end of file diff --git a/screens/urls.py b/screens/urls.py new file mode 100644 index 0000000..d54e82d --- /dev/null +++ b/screens/urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + path("", views.screens, name="screens"), + path("crear/", views.create_screen, name="create_screen"), + path("/", views.screen, name="screen"), + path("/editar/", views.edit_screen, name="edit_screen"), +] \ No newline at end of file diff --git a/screens/views.py b/screens/views.py index 91ea44a..b8fec4a 100644 --- a/screens/views.py +++ b/screens/views.py @@ -1,3 +1,21 @@ from django.shortcuts import render # Create your views here. + + +def screens(request): + return render(request, "screens.html") + + +def create_screen(request): + return render(request, "create_screen.html") + + +def screen(request, screen_id): + context = {"screen_id": screen_id} + return render(request, "screen.html", context) + + +def edit_screen(request, screen_id): + context = {"screen_id": screen_id} + return render(request, "edit_screen.html", context) diff --git a/website/templates/about.html b/website/templates/about.html new file mode 100644 index 0000000..f592844 --- /dev/null +++ b/website/templates/about.html @@ -0,0 +1 @@ +

Información sobre el proyecto

\ No newline at end of file diff --git a/website/templates/index.html b/website/templates/index.html new file mode 100644 index 0000000..ce693d8 --- /dev/null +++ b/website/templates/index.html @@ -0,0 +1,4 @@ +

Esta es la página principal

+

+ Aquí habrá una referencia a la página de inicio de sesión y a la página de registro. +

\ No newline at end of file diff --git a/website/templates/profile.html b/website/templates/profile.html new file mode 100644 index 0000000..b4d5a7d --- /dev/null +++ b/website/templates/profile.html @@ -0,0 +1 @@ +

Página de perfil del usuario

\ No newline at end of file diff --git a/website/urls.py b/website/urls.py new file mode 100644 index 0000000..45edc4f --- /dev/null +++ b/website/urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + path("", views.index, name="index"), + path("sobre/", views.about, name="about"), + path("perfil/", views.profile, name="profile"), +] diff --git a/website/views.py b/website/views.py index 91ea44a..7ce0fe1 100644 --- a/website/views.py +++ b/website/views.py @@ -1,3 +1,13 @@ from django.shortcuts import render # Create your views here. + + +def index(request): + return render(request, "index.html") + +def about(request): + return render(request, "about.html") + +def profile(request): + return render(request, "profile.html") From c3006a7f30f2507a60f21c2b6820f74c7ffe0e99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Mon, 18 Dec 2023 18:25:57 -0300 Subject: [PATCH 007/244] Create DEVELOPMENT.md Esbozo de las tareas del sistema. --- DEVELOPMENT.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 DEVELOPMENT.md diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..013e259 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,25 @@ +# Desarrollo funcional + +## Recopilación de `FeedMessage` cada $N$ segundos + - Opciones: Apache Airflow o como proceso del sistema con paquetes de Python para tareas repetitivas (versión más fácil). + - Es necesario usar los paquetes de Google para procesar los `.pb` y convertirlo a un diccionario o un JSON o un DataFrame de Pandas. + +## *Script* de clasificación y ordenamiento de GTFS Realtime + +Es necesario separar los datos que son relevantes para cada parada (¿tópicos?). + +- Es posible trabajarlo con puro Python (sin preocuparse todavía de Django) +- Aquí es importante la eficiencia computacional (un poco) +- Herramientas posibles: ¿Pandas? (muy lento), o guardar directamente a la base de datos y ordernarlo desde ahí. +- Depende del análisis del GTFS del sistema de buses, porque hay que analizar cuáles buses van a pasar o ya pasaron por una parada en particular, etc. +- También hay que definir el formato de lo datos que serán compartidos (ya no es GTFS Realtime, necesariamente). + +## Actualización de la información de cada pantalla + +(Django WebSockets) + +## Configuración del hardware de las pantallas + +(Raspberry Pi) + +Ejemplo de primer prototipo: Soda de Ingeniería (no requiere protección). From 5c76e0e378c9d4d0a8c8a5301501ed75f96a5600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Thu, 18 Jan 2024 10:35:28 -0300 Subject: [PATCH 008/244] Actualizar con el equipo de trabajo --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 7655b2b..6903f0f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,24 @@ # gtfs-screens Implementación de pantallas con GTFS + +## Equipo de trabajo + +### Organización de la información GTFS Realtime + +> "La oficina de correos" + +Obtener periódicamente la información del _feed_ GTFS Realtime (el servidor del proyecto `gtfs-realtime`), ordenarla según pantallas o servicios y distribuirla. + +- David Segura +- Josué Vargas + +### Implementación de pantallas y despliegue de información + +> "La entrega de los paquetes de correo" + +Con la información asignada a cada pantalla y la plantilla para desplegar información, actualizar las pantallas cada $N$ segundos. Además, claramente, la implementación propiamente de las pantallas. + +Primera implementación: soda de la Facultad de Ingeniería, porque: 1. ya existe una pantalla ahí, 2. es más fácil pedir permisos, 3. no requiere protección contra la intemperie, 4. es nuestra Facultad. + +- José David Murillo +- Mateo Ortigoza From 3aff96992fc10b7574f10d13571fa207e1123b79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Thu, 1 Feb 2024 16:29:43 -0300 Subject: [PATCH 009/244] Create requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..24ce15a --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +numpy From 8196fc5948460e84ce1db7fa6b3ed312b35c1a59 Mon Sep 17 00:00:00 2001 From: Jose David Murillo Date: Thu, 1 Feb 2024 13:35:40 -0600 Subject: [PATCH 010/244] chg: dev: prueba Signed-off-by: Jose David Murillo --- test.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 test.txt diff --git a/test.txt b/test.txt new file mode 100644 index 0000000..5c1b149 --- /dev/null +++ b/test.txt @@ -0,0 +1 @@ +hola From 3d7a6d82b090c24cd5132b53e4bc88d48f0af62b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Thu, 1 Feb 2024 16:42:08 -0300 Subject: [PATCH 011/244] Update requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 24ce15a..fd81a45 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -numpy +python-decouple From 1f89ae0a90cb91092f35aaab6e8921e9821f8157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Thu, 1 Feb 2024 17:33:46 -0300 Subject: [PATCH 012/244] Update requirements.txt with Django (of course) --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index fd81a45..d9f850c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ +django python-decouple From baec114b1d124c2820e45f0c0e244d079836fcf5 Mon Sep 17 00:00:00 2001 From: Jose David Murillo Date: Thu, 1 Feb 2024 14:52:13 -0600 Subject: [PATCH 013/244] chg: dev: First update Signed-off-by: Jose David Murillo --- gtfs/templates/schedule.html | 8 +++++++- gtfs/views.py | 8 +++++++- requirements.txt | 2 +- test.txt | 1 - 4 files changed, 15 insertions(+), 4 deletions(-) delete mode 100644 test.txt diff --git a/gtfs/templates/schedule.html b/gtfs/templates/schedule.html index 4167dc7..9c7c01c 100644 --- a/gtfs/templates/schedule.html +++ b/gtfs/templates/schedule.html @@ -6,4 +6,10 @@

Sitio de configuración GTFS Schedule

El más importante: schedule_url

-

Cosas para hacer por aquí: validad URL, hacer un request de prueba, también para ver datos históricos del feed, etc.

\ No newline at end of file +

Cosas para hacer por aquí: validad URL, hacer un request de prueba, también para ver datos históricos del feed, etc.

+ +
    + {% for user in users %} +
  1. {{ user.first_name }}: {{ user.last_name }}
  2. + {% endfor %} +
diff --git a/gtfs/views.py b/gtfs/views.py index 0203f3f..1291d2a 100644 --- a/gtfs/views.py +++ b/gtfs/views.py @@ -1,4 +1,5 @@ from django.shortcuts import render +from django.contrib.auth.models import User # Create your views here. @@ -8,7 +9,12 @@ def gtfs(request): def schedule(request): - return render(request, "schedule.html") + users = User.objects.all() + print(users) + context = { + "users": users, + } + return render(request, "schedule.html", context) def realtime(request): diff --git a/requirements.txt b/requirements.txt index 24ce15a..fd81a45 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -numpy +python-decouple diff --git a/test.txt b/test.txt deleted file mode 100644 index 5c1b149..0000000 --- a/test.txt +++ /dev/null @@ -1 +0,0 @@ -hola From e11f81d8a89aeee46428afd03e0affbf154b129c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Thu, 1 Feb 2024 17:33:46 -0300 Subject: [PATCH 014/244] Update requirements.txt with Django (of course) --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index fd81a45..d9f850c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ +django python-decouple From 8f14a2bc0f8c8ce289871ded8ab9e205fbf416c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Fri, 2 Feb 2024 13:19:06 -0300 Subject: [PATCH 015/244] Ejemplo de tiempo de llegada aleatorio --- screens/templates/screen.html | 2 +- screens/views.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/screens/templates/screen.html b/screens/templates/screen.html index 68972da..3849282 100644 --- a/screens/templates/screen.html +++ b/screens/templates/screen.html @@ -1,6 +1,6 @@

Ruta UCR

-

Faltan 5 minutos para el bus

+

Faltan {{ minutes }} minutos para el bus

Pantalla: {{ screen_id }} diff --git a/screens/views.py b/screens/views.py index b8fec4a..0cf627a 100644 --- a/screens/views.py +++ b/screens/views.py @@ -1,4 +1,5 @@ from django.shortcuts import render +import random # Create your views here. @@ -12,7 +13,10 @@ def create_screen(request): def screen(request, screen_id): - context = {"screen_id": screen_id} + seed = screen_id + random.seed(seed) + minutes = random.randint(0, 30) + context = {"screen_id": screen_id, "minutes": minutes} return render(request, "screen.html", context) From 1edc92080bf9887ff9d36e9496a42cf7acb152e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Fri, 2 Feb 2024 13:26:23 -0300 Subject: [PATCH 016/244] =?UTF-8?q?Actualizaci=C3=B3n=20de=20paquetes=20de?= =?UTF-8?q?=20Python=20con=20lo=20requerido=20en=20la=20app=20de=20Realtim?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/requirements.txt b/requirements.txt index d9f850c..723e6a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,6 @@ django python-decouple +configparser +schedule +pandas +gtfs-realtime-bindings From 7e0a11736cad50f2db8e78e6f4036f328656087b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Fri, 2 Feb 2024 13:30:10 -0300 Subject: [PATCH 017/244] =?UTF-8?q?Creaci=C3=B3n=20de=20nueva=20app=20para?= =?UTF-8?q?=20manejo=20de=20GTFS=20Realtime?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gtfs_screens/settings.py | 1 + realtime/__init__.py | 0 realtime/admin.py | 3 +++ realtime/apps.py | 6 ++++++ realtime/migrations/__init__.py | 0 realtime/models.py | 3 +++ realtime/tests.py | 3 +++ realtime/views.py | 3 +++ 8 files changed, 19 insertions(+) create mode 100644 realtime/__init__.py create mode 100644 realtime/admin.py create mode 100644 realtime/apps.py create mode 100644 realtime/migrations/__init__.py create mode 100644 realtime/models.py create mode 100644 realtime/tests.py create mode 100644 realtime/views.py diff --git a/gtfs_screens/settings.py b/gtfs_screens/settings.py index 1e090ef..998a8f3 100644 --- a/gtfs_screens/settings.py +++ b/gtfs_screens/settings.py @@ -34,6 +34,7 @@ INSTALLED_APPS = [ "website.apps.WebsiteConfig", "gtfs.apps.GtfsConfig", + "realtime.apps.RealtimeConfig", "screens.apps.ScreensConfig", "django.contrib.admin", "django.contrib.auth", diff --git a/realtime/__init__.py b/realtime/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/realtime/admin.py b/realtime/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/realtime/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/realtime/apps.py b/realtime/apps.py new file mode 100644 index 0000000..643c3f9 --- /dev/null +++ b/realtime/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class RealtimeConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "realtime" diff --git a/realtime/migrations/__init__.py b/realtime/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/realtime/models.py b/realtime/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/realtime/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/realtime/tests.py b/realtime/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/realtime/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/realtime/views.py b/realtime/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/realtime/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From b4492064aff3fa5b67a3245ea75f766ff2689ef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Tue, 6 Feb 2024 11:00:04 -0300 Subject: [PATCH 018/244] Configurar nueva base de datos en PostgreSQL con PostGIS --- gtfs_screens/settings.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/gtfs_screens/settings.py b/gtfs_screens/settings.py index 998a8f3..f4db967 100644 --- a/gtfs_screens/settings.py +++ b/gtfs_screens/settings.py @@ -12,6 +12,7 @@ from pathlib import Path from decouple import config, Csv +import platform # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -80,11 +81,16 @@ DATABASES = { "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", - } + "ENGINE": "django.contrib.gis.db.backends.postgis", + "NAME": config('DB_NAME'), + "USER": config('DB_USER'), + }, } +if not (platform.platform() == "Linux" or platform.machine() == "x86_64"): + GDAL_LIBRARY_PATH = config('GDAL_LIBRARY_PATH') + GEOS_LIBRARY_PATH = config('GEOS_LIBRARY_PATH') + # Password validation # https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators From 3b3a05a2d8dfd64a88541533a270ae936d184de7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Wed, 7 Feb 2024 09:12:18 -0300 Subject: [PATCH 019/244] Cambiar el nombre del proyecto a gtfs2screens por consistencia interna y con otros proyectos --- {gtfs_screens => gtfs2screens}/__init__.py | 0 {gtfs_screens => gtfs2screens}/asgi.py | 4 ++-- {gtfs_screens => gtfs2screens}/settings.py | 6 +++--- {gtfs_screens => gtfs2screens}/urls.py | 2 +- {gtfs_screens => gtfs2screens}/wsgi.py | 4 ++-- manage.py | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) rename {gtfs_screens => gtfs2screens}/__init__.py (100%) rename {gtfs_screens => gtfs2screens}/asgi.py (74%) rename {gtfs_screens => gtfs2screens}/settings.py (96%) rename {gtfs_screens => gtfs2screens}/urls.py (95%) rename {gtfs_screens => gtfs2screens}/wsgi.py (74%) diff --git a/gtfs_screens/__init__.py b/gtfs2screens/__init__.py similarity index 100% rename from gtfs_screens/__init__.py rename to gtfs2screens/__init__.py diff --git a/gtfs_screens/asgi.py b/gtfs2screens/asgi.py similarity index 74% rename from gtfs_screens/asgi.py rename to gtfs2screens/asgi.py index e338716..697340a 100644 --- a/gtfs_screens/asgi.py +++ b/gtfs2screens/asgi.py @@ -1,5 +1,5 @@ """ -ASGI config for gtfs_screens project. +ASGI config for gtfs2screens project. It exposes the ASGI callable as a module-level variable named ``application``. @@ -11,6 +11,6 @@ from django.core.asgi import get_asgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gtfs_screens.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gtfs2screens.settings") application = get_asgi_application() diff --git a/gtfs_screens/settings.py b/gtfs2screens/settings.py similarity index 96% rename from gtfs_screens/settings.py rename to gtfs2screens/settings.py index f4db967..b37d052 100644 --- a/gtfs_screens/settings.py +++ b/gtfs2screens/settings.py @@ -1,5 +1,5 @@ """ -Django settings for gtfs_screens project. +Django settings for gtfs2screens project. Generated by 'django-admin startproject' using Django 5.0. @@ -55,7 +55,7 @@ "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = "gtfs_screens.urls" +ROOT_URLCONF = "gtfs2screens.urls" TEMPLATES = [ { @@ -73,7 +73,7 @@ }, ] -WSGI_APPLICATION = "gtfs_screens.wsgi.application" +WSGI_APPLICATION = "gtfs2screens.wsgi.application" # Database diff --git a/gtfs_screens/urls.py b/gtfs2screens/urls.py similarity index 95% rename from gtfs_screens/urls.py rename to gtfs2screens/urls.py index c23322f..f7dbe5c 100644 --- a/gtfs_screens/urls.py +++ b/gtfs2screens/urls.py @@ -1,5 +1,5 @@ """ -URL configuration for gtfs_screens project. +URL configuration for gtfs2screens project. The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/5.0/topics/http/urls/ diff --git a/gtfs_screens/wsgi.py b/gtfs2screens/wsgi.py similarity index 74% rename from gtfs_screens/wsgi.py rename to gtfs2screens/wsgi.py index 27a6989..de2ad20 100644 --- a/gtfs_screens/wsgi.py +++ b/gtfs2screens/wsgi.py @@ -1,5 +1,5 @@ """ -WSGI config for gtfs_screens project. +WSGI config for gtfs2screens project. It exposes the WSGI callable as a module-level variable named ``application``. @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gtfs_screens.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gtfs2screens.settings") application = get_wsgi_application() diff --git a/manage.py b/manage.py index b122dc7..1844b0d 100755 --- a/manage.py +++ b/manage.py @@ -6,7 +6,7 @@ def main(): """Run administrative tasks.""" - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gtfs_screens.settings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gtfs2screens.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: From 141e55041fad1ad75bf1b37bbf7818b51448ffa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Mon, 12 Feb 2024 09:50:09 -0300 Subject: [PATCH 020/244] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6903f0f..66d8a11 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ -# gtfs-screens -Implementación de pantallas con GTFS +# gtfs2screens + +Implementación de pantallas con información en tiempo real de transporte público a partir de la especificación GTFS ## Equipo de trabajo From b97f7ef201488023c4064b2b33e9d3e84dadc374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Thu, 8 Feb 2024 11:25:43 -0300 Subject: [PATCH 021/244] =?UTF-8?q?Primera=20configuraci=C3=B3n=20de=20Cel?= =?UTF-8?q?ery=20con=20Django?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + gtfs2screens/__init__.py | 5 +++++ gtfs2screens/celery.py | 22 ++++++++++++++++++++++ gtfs2screens/settings.py | 16 ++++++++++++---- realtime/tasks.py | 27 +++++++++++++++++++++++++++ requirements.txt | 2 ++ website/admin.py | 3 +++ website/models.py | 12 ++++++++++++ 8 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 gtfs2screens/celery.py create mode 100644 realtime/tasks.py diff --git a/.gitignore b/.gitignore index d2fd289..b63a612 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,7 @@ cover/ local_settings.py db.sqlite3 db.sqlite3-journal +migrations/ # macOS stuff: .DS_Store diff --git a/gtfs2screens/__init__.py b/gtfs2screens/__init__.py index e69de29..5568b6d 100644 --- a/gtfs2screens/__init__.py +++ b/gtfs2screens/__init__.py @@ -0,0 +1,5 @@ +# This will make sure the app is always imported when +# Django starts so that shared_task will use this app. +from .celery import app as celery_app + +__all__ = ("celery_app",) diff --git a/gtfs2screens/celery.py b/gtfs2screens/celery.py new file mode 100644 index 0000000..bbb50c6 --- /dev/null +++ b/gtfs2screens/celery.py @@ -0,0 +1,22 @@ +import os + +from celery import Celery + +# Set the default Django settings module for the 'celery' program. +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gtfs2screens.settings") + +app = Celery("gtfs2screens") + +# Using a string here means the worker doesn't have to serialize +# the configuration object to child processes. +# - namespace='CELERY' means all celery-related configuration keys +# should have a `CELERY_` prefix. +app.config_from_object("django.conf:settings", namespace="CELERY") + +# Load task modules from all registered Django apps. +app.autodiscover_tasks() + + +@app.task(bind=True, ignore_result=True) +def debug_task(self): + print(f"Celery request: {self.request!r}") diff --git a/gtfs2screens/settings.py b/gtfs2screens/settings.py index b37d052..b9589b8 100644 --- a/gtfs2screens/settings.py +++ b/gtfs2screens/settings.py @@ -37,6 +37,8 @@ "gtfs.apps.GtfsConfig", "realtime.apps.RealtimeConfig", "screens.apps.ScreensConfig", + "django_celery_results", + "django_celery_beat", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", @@ -82,14 +84,14 @@ DATABASES = { "default": { "ENGINE": "django.contrib.gis.db.backends.postgis", - "NAME": config('DB_NAME'), - "USER": config('DB_USER'), + "NAME": config("DB_NAME"), + "USER": config("DB_USER"), }, } if not (platform.platform() == "Linux" or platform.machine() == "x86_64"): - GDAL_LIBRARY_PATH = config('GDAL_LIBRARY_PATH') - GEOS_LIBRARY_PATH = config('GEOS_LIBRARY_PATH') + GDAL_LIBRARY_PATH = config("GDAL_LIBRARY_PATH") + GEOS_LIBRARY_PATH = config("GEOS_LIBRARY_PATH") # Password validation @@ -110,6 +112,12 @@ }, ] +# Celery settings + +CELERY_BROKER_URL = "redis://localhost:6379/0" +CELERY_RESULT_BACKEND = "django-db" +CELERY_CACHE_BACKEND = "django-cache" + # Internationalization # https://docs.djangoproject.com/en/5.0/topics/i18n/ diff --git a/realtime/tasks.py b/realtime/tasks.py new file mode 100644 index 0000000..5705e09 --- /dev/null +++ b/realtime/tasks.py @@ -0,0 +1,27 @@ +# Create your tasks here + +from website.models import User + +from celery import shared_task + + +@shared_task +def add(x, y): + return x + y + + +@shared_task +def mul(x, y): + return x * y + + +@shared_task +def count_users(): + return User.objects.count() + + +@shared_task +def rename_user(user_id, name): + w = User.objects.get(user_id=user_id) + w.name = name + w.save() diff --git a/requirements.txt b/requirements.txt index 723e6a2..1a11bce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,5 @@ configparser schedule pandas gtfs-realtime-bindings +django-celery-results +django-celery-beat diff --git a/website/admin.py b/website/admin.py index 8c38f3f..df77129 100644 --- a/website/admin.py +++ b/website/admin.py @@ -1,3 +1,6 @@ from django.contrib import admin +from .models import User # Register your models here. + +admin.site.register(User) diff --git a/website/models.py b/website/models.py index 71a8362..5b20f23 100644 --- a/website/models.py +++ b/website/models.py @@ -1,3 +1,15 @@ from django.db import models # Create your models here. + + +class User(models.Model): + user_id = models.AutoField(primary_key=True) + name = models.CharField(max_length=100) + email = models.EmailField() + password = models.CharField(max_length=100) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.name From 4bcfd29a18b4257f6e402d8f08a57ee3dce72e23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Fri, 9 Feb 2024 08:37:51 -0300 Subject: [PATCH 022/244] Incluir archivos *.rdb de Redis para excluir del repositorio --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b63a612..3d701e7 100644 --- a/.gitignore +++ b/.gitignore @@ -62,6 +62,9 @@ db.sqlite3 db.sqlite3-journal migrations/ +# Redis stuff +*.rdb + # macOS stuff: .DS_Store From af316ed29ae4b76577f6c74fbc486216ec10cac9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Fri, 9 Feb 2024 11:00:03 -0300 Subject: [PATCH 023/244] Crear pruebas de Celery y Celery Beat con API y acceso a base de datos --- HOWTO.md | 9 +++++++++ gtfs2screens/settings.py | 1 + gtfs2screens/urls.py | 1 + realtime/admin.py | 3 +++ realtime/models.py | 8 ++++++++ realtime/tasks.py | 29 +++++++++++++---------------- realtime/urls.py | 8 ++++++++ realtime/views.py | 14 +++++++++++++- 8 files changed, 56 insertions(+), 17 deletions(-) create mode 100644 HOWTO.md create mode 100644 realtime/urls.py diff --git a/HOWTO.md b/HOWTO.md new file mode 100644 index 0000000..eedfacd --- /dev/null +++ b/HOWTO.md @@ -0,0 +1,9 @@ +# Instrucciones de ejecución de la plataforma + +## Celery + +Celery utiliza los paquetes de integración con Django `django-celery-results` y `django-celery-beat`, y el intermediador de mensajes Redis. + +```bash +celery -A gtfs2screens beat -l INFO --scheduler django_celery_beat.schedulers:DatabaseScheduler +``` \ No newline at end of file diff --git a/gtfs2screens/settings.py b/gtfs2screens/settings.py index b9589b8..4d4d2f4 100644 --- a/gtfs2screens/settings.py +++ b/gtfs2screens/settings.py @@ -117,6 +117,7 @@ CELERY_BROKER_URL = "redis://localhost:6379/0" CELERY_RESULT_BACKEND = "django-db" CELERY_CACHE_BACKEND = "django-cache" +CELERY_RESULTS_EXTENDED = True # Internationalization diff --git a/gtfs2screens/urls.py b/gtfs2screens/urls.py index f7dbe5c..c7f5f89 100644 --- a/gtfs2screens/urls.py +++ b/gtfs2screens/urls.py @@ -21,5 +21,6 @@ path("admin/", admin.site.urls), path("", include("website.urls"), name="index"), path("gtfs/", include("gtfs.urls"), name="gtfs_page"), + path("realtime/", include("realtime.urls"), name="realtime_page"), path("pantallas/", include("screens.urls"), name="screens_page"), ] diff --git a/realtime/admin.py b/realtime/admin.py index 8c38f3f..23fefe3 100644 --- a/realtime/admin.py +++ b/realtime/admin.py @@ -1,3 +1,6 @@ from django.contrib import admin +from .models import Test # Register your models here. + +admin.site.register(Test) \ No newline at end of file diff --git a/realtime/models.py b/realtime/models.py index 71a8362..ae86590 100644 --- a/realtime/models.py +++ b/realtime/models.py @@ -1,3 +1,11 @@ from django.db import models # Create your models here. + + +class Test(models.Model): + joke = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.created_at}: {self.joke}" diff --git a/realtime/tasks.py b/realtime/tasks.py index 5705e09..2e1d672 100644 --- a/realtime/tasks.py +++ b/realtime/tasks.py @@ -1,27 +1,24 @@ # Create your tasks here -from website.models import User +from .models import Test from celery import shared_task - -@shared_task -def add(x, y): - return x + y - - -@shared_task -def mul(x, y): - return x * y +import requests +from time import sleep @shared_task -def count_users(): - return User.objects.count() +def test_celery(): + response = requests.get("https://api.chucknorris.io/jokes/random") + joke = response.json()["value"] + Test.objects.create(joke=joke) + return joke @shared_task -def rename_user(user_id, name): - w = User.objects.get(user_id=user_id) - w.name = name - w.save() +def hello_celery(x, y): + for i in range(6): + print(i) + sleep(1) + return f"Done! {x} + {y} = {x + y}" diff --git a/realtime/urls.py b/realtime/urls.py new file mode 100644 index 0000000..2d85b5d --- /dev/null +++ b/realtime/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + path("test/", views.test, name="test"), + path("hello/", views.hello, name="hello"), +] \ No newline at end of file diff --git a/realtime/views.py b/realtime/views.py index 91ea44a..17f12d8 100644 --- a/realtime/views.py +++ b/realtime/views.py @@ -1,3 +1,15 @@ -from django.shortcuts import render +from django.shortcuts import render, HttpResponse + +from .tasks import test_celery, hello_celery # Create your views here. + + +def test(request): + joke = test_celery.delay() + return HttpResponse(joke.get()) + + +def hello(request): + hello_celery.delay(2, 3) + return HttpResponse("Hello, world!") From 998aa147ac326db19565963a1d0a575e86a52103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Fri, 9 Feb 2024 11:51:00 -0300 Subject: [PATCH 024/244] Ponerle nombre a las tareas que tenemos que configurar --- realtime/tasks.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/realtime/tasks.py b/realtime/tasks.py index 2e1d672..6a1861b 100644 --- a/realtime/tasks.py +++ b/realtime/tasks.py @@ -22,3 +22,18 @@ def hello_celery(x, y): print(i) sleep(1) return f"Done! {x} + {y} = {x + y}" + + +@shared_task +def get_vehiclepositions(): + return "VehiclePositions" + + +@shared_task +def get_tripupdates(): + return "TripUpdates" + + +@shared_task +def get_gtfs(): + return "GTFS" \ No newline at end of file From 61086db06d081e799fcb5a84ebcbdc58420419a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Fri, 9 Feb 2024 11:59:13 -0300 Subject: [PATCH 025/244] =?UTF-8?q?Mejorar=20la=20estructura=20del=20tutor?= =?UTF-8?q?ial=20de=20instalaci=C3=B3n=20del=20proyecto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HOWTO.md | 47 +++++++++++++++++++++++++++++++++++++++++++++-- requirements.txt | 1 + 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/HOWTO.md b/HOWTO.md index eedfacd..6b179d8 100644 --- a/HOWTO.md +++ b/HOWTO.md @@ -1,9 +1,52 @@ # Instrucciones de ejecución de la plataforma +El sistema requiere de: + +- Django / Python +- PostgreSQL / PostGIS +- Celery / Celery Beat +- Redis + +## Django + +La plataforma de Django será útil para: + +- Crear el sitio web con el panel de administración +- Administrar las tareas periódicas con su integración con Celery y Celery Beat +- Actualizar la informació en tiempo real con las pantallas con WebSockets + +Es necesario instalar Django 5.0 y la extensión Django Channels: + +```bash +pip install django +pip install channels +``` + +## PostgreSQL / PostGIS + +El proyecto requiere de una base de datos con una extensión para datos geoespaciales... + ## Celery +Celery es un administrador de tareas (_task manager_)... + +### Celery + +Instalar Celery... + +Ejecutar Celery con: + +```bash +celery -A gtfs2screens worker +``` + +### Celery Beat + Celery utiliza los paquetes de integración con Django `django-celery-results` y `django-celery-beat`, y el intermediador de mensajes Redis. ```bash -celery -A gtfs2screens beat -l INFO --scheduler django_celery_beat.schedulers:DatabaseScheduler -``` \ No newline at end of file +celery -A gtfs2screens beat --scheduler django_celery_beat.schedulers:DatabaseScheduler --loglevel=info +``` + +## Redis + diff --git a/requirements.txt b/requirements.txt index 1a11bce..54b581c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ django +channels python-decouple configparser schedule From 3ad02e028d6d3cd637e38f662d346bf62645bb6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Fri, 9 Feb 2024 13:29:17 -0300 Subject: [PATCH 026/244] =?UTF-8?q?Actualizaci=C3=B3n=20de=20requisitos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 54b581c..a9e7bfa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,8 +2,10 @@ django channels python-decouple configparser +requests schedule pandas gtfs-realtime-bindings +celery[redis] django-celery-results django-celery-beat From 5b61372fac653166f0bd92323972e091fe8b53f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Fri, 9 Feb 2024 14:25:52 -0300 Subject: [PATCH 027/244] =?UTF-8?q?Un=20poco=20m=C3=A1s=20de=20progreso=20?= =?UTF-8?q?en=20la=20explicaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HOWTO.md | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/HOWTO.md b/HOWTO.md index 6b179d8..90cb145 100644 --- a/HOWTO.md +++ b/HOWTO.md @@ -26,6 +26,52 @@ pip install channels El proyecto requiere de una base de datos con una extensión para datos geoespaciales... +```bash +sudo apt install postgresql +(...) +sudo apt install postgis +``` + +Modificar `pg_hba.conf` de forma que sea: + +```text +local all postgres trust + +local all all trust +``` +y así no tendrá contraseñas. Luego en la terminal: + +```bash +sudo -u postgres psql +``` + +que nos lleva a la interfaz de `psql` para configurar un nuevo usuario: + +```bash +postgres=# CREATE ROLE user_name SUPERUSER; +postgres=# ALTER ROLE user_name LOGIN; +``` + +Ahora podemos crear una base de datos, para este proyecto: + +```bash +createdb gtfs2screens +``` + +ahora hay que ingresar a esa base de datos: + +```bash +psql gtfs2screens +``` + +y ahí crear la extensión de PostGIS con: + +```bash +gtfs2screens=# CREATE EXTENSION postgis; +``` + +Con esto quedaría lista la base de datos para conectarnos desde Django. + ## Celery Celery es un administrador de tareas (_task manager_)... @@ -34,10 +80,16 @@ Celery es un administrador de tareas (_task manager_)... Instalar Celery... +```bash +pip install version +``` + +y probar con `celery --version`. + Ejecutar Celery con: ```bash -celery -A gtfs2screens worker +celery -A gtfs2screens worker --loglevel=info ``` ### Celery Beat @@ -50,3 +102,25 @@ celery -A gtfs2screens beat --scheduler django_celery_beat.schedulers:DatabaseSc ## Redis +```bash +sudo apt install redis-server +``` + +Nota: en macOS: + +```bash +brew install redis +``` + +Probar su estado como proceso del sistema: + +```bash +sudo systemctl status redis-server +``` + +Probar la conexión: + +```bash +>>> redis-cli ping +PONG +``` \ No newline at end of file From 5f2b350dc5ecbca811446f174c5005c2985c1627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Mon, 12 Feb 2024 10:07:01 -0300 Subject: [PATCH 028/244] Cambiar text de prueba para 'random facts' (los de Chuck Norris estaban un poco pasados) --- realtime/models.py | 4 ++-- realtime/tasks.py | 8 ++++---- realtime/views.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/realtime/models.py b/realtime/models.py index ae86590..496ad36 100644 --- a/realtime/models.py +++ b/realtime/models.py @@ -4,8 +4,8 @@ class Test(models.Model): - joke = models.TextField() + text = models.TextField() created_at = models.DateTimeField(auto_now_add=True) def __str__(self): - return f"{self.created_at}: {self.joke}" + return f"{self.created_at}: {self.text}" diff --git a/realtime/tasks.py b/realtime/tasks.py index 6a1861b..6ffae21 100644 --- a/realtime/tasks.py +++ b/realtime/tasks.py @@ -10,10 +10,10 @@ @shared_task def test_celery(): - response = requests.get("https://api.chucknorris.io/jokes/random") - joke = response.json()["value"] - Test.objects.create(joke=joke) - return joke + response = requests.get("https://uselessfacts.jsph.pl/api/v2/facts/random") + text = response.json()["text"] + Test.objects.create(text=text) + return text @shared_task diff --git a/realtime/views.py b/realtime/views.py index 17f12d8..8c96696 100644 --- a/realtime/views.py +++ b/realtime/views.py @@ -6,8 +6,8 @@ def test(request): - joke = test_celery.delay() - return HttpResponse(joke.get()) + text = test_celery.delay() + return HttpResponse(text.get()) def hello(request): From 6d0917ccbad5e947998fc691c45975ed2d7d004a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Mon, 12 Feb 2024 14:12:01 -0300 Subject: [PATCH 029/244] =?UTF-8?q?Primera=20implementaci=C3=B3n=20de=20pr?= =?UTF-8?q?ueba=20de=20WebSockets=20con=20un=20chat=20de=20ejemplo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HOWTO.md | 26 ++++++++++++++++++- gtfs2screens/asgi.py | 10 ++++++- gtfs2screens/settings.py | 20 +++++++++++++- requirements.txt | 13 ++++++---- screens/consumers.py | 35 +++++++++++++++++++++++++ screens/routing.py | 7 +++++ screens/templates/chat.html | 49 +++++++++++++++++++++++++++++++++++ screens/templates/screen.html | 19 ++++++++++++++ screens/urls.py | 1 + screens/views.py | 5 ++++ 10 files changed, 177 insertions(+), 8 deletions(-) create mode 100644 screens/consumers.py create mode 100644 screens/routing.py create mode 100644 screens/templates/chat.html diff --git a/HOWTO.md b/HOWTO.md index 90cb145..4227d23 100644 --- a/HOWTO.md +++ b/HOWTO.md @@ -123,4 +123,28 @@ Probar la conexión: ```bash >>> redis-cli ping PONG -``` \ No newline at end of file +``` + +## Django Channels + +Para habilitar la conexión permanente y bidireccional entre cliente y servidor con WebSockets, es necesario utilizar la extensión Django [Channels](https://channels.readthedocs.io/en/latest/), con [Daphne](https://github.com/django/daphne) como servidor HTTP/WebSocket (`http://`/`ws://`) y con [Redis](https://github.com/django/channels_redis) como intermediador de mensajes nuevamente. Para esto son necesarios los paquetes: + +- `channels` +- `daphne` +- `redis` +- `channel-redis` + +Este es un modo de conexión asíncrono, y por tanto requiere de la configuración ASGI (*Asynchronous Server Gateway Interface*). Esto se hace en el archivo `asgi.py`. + +Similar a `urls.py`, Channels requiere un archivo `routing.py` donde establece los `websocket_urlpatterns`, es decir, las rutas o URLs donde se establece la conexión del WebSocket `ws://`. + +También, similar a `views.py`, Channels define un archivo `consumers.py` donde define la lógica a realizar durante la conexión. + +A diferencia de + +Al configurar `settings.py` con Daphne, el comando `python manage.py runserver` ahora ejecuta también ASGI. De hecho, ahora en la terminal se muestra: + +```bash +Starting ASGI/Daphne version 4.1.0 development server at http://127.0.0.1:8000/ +``` +y toda la funcionalidad "regular" (WSGI) continúa operando. \ No newline at end of file diff --git a/gtfs2screens/asgi.py b/gtfs2screens/asgi.py index 697340a..115d4a1 100644 --- a/gtfs2screens/asgi.py +++ b/gtfs2screens/asgi.py @@ -9,8 +9,16 @@ import os +from channels.routing import ProtocolTypeRouter, URLRouter from django.core.asgi import get_asgi_application +from screens.routing import websocket_urlpatterns + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gtfs2screens.settings") -application = get_asgi_application() +application = ProtocolTypeRouter( + { + "http": get_asgi_application(), + "websocket": URLRouter(websocket_urlpatterns), + } +) diff --git a/gtfs2screens/settings.py b/gtfs2screens/settings.py index 4d4d2f4..d34288b 100644 --- a/gtfs2screens/settings.py +++ b/gtfs2screens/settings.py @@ -33,6 +33,8 @@ # Application definition INSTALLED_APPS = [ + "daphne", + "channels", "website.apps.WebsiteConfig", "gtfs.apps.GtfsConfig", "realtime.apps.RealtimeConfig", @@ -76,6 +78,7 @@ ] WSGI_APPLICATION = "gtfs2screens.wsgi.application" +ASGI_APPLICATION = "gtfs2screens.asgi.application" # Database @@ -112,13 +115,28 @@ }, ] +# Redis settings + +REDIS_HOST = config("REDIS_HOST") +REDIS_PORT = config("REDIS_PORT") + # Celery settings -CELERY_BROKER_URL = "redis://localhost:6379/0" +CELERY_BROKER_URL = f"redis://{REDIS_HOST}:{REDIS_PORT}/0" CELERY_RESULT_BACKEND = "django-db" CELERY_CACHE_BACKEND = "django-cache" CELERY_RESULTS_EXTENDED = True +# Channels settings + +CHANNEL_LAYERS = { + "default": { + "BACKEND": "channels_redis.core.RedisChannelLayer", + "CONFIG": { + "hosts": [(REDIS_HOST, REDIS_PORT)], + }, + }, +} # Internationalization # https://docs.djangoproject.com/en/5.0/topics/i18n/ diff --git a/requirements.txt b/requirements.txt index a9e7bfa..9bd3b53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,14 @@ django channels -python-decouple -configparser +daphne +redis +channels-redis +celery[redis] +django-celery-results +django-celery-beat requests +configparser schedule pandas gtfs-realtime-bindings -celery[redis] -django-celery-results -django-celery-beat +python-decouple diff --git a/screens/consumers.py b/screens/consumers.py new file mode 100644 index 0000000..721881c --- /dev/null +++ b/screens/consumers.py @@ -0,0 +1,35 @@ +import json + +from channels.generic.websocket import AsyncWebsocketConsumer + + +class ChatConsumer(AsyncWebsocketConsumer): + async def connect(self): + self.room_name = self.scope["url_route"]["kwargs"]["room_name"] + self.room_group_name = f"chat_{self.room_name}" + + # Join room group + await self.channel_layer.group_add(self.room_group_name, self.channel_name) + + await self.accept() + + async def disconnect(self, close_code): + # Leave room group + await self.channel_layer.group_discard(self.room_group_name, self.channel_name) + + # Receive message from WebSocket + async def receive(self, text_data): + text_data_json = json.loads(text_data) + message = text_data_json["message"] + + # Send message to room group + await self.channel_layer.group_send( + self.room_group_name, {"type": "chat.message", "message": message} + ) + + # Receive message from room group + async def chat_message(self, event): + message = event["message"] + + # Send message to WebSocket + await self.send(text_data=json.dumps({"message": message})) diff --git a/screens/routing.py b/screens/routing.py new file mode 100644 index 0000000..f57325e --- /dev/null +++ b/screens/routing.py @@ -0,0 +1,7 @@ +from django.urls import re_path + +from .consumers import ChatConsumer + +websocket_urlpatterns = [ + re_path(r"ws/screen/(?P\w+)/$", ChatConsumer.as_asgi()), +] \ No newline at end of file diff --git a/screens/templates/chat.html b/screens/templates/chat.html new file mode 100644 index 0000000..0999683 --- /dev/null +++ b/screens/templates/chat.html @@ -0,0 +1,49 @@ + + + + + Chat Room + + +
+
+ + {{ room_name|json_script:"room-name" }} + + + \ No newline at end of file diff --git a/screens/templates/screen.html b/screens/templates/screen.html index 3849282..8897a05 100644 --- a/screens/templates/screen.html +++ b/screens/templates/screen.html @@ -6,7 +6,26 @@

Ruta UCR

Pantalla: {{ screen_id }}

+

+ \ No newline at end of file diff --git a/screens/urls.py b/screens/urls.py index d54e82d..10f731d 100644 --- a/screens/urls.py +++ b/screens/urls.py @@ -5,6 +5,7 @@ urlpatterns = [ path("", views.screens, name="screens"), path("crear/", views.create_screen, name="create_screen"), + path("chat//", views.chat, name="chat"), path("/", views.screen, name="screen"), path("/editar/", views.edit_screen, name="edit_screen"), ] \ No newline at end of file diff --git a/screens/views.py b/screens/views.py index 0cf627a..81de0dd 100644 --- a/screens/views.py +++ b/screens/views.py @@ -23,3 +23,8 @@ def screen(request, screen_id): def edit_screen(request, screen_id): context = {"screen_id": screen_id} return render(request, "edit_screen.html", context) + + +# Testing the websocket +def chat(request, room_name): + return render(request, "chat.html", {"room_name": room_name}) \ No newline at end of file From ce415e8a744028a44dc10f80eb6154393c6aae4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Mon, 12 Feb 2024 14:44:55 -0300 Subject: [PATCH 030/244] =?UTF-8?q?Esbozo=20(no=20funcional)=20de=20config?= =?UTF-8?q?uraci=C3=B3n=20para=20conectar=20Celery=20con=20WebSockets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- realtime/tasks.py | 11 +++++++++++ screens/consumers.py | 32 ++++++++++++++++++++++++++++++++ screens/routing.py | 5 +++-- screens/templates/screen.html | 4 ++-- 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/realtime/tasks.py b/realtime/tasks.py index 6ffae21..6bb0654 100644 --- a/realtime/tasks.py +++ b/realtime/tasks.py @@ -4,6 +4,9 @@ from celery import shared_task +from channels.layers import get_channel_layer +from asgiref.sync import async_to_sync + import requests from time import sleep @@ -13,6 +16,14 @@ def test_celery(): response = requests.get("https://uselessfacts.jsph.pl/api/v2/facts/random") text = response.json()["text"] Test.objects.create(text=text) + channel_layer = get_channel_layer() + async_to_sync(channel_layer.group_send)( + "screen", + { + "type": "screen_message", + "message": text, + }, + ) return text diff --git a/screens/consumers.py b/screens/consumers.py index 721881c..8ce6466 100644 --- a/screens/consumers.py +++ b/screens/consumers.py @@ -33,3 +33,35 @@ async def chat_message(self, event): # Send message to WebSocket await self.send(text_data=json.dumps({"message": message})) + + +class ScreenConsumer(AsyncWebsocketConsumer): + async def connect(self): + self.screen_id = self.scope["url_route"]["kwargs"]["screen_id"] + self.screen_group_name = "screen" # f"screen_{self.screen_id}" + + # Join room group + await self.channel_layer.group_add(self.screen_group_name, self.channel_name) + + await self.accept() + + async def disconnect(self, close_code): + # Leave room group + await self.channel_layer.group_discard(self.screen_group_name, self.channel_name) + + # Receive message from WebSocket + async def receive(self, text_data): + text_data_json = json.loads(text_data) + message = text_data_json["message"] + + # Send message to room group + await self.channel_layer.group_send( + self.screen_group_name, {"type": "screen.message", "message": message} + ) + + # Receive message from room group + async def screen_message(self, event): + message = event["message"] + + # Send message to WebSocket + await self.send(text_data=json.dumps({"message": message})) \ No newline at end of file diff --git a/screens/routing.py b/screens/routing.py index f57325e..15c1ac1 100644 --- a/screens/routing.py +++ b/screens/routing.py @@ -1,7 +1,8 @@ from django.urls import re_path -from .consumers import ChatConsumer +from .consumers import ChatConsumer, ScreenConsumer websocket_urlpatterns = [ - re_path(r"ws/screen/(?P\w+)/$", ChatConsumer.as_asgi()), + re_path(r"ws/chat/(?P\w+)/$", ChatConsumer.as_asgi()), + re_path(r"ws/screen/(?P\w+)/$", ScreenConsumer.as_asgi()), ] \ No newline at end of file diff --git a/screens/templates/screen.html b/screens/templates/screen.html index 8897a05..3e5f814 100644 --- a/screens/templates/screen.html +++ b/screens/templates/screen.html @@ -12,11 +12,11 @@

Ruta UCR

// Aquí un archivo para manejar las actualizaciones de la pantalla con WebSockets // https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications // Crear un WebSocket - const screenSocket = new WebSocket('ws://127.0.0.1:8000/ws/screen/'); + const screenSocket = new WebSocket('ws://127.0.0.1:8000/ws/screen/{{ screen_id }}/'); // Registrar la conexión abierta screenSocket.onopen = function (event) { - console.log('Conexión abierta'); + console.log('gtfs2screens: Conexión abierta exitosamente.'); }; // Manejo de eventos de mensajes From d03fb478a6df89fa2d088202214e10ce12610ef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Mon, 12 Feb 2024 17:51:34 -0300 Subject: [PATCH 031/244] =?UTF-8?q?Revisi=C3=B3n=20del=20WebSocket=20funci?= =?UTF-8?q?onando=20con=20ejemplo=20de=20prueba=20incluyendo=20Celery=20Be?= =?UTF-8?q?at?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- screens/consumers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/screens/consumers.py b/screens/consumers.py index 8ce6466..8e89292 100644 --- a/screens/consumers.py +++ b/screens/consumers.py @@ -59,9 +59,9 @@ async def receive(self, text_data): self.screen_group_name, {"type": "screen.message", "message": message} ) - # Receive message from room group + # Send message to WebSocket async def screen_message(self, event): message = event["message"] - # Send message to WebSocket - await self.send(text_data=json.dumps({"message": message})) \ No newline at end of file + await self.send(text_data=json.dumps({"message": message})) + \ No newline at end of file From 15f929c92e50cc585f3a05e599ae3fed7fac4ef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Tue, 13 Feb 2024 12:44:11 -0300 Subject: [PATCH 032/244] Esbozo de la estructura de las pantallas y los modelos --- screens/models.py | 40 +++++++++++++++++++++++++++++++++++++++- screens/views.py | 17 ++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/screens/models.py b/screens/models.py index 71a8362..843748d 100644 --- a/screens/models.py +++ b/screens/models.py @@ -1,3 +1,41 @@ -from django.db import models +from django.contrib.gis.db import models +from gtfs.models import Stop # Create your models here. + + +class Screen(models.Model): + ORIENTATION_CHOICES = [ + ("landscape", "Landscape"), + ("portrait", "Portrait"), + ] + RATIO_CHOICES = [ + ("4:3", "4:3"), + ("16:9", "16:9"), + ("16:10", "16:10"), + ] + + screen_id = models.CharField(max_length=100) + name = models.CharField(max_length=100) + description = models.TextField() + location = models.PointField() + address = models.TextField() + orientation = models.CharField( + max_length=10, choices=ORIENTATION_CHOICES, default="landscape" + ) + ratio = models.CharField(max_length=10, choices=RATIO_CHOICES, default="16:9") + size = models.PositiveIntegerField(help_text="in inches") + has_audio = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.name + + +class ScreenStops(models.Model): + screen = models.ForeignKey(Screen, on_delete=models.CASCADE) + stop = models.ForeignKey(Stop, on_delete=models.CASCADE) + + def __str__(self): + return f"{self.screen} - {self.stop}" diff --git a/screens/views.py b/screens/views.py index 81de0dd..82b4f50 100644 --- a/screens/views.py +++ b/screens/views.py @@ -5,14 +5,20 @@ def screens(request): + """Render a list of screens. + """ return render(request, "screens.html") def create_screen(request): + """Create and configure a new screen. + """ return render(request, "create_screen.html") def screen(request, screen_id): + """Render a screen. + """ seed = screen_id random.seed(seed) minutes = random.randint(0, 30) @@ -21,10 +27,19 @@ def screen(request, screen_id): def edit_screen(request, screen_id): + """Edit a screen. + """ context = {"screen_id": screen_id} return render(request, "edit_screen.html", context) +def update_screen(request, screen_id): + """Update a screen. + """ + # For each screen, collect all data linked to it and send it, with a given format, to the screen via websocket. + return 0 + + # Testing the websocket def chat(request, room_name): - return render(request, "chat.html", {"room_name": room_name}) \ No newline at end of file + return render(request, "chat.html", {"room_name": room_name}) From a448de37dc075bfe4dc44b45970ee75c04f12c35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Tue, 13 Feb 2024 13:00:41 -0300 Subject: [PATCH 033/244] Cambio menor --- screens/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/screens/views.py b/screens/views.py index 82b4f50..02cd094 100644 --- a/screens/views.py +++ b/screens/views.py @@ -36,6 +36,7 @@ def edit_screen(request, screen_id): def update_screen(request, screen_id): """Update a screen. """ + # Get a Django Signal signaling that the FeedMessage has been processed and there are updates for each stop. # For each screen, collect all data linked to it and send it, with a given format, to the screen via websocket. return 0 From e4f87f1005fac9289ba4f3a197d89d8bff167dde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Tue, 13 Feb 2024 14:07:21 -0300 Subject: [PATCH 034/244] =?UTF-8?q?Crear=20modelo=20Stop=20auxiliar=20sola?= =?UTF-8?q?mente=20para=20que=20funcione=20el=20c=C3=B3digo=20de=20prueba?= =?UTF-8?q?=20en=20views.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gtfs/models.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/gtfs/models.py b/gtfs/models.py index 71a8362..3744986 100644 --- a/gtfs/models.py +++ b/gtfs/models.py @@ -1,3 +1,11 @@ from django.db import models # Create your models here. + + +class Stop(models.Model): + stop_id = models.CharField(max_length=100) + name = models.CharField(max_length=100) + + def __str__(self): + return self.name \ No newline at end of file From c5e4713d87a962f997902d5b763fd8daafb25b56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Mon, 26 Feb 2024 20:16:28 -0300 Subject: [PATCH 035/244] =?UTF-8?q?Comenzar=20con=20la=20documentaci=C3=B3?= =?UTF-8?q?n=20en=20Material=20for=20MkDocs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/about.md | 10 ++++++++++ docs/architecture.md | 1 + docs/index.md | 17 +++++++++++++++++ mkdocs.yml | 13 +++++++++++++ 4 files changed, 41 insertions(+) create mode 100644 docs/about.md create mode 100644 docs/architecture.md create mode 100644 docs/index.md create mode 100644 mkdocs.yml diff --git a/docs/about.md b/docs/about.md new file mode 100644 index 0000000..de550d5 --- /dev/null +++ b/docs/about.md @@ -0,0 +1,10 @@ +# Sobre el proyecto + +Este es un proyecto realizado en conjunto.... + + + +Mateo + + +Murillo \ No newline at end of file diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..52ab7fd --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1 @@ +# Arquitectura del sistema \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..3b1e0d5 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,17 @@ +# Sistema de pantallas en tiempo real del bus interno de la UCR + +For full documentation visit [mkdocs.org](https://www.mkdocs.org). + +## Commands + +* `mkdocs new [dir-name]` - Create a new project. +* `mkdocs serve` - Start the live-reloading docs server. +* `mkdocs build` - Build the documentation site. +* `mkdocs -h` - Print help message and exit. + +## Project layout + + mkdocs.yml # The configuration file. + docs/ + index.md # The documentation homepage. + ... # Other markdown pages, images and other files. diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..cc28f44 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,13 @@ +site_name: Sistema de pantallas en tiempo real + +nav: + - Inicio: index.md + - Arquitectura: architecture.md + - Sobre el proyecto: about.md + +theme: + name: material + features: + - navigation.expand + - navigation.tabs + - toc.integrate From 932d6d9b190d03f5ba0503eb35be0e6afe93d601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Mon, 26 Feb 2024 21:33:47 -0300 Subject: [PATCH 036/244] Modificar y migrar modelo Screen, incorporarlo al admin y habilitar el mapa con GIS --- gtfs2screens/settings.py | 1 + screens/admin.py | 6 +++++- screens/models.py | 21 +++++++++++++++------ 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/gtfs2screens/settings.py b/gtfs2screens/settings.py index d34288b..2198f6e 100644 --- a/gtfs2screens/settings.py +++ b/gtfs2screens/settings.py @@ -47,6 +47,7 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "django.contrib.gis", ] MIDDLEWARE = [ diff --git a/screens/admin.py b/screens/admin.py index 8c38f3f..d73f1ff 100644 --- a/screens/admin.py +++ b/screens/admin.py @@ -1,3 +1,7 @@ -from django.contrib import admin +from django.contrib.gis import admin +from .models import Screen, ScreenStops # Register your models here. + +admin.site.register(Screen, admin.GISModelAdmin) +admin.site.register(ScreenStops) diff --git a/screens/models.py b/screens/models.py index 843748d..dae9877 100644 --- a/screens/models.py +++ b/screens/models.py @@ -17,17 +17,26 @@ class Screen(models.Model): screen_id = models.CharField(max_length=100) name = models.CharField(max_length=100) - description = models.TextField() - location = models.PointField() - address = models.TextField() + description = models.TextField(blank=True, null=True) + location = models.PointField(blank=True, null=True) + address = models.TextField(blank=True, null=True) orientation = models.CharField( - max_length=10, choices=ORIENTATION_CHOICES, default="landscape" + max_length=10, + choices=ORIENTATION_CHOICES, + default="landscape", + blank=True, null=True, ) - ratio = models.CharField(max_length=10, choices=RATIO_CHOICES, default="16:9") - size = models.PositiveIntegerField(help_text="in inches") + ratio = models.CharField( + max_length=10, + choices=RATIO_CHOICES, + default="16:9", + blank=True, null=True + ) + size = models.PositiveIntegerField(help_text="in inches", blank=True, null=True) has_audio = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + is_active = models.BooleanField(default=False) def __str__(self): return self.name From 4851fd98951f11bd3005cdf8cd1e515a6678e2f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Mon, 26 Feb 2024 21:35:42 -0300 Subject: [PATCH 037/244] =?UTF-8?q?Al=20conectar=20un=20WebSocket=20de=20u?= =?UTF-8?q?na=20pantalla,=20cambiar=20su=20estado=20en=20la=20base=20de=20?= =?UTF-8?q?datos=20a=20is=5Factive=3DTrue,=20primer=20intento=20de=20envia?= =?UTF-8?q?r=20mensajes=20a=20cada=20pantalla=20(no=20funcion=C3=B3=20en?= =?UTF-8?q?=20tasks.py)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- realtime/tasks.py | 20 ++++++++++++-------- screens/consumers.py | 30 +++++++++++++++++++++++------- screens/templates/screen.html | 9 +++++++-- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/realtime/tasks.py b/realtime/tasks.py index 6bb0654..19656bc 100644 --- a/realtime/tasks.py +++ b/realtime/tasks.py @@ -1,11 +1,12 @@ # Create your tasks here from .models import Test +from screens.models import Screen from celery import shared_task from channels.layers import get_channel_layer -from asgiref.sync import async_to_sync +from asgiref.sync import async_to_sync, sync_to_async import requests from time import sleep @@ -17,13 +18,16 @@ def test_celery(): text = response.json()["text"] Test.objects.create(text=text) channel_layer = get_channel_layer() - async_to_sync(channel_layer.group_send)( - "screen", - { - "type": "screen_message", - "message": text, - }, - ) + screens = Screen.objects.filter(is_active=True) + sync_to_async(print)(f"Task: {screen}") + for screen in screens: + async_to_sync(channel_layer.group_send)( + f"screen_{screen.screen_id}", + { + "type": "screen_message", + "message": f"{screen.screen_id}: {text}", + }, + ) return text diff --git a/screens/consumers.py b/screens/consumers.py index 8e89292..9ed5c3c 100644 --- a/screens/consumers.py +++ b/screens/consumers.py @@ -1,6 +1,7 @@ import json - from channels.generic.websocket import AsyncWebsocketConsumer +from .models import Screen +from asgiref.sync import sync_to_async class ChatConsumer(AsyncWebsocketConsumer): @@ -38,16 +39,32 @@ async def chat_message(self, event): class ScreenConsumer(AsyncWebsocketConsumer): async def connect(self): self.screen_id = self.scope["url_route"]["kwargs"]["screen_id"] - self.screen_group_name = "screen" # f"screen_{self.screen_id}" + self.screen_group_name = f"screen_{self.screen_id}" - # Join room group await self.channel_layer.group_add(self.screen_group_name, self.channel_name) - await self.accept() + await self.activate_screen(self.screen_id) + + @sync_to_async + def activate_screen(self, screen_id): + screen = Screen.objects.get(screen_id=screen_id) + screen.is_active = True + print(f"Screen {screen_id} is now active") + screen.save() async def disconnect(self, close_code): - # Leave room group - await self.channel_layer.group_discard(self.screen_group_name, self.channel_name) + + await self.channel_layer.group_discard( + self.screen_group_name, self.channel_name + ) + await self.deactivate_screen(self.screen_id) + + @sync_to_async + def deactivate_screen(self, screen_id): + screen = Screen.objects.get(screen_id=screen_id) + screen.is_active = False + print(f"Screen {screen_id} is now inactive") + screen.save() # Receive message from WebSocket async def receive(self, text_data): @@ -64,4 +81,3 @@ async def screen_message(self, event): message = event["message"] await self.send(text_data=json.dumps({"message": message})) - \ No newline at end of file diff --git a/screens/templates/screen.html b/screens/templates/screen.html index 3e5f814..e4973f8 100644 --- a/screens/templates/screen.html +++ b/screens/templates/screen.html @@ -12,11 +12,12 @@

Ruta UCR

// Aquí un archivo para manejar las actualizaciones de la pantalla con WebSockets // https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications // Crear un WebSocket - const screenSocket = new WebSocket('ws://127.0.0.1:8000/ws/screen/{{ screen_id }}/'); + const wsUrl = 'ws://' + window.location.host + '/ws/screen/{{ screen_id }}/'; + const screenSocket = new WebSocket(wsUrl); // Registrar la conexión abierta screenSocket.onopen = function (event) { - console.log('gtfs2screens: Conexión abierta exitosamente.'); + console.log('gtfs2screens: Conexión abierta exitosamente en ' + wsUrl); }; // Manejo de eventos de mensajes @@ -28,4 +29,8 @@

Ruta UCR

document.getElementById('message').innerHTML = message; } }; + + window.addEventListener('beforeunload', function (event) { + screenSocket.close(); + }); \ No newline at end of file From d1fb28d9d0f28e5f5b8bee749a65bd5227c71bf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Mon, 26 Feb 2024 22:08:52 -0300 Subject: [PATCH 038/244] =?UTF-8?q?Revisi=C3=B3n=20b=C3=A1sica=20de=20la?= =?UTF-8?q?=20documentaci=C3=B3n,=20logo=20b,=20otras=20configuraciones,?= =?UTF-8?q?=20algo=20de=20texto=20inicial?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/about.md | 11 +++++------ docs/assets/b.png | Bin 0 -> 266242 bytes docs/index.md | 16 +++++----------- docs/stylesheets/extra.css | 5 +++++ mkdocs.yml | 13 +++++++++++++ 5 files changed, 28 insertions(+), 17 deletions(-) create mode 100644 docs/assets/b.png create mode 100644 docs/stylesheets/extra.css diff --git a/docs/about.md b/docs/about.md index de550d5..5500509 100644 --- a/docs/about.md +++ b/docs/about.md @@ -1,10 +1,9 @@ # Sobre el proyecto -Este es un proyecto realizado en conjunto.... +Este es un proyecto realizado en conjunto por +- Trabajo final de graduación de licenciatura "Diseño de una arquitectura de referencia para un sistema inteligente de transporte público en Costa Rica" +- Proyecto de investigación **322-C3-184** "Análisis de datos abiertos de sistemas inteligentes de transporte público con herramientas de aprendizaje automático" +- Proyecto de transporte público dentro del trabajo comunal universitario **TC-691** "Tropicalización de la tecnología" - -Mateo - - -Murillo \ No newline at end of file +de la Escuela de Ingeniería Eléctrica de la Universidad de Costa Rica, con el auspicio la Oficina de Servicios Generales. \ No newline at end of file diff --git a/docs/assets/b.png b/docs/assets/b.png new file mode 100644 index 0000000000000000000000000000000000000000..4848d11497ceddba5e253f70627f00d1c9d80d68 GIT binary patch literal 266242 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ⅆ!xA)D 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

nbSo-x~*A}2;upx;Rwtu|6aL>drZr*?X8Tg*` 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 z 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 vBB@jDu}KSz4=)e6J&1Oy; literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md index 3b1e0d5..af5daac 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,17 +1,11 @@ # Sistema de pantallas en tiempo real del bus interno de la UCR -For full documentation visit [mkdocs.org](https://www.mkdocs.org). +La información en tiempo real sobre el transporte público es uno de los servicios mejor apreciados por las personas usuarias. -## Commands +## Descripción -* `mkdocs new [dir-name]` - Create a new project. -* `mkdocs serve` - Start the live-reloading docs server. -* `mkdocs build` - Build the documentation site. -* `mkdocs -h` - Print help message and exit. +Este es un proyecto realizado por la Escuela de Ingeniería Eléctrica de la Universidad de Costa Rica, en conjunto con la Oficina de Servicios Generales. -## Project layout +## Implementación - mkdocs.yml # The configuration file. - docs/ - index.md # The documentation homepage. - ... # Other markdown pages, images and other files. +El primer prototipo del sistema de pantallas en tiempo real será implementado en la segunda mitad de 2024. diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 0000000..37ec2b6 --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,5 @@ +[data-md-color-scheme="ucr"] { + --md-primary-fg-color: #005DA4; + --md-primary-fg-color--light: #00C0F3; + --md-primary-fg-color--dark: #008641; +} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index cc28f44..98242e2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,9 +5,22 @@ nav: - Arquitectura: architecture.md - Sobre el proyecto: about.md +extra_css: + - stylesheets/extra.css + theme: name: material + language: es + palette: + scheme: ucr + logo: assets/b.png + favicon: assets/b.png features: - navigation.expand - navigation.tabs - toc.integrate + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences \ No newline at end of file From 49605569cb946a32283a100b0983993a5783ae74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Thu, 29 Feb 2024 09:15:12 -0300 Subject: [PATCH 039/244] =?UTF-8?q?Mover=20los=20archivos=20viejos=20de=20?= =?UTF-8?q?documentaci=C3=B3n=20para=20la=20nueva=20carpeta=20de=20MkDocs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ARCHITECTURE.md | 79 -------------------------- docs/architecture.md | 80 ++++++++++++++++++++++++++- DEVELOPMENT.md => docs/development.md | 0 mkdocs.yml | 1 + 4 files changed, 80 insertions(+), 80 deletions(-) delete mode 100644 ARCHITECTURE.md rename DEVELOPMENT.md => docs/development.md (100%) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index 2575da2..0000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -1,79 +0,0 @@ -# Descripción del servidor de aplicaciones para pantallas con información GTFS Realtime - -## Tareas esenciales del servidor - -1. Periódicamente recopilar los `FeedMessage` de GTFS Realtime (este es el servidor "hermano" `gtfs-realtime`). -2. Organizar la información contenida para cada pantalla, según sea relevante (ejemplo: clasificar los viajes que van a pasar por esa parada (asumiendo que las pantallas están en o cerca de una parada), o anuncios de alertas que sean relevantes para el servicio en esa pantalla, etc.). -3. (Opcional) Cargar la plantilla HTML en el navegador de la pantalla cuando se carga el sitio por primera vez (ejemplo: al prender las pantallas). -4. Actualizar la información desplegada en las pantallas utilizando los WebSockets de Django. - -## Aplicaciones y sitios del proyecto de Django - -### Django app: `website` - -> Manejo de páginas misceláneas del sitio. - -- `/`: Página de bienvenida del sistema -- `/sobre/`: Información del proyecto -- `/perfil/`: Perfil de usuario registrado - -#### Modelos asociados - -- `class User`: Información de usuarios del sistema - - `type` - -### Django app: `screens` - -> Páginas de administración de las pantallas (HTML) y actualización de datos en tiempo real (WebSockets) - -- `/pantallas/`: Lista de pantallas del sistema -- `/pantallas/crear/`: Página de creación de nueva pantalla -- `/pantallas//`: Visualización de la pantalla (**contenido de la pantalla**) -- `/pantallas//configuracion/`: Sitio de configuración de la pantalla `screen_id` - - -Nota: las pantallas por ahora asumimos que son Raspberry Pi en [modo kiosko](https://www.raspberrypi.com/tutorials/how-to-use-a-raspberry-pi-in-kiosk-mode/) que utilizan [Chromium](https://www.chromium.org/chromium-projects/) para navegar el sitio. - -#### Modelos asociados - -- `class Screen`: Información de cada pantalla - - `screen_id` - - `name` - - `address` - - `location` (ejemplo: 9.93752787687643, -84.04463400265841 con PostGIS y GeoDjango) - - `size` (ejemplo: 32") - - `ratio` (ejemplo: 16:9) - - `orientation` (ejemplo: `VERTICAL`, `HORIZONTAL`) - - `has_sound` (ejemplo: `True`, `False`, booleano) - -### Django app: `gtfs` - -> Páginas de administación de información GTFS Schedule y GTFS Realtime. - -- `/gtfs/`: -- `/gtfs/schedule/`: Información y configuración del *feed* GTFS Schedule utilizado -- `/gtfs/realtime/`: Información y configuración del *feed* GTFS Realtime utilizado -- `/gtfs/company/`: Las pantallas pueden desplegar información de uno o más *feeds* provenientes de una o más compañías - - #### Modelos asociados - -- (Todos los modelos de GTFS Schedule, como `Agency`, `Route`, etc.) -- (Todos los modelos de GTFS Realtime, como `VehiclePosition`, etc.) -- `class Company` - - `company_id` - - `name` - - `address` - - `phone` - - `email` - - `website` - - `logo` -- `class Schedule` - - `company_id` (ejemplo: "MBTA") - - `schedule_url`: *feed* estático - - `last_updated` -- `class Realtime` - - `company_id` (ForeignKey) (ejemplo: "MBTA") - - `alerts_url`: *feed message* de alertas del servicio - - `trip_updates_url`: *feed message* de actualizaciones de los viajes - - `vehicle_positions_url`: *feed message* de posición de los vehículos - - `alerts_last_updated` diff --git a/docs/architecture.md b/docs/architecture.md index 52ab7fd..2575da2 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1 +1,79 @@ -# Arquitectura del sistema \ No newline at end of file +# Descripción del servidor de aplicaciones para pantallas con información GTFS Realtime + +## Tareas esenciales del servidor + +1. Periódicamente recopilar los `FeedMessage` de GTFS Realtime (este es el servidor "hermano" `gtfs-realtime`). +2. Organizar la información contenida para cada pantalla, según sea relevante (ejemplo: clasificar los viajes que van a pasar por esa parada (asumiendo que las pantallas están en o cerca de una parada), o anuncios de alertas que sean relevantes para el servicio en esa pantalla, etc.). +3. (Opcional) Cargar la plantilla HTML en el navegador de la pantalla cuando se carga el sitio por primera vez (ejemplo: al prender las pantallas). +4. Actualizar la información desplegada en las pantallas utilizando los WebSockets de Django. + +## Aplicaciones y sitios del proyecto de Django + +### Django app: `website` + +> Manejo de páginas misceláneas del sitio. + +- `/`: Página de bienvenida del sistema +- `/sobre/`: Información del proyecto +- `/perfil/`: Perfil de usuario registrado + +#### Modelos asociados + +- `class User`: Información de usuarios del sistema + - `type` + +### Django app: `screens` + +> Páginas de administración de las pantallas (HTML) y actualización de datos en tiempo real (WebSockets) + +- `/pantallas/`: Lista de pantallas del sistema +- `/pantallas/crear/`: Página de creación de nueva pantalla +- `/pantallas//`: Visualización de la pantalla (**contenido de la pantalla**) +- `/pantallas//configuracion/`: Sitio de configuración de la pantalla `screen_id` + + +Nota: las pantallas por ahora asumimos que son Raspberry Pi en [modo kiosko](https://www.raspberrypi.com/tutorials/how-to-use-a-raspberry-pi-in-kiosk-mode/) que utilizan [Chromium](https://www.chromium.org/chromium-projects/) para navegar el sitio. + +#### Modelos asociados + +- `class Screen`: Información de cada pantalla + - `screen_id` + - `name` + - `address` + - `location` (ejemplo: 9.93752787687643, -84.04463400265841 con PostGIS y GeoDjango) + - `size` (ejemplo: 32") + - `ratio` (ejemplo: 16:9) + - `orientation` (ejemplo: `VERTICAL`, `HORIZONTAL`) + - `has_sound` (ejemplo: `True`, `False`, booleano) + +### Django app: `gtfs` + +> Páginas de administación de información GTFS Schedule y GTFS Realtime. + +- `/gtfs/`: +- `/gtfs/schedule/`: Información y configuración del *feed* GTFS Schedule utilizado +- `/gtfs/realtime/`: Información y configuración del *feed* GTFS Realtime utilizado +- `/gtfs/company/`: Las pantallas pueden desplegar información de uno o más *feeds* provenientes de una o más compañías + + #### Modelos asociados + +- (Todos los modelos de GTFS Schedule, como `Agency`, `Route`, etc.) +- (Todos los modelos de GTFS Realtime, como `VehiclePosition`, etc.) +- `class Company` + - `company_id` + - `name` + - `address` + - `phone` + - `email` + - `website` + - `logo` +- `class Schedule` + - `company_id` (ejemplo: "MBTA") + - `schedule_url`: *feed* estático + - `last_updated` +- `class Realtime` + - `company_id` (ForeignKey) (ejemplo: "MBTA") + - `alerts_url`: *feed message* de alertas del servicio + - `trip_updates_url`: *feed message* de actualizaciones de los viajes + - `vehicle_positions_url`: *feed message* de posición de los vehículos + - `alerts_last_updated` diff --git a/DEVELOPMENT.md b/docs/development.md similarity index 100% rename from DEVELOPMENT.md rename to docs/development.md diff --git a/mkdocs.yml b/mkdocs.yml index 98242e2..30301a2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,6 +3,7 @@ site_name: Sistema de pantallas en tiempo real nav: - Inicio: index.md - Arquitectura: architecture.md + - Desarrollo: development.md - Sobre el proyecto: about.md extra_css: From 7f02a6e5e1bffe57da29524d29a34f070c28d29c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Thu, 29 Feb 2024 11:37:34 -0300 Subject: [PATCH 040/244] =?UTF-8?q?Agregar=20modelos=20de=20GTFS=20Schedul?= =?UTF-8?q?e=20(m=C3=ADnimos=20necesarios)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gtfs/models.py | 443 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 438 insertions(+), 5 deletions(-) diff --git a/gtfs/models.py b/gtfs/models.py index 3744986..de11b0d 100644 --- a/gtfs/models.py +++ b/gtfs/models.py @@ -1,11 +1,444 @@ -from django.db import models +from django.contrib.gis.db import models +from django.contrib.gis.geos import Point -# Create your models here. + +# ------------- +# GTFS Schedule +# ------------- + + +class Feed(models.Model): + feed_id = models.CharField(max_length=100) + is_current = models.BooleanField() + retrieved_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.retrieved_at + + +class Agency(models.Model): + """One or more transit agencies that provide the data in this feed. + Maps to agency.txt in the GTFS feed. + """ + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + agency_id = models.CharField( + max_length=255, + blank=True, + help_text="Identificador único de la agencia de transportes.", + ) + agency_name = models.CharField( + max_length=255, help_text="Nombre completo de la agencia de transportes." + ) + agency_url = models.URLField(help_text="URL de la agencia de transportes.") + agency_timezone = models.CharField( + max_length=255, help_text="Zona horaria de la agencia de transportes." + ) + agency_lang = models.CharField( + max_length=2, blank=True, help_text="Código ISO 639-1 de idioma primario." + ) + agency_phone = models.CharField( + max_length=127, blank=True, null=True, help_text="Número de teléfono." + ) + agency_fare_url = models.URLField( + blank=True, null=True, help_text="URL para la compra de tiquetes en línea." + ) + agency_email = models.EmailField( + max_length=254, + blank=True, + null=True, + help_text="Correo electrónico de servicio al cliente.", + ) + + class Meta: + verbose_name = "agency" + verbose_name_plural = "agencies" + + def __str__(self): + return self.agency_name class Stop(models.Model): - stop_id = models.CharField(max_length=100) - name = models.CharField(max_length=100) + """Individual locations where vehicles pick up or drop off riders. + Maps to stops.txt in the GTFS feed. + """ + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + stop_id = models.CharField( + max_length=255, help_text="Identificador único de la parada." + ) + stop_code = models.CharField( + max_length=255, blank=True, null=True, help_text="Código de la parada." + ) + stop_name = models.CharField(max_length=255, help_text="Nombre de la parada.") + stop_desc = models.TextField( + blank=True, null=True, help_text="Descripción de la parada." + ) + stop_lat = models.DecimalField( + max_digits=9, + decimal_places=6, + blank=True, + null=True, + help_text="Latitud de la parada.", + ) + stop_lon = models.DecimalField( + max_digits=9, + decimal_places=6, + blank=True, + null=True, + help_text="Longitud de la parada.", + ) + stop_point = models.PointField( + blank=True, null=True, help_text="Punto georreferenciado de la parada." + ) + zone_id = models.CharField( + max_length=255, blank=True, null=True, help_text="Identificador de la zona." + ) + stop_url = models.URLField(blank=True, null=True, help_text="URL de la parada.") + location_type = models.PositiveSmallIntegerField( + blank=True, null=True, help_text="Tipo de parada." + ) + parent_station = models.CharField( + max_length=255, blank=True, help_text="Estación principal." + ) + stop_timezone = models.CharField( + max_length=255, blank=True, help_text="Zona horaria de la parada." + ) + wheelchair_boarding = models.PositiveSmallIntegerField( + blank=True, null=True, help_text="Acceso para sillas de ruedas." + ) + level_id = models.CharField( + max_length=255, blank=True, help_text="Identificador del nivel." + ) + platform_code = models.CharField( + max_length=255, blank=True, help_text="Código de la plataforma." + ) + + # Build stop_point from stop_lat and stop_lon + def save(self, *args, **kwargs): + self.stop_point = Point(self.stop_lon, self.stop_lat) + super(Stop, self).save(*args, **kwargs) + + def __str__(self): + return self.stop_name + + +class Route(models.Model): + """A group of trips that are displayed to riders as a single service. + Maps to routes.txt in the GTFS feed. + """ + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + route_id = models.CharField( + max_length=255, help_text="Identificador único de la ruta." + ) + agency_id = models.ForeignKey( + Agency, blank=True, null=True, on_delete=models.SET_NULL + ) + route_short_name = models.CharField( + max_length=63, blank=True, null=True, help_text="Nombre corto de la ruta." + ) + route_long_name = models.CharField( + max_length=255, help_text="Nombre largo de la ruta." + ) + route_desc = models.TextField( + blank=True, null=True, help_text="Descripción de la ruta." + ) + route_type = models.PositiveSmallIntegerField( + choices=( + (0, "Tranvía o tren ligero."), + (1, "Subterráneo o metro."), + (2, "Ferrocarril."), + (3, "Bus."), + (4, "Ferry."), + (5, "Teleférico."), + (6, "Góndola."), + (7, "Funicular."), + ), + help_text="Tipo de ruta (bus, subway, train, tram, ferry, cable car, gondola, funicular).", + ) + route_url = models.URLField( + blank=True, null=True, help_text="URL de la ruta en el sitio web de la agencia." + ) + route_color = models.CharField( + max_length=6, + blank=True, + null=True, + help_text="Color que representa la ruta en formato hexadecimal.", + ) + route_text_color = models.CharField( + max_length=6, + blank=True, + null=True, + help_text="Color del texto que representa la ruta en formato hexadecimal.", + ) + route_sort_order = models.PositiveSmallIntegerField(blank=True, null=True) + + def __str__(self): + return f"{self.route_short_name}: {self.route_long_name}" + + +class Calendar(models.Model): + """Dates for service IDs using a weekly schedule. + Maps to calendar.txt in the GTFS feed. + """ + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + service_id = models.CharField( + max_length=255, help_text="Identificador único del servicio." + ) + monday = models.BooleanField(help_text="Lunes") + tuesday = models.BooleanField(help_text="Martes") + wednesday = models.BooleanField(help_text="Miércoles") + thursday = models.BooleanField(help_text="Jueves") + friday = models.BooleanField(help_text="Viernes") + saturday = models.BooleanField(help_text="Sábado") + sunday = models.BooleanField(help_text="Domingo") + start_date = models.DateField(help_text="Fecha de inicio del servicio.") + end_date = models.DateField(help_text="Fecha de finalización del servicio.") + + def __str__(self): + return self.service_id + + +class CalendarDate(models.Model): + """Exceptions for the service IDs defined in the calendar.txt file. + Maps to calendar_dates.txt in the GTFS feed. + """ + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + service_id = models.CharField( + max_length=255, help_text="Identificador único del servicio." + ) + date = models.DateField(help_text="Fecha de excepción.") + exception_type = models.PositiveSmallIntegerField( + choices=((1, "Agregar"), (2, "Eliminar")), help_text="Tipo de excepción." + ) + + def __str__(self): + return self.service_id + + +class Shape(models.Model): + """Rules for drawing lines on a map to represent a transit organization's routes. + Maps to shapes.txt in the GTFS feed. + """ + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + shape_id = models.CharField( + max_length=255, help_text="Identificador único de la trayectoria." + ) + shape_pt_lat = models.DecimalField( + max_digits=9, + decimal_places=6, + help_text="Latitud de un punto en la trayectoria.", + ) + shape_pt_lon = models.DecimalField( + max_digits=9, + decimal_places=6, + help_text="Longitud de un punto en la trayectoria.", + ) + shape_pt_sequence = models.PositiveSmallIntegerField( + help_text="Secuencia del punto en la trayectoria." + ) + shape_dist_traveled = models.DecimalField( + max_digits=6, + decimal_places=4, + blank=True, + null=True, + help_text="Distancia recorrida en la trayectoria.", + ) + + def __str__(self): + return self.shape_id + + +class GeoShape(models.Model): + """Rules for drawing lines on a map to represent a transit organization's routes. + Maps to shapes.txt in the GTFS feed. + """ + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + geoshape_id = models.CharField( + max_length=255, help_text="Identificador único de la trayectoria." + ) + geoshape = models.LineStringField(help_text="Trayectoria de la ruta.") + + def __str__(self): + return self.geoshape_id + + +class Trip(models.Model): + """Trips for each route. A trip is a sequence of two or more stops that occurs at specific time. + Maps to trips.txt in the GTFS feed. + """ + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + route_id = models.ForeignKey(Route, on_delete=models.CASCADE) + service_id = models.ForeignKey(Calendar, on_delete=models.CASCADE) + trip_id = models.CharField( + max_length=255, help_text="Identificador único del viaje." + ) + trip_headsign = models.CharField( + max_length=255, blank=True, null=True, help_text="Destino del viaje." + ) + trip_short_name = models.CharField( + max_length=255, blank=True, null=True, help_text="Nombre corto del viaje." + ) + direction_id = models.PositiveSmallIntegerField( + choices=((0, "En un sentido"), (1, "En el otro")), + help_text="Dirección del viaje.", + ) + block_id = models.CharField( + max_length=255, blank=True, null=True, help_text="Identificador del bloque." + ) + shape_id = models.CharField(max_length=255, blank=True, null=True) + geoshape_id = models.ForeignKey( + GeoShape, blank=True, null=True, on_delete=models.SET_NULL + ) + wheelchair_accessible = models.PositiveSmallIntegerField( + choices=((0, "No especificado"), (1, "Accesible"), (2, "No accesible")), + help_text="¿Tiene acceso para sillas de ruedas?", + ) + bikes_allowed = models.PositiveSmallIntegerField( + choices=((0, "No especificado"), (1, "Permitido"), (2, "No permitido")), + help_text="¿ Es permitido llevar bicicletas?", + ) + + def __str__(self): + return self.trip_id + + +class StopTime(models.Model): + """Times that a vehicle arrives at and departs from individual stops for each trip. + Maps to stop_times.txt in the GTFS feed. + """ + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + trip_id = models.ForeignKey(Trip, on_delete=models.CASCADE) + arrival_time = models.TimeField( + help_text="Hora de llegada a la parada.", blank=True, null=True + ) + departure_time = models.TimeField( + help_text="Hora de salida de la parada.", blank=True, null=True + ) + stop_id = models.ForeignKey(Stop, on_delete=models.CASCADE) + stop_sequence = models.PositiveIntegerField( + help_text="Secuencia de la parada en el viaje." + ) + stop_headsign = models.CharField( + max_length=255, blank=True, null=True, help_text="Destino de la parada." + ) + pickup_type = models.PositiveSmallIntegerField( + help_text="Tipo de recogida de pasajeros.", + ) + drop_off_type = models.PositiveSmallIntegerField( + help_text="Tipo de bajada de pasajeros.", + ) + shape_dist_traveled = models.DecimalField( + max_digits=6, + decimal_places=4, + blank=True, + null=True, + help_text="Distancia recorrida en la trayectoria.", + ) + timepoint = models.BooleanField( + blank=True, + null=True, + help_text="¿Es un punto de tiempo programado?", + ) def __str__(self): - return self.name \ No newline at end of file + return f"{self.trip_id}: {self.stop_id} ({self.stop_sequence})" + + +class FeedInfo(models.Model): + """Additional information about the feed itself, including publisher, version, and expiration information. + Maps to feed_info.txt in the GTFS feed. + """ + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + feed_publisher_name = models.CharField( + max_length=255, help_text="Nombre del editor del feed." + ) + feed_publisher_url = models.URLField( + help_text="URL del editor del feed.", blank=True, null=True + ) + feed_lang = models.CharField( + max_length=2, help_text="Código ISO 639-1 de idioma primario." + ) + feed_start_date = models.DateField( + help_text="Fecha de inicio de la información del feed.", blank=True, null=True + ) + feed_end_date = models.DateField( + help_text="Fecha de finalización de la información del feed.", blank=True, null=True + ) + feed_version = models.CharField( + max_length=255, blank=True, null=True, help_text="Versión del feed." + ) + + def __str__(self): + return f"{self.feed_publisher_name}: {self.feed_version}" + + +class FareAttribute(models.Model): + """Rules for how to calculate the fare for a certain kind of trip. + Maps to fare_attributes.txt in the GTFS feed. + """ + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + fare_id = models.CharField( + max_length=255, help_text="Identificador único de la tarifa." + ) + price = models.DecimalField( + max_digits=6, decimal_places=2, help_text="Precio de la tarifa." + ) + currency_type = models.CharField( + max_length=3, help_text="Código ISO 4217 de la moneda." + ) + payment_method = models.PositiveSmallIntegerField( + choices=((0, "Pago a bordo"), (1, "Pago anticipado")), + help_text="Método de pago.", + ) + transfers = models.PositiveSmallIntegerField( + choices=((0, "No permitido"), (1, "Permitido"), (2, "Permitido dentro de la misma agencia")), + help_text="Número de transferencias permitidas.", + ) + transfer_duration = models.PositiveSmallIntegerField( + blank=True, null=True, help_text="Duración de la transferencia." + ) + + def __str__(self): + return self.fare_id + + +class FareRule(models.Model): + """Rules for which fare to apply in a given situation. + Maps to fare_rules.txt in the GTFS feed. + """ + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + fare_id = models.ForeignKey(FareAttribute, on_delete=models.CASCADE) + route_id = models.ForeignKey(Route, on_delete=models.CASCADE) + origin_id = models.CharField(max_length=255, blank=True, null=True) + destination_id = models.CharField(max_length=255, blank=True, null=True) + contains_id = models.CharField(max_length=255, blank=True, null=True) + + def __str__(self): + return f"{self.fare_id}: {self.route_id}" + +# ------------- +# GTFS Realtime +# ------------- From a3ec70af0b1c5cb93634beaf8f604ad4ff8959a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Thu, 29 Feb 2024 11:37:52 -0300 Subject: [PATCH 041/244] =?UTF-8?q?Comentario=20sobre=20c=C3=B3mo=20hacer?= =?UTF-8?q?=20cuando=20no=20reconoce=20migraciones=20por=20hacer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/development.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/development.md b/docs/development.md index 013e259..68b15fa 100644 --- a/docs/development.md +++ b/docs/development.md @@ -23,3 +23,14 @@ Es necesario separar los datos que son relevantes para cada parada (¿tópicos?) (Raspberry Pi) Ejemplo de primer prototipo: Soda de Ingeniería (no requiere protección). + +## Notas sobre problemas + +Cuando hay problemas con migraciones: + +```bash +$ python manage.py migrate --fake zero +$ (borrar migrations) +$ python manage.py makemigrations +$ python manage.py migrate +``` \ No newline at end of file From 13da28da95d09a234ca188ff5cf4d2c45570a074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Thu, 29 Feb 2024 11:39:03 -0300 Subject: [PATCH 042/244] =?UTF-8?q?Eliminar=20un=20archivo=20que=20no=20de?= =?UTF-8?q?ber=C3=ADa=20estar=20en=20el=20repositorio=20.gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- screens/migrations/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 screens/migrations/__init__.py diff --git a/screens/migrations/__init__.py b/screens/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 From 7c233b08062bd70000b6d1570a8653a5c3ab3715 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Tue, 5 Mar 2024 14:39:24 -0300 Subject: [PATCH 043/244] =?UTF-8?q?Agregar=20los=20modelos=20de=20GTFS=20R?= =?UTF-8?q?ealtime=20para=20TripUpdate=20y=20VehiclePosition=20(Alert=20ex?= =?UTF-8?q?cluido=20para=20esta=20implementaci=C3=B3n=20por=20ahora)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gtfs/models.py | 175 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 173 insertions(+), 2 deletions(-) diff --git a/gtfs/models.py b/gtfs/models.py index de11b0d..2657970 100644 --- a/gtfs/models.py +++ b/gtfs/models.py @@ -381,7 +381,9 @@ class FeedInfo(models.Model): help_text="Fecha de inicio de la información del feed.", blank=True, null=True ) feed_end_date = models.DateField( - help_text="Fecha de finalización de la información del feed.", blank=True, null=True + help_text="Fecha de finalización de la información del feed.", + blank=True, + null=True, ) feed_version = models.CharField( max_length=255, blank=True, null=True, help_text="Versión del feed." @@ -412,7 +414,11 @@ class FareAttribute(models.Model): help_text="Método de pago.", ) transfers = models.PositiveSmallIntegerField( - choices=((0, "No permitido"), (1, "Permitido"), (2, "Permitido dentro de la misma agencia")), + choices=( + (0, "No permitido"), + (1, "Permitido"), + (2, "Permitido dentro de la misma agencia"), + ), help_text="Número de transferencias permitidas.", ) transfer_duration = models.PositiveSmallIntegerField( @@ -439,6 +445,171 @@ class FareRule(models.Model): def __str__(self): return f"{self.fare_id}: {self.route_id}" + # ------------- # GTFS Realtime # ------------- + + +class FeedMessage(models.Model): + """ + Header of a GTFS Realtime FeedMessage. + + This is metadata to link records of other models to a retrieved FeedMessage containing several entities, typically (necessarily, in this implementation) of a single kind. + """ + + feed_message_id = models.BigAutoField(primary_key=True) + timestamp = models.DateTimeField(auto_now=True) + entity_type = models.CharField(max_length=63) + incrementality = models.CharField(max_length=15) + gtfs_realtime_version = models.CharField(max_length=15) + + class Meta: + unique_together = (("timestamp", "entity_type"),) + + def __str__(self): + return f"{self.entity_type} ({self.timestamp})" + + +class TripUpdate(models.Model): + """ + GTFS Realtime TripUpdate entity v2.0 (normalized). + + Trip updates represent fluctuations in the timetable. + """ + + trip_update_id = models.BigAutoField(primary_key=True) + entity_id = models.CharField(max_length=127) + + # Foreign key to FeedMessage model + feed_message = models.ForeignKey("FeedMessage", on_delete=models.CASCADE) + + # TripDescriptor (message) + trip_trip_id = models.CharField(max_length=255, blank=True, null=True) + trip_route_id = models.CharField(max_length=255, blank=True, null=True) + trip_direction_id = models.IntegerField(blank=True, null=True) + trip_start_time = models.TimeField(blank=True, null=True) + trip_start_date = models.DateField(blank=True, null=True) + trip_schedule_relationship = models.CharField( + max_length=31, blank=True, null=True + ) # (enum) + + # VehicleDescriptor (message) + vehicle_id = models.CharField(max_length=255, blank=True, null=True) + vehicle_label = models.CharField(max_length=255, blank=True, null=True) + vehicle_license_plate = models.CharField(max_length=255, blank=True, null=True) + vehicle_wheelchair_accessible = models.CharField( + max_length=31, blank=True, null=True + ) # (enum) + + # Timestamp (uint64) + timestamp = models.DateTimeField(blank=True, null=True) + + # Delay (int32) + delay = models.IntegerField(blank=True, null=True) + + def __str__(self): + return f"{self.entity_id} ({self.feed_message})" + + +class StopTimeUpdate(models.Model): + """ + GTFS Realtime TripUpdate message v2.0 (normalized). + + Realtime update for arrival and/or departure events for a given stop on a trip, linked to a TripUpdate entity in a FeedMessage. + """ + + stop_time_update_id = models.BigAutoField(primary_key=True) + + # Foreign key to TripUpdate model + trip_update = models.ForeignKey("TripUpdate", on_delete=models.CASCADE) + + # Stop ID (string) + stop_sequence = models.IntegerField() + stop_id = models.CharField(max_length=127, blank=True, null=True) + + # StopTimeEvent (message): arrival + arrival_delay = models.IntegerField(blank=True, null=True) + arrival_time = models.DateTimeField(blank=True, null=True) + arrival_uncertainty = models.IntegerField(blank=True, null=True) + + # StopTimeEvent (message): departure + departure_delay = models.IntegerField(blank=True, null=True) + departure_time = models.DateTimeField(blank=True, null=True) + departure_uncertainty = models.IntegerField(blank=True, null=True) + + # OccupancyStatus (enum) + departure_occupancy_status = models.CharField(max_length=255, blank=True, null=True) + + # ScheduleRelationship (enum) + schedule_relationship = models.CharField(max_length=255, blank=True, null=True) + + def __str__(self): + return f"{self.stop_id} ({self.trip_update})" + + +class VehiclePosition(models.Model): + """ + GTFS Realtime VehiclePosition entity v2.0 (normalized). + + Vehicle position represents a few basic pieces of information about a particular vehicle on the network. + """ + + vehicle_position_id = models.BigAutoField(primary_key=True) + entity_id = models.CharField(max_length=127) + + # Foreign key to FeedMessage model + feed_message = models.ForeignKey("FeedMessage", on_delete=models.CASCADE) + + # TripDescriptor (message) + trip_trip_id = models.CharField(max_length=255) + vehicle_trip_route_id = models.CharField(max_length=255, blank=True, null=True) + trip_direction_id = models.IntegerField(blank=True, null=True) + trip_start_time = models.TimeField(blank=True, null=True) + trip_start_date = models.DateField(blank=True, null=True) + trip_schedule_relationship = models.CharField( + max_length=31, blank=True, null=True + ) # (enum) + + # VehicleDescriptor (message) + vehicle_id = models.CharField(max_length=255, blank=True, null=True) + vehicle_label = models.CharField(max_length=255, blank=True, null=True) + vehicle_license_plate = models.CharField(max_length=255, blank=True, null=True) + vehicle_wheelchair_accessible = models.CharField( + max_length=31, blank=True, null=True + ) # (enum) + + # Position (message) + position_latitude = models.FloatField(blank=True, null=True) + position_longitude = models.FloatField(blank=True, null=True) + position_point = models.PointField(srid=4326, blank=True, null=True) + position_bearing = models.FloatField(blank=True, null=True) + position_odometer = models.FloatField(blank=True, null=True) + position_speed = models.FloatField(blank=True, null=True) # (meters/second) + + # Current stop sequence (uint32) + current_stop_sequence = models.IntegerField(blank=True, null=True) + + # Stop ID (string) + stop_id = models.CharField(max_length=255, blank=True, null=True) + + # VehicleStopStatus (enum) + current_status = models.CharField(max_length=255, blank=True, null=True) + + # Timestamp (uint64) + timestamp = models.DateTimeField(blank=True, null=True) + + # CongestionLevel (enum) + congestion_level = models.CharField(max_length=255, blank=True, null=True) + + # OccupancyStatus (enum) + occupancy_status = models.CharField(max_length=255, blank=True, null=True) + + # OccupancyPercentage (uint32) + occupancy_percentage = models.IntegerField(blank=True, null=True) + + # CarriageDetails (message) + multi_carriage_details = models.CharField(max_length=255, blank=True, null=True) + + def __str__(self): + return f"{self.entity_id} ({self.feed_message})" From 8443db41e0f191ec5d1bb1967297c07888adc311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Tue, 5 Mar 2024 14:41:59 -0300 Subject: [PATCH 044/244] =?UTF-8?q?Guardar=20position=5Fpoint=20como=20una?= =?UTF-8?q?=20geometr=C3=ADa=20Point=20a=20partir=20de=20position=5Flatitu?= =?UTF-8?q?de=20y=20position=5Flongitude?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gtfs/models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gtfs/models.py b/gtfs/models.py index 2657970..0a17804 100644 --- a/gtfs/models.py +++ b/gtfs/models.py @@ -611,5 +611,9 @@ class VehiclePosition(models.Model): # CarriageDetails (message) multi_carriage_details = models.CharField(max_length=255, blank=True, null=True) + def save(self, *args, **kwargs): + self.position_point = Point(self.position_longitude, self.position_latitude) + super(VehiclePosition, self).save(*args, **kwargs) + def __str__(self): return f"{self.entity_id} ({self.feed_message})" From 64e88e8ab500758f5569983a82322b01a73cc045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Tue, 5 Mar 2024 14:53:50 -0300 Subject: [PATCH 045/244] =?UTF-8?q?Registrar=20los=20nuevos=20modelos=20en?= =?UTF-8?q?=20el=20panel=20de=20administraci=C3=B3n=20de=20Django?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gtfs/admin.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/gtfs/admin.py b/gtfs/admin.py index 8c38f3f..67f400f 100644 --- a/gtfs/admin.py +++ b/gtfs/admin.py @@ -1,3 +1,23 @@ from django.contrib import admin +from .models import * + # Register your models here. + +admin.site.register(Feed) +admin.site.register(Agency) +admin.site.register(Stop) +admin.site.register(Route) +admin.site.register(Calendar) +admin.site.register(CalendarDate) +admin.site.register(Shape) +admin.site.register(GeoShape) +admin.site.register(Trip) +admin.site.register(StopTime) +admin.site.register(FeedInfo) +admin.site.register(FareAttribute) +admin.site.register(FareRule) +admin.site.register(FeedMessage) +admin.site.register(TripUpdate) +admin.site.register(StopTimeUpdate) +admin.site.register(VehiclePosition) From e46b1252bdf644fc7bd40116dbdeef5d34529f40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Wed, 6 Mar 2024 12:10:35 -0300 Subject: [PATCH 046/244] =?UTF-8?q?Eliminar=20c=C3=B3digo=20de=20las=20pru?= =?UTF-8?q?ebas=20de=20WebSockets=20con=20un=20chat=20y=20ordenar=20las=20?= =?UTF-8?q?funciones?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gtfs/tasks.py | 16 ++++++++++++++++ gtfs/views.py | 8 +------- screens/consumers.py | 38 -------------------------------------- screens/routing.py | 3 +-- screens/urls.py | 1 - screens/views.py | 15 --------------- 6 files changed, 18 insertions(+), 63 deletions(-) create mode 100644 gtfs/tasks.py diff --git a/gtfs/tasks.py b/gtfs/tasks.py new file mode 100644 index 0000000..1bd86e1 --- /dev/null +++ b/gtfs/tasks.py @@ -0,0 +1,16 @@ +from celery import shared_task + + +@shared_task +def get_schedule(): + return "Fetching Schedule" + + +@shared_task +def get_vehiclepositions(): + return "Fetching VehiclePositions" + + +@shared_task +def get_tripupdates(): + return "Fetching TripUpdates" diff --git a/gtfs/views.py b/gtfs/views.py index 1291d2a..0203f3f 100644 --- a/gtfs/views.py +++ b/gtfs/views.py @@ -1,5 +1,4 @@ from django.shortcuts import render -from django.contrib.auth.models import User # Create your views here. @@ -9,12 +8,7 @@ def gtfs(request): def schedule(request): - users = User.objects.all() - print(users) - context = { - "users": users, - } - return render(request, "schedule.html", context) + return render(request, "schedule.html") def realtime(request): diff --git a/screens/consumers.py b/screens/consumers.py index 9ed5c3c..29edd70 100644 --- a/screens/consumers.py +++ b/screens/consumers.py @@ -4,43 +4,10 @@ from asgiref.sync import sync_to_async -class ChatConsumer(AsyncWebsocketConsumer): - async def connect(self): - self.room_name = self.scope["url_route"]["kwargs"]["room_name"] - self.room_group_name = f"chat_{self.room_name}" - - # Join room group - await self.channel_layer.group_add(self.room_group_name, self.channel_name) - - await self.accept() - - async def disconnect(self, close_code): - # Leave room group - await self.channel_layer.group_discard(self.room_group_name, self.channel_name) - - # Receive message from WebSocket - async def receive(self, text_data): - text_data_json = json.loads(text_data) - message = text_data_json["message"] - - # Send message to room group - await self.channel_layer.group_send( - self.room_group_name, {"type": "chat.message", "message": message} - ) - - # Receive message from room group - async def chat_message(self, event): - message = event["message"] - - # Send message to WebSocket - await self.send(text_data=json.dumps({"message": message})) - - class ScreenConsumer(AsyncWebsocketConsumer): async def connect(self): self.screen_id = self.scope["url_route"]["kwargs"]["screen_id"] self.screen_group_name = f"screen_{self.screen_id}" - await self.channel_layer.group_add(self.screen_group_name, self.channel_name) await self.accept() await self.activate_screen(self.screen_id) @@ -53,7 +20,6 @@ def activate_screen(self, screen_id): screen.save() async def disconnect(self, close_code): - await self.channel_layer.group_discard( self.screen_group_name, self.channel_name ) @@ -66,17 +32,13 @@ def deactivate_screen(self, screen_id): print(f"Screen {screen_id} is now inactive") screen.save() - # Receive message from WebSocket async def receive(self, text_data): text_data_json = json.loads(text_data) message = text_data_json["message"] - - # Send message to room group await self.channel_layer.group_send( self.screen_group_name, {"type": "screen.message", "message": message} ) - # Send message to WebSocket async def screen_message(self, event): message = event["message"] diff --git a/screens/routing.py b/screens/routing.py index 15c1ac1..8cfe3e9 100644 --- a/screens/routing.py +++ b/screens/routing.py @@ -1,8 +1,7 @@ from django.urls import re_path -from .consumers import ChatConsumer, ScreenConsumer +from .consumers import ScreenConsumer websocket_urlpatterns = [ - re_path(r"ws/chat/(?P\w+)/$", ChatConsumer.as_asgi()), re_path(r"ws/screen/(?P\w+)/$", ScreenConsumer.as_asgi()), ] \ No newline at end of file diff --git a/screens/urls.py b/screens/urls.py index 10f731d..d54e82d 100644 --- a/screens/urls.py +++ b/screens/urls.py @@ -5,7 +5,6 @@ urlpatterns = [ path("", views.screens, name="screens"), path("crear/", views.create_screen, name="create_screen"), - path("chat//", views.chat, name="chat"), path("/", views.screen, name="screen"), path("/editar/", views.edit_screen, name="edit_screen"), ] \ No newline at end of file diff --git a/screens/views.py b/screens/views.py index 02cd094..c442f0c 100644 --- a/screens/views.py +++ b/screens/views.py @@ -5,20 +5,14 @@ def screens(request): - """Render a list of screens. - """ return render(request, "screens.html") def create_screen(request): - """Create and configure a new screen. - """ return render(request, "create_screen.html") def screen(request, screen_id): - """Render a screen. - """ seed = screen_id random.seed(seed) minutes = random.randint(0, 30) @@ -27,20 +21,11 @@ def screen(request, screen_id): def edit_screen(request, screen_id): - """Edit a screen. - """ context = {"screen_id": screen_id} return render(request, "edit_screen.html", context) def update_screen(request, screen_id): - """Update a screen. - """ # Get a Django Signal signaling that the FeedMessage has been processed and there are updates for each stop. # For each screen, collect all data linked to it and send it, with a given format, to the screen via websocket. return 0 - - -# Testing the websocket -def chat(request, room_name): - return render(request, "chat.html", {"room_name": room_name}) From 589166e99b88a0c57dbd8339c4fba5649f7ee55b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Wed, 6 Mar 2024 12:11:00 -0300 Subject: [PATCH 047/244] Ordenar URLs y vistas, eliminar algunos archivos de pruebas --- gtfs/templates/schedule.html | 6 ---- realtime/tasks.py | 54 -------------------------------- realtime/templates/realtime.html | 0 realtime/urls.py | 3 +- realtime/views.py | 14 ++------- screens/templates/chat.html | 49 ----------------------------- website/models.py | 12 +++---- website/views.py | 2 ++ 8 files changed, 11 insertions(+), 129 deletions(-) delete mode 100644 realtime/tasks.py create mode 100644 realtime/templates/realtime.html delete mode 100644 screens/templates/chat.html diff --git a/gtfs/templates/schedule.html b/gtfs/templates/schedule.html index 9c7c01c..b7e51fa 100644 --- a/gtfs/templates/schedule.html +++ b/gtfs/templates/schedule.html @@ -7,9 +7,3 @@

Sitio de configuración GTFS Schedule

El más importante: schedule_url

Cosas para hacer por aquí: validad URL, hacer un request de prueba, también para ver datos históricos del feed, etc.

- -
    - {% for user in users %} -
  1. {{ user.first_name }}: {{ user.last_name }}
  2. - {% endfor %} -
diff --git a/realtime/tasks.py b/realtime/tasks.py deleted file mode 100644 index 19656bc..0000000 --- a/realtime/tasks.py +++ /dev/null @@ -1,54 +0,0 @@ -# Create your tasks here - -from .models import Test -from screens.models import Screen - -from celery import shared_task - -from channels.layers import get_channel_layer -from asgiref.sync import async_to_sync, sync_to_async - -import requests -from time import sleep - - -@shared_task -def test_celery(): - response = requests.get("https://uselessfacts.jsph.pl/api/v2/facts/random") - text = response.json()["text"] - Test.objects.create(text=text) - channel_layer = get_channel_layer() - screens = Screen.objects.filter(is_active=True) - sync_to_async(print)(f"Task: {screen}") - for screen in screens: - async_to_sync(channel_layer.group_send)( - f"screen_{screen.screen_id}", - { - "type": "screen_message", - "message": f"{screen.screen_id}: {text}", - }, - ) - return text - - -@shared_task -def hello_celery(x, y): - for i in range(6): - print(i) - sleep(1) - return f"Done! {x} + {y} = {x + y}" - - -@shared_task -def get_vehiclepositions(): - return "VehiclePositions" - - -@shared_task -def get_tripupdates(): - return "TripUpdates" - - -@shared_task -def get_gtfs(): - return "GTFS" \ No newline at end of file diff --git a/realtime/templates/realtime.html b/realtime/templates/realtime.html new file mode 100644 index 0000000..e69de29 diff --git a/realtime/urls.py b/realtime/urls.py index 2d85b5d..e0e8e65 100644 --- a/realtime/urls.py +++ b/realtime/urls.py @@ -3,6 +3,5 @@ from . import views urlpatterns = [ - path("test/", views.test, name="test"), - path("hello/", views.hello, name="hello"), + path("", views.realtime), ] \ No newline at end of file diff --git a/realtime/views.py b/realtime/views.py index 8c96696..eb5b6bf 100644 --- a/realtime/views.py +++ b/realtime/views.py @@ -1,15 +1,7 @@ -from django.shortcuts import render, HttpResponse - -from .tasks import test_celery, hello_celery +from django.shortcuts import render # Create your views here. -def test(request): - text = test_celery.delay() - return HttpResponse(text.get()) - - -def hello(request): - hello_celery.delay(2, 3) - return HttpResponse("Hello, world!") +def realtime(request): + return render(request, "realtime.html") diff --git a/screens/templates/chat.html b/screens/templates/chat.html deleted file mode 100644 index 0999683..0000000 --- a/screens/templates/chat.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - - Chat Room - - -
-
- - {{ room_name|json_script:"room-name" }} - - - \ No newline at end of file diff --git a/website/models.py b/website/models.py index 5b20f23..a3f9a1e 100644 --- a/website/models.py +++ b/website/models.py @@ -1,15 +1,13 @@ from django.db import models +from django.contrib.auth.models import User # Create your models here. class User(models.Model): - user_id = models.AutoField(primary_key=True) - name = models.CharField(max_length=100) - email = models.EmailField() - password = models.CharField(max_length=100) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) + user = models.OneToOneField(User, on_delete=models.CASCADE) + company = models.CharField(max_length=100) + position = models.CharField(max_length=100) def __str__(self): - return self.name + return self.user.username diff --git a/website/views.py b/website/views.py index 7ce0fe1..1870eef 100644 --- a/website/views.py +++ b/website/views.py @@ -6,8 +6,10 @@ def index(request): return render(request, "index.html") + def about(request): return render(request, "about.html") + def profile(request): return render(request, "profile.html") From e13167292c8a63e16e25907c6d2c210dfed48bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Wed, 6 Mar 2024 12:44:13 -0300 Subject: [PATCH 048/244] =?UTF-8?q?Revisar=20las=20migraciones=20y=20corre?= =?UTF-8?q?gir=20la=20falta=20de=20id=20en=20algunos=20modelos=20(con=20un?= =?UTF-8?q?a=20confusi=C3=B3n=20por=20haber=20puesto=20un=20string=20de=20?= =?UTF-8?q?valor=20por=20defecto=20cuando=20era=20un=20bigint)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gtfs/models.py | 11 ++++------- realtime/admin.py | 2 -- realtime/models.py | 8 -------- screens/models.py | 9 +++++---- website/models.py | 7 ++++--- 5 files changed, 13 insertions(+), 24 deletions(-) diff --git a/gtfs/models.py b/gtfs/models.py index 0a17804..604bc65 100644 --- a/gtfs/models.py +++ b/gtfs/models.py @@ -8,7 +8,7 @@ class Feed(models.Model): - feed_id = models.CharField(max_length=100) + feed_id = models.CharField(max_length=100, primary_key=True) is_current = models.BooleanField() retrieved_at = models.DateTimeField(auto_now=True) @@ -464,9 +464,6 @@ class FeedMessage(models.Model): incrementality = models.CharField(max_length=15) gtfs_realtime_version = models.CharField(max_length=15) - class Meta: - unique_together = (("timestamp", "entity_type"),) - def __str__(self): return f"{self.entity_type} ({self.timestamp})" @@ -478,7 +475,7 @@ class TripUpdate(models.Model): Trip updates represent fluctuations in the timetable. """ - trip_update_id = models.BigAutoField(primary_key=True) + id = models.BigAutoField(primary_key=True) entity_id = models.CharField(max_length=127) # Foreign key to FeedMessage model @@ -519,7 +516,7 @@ class StopTimeUpdate(models.Model): Realtime update for arrival and/or departure events for a given stop on a trip, linked to a TripUpdate entity in a FeedMessage. """ - stop_time_update_id = models.BigAutoField(primary_key=True) + id = models.BigAutoField(primary_key=True) # Foreign key to TripUpdate model trip_update = models.ForeignKey("TripUpdate", on_delete=models.CASCADE) @@ -555,7 +552,7 @@ class VehiclePosition(models.Model): Vehicle position represents a few basic pieces of information about a particular vehicle on the network. """ - vehicle_position_id = models.BigAutoField(primary_key=True) + id = models.BigAutoField(primary_key=True) entity_id = models.CharField(max_length=127) # Foreign key to FeedMessage model diff --git a/realtime/admin.py b/realtime/admin.py index 23fefe3..ec50db8 100644 --- a/realtime/admin.py +++ b/realtime/admin.py @@ -1,6 +1,4 @@ from django.contrib import admin -from .models import Test # Register your models here. -admin.site.register(Test) \ No newline at end of file diff --git a/realtime/models.py b/realtime/models.py index 496ad36..71a8362 100644 --- a/realtime/models.py +++ b/realtime/models.py @@ -1,11 +1,3 @@ from django.db import models # Create your models here. - - -class Test(models.Model): - text = models.TextField() - created_at = models.DateTimeField(auto_now_add=True) - - def __str__(self): - return f"{self.created_at}: {self.text}" diff --git a/screens/models.py b/screens/models.py index dae9877..844f7a7 100644 --- a/screens/models.py +++ b/screens/models.py @@ -6,8 +6,8 @@ class Screen(models.Model): ORIENTATION_CHOICES = [ - ("landscape", "Landscape"), - ("portrait", "Portrait"), + ("landscape", "Horizontal"), + ("portrait", "Vertical"), ] RATIO_CHOICES = [ ("4:3", "4:3"), @@ -15,7 +15,7 @@ class Screen(models.Model): ("16:10", "16:10"), ] - screen_id = models.CharField(max_length=100) + screen_id = models.CharField(max_length=100, primary_key=True) name = models.CharField(max_length=100) description = models.TextField(blank=True, null=True) location = models.PointField(blank=True, null=True) @@ -32,7 +32,7 @@ class Screen(models.Model): default="16:9", blank=True, null=True ) - size = models.PositiveIntegerField(help_text="in inches", blank=True, null=True) + size = models.PositiveIntegerField(help_text="diagonal en pulgadas", blank=True, null=True) has_audio = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -43,6 +43,7 @@ def __str__(self): class ScreenStops(models.Model): + id = models.AutoField(primary_key=True) screen = models.ForeignKey(Screen, on_delete=models.CASCADE) stop = models.ForeignKey(Stop, on_delete=models.CASCADE) diff --git a/website/models.py b/website/models.py index a3f9a1e..465ad93 100644 --- a/website/models.py +++ b/website/models.py @@ -5,9 +5,10 @@ class User(models.Model): - user = models.OneToOneField(User, on_delete=models.CASCADE) - company = models.CharField(max_length=100) - position = models.CharField(max_length=100) + id = models.AutoField(primary_key=True) + user = models.OneToOneField(User, on_delete=models.CASCADE, blank=True, null=True) + company = models.CharField(max_length=100, blank=True, null=True) + position = models.CharField(max_length=100, blank=True, null=True) def __str__(self): return self.user.username From 54c8731cb66f681812a8170ada6f720c5063fe09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Wed, 6 Mar 2024 13:34:41 -0300 Subject: [PATCH 049/244] =?UTF-8?q?Agregar=20modelo=20Company=20a=20la=20a?= =?UTF-8?q?plicaci=C3=B3n=20gtfs,=20a=20la=20que=20estar=C3=A1n=20ligados?= =?UTF-8?q?=20los=20Feed=20y=20los=20FeedMessage=20(el=20sistema=20puede?= =?UTF-8?q?=20administrar=20m=C3=A1s=20de=20una=20agencia=20en=20un=20solo?= =?UTF-8?q?=20suministro=20y=20m=C3=A1s=20de=20un=20suministro=20por=20dif?= =?UTF-8?q?erentes=20compa=C3=B1=C3=ADas,=20pero=20una=20compa=C3=B1=C3=AD?= =?UTF-8?q?a=20solamente=20puede=20crear=20un=20suministro/feed)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gtfs/admin.py | 1 + gtfs/models.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/gtfs/admin.py b/gtfs/admin.py index 67f400f..3395a72 100644 --- a/gtfs/admin.py +++ b/gtfs/admin.py @@ -4,6 +4,7 @@ # Register your models here. +admin.site.register(Company) admin.site.register(Feed) admin.site.register(Agency) admin.site.register(Stop) diff --git a/gtfs/models.py b/gtfs/models.py index 604bc65..c51ee14 100644 --- a/gtfs/models.py +++ b/gtfs/models.py @@ -2,6 +2,41 @@ from django.contrib.gis.geos import Point +class Company(models.Model): + """A company provides transportation services GTFS data. + + It might or might not be the same as the agency in the GTFS feed. A company can have multiple agencies. + """ + + company_id = models.BigAutoField(primary_key=True) + name = models.CharField(max_length=255, help_text="Nombre de la empresa.") + description = models.TextField(blank=True, null=True, help_text="Descripción de la institución o empresa.") + website = models.URLField( + blank=True, null=True, help_text="Sitio web de la empresa." + ) + schedule_url = models.URLField( + blank=True, null=True, help_text="URL del suministro (Feed) de GTFS Schedule." + ) + trip_updates_url = models.URLField( + blank=True, + null=True, + help_text="URL del suministro (FeedMessage) Protobuf (.pb) de GTFS Realtime TripUpdates.", + ) + vehicle_positions_url = models.URLField( + blank=True, + null=True, + help_text="URL del suministro (FeedMessage) Protobuf (.pb) de GTFS Realtime VehiclePositions.", + ) + service_alerts_url = models.URLField( + blank=True, + null=True, + help_text="URL del suministro (FeedMessage) Protobuf (.pb) de GTFS Realtime ServiceAlerts.", + ) + + def __str__(self): + return self.name + + # ------------- # GTFS Schedule # ------------- @@ -9,6 +44,7 @@ class Feed(models.Model): feed_id = models.CharField(max_length=100, primary_key=True) + company = models.ForeignKey(Company, on_delete=models.SET_NULL, blank=True, null=True) is_current = models.BooleanField() retrieved_at = models.DateTimeField(auto_now=True) @@ -55,6 +91,8 @@ class Meta: verbose_name = "agency" verbose_name_plural = "agencies" + # TODO: colocar las restricciones con unique_constraint. Por ejemplo: la combinación agency_id + feed debe ser única. + def __str__(self): return self.agency_name @@ -459,6 +497,7 @@ class FeedMessage(models.Model): """ feed_message_id = models.BigAutoField(primary_key=True) + company = models.ForeignKey(Company, on_delete=models.SET_NULL, blank=True, null=True) timestamp = models.DateTimeField(auto_now=True) entity_type = models.CharField(max_length=63) incrementality = models.CharField(max_length=15) From ee246a7147342705b7dd803709a60fa119e2dc31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Wed, 6 Mar 2024 13:35:10 -0300 Subject: [PATCH 050/244] =?UTF-8?q?Excluir=20archivos=20de=20configuraci?= =?UTF-8?q?=C3=B3n=20(donde=20van=20a=20estar=20algunos=20datos=20de=20las?= =?UTF-8?q?=20bases=20de=20datos)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 3d701e7..069b479 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Configuration files +*.cfg + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] From 0a8da8fc222003cfb5334ddfa8279637fec4b410 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Thu, 7 Mar 2024 11:15:45 -0300 Subject: [PATCH 051/244] Logramos guardar el registo de Feed nuevo desde MBTA, falta agregar el resto de tablas --- gtfs/models.py | 4 ++- gtfs/tasks.py | 88 ++++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + 3 files changed, 92 insertions(+), 1 deletion(-) diff --git a/gtfs/models.py b/gtfs/models.py index c51ee14..8e1ea20 100644 --- a/gtfs/models.py +++ b/gtfs/models.py @@ -45,7 +45,9 @@ def __str__(self): class Feed(models.Model): feed_id = models.CharField(max_length=100, primary_key=True) company = models.ForeignKey(Company, on_delete=models.SET_NULL, blank=True, null=True) - is_current = models.BooleanField() + http_etag = models.CharField(max_length=1023, blank=True, null=True) + is_current = models.BooleanField(blank=True, null=True) + last_modified = models.DateTimeField(blank=True, null=True) retrieved_at = models.DateTimeField(auto_now=True) def __str__(self): diff --git a/gtfs/tasks.py b/gtfs/tasks.py index 1bd86e1..dd8a259 100644 --- a/gtfs/tasks.py +++ b/gtfs/tasks.py @@ -1,8 +1,96 @@ from celery import shared_task +import logging +import configparser +import datetime +from sqlalchemy import create_engine +import pandas as pd +import requests +from asgiref.sync import async_to_sync, sync_to_async + +from .models import * + @shared_task def get_schedule(): + + # Logging configuration + logging.basicConfig( + filename="schedule.log", + format="%(levelname)s: %(message)s", + encoding="utf-8", + level=logging.INFO, + ) + + # Configuration file + # config = configparser.ConfigParser() + # config.read("./gtfs.cfg") + + # Database information (get from .env file? It'd be better) + system = "postgresql" # config.get("database", "system") + host = "localhost" # config.get("database", "host") + port = 5432 # config.get("database", "port") + name = "gtfs2screens" # config.get("database", "name") + user = "fabian" # config.get("database", "user") + password = "" # config.get("database", "password") + + # Create database engine + engine = create_engine(f"{system}://{user}:{password}@{host}:{port}/{name}") + + # GTFS information + feed_transit_system = "MBTA" # config.get("gtfs", "transit_system") + schedule_url = "https://cdn.mbta.com/MBTA_GTFS.zip" # config.get("gtfs", "schedule_url") + tables = { + "agency": "Agency", + "stops": "Stop", + "shapes": "Shape", + "calendar": "Calendar", + "calendar_dates": "CalendarDate", + "routes": "Route", + "trips": "Trip", + "stop_times": "StopTime", + "frequencies": "Frequency", + "feed_info": "FeedInfo", + } # They must be loaded in this order + + logging.info( + f"New GTFS Schedule updating session\n{feed_transit_system}\n{datetime.datetime.now()}\nData source: {schedule_url}" + ) + + # Check if the feed has been updated + last_feed_tag = {"value": None} + try: + last_feed_tag["value"] = ( + Feed.objects.all() + .order_by("-retrieved_at") + .first() + .http_etag + ) + except: + logging.info("No http_etag found in the table 'feeds'!") + logging.info("A new feed will be imported!") + + logging.info(f"Performing fetch at {datetime.datetime.now()}") + + schedule_check = requests.head(schedule_url) + feed_tag = schedule_check.headers["ETag"] + + if not feed_tag == last_feed_tag["value"]: + logging.info(f"New GTFS Schedule feed detected: {feed_tag}") + logging.info("Importing new feed") + + # Save new Feed record + last_modified = datetime.datetime.strptime( + schedule_check.headers["Last-Modified"], "%a, %d %b %Y %H:%M:%S %Z" + ) + feed_id = last_modified.strftime("%Y-%m-%dT%H:%M:%S") + # Save to database with the Feed model + Feed.objects.create( + feed_id=feed_id, + http_etag=feed_tag, + last_modified=last_modified, + ) + return "Fetching Schedule" diff --git a/requirements.txt b/requirements.txt index 9bd3b53..77a71ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,4 @@ schedule pandas gtfs-realtime-bindings python-decouple +sqlalchemy From cc84f2c210759696080fd7aa562124a90aec4e9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Thu, 7 Mar 2024 17:29:19 -0300 Subject: [PATCH 052/244] =?UTF-8?q?Primera=20implementaci=C3=B3n=20de=20un?= =?UTF-8?q?a=20se=C3=B1al=20para=20iniciar=20una=20nueva=20tarea=20despu?= =?UTF-8?q?=C3=A9s=20de=20las=20tareas=20peri=C3=B3dicas=20de=20Celery,=20?= =?UTF-8?q?y=20definici=C3=B3n=20del=20documento=20screens/multicast.py=20?= =?UTF-8?q?donde=20se=20va=20a=20gestionar=20la=20distribuci=C3=B3n=20de?= =?UTF-8?q?=20informaci=C3=B3n=20a=20las=20pantallas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gtfs/models.py | 40 +++++++++++++++++++++++++++++++++++++--- gtfs/tasks.py | 4 ++++ screens/multicast.py | 2 ++ 3 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 screens/multicast.py diff --git a/gtfs/models.py b/gtfs/models.py index 8e1ea20..297dae6 100644 --- a/gtfs/models.py +++ b/gtfs/models.py @@ -1,6 +1,10 @@ from django.contrib.gis.db import models from django.contrib.gis.geos import Point +from django.db.models.signals import post_save + +from screens.multicast import test_signal + class Company(models.Model): """A company provides transportation services GTFS data. @@ -10,7 +14,9 @@ class Company(models.Model): company_id = models.BigAutoField(primary_key=True) name = models.CharField(max_length=255, help_text="Nombre de la empresa.") - description = models.TextField(blank=True, null=True, help_text="Descripción de la institución o empresa.") + description = models.TextField( + blank=True, null=True, help_text="Descripción de la institución o empresa." + ) website = models.URLField( blank=True, null=True, help_text="Sitio web de la empresa." ) @@ -44,7 +50,9 @@ def __str__(self): class Feed(models.Model): feed_id = models.CharField(max_length=100, primary_key=True) - company = models.ForeignKey(Company, on_delete=models.SET_NULL, blank=True, null=True) + company = models.ForeignKey( + Company, on_delete=models.SET_NULL, blank=True, null=True + ) http_etag = models.CharField(max_length=1023, blank=True, null=True) is_current = models.BooleanField(blank=True, null=True) last_modified = models.DateTimeField(blank=True, null=True) @@ -499,7 +507,9 @@ class FeedMessage(models.Model): """ feed_message_id = models.BigAutoField(primary_key=True) - company = models.ForeignKey(Company, on_delete=models.SET_NULL, blank=True, null=True) + company = models.ForeignKey( + Company, on_delete=models.SET_NULL, blank=True, null=True + ) timestamp = models.DateTimeField(auto_now=True) entity_type = models.CharField(max_length=63) incrementality = models.CharField(max_length=15) @@ -655,3 +665,27 @@ def save(self, *args, **kwargs): def __str__(self): return f"{self.entity_id} ({self.feed_message})" + + +# ------- +# Records +# ------- + + +class Record(models.Model): + """A log of the GTFS Schedule updating sessions.""" + + id = models.BigAutoField(primary_key=True) + timestamp = models.DateTimeField(auto_now=True) + company = models.ForeignKey( + Company, on_delete=models.SET_NULL, blank=True, null=True + ) + data_source = models.URLField(blank=True, null=True) + description = models.TextField(blank=True, null=True) + + def __str__(self): + return f"{self.feed_transit_system} ({self.timestamp})" + + +# Django Model Signal (find a good place for this) +post_save.connect(test_signal, sender=Record) diff --git a/gtfs/tasks.py b/gtfs/tasks.py index dd8a259..07b9ecb 100644 --- a/gtfs/tasks.py +++ b/gtfs/tasks.py @@ -91,6 +91,10 @@ def get_schedule(): last_modified=last_modified, ) + Record.objects.create( + data_source = "https://cdn.mbta.com/MBTA_GTFS.zip", + ) + return "Fetching Schedule" diff --git a/screens/multicast.py b/screens/multicast.py new file mode 100644 index 0000000..80ce2d4 --- /dev/null +++ b/screens/multicast.py @@ -0,0 +1,2 @@ +def test_signal(sender, **kwargs): + print("Vamos a ver cómo está la cosa...") \ No newline at end of file From 839a8d7284f082a1961cfc459357899b0f0a4684 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Tue, 19 Mar 2024 22:23:01 -0300 Subject: [PATCH 053/244] =?UTF-8?q?Primera=20pruebas=20de=20importaci?= =?UTF-8?q?=C3=B3n=20de=20tablas=20a=20la=20base=20de=20datos=20(parcialme?= =?UTF-8?q?nte=20exitosas)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gtfs/models.py | 6 +-- gtfs/tasks.py | 109 +++++++++++++++++++++++++------------------------ 2 files changed, 58 insertions(+), 57 deletions(-) diff --git a/gtfs/models.py b/gtfs/models.py index 297dae6..811f22e 100644 --- a/gtfs/models.py +++ b/gtfs/models.py @@ -49,13 +49,13 @@ def __str__(self): class Feed(models.Model): - feed_id = models.CharField(max_length=100, primary_key=True) + feed_id = models.CharField(max_length=100, primary_key=True, unique=True) company = models.ForeignKey( Company, on_delete=models.SET_NULL, blank=True, null=True ) http_etag = models.CharField(max_length=1023, blank=True, null=True) + http_last_modified = models.DateTimeField(blank=True, null=True) is_current = models.BooleanField(blank=True, null=True) - last_modified = models.DateTimeField(blank=True, null=True) retrieved_at = models.DateTimeField(auto_now=True) def __str__(self): @@ -68,7 +68,7 @@ class Agency(models.Model): """ id = models.BigAutoField(primary_key=True) - feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + feed = models.ForeignKey(Feed, to_field="feed_id", on_delete=models.CASCADE) agency_id = models.CharField( max_length=255, blank=True, diff --git a/gtfs/tasks.py b/gtfs/tasks.py index 07b9ecb..b7df26a 100644 --- a/gtfs/tasks.py +++ b/gtfs/tasks.py @@ -1,9 +1,10 @@ from celery import shared_task import logging -import configparser -import datetime -from sqlalchemy import create_engine +from datetime import datetime +import pytz +import zipfile +import io import pandas as pd import requests from asgiref.sync import async_to_sync, sync_to_async @@ -13,86 +14,86 @@ @shared_task def get_schedule(): - + # Logging configuration logging.basicConfig( - filename="schedule.log", format="%(levelname)s: %(message)s", encoding="utf-8", level=logging.INFO, ) - # Configuration file - # config = configparser.ConfigParser() - # config.read("./gtfs.cfg") - - # Database information (get from .env file? It'd be better) - system = "postgresql" # config.get("database", "system") - host = "localhost" # config.get("database", "host") - port = 5432 # config.get("database", "port") - name = "gtfs2screens" # config.get("database", "name") - user = "fabian" # config.get("database", "user") - password = "" # config.get("database", "password") - - # Create database engine - engine = create_engine(f"{system}://{user}:{password}@{host}:{port}/{name}") - # GTFS information - feed_transit_system = "MBTA" # config.get("gtfs", "transit_system") - schedule_url = "https://cdn.mbta.com/MBTA_GTFS.zip" # config.get("gtfs", "schedule_url") - tables = { - "agency": "Agency", - "stops": "Stop", - "shapes": "Shape", - "calendar": "Calendar", - "calendar_dates": "CalendarDate", - "routes": "Route", - "trips": "Trip", - "stop_times": "StopTime", - "frequencies": "Frequency", - "feed_info": "FeedInfo", - } # They must be loaded in this order + company = "MBTA" + schedule_url = "https://cdn.mbta.com/MBTA_GTFS.zip" logging.info( - f"New GTFS Schedule updating session\n{feed_transit_system}\n{datetime.datetime.now()}\nData source: {schedule_url}" + f"GTFS Schedule updating session\n{company}\n{datetime.now()}\nData source: {schedule_url}" ) # Check if the feed has been updated last_feed_tag = {"value": None} try: last_feed_tag["value"] = ( - Feed.objects.all() - .order_by("-retrieved_at") - .first() - .http_etag + Feed.objects.all().order_by("-retrieved_at").first().http_etag ) except: - logging.info("No http_etag found in the table 'feeds'!") - logging.info("A new feed will be imported!") - - logging.info(f"Performing fetch at {datetime.datetime.now()}") + logging.info("No records found in the table 'feeds'.") + # Get the feed's ETag to compare with the last one schedule_check = requests.head(schedule_url) feed_tag = schedule_check.headers["ETag"] if not feed_tag == last_feed_tag["value"]: - logging.info(f"New GTFS Schedule feed detected: {feed_tag}") - logging.info("Importing new feed") + logging.info(f"Importing new GTFS Schedule feed detected: {feed_tag}") - # Save new Feed record - last_modified = datetime.datetime.strptime( - schedule_check.headers["Last-Modified"], "%a, %d %b %Y %H:%M:%S %Z" - ) - feed_id = last_modified.strftime("%Y-%m-%dT%H:%M:%S") - # Save to database with the Feed model - Feed.objects.create( + # Request feed + schedule_response = requests.get(schedule_url) + schedule_zip = zipfile.ZipFile(io.BytesIO(schedule_response.content)) + + last_modified = schedule_check.headers["Last-Modified"] + last_modified = datetime.strptime(last_modified, "%a, %d %b %Y %H:%M:%S %Z") + last_modified = last_modified.replace(tzinfo=pytz.UTC) + feed_id = f"{company}-{int(last_modified.timestamp())}" + + # Save feed record to database with the Feed model + feed = Feed.objects.create( feed_id=feed_id, http_etag=feed_tag, - last_modified=last_modified, + http_last_modified=last_modified, ) + tables = { + "agency": "Agency", + "stops": "Stop", + "shapes": "Shape", + "calendar": "Calendar", + "calendar_dates": "CalendarDate", + "routes": "Route", + "trips": "Trip", + "stop_times": "StopTime", + "feed_info": "FeedInfo", + } # They must be loaded in this order + + # Import and save tables + for table_name in tables.keys(): + file = f"{table_name}.txt" + if file in schedule_zip.namelist(): + model = eval(f"{tables[table_name]}") + fields = [field.name for field in model._meta.fields] + table = pd.read_csv( + schedule_zip.open(file), + dtype=str, + keep_default_na=False, + na_values="", + ) + table = table[[col for col in fields if col in table.columns]] + table["feed"] = feed + objects = [model(**row) for row in table.to_dict(orient="records")] + model.objects.bulk_create(objects) + logging.info(f"{file} imported successfully") + Record.objects.create( - data_source = "https://cdn.mbta.com/MBTA_GTFS.zip", + data_source="https://cdn.mbta.com/MBTA_GTFS.zip", ) return "Fetching Schedule" From 34d88b2fccdbcbe1d03235a4d3f55906e7e7fd72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Sat, 20 Apr 2024 14:15:41 -0300 Subject: [PATCH 054/244] Cambiar el nombre del proyecto de gtfs2screens a datahub y migrar a nueva base de datos --- HOWTO.md | 10 ++++---- README.md | 2 +- {gtfs2screens => datahub}/__init__.py | 0 {gtfs2screens => datahub}/asgi.py | 4 +-- {gtfs2screens => datahub}/celery.py | 4 +-- {gtfs2screens => datahub}/settings.py | 8 +++--- {gtfs2screens => datahub}/urls.py | 2 +- {gtfs2screens => datahub}/wsgi.py | 4 +-- gtfs/tasks.py | 2 +- manage.py | 2 +- realtime/migrations/__init__.py | 0 requirements.txt | 1 - screens/templates/create_screen.html | 20 --------------- screens/templates/edit_screen.html | 5 ---- screens/templates/screen.html | 36 --------------------------- screens/templates/screens.html | 5 ---- website/templates/index.html | 2 +- 17 files changed, 20 insertions(+), 87 deletions(-) rename {gtfs2screens => datahub}/__init__.py (100%) rename {gtfs2screens => datahub}/asgi.py (82%) rename {gtfs2screens => datahub}/celery.py (85%) rename {gtfs2screens => datahub}/settings.py (95%) rename {gtfs2screens => datahub}/urls.py (95%) rename {gtfs2screens => datahub}/wsgi.py (72%) delete mode 100644 realtime/migrations/__init__.py delete mode 100644 screens/templates/create_screen.html delete mode 100644 screens/templates/edit_screen.html delete mode 100644 screens/templates/screen.html delete mode 100644 screens/templates/screens.html diff --git a/HOWTO.md b/HOWTO.md index 4227d23..949a80b 100644 --- a/HOWTO.md +++ b/HOWTO.md @@ -55,19 +55,19 @@ postgres=# ALTER ROLE user_name LOGIN; Ahora podemos crear una base de datos, para este proyecto: ```bash -createdb gtfs2screens +createdb datahub ``` ahora hay que ingresar a esa base de datos: ```bash -psql gtfs2screens +psql datahub ``` y ahí crear la extensión de PostGIS con: ```bash -gtfs2screens=# CREATE EXTENSION postgis; +datahub=# CREATE EXTENSION postgis; ``` Con esto quedaría lista la base de datos para conectarnos desde Django. @@ -89,7 +89,7 @@ y probar con `celery --version`. Ejecutar Celery con: ```bash -celery -A gtfs2screens worker --loglevel=info +celery -A datahub worker --loglevel=info ``` ### Celery Beat @@ -97,7 +97,7 @@ celery -A gtfs2screens worker --loglevel=info Celery utiliza los paquetes de integración con Django `django-celery-results` y `django-celery-beat`, y el intermediador de mensajes Redis. ```bash -celery -A gtfs2screens beat --scheduler django_celery_beat.schedulers:DatabaseScheduler --loglevel=info +celery -A datahub beat --scheduler django_celery_beat.schedulers:DatabaseScheduler --loglevel=info ``` ## Redis diff --git a/README.md b/README.md index 66d8a11..089f933 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# gtfs2screens +# datahub Implementación de pantallas con información en tiempo real de transporte público a partir de la especificación GTFS diff --git a/gtfs2screens/__init__.py b/datahub/__init__.py similarity index 100% rename from gtfs2screens/__init__.py rename to datahub/__init__.py diff --git a/gtfs2screens/asgi.py b/datahub/asgi.py similarity index 82% rename from gtfs2screens/asgi.py rename to datahub/asgi.py index 115d4a1..9eabacc 100644 --- a/gtfs2screens/asgi.py +++ b/datahub/asgi.py @@ -1,5 +1,5 @@ """ -ASGI config for gtfs2screens project. +ASGI config for datahub project. It exposes the ASGI callable as a module-level variable named ``application``. @@ -14,7 +14,7 @@ from screens.routing import websocket_urlpatterns -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gtfs2screens.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "datahub.settings") application = ProtocolTypeRouter( { diff --git a/gtfs2screens/celery.py b/datahub/celery.py similarity index 85% rename from gtfs2screens/celery.py rename to datahub/celery.py index bbb50c6..1a5db5b 100644 --- a/gtfs2screens/celery.py +++ b/datahub/celery.py @@ -3,9 +3,9 @@ from celery import Celery # Set the default Django settings module for the 'celery' program. -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gtfs2screens.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "datahub.settings") -app = Celery("gtfs2screens") +app = Celery("datahub") # Using a string here means the worker doesn't have to serialize # the configuration object to child processes. diff --git a/gtfs2screens/settings.py b/datahub/settings.py similarity index 95% rename from gtfs2screens/settings.py rename to datahub/settings.py index 2198f6e..f2bec79 100644 --- a/gtfs2screens/settings.py +++ b/datahub/settings.py @@ -1,5 +1,5 @@ """ -Django settings for gtfs2screens project. +Django settings for datahub project. Generated by 'django-admin startproject' using Django 5.0. @@ -60,7 +60,7 @@ "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = "gtfs2screens.urls" +ROOT_URLCONF = "datahub.urls" TEMPLATES = [ { @@ -78,8 +78,8 @@ }, ] -WSGI_APPLICATION = "gtfs2screens.wsgi.application" -ASGI_APPLICATION = "gtfs2screens.asgi.application" +WSGI_APPLICATION = "datahub.wsgi.application" +ASGI_APPLICATION = "datahub.asgi.application" # Database diff --git a/gtfs2screens/urls.py b/datahub/urls.py similarity index 95% rename from gtfs2screens/urls.py rename to datahub/urls.py index c7f5f89..f5cba79 100644 --- a/gtfs2screens/urls.py +++ b/datahub/urls.py @@ -1,5 +1,5 @@ """ -URL configuration for gtfs2screens project. +URL configuration for datahub project. The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/5.0/topics/http/urls/ diff --git a/gtfs2screens/wsgi.py b/datahub/wsgi.py similarity index 72% rename from gtfs2screens/wsgi.py rename to datahub/wsgi.py index de2ad20..d930ce1 100644 --- a/gtfs2screens/wsgi.py +++ b/datahub/wsgi.py @@ -1,5 +1,5 @@ """ -WSGI config for gtfs2screens project. +WSGI config for datahub project. It exposes the WSGI callable as a module-level variable named ``application``. @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gtfs2screens.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "datahub.settings") application = get_wsgi_application() diff --git a/gtfs/tasks.py b/gtfs/tasks.py index 07b9ecb..b4c73d1 100644 --- a/gtfs/tasks.py +++ b/gtfs/tasks.py @@ -30,7 +30,7 @@ def get_schedule(): system = "postgresql" # config.get("database", "system") host = "localhost" # config.get("database", "host") port = 5432 # config.get("database", "port") - name = "gtfs2screens" # config.get("database", "name") + name = "datahub" # config.get("database", "name") user = "fabian" # config.get("database", "user") password = "" # config.get("database", "password") diff --git a/manage.py b/manage.py index 1844b0d..75f7874 100755 --- a/manage.py +++ b/manage.py @@ -6,7 +6,7 @@ def main(): """Run administrative tasks.""" - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gtfs2screens.settings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "datahub.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: diff --git a/realtime/migrations/__init__.py b/realtime/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/requirements.txt b/requirements.txt index 77a71ae..9bd3b53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,4 +12,3 @@ schedule pandas gtfs-realtime-bindings python-decouple -sqlalchemy diff --git a/screens/templates/create_screen.html b/screens/templates/create_screen.html deleted file mode 100644 index e323277..0000000 --- a/screens/templates/create_screen.html +++ /dev/null @@ -1,20 +0,0 @@ -

Página de creación/registro/configuración de una nueva pantalla

- -

- Formulario con toda la información necesaria para la pantalla. -

- -
    -
  • screen_id
  • -
  • name
  • -
  • address
  • -
  • location (ejemplo: 9.93752787687643, -84.04463400265841 con PostGIS y GeoDjango)
  • -
  • size (ejemplo: 32")
  • -
  • ratio (ejemplo: 16:9)
  • -
  • orientation (ejemplo: VERTICAL, HORIZONTAL)
  • -
  • has_sound
  • -
- -

- Aquí hay que hacer una validación de los datos ingresados por el usuario. -

\ No newline at end of file diff --git a/screens/templates/edit_screen.html b/screens/templates/edit_screen.html deleted file mode 100644 index 017e2f1..0000000 --- a/screens/templates/edit_screen.html +++ /dev/null @@ -1,5 +0,0 @@ -

Página de edición de los datos de configuración de cada pantalla

- -

- Pantalla: {{ screen_id }} -

\ No newline at end of file diff --git a/screens/templates/screen.html b/screens/templates/screen.html deleted file mode 100644 index e4973f8..0000000 --- a/screens/templates/screen.html +++ /dev/null @@ -1,36 +0,0 @@ -

Ruta UCR

- -

Faltan {{ minutes }} minutos para el bus

- -

- Pantalla: {{ screen_id }} -

- -

- - \ No newline at end of file diff --git a/screens/templates/screens.html b/screens/templates/screens.html deleted file mode 100644 index 6e5710c..0000000 --- a/screens/templates/screens.html +++ /dev/null @@ -1,5 +0,0 @@ -

Lista administrativa de todas las pantallas

- -

- Aquí pueden haber tablas con la lista, mapas con la ubicación de las pantallas, etc. -

\ No newline at end of file diff --git a/website/templates/index.html b/website/templates/index.html index ce693d8..642cab9 100644 --- a/website/templates/index.html +++ b/website/templates/index.html @@ -1,4 +1,4 @@ -

Esta es la página principal

+

datahub

Aquí habrá una referencia a la página de inicio de sesión y a la página de registro.

\ No newline at end of file From 17745e7ca41660d51fad5fc02afeb0a0b8a49607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Sat, 20 Apr 2024 14:44:34 -0300 Subject: [PATCH 055/244] Cambiar app screens a alerts para un futuro sistema de alertas complementario al API --- {screens => alerts}/__init__.py | 0 {screens => alerts}/admin.py | 0 {screens => alerts}/apps.py | 4 ++-- {screens => alerts}/consumers.py | 0 {screens => alerts}/models.py | 0 {screens => alerts}/multicast.py | 0 {screens => alerts}/routing.py | 0 {screens => alerts}/tests.py | 0 {screens => alerts}/urls.py | 0 {screens => alerts}/views.py | 0 datahub/asgi.py | 2 +- datahub/settings.py | 2 +- datahub/urls.py | 2 +- docs/architecture.md | 2 +- docs/index.md | 2 ++ gtfs/models.py | 2 +- 16 files changed, 9 insertions(+), 7 deletions(-) rename {screens => alerts}/__init__.py (100%) rename {screens => alerts}/admin.py (100%) rename {screens => alerts}/apps.py (63%) rename {screens => alerts}/consumers.py (100%) rename {screens => alerts}/models.py (100%) rename {screens => alerts}/multicast.py (100%) rename {screens => alerts}/routing.py (100%) rename {screens => alerts}/tests.py (100%) rename {screens => alerts}/urls.py (100%) rename {screens => alerts}/views.py (100%) diff --git a/screens/__init__.py b/alerts/__init__.py similarity index 100% rename from screens/__init__.py rename to alerts/__init__.py diff --git a/screens/admin.py b/alerts/admin.py similarity index 100% rename from screens/admin.py rename to alerts/admin.py diff --git a/screens/apps.py b/alerts/apps.py similarity index 63% rename from screens/apps.py rename to alerts/apps.py index de60361..2be2313 100644 --- a/screens/apps.py +++ b/alerts/apps.py @@ -1,6 +1,6 @@ from django.apps import AppConfig -class ScreensConfig(AppConfig): +class AlertsConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "screens" + name = "alerts" diff --git a/screens/consumers.py b/alerts/consumers.py similarity index 100% rename from screens/consumers.py rename to alerts/consumers.py diff --git a/screens/models.py b/alerts/models.py similarity index 100% rename from screens/models.py rename to alerts/models.py diff --git a/screens/multicast.py b/alerts/multicast.py similarity index 100% rename from screens/multicast.py rename to alerts/multicast.py diff --git a/screens/routing.py b/alerts/routing.py similarity index 100% rename from screens/routing.py rename to alerts/routing.py diff --git a/screens/tests.py b/alerts/tests.py similarity index 100% rename from screens/tests.py rename to alerts/tests.py diff --git a/screens/urls.py b/alerts/urls.py similarity index 100% rename from screens/urls.py rename to alerts/urls.py diff --git a/screens/views.py b/alerts/views.py similarity index 100% rename from screens/views.py rename to alerts/views.py diff --git a/datahub/asgi.py b/datahub/asgi.py index 9eabacc..b0751d6 100644 --- a/datahub/asgi.py +++ b/datahub/asgi.py @@ -12,7 +12,7 @@ from channels.routing import ProtocolTypeRouter, URLRouter from django.core.asgi import get_asgi_application -from screens.routing import websocket_urlpatterns +from alerts.routing import websocket_urlpatterns os.environ.setdefault("DJANGO_SETTINGS_MODULE", "datahub.settings") diff --git a/datahub/settings.py b/datahub/settings.py index f2bec79..1e3348a 100644 --- a/datahub/settings.py +++ b/datahub/settings.py @@ -38,7 +38,7 @@ "website.apps.WebsiteConfig", "gtfs.apps.GtfsConfig", "realtime.apps.RealtimeConfig", - "screens.apps.ScreensConfig", + "alerts.apps.AlertsConfig", "django_celery_results", "django_celery_beat", "django.contrib.admin", diff --git a/datahub/urls.py b/datahub/urls.py index f5cba79..fda0efc 100644 --- a/datahub/urls.py +++ b/datahub/urls.py @@ -22,5 +22,5 @@ path("", include("website.urls"), name="index"), path("gtfs/", include("gtfs.urls"), name="gtfs_page"), path("realtime/", include("realtime.urls"), name="realtime_page"), - path("pantallas/", include("screens.urls"), name="screens_page"), + path("pantallas/", include("alerts.urls"), name="alerts_page"), ] diff --git a/docs/architecture.md b/docs/architecture.md index 2575da2..89dd9ac 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -22,7 +22,7 @@ - `class User`: Información de usuarios del sistema - `type` -### Django app: `screens` +### Django app: `alerts` > Páginas de administración de las pantallas (HTML) y actualización de datos en tiempo real (WebSockets) diff --git a/docs/index.md b/docs/index.md index af5daac..e0a7c51 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,5 +1,7 @@ # Sistema de pantallas en tiempo real del bus interno de la UCR +`datahub` ofrece un API y un sistema de alertas con un modelo de comunicación publicación/suscripción. + La información en tiempo real sobre el transporte público es uno de los servicios mejor apreciados por las personas usuarias. ## Descripción diff --git a/gtfs/models.py b/gtfs/models.py index 297dae6..497180b 100644 --- a/gtfs/models.py +++ b/gtfs/models.py @@ -3,7 +3,7 @@ from django.db.models.signals import post_save -from screens.multicast import test_signal +from alerts.multicast import test_signal class Company(models.Model): From 928d8e664b5ef5bc18b265e1047ad34dccc98d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Tue, 23 Apr 2024 10:12:46 -0300 Subject: [PATCH 056/244] Agregar a los requisitos: psycopg2, sqlalchemy, gunicorn --- requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements.txt b/requirements.txt index 9bd3b53..066e071 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,6 @@ schedule pandas gtfs-realtime-bindings python-decouple +psycopg2-binary +sqlalchemy +gunicorn From d0a06dda484ced125f4768e075b97bfa5970a063 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Tue, 23 Apr 2024 10:13:00 -0300 Subject: [PATCH 057/244] =?UTF-8?q?P=C3=A1gina=20de=20demostraci=C3=B3n=20?= =?UTF-8?q?sencilla?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/templates/index.html | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/website/templates/index.html b/website/templates/index.html index 642cab9..4490e6a 100644 --- a/website/templates/index.html +++ b/website/templates/index.html @@ -1,4 +1,27 @@ -

datahub

-

- Aquí habrá una referencia a la página de inicio de sesión y a la página de registro. -

\ No newline at end of file +{% load static %} + + + + + + + + bUCR | Servidor de datos + + + + + +
+
+ +

bUCR

+

Servidor de datos

+

Concentrador de datos GTFS Realtime y otros para la conexión con servicios como páginas web, aplicaciones, pantallas, análisis de datos y otros.

+
+
+ + + + + \ No newline at end of file From e3afe851e47d12d342443d25b93cff06896d7b6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Wed, 24 Apr 2024 11:01:57 -0300 Subject: [PATCH 058/244] =?UTF-8?q?Primeros=20ajustes=20a=20modelos=20para?= =?UTF-8?q?=20facilitar=20la=20importaci=C3=B3n=20de=20GTFS=20Schedule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gtfs/models.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/gtfs/models.py b/gtfs/models.py index 22c14c3..1dca601 100644 --- a/gtfs/models.py +++ b/gtfs/models.py @@ -183,9 +183,7 @@ class Route(models.Model): route_id = models.CharField( max_length=255, help_text="Identificador único de la ruta." ) - agency_id = models.ForeignKey( - Agency, blank=True, null=True, on_delete=models.SET_NULL - ) + agency_id = models.CharField(max_length=200) route_short_name = models.CharField( max_length=63, blank=True, null=True, help_text="Nombre corto de la ruta." ) @@ -330,8 +328,8 @@ class Trip(models.Model): id = models.BigAutoField(primary_key=True) feed = models.ForeignKey(Feed, on_delete=models.CASCADE) - route_id = models.ForeignKey(Route, on_delete=models.CASCADE) - service_id = models.ForeignKey(Calendar, on_delete=models.CASCADE) + route_id = models.CharField(max_length=200) + service_id = models.CharField(max_length=200) trip_id = models.CharField( max_length=255, help_text="Identificador único del viaje." ) @@ -349,9 +347,7 @@ class Trip(models.Model): max_length=255, blank=True, null=True, help_text="Identificador del bloque." ) shape_id = models.CharField(max_length=255, blank=True, null=True) - geoshape_id = models.ForeignKey( - GeoShape, blank=True, null=True, on_delete=models.SET_NULL - ) + geoshape_id = models.CharField(max_length=200) wheelchair_accessible = models.PositiveSmallIntegerField( choices=((0, "No especificado"), (1, "Accesible"), (2, "No accesible")), help_text="¿Tiene acceso para sillas de ruedas?", @@ -372,14 +368,14 @@ class StopTime(models.Model): id = models.BigAutoField(primary_key=True) feed = models.ForeignKey(Feed, on_delete=models.CASCADE) - trip_id = models.ForeignKey(Trip, on_delete=models.CASCADE) + trip_id = models.CharField(max_length=200) arrival_time = models.TimeField( help_text="Hora de llegada a la parada.", blank=True, null=True ) departure_time = models.TimeField( help_text="Hora de salida de la parada.", blank=True, null=True ) - stop_id = models.ForeignKey(Stop, on_delete=models.CASCADE) + stop_id = models.CharField(max_length=200) stop_sequence = models.PositiveIntegerField( help_text="Secuencia de la parada en el viaje." ) @@ -484,8 +480,8 @@ class FareRule(models.Model): id = models.BigAutoField(primary_key=True) feed = models.ForeignKey(Feed, on_delete=models.CASCADE) - fare_id = models.ForeignKey(FareAttribute, on_delete=models.CASCADE) - route_id = models.ForeignKey(Route, on_delete=models.CASCADE) + fare_id = models.CharField(max_length=200) + route_id = models.CharField(max_length=200) origin_id = models.CharField(max_length=255, blank=True, null=True) destination_id = models.CharField(max_length=255, blank=True, null=True) contains_id = models.CharField(max_length=255, blank=True, null=True) From ac49c79e9fdd9880e8579fb059efae25368e2b21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Wed, 24 Apr 2024 11:05:29 -0300 Subject: [PATCH 059/244] =?UTF-8?q?Cambiar=20configuraci=C3=B3n=20de=20car?= =?UTF-8?q?peta=20de=20archivos=20est=C3=A1ticos=20y=20agregar=20imagen=20?= =?UTF-8?q?a=20p=C3=A1gina=20de=20bienvenida?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- datahub/settings.py | 2 ++ website/templates/index.html | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/datahub/settings.py b/datahub/settings.py index 1e3348a..d103890 100644 --- a/datahub/settings.py +++ b/datahub/settings.py @@ -13,6 +13,7 @@ from pathlib import Path from decouple import config, Csv import platform +import os # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -155,6 +156,7 @@ # https://docs.djangoproject.com/en/5.0/howto/static-files/ STATIC_URL = "static/" +STATIC_ROOT = os.path.join(BASE_DIR, "static") # Default primary key field type # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field diff --git a/website/templates/index.html b/website/templates/index.html index 4490e6a..9acb0e2 100644 --- a/website/templates/index.html +++ b/website/templates/index.html @@ -14,10 +14,10 @@
- -

bUCR

+ Símbolo servidor en tiempo real

Servidor de datos

Concentrador de datos GTFS Realtime y otros para la conexión con servicios como páginas web, aplicaciones, pantallas, análisis de datos y otros.

+ En desarrollo
From 8bb772c1dd32f1c1e7868e718002df8e412526f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Wed, 24 Apr 2024 11:09:14 -0300 Subject: [PATCH 060/244] Crear carpeta static y agregar imagen de logo --- static/img/b_azul_fondo_blanco.png | Bin 0 -> 115575 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 static/img/b_azul_fondo_blanco.png diff --git a/static/img/b_azul_fondo_blanco.png b/static/img/b_azul_fondo_blanco.png new file mode 100644 index 0000000000000000000000000000000000000000..a1461bc6005e7c032d95759fac366ccf8a22d2bc GIT binary patch literal 115575 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%0A45UUpo#H4excsxT7tX3JGh_OWYl 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 C!D>qY literal 0 HcmV?d00001 From 4d836cfe169a7dcea341ed30eb1c08ba8c12aa91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Wed, 24 Apr 2024 18:56:39 -0300 Subject: [PATCH 061/244] Agregar favicon --- static/favicon.ico | Bin 0 -> 15406 bytes website/templates/index.html | 1 + 2 files changed, 1 insertion(+) create mode 100644 static/favicon.ico diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..307413ba139239beee1c9ed5fe67557cb313d5d4 GIT binary patch literal 15406 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 GYv5lib%|*J literal 0 HcmV?d00001 diff --git a/website/templates/index.html b/website/templates/index.html index 9acb0e2..3a68c49 100644 --- a/website/templates/index.html +++ b/website/templates/index.html @@ -7,6 +7,7 @@ bUCR | Servidor de datos + From 0251a04fd3125ef69ad25d3ebf99645c2d77afa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Wed, 24 Apr 2024 19:03:32 -0300 Subject: [PATCH 062/244] Nuevo favicon --- static/favicon.ico | Bin 15406 -> 15406 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/static/favicon.ico b/static/favicon.ico index 307413ba139239beee1c9ed5fe67557cb313d5d4..3c898741c203597d723e8458ba042e8fcdc4187a 100644 GIT binary patch literal 15406 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 literal 15406 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 GYv5lib%|*J From 61ca8e5d80e495a458213675b7a20b0a29bd2706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Thu, 25 Apr 2024 10:02:58 -0300 Subject: [PATCH 063/244] =?UTF-8?q?Creaci=C3=B3n=20de=20app=20nueva=20api?= =?UTF-8?q?=20y=20cambiar=20nombre=20de=20app=20realtime=20a=20feed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- {realtime => api}/__init__.py | 0 api/admin.py | 3 +++ {realtime => api}/apps.py | 4 ++-- {realtime => api}/models.py | 0 realtime/templates/realtime.html => api/serializers.py | 0 {realtime => api}/tests.py | 0 api/views.py | 3 +++ datahub/settings.py | 3 ++- datahub/urls.py | 2 +- feed/__init__.py | 0 {realtime => feed}/admin.py | 0 feed/apps.py | 6 ++++++ feed/models.py | 3 +++ feed/templates/feed.html | 1 + feed/tests.py | 3 +++ {realtime => feed}/urls.py | 2 +- feed/views.py | 7 +++++++ realtime/views.py | 7 ------- requirements.txt | 2 ++ 19 files changed, 34 insertions(+), 12 deletions(-) rename {realtime => api}/__init__.py (100%) create mode 100644 api/admin.py rename {realtime => api}/apps.py (62%) rename {realtime => api}/models.py (100%) rename realtime/templates/realtime.html => api/serializers.py (100%) rename {realtime => api}/tests.py (100%) create mode 100644 api/views.py create mode 100644 feed/__init__.py rename {realtime => feed}/admin.py (100%) create mode 100644 feed/apps.py create mode 100644 feed/models.py create mode 100644 feed/templates/feed.html create mode 100644 feed/tests.py rename {realtime => feed}/urls.py (69%) create mode 100644 feed/views.py delete mode 100644 realtime/views.py diff --git a/realtime/__init__.py b/api/__init__.py similarity index 100% rename from realtime/__init__.py rename to api/__init__.py diff --git a/api/admin.py b/api/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/api/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/realtime/apps.py b/api/apps.py similarity index 62% rename from realtime/apps.py rename to api/apps.py index 643c3f9..878e7d5 100644 --- a/realtime/apps.py +++ b/api/apps.py @@ -1,6 +1,6 @@ from django.apps import AppConfig -class RealtimeConfig(AppConfig): +class ApiConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "realtime" + name = "api" diff --git a/realtime/models.py b/api/models.py similarity index 100% rename from realtime/models.py rename to api/models.py diff --git a/realtime/templates/realtime.html b/api/serializers.py similarity index 100% rename from realtime/templates/realtime.html rename to api/serializers.py diff --git a/realtime/tests.py b/api/tests.py similarity index 100% rename from realtime/tests.py rename to api/tests.py diff --git a/api/views.py b/api/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/api/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/datahub/settings.py b/datahub/settings.py index d103890..2d32cac 100644 --- a/datahub/settings.py +++ b/datahub/settings.py @@ -38,8 +38,9 @@ "channels", "website.apps.WebsiteConfig", "gtfs.apps.GtfsConfig", - "realtime.apps.RealtimeConfig", + "feed.apps.FeedConfig", "alerts.apps.AlertsConfig", + "api.apps.ApiConfig", "django_celery_results", "django_celery_beat", "django.contrib.admin", diff --git a/datahub/urls.py b/datahub/urls.py index fda0efc..409b745 100644 --- a/datahub/urls.py +++ b/datahub/urls.py @@ -21,6 +21,6 @@ path("admin/", admin.site.urls), path("", include("website.urls"), name="index"), path("gtfs/", include("gtfs.urls"), name="gtfs_page"), - path("realtime/", include("realtime.urls"), name="realtime_page"), + path("feed/", include("feed.urls"), name="feed_page"), path("pantallas/", include("alerts.urls"), name="alerts_page"), ] diff --git a/feed/__init__.py b/feed/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/realtime/admin.py b/feed/admin.py similarity index 100% rename from realtime/admin.py rename to feed/admin.py diff --git a/feed/apps.py b/feed/apps.py new file mode 100644 index 0000000..3ff3cae --- /dev/null +++ b/feed/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class FeedConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "feed" diff --git a/feed/models.py b/feed/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/feed/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/feed/templates/feed.html b/feed/templates/feed.html new file mode 100644 index 0000000..6153158 --- /dev/null +++ b/feed/templates/feed.html @@ -0,0 +1 @@ +Feed \ No newline at end of file diff --git a/feed/tests.py b/feed/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/feed/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/realtime/urls.py b/feed/urls.py similarity index 69% rename from realtime/urls.py rename to feed/urls.py index e0e8e65..44ef99c 100644 --- a/realtime/urls.py +++ b/feed/urls.py @@ -3,5 +3,5 @@ from . import views urlpatterns = [ - path("", views.realtime), + path("", views.feed), ] \ No newline at end of file diff --git a/feed/views.py b/feed/views.py new file mode 100644 index 0000000..cf8b731 --- /dev/null +++ b/feed/views.py @@ -0,0 +1,7 @@ +from django.shortcuts import render + +# Create your views here. + + +def feed(request): + return render(request, "feed.html") diff --git a/realtime/views.py b/realtime/views.py deleted file mode 100644 index eb5b6bf..0000000 --- a/realtime/views.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.shortcuts import render - -# Create your views here. - - -def realtime(request): - return render(request, "realtime.html") diff --git a/requirements.txt b/requirements.txt index 066e071..0df9e1a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,8 @@ channels-redis celery[redis] django-celery-results django-celery-beat +djangorestframework +drf-spectacular requests configparser schedule From b62de32e1132c340a7f6e5b96c4e74d2689fbce3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Abarca?= Date: Thu, 25 Apr 2024 10:52:41 -0300 Subject: [PATCH 064/244] =?UTF-8?q?Configurar=20API=20con=20autorizaci?= =?UTF-8?q?=C3=B3n=20por=20token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/datahub.yml | 151 + api/serializers.py | 8 + api/urls.py | 17 + api/views.py | 25 +- datahub/settings.py | 11 + datahub/urls.py | 1 + feed/models.py | 10 + static/admin/css/autocomplete.css | 275 + static/admin/css/base.css | 1145 ++ static/admin/css/changelists.css | 328 + static/admin/css/dark_mode.css | 137 + static/admin/css/dashboard.css | 29 + static/admin/css/forms.css | 534 + static/admin/css/login.css | 61 + static/admin/css/nav_sidebar.css | 144 + static/admin/css/responsive.css | 999 ++ static/admin/css/responsive_rtl.css | 84 + static/admin/css/rtl.css | 298 + .../css/vendor/select2/LICENSE-SELECT2.md | 21 + static/admin/css/vendor/select2/select2.css | 481 + .../admin/css/vendor/select2/select2.min.css | 1 + static/admin/css/widgets.css | 604 + static/admin/img/LICENSE | 20 + static/admin/img/README.txt | 7 + static/admin/img/calendar-icons.svg | 14 + static/admin/img/gis/move_vertex_off.svg | 1 + static/admin/img/gis/move_vertex_on.svg | 1 + static/admin/img/icon-addlink.svg | 3 + static/admin/img/icon-alert.svg | 3 + static/admin/img/icon-calendar.svg | 9 + static/admin/img/icon-changelink.svg | 3 + static/admin/img/icon-clock.svg | 9 + static/admin/img/icon-deletelink.svg | 3 + static/admin/img/icon-no.svg | 3 + static/admin/img/icon-unknown-alt.svg | 3 + static/admin/img/icon-unknown.svg | 3 + static/admin/img/icon-viewlink.svg | 3 + static/admin/img/icon-yes.svg | 3 + static/admin/img/inline-delete.svg | 3 + static/admin/img/search.svg | 3 + static/admin/img/selector-icons.svg | 34 + static/admin/img/sorting-icons.svg | 19 + static/admin/img/tooltag-add.svg | 3 + static/admin/img/tooltag-arrowright.svg | 3 + static/admin/js/SelectBox.js | 116 + static/admin/js/SelectFilter2.js | 283 + static/admin/js/actions.js | 201 + static/admin/js/admin/DateTimeShortcuts.js | 408 + static/admin/js/admin/RelatedObjectLookups.js | 238 + static/admin/js/autocomplete.js | 33 + static/admin/js/calendar.js | 221 + static/admin/js/cancel.js | 29 + static/admin/js/change_form.js | 16 + static/admin/js/collapse.js | 43 + static/admin/js/core.js | 170 + static/admin/js/filters.js | 30 + static/admin/js/inlines.js | 359 + static/admin/js/jquery.init.js | 8 + static/admin/js/nav_sidebar.js | 79 + static/admin/js/popup_response.js | 16 + static/admin/js/prepopulate.js | 43 + static/admin/js/prepopulate_init.js | 15 + static/admin/js/theme.js | 56 + static/admin/js/urlify.js | 169 + static/admin/js/vendor/jquery/LICENSE.txt | 20 + static/admin/js/vendor/jquery/jquery.js | 10965 ++++++++++++++++ static/admin/js/vendor/jquery/jquery.min.js | 2 + static/admin/js/vendor/select2/LICENSE.md | 21 + static/admin/js/vendor/select2/i18n/af.js | 3 + static/admin/js/vendor/select2/i18n/ar.js | 3 + static/admin/js/vendor/select2/i18n/az.js | 3 + static/admin/js/vendor/select2/i18n/bg.js | 3 + static/admin/js/vendor/select2/i18n/bn.js | 3 + static/admin/js/vendor/select2/i18n/bs.js | 3 + static/admin/js/vendor/select2/i18n/ca.js | 3 + static/admin/js/vendor/select2/i18n/cs.js | 3 + static/admin/js/vendor/select2/i18n/da.js | 3 + static/admin/js/vendor/select2/i18n/de.js | 3 + static/admin/js/vendor/select2/i18n/dsb.js | 3 + static/admin/js/vendor/select2/i18n/el.js | 3 + static/admin/js/vendor/select2/i18n/en.js | 3 + static/admin/js/vendor/select2/i18n/es.js | 3 + static/admin/js/vendor/select2/i18n/et.js | 3 + static/admin/js/vendor/select2/i18n/eu.js | 3 + static/admin/js/vendor/select2/i18n/fa.js | 3 + static/admin/js/vendor/select2/i18n/fi.js | 3 + static/admin/js/vendor/select2/i18n/fr.js | 3 + static/admin/js/vendor/select2/i18n/gl.js | 3 + static/admin/js/vendor/select2/i18n/he.js | 3 + static/admin/js/vendor/select2/i18n/hi.js | 3 + static/admin/js/vendor/select2/i18n/hr.js | 3 + static/admin/js/vendor/select2/i18n/hsb.js | 3 + static/admin/js/vendor/select2/i18n/hu.js | 3 + static/admin/js/vendor/select2/i18n/hy.js | 3 + static/admin/js/vendor/select2/i18n/id.js | 3 + static/admin/js/vendor/select2/i18n/is.js | 3 + static/admin/js/vendor/select2/i18n/it.js | 3 + static/admin/js/vendor/select2/i18n/ja.js | 3 + static/admin/js/vendor/select2/i18n/ka.js | 3 + static/admin/js/vendor/select2/i18n/km.js | 3 + static/admin/js/vendor/select2/i18n/ko.js | 3 + static/admin/js/vendor/select2/i18n/lt.js | 3 + static/admin/js/vendor/select2/i18n/lv.js | 3 + static/admin/js/vendor/select2/i18n/mk.js | 3 + static/admin/js/vendor/select2/i18n/ms.js | 3 + static/admin/js/vendor/select2/i18n/nb.js | 3 + static/admin/js/vendor/select2/i18n/ne.js | 3 + static/admin/js/vendor/select2/i18n/nl.js | 3 + static/admin/js/vendor/select2/i18n/pl.js | 3 + static/admin/js/vendor/select2/i18n/ps.js | 3 + static/admin/js/vendor/select2/i18n/pt-BR.js | 3 + static/admin/js/vendor/select2/i18n/pt.js | 3 + static/admin/js/vendor/select2/i18n/ro.js | 3 + static/admin/js/vendor/select2/i18n/ru.js | 3 + static/admin/js/vendor/select2/i18n/sk.js | 3 + static/admin/js/vendor/select2/i18n/sl.js | 3 + static/admin/js/vendor/select2/i18n/sq.js | 3 + .../admin/js/vendor/select2/i18n/sr-Cyrl.js | 3 + static/admin/js/vendor/select2/i18n/sr.js | 3 + static/admin/js/vendor/select2/i18n/sv.js | 3 + static/admin/js/vendor/select2/i18n/th.js | 3 + static/admin/js/vendor/select2/i18n/tk.js | 3 + static/admin/js/vendor/select2/i18n/tr.js | 3 + static/admin/js/vendor/select2/i18n/uk.js | 3 + static/admin/js/vendor/select2/i18n/vi.js | 3 + static/admin/js/vendor/select2/i18n/zh-CN.js | 3 + static/admin/js/vendor/select2/i18n/zh-TW.js | 3 + .../admin/js/vendor/select2/select2.full.js | 6820 ++++++++++ .../js/vendor/select2/select2.full.min.js | 2 + static/admin/js/vendor/xregexp/LICENSE.txt | 21 + static/admin/js/vendor/xregexp/xregexp.js | 4652 +++++++ static/admin/js/vendor/xregexp/xregexp.min.js | 160 + static/gis/css/ol3.css | 39 + static/gis/img/draw_line_off.svg | 1 + static/gis/img/draw_line_on.svg | 1 + static/gis/img/draw_point_off.svg | 1 + static/gis/img/draw_point_on.svg | 1 + static/gis/img/draw_polygon_off.svg | 1 + static/gis/img/draw_polygon_on.svg | 1 + static/gis/js/OLMapWidget.js | 238 + 140 files changed, 31171 insertions(+), 2 deletions(-) create mode 100644 api/datahub.yml create mode 100644 api/urls.py create mode 100644 static/admin/css/autocomplete.css create mode 100644 static/admin/css/base.css create mode 100644 static/admin/css/changelists.css create mode 100644 static/admin/css/dark_mode.css create mode 100644 static/admin/css/dashboard.css create mode 100644 static/admin/css/forms.css create mode 100644 static/admin/css/login.css create mode 100644 static/admin/css/nav_sidebar.css create mode 100644 static/admin/css/responsive.css create mode 100644 static/admin/css/responsive_rtl.css create mode 100644 static/admin/css/rtl.css create mode 100644 static/admin/css/vendor/select2/LICENSE-SELECT2.md create mode 100644 static/admin/css/vendor/select2/select2.css create mode 100644 static/admin/css/vendor/select2/select2.min.css create mode 100644 static/admin/css/widgets.css create mode 100644 static/admin/img/LICENSE create mode 100644 static/admin/img/README.txt create mode 100644 static/admin/img/calendar-icons.svg create mode 100644 static/admin/img/gis/move_vertex_off.svg create mode 100644 static/admin/img/gis/move_vertex_on.svg create mode 100644 static/admin/img/icon-addlink.svg create mode 100644 static/admin/img/icon-alert.svg create mode 100644 static/admin/img/icon-calendar.svg create mode 100644 static/admin/img/icon-changelink.svg create mode 100644 static/admin/img/icon-clock.svg create mode 100644 static/admin/img/icon-deletelink.svg create mode 100644 static/admin/img/icon-no.svg create mode 100644 static/admin/img/icon-unknown-alt.svg create mode 100644 static/admin/img/icon-unknown.svg create mode 100644 static/admin/img/icon-viewlink.svg create mode 100644 static/admin/img/icon-yes.svg create mode 100644 static/admin/img/inline-delete.svg create mode 100644 static/admin/img/search.svg create mode 100644 static/admin/img/selector-icons.svg create mode 100644 static/admin/img/sorting-icons.svg create mode 100644 static/admin/img/tooltag-add.svg create mode 100644 static/admin/img/tooltag-arrowright.svg create mode 100644 static/admin/js/SelectBox.js create mode 100644 static/admin/js/SelectFilter2.js create mode 100644 static/admin/js/actions.js create mode 100644 static/admin/js/admin/DateTimeShortcuts.js create mode 100644 static/admin/js/admin/RelatedObjectLookups.js create mode 100644 static/admin/js/autocomplete.js create mode 100644 static/admin/js/calendar.js create mode 100644 static/admin/js/cancel.js create mode 100644 static/admin/js/change_form.js create mode 100644 static/admin/js/collapse.js create mode 100644 static/admin/js/core.js create mode 100644 static/admin/js/filters.js create mode 100644 static/admin/js/inlines.js create mode 100644 static/admin/js/jquery.init.js create mode 100644 static/admin/js/nav_sidebar.js create mode 100644 static/admin/js/popup_response.js create mode 100644 static/admin/js/prepopulate.js create mode 100644 static/admin/js/prepopulate_init.js create mode 100644 static/admin/js/theme.js create mode 100644 static/admin/js/urlify.js create mode 100644 static/admin/js/vendor/jquery/LICENSE.txt create mode 100644 static/admin/js/vendor/jquery/jquery.js create mode 100644 static/admin/js/vendor/jquery/jquery.min.js create mode 100644 static/admin/js/vendor/select2/LICENSE.md create mode 100644 static/admin/js/vendor/select2/i18n/af.js create mode 100644 static/admin/js/vendor/select2/i18n/ar.js create mode 100644 static/admin/js/vendor/select2/i18n/az.js create mode 100644 static/admin/js/vendor/select2/i18n/bg.js create mode 100644 static/admin/js/vendor/select2/i18n/bn.js create mode 100644 static/admin/js/vendor/select2/i18n/bs.js create mode 100644 static/admin/js/vendor/select2/i18n/ca.js create mode 100644 static/admin/js/vendor/select2/i18n/cs.js create mode 100644 static/admin/js/vendor/select2/i18n/da.js create mode 100644 static/admin/js/vendor/select2/i18n/de.js create mode 100644 static/admin/js/vendor/select2/i18n/dsb.js create mode 100644 static/admin/js/vendor/select2/i18n/el.js create mode 100644 static/admin/js/vendor/select2/i18n/en.js create mode 100644 static/admin/js/vendor/select2/i18n/es.js create mode 100644 static/admin/js/vendor/select2/i18n/et.js create mode 100644 static/admin/js/vendor/select2/i18n/eu.js create mode 100644 static/admin/js/vendor/select2/i18n/fa.js create mode 100644 static/admin/js/vendor/select2/i18n/fi.js create mode 100644 static/admin/js/vendor/select2/i18n/fr.js create mode 100644 static/admin/js/vendor/select2/i18n/gl.js create mode 100644 static/admin/js/vendor/select2/i18n/he.js create mode 100644 static/admin/js/vendor/select2/i18n/hi.js create mode 100644 static/admin/js/vendor/select2/i18n/hr.js create mode 100644 static/admin/js/vendor/select2/i18n/hsb.js create mode 100644 static/admin/js/vendor/select2/i18n/hu.js create mode 100644 static/admin/js/vendor/select2/i18n/hy.js create mode 100644 static/admin/js/vendor/select2/i18n/id.js create mode 100644 static/admin/js/vendor/select2/i18n/is.js create mode 100644 static/admin/js/vendor/select2/i18n/it.js create mode 100644 static/admin/js/vendor/select2/i18n/ja.js create mode 100644 static/admin/js/vendor/select2/i18n/ka.js create mode 100644 static/admin/js/vendor/select2/i18n/km.js create mode 100644 static/admin/js/vendor/select2/i18n/ko.js create mode 100644 static/admin/js/vendor/select2/i18n/lt.js create mode 100644 static/admin/js/vendor/select2/i18n/lv.js create mode 100644 static/admin/js/vendor/select2/i18n/mk.js create mode 100644 static/admin/js/vendor/select2/i18n/ms.js create mode 100644 static/admin/js/vendor/select2/i18n/nb.js create mode 100644 static/admin/js/vendor/select2/i18n/ne.js create mode 100644 static/admin/js/vendor/select2/i18n/nl.js create mode 100644 static/admin/js/vendor/select2/i18n/pl.js create mode 100644 static/admin/js/vendor/select2/i18n/ps.js create mode 100644 static/admin/js/vendor/select2/i18n/pt-BR.js create mode 100644 static/admin/js/vendor/select2/i18n/pt.js create mode 100644 static/admin/js/vendor/select2/i18n/ro.js create mode 100644 static/admin/js/vendor/select2/i18n/ru.js create mode 100644 static/admin/js/vendor/select2/i18n/sk.js create mode 100644 static/admin/js/vendor/select2/i18n/sl.js create mode 100644 static/admin/js/vendor/select2/i18n/sq.js create mode 100644 static/admin/js/vendor/select2/i18n/sr-Cyrl.js create mode 100644 static/admin/js/vendor/select2/i18n/sr.js create mode 100644 static/admin/js/vendor/select2/i18n/sv.js create mode 100644 static/admin/js/vendor/select2/i18n/th.js create mode 100644 static/admin/js/vendor/select2/i18n/tk.js create mode 100644 static/admin/js/vendor/select2/i18n/tr.js create mode 100644 static/admin/js/vendor/select2/i18n/uk.js create mode 100644 static/admin/js/vendor/select2/i18n/vi.js create mode 100644 static/admin/js/vendor/select2/i18n/zh-CN.js create mode 100644 static/admin/js/vendor/select2/i18n/zh-TW.js create mode 100644 static/admin/js/vendor/select2/select2.full.js create mode 100644 static/admin/js/vendor/select2/select2.full.min.js create mode 100644 static/admin/js/vendor/xregexp/LICENSE.txt create mode 100644 static/admin/js/vendor/xregexp/xregexp.js create mode 100644 static/admin/js/vendor/xregexp/xregexp.min.js create mode 100644 static/gis/css/ol3.css create mode 100644 static/gis/img/draw_line_off.svg create mode 100644 static/gis/img/draw_line_on.svg create mode 100644 static/gis/img/draw_point_off.svg create mode 100644 static/gis/img/draw_point_on.svg create mode 100644 static/gis/img/draw_polygon_off.svg create mode 100644 static/gis/img/draw_polygon_on.svg create mode 100644 static/gis/js/OLMapWidget.js diff --git a/api/datahub.yml b/api/datahub.yml new file mode 100644 index 0000000..d1eb08b --- /dev/null +++ b/api/datahub.yml @@ -0,0 +1,151 @@ +openapi: '3.0.2' +info: + title: Servidor de datos + description: Un servidor para suplir datos a aplicaciones. + version: '1.0' + contact: + email: tropicalizacion@ucr.ac.cr + x-logo: + url: https://fabianabarca.github.io/senaletica/assets/logos/b_azul_fondo_blanco.png + altText: Logo bUCR + license: + name: MIT + url: 'https://opensource.org/license/mit/' +servers: + - url: https://realtime.bucr.digital/api +paths: + /vehicle-positions: + get: + description: GTFS Realtime VehiclePositions + tags: + - GTFS + responses: + '200': + description: OK + /trip-updates: + get: + description: GTFS Realtime TripUpdates + tags: + - GTFS + responses: + '200': + description: OK + /service-alerts: + get: + description: GTFS Realtime Alerts + tags: + - GTFS + responses: + '200': + description: OK + /equipment: + get: + tags: + - Vehicle + responses: + '200': + description: OK + /alarms: + get: + tags: + - Alarms + responses: + '200': + description: OK + /authorizations: + get: + tags: + - Authorizations + responses: + '200': + description: OK + /fares: + get: + tags: + - Users + responses: + '200': + description: OK + /transfers: + get: + tags: + - Users + responses: + '200': + description: OK + /travelers: + get: + tags: + - Users + responses: + '200': + description: OK + /users: + get: + tags: + - Users + responses: + '200': + description: OK + /conditions: + get: + tags: + - Vehicle + responses: + '200': + description: OK + /emissions: + get: + tags: + - Vehicle + responses: + '200': + description: OK + /loading: + get: + tags: + - Trip + responses: + '200': + description: OK + /location: + post: + tags: + - Trip + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Location' + responses: + '200': + description: OK + /schedule: + get: + tags: + - Trip + responses: + '200': + description: OK + /operator: + get: + tags: + - Trip + responses: + '200': + description: OK +components: + schemas: + Location: + type: object + properties: + latitude: + type: number + format: float + longitude: + type: number + format: float + altitude: + type: number + format: float diff --git a/api/serializers.py b/api/serializers.py index e69de29..5fbcd2d 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -0,0 +1,8 @@ +from feed.models import Application +from rest_framework import serializers + + +class ApplicationSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Application + fields = ["url", "name", "description", "created_at", "updated_at"] diff --git a/api/urls.py b/api/urls.py new file mode 100644 index 0000000..7636c5d --- /dev/null +++ b/api/urls.py @@ -0,0 +1,17 @@ +from django.urls import include, path +from rest_framework import routers +from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView + +from . import views + +router = routers.DefaultRouter() +router.register(r"application", views.ApplicationViewSet) + +# Wire up our API using automatic URL routing. +# Additionally, we include login URLs for the browsable API. +urlpatterns = [ + path("", include(router.urls)), + path("api-auth/", include("rest_framework.urls", namespace="rest_framework")), + path("docs/schema/", views.get_schema, name="schema"), + path("docs/", SpectacularRedocView.as_view(url_name="schema"), name="api_docs"), +] diff --git a/api/views.py b/api/views.py index 91ea44a..f84b0e0 100644 --- a/api/views.py +++ b/api/views.py @@ -1,3 +1,24 @@ -from django.shortcuts import render +from django.conf import settings +from django.http import FileResponse +from feed.models import Application +from rest_framework import viewsets, permissions, authentication -# Create your views here. +from .serializers import ApplicationSerializer + + +class ApplicationViewSet(viewsets.ModelViewSet): + """ + Aplicaciones conectadas al servidor de datos. + """ + + queryset = Application.objects.all().order_by("created_at") + serializer_class = ApplicationSerializer + permission_classes = [permissions.IsAuthenticated] + authentication_classes = [authentication.TokenAuthentication] + + +def get_schema(request): + file_path = settings.BASE_DIR / "api" / "datahub.yml" + return FileResponse( + open(file_path, "rb"), as_attachment=True, filename="datahub.yml" + ) diff --git a/datahub/settings.py b/datahub/settings.py index 2d32cac..7ca8fc2 100644 --- a/datahub/settings.py +++ b/datahub/settings.py @@ -41,6 +41,9 @@ "feed.apps.FeedConfig", "alerts.apps.AlertsConfig", "api.apps.ApiConfig", + "rest_framework", + "rest_framework.authtoken", + "drf_spectacular", "django_celery_results", "django_celery_beat", "django.contrib.admin", @@ -141,6 +144,14 @@ }, } +# REST Framework settings + +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": [ + "rest_framework.authentication.TokenAuthentication", + ], +} + # Internationalization # https://docs.djangoproject.com/en/5.0/topics/i18n/ diff --git a/datahub/urls.py b/datahub/urls.py index 409b745..4490549 100644 --- a/datahub/urls.py +++ b/datahub/urls.py @@ -20,6 +20,7 @@ urlpatterns = [ path("admin/", admin.site.urls), path("", include("website.urls"), name="index"), + path("api/", include("api.urls"), name="api"), path("gtfs/", include("gtfs.urls"), name="gtfs_page"), path("feed/", include("feed.urls"), name="feed_page"), path("pantallas/", include("alerts.urls"), name="alerts_page"), diff --git a/feed/models.py b/feed/models.py index 71a8362..b976205 100644 --- a/feed/models.py +++ b/feed/models.py @@ -1,3 +1,13 @@ from django.db import models # Create your models here. + + +class Application(models.Model): + name = models.CharField(max_length=100) + description = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.name diff --git a/static/admin/css/autocomplete.css b/static/admin/css/autocomplete.css new file mode 100644 index 0000000..69c94e7 --- /dev/null +++ b/static/admin/css/autocomplete.css @@ -0,0 +1,275 @@ +select.admin-autocomplete { + width: 20em; +} + +.select2-container--admin-autocomplete.select2-container { + min-height: 30px; +} + +.select2-container--admin-autocomplete .select2-selection--single, +.select2-container--admin-autocomplete .select2-selection--multiple { + min-height: 30px; + padding: 0; +} + +.select2-container--admin-autocomplete.select2-container--focus .select2-selection, +.select2-container--admin-autocomplete.select2-container--open .select2-selection { + border-color: var(--body-quiet-color); + min-height: 30px; +} + +.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--single, +.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--single { + padding: 0; +} + +.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--multiple, +.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--multiple { + padding: 0; +} + +.select2-container--admin-autocomplete .select2-selection--single { + background-color: var(--body-bg); + border: 1px solid var(--border-color); + border-radius: 4px; +} + +.select2-container--admin-autocomplete .select2-selection--single .select2-selection__rendered { + color: var(--body-fg); + line-height: 30px; +} + +.select2-container--admin-autocomplete .select2-selection--single .select2-selection__clear { + cursor: pointer; + float: right; + font-weight: bold; +} + +.select2-container--admin-autocomplete .select2-selection--single .select2-selection__placeholder { + color: var(--body-quiet-color); +} + +.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow { + height: 26px; + position: absolute; + top: 1px; + right: 1px; + width: 20px; +} + +.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow b { + border-color: #888 transparent transparent transparent; + border-style: solid; + border-width: 5px 4px 0 4px; + height: 0; + left: 50%; + margin-left: -4px; + margin-top: -2px; + position: absolute; + top: 50%; + width: 0; +} + +.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__clear { + float: left; +} + +.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__arrow { + left: 1px; + right: auto; +} + +.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single { + background-color: var(--darkened-bg); + cursor: default; +} + +.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single .select2-selection__clear { + display: none; +} + +.select2-container--admin-autocomplete.select2-container--open .select2-selection--single .select2-selection__arrow b { + border-color: transparent transparent #888 transparent; + border-width: 0 4px 5px 4px; +} + +.select2-container--admin-autocomplete .select2-selection--multiple { + background-color: var(--body-bg); + border: 1px solid var(--border-color); + border-radius: 4px; + cursor: text; +} + +.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered { + box-sizing: border-box; + list-style: none; + margin: 0; + padding: 0 10px 5px 5px; + width: 100%; + display: flex; + flex-wrap: wrap; +} + +.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered li { + list-style: none; +} + +.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__placeholder { + color: var(--body-quiet-color); + margin-top: 5px; + float: left; +} + +.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__clear { + cursor: pointer; + float: right; + font-weight: bold; + margin: 5px; + position: absolute; + right: 0; +} + +.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice { + background-color: var(--darkened-bg); + border: 1px solid var(--border-color); + border-radius: 4px; + cursor: default; + float: left; + margin-right: 5px; + margin-top: 5px; + padding: 0 5px; +} + +.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove { + color: var(--body-quiet-color); + cursor: pointer; + display: inline-block; + font-weight: bold; + margin-right: 2px; +} + +.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove:hover { + color: var(--body-fg); +} + +.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-search--inline { + float: right; +} + +.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice { + margin-left: 5px; + margin-right: auto; +} + +.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove { + margin-left: 2px; + margin-right: auto; +} + +.select2-container--admin-autocomplete.select2-container--focus .select2-selection--multiple { + border: solid var(--body-quiet-color) 1px; + outline: 0; +} + +.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--multiple { + background-color: var(--darkened-bg); + cursor: default; +} + +.select2-container--admin-autocomplete.select2-container--disabled .select2-selection__choice__remove { + display: none; +} + +.select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--multiple { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--multiple { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.select2-container--admin-autocomplete .select2-search--dropdown { + background: var(--darkened-bg); +} + +.select2-container--admin-autocomplete .select2-search--dropdown .select2-search__field { + background: var(--body-bg); + color: var(--body-fg); + border: 1px solid var(--border-color); + border-radius: 4px; +} + +.select2-container--admin-autocomplete .select2-search--inline .select2-search__field { + background: transparent; + color: var(--body-fg); + border: none; + outline: 0; + box-shadow: none; + -webkit-appearance: textfield; +} + +.select2-container--admin-autocomplete .select2-results > .select2-results__options { + max-height: 200px; + overflow-y: auto; + color: var(--body-fg); + background: var(--body-bg); +} + +.select2-container--admin-autocomplete .select2-results__option[role=group] { + padding: 0; +} + +.select2-container--admin-autocomplete .select2-results__option[aria-disabled=true] { + color: var(--body-quiet-color); +} + +.select2-container--admin-autocomplete .select2-results__option[aria-selected=true] { + background-color: var(--selected-bg); + color: var(--body-fg); +} + +.select2-container--admin-autocomplete .select2-results__option .select2-results__option { + padding-left: 1em; +} + +.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__group { + padding-left: 0; +} + +.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option { + margin-left: -1em; + padding-left: 2em; +} + +.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -2em; + padding-left: 3em; +} + +.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -3em; + padding-left: 4em; +} + +.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -4em; + padding-left: 5em; +} + +.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -5em; + padding-left: 6em; +} + +.select2-container--admin-autocomplete .select2-results__option--highlighted[aria-selected] { + background-color: var(--primary); + color: var(--primary-fg); +} + +.select2-container--admin-autocomplete .select2-results__group { + cursor: default; + display: block; + padding: 6px; +} diff --git a/static/admin/css/base.css b/static/admin/css/base.css new file mode 100644 index 0000000..93db7d0 --- /dev/null +++ b/static/admin/css/base.css @@ -0,0 +1,1145 @@ +/* + DJANGO Admin styles +*/ + +/* VARIABLE DEFINITIONS */ +html[data-theme="light"], +:root { + --primary: #79aec8; + --secondary: #417690; + --accent: #f5dd5d; + --primary-fg: #fff; + + --body-fg: #333; + --body-bg: #fff; + --body-quiet-color: #666; + --body-loud-color: #000; + + --header-color: #ffc; + --header-branding-color: var(--accent); + --header-bg: var(--secondary); + --header-link-color: var(--primary-fg); + + --breadcrumbs-fg: #c4dce8; + --breadcrumbs-link-fg: var(--body-bg); + --breadcrumbs-bg: var(--primary); + + --link-fg: #417893; + --link-hover-color: #036; + --link-selected-fg: #5b80b2; + + --hairline-color: #e8e8e8; + --border-color: #ccc; + + --error-fg: #ba2121; + + --message-success-bg: #dfd; + --message-warning-bg: #ffc; + --message-error-bg: #ffefef; + + --darkened-bg: #f8f8f8; /* A bit darker than --body-bg */ + --selected-bg: #e4e4e4; /* E.g. selected table cells */ + --selected-row: #ffc; + + --button-fg: #fff; + --button-bg: var(--primary); + --button-hover-bg: #609ab6; + --default-button-bg: var(--secondary); + --default-button-hover-bg: #205067; + --close-button-bg: #747474; + --close-button-hover-bg: #333; + --delete-button-bg: #ba2121; + --delete-button-hover-bg: #a41515; + + --object-tools-fg: var(--button-fg); + --object-tools-bg: var(--close-button-bg); + --object-tools-hover-bg: var(--close-button-hover-bg); + + --font-family-primary: + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + system-ui, + Roboto, + "Helvetica Neue", + Arial, + sans-serif, + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol", + "Noto Color Emoji"; + --font-family-monospace: + ui-monospace, + Menlo, + Monaco, + "Cascadia Mono", + "Segoe UI Mono", + "Roboto Mono", + "Oxygen Mono", + "Ubuntu Monospace", + "Source Code Pro", + "Fira Mono", + "Droid Sans Mono", + "Courier New", + monospace, + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol", + "Noto Color Emoji"; +} + +html, body { + height: 100%; +} + +body { + margin: 0; + padding: 0; + font-size: 0.875rem; + font-family: var(--font-family-primary); + color: var(--body-fg); + background: var(--body-bg); +} + +/* LINKS */ + +a:link, a:visited { + color: var(--link-fg); + text-decoration: none; + transition: color 0.15s, background 0.15s; +} + +a:focus, a:hover { + color: var(--link-hover-color); +} + +a:focus { + text-decoration: underline; +} + +a img { + border: none; +} + +a.section:link, a.section:visited { + color: var(--header-link-color); + text-decoration: none; +} + +a.section:focus, a.section:hover { + text-decoration: underline; +} + +/* GLOBAL DEFAULTS */ + +p, ol, ul, dl { + margin: .2em 0 .8em 0; +} + +p { + padding: 0; + line-height: 140%; +} + +h1,h2,h3,h4,h5 { + font-weight: bold; +} + +h1 { + margin: 0 0 20px; + font-weight: 300; + font-size: 1.25rem; + color: var(--body-quiet-color); +} + +h2 { + font-size: 1rem; + margin: 1em 0 .5em 0; +} + +h2.subhead { + font-weight: normal; + margin-top: 0; +} + +h3 { + font-size: 0.875rem; + margin: .8em 0 .3em 0; + color: var(--body-quiet-color); + font-weight: bold; +} + +h4 { + font-size: 0.75rem; + margin: 1em 0 .8em 0; + padding-bottom: 3px; +} + +h5 { + font-size: 0.625rem; + margin: 1.5em 0 .5em 0; + color: var(--body-quiet-color); + text-transform: uppercase; + letter-spacing: 1px; +} + +ul > li { + list-style-type: square; + padding: 1px 0; +} + +li ul { + margin-bottom: 0; +} + +li, dt, dd { + font-size: 0.8125rem; + line-height: 1.25rem; +} + +dt { + font-weight: bold; + margin-top: 4px; +} + +dd { + margin-left: 0; +} + +form { + margin: 0; + padding: 0; +} + +fieldset { + margin: 0; + min-width: 0; + padding: 0; + border: none; + border-top: 1px solid var(--hairline-color); +} + +blockquote { + font-size: 0.6875rem; + color: #777; + margin-left: 2px; + padding-left: 10px; + border-left: 5px solid #ddd; +} + +code, pre { + font-family: var(--font-family-monospace); + color: var(--body-quiet-color); + font-size: 0.75rem; + overflow-x: auto; +} + +pre.literal-block { + margin: 10px; + background: var(--darkened-bg); + padding: 6px 8px; +} + +code strong { + color: #930; +} + +hr { + clear: both; + color: var(--hairline-color); + background-color: var(--hairline-color); + height: 1px; + border: none; + margin: 0; + padding: 0; + line-height: 1px; +} + +/* TEXT STYLES & MODIFIERS */ + +.small { + font-size: 0.6875rem; +} + +.mini { + font-size: 0.625rem; +} + +.help, p.help, form p.help, div.help, form div.help, div.help li { + font-size: 0.6875rem; + color: var(--body-quiet-color); +} + +div.help ul { + margin-bottom: 0; +} + +.help-tooltip { + cursor: help; +} + +p img, h1 img, h2 img, h3 img, h4 img, td img { + vertical-align: middle; +} + +.quiet, a.quiet:link, a.quiet:visited { + color: var(--body-quiet-color); + font-weight: normal; +} + +.clear { + clear: both; +} + +.nowrap { + white-space: nowrap; +} + +.hidden { + display: none !important; +} + +/* TABLES */ + +table { + border-collapse: collapse; + border-color: var(--border-color); +} + +td, th { + font-size: 0.8125rem; + line-height: 1rem; + border-bottom: 1px solid var(--hairline-color); + vertical-align: top; + padding: 8px; +} + +th { + font-weight: 600; + text-align: left; +} + +thead th, +tfoot td { + color: var(--body-quiet-color); + padding: 5px 10px; + font-size: 0.6875rem; + background: var(--body-bg); + border: none; + border-top: 1px solid var(--hairline-color); + border-bottom: 1px solid var(--hairline-color); +} + +tfoot td { + border-bottom: none; + border-top: 1px solid var(--hairline-color); +} + +thead th.required { + color: var(--body-loud-color); +} + +tr.alt { + background: var(--darkened-bg); +} + +tr:nth-child(odd), .row-form-errors { + background: var(--body-bg); +} + +tr:nth-child(even), +tr:nth-child(even) .errorlist, +tr:nth-child(odd) + .row-form-errors, +tr:nth-child(odd) + .row-form-errors .errorlist { + background: var(--darkened-bg); +} + +/* SORTABLE TABLES */ + +thead th { + padding: 5px 10px; + line-height: normal; + text-transform: uppercase; + background: var(--darkened-bg); +} + +thead th a:link, thead th a:visited { + color: var(--body-quiet-color); +} + +thead th.sorted { + background: var(--selected-bg); +} + +thead th.sorted .text { + padding-right: 42px; +} + +table thead th .text span { + padding: 8px 10px; + display: block; +} + +table thead th .text a { + display: block; + cursor: pointer; + padding: 8px 10px; +} + +table thead th .text a:focus, table thead th .text a:hover { + background: var(--selected-bg); +} + +thead th.sorted a.sortremove { + visibility: hidden; +} + +table thead th.sorted:hover a.sortremove { + visibility: visible; +} + +table thead th.sorted .sortoptions { + display: block; + padding: 9px 5px 0 5px; + float: right; + text-align: right; +} + +table thead th.sorted .sortpriority { + font-size: .8em; + min-width: 12px; + text-align: center; + vertical-align: 3px; + margin-left: 2px; + margin-right: 2px; +} + +table thead th.sorted .sortoptions a { + position: relative; + width: 14px; + height: 14px; + display: inline-block; + background: url(../img/sorting-icons.svg) 0 0 no-repeat; + background-size: 14px auto; +} + +table thead th.sorted .sortoptions a.sortremove { + background-position: 0 0; +} + +table thead th.sorted .sortoptions a.sortremove:after { + content: '\\'; + position: absolute; + top: -6px; + left: 3px; + font-weight: 200; + font-size: 1.125rem; + color: var(--body-quiet-color); +} + +table thead th.sorted .sortoptions a.sortremove:focus:after, +table thead th.sorted .sortoptions a.sortremove:hover:after { + color: var(--link-fg); +} + +table thead th.sorted .sortoptions a.sortremove:focus, +table thead th.sorted .sortoptions a.sortremove:hover { + background-position: 0 -14px; +} + +table thead th.sorted .sortoptions a.ascending { + background-position: 0 -28px; +} + +table thead th.sorted .sortoptions a.ascending:focus, +table thead th.sorted .sortoptions a.ascending:hover { + background-position: 0 -42px; +} + +table thead th.sorted .sortoptions a.descending { + top: 1px; + background-position: 0 -56px; +} + +table thead th.sorted .sortoptions a.descending:focus, +table thead th.sorted .sortoptions a.descending:hover { + background-position: 0 -70px; +} + +/* FORM DEFAULTS */ + +input, textarea, select, .form-row p, form .button { + margin: 2px 0; + padding: 2px 3px; + vertical-align: middle; + font-family: var(--font-family-primary); + font-weight: normal; + font-size: 0.8125rem; +} +.form-row div.help { + padding: 2px 3px; +} + +textarea { + vertical-align: top; +} + +input[type=text], input[type=password], input[type=email], input[type=url], +input[type=number], input[type=tel], textarea, select, .vTextField { + border: 1px solid var(--border-color); + border-radius: 4px; + padding: 5px 6px; + margin-top: 0; + color: var(--body-fg); + background-color: var(--body-bg); +} + +input[type=text]:focus, input[type=password]:focus, input[type=email]:focus, +input[type=url]:focus, input[type=number]:focus, input[type=tel]:focus, +textarea:focus, select:focus, .vTextField:focus { + border-color: var(--body-quiet-color); +} + +select { + height: 1.875rem; +} + +select[multiple] { + /* Allow HTML size attribute to override the height in the rule above. */ + height: auto; + min-height: 150px; +} + +/* FORM BUTTONS */ + +.button, input[type=submit], input[type=button], .submit-row input, a.button { + background: var(--button-bg); + padding: 10px 15px; + border: none; + border-radius: 4px; + color: var(--button-fg); + cursor: pointer; + transition: background 0.15s; +} + +a.button { + padding: 4px 5px; +} + +.button:active, input[type=submit]:active, input[type=button]:active, +.button:focus, input[type=submit]:focus, input[type=button]:focus, +.button:hover, input[type=submit]:hover, input[type=button]:hover { + background: var(--button-hover-bg); +} + +.button[disabled], input[type=submit][disabled], input[type=button][disabled] { + opacity: 0.4; +} + +.button.default, input[type=submit].default, .submit-row input.default { + border: none; + font-weight: 400; + background: var(--default-button-bg); +} + +.button.default:active, input[type=submit].default:active, +.button.default:focus, input[type=submit].default:focus, +.button.default:hover, input[type=submit].default:hover { + background: var(--default-button-hover-bg); +} + +.button[disabled].default, +input[type=submit][disabled].default, +input[type=button][disabled].default { + opacity: 0.4; +} + + +/* MODULES */ + +.module { + border: none; + margin-bottom: 30px; + background: var(--body-bg); +} + +.module p, .module ul, .module h3, .module h4, .module dl, .module pre { + padding-left: 10px; + padding-right: 10px; +} + +.module blockquote { + margin-left: 12px; +} + +.module ul, .module ol { + margin-left: 1.5em; +} + +.module h3 { + margin-top: .6em; +} + +.module h2, .module caption, .inline-group h2 { + margin: 0; + padding: 8px; + font-weight: 400; + font-size: 0.8125rem; + text-align: left; + background: var(--primary); + color: var(--header-link-color); +} + +.module caption, +.inline-group h2 { + font-size: 0.75rem; + letter-spacing: 0.5px; + text-transform: uppercase; +} + +.module table { + border-collapse: collapse; +} + +/* MESSAGES & ERRORS */ + +ul.messagelist { + padding: 0; + margin: 0; +} + +ul.messagelist li { + display: block; + font-weight: 400; + font-size: 0.8125rem; + padding: 10px 10px 10px 65px; + margin: 0 0 10px 0; + background: var(--message-success-bg) url(../img/icon-yes.svg) 40px 12px no-repeat; + background-size: 16px auto; + color: var(--body-fg); + word-break: break-word; +} + +ul.messagelist li.warning { + background: var(--message-warning-bg) url(../img/icon-alert.svg) 40px 14px no-repeat; + background-size: 14px auto; +} + +ul.messagelist li.error { + background: var(--message-error-bg) url(../img/icon-no.svg) 40px 12px no-repeat; + background-size: 16px auto; +} + +.errornote { + font-size: 0.875rem; + font-weight: 700; + display: block; + padding: 10px 12px; + margin: 0 0 10px 0; + color: var(--error-fg); + border: 1px solid var(--error-fg); + border-radius: 4px; + background-color: var(--body-bg); + background-position: 5px 12px; + overflow-wrap: break-word; +} + +ul.errorlist { + margin: 0 0 4px; + padding: 0; + color: var(--error-fg); + background: var(--body-bg); +} + +ul.errorlist li { + font-size: 0.8125rem; + display: block; + margin-bottom: 4px; + overflow-wrap: break-word; +} + +ul.errorlist li:first-child { + margin-top: 0; +} + +ul.errorlist li a { + color: inherit; + text-decoration: underline; +} + +td ul.errorlist { + margin: 0; + padding: 0; +} + +td ul.errorlist li { + margin: 0; +} + +.form-row.errors { + margin: 0; + border: none; + border-bottom: 1px solid var(--hairline-color); + background: none; +} + +.form-row.errors ul.errorlist li { + padding-left: 0; +} + +.errors input, .errors select, .errors textarea, +td ul.errorlist + input, td ul.errorlist + select, td ul.errorlist + textarea { + border: 1px solid var(--error-fg); +} + +.description { + font-size: 0.75rem; + padding: 5px 0 0 12px; +} + +/* BREADCRUMBS */ + +div.breadcrumbs { + background: var(--breadcrumbs-bg); + padding: 10px 40px; + border: none; + color: var(--breadcrumbs-fg); + text-align: left; +} + +div.breadcrumbs a { + color: var(--breadcrumbs-link-fg); +} + +div.breadcrumbs a:focus, div.breadcrumbs a:hover { + color: var(--breadcrumbs-fg); +} + +/* ACTION ICONS */ + +.viewlink, .inlineviewlink { + padding-left: 16px; + background: url(../img/icon-viewlink.svg) 0 1px no-repeat; +} + +.addlink { + padding-left: 16px; + background: url(../img/icon-addlink.svg) 0 1px no-repeat; +} + +.changelink, .inlinechangelink { + padding-left: 16px; + background: url(../img/icon-changelink.svg) 0 1px no-repeat; +} + +.deletelink { + padding-left: 16px; + background: url(../img/icon-deletelink.svg) 0 1px no-repeat; +} + +a.deletelink:link, a.deletelink:visited { + color: #CC3434; /* XXX Probably unused? */ +} + +a.deletelink:focus, a.deletelink:hover { + color: #993333; /* XXX Probably unused? */ + text-decoration: none; +} + +/* OBJECT TOOLS */ + +.object-tools { + font-size: 0.625rem; + font-weight: bold; + padding-left: 0; + float: right; + position: relative; + margin-top: -48px; +} + +.object-tools li { + display: block; + float: left; + margin-left: 5px; + height: 1rem; +} + +.object-tools a { + border-radius: 15px; +} + +.object-tools a:link, .object-tools a:visited { + display: block; + float: left; + padding: 3px 12px; + background: var(--object-tools-bg); + color: var(--object-tools-fg); + font-weight: 400; + font-size: 0.6875rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.object-tools a:focus, .object-tools a:hover { + background-color: var(--object-tools-hover-bg); +} + +.object-tools a:focus{ + text-decoration: none; +} + +.object-tools a.viewsitelink, .object-tools a.addlink { + background-repeat: no-repeat; + background-position: right 7px center; + padding-right: 26px; +} + +.object-tools a.viewsitelink { + background-image: url(../img/tooltag-arrowright.svg); +} + +.object-tools a.addlink { + background-image: url(../img/tooltag-add.svg); +} + +/* OBJECT HISTORY */ + +#change-history table { + width: 100%; +} + +#change-history table tbody th { + width: 16em; +} + +#change-history .paginator { + color: var(--body-quiet-color); + border-bottom: 1px solid var(--hairline-color); + background: var(--body-bg); + overflow: hidden; +} + +/* PAGE STRUCTURE */ + +#container { + position: relative; + width: 100%; + min-width: 980px; + padding: 0; + display: flex; + flex-direction: column; + height: 100%; +} + +#container > div { + flex-shrink: 0; +} + +#container > .main { + display: flex; + flex: 1 0 auto; +} + +.main > .content { + flex: 1 0; + max-width: 100%; +} + +.skip-to-content-link { + position: absolute; + top: -999px; + margin: 5px; + padding: 5px; + background: var(--body-bg); + z-index: 1; +} + +.skip-to-content-link:focus { + left: 0px; + top: 0px; +} + +#content { + padding: 20px 40px; +} + +.dashboard #content { + width: 600px; +} + +#content-main { + float: left; + width: 100%; +} + +#content-related { + float: right; + width: 260px; + position: relative; + margin-right: -300px; +} + +#footer { + clear: both; + padding: 10px; +} + +/* COLUMN TYPES */ + +.colMS { + margin-right: 300px; +} + +.colSM { + margin-left: 300px; +} + +.colSM #content-related { + float: left; + margin-right: 0; + margin-left: -300px; +} + +.colSM #content-main { + float: right; +} + +.popup .colM { + width: auto; +} + +/* HEADER */ + +#header { + width: auto; + height: auto; + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 40px; + background: var(--header-bg); + color: var(--header-color); + overflow: hidden; +} + +#header a:link, #header a:visited, #logout-form button { + color: var(--header-link-color); +} + +#header a:focus , #header a:hover { + text-decoration: underline; +} + +#branding { + display: flex; +} + +#branding h1 { + padding: 0; + margin: 0; + margin-inline-end: 20px; + font-weight: 300; + font-size: 1.5rem; + color: var(--header-branding-color); +} + +#branding h1 a:link, #branding h1 a:visited { + color: var(--accent); +} + +#branding h2 { + padding: 0 10px; + font-size: 0.875rem; + margin: -8px 0 8px 0; + font-weight: normal; + color: var(--header-color); +} + +#branding a:hover { + text-decoration: none; +} + +#logout-form { + display: inline; +} + +#logout-form button { + background: none; + border: 0; + cursor: pointer; + font-family: var(--font-family-primary); +} + +#user-tools { + float: right; + margin: 0 0 0 20px; + text-align: right; +} + +#user-tools, #logout-form button{ + padding: 0; + font-weight: 300; + font-size: 0.6875rem; + letter-spacing: 0.5px; + text-transform: uppercase; +} + +#user-tools a, #logout-form button { + border-bottom: 1px solid rgba(255, 255, 255, 0.25); +} + +#user-tools a:focus, #user-tools a:hover, +#logout-form button:active, #logout-form button:hover { + text-decoration: none; + border-bottom: 0; +} + +#logout-form button:active, #logout-form button:hover { + margin-bottom: 1px; +} + +/* SIDEBAR */ + +#content-related { + background: var(--darkened-bg); +} + +#content-related .module { + background: none; +} + +#content-related h3 { + color: var(--body-quiet-color); + padding: 0 16px; + margin: 0 0 16px; +} + +#content-related h4 { + font-size: 0.8125rem; +} + +#content-related p { + padding-left: 16px; + padding-right: 16px; +} + +#content-related .actionlist { + padding: 0; + margin: 16px; +} + +#content-related .actionlist li { + line-height: 1.2; + margin-bottom: 10px; + padding-left: 18px; +} + +#content-related .module h2 { + background: none; + padding: 16px; + margin-bottom: 16px; + border-bottom: 1px solid var(--hairline-color); + font-size: 1.125rem; + color: var(--body-fg); +} + +.delete-confirmation form input[type="submit"] { + background: var(--delete-button-bg); + border-radius: 4px; + padding: 10px 15px; + color: var(--button-fg); +} + +.delete-confirmation form input[type="submit"]:active, +.delete-confirmation form input[type="submit"]:focus, +.delete-confirmation form input[type="submit"]:hover { + background: var(--delete-button-hover-bg); +} + +.delete-confirmation form .cancel-link { + display: inline-block; + vertical-align: middle; + height: 0.9375rem; + line-height: 0.9375rem; + border-radius: 4px; + padding: 10px 15px; + color: var(--button-fg); + background: var(--close-button-bg); + margin: 0 0 0 10px; +} + +.delete-confirmation form .cancel-link:active, +.delete-confirmation form .cancel-link:focus, +.delete-confirmation form .cancel-link:hover { + background: var(--close-button-hover-bg); +} + +/* POPUP */ +.popup #content { + padding: 20px; +} + +.popup #container { + min-width: 0; +} + +.popup #header { + padding: 10px 20px; +} + +/* PAGINATOR */ + +.paginator { + display: flex; + align-items: center; + gap: 4px; + font-size: 0.8125rem; + padding-top: 10px; + padding-bottom: 10px; + line-height: 22px; + margin: 0; + border-top: 1px solid var(--hairline-color); + width: 100%; +} + +.paginator a:link, .paginator a:visited { + padding: 2px 6px; + background: var(--button-bg); + text-decoration: none; + color: var(--button-fg); +} + +.paginator a.showall { + border: none; + background: none; + color: var(--link-fg); +} + +.paginator a.showall:focus, .paginator a.showall:hover { + background: none; + color: var(--link-hover-color); +} + +.paginator .end { + margin-right: 6px; +} + +.paginator .this-page { + padding: 2px 6px; + font-weight: bold; + font-size: 0.8125rem; + vertical-align: top; +} + +.paginator a:focus, .paginator a:hover { + color: white; + background: var(--link-hover-color); +} + +.paginator input { + margin-left: auto; +} + +.base-svgs { + display: none; +} diff --git a/static/admin/css/changelists.css b/static/admin/css/changelists.css new file mode 100644 index 0000000..a754513 --- /dev/null +++ b/static/admin/css/changelists.css @@ -0,0 +1,328 @@ +/* CHANGELISTS */ + +#changelist { + display: flex; + align-items: flex-start; + justify-content: space-between; +} + +#changelist .changelist-form-container { + flex: 1 1 auto; + min-width: 0; +} + +#changelist table { + width: 100%; +} + +.change-list .hiddenfields { display:none; } + +.change-list .filtered table { + border-right: none; +} + +.change-list .filtered { + min-height: 400px; +} + +.change-list .filtered .results, .change-list .filtered .paginator, +.filtered #toolbar, .filtered div.xfull { + width: auto; +} + +.change-list .filtered table tbody th { + padding-right: 1em; +} + +#changelist-form .results { + overflow-x: auto; + width: 100%; +} + +#changelist .toplinks { + border-bottom: 1px solid var(--hairline-color); +} + +#changelist .paginator { + color: var(--body-quiet-color); + border-bottom: 1px solid var(--hairline-color); + background: var(--body-bg); + overflow: hidden; +} + +/* CHANGELIST TABLES */ + +#changelist table thead th { + padding: 0; + white-space: nowrap; + vertical-align: middle; +} + +#changelist table thead th.action-checkbox-column { + width: 1.5em; + text-align: center; +} + +#changelist table tbody td.action-checkbox { + text-align: center; +} + +#changelist table tfoot { + color: var(--body-quiet-color); +} + +/* TOOLBAR */ + +#toolbar { + padding: 8px 10px; + margin-bottom: 15px; + border-top: 1px solid var(--hairline-color); + border-bottom: 1px solid var(--hairline-color); + background: var(--darkened-bg); + color: var(--body-quiet-color); +} + +#toolbar form input { + border-radius: 4px; + font-size: 0.875rem; + padding: 5px; + color: var(--body-fg); +} + +#toolbar #searchbar { + height: 1.1875rem; + border: 1px solid var(--border-color); + padding: 2px 5px; + margin: 0; + vertical-align: top; + font-size: 0.8125rem; + max-width: 100%; +} + +#toolbar #searchbar:focus { + border-color: var(--body-quiet-color); +} + +#toolbar form input[type="submit"] { + border: 1px solid var(--border-color); + font-size: 0.8125rem; + padding: 4px 8px; + margin: 0; + vertical-align: middle; + background: var(--body-bg); + box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset; + cursor: pointer; + color: var(--body-fg); +} + +#toolbar form input[type="submit"]:focus, +#toolbar form input[type="submit"]:hover { + border-color: var(--body-quiet-color); +} + +#changelist-search img { + vertical-align: middle; + margin-right: 4px; +} + +#changelist-search .help { + word-break: break-word; +} + +/* FILTER COLUMN */ + +#changelist-filter { + flex: 0 0 240px; + order: 1; + background: var(--darkened-bg); + border-left: none; + margin: 0 0 0 30px; +} + +#changelist-filter h2 { + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.5px; + padding: 5px 15px; + margin-bottom: 12px; + border-bottom: none; +} + +#changelist-filter h3, +#changelist-filter details summary { + font-weight: 400; + padding: 0 15px; + margin-bottom: 10px; +} + +#changelist-filter details summary > * { + display: inline; +} + +#changelist-filter details > summary { + list-style-type: none; +} + +#changelist-filter details > summary::-webkit-details-marker { + display: none; +} + +#changelist-filter details > summary::before { + content: '→'; + font-weight: bold; + color: var(--link-hover-color); +} + +#changelist-filter details[open] > summary::before { + content: '↓'; +} + +#changelist-filter ul { + margin: 5px 0; + padding: 0 15px 15px; + border-bottom: 1px solid var(--hairline-color); +} + +#changelist-filter ul:last-child { + border-bottom: none; +} + +#changelist-filter li { + list-style-type: none; + margin-left: 0; + padding-left: 0; +} + +#changelist-filter a { + display: block; + color: var(--body-quiet-color); + word-break: break-word; +} + +#changelist-filter li.selected { + border-left: 5px solid var(--hairline-color); + padding-left: 10px; + margin-left: -15px; +} + +#changelist-filter li.selected a { + color: var(--link-selected-fg); +} + +#changelist-filter a:focus, #changelist-filter a:hover, +#changelist-filter li.selected a:focus, +#changelist-filter li.selected a:hover { + color: var(--link-hover-color); +} + +#changelist-filter #changelist-filter-clear a { + font-size: 0.8125rem; + padding-bottom: 10px; + border-bottom: 1px solid var(--hairline-color); +} + +/* DATE DRILLDOWN */ + +.change-list .toplinks { + display: flex; + padding-bottom: 5px; + flex-wrap: wrap; + gap: 3px 17px; + font-weight: bold; +} + +.change-list .toplinks a { + font-size: 0.8125rem; +} + +.change-list .toplinks .date-back { + color: var(--body-quiet-color); +} + +.change-list .toplinks .date-back:focus, +.change-list .toplinks .date-back:hover { + color: var(--link-hover-color); +} + +/* ACTIONS */ + +.filtered .actions { + border-right: none; +} + +#changelist table input { + margin: 0; + vertical-align: baseline; +} + +/* Once the :has() pseudo-class is supported by all browsers, the tr.selected + selector and the JS adding the class can be removed. */ +#changelist tbody tr.selected { + background-color: var(--selected-row); +} + +#changelist tbody tr:has(.action-select:checked) { + background-color: var(--selected-row); +} + +#changelist .actions { + padding: 10px; + background: var(--body-bg); + border-top: none; + border-bottom: none; + line-height: 1.5rem; + color: var(--body-quiet-color); + width: 100%; +} + +#changelist .actions span.all, +#changelist .actions span.action-counter, +#changelist .actions span.clear, +#changelist .actions span.question { + font-size: 0.8125rem; + margin: 0 0.5em; +} + +#changelist .actions:last-child { + border-bottom: none; +} + +#changelist .actions select { + vertical-align: top; + height: 1.5rem; + color: var(--body-fg); + border: 1px solid var(--border-color); + border-radius: 4px; + font-size: 0.875rem; + padding: 0 0 0 4px; + margin: 0; + margin-left: 10px; +} + +#changelist .actions select:focus { + border-color: var(--body-quiet-color); +} + +#changelist .actions label { + display: inline-block; + vertical-align: middle; + font-size: 0.8125rem; +} + +#changelist .actions .button { + font-size: 0.8125rem; + border: 1px solid var(--border-color); + border-radius: 4px; + background: var(--body-bg); + box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset; + cursor: pointer; + height: 1.5rem; + line-height: 1; + padding: 4px 8px; + margin: 0; + color: var(--body-fg); +} + +#changelist .actions .button:focus, #changelist .actions .button:hover { + border-color: var(--body-quiet-color); +} diff --git a/static/admin/css/dark_mode.css b/static/admin/css/dark_mode.css new file mode 100644 index 0000000..6d08233 --- /dev/null +++ b/static/admin/css/dark_mode.css @@ -0,0 +1,137 @@ +@media (prefers-color-scheme: dark) { + :root { + --primary: #264b5d; + --primary-fg: #f7f7f7; + + --body-fg: #eeeeee; + --body-bg: #121212; + --body-quiet-color: #e0e0e0; + --body-loud-color: #ffffff; + + --breadcrumbs-link-fg: #e0e0e0; + --breadcrumbs-bg: var(--primary); + + --link-fg: #81d4fa; + --link-hover-color: #4ac1f7; + --link-selected-fg: #6f94c6; + + --hairline-color: #272727; + --border-color: #353535; + + --error-fg: #e35f5f; + --message-success-bg: #006b1b; + --message-warning-bg: #583305; + --message-error-bg: #570808; + + --darkened-bg: #212121; + --selected-bg: #1b1b1b; + --selected-row: #00363a; + + --close-button-bg: #333333; + --close-button-hover-bg: #666666; + } + } + + +html[data-theme="dark"] { + --primary: #264b5d; + --primary-fg: #f7f7f7; + + --body-fg: #eeeeee; + --body-bg: #121212; + --body-quiet-color: #e0e0e0; + --body-loud-color: #ffffff; + + --breadcrumbs-link-fg: #e0e0e0; + --breadcrumbs-bg: var(--primary); + + --link-fg: #81d4fa; + --link-hover-color: #4ac1f7; + --link-selected-fg: #6f94c6; + + --hairline-color: #272727; + --border-color: #353535; + + --error-fg: #e35f5f; + --message-success-bg: #006b1b; + --message-warning-bg: #583305; + --message-error-bg: #570808; + + --darkened-bg: #212121; + --selected-bg: #1b1b1b; + --selected-row: #00363a; + + --close-button-bg: #333333; + --close-button-hover-bg: #666666; +} + +/* THEME SWITCH */ +.theme-toggle { + cursor: pointer; + border: none; + padding: 0; + background: transparent; + vertical-align: middle; + margin-inline-start: 5px; + margin-top: -1px; +} + +.theme-toggle svg { + vertical-align: middle; + height: 1rem; + width: 1rem; + display: none; +} + +/* +Fully hide screen reader text so we only show the one matching the current +theme. +*/ +.theme-toggle .visually-hidden { + display: none; +} + +html[data-theme="auto"] .theme-toggle .theme-label-when-auto { + display: block; +} + +html[data-theme="dark"] .theme-toggle .theme-label-when-dark { + display: block; +} + +html[data-theme="light"] .theme-toggle .theme-label-when-light { + display: block; +} + +/* ICONS */ +.theme-toggle svg.theme-icon-when-auto, +.theme-toggle svg.theme-icon-when-dark, +.theme-toggle svg.theme-icon-when-light { + fill: var(--header-link-color); + color: var(--header-bg); +} + +html[data-theme="auto"] .theme-toggle svg.theme-icon-when-auto { + display: block; +} + +html[data-theme="dark"] .theme-toggle svg.theme-icon-when-dark { + display: block; +} + +html[data-theme="light"] .theme-toggle svg.theme-icon-when-light { + display: block; +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + overflow: hidden; + clip: rect(0,0,0,0); + white-space: nowrap; + border: 0; + color: var(--body-fg); + background-color: var(--body-bg); +} diff --git a/static/admin/css/dashboard.css b/static/admin/css/dashboard.css new file mode 100644 index 0000000..242b81a --- /dev/null +++ b/static/admin/css/dashboard.css @@ -0,0 +1,29 @@ +/* DASHBOARD */ +.dashboard td, .dashboard th { + word-break: break-word; +} + +.dashboard .module table th { + width: 100%; +} + +.dashboard .module table td { + white-space: nowrap; +} + +.dashboard .module table td a { + display: block; + padding-right: .6em; +} + +/* RECENT ACTIONS MODULE */ + +.module ul.actionlist { + margin-left: 0; +} + +ul.actionlist li { + list-style-type: none; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/static/admin/css/forms.css b/static/admin/css/forms.css new file mode 100644 index 0000000..9a8dad0 --- /dev/null +++ b/static/admin/css/forms.css @@ -0,0 +1,534 @@ +@import url('widgets.css'); + +/* FORM ROWS */ + +.form-row { + overflow: hidden; + padding: 10px; + font-size: 0.8125rem; + border-bottom: 1px solid var(--hairline-color); +} + +.form-row img, .form-row input { + vertical-align: middle; +} + +.form-row label input[type="checkbox"] { + margin-top: 0; + vertical-align: 0; +} + +form .form-row p { + padding-left: 0; +} + +.flex-container { + display: flex; +} + +.form-multiline { + flex-wrap: wrap; +} + +.form-multiline > div { + padding-bottom: 10px; +} + +/* FORM LABELS */ + +label { + font-weight: normal; + color: var(--body-quiet-color); + font-size: 0.8125rem; +} + +.required label, label.required { + font-weight: bold; + color: var(--body-fg); +} + +/* RADIO BUTTONS */ + +form div.radiolist div { + padding-right: 7px; +} + +form div.radiolist.inline div { + display: inline-block; +} + +form div.radiolist label { + width: auto; +} + +form div.radiolist input[type="radio"] { + margin: -2px 4px 0 0; + padding: 0; +} + +form ul.inline { + margin-left: 0; + padding: 0; +} + +form ul.inline li { + float: left; + padding-right: 7px; +} + +/* ALIGNED FIELDSETS */ + +.aligned label { + display: block; + padding: 4px 10px 0 0; + min-width: 160px; + width: 160px; + word-wrap: break-word; + line-height: 1; +} + +.aligned label:not(.vCheckboxLabel):after { + content: ''; + display: inline-block; + vertical-align: middle; + height: 1.625rem; +} + +.aligned label + p, .aligned .checkbox-row + div.help, .aligned label + div.readonly { + padding: 6px 0; + margin-top: 0; + margin-bottom: 0; + margin-left: 0; + overflow-wrap: break-word; +} + +.aligned ul label { + display: inline; + float: none; + width: auto; +} + +.aligned .form-row input { + margin-bottom: 0; +} + +.colMS .aligned .vLargeTextField, .colMS .aligned .vXMLLargeTextField { + width: 350px; +} + +form .aligned ul { + margin-left: 160px; + padding-left: 10px; +} + +form .aligned div.radiolist { + display: inline-block; + margin: 0; + padding: 0; +} + +form .aligned p.help, +form .aligned div.help { + margin-top: 0; + margin-left: 160px; + padding-left: 10px; +} + +form .aligned p.date div.help.timezonewarning, +form .aligned p.datetime div.help.timezonewarning, +form .aligned p.time div.help.timezonewarning { + margin-left: 0; + padding-left: 0; + font-weight: normal; +} + +form .aligned p.help:last-child, +form .aligned div.help:last-child { + margin-bottom: 0; + padding-bottom: 0; +} + +form .aligned input + p.help, +form .aligned textarea + p.help, +form .aligned select + p.help, +form .aligned input + div.help, +form .aligned textarea + div.help, +form .aligned select + div.help { + margin-left: 160px; + padding-left: 10px; +} + +form .aligned ul li { + list-style: none; +} + +form .aligned table p { + margin-left: 0; + padding-left: 0; +} + +.aligned .vCheckboxLabel { + float: none; + width: auto; + display: inline-block; + vertical-align: -3px; + padding: 0 0 5px 5px; +} + +.aligned .vCheckboxLabel + p.help, +.aligned .vCheckboxLabel + div.help { + margin-top: -4px; +} + +.colM .aligned .vLargeTextField, .colM .aligned .vXMLLargeTextField { + width: 610px; +} + +fieldset .fieldBox { + margin-right: 20px; +} + +/* WIDE FIELDSETS */ + +.wide label { + width: 200px; +} + +form .wide p, +form .wide ul.errorlist, +form .wide input + p.help, +form .wide input + div.help { + margin-left: 200px; +} + +form .wide p.help, +form .wide div.help { + padding-left: 50px; +} + +form div.help ul { + padding-left: 0; + margin-left: 0; +} + +.colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField { + width: 450px; +} + +/* COLLAPSED FIELDSETS */ + +fieldset.collapsed * { + display: none; +} + +fieldset.collapsed h2, fieldset.collapsed { + display: block; +} + +fieldset.collapsed { + border: 1px solid var(--hairline-color); + border-radius: 4px; + overflow: hidden; +} + +fieldset.collapsed h2 { + background: var(--darkened-bg); + color: var(--body-quiet-color); +} + +fieldset .collapse-toggle { + color: var(--header-link-color); +} + +fieldset.collapsed .collapse-toggle { + background: transparent; + display: inline; + color: var(--link-fg); +} + +/* MONOSPACE TEXTAREAS */ + +fieldset.monospace textarea { + font-family: var(--font-family-monospace); +} + +/* SUBMIT ROW */ + +.submit-row { + padding: 12px 14px 12px; + margin: 0 0 20px; + background: var(--darkened-bg); + border: 1px solid var(--hairline-color); + border-radius: 4px; + overflow: hidden; + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +body.popup .submit-row { + overflow: auto; +} + +.submit-row input { + height: 2.1875rem; + line-height: 0.9375rem; +} + +.submit-row input, .submit-row a { + margin: 0; +} + +.submit-row input.default { + text-transform: uppercase; +} + +.submit-row a.deletelink { + margin-left: auto; +} + +.submit-row a.deletelink { + display: block; + background: var(--delete-button-bg); + border-radius: 4px; + padding: 0.625rem 0.9375rem; + height: 0.9375rem; + line-height: 0.9375rem; + color: var(--button-fg); +} + +.submit-row a.closelink { + display: inline-block; + background: var(--close-button-bg); + border-radius: 4px; + padding: 10px 15px; + height: 0.9375rem; + line-height: 0.9375rem; + color: var(--button-fg); +} + +.submit-row a.deletelink:focus, +.submit-row a.deletelink:hover, +.submit-row a.deletelink:active { + background: var(--delete-button-hover-bg); + text-decoration: none; +} + +.submit-row a.closelink:focus, +.submit-row a.closelink:hover, +.submit-row a.closelink:active { + background: var(--close-button-hover-bg); + text-decoration: none; +} + +/* CUSTOM FORM FIELDS */ + +.vSelectMultipleField { + vertical-align: top; +} + +.vCheckboxField { + border: none; +} + +.vDateField, .vTimeField { + margin-right: 2px; + margin-bottom: 4px; +} + +.vDateField { + min-width: 6.85em; +} + +.vTimeField { + min-width: 4.7em; +} + +.vURLField { + width: 30em; +} + +.vLargeTextField, .vXMLLargeTextField { + width: 48em; +} + +.flatpages-flatpage #id_content { + height: 40.2em; +} + +.module table .vPositiveSmallIntegerField { + width: 2.2em; +} + +.vIntegerField { + width: 5em; +} + +.vBigIntegerField { + width: 10em; +} + +.vForeignKeyRawIdAdminField { + width: 5em; +} + +.vTextField, .vUUIDField { + width: 20em; +} + +/* INLINES */ + +.inline-group { + padding: 0; + margin: 0 0 30px; +} + +.inline-group thead th { + padding: 8px 10px; +} + +.inline-group .aligned label { + width: 160px; +} + +.inline-related { + position: relative; +} + +.inline-related h3 { + margin: 0; + color: var(--body-quiet-color); + padding: 5px; + font-size: 0.8125rem; + background: var(--darkened-bg); + border-top: 1px solid var(--hairline-color); + border-bottom: 1px solid var(--hairline-color); +} + +.inline-related h3 span.delete { + float: right; +} + +.inline-related h3 span.delete label { + margin-left: 2px; + font-size: 0.6875rem; +} + +.inline-related fieldset { + margin: 0; + background: var(--body-bg); + border: none; + width: 100%; +} + +.inline-related fieldset.module h3 { + margin: 0; + padding: 2px 5px 3px 5px; + font-size: 0.6875rem; + text-align: left; + font-weight: bold; + background: #bcd; + color: var(--body-bg); +} + +.inline-group .tabular fieldset.module { + border: none; +} + +.inline-related.tabular fieldset.module table { + width: 100%; + overflow-x: scroll; +} + +.last-related fieldset { + border: none; +} + +.inline-group .tabular tr.has_original td { + padding-top: 2em; +} + +.inline-group .tabular tr td.original { + padding: 2px 0 0 0; + width: 0; + _position: relative; +} + +.inline-group .tabular th.original { + width: 0px; + padding: 0; +} + +.inline-group .tabular td.original p { + position: absolute; + left: 0; + height: 1.1em; + padding: 2px 9px; + overflow: hidden; + font-size: 0.5625rem; + font-weight: bold; + color: var(--body-quiet-color); + _width: 700px; +} + +.inline-group ul.tools { + padding: 0; + margin: 0; + list-style: none; +} + +.inline-group ul.tools li { + display: inline; + padding: 0 5px; +} + +.inline-group div.add-row, +.inline-group .tabular tr.add-row td { + color: var(--body-quiet-color); + background: var(--darkened-bg); + padding: 8px 10px; + border-bottom: 1px solid var(--hairline-color); +} + +.inline-group .tabular tr.add-row td { + padding: 8px 10px; + border-bottom: 1px solid var(--hairline-color); +} + +.inline-group ul.tools a.add, +.inline-group div.add-row a, +.inline-group .tabular tr.add-row td a { + background: url(../img/icon-addlink.svg) 0 1px no-repeat; + padding-left: 16px; + font-size: 0.75rem; +} + +.empty-form { + display: none; +} + +/* RELATED FIELD ADD ONE / LOOKUP */ + +.related-lookup { + margin-left: 5px; + display: inline-block; + vertical-align: middle; + background-repeat: no-repeat; + background-size: 14px; +} + +.related-lookup { + width: 1rem; + height: 1rem; + background-image: url(../img/search.svg); +} + +form .related-widget-wrapper ul { + display: inline-block; + margin-left: 0; + padding-left: 0; +} + +.clearable-file-input input { + margin-top: 0; +} diff --git a/static/admin/css/login.css b/static/admin/css/login.css new file mode 100644 index 0000000..389772f --- /dev/null +++ b/static/admin/css/login.css @@ -0,0 +1,61 @@ +/* LOGIN FORM */ + +.login { + background: var(--darkened-bg); + height: auto; +} + +.login #header { + height: auto; + padding: 15px 16px; + justify-content: center; +} + +.login #header h1 { + font-size: 1.125rem; + margin: 0; +} + +.login #header h1 a { + color: var(--header-link-color); +} + +.login #content { + padding: 20px 20px 0; +} + +.login #container { + background: var(--body-bg); + border: 1px solid var(--hairline-color); + border-radius: 4px; + overflow: hidden; + width: 28em; + min-width: 300px; + margin: 100px auto; + height: auto; +} + +.login .form-row { + padding: 4px 0; +} + +.login .form-row label { + display: block; + line-height: 2em; +} + +.login .form-row #id_username, .login .form-row #id_password { + padding: 8px; + width: 100%; + box-sizing: border-box; +} + +.login .submit-row { + padding: 1em 0 0 0; + margin: 0; + text-align: center; +} + +.login .password-reset-link { + text-align: center; +} diff --git a/static/admin/css/nav_sidebar.css b/static/admin/css/nav_sidebar.css new file mode 100644 index 0000000..f76e6ce --- /dev/null +++ b/static/admin/css/nav_sidebar.css @@ -0,0 +1,144 @@ +.sticky { + position: sticky; + top: 0; + max-height: 100vh; +} + +.toggle-nav-sidebar { + z-index: 20; + left: 0; + display: flex; + align-items: center; + justify-content: center; + flex: 0 0 23px; + width: 23px; + border: 0; + border-right: 1px solid var(--hairline-color); + background-color: var(--body-bg); + cursor: pointer; + font-size: 1.25rem; + color: var(--link-fg); + padding: 0; +} + +[dir="rtl"] .toggle-nav-sidebar { + border-left: 1px solid var(--hairline-color); + border-right: 0; +} + +.toggle-nav-sidebar:hover, +.toggle-nav-sidebar:focus { + background-color: var(--darkened-bg); +} + +#nav-sidebar { + z-index: 15; + flex: 0 0 275px; + left: -276px; + margin-left: -276px; + border-top: 1px solid transparent; + border-right: 1px solid var(--hairline-color); + background-color: var(--body-bg); + overflow: auto; +} + +[dir="rtl"] #nav-sidebar { + border-left: 1px solid var(--hairline-color); + border-right: 0; + left: 0; + margin-left: 0; + right: -276px; + margin-right: -276px; +} + +.toggle-nav-sidebar::before { + content: '\00BB'; +} + +.main.shifted .toggle-nav-sidebar::before { + content: '\00AB'; +} + +.main > #nav-sidebar { + visibility: hidden; +} + +.main.shifted > #nav-sidebar { + margin-left: 0; + visibility: visible; +} + +[dir="rtl"] .main.shifted > #nav-sidebar { + margin-right: 0; +} + +#nav-sidebar .module th { + width: 100%; + overflow-wrap: anywhere; +} + +#nav-sidebar .module th, +#nav-sidebar .module caption { + padding-left: 16px; +} + +#nav-sidebar .module td { + white-space: nowrap; +} + +[dir="rtl"] #nav-sidebar .module th, +[dir="rtl"] #nav-sidebar .module caption { + padding-left: 8px; + padding-right: 16px; +} + +#nav-sidebar .current-app .section:link, +#nav-sidebar .current-app .section:visited { + color: var(--header-color); + font-weight: bold; +} + +#nav-sidebar .current-model { + background: var(--selected-row); +} + +.main > #nav-sidebar + .content { + max-width: calc(100% - 23px); +} + +.main.shifted > #nav-sidebar + .content { + max-width: calc(100% - 299px); +} + +@media (max-width: 767px) { + #nav-sidebar, #toggle-nav-sidebar { + display: none; + } + + .main > #nav-sidebar + .content, + .main.shifted > #nav-sidebar + .content { + max-width: 100%; + } +} + +#nav-filter { + width: 100%; + box-sizing: border-box; + padding: 2px 5px; + margin: 5px 0; + border: 1px solid var(--border-color); + background-color: var(--darkened-bg); + color: var(--body-fg); +} + +#nav-filter:focus { + border-color: var(--body-quiet-color); +} + +#nav-filter.no-results { + background: var(--message-error-bg); +} + +#nav-sidebar table { + width: 100%; +} diff --git a/static/admin/css/responsive.css b/static/admin/css/responsive.css new file mode 100644 index 0000000..1d0a188 --- /dev/null +++ b/static/admin/css/responsive.css @@ -0,0 +1,999 @@ +/* Tablets */ + +input[type="submit"], button { + -webkit-appearance: none; + appearance: none; +} + +@media (max-width: 1024px) { + /* Basic */ + + html { + -webkit-text-size-adjust: 100%; + } + + td, th { + padding: 10px; + font-size: 0.875rem; + } + + .small { + font-size: 0.75rem; + } + + /* Layout */ + + #container { + min-width: 0; + } + + #content { + padding: 15px 20px 20px; + } + + div.breadcrumbs { + padding: 10px 30px; + } + + /* Header */ + + #header { + flex-direction: column; + padding: 15px 30px; + justify-content: flex-start; + } + + #branding h1 { + margin: 0 0 8px; + line-height: 1.2; + } + + #user-tools { + margin: 0; + font-weight: 400; + line-height: 1.85; + text-align: left; + } + + #user-tools a { + display: inline-block; + line-height: 1.4; + } + + /* Dashboard */ + + .dashboard #content { + width: auto; + } + + #content-related { + margin-right: -290px; + } + + .colSM #content-related { + margin-left: -290px; + } + + .colMS { + margin-right: 290px; + } + + .colSM { + margin-left: 290px; + } + + .dashboard .module table td a { + padding-right: 0; + } + + td .changelink, td .addlink { + font-size: 0.8125rem; + } + + /* Changelist */ + + #toolbar { + border: none; + padding: 15px; + } + + #changelist-search > div { + display: flex; + flex-wrap: nowrap; + max-width: 480px; + } + + #changelist-search label { + line-height: 1.375rem; + } + + #toolbar form #searchbar { + flex: 1 0 auto; + width: 0; + height: 1.375rem; + margin: 0 10px 0 6px; + } + + #toolbar form input[type=submit] { + flex: 0 1 auto; + } + + #changelist-search .quiet { + width: 0; + flex: 1 0 auto; + margin: 5px 0 0 25px; + } + + #changelist .actions { + display: flex; + flex-wrap: wrap; + padding: 15px 0; + } + + #changelist .actions label { + display: flex; + } + + #changelist .actions select { + background: var(--body-bg); + } + + #changelist .actions .button { + min-width: 48px; + margin: 0 10px; + } + + #changelist .actions span.all, + #changelist .actions span.clear, + #changelist .actions span.question, + #changelist .actions span.action-counter { + font-size: 0.6875rem; + margin: 0 10px 0 0; + } + + #changelist-filter { + flex-basis: 200px; + } + + .change-list .filtered .results, + .change-list .filtered .paginator, + .filtered #toolbar, + .filtered .actions, + + #changelist .paginator { + border-top-color: var(--hairline-color); /* XXX Is this used at all? */ + } + + #changelist .results + .paginator { + border-top: none; + } + + /* Forms */ + + label { + font-size: 0.875rem; + } + + .form-row input[type=text], + .form-row input[type=password], + .form-row input[type=email], + .form-row input[type=url], + .form-row input[type=tel], + .form-row input[type=number], + .form-row textarea, + .form-row select, + .form-row .vTextField { + box-sizing: border-box; + margin: 0; + padding: 6px 8px; + min-height: 2.25rem; + font-size: 0.875rem; + } + + .form-row select { + height: 2.25rem; + } + + .form-row select[multiple] { + height: auto; + min-height: 0; + } + + fieldset .fieldBox + .fieldBox { + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid var(--hairline-color); + } + + textarea { + max-width: 100%; + max-height: 120px; + } + + .aligned label { + padding-top: 6px; + } + + .aligned .related-lookup, + .aligned .datetimeshortcuts, + .aligned .related-lookup + strong { + align-self: center; + margin-left: 15px; + } + + form .aligned div.radiolist { + margin-left: 2px; + } + + .submit-row { + padding: 8px; + } + + .submit-row a.deletelink { + padding: 10px 7px; + } + + .button, input[type=submit], input[type=button], .submit-row input, a.button { + padding: 7px; + } + + /* Related widget */ + + .related-widget-wrapper { + float: none; + } + + .related-widget-wrapper-link + .selector { + max-width: calc(100% - 30px); + margin-right: 15px; + } + + select + .related-widget-wrapper-link, + .related-widget-wrapper-link + .related-widget-wrapper-link { + margin-left: 10px; + } + + /* Selector */ + + .selector { + display: flex; + width: 100%; + } + + .selector .selector-filter { + display: flex; + align-items: center; + } + + .selector .selector-filter label { + margin: 0 8px 0 0; + } + + .selector .selector-filter input { + width: auto; + min-height: 0; + flex: 1 1; + } + + .selector-available, .selector-chosen { + width: auto; + flex: 1 1; + display: flex; + flex-direction: column; + } + + .selector select { + width: 100%; + flex: 1 0 auto; + margin-bottom: 5px; + } + + .selector ul.selector-chooser { + width: 26px; + height: 52px; + padding: 2px 0; + margin: auto 15px; + border-radius: 20px; + transform: translateY(-10px); + } + + .selector-add, .selector-remove { + width: 20px; + height: 20px; + background-size: 20px auto; + } + + .selector-add { + background-position: 0 -120px; + } + + .selector-remove { + background-position: 0 -80px; + } + + a.selector-chooseall, a.selector-clearall { + align-self: center; + } + + .stacked { + flex-direction: column; + max-width: 480px; + } + + .stacked > * { + flex: 0 1 auto; + } + + .stacked select { + margin-bottom: 0; + } + + .stacked .selector-available, .stacked .selector-chosen { + width: auto; + } + + .stacked ul.selector-chooser { + width: 52px; + height: 26px; + padding: 0 2px; + margin: 15px auto; + transform: none; + } + + .stacked .selector-chooser li { + padding: 3px; + } + + .stacked .selector-add, .stacked .selector-remove { + background-size: 20px auto; + } + + .stacked .selector-add { + background-position: 0 -40px; + } + + .stacked .active.selector-add { + background-position: 0 -40px; + } + + .active.selector-add:focus, .active.selector-add:hover { + background-position: 0 -140px; + } + + .stacked .active.selector-add:focus, .stacked .active.selector-add:hover { + background-position: 0 -60px; + } + + .stacked .selector-remove { + background-position: 0 0; + } + + .stacked .active.selector-remove { + background-position: 0 0; + } + + .active.selector-remove:focus, .active.selector-remove:hover { + background-position: 0 -100px; + } + + .stacked .active.selector-remove:focus, .stacked .active.selector-remove:hover { + background-position: 0 -20px; + } + + .help-tooltip, .selector .help-icon { + display: none; + } + + .datetime input { + width: 50%; + max-width: 120px; + } + + .datetime span { + font-size: 0.8125rem; + } + + .datetime .timezonewarning { + display: block; + font-size: 0.6875rem; + color: var(--body-quiet-color); + } + + .datetimeshortcuts { + color: var(--border-color); /* XXX Redundant, .datetime span also sets #ccc */ + } + + .form-row .datetime input.vDateField, .form-row .datetime input.vTimeField { + width: 75%; + } + + .inline-group { + overflow: auto; + } + + /* Messages */ + + ul.messagelist li { + padding-left: 55px; + background-position: 30px 12px; + } + + ul.messagelist li.error { + background-position: 30px 12px; + } + + ul.messagelist li.warning { + background-position: 30px 14px; + } + + /* Login */ + + .login #header { + padding: 15px 20px; + } + + .login #branding h1 { + margin: 0; + } + + /* GIS */ + + div.olMap { + max-width: calc(100vw - 30px); + max-height: 300px; + } + + .olMap + .clear_features { + display: block; + margin-top: 10px; + } + + /* Docs */ + + .module table.xfull { + width: 100%; + } + + pre.literal-block { + overflow: auto; + } +} + +/* Mobile */ + +@media (max-width: 767px) { + /* Layout */ + + #header, #content, #footer { + padding: 15px; + } + + #footer:empty { + padding: 0; + } + + div.breadcrumbs { + padding: 10px 15px; + } + + /* Dashboard */ + + .colMS, .colSM { + margin: 0; + } + + #content-related, .colSM #content-related { + width: 100%; + margin: 0; + } + + #content-related .module { + margin-bottom: 0; + } + + #content-related .module h2 { + padding: 10px 15px; + font-size: 1rem; + } + + /* Changelist */ + + #changelist { + align-items: stretch; + flex-direction: column; + } + + #toolbar { + padding: 10px; + } + + #changelist-filter { + margin-left: 0; + } + + #changelist .actions label { + flex: 1 1; + } + + #changelist .actions select { + flex: 1 0; + width: 100%; + } + + #changelist .actions span { + flex: 1 0 100%; + } + + #changelist-filter { + position: static; + width: auto; + margin-top: 30px; + } + + .object-tools { + float: none; + margin: 0 0 15px; + padding: 0; + overflow: hidden; + } + + .object-tools li { + height: auto; + margin-left: 0; + } + + .object-tools li + li { + margin-left: 15px; + } + + /* Forms */ + + .form-row { + padding: 15px 0; + } + + .aligned .form-row, + .aligned .form-row > div { + max-width: 100vw; + } + + .aligned .form-row > div { + width: calc(100vw - 30px); + } + + .flex-container { + flex-flow: column; + } + + .flex-container.checkbox-row { + flex-flow: row; + } + + textarea { + max-width: none; + } + + .vURLField { + width: auto; + } + + fieldset .fieldBox + .fieldBox { + margin-top: 15px; + padding-top: 15px; + } + + fieldset.collapsed .form-row { + display: none; + } + + .aligned label { + width: 100%; + min-width: auto; + padding: 0 0 10px; + } + + .aligned label:after { + max-height: 0; + } + + .aligned .form-row input, + .aligned .form-row select, + .aligned .form-row textarea { + flex: 1 1 auto; + max-width: 100%; + } + + .aligned .checkbox-row input { + flex: 0 1 auto; + margin: 0; + } + + .aligned .vCheckboxLabel { + flex: 1 0; + padding: 1px 0 0 5px; + } + + .aligned label + p, + .aligned label + div.help, + .aligned label + div.readonly { + padding: 0; + margin-left: 0; + } + + .aligned p.file-upload { + font-size: 0.8125rem; + } + + span.clearable-file-input { + margin-left: 15px; + } + + span.clearable-file-input label { + font-size: 0.8125rem; + padding-bottom: 0; + } + + .aligned .timezonewarning { + flex: 1 0 100%; + margin-top: 5px; + } + + form .aligned .form-row div.help { + width: 100%; + margin: 5px 0 0; + padding: 0; + } + + form .aligned ul, + form .aligned ul.errorlist { + margin-left: 0; + padding-left: 0; + } + + form .aligned div.radiolist { + margin-top: 5px; + margin-right: 15px; + margin-bottom: -3px; + } + + form .aligned div.radiolist:not(.inline) div + div { + margin-top: 5px; + } + + /* Related widget */ + + .related-widget-wrapper { + width: 100%; + display: flex; + align-items: flex-start; + } + + .related-widget-wrapper .selector { + order: 1; + } + + .related-widget-wrapper > a { + order: 2; + } + + .related-widget-wrapper .radiolist ~ a { + align-self: flex-end; + } + + .related-widget-wrapper > select ~ a { + align-self: center; + } + + select + .related-widget-wrapper-link, + .related-widget-wrapper-link + .related-widget-wrapper-link { + margin-left: 15px; + } + + /* Selector */ + + .selector { + flex-direction: column; + } + + .selector > * { + float: none; + } + + .selector-available, .selector-chosen { + margin-bottom: 0; + flex: 1 1 auto; + } + + .selector select { + max-height: 96px; + } + + .selector ul.selector-chooser { + display: block; + float: none; + width: 52px; + height: 26px; + padding: 0 2px; + margin: 15px auto 20px; + transform: none; + } + + .selector ul.selector-chooser li { + float: left; + } + + .selector-remove { + background-position: 0 0; + } + + .active.selector-remove:focus, .active.selector-remove:hover { + background-position: 0 -20px; + } + + .selector-add { + background-position: 0 -40px; + } + + .active.selector-add:focus, .active.selector-add:hover { + background-position: 0 -60px; + } + + /* Inlines */ + + .inline-group[data-inline-type="stacked"] .inline-related { + border: 1px solid var(--hairline-color); + border-radius: 4px; + margin-top: 15px; + overflow: auto; + } + + .inline-group[data-inline-type="stacked"] .inline-related > * { + box-sizing: border-box; + } + + .inline-group[data-inline-type="stacked"] .inline-related .module { + padding: 0 10px; + } + + .inline-group[data-inline-type="stacked"] .inline-related .module .form-row { + border-top: 1px solid var(--hairline-color); + border-bottom: none; + } + + .inline-group[data-inline-type="stacked"] .inline-related .module .form-row:first-child { + border-top: none; + } + + .inline-group[data-inline-type="stacked"] .inline-related h3 { + padding: 10px; + border-top-width: 0; + border-bottom-width: 2px; + display: flex; + flex-wrap: wrap; + align-items: center; + } + + .inline-group[data-inline-type="stacked"] .inline-related h3 .inline_label { + margin-right: auto; + } + + .inline-group[data-inline-type="stacked"] .inline-related h3 span.delete { + float: none; + flex: 1 1 100%; + margin-top: 5px; + } + + .inline-group[data-inline-type="stacked"] .aligned .form-row > div:not([class]) { + width: 100%; + } + + .inline-group[data-inline-type="stacked"] .aligned label { + width: 100%; + } + + .inline-group[data-inline-type="stacked"] div.add-row { + margin-top: 15px; + border: 1px solid var(--hairline-color); + border-radius: 4px; + } + + .inline-group div.add-row, + .inline-group .tabular tr.add-row td { + padding: 0; + } + + .inline-group div.add-row a, + .inline-group .tabular tr.add-row td a { + display: block; + padding: 8px 10px 8px 26px; + background-position: 8px 9px; + } + + /* Submit row */ + + .submit-row { + padding: 10px; + margin: 0 0 15px; + flex-direction: column; + gap: 8px; + } + + .submit-row input, .submit-row input.default, .submit-row a { + text-align: center; + } + + .submit-row a.closelink { + padding: 10px 0; + text-align: center; + } + + .submit-row a.deletelink { + margin: 0; + } + + /* Messages */ + + ul.messagelist li { + padding-left: 40px; + background-position: 15px 12px; + } + + ul.messagelist li.error { + background-position: 15px 12px; + } + + ul.messagelist li.warning { + background-position: 15px 14px; + } + + /* Paginator */ + + .paginator .this-page, .paginator a:link, .paginator a:visited { + padding: 4px 10px; + } + + /* Login */ + + body.login { + padding: 0 15px; + } + + .login #container { + width: auto; + max-width: 480px; + margin: 50px auto; + } + + .login #header, + .login #content { + padding: 15px; + } + + .login #content-main { + float: none; + } + + .login .form-row { + padding: 0; + } + + .login .form-row + .form-row { + margin-top: 15px; + } + + .login .form-row label { + margin: 0 0 5px; + line-height: 1.2; + } + + .login .submit-row { + padding: 15px 0 0; + } + + .login br { + display: none; + } + + .login .submit-row input { + margin: 0; + text-transform: uppercase; + } + + .errornote { + margin: 0 0 20px; + padding: 8px 12px; + font-size: 0.8125rem; + } + + /* Calendar and clock */ + + .calendarbox, .clockbox { + position: fixed !important; + top: 50% !important; + left: 50% !important; + transform: translate(-50%, -50%); + margin: 0; + border: none; + overflow: visible; + } + + .calendarbox:before, .clockbox:before { + content: ''; + position: fixed; + top: 50%; + left: 50%; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.75); + transform: translate(-50%, -50%); + } + + .calendarbox > *, .clockbox > * { + position: relative; + z-index: 1; + } + + .calendarbox > div:first-child { + z-index: 2; + } + + .calendarbox .calendar, .clockbox h2 { + border-radius: 4px 4px 0 0; + overflow: hidden; + } + + .calendarbox .calendar-cancel, .clockbox .calendar-cancel { + border-radius: 0 0 4px 4px; + overflow: hidden; + } + + .calendar-shortcuts { + padding: 10px 0; + font-size: 0.75rem; + line-height: 0.75rem; + } + + .calendar-shortcuts a { + margin: 0 4px; + } + + .timelist a { + background: var(--body-bg); + padding: 4px; + } + + .calendar-cancel { + padding: 8px 10px; + } + + .clockbox h2 { + padding: 8px 15px; + } + + .calendar caption { + padding: 10px; + } + + .calendarbox .calendarnav-previous, .calendarbox .calendarnav-next { + z-index: 1; + top: 10px; + } + + /* History */ + + table#change-history tbody th, table#change-history tbody td { + font-size: 0.8125rem; + word-break: break-word; + } + + table#change-history tbody th { + width: auto; + } + + /* Docs */ + + table.model tbody th, table.model tbody td { + font-size: 0.8125rem; + word-break: break-word; + } +} diff --git a/static/admin/css/responsive_rtl.css b/static/admin/css/responsive_rtl.css new file mode 100644 index 0000000..31dc8ff --- /dev/null +++ b/static/admin/css/responsive_rtl.css @@ -0,0 +1,84 @@ +/* TABLETS */ + +@media (max-width: 1024px) { + [dir="rtl"] .colMS { + margin-right: 0; + } + + [dir="rtl"] #user-tools { + text-align: right; + } + + [dir="rtl"] #changelist .actions label { + padding-left: 10px; + padding-right: 0; + } + + [dir="rtl"] #changelist .actions select { + margin-left: 0; + margin-right: 15px; + } + + [dir="rtl"] .change-list .filtered .results, + [dir="rtl"] .change-list .filtered .paginator, + [dir="rtl"] .filtered #toolbar, + [dir="rtl"] .filtered div.xfull, + [dir="rtl"] .filtered .actions, + [dir="rtl"] #changelist-filter { + margin-left: 0; + } + + [dir="rtl"] .inline-group ul.tools a.add, + [dir="rtl"] .inline-group div.add-row a, + [dir="rtl"] .inline-group .tabular tr.add-row td a { + padding: 8px 26px 8px 10px; + background-position: calc(100% - 8px) 9px; + } + + [dir="rtl"] .related-widget-wrapper-link + .selector { + margin-right: 0; + margin-left: 15px; + } + + [dir="rtl"] .selector .selector-filter label { + margin-right: 0; + margin-left: 8px; + } + + [dir="rtl"] .object-tools li { + float: right; + } + + [dir="rtl"] .object-tools li + li { + margin-left: 0; + margin-right: 15px; + } + + [dir="rtl"] .dashboard .module table td a { + padding-left: 0; + padding-right: 16px; + } +} + +/* MOBILE */ + +@media (max-width: 767px) { + [dir="rtl"] .aligned .related-lookup, + [dir="rtl"] .aligned .datetimeshortcuts { + margin-left: 0; + margin-right: 15px; + } + + [dir="rtl"] .aligned ul, + [dir="rtl"] form .aligned ul.errorlist { + margin-right: 0; + } + + [dir="rtl"] #changelist-filter { + margin-left: 0; + margin-right: 0; + } + [dir="rtl"] .aligned .vCheckboxLabel { + padding: 1px 5px 0 0; + } +} diff --git a/static/admin/css/rtl.css b/static/admin/css/rtl.css new file mode 100644 index 0000000..c349a93 --- /dev/null +++ b/static/admin/css/rtl.css @@ -0,0 +1,298 @@ +/* GLOBAL */ + +th { + text-align: right; +} + +.module h2, .module caption { + text-align: right; +} + +.module ul, .module ol { + margin-left: 0; + margin-right: 1.5em; +} + +.viewlink, .addlink, .changelink { + padding-left: 0; + padding-right: 16px; + background-position: 100% 1px; +} + +.deletelink { + padding-left: 0; + padding-right: 16px; + background-position: 100% 1px; +} + +.object-tools { + float: left; +} + +thead th:first-child, +tfoot td:first-child { + border-left: none; +} + +/* LAYOUT */ + +#user-tools { + right: auto; + left: 0; + text-align: left; +} + +div.breadcrumbs { + text-align: right; +} + +#content-main { + float: right; +} + +#content-related { + float: left; + margin-left: -300px; + margin-right: auto; +} + +.colMS { + margin-left: 300px; + margin-right: 0; +} + +/* SORTABLE TABLES */ + +table thead th.sorted .sortoptions { + float: left; +} + +thead th.sorted .text { + padding-right: 0; + padding-left: 42px; +} + +/* dashboard styles */ + +.dashboard .module table td a { + padding-left: .6em; + padding-right: 16px; +} + +/* changelists styles */ + +.change-list .filtered table { + border-left: none; + border-right: 0px none; +} + +#changelist-filter { + border-left: none; + border-right: none; + margin-left: 0; + margin-right: 30px; +} + +#changelist-filter li.selected { + border-left: none; + padding-left: 10px; + margin-left: 0; + border-right: 5px solid var(--hairline-color); + padding-right: 10px; + margin-right: -15px; +} + +#changelist table tbody td:first-child, #changelist table tbody th:first-child { + border-right: none; + border-left: none; +} + +.paginator .end { + margin-left: 6px; + margin-right: 0; +} + +.paginator input { + margin-left: 0; + margin-right: auto; +} + +/* FORMS */ + +.aligned label { + padding: 0 0 3px 1em; +} + +.submit-row a.deletelink { + margin-left: 0; + margin-right: auto; +} + +.vDateField, .vTimeField { + margin-left: 2px; +} + +.aligned .form-row input { + margin-left: 5px; +} + +form .aligned ul { + margin-right: 163px; + padding-right: 10px; + margin-left: 0; + padding-left: 0; +} + +form ul.inline li { + float: right; + padding-right: 0; + padding-left: 7px; +} + +form .aligned p.help, +form .aligned div.help { + margin-right: 160px; + padding-right: 10px; +} + +form div.help ul, +form .aligned .checkbox-row + .help, +form .aligned p.date div.help.timezonewarning, +form .aligned p.datetime div.help.timezonewarning, +form .aligned p.time div.help.timezonewarning { + margin-right: 0; + padding-right: 0; +} + +form .wide p.help, form .wide div.help { + padding-left: 0; + padding-right: 50px; +} + +form .wide p, +form .wide ul.errorlist, +form .wide input + p.help, +form .wide input + div.help { + margin-right: 200px; + margin-left: 0px; +} + +.submit-row { + text-align: right; +} + +fieldset .fieldBox { + margin-left: 20px; + margin-right: 0; +} + +.errorlist li { + background-position: 100% 12px; + padding: 0; +} + +.errornote { + background-position: 100% 12px; + padding: 10px 12px; +} + +/* WIDGETS */ + +.calendarnav-previous { + top: 0; + left: auto; + right: 10px; + background: url(../img/calendar-icons.svg) 0 -30px no-repeat; +} + +.calendarbox .calendarnav-previous:focus, +.calendarbox .calendarnav-previous:hover { + background-position: 0 -45px; +} + +.calendarnav-next { + top: 0; + right: auto; + left: 10px; + background: url(../img/calendar-icons.svg) 0 0 no-repeat; +} + +.calendarbox .calendarnav-next:focus, +.calendarbox .calendarnav-next:hover { + background-position: 0 -15px; +} + +.calendar caption, .calendarbox h2 { + text-align: center; +} + +.selector { + float: right; +} + +.selector .selector-filter { + text-align: right; +} + +.selector-add { + background: url(../img/selector-icons.svg) 0 -64px no-repeat; +} + +.active.selector-add:focus, .active.selector-add:hover { + background-position: 0 -80px; +} + +.selector-remove { + background: url(../img/selector-icons.svg) 0 -96px no-repeat; +} + +.active.selector-remove:focus, .active.selector-remove:hover { + background-position: 0 -112px; +} + +a.selector-chooseall { + background: url(../img/selector-icons.svg) right -128px no-repeat; +} + +a.active.selector-chooseall:focus, a.active.selector-chooseall:hover { + background-position: 100% -144px; +} + +a.selector-clearall { + background: url(../img/selector-icons.svg) 0 -160px no-repeat; +} + +a.active.selector-clearall:focus, a.active.selector-clearall:hover { + background-position: 0 -176px; +} + +.inline-deletelink { + float: left; +} + +form .form-row p.datetime { + overflow: hidden; +} + +.related-widget-wrapper { + float: right; +} + +/* MISC */ + +.inline-related h2, .inline-group h2 { + text-align: right +} + +.inline-related h3 span.delete { + padding-right: 20px; + padding-left: inherit; + left: 10px; + right: inherit; + float:left; +} + +.inline-related h3 span.delete label { + margin-left: inherit; + margin-right: 2px; +} diff --git a/static/admin/css/vendor/select2/LICENSE-SELECT2.md b/static/admin/css/vendor/select2/LICENSE-SELECT2.md new file mode 100644 index 0000000..8cb8a2b --- /dev/null +++ b/static/admin/css/vendor/select2/LICENSE-SELECT2.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2012-2017 Kevin Brown, Igor Vaynberg, and Select2 contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/static/admin/css/vendor/select2/select2.css b/static/admin/css/vendor/select2/select2.css new file mode 100644 index 0000000..750b320 --- /dev/null +++ b/static/admin/css/vendor/select2/select2.css @@ -0,0 +1,481 @@ +.select2-container { + box-sizing: border-box; + display: inline-block; + margin: 0; + position: relative; + vertical-align: middle; } + .select2-container .select2-selection--single { + box-sizing: border-box; + cursor: pointer; + display: block; + height: 28px; + user-select: none; + -webkit-user-select: none; } + .select2-container .select2-selection--single .select2-selection__rendered { + display: block; + padding-left: 8px; + padding-right: 20px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } + .select2-container .select2-selection--single .select2-selection__clear { + position: relative; } + .select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered { + padding-right: 8px; + padding-left: 20px; } + .select2-container .select2-selection--multiple { + box-sizing: border-box; + cursor: pointer; + display: block; + min-height: 32px; + user-select: none; + -webkit-user-select: none; } + .select2-container .select2-selection--multiple .select2-selection__rendered { + display: inline-block; + overflow: hidden; + padding-left: 8px; + text-overflow: ellipsis; + white-space: nowrap; } + .select2-container .select2-search--inline { + float: left; } + .select2-container .select2-search--inline .select2-search__field { + box-sizing: border-box; + border: none; + font-size: 100%; + margin-top: 5px; + padding: 0; } + .select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button { + -webkit-appearance: none; } + +.select2-dropdown { + background-color: white; + border: 1px solid #aaa; + border-radius: 4px; + box-sizing: border-box; + display: block; + position: absolute; + left: -100000px; + width: 100%; + z-index: 1051; } + +.select2-results { + display: block; } + +.select2-results__options { + list-style: none; + margin: 0; + padding: 0; } + +.select2-results__option { + padding: 6px; + user-select: none; + -webkit-user-select: none; } + .select2-results__option[aria-selected] { + cursor: pointer; } + +.select2-container--open .select2-dropdown { + left: 0; } + +.select2-container--open .select2-dropdown--above { + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } + +.select2-container--open .select2-dropdown--below { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; } + +.select2-search--dropdown { + display: block; + padding: 4px; } + .select2-search--dropdown .select2-search__field { + padding: 4px; + width: 100%; + box-sizing: border-box; } + .select2-search--dropdown .select2-search__field::-webkit-search-cancel-button { + -webkit-appearance: none; } + .select2-search--dropdown.select2-search--hide { + display: none; } + +.select2-close-mask { + border: 0; + margin: 0; + padding: 0; + display: block; + position: fixed; + left: 0; + top: 0; + min-height: 100%; + min-width: 100%; + height: auto; + width: auto; + opacity: 0; + z-index: 99; + background-color: #fff; + filter: alpha(opacity=0); } + +.select2-hidden-accessible { + border: 0 !important; + clip: rect(0 0 0 0) !important; + -webkit-clip-path: inset(50%) !important; + clip-path: inset(50%) !important; + height: 1px !important; + overflow: hidden !important; + padding: 0 !important; + position: absolute !important; + width: 1px !important; + white-space: nowrap !important; } + +.select2-container--default .select2-selection--single { + background-color: #fff; + border: 1px solid #aaa; + border-radius: 4px; } + .select2-container--default .select2-selection--single .select2-selection__rendered { + color: #444; + line-height: 28px; } + .select2-container--default .select2-selection--single .select2-selection__clear { + cursor: pointer; + float: right; + font-weight: bold; } + .select2-container--default .select2-selection--single .select2-selection__placeholder { + color: #999; } + .select2-container--default .select2-selection--single .select2-selection__arrow { + height: 26px; + position: absolute; + top: 1px; + right: 1px; + width: 20px; } + .select2-container--default .select2-selection--single .select2-selection__arrow b { + border-color: #888 transparent transparent transparent; + border-style: solid; + border-width: 5px 4px 0 4px; + height: 0; + left: 50%; + margin-left: -4px; + margin-top: -2px; + position: absolute; + top: 50%; + width: 0; } + +.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear { + float: left; } + +.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow { + left: 1px; + right: auto; } + +.select2-container--default.select2-container--disabled .select2-selection--single { + background-color: #eee; + cursor: default; } + .select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear { + display: none; } + +.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b { + border-color: transparent transparent #888 transparent; + border-width: 0 4px 5px 4px; } + +.select2-container--default .select2-selection--multiple { + background-color: white; + border: 1px solid #aaa; + border-radius: 4px; + cursor: text; } + .select2-container--default .select2-selection--multiple .select2-selection__rendered { + box-sizing: border-box; + list-style: none; + margin: 0; + padding: 0 5px; + width: 100%; } + .select2-container--default .select2-selection--multiple .select2-selection__rendered li { + list-style: none; } + .select2-container--default .select2-selection--multiple .select2-selection__clear { + cursor: pointer; + float: right; + font-weight: bold; + margin-top: 5px; + margin-right: 10px; + padding: 1px; } + .select2-container--default .select2-selection--multiple .select2-selection__choice { + background-color: #e4e4e4; + border: 1px solid #aaa; + border-radius: 4px; + cursor: default; + float: left; + margin-right: 5px; + margin-top: 5px; + padding: 0 5px; } + .select2-container--default .select2-selection--multiple .select2-selection__choice__remove { + color: #999; + cursor: pointer; + display: inline-block; + font-weight: bold; + margin-right: 2px; } + .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover { + color: #333; } + +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline { + float: right; } + +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice { + margin-left: 5px; + margin-right: auto; } + +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove { + margin-left: 2px; + margin-right: auto; } + +.select2-container--default.select2-container--focus .select2-selection--multiple { + border: solid black 1px; + outline: 0; } + +.select2-container--default.select2-container--disabled .select2-selection--multiple { + background-color: #eee; + cursor: default; } + +.select2-container--default.select2-container--disabled .select2-selection__choice__remove { + display: none; } + +.select2-container--default.select2-container--open.select2-container--above .select2-selection--single, .select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple { + border-top-left-radius: 0; + border-top-right-radius: 0; } + +.select2-container--default.select2-container--open.select2-container--below .select2-selection--single, .select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } + +.select2-container--default .select2-search--dropdown .select2-search__field { + border: 1px solid #aaa; } + +.select2-container--default .select2-search--inline .select2-search__field { + background: transparent; + border: none; + outline: 0; + box-shadow: none; + -webkit-appearance: textfield; } + +.select2-container--default .select2-results > .select2-results__options { + max-height: 200px; + overflow-y: auto; } + +.select2-container--default .select2-results__option[role=group] { + padding: 0; } + +.select2-container--default .select2-results__option[aria-disabled=true] { + color: #999; } + +.select2-container--default .select2-results__option[aria-selected=true] { + background-color: #ddd; } + +.select2-container--default .select2-results__option .select2-results__option { + padding-left: 1em; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__group { + padding-left: 0; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__option { + margin-left: -1em; + padding-left: 2em; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -2em; + padding-left: 3em; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -3em; + padding-left: 4em; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -4em; + padding-left: 5em; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -5em; + padding-left: 6em; } + +.select2-container--default .select2-results__option--highlighted[aria-selected] { + background-color: #5897fb; + color: white; } + +.select2-container--default .select2-results__group { + cursor: default; + display: block; + padding: 6px; } + +.select2-container--classic .select2-selection--single { + background-color: #f7f7f7; + border: 1px solid #aaa; + border-radius: 4px; + outline: 0; + background-image: -webkit-linear-gradient(top, white 50%, #eeeeee 100%); + background-image: -o-linear-gradient(top, white 50%, #eeeeee 100%); + background-image: linear-gradient(to bottom, white 50%, #eeeeee 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); } + .select2-container--classic .select2-selection--single:focus { + border: 1px solid #5897fb; } + .select2-container--classic .select2-selection--single .select2-selection__rendered { + color: #444; + line-height: 28px; } + .select2-container--classic .select2-selection--single .select2-selection__clear { + cursor: pointer; + float: right; + font-weight: bold; + margin-right: 10px; } + .select2-container--classic .select2-selection--single .select2-selection__placeholder { + color: #999; } + .select2-container--classic .select2-selection--single .select2-selection__arrow { + background-color: #ddd; + border: none; + border-left: 1px solid #aaa; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + height: 26px; + position: absolute; + top: 1px; + right: 1px; + width: 20px; + background-image: -webkit-linear-gradient(top, #eeeeee 50%, #cccccc 100%); + background-image: -o-linear-gradient(top, #eeeeee 50%, #cccccc 100%); + background-image: linear-gradient(to bottom, #eeeeee 50%, #cccccc 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0); } + .select2-container--classic .select2-selection--single .select2-selection__arrow b { + border-color: #888 transparent transparent transparent; + border-style: solid; + border-width: 5px 4px 0 4px; + height: 0; + left: 50%; + margin-left: -4px; + margin-top: -2px; + position: absolute; + top: 50%; + width: 0; } + +.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear { + float: left; } + +.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow { + border: none; + border-right: 1px solid #aaa; + border-radius: 0; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + left: 1px; + right: auto; } + +.select2-container--classic.select2-container--open .select2-selection--single { + border: 1px solid #5897fb; } + .select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow { + background: transparent; + border: none; } + .select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b { + border-color: transparent transparent #888 transparent; + border-width: 0 4px 5px 4px; } + +.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; + background-image: -webkit-linear-gradient(top, white 0%, #eeeeee 50%); + background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%); + background-image: linear-gradient(to bottom, white 0%, #eeeeee 50%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); } + +.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single { + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + background-image: -webkit-linear-gradient(top, #eeeeee 50%, white 100%); + background-image: -o-linear-gradient(top, #eeeeee 50%, white 100%); + background-image: linear-gradient(to bottom, #eeeeee 50%, white 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0); } + +.select2-container--classic .select2-selection--multiple { + background-color: white; + border: 1px solid #aaa; + border-radius: 4px; + cursor: text; + outline: 0; } + .select2-container--classic .select2-selection--multiple:focus { + border: 1px solid #5897fb; } + .select2-container--classic .select2-selection--multiple .select2-selection__rendered { + list-style: none; + margin: 0; + padding: 0 5px; } + .select2-container--classic .select2-selection--multiple .select2-selection__clear { + display: none; } + .select2-container--classic .select2-selection--multiple .select2-selection__choice { + background-color: #e4e4e4; + border: 1px solid #aaa; + border-radius: 4px; + cursor: default; + float: left; + margin-right: 5px; + margin-top: 5px; + padding: 0 5px; } + .select2-container--classic .select2-selection--multiple .select2-selection__choice__remove { + color: #888; + cursor: pointer; + display: inline-block; + font-weight: bold; + margin-right: 2px; } + .select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover { + color: #555; } + +.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice { + float: right; + margin-left: 5px; + margin-right: auto; } + +.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove { + margin-left: 2px; + margin-right: auto; } + +.select2-container--classic.select2-container--open .select2-selection--multiple { + border: 1px solid #5897fb; } + +.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; } + +.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple { + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } + +.select2-container--classic .select2-search--dropdown .select2-search__field { + border: 1px solid #aaa; + outline: 0; } + +.select2-container--classic .select2-search--inline .select2-search__field { + outline: 0; + box-shadow: none; } + +.select2-container--classic .select2-dropdown { + background-color: white; + border: 1px solid transparent; } + +.select2-container--classic .select2-dropdown--above { + border-bottom: none; } + +.select2-container--classic .select2-dropdown--below { + border-top: none; } + +.select2-container--classic .select2-results > .select2-results__options { + max-height: 200px; + overflow-y: auto; } + +.select2-container--classic .select2-results__option[role=group] { + padding: 0; } + +.select2-container--classic .select2-results__option[aria-disabled=true] { + color: grey; } + +.select2-container--classic .select2-results__option--highlighted[aria-selected] { + background-color: #3875d7; + color: white; } + +.select2-container--classic .select2-results__group { + cursor: default; + display: block; + padding: 6px; } + +.select2-container--classic.select2-container--open .select2-dropdown { + border-color: #5897fb; } diff --git a/static/admin/css/vendor/select2/select2.min.css b/static/admin/css/vendor/select2/select2.min.css new file mode 100644 index 0000000..7c18ad5 --- /dev/null +++ b/static/admin/css/vendor/select2/select2.min.css @@ -0,0 +1 @@ +.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{position:relative}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline-block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-search--inline{float:left}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;padding:0}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option[aria-selected]{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0)}.select2-hidden-accessible{border:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(50%) !important;clip-path:inset(50%) !important;height:1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important;white-space:nowrap !important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text}.select2-container--default .select2-selection--multiple .select2-selection__rendered{box-sizing:border-box;list-style:none;margin:0;padding:0 5px;width:100%}.select2-container--default .select2-selection--multiple .select2-selection__rendered li{list-style:none}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-top:5px;margin-right:10px;padding:1px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:#999;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#333}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline{float:right}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid black 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--single,.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--single,.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:transparent;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option[role=group]{padding:0}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-right:10px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__rendered{list-style:none;margin:0;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{color:#888;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{float:right;margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option[role=group]{padding:0}.select2-container--classic .select2-results__option[aria-disabled=true]{color:grey}.select2-container--classic .select2-results__option--highlighted[aria-selected]{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb} diff --git a/static/admin/css/widgets.css b/static/admin/css/widgets.css new file mode 100644 index 0000000..1104e8b --- /dev/null +++ b/static/admin/css/widgets.css @@ -0,0 +1,604 @@ +/* SELECTOR (FILTER INTERFACE) */ + +.selector { + width: 800px; + float: left; + display: flex; +} + +.selector select { + width: 380px; + height: 17.2em; + flex: 1 0 auto; +} + +.selector-available, .selector-chosen { + width: 380px; + text-align: center; + margin-bottom: 5px; + display: flex; + flex-direction: column; +} + +.selector-available h2, .selector-chosen h2 { + border: 1px solid var(--border-color); + border-radius: 4px 4px 0 0; +} + +.selector-chosen .list-footer-display { + border: 1px solid var(--border-color); + border-top: none; + border-radius: 0 0 4px 4px; + margin: 0 0 10px; + padding: 8px; + text-align: center; + background: var(--primary); + color: var(--header-link-color); + cursor: pointer; +} +.selector-chosen .list-footer-display__clear { + color: var(--breadcrumbs-fg); +} + +.selector-chosen h2 { + background: var(--primary); + color: var(--header-link-color); +} + +.selector .selector-available h2 { + background: var(--darkened-bg); + color: var(--body-quiet-color); +} + +.selector .selector-filter { + border: 1px solid var(--border-color); + border-width: 0 1px; + padding: 8px; + color: var(--body-quiet-color); + font-size: 0.625rem; + margin: 0; + text-align: left; +} + +.selector .selector-filter label, +.inline-group .aligned .selector .selector-filter label { + float: left; + margin: 7px 0 0; + width: 18px; + height: 18px; + padding: 0; + overflow: hidden; + line-height: 1; + min-width: auto; +} + +.selector .selector-available input, +.selector .selector-chosen input { + width: 320px; + margin-left: 8px; +} + +.selector ul.selector-chooser { + align-self: center; + width: 22px; + background-color: var(--selected-bg); + border-radius: 10px; + margin: 0 5px; + padding: 0; + transform: translateY(-17px); +} + +.selector-chooser li { + margin: 0; + padding: 3px; + list-style-type: none; +} + +.selector select { + padding: 0 10px; + margin: 0 0 10px; + border-radius: 0 0 4px 4px; +} +.selector .selector-chosen--with-filtered select { + margin: 0; + border-radius: 0; + height: 14em; +} + +.selector .selector-chosen:not(.selector-chosen--with-filtered) .list-footer-display { + display: none; +} + +.selector-add, .selector-remove { + width: 16px; + height: 16px; + display: block; + text-indent: -3000px; + overflow: hidden; + cursor: default; + opacity: 0.55; +} + +.active.selector-add, .active.selector-remove { + opacity: 1; +} + +.active.selector-add:hover, .active.selector-remove:hover { + cursor: pointer; +} + +.selector-add { + background: url(../img/selector-icons.svg) 0 -96px no-repeat; +} + +.active.selector-add:focus, .active.selector-add:hover { + background-position: 0 -112px; +} + +.selector-remove { + background: url(../img/selector-icons.svg) 0 -64px no-repeat; +} + +.active.selector-remove:focus, .active.selector-remove:hover { + background-position: 0 -80px; +} + +a.selector-chooseall, a.selector-clearall { + display: inline-block; + height: 16px; + text-align: left; + margin: 1px auto 3px; + overflow: hidden; + font-weight: bold; + line-height: 16px; + color: var(--body-quiet-color); + text-decoration: none; + opacity: 0.55; +} + +a.active.selector-chooseall:focus, a.active.selector-clearall:focus, +a.active.selector-chooseall:hover, a.active.selector-clearall:hover { + color: var(--link-fg); +} + +a.active.selector-chooseall, a.active.selector-clearall { + opacity: 1; +} + +a.active.selector-chooseall:hover, a.active.selector-clearall:hover { + cursor: pointer; +} + +a.selector-chooseall { + padding: 0 18px 0 0; + background: url(../img/selector-icons.svg) right -160px no-repeat; + cursor: default; +} + +a.active.selector-chooseall:focus, a.active.selector-chooseall:hover { + background-position: 100% -176px; +} + +a.selector-clearall { + padding: 0 0 0 18px; + background: url(../img/selector-icons.svg) 0 -128px no-repeat; + cursor: default; +} + +a.active.selector-clearall:focus, a.active.selector-clearall:hover { + background-position: 0 -144px; +} + +/* STACKED SELECTORS */ + +.stacked { + float: left; + width: 490px; + display: block; +} + +.stacked select { + width: 480px; + height: 10.1em; +} + +.stacked .selector-available, .stacked .selector-chosen { + width: 480px; +} + +.stacked .selector-available { + margin-bottom: 0; +} + +.stacked .selector-available input { + width: 422px; +} + +.stacked ul.selector-chooser { + height: 22px; + width: 50px; + margin: 0 0 10px 40%; + background-color: #eee; + border-radius: 10px; + transform: none; +} + +.stacked .selector-chooser li { + float: left; + padding: 3px 3px 3px 5px; +} + +.stacked .selector-chooseall, .stacked .selector-clearall { + display: none; +} + +.stacked .selector-add { + background: url(../img/selector-icons.svg) 0 -32px no-repeat; + cursor: default; +} + +.stacked .active.selector-add { + background-position: 0 -32px; + cursor: pointer; +} + +.stacked .active.selector-add:focus, .stacked .active.selector-add:hover { + background-position: 0 -48px; + cursor: pointer; +} + +.stacked .selector-remove { + background: url(../img/selector-icons.svg) 0 0 no-repeat; + cursor: default; +} + +.stacked .active.selector-remove { + background-position: 0 0px; + cursor: pointer; +} + +.stacked .active.selector-remove:focus, .stacked .active.selector-remove:hover { + background-position: 0 -16px; + cursor: pointer; +} + +.selector .help-icon { + background: url(../img/icon-unknown.svg) 0 0 no-repeat; + display: inline-block; + vertical-align: middle; + margin: -2px 0 0 2px; + width: 13px; + height: 13px; +} + +.selector .selector-chosen .help-icon { + background: url(../img/icon-unknown-alt.svg) 0 0 no-repeat; +} + +.selector .search-label-icon { + background: url(../img/search.svg) 0 0 no-repeat; + display: inline-block; + height: 1.125rem; + width: 1.125rem; +} + +/* DATE AND TIME */ + +p.datetime { + line-height: 20px; + margin: 0; + padding: 0; + color: var(--body-quiet-color); + font-weight: bold; +} + +.datetime span { + white-space: nowrap; + font-weight: normal; + font-size: 0.6875rem; + color: var(--body-quiet-color); +} + +.datetime input, .form-row .datetime input.vDateField, .form-row .datetime input.vTimeField { + margin-left: 5px; + margin-bottom: 4px; +} + +table p.datetime { + font-size: 0.6875rem; + margin-left: 0; + padding-left: 0; +} + +.datetimeshortcuts .clock-icon, .datetimeshortcuts .date-icon { + position: relative; + display: inline-block; + vertical-align: middle; + height: 16px; + width: 16px; + overflow: hidden; +} + +.datetimeshortcuts .clock-icon { + background: url(../img/icon-clock.svg) 0 0 no-repeat; +} + +.datetimeshortcuts a:focus .clock-icon, +.datetimeshortcuts a:hover .clock-icon { + background-position: 0 -16px; +} + +.datetimeshortcuts .date-icon { + background: url(../img/icon-calendar.svg) 0 0 no-repeat; + top: -1px; +} + +.datetimeshortcuts a:focus .date-icon, +.datetimeshortcuts a:hover .date-icon { + background-position: 0 -16px; +} + +.timezonewarning { + font-size: 0.6875rem; + color: var(--body-quiet-color); +} + +/* URL */ + +p.url { + line-height: 20px; + margin: 0; + padding: 0; + color: var(--body-quiet-color); + font-size: 0.6875rem; + font-weight: bold; +} + +.url a { + font-weight: normal; +} + +/* FILE UPLOADS */ + +p.file-upload { + line-height: 20px; + margin: 0; + padding: 0; + color: var(--body-quiet-color); + font-size: 0.6875rem; + font-weight: bold; +} + +.file-upload a { + font-weight: normal; +} + +.file-upload .deletelink { + margin-left: 5px; +} + +span.clearable-file-input label { + color: var(--body-fg); + font-size: 0.6875rem; + display: inline; + float: none; +} + +/* CALENDARS & CLOCKS */ + +.calendarbox, .clockbox { + margin: 5px auto; + font-size: 0.75rem; + width: 19em; + text-align: center; + background: var(--body-bg); + color: var(--body-fg); + border: 1px solid var(--hairline-color); + border-radius: 4px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); + overflow: hidden; + position: relative; +} + +.clockbox { + width: auto; +} + +.calendar { + margin: 0; + padding: 0; +} + +.calendar table { + margin: 0; + padding: 0; + border-collapse: collapse; + background: white; + width: 100%; +} + +.calendar caption, .calendarbox h2 { + margin: 0; + text-align: center; + border-top: none; + font-weight: 700; + font-size: 0.75rem; + color: #333; + background: var(--accent); +} + +.calendar th { + padding: 8px 5px; + background: var(--darkened-bg); + border-bottom: 1px solid var(--border-color); + font-weight: 400; + font-size: 0.75rem; + text-align: center; + color: var(--body-quiet-color); +} + +.calendar td { + font-weight: 400; + font-size: 0.75rem; + text-align: center; + padding: 0; + border-top: 1px solid var(--hairline-color); + border-bottom: none; +} + +.calendar td.selected a { + background: var(--primary); + color: var(--button-fg); +} + +.calendar td.nonday { + background: var(--darkened-bg); +} + +.calendar td.today a { + font-weight: 700; +} + +.calendar td a, .timelist a { + display: block; + font-weight: 400; + padding: 6px; + text-decoration: none; + color: var(--body-quiet-color); +} + +.calendar td a:focus, .timelist a:focus, +.calendar td a:hover, .timelist a:hover { + background: var(--primary); + color: white; +} + +.calendar td a:active, .timelist a:active { + background: var(--header-bg); + color: white; +} + +.calendarnav { + font-size: 0.625rem; + text-align: center; + color: #ccc; + margin: 0; + padding: 1px 3px; +} + +.calendarnav a:link, #calendarnav a:visited, +#calendarnav a:focus, #calendarnav a:hover { + color: var(--body-quiet-color); +} + +.calendar-shortcuts { + background: var(--body-bg); + color: var(--body-quiet-color); + font-size: 0.6875rem; + line-height: 0.6875rem; + border-top: 1px solid var(--hairline-color); + padding: 8px 0; +} + +.calendarbox .calendarnav-previous, .calendarbox .calendarnav-next { + display: block; + position: absolute; + top: 8px; + width: 15px; + height: 15px; + text-indent: -9999px; + padding: 0; +} + +.calendarnav-previous { + left: 10px; + background: url(../img/calendar-icons.svg) 0 0 no-repeat; +} + +.calendarbox .calendarnav-previous:focus, +.calendarbox .calendarnav-previous:hover { + background-position: 0 -15px; +} + +.calendarnav-next { + right: 10px; + background: url(../img/calendar-icons.svg) 0 -30px no-repeat; +} + +.calendarbox .calendarnav-next:focus, +.calendarbox .calendarnav-next:hover { + background-position: 0 -45px; +} + +.calendar-cancel { + margin: 0; + padding: 4px 0; + font-size: 0.75rem; + background: #eee; + border-top: 1px solid var(--border-color); + color: var(--body-fg); +} + +.calendar-cancel:focus, .calendar-cancel:hover { + background: #ddd; +} + +.calendar-cancel a { + color: black; + display: block; +} + +ul.timelist, .timelist li { + list-style-type: none; + margin: 0; + padding: 0; +} + +.timelist a { + padding: 2px; +} + +/* EDIT INLINE */ + +.inline-deletelink { + float: right; + text-indent: -9999px; + background: url(../img/inline-delete.svg) 0 0 no-repeat; + width: 16px; + height: 16px; + border: 0px none; +} + +.inline-deletelink:focus, .inline-deletelink:hover { + cursor: pointer; +} + +/* RELATED WIDGET WRAPPER */ +.related-widget-wrapper { + float: left; /* display properly in form rows with multiple fields */ + overflow: hidden; /* clear floated contents */ +} + +.related-widget-wrapper-link { + opacity: 0.3; +} + +.related-widget-wrapper-link:link { + opacity: .8; +} + +.related-widget-wrapper-link:link:focus, +.related-widget-wrapper-link:link:hover { + opacity: 1; +} + +select + .related-widget-wrapper-link, +.related-widget-wrapper-link + .related-widget-wrapper-link { + margin-left: 7px; +} + +/* GIS MAPS */ +.dj_map { + width: 600px; + height: 400px; +} diff --git a/static/admin/img/LICENSE b/static/admin/img/LICENSE new file mode 100644 index 0000000..a4faaa1 --- /dev/null +++ b/static/admin/img/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 Code Charm Ltd + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/static/admin/img/README.txt b/static/admin/img/README.txt new file mode 100644 index 0000000..4eb2e49 --- /dev/null +++ b/static/admin/img/README.txt @@ -0,0 +1,7 @@ +All icons are taken from Font Awesome (http://fontawesome.io/) project. +The Font Awesome font is licensed under the SIL OFL 1.1: +- https://scripts.sil.org/OFL + +SVG icons source: https://github.com/encharm/Font-Awesome-SVG-PNG +Font-Awesome-SVG-PNG is licensed under the MIT license (see file license +in current folder). diff --git a/static/admin/img/calendar-icons.svg b/static/admin/img/calendar-icons.svg new file mode 100644 index 0000000..dbf21c3 --- /dev/null +++ b/static/admin/img/calendar-icons.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/static/admin/img/gis/move_vertex_off.svg b/static/admin/img/gis/move_vertex_off.svg new file mode 100644 index 0000000..228854f --- /dev/null +++ b/static/admin/img/gis/move_vertex_off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/admin/img/gis/move_vertex_on.svg b/static/admin/img/gis/move_vertex_on.svg new file mode 100644 index 0000000..96b87fd --- /dev/null +++ b/static/admin/img/gis/move_vertex_on.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/admin/img/icon-addlink.svg b/static/admin/img/icon-addlink.svg new file mode 100644 index 0000000..e004fb1 --- /dev/null +++ b/static/admin/img/icon-addlink.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/admin/img/icon-alert.svg b/static/admin/img/icon-alert.svg new file mode 100644 index 0000000..e51ea83 --- /dev/null +++ b/static/admin/img/icon-alert.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/admin/img/icon-calendar.svg b/static/admin/img/icon-calendar.svg new file mode 100644 index 0000000..97910a9 --- /dev/null +++ b/static/admin/img/icon-calendar.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/static/admin/img/icon-changelink.svg b/static/admin/img/icon-changelink.svg new file mode 100644 index 0000000..bbb137a --- /dev/null +++ b/static/admin/img/icon-changelink.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/admin/img/icon-clock.svg b/static/admin/img/icon-clock.svg new file mode 100644 index 0000000..bf9985d --- /dev/null +++ b/static/admin/img/icon-clock.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/static/admin/img/icon-deletelink.svg b/static/admin/img/icon-deletelink.svg new file mode 100644 index 0000000..4059b15 --- /dev/null +++ b/static/admin/img/icon-deletelink.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/admin/img/icon-no.svg b/static/admin/img/icon-no.svg new file mode 100644 index 0000000..2e0d383 --- /dev/null +++ b/static/admin/img/icon-no.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/admin/img/icon-unknown-alt.svg b/static/admin/img/icon-unknown-alt.svg new file mode 100644 index 0000000..1c6b99f --- /dev/null +++ b/static/admin/img/icon-unknown-alt.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/admin/img/icon-unknown.svg b/static/admin/img/icon-unknown.svg new file mode 100644 index 0000000..50b4f97 --- /dev/null +++ b/static/admin/img/icon-unknown.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/admin/img/icon-viewlink.svg b/static/admin/img/icon-viewlink.svg new file mode 100644 index 0000000..a1ca1d3 --- /dev/null +++ b/static/admin/img/icon-viewlink.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/admin/img/icon-yes.svg b/static/admin/img/icon-yes.svg new file mode 100644 index 0000000..5883d87 --- /dev/null +++ b/static/admin/img/icon-yes.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/admin/img/inline-delete.svg b/static/admin/img/inline-delete.svg new file mode 100644 index 0000000..17d1ad6 --- /dev/null +++ b/static/admin/img/inline-delete.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/admin/img/search.svg b/static/admin/img/search.svg new file mode 100644 index 0000000..c8c69b2 --- /dev/null +++ b/static/admin/img/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/admin/img/selector-icons.svg b/static/admin/img/selector-icons.svg new file mode 100644 index 0000000..926b8e2 --- /dev/null +++ b/static/admin/img/selector-icons.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/admin/img/sorting-icons.svg b/static/admin/img/sorting-icons.svg new file mode 100644 index 0000000..7c31ec9 --- /dev/null +++ b/static/admin/img/sorting-icons.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/static/admin/img/tooltag-add.svg b/static/admin/img/tooltag-add.svg new file mode 100644 index 0000000..1ca64ae --- /dev/null +++ b/static/admin/img/tooltag-add.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/admin/img/tooltag-arrowright.svg b/static/admin/img/tooltag-arrowright.svg new file mode 100644 index 0000000..b664d61 --- /dev/null +++ b/static/admin/img/tooltag-arrowright.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/admin/js/SelectBox.js b/static/admin/js/SelectBox.js new file mode 100644 index 0000000..3db4ec7 --- /dev/null +++ b/static/admin/js/SelectBox.js @@ -0,0 +1,116 @@ +'use strict'; +{ + const SelectBox = { + cache: {}, + init: function(id) { + const box = document.getElementById(id); + SelectBox.cache[id] = []; + const cache = SelectBox.cache[id]; + for (const node of box.options) { + cache.push({value: node.value, text: node.text, displayed: 1}); + } + }, + redisplay: function(id) { + // Repopulate HTML select box from cache + const box = document.getElementById(id); + const scroll_value_from_top = box.scrollTop; + box.innerHTML = ''; + for (const node of SelectBox.cache[id]) { + if (node.displayed) { + const new_option = new Option(node.text, node.value, false, false); + // Shows a tooltip when hovering over the option + new_option.title = node.text; + box.appendChild(new_option); + } + } + box.scrollTop = scroll_value_from_top; + }, + filter: function(id, text) { + // Redisplay the HTML select box, displaying only the choices containing ALL + // the words in text. (It's an AND search.) + const tokens = text.toLowerCase().split(/\s+/); + for (const node of SelectBox.cache[id]) { + node.displayed = 1; + const node_text = node.text.toLowerCase(); + for (const token of tokens) { + if (!node_text.includes(token)) { + node.displayed = 0; + break; // Once the first token isn't found we're done + } + } + } + SelectBox.redisplay(id); + }, + get_hidden_node_count(id) { + const cache = SelectBox.cache[id] || []; + return cache.filter(node => node.displayed === 0).length; + }, + delete_from_cache: function(id, value) { + let delete_index = null; + const cache = SelectBox.cache[id]; + for (const [i, node] of cache.entries()) { + if (node.value === value) { + delete_index = i; + break; + } + } + cache.splice(delete_index, 1); + }, + add_to_cache: function(id, option) { + SelectBox.cache[id].push({value: option.value, text: option.text, displayed: 1}); + }, + cache_contains: function(id, value) { + // Check if an item is contained in the cache + for (const node of SelectBox.cache[id]) { + if (node.value === value) { + return true; + } + } + return false; + }, + move: function(from, to) { + const from_box = document.getElementById(from); + for (const option of from_box.options) { + const option_value = option.value; + if (option.selected && SelectBox.cache_contains(from, option_value)) { + SelectBox.add_to_cache(to, {value: option_value, text: option.text, displayed: 1}); + SelectBox.delete_from_cache(from, option_value); + } + } + SelectBox.redisplay(from); + SelectBox.redisplay(to); + }, + move_all: function(from, to) { + const from_box = document.getElementById(from); + for (const option of from_box.options) { + const option_value = option.value; + if (SelectBox.cache_contains(from, option_value)) { + SelectBox.add_to_cache(to, {value: option_value, text: option.text, displayed: 1}); + SelectBox.delete_from_cache(from, option_value); + } + } + SelectBox.redisplay(from); + SelectBox.redisplay(to); + }, + sort: function(id) { + SelectBox.cache[id].sort(function(a, b) { + a = a.text.toLowerCase(); + b = b.text.toLowerCase(); + if (a > b) { + return 1; + } + if (a < b) { + return -1; + } + return 0; + } ); + }, + select_all: function(id) { + const box = document.getElementById(id); + for (const option of box.options) { + option.selected = true; + } + } + }; + window.SelectBox = SelectBox; +} diff --git a/static/admin/js/SelectFilter2.js b/static/admin/js/SelectFilter2.js new file mode 100644 index 0000000..9a4e0a3 --- /dev/null +++ b/static/admin/js/SelectFilter2.js @@ -0,0 +1,283 @@ +/*global SelectBox, gettext, interpolate, quickElement, SelectFilter*/ +/* +SelectFilter2 - Turns a multiple-select box into a filter interface. + +Requires core.js and SelectBox.js. +*/ +'use strict'; +{ + window.SelectFilter = { + init: function(field_id, field_name, is_stacked) { + if (field_id.match(/__prefix__/)) { + // Don't initialize on empty forms. + return; + } + const from_box = document.getElementById(field_id); + from_box.id += '_from'; // change its ID + from_box.className = 'filtered'; + + for (const p of from_box.parentNode.getElementsByTagName('p')) { + if (p.classList.contains("info")) { + // Remove

, because it just gets in the way. + from_box.parentNode.removeChild(p); + } else if (p.classList.contains("help")) { + // Move help text up to the top so it isn't below the select + // boxes or wrapped off on the side to the right of the add + // button: + from_box.parentNode.insertBefore(p, from_box.parentNode.firstChild); + } + } + + //

or
+ const selector_div = quickElement('div', from_box.parentNode); + selector_div.className = is_stacked ? 'selector stacked' : 'selector'; + + //
+ const selector_available = quickElement('div', selector_div); + selector_available.className = 'selector-available'; + const title_available = quickElement('h2', selector_available, interpolate(gettext('Available %s') + ' ', [field_name])); + quickElement( + 'span', title_available, '', + 'class', 'help help-tooltip help-icon', + 'title', interpolate( + gettext( + 'This is the list of available %s. You may choose some by ' + + 'selecting them in the box below and then clicking the ' + + '"Choose" arrow between the two boxes.' + ), + [field_name] + ) + ); + + const filter_p = quickElement('p', selector_available, '', 'id', field_id + '_filter'); + filter_p.className = 'selector-filter'; + + const search_filter_label = quickElement('label', filter_p, '', 'for', field_id + '_input'); + + quickElement( + 'span', search_filter_label, '', + 'class', 'help-tooltip search-label-icon', + 'title', interpolate(gettext("Type into this box to filter down the list of available %s."), [field_name]) + ); + + filter_p.appendChild(document.createTextNode(' ')); + + const filter_input = quickElement('input', filter_p, '', 'type', 'text', 'placeholder', gettext("Filter")); + filter_input.id = field_id + '_input'; + + selector_available.appendChild(from_box); + const choose_all = quickElement('a', selector_available, gettext('Choose all'), 'title', interpolate(gettext('Click to choose all %s at once.'), [field_name]), 'href', '#', 'id', field_id + '_add_all_link'); + choose_all.className = 'selector-chooseall'; + + //
    + const selector_chooser = quickElement('ul', selector_div); + selector_chooser.className = 'selector-chooser'; + const add_link = quickElement('a', quickElement('li', selector_chooser), gettext('Choose'), 'title', gettext('Choose'), 'href', '#', 'id', field_id + '_add_link'); + add_link.className = 'selector-add'; + const remove_link = quickElement('a', quickElement('li', selector_chooser), gettext('Remove'), 'title', gettext('Remove'), 'href', '#', 'id', field_id + '_remove_link'); + remove_link.className = 'selector-remove'; + + //
    + const selector_chosen = quickElement('div', selector_div, '', 'id', field_id + '_selector_chosen'); + selector_chosen.className = 'selector-chosen'; + const title_chosen = quickElement('h2', selector_chosen, interpolate(gettext('Chosen %s') + ' ', [field_name])); + quickElement( + 'span', title_chosen, '', + 'class', 'help help-tooltip help-icon', + 'title', interpolate( + gettext( + 'This is the list of chosen %s. You may remove some by ' + + 'selecting them in the box below and then clicking the ' + + '"Remove" arrow between the two boxes.' + ), + [field_name] + ) + ); + + const filter_selected_p = quickElement('p', selector_chosen, '', 'id', field_id + '_filter_selected'); + filter_selected_p.className = 'selector-filter'; + + const search_filter_selected_label = quickElement('label', filter_selected_p, '', 'for', field_id + '_selected_input'); + + quickElement( + 'span', search_filter_selected_label, '', + 'class', 'help-tooltip search-label-icon', + 'title', interpolate(gettext("Type into this box to filter down the list of selected %s."), [field_name]) + ); + + filter_selected_p.appendChild(document.createTextNode(' ')); + + const filter_selected_input = quickElement('input', filter_selected_p, '', 'type', 'text', 'placeholder', gettext("Filter")); + filter_selected_input.id = field_id + '_selected_input'; + + const to_box = quickElement('select', selector_chosen, '', 'id', field_id + '_to', 'multiple', '', 'size', from_box.size, 'name', from_box.name); + to_box.className = 'filtered'; + + const warning_footer = quickElement('div', selector_chosen, '', 'class', 'list-footer-display'); + quickElement('span', warning_footer, '', 'id', field_id + '_list-footer-display-text'); + quickElement('span', warning_footer, ' (click to clear)', 'class', 'list-footer-display__clear'); + + const clear_all = quickElement('a', selector_chosen, gettext('Remove all'), 'title', interpolate(gettext('Click to remove all chosen %s at once.'), [field_name]), 'href', '#', 'id', field_id + '_remove_all_link'); + clear_all.className = 'selector-clearall'; + + from_box.name = from_box.name + '_old'; + + // Set up the JavaScript event handlers for the select box filter interface + const move_selection = function(e, elem, move_func, from, to) { + if (elem.classList.contains('active')) { + move_func(from, to); + SelectFilter.refresh_icons(field_id); + SelectFilter.refresh_filtered_selects(field_id); + SelectFilter.refresh_filtered_warning(field_id); + } + e.preventDefault(); + }; + choose_all.addEventListener('click', function(e) { + move_selection(e, this, SelectBox.move_all, field_id + '_from', field_id + '_to'); + }); + add_link.addEventListener('click', function(e) { + move_selection(e, this, SelectBox.move, field_id + '_from', field_id + '_to'); + }); + remove_link.addEventListener('click', function(e) { + move_selection(e, this, SelectBox.move, field_id + '_to', field_id + '_from'); + }); + clear_all.addEventListener('click', function(e) { + move_selection(e, this, SelectBox.move_all, field_id + '_to', field_id + '_from'); + }); + warning_footer.addEventListener('click', function(e) { + filter_selected_input.value = ''; + SelectBox.filter(field_id + '_to', ''); + SelectFilter.refresh_filtered_warning(field_id); + SelectFilter.refresh_icons(field_id); + }); + filter_input.addEventListener('keypress', function(e) { + SelectFilter.filter_key_press(e, field_id, '_from', '_to'); + }); + filter_input.addEventListener('keyup', function(e) { + SelectFilter.filter_key_up(e, field_id, '_from'); + }); + filter_input.addEventListener('keydown', function(e) { + SelectFilter.filter_key_down(e, field_id, '_from', '_to'); + }); + filter_selected_input.addEventListener('keypress', function(e) { + SelectFilter.filter_key_press(e, field_id, '_to', '_from'); + }); + filter_selected_input.addEventListener('keyup', function(e) { + SelectFilter.filter_key_up(e, field_id, '_to', '_selected_input'); + }); + filter_selected_input.addEventListener('keydown', function(e) { + SelectFilter.filter_key_down(e, field_id, '_to', '_from'); + }); + selector_div.addEventListener('change', function(e) { + if (e.target.tagName === 'SELECT') { + SelectFilter.refresh_icons(field_id); + } + }); + selector_div.addEventListener('dblclick', function(e) { + if (e.target.tagName === 'OPTION') { + if (e.target.closest('select').id === field_id + '_to') { + SelectBox.move(field_id + '_to', field_id + '_from'); + } else { + SelectBox.move(field_id + '_from', field_id + '_to'); + } + SelectFilter.refresh_icons(field_id); + } + }); + from_box.closest('form').addEventListener('submit', function() { + SelectBox.filter(field_id + '_to', ''); + SelectBox.select_all(field_id + '_to'); + }); + SelectBox.init(field_id + '_from'); + SelectBox.init(field_id + '_to'); + // Move selected from_box options to to_box + SelectBox.move(field_id + '_from', field_id + '_to'); + + // Initial icon refresh + SelectFilter.refresh_icons(field_id); + }, + any_selected: function(field) { + // Temporarily add the required attribute and check validity. + field.required = true; + const any_selected = field.checkValidity(); + field.required = false; + return any_selected; + }, + refresh_filtered_warning: function(field_id) { + const count = SelectBox.get_hidden_node_count(field_id + '_to'); + const selector = document.getElementById(field_id + '_selector_chosen'); + const warning = document.getElementById(field_id + '_list-footer-display-text'); + selector.className = selector.className.replace('selector-chosen--with-filtered', ''); + warning.textContent = interpolate(ngettext( + '%s selected option not visible', + '%s selected options not visible', + count + ), [count]); + if(count > 0) { + selector.className += ' selector-chosen--with-filtered'; + } + }, + refresh_filtered_selects: function(field_id) { + SelectBox.filter(field_id + '_from', document.getElementById(field_id + "_input").value); + SelectBox.filter(field_id + '_to', document.getElementById(field_id + "_selected_input").value); + }, + refresh_icons: function(field_id) { + const from = document.getElementById(field_id + '_from'); + const to = document.getElementById(field_id + '_to'); + // Active if at least one item is selected + document.getElementById(field_id + '_add_link').classList.toggle('active', SelectFilter.any_selected(from)); + document.getElementById(field_id + '_remove_link').classList.toggle('active', SelectFilter.any_selected(to)); + // Active if the corresponding box isn't empty + document.getElementById(field_id + '_add_all_link').classList.toggle('active', from.querySelector('option')); + document.getElementById(field_id + '_remove_all_link').classList.toggle('active', to.querySelector('option')); + SelectFilter.refresh_filtered_warning(field_id); + }, + filter_key_press: function(event, field_id, source, target) { + const source_box = document.getElementById(field_id + source); + // don't submit form if user pressed Enter + if ((event.which && event.which === 13) || (event.keyCode && event.keyCode === 13)) { + source_box.selectedIndex = 0; + SelectBox.move(field_id + source, field_id + target); + source_box.selectedIndex = 0; + event.preventDefault(); + } + }, + filter_key_up: function(event, field_id, source, filter_input) { + const input = filter_input || '_input'; + const source_box = document.getElementById(field_id + source); + const temp = source_box.selectedIndex; + SelectBox.filter(field_id + source, document.getElementById(field_id + input).value); + source_box.selectedIndex = temp; + SelectFilter.refresh_filtered_warning(field_id); + SelectFilter.refresh_icons(field_id); + }, + filter_key_down: function(event, field_id, source, target) { + const source_box = document.getElementById(field_id + source); + // right key (39) or left key (37) + const direction = source === '_from' ? 39 : 37; + // right arrow -- move across + if ((event.which && event.which === direction) || (event.keyCode && event.keyCode === direction)) { + const old_index = source_box.selectedIndex; + SelectBox.move(field_id + source, field_id + target); + SelectFilter.refresh_filtered_selects(field_id); + SelectFilter.refresh_filtered_warning(field_id); + source_box.selectedIndex = (old_index === source_box.length) ? source_box.length - 1 : old_index; + return; + } + // down arrow -- wrap around + if ((event.which && event.which === 40) || (event.keyCode && event.keyCode === 40)) { + source_box.selectedIndex = (source_box.length === source_box.selectedIndex + 1) ? 0 : source_box.selectedIndex + 1; + } + // up arrow -- wrap around + if ((event.which && event.which === 38) || (event.keyCode && event.keyCode === 38)) { + source_box.selectedIndex = (source_box.selectedIndex === 0) ? source_box.length - 1 : source_box.selectedIndex - 1; + } + } + }; + + window.addEventListener('load', function(e) { + document.querySelectorAll('select.selectfilter, select.selectfilterstacked').forEach(function(el) { + const data = el.dataset; + SelectFilter.init(el.id, data.fieldName, parseInt(data.isStacked, 10)); + }); + }); +} diff --git a/static/admin/js/actions.js b/static/admin/js/actions.js new file mode 100644 index 0000000..20a5c14 --- /dev/null +++ b/static/admin/js/actions.js @@ -0,0 +1,201 @@ +/*global gettext, interpolate, ngettext*/ +'use strict'; +{ + function show(selector) { + document.querySelectorAll(selector).forEach(function(el) { + el.classList.remove('hidden'); + }); + } + + function hide(selector) { + document.querySelectorAll(selector).forEach(function(el) { + el.classList.add('hidden'); + }); + } + + function showQuestion(options) { + hide(options.acrossClears); + show(options.acrossQuestions); + hide(options.allContainer); + } + + function showClear(options) { + show(options.acrossClears); + hide(options.acrossQuestions); + document.querySelector(options.actionContainer).classList.remove(options.selectedClass); + show(options.allContainer); + hide(options.counterContainer); + } + + function reset(options) { + hide(options.acrossClears); + hide(options.acrossQuestions); + hide(options.allContainer); + show(options.counterContainer); + } + + function clearAcross(options) { + reset(options); + const acrossInputs = document.querySelectorAll(options.acrossInput); + acrossInputs.forEach(function(acrossInput) { + acrossInput.value = 0; + }); + document.querySelector(options.actionContainer).classList.remove(options.selectedClass); + } + + function checker(actionCheckboxes, options, checked) { + if (checked) { + showQuestion(options); + } else { + reset(options); + } + actionCheckboxes.forEach(function(el) { + el.checked = checked; + el.closest('tr').classList.toggle(options.selectedClass, checked); + }); + } + + function updateCounter(actionCheckboxes, options) { + const sel = Array.from(actionCheckboxes).filter(function(el) { + return el.checked; + }).length; + const counter = document.querySelector(options.counterContainer); + // data-actions-icnt is defined in the generated HTML + // and contains the total amount of objects in the queryset + const actions_icnt = Number(counter.dataset.actionsIcnt); + counter.textContent = interpolate( + ngettext('%(sel)s of %(cnt)s selected', '%(sel)s of %(cnt)s selected', sel), { + sel: sel, + cnt: actions_icnt + }, true); + const allToggle = document.getElementById(options.allToggleId); + allToggle.checked = sel === actionCheckboxes.length; + if (allToggle.checked) { + showQuestion(options); + } else { + clearAcross(options); + } + } + + const defaults = { + actionContainer: "div.actions", + counterContainer: "span.action-counter", + allContainer: "div.actions span.all", + acrossInput: "div.actions input.select-across", + acrossQuestions: "div.actions span.question", + acrossClears: "div.actions span.clear", + allToggleId: "action-toggle", + selectedClass: "selected" + }; + + window.Actions = function(actionCheckboxes, options) { + options = Object.assign({}, defaults, options); + let list_editable_changed = false; + let lastChecked = null; + let shiftPressed = false; + + document.addEventListener('keydown', (event) => { + shiftPressed = event.shiftKey; + }); + + document.addEventListener('keyup', (event) => { + shiftPressed = event.shiftKey; + }); + + document.getElementById(options.allToggleId).addEventListener('click', function(event) { + checker(actionCheckboxes, options, this.checked); + updateCounter(actionCheckboxes, options); + }); + + document.querySelectorAll(options.acrossQuestions + " a").forEach(function(el) { + el.addEventListener('click', function(event) { + event.preventDefault(); + const acrossInputs = document.querySelectorAll(options.acrossInput); + acrossInputs.forEach(function(acrossInput) { + acrossInput.value = 1; + }); + showClear(options); + }); + }); + + document.querySelectorAll(options.acrossClears + " a").forEach(function(el) { + el.addEventListener('click', function(event) { + event.preventDefault(); + document.getElementById(options.allToggleId).checked = false; + clearAcross(options); + checker(actionCheckboxes, options, false); + updateCounter(actionCheckboxes, options); + }); + }); + + function affectedCheckboxes(target, withModifier) { + const multiSelect = (lastChecked && withModifier && lastChecked !== target); + if (!multiSelect) { + return [target]; + } + const checkboxes = Array.from(actionCheckboxes); + const targetIndex = checkboxes.findIndex(el => el === target); + const lastCheckedIndex = checkboxes.findIndex(el => el === lastChecked); + const startIndex = Math.min(targetIndex, lastCheckedIndex); + const endIndex = Math.max(targetIndex, lastCheckedIndex); + const filtered = checkboxes.filter((el, index) => (startIndex <= index) && (index <= endIndex)); + return filtered; + }; + + Array.from(document.getElementById('result_list').tBodies).forEach(function(el) { + el.addEventListener('change', function(event) { + const target = event.target; + if (target.classList.contains('action-select')) { + const checkboxes = affectedCheckboxes(target, shiftPressed); + checker(checkboxes, options, target.checked); + updateCounter(actionCheckboxes, options); + lastChecked = target; + } else { + list_editable_changed = true; + } + }); + }); + + document.querySelector('#changelist-form button[name=index]').addEventListener('click', function(event) { + if (list_editable_changed) { + const confirmed = confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost.")); + if (!confirmed) { + event.preventDefault(); + } + } + }); + + const el = document.querySelector('#changelist-form input[name=_save]'); + // The button does not exist if no fields are editable. + if (el) { + el.addEventListener('click', function(event) { + if (document.querySelector('[name=action]').value) { + const text = list_editable_changed + ? gettext("You have selected an action, but you haven’t saved your changes to individual fields yet. Please click OK to save. You’ll need to re-run the action.") + : gettext("You have selected an action, and you haven’t made any changes on individual fields. You’re probably looking for the Go button rather than the Save button."); + if (!confirm(text)) { + event.preventDefault(); + } + } + }); + } + }; + + // Call function fn when the DOM is loaded and ready. If it is already + // loaded, call the function now. + // http://youmightnotneedjquery.com/#ready + function ready(fn) { + if (document.readyState !== 'loading') { + fn(); + } else { + document.addEventListener('DOMContentLoaded', fn); + } + } + + ready(function() { + const actionsEls = document.querySelectorAll('tr input.action-select'); + if (actionsEls.length > 0) { + Actions(actionsEls); + } + }); +} diff --git a/static/admin/js/admin/DateTimeShortcuts.js b/static/admin/js/admin/DateTimeShortcuts.js new file mode 100644 index 0000000..aa1cae9 --- /dev/null +++ b/static/admin/js/admin/DateTimeShortcuts.js @@ -0,0 +1,408 @@ +/*global Calendar, findPosX, findPosY, get_format, gettext, gettext_noop, interpolate, ngettext, quickElement*/ +// Inserts shortcut buttons after all of the following: +// +// +'use strict'; +{ + const DateTimeShortcuts = { + calendars: [], + calendarInputs: [], + clockInputs: [], + clockHours: { + default_: [ + [gettext_noop('Now'), -1], + [gettext_noop('Midnight'), 0], + [gettext_noop('6 a.m.'), 6], + [gettext_noop('Noon'), 12], + [gettext_noop('6 p.m.'), 18] + ] + }, + dismissClockFunc: [], + dismissCalendarFunc: [], + calendarDivName1: 'calendarbox', // name of calendar
    that gets toggled + calendarDivName2: 'calendarin', // name of
    that contains calendar + calendarLinkName: 'calendarlink', // name of the link that is used to toggle + clockDivName: 'clockbox', // name of clock
    that gets toggled + clockLinkName: 'clocklink', // name of the link that is used to toggle + shortCutsClass: 'datetimeshortcuts', // class of the clock and cal shortcuts + timezoneWarningClass: 'timezonewarning', // class of the warning for timezone mismatch + timezoneOffset: 0, + init: function() { + const serverOffset = document.body.dataset.adminUtcOffset; + if (serverOffset) { + const localOffset = new Date().getTimezoneOffset() * -60; + DateTimeShortcuts.timezoneOffset = localOffset - serverOffset; + } + + for (const inp of document.getElementsByTagName('input')) { + if (inp.type === 'text' && inp.classList.contains('vTimeField')) { + DateTimeShortcuts.addClock(inp); + DateTimeShortcuts.addTimezoneWarning(inp); + } + else if (inp.type === 'text' && inp.classList.contains('vDateField')) { + DateTimeShortcuts.addCalendar(inp); + DateTimeShortcuts.addTimezoneWarning(inp); + } + } + }, + // Return the current time while accounting for the server timezone. + now: function() { + const serverOffset = document.body.dataset.adminUtcOffset; + if (serverOffset) { + const localNow = new Date(); + const localOffset = localNow.getTimezoneOffset() * -60; + localNow.setTime(localNow.getTime() + 1000 * (serverOffset - localOffset)); + return localNow; + } else { + return new Date(); + } + }, + // Add a warning when the time zone in the browser and backend do not match. + addTimezoneWarning: function(inp) { + const warningClass = DateTimeShortcuts.timezoneWarningClass; + let timezoneOffset = DateTimeShortcuts.timezoneOffset / 3600; + + // Only warn if there is a time zone mismatch. + if (!timezoneOffset) { + return; + } + + // Check if warning is already there. + if (inp.parentNode.querySelectorAll('.' + warningClass).length) { + return; + } + + let message; + if (timezoneOffset > 0) { + message = ngettext( + 'Note: You are %s hour ahead of server time.', + 'Note: You are %s hours ahead of server time.', + timezoneOffset + ); + } + else { + timezoneOffset *= -1; + message = ngettext( + 'Note: You are %s hour behind server time.', + 'Note: You are %s hours behind server time.', + timezoneOffset + ); + } + message = interpolate(message, [timezoneOffset]); + + const warning = document.createElement('div'); + warning.classList.add('help', warningClass); + warning.textContent = message; + inp.parentNode.appendChild(warning); + }, + // Add clock widget to a given field + addClock: function(inp) { + const num = DateTimeShortcuts.clockInputs.length; + DateTimeShortcuts.clockInputs[num] = inp; + DateTimeShortcuts.dismissClockFunc[num] = function() { DateTimeShortcuts.dismissClock(num); return true; }; + + // Shortcut links (clock icon and "Now" link) + const shortcuts_span = document.createElement('span'); + shortcuts_span.className = DateTimeShortcuts.shortCutsClass; + inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling); + const now_link = document.createElement('a'); + now_link.href = "#"; + now_link.textContent = gettext('Now'); + now_link.addEventListener('click', function(e) { + e.preventDefault(); + DateTimeShortcuts.handleClockQuicklink(num, -1); + }); + const clock_link = document.createElement('a'); + clock_link.href = '#'; + clock_link.id = DateTimeShortcuts.clockLinkName + num; + clock_link.addEventListener('click', function(e) { + e.preventDefault(); + // avoid triggering the document click handler to dismiss the clock + e.stopPropagation(); + DateTimeShortcuts.openClock(num); + }); + + quickElement( + 'span', clock_link, '', + 'class', 'clock-icon', + 'title', gettext('Choose a Time') + ); + shortcuts_span.appendChild(document.createTextNode('\u00A0')); + shortcuts_span.appendChild(now_link); + shortcuts_span.appendChild(document.createTextNode('\u00A0|\u00A0')); + shortcuts_span.appendChild(clock_link); + + // Create clock link div + // + // Markup looks like: + // + + const clock_box = document.createElement('div'); + clock_box.style.display = 'none'; + clock_box.style.position = 'absolute'; + clock_box.className = 'clockbox module'; + clock_box.id = DateTimeShortcuts.clockDivName + num; + document.body.appendChild(clock_box); + clock_box.addEventListener('click', function(e) { e.stopPropagation(); }); + + quickElement('h2', clock_box, gettext('Choose a time')); + const time_list = quickElement('ul', clock_box); + time_list.className = 'timelist'; + // The list of choices can be overridden in JavaScript like this: + // DateTimeShortcuts.clockHours.name = [['3 a.m.', 3]]; + // where name is the name attribute of the . + const name = typeof DateTimeShortcuts.clockHours[inp.name] === 'undefined' ? 'default_' : inp.name; + DateTimeShortcuts.clockHours[name].forEach(function(element) { + const time_link = quickElement('a', quickElement('li', time_list), gettext(element[0]), 'href', '#'); + time_link.addEventListener('click', function(e) { + e.preventDefault(); + DateTimeShortcuts.handleClockQuicklink(num, element[1]); + }); + }); + + const cancel_p = quickElement('p', clock_box); + cancel_p.className = 'calendar-cancel'; + const cancel_link = quickElement('a', cancel_p, gettext('Cancel'), 'href', '#'); + cancel_link.addEventListener('click', function(e) { + e.preventDefault(); + DateTimeShortcuts.dismissClock(num); + }); + + document.addEventListener('keyup', function(event) { + if (event.which === 27) { + // ESC key closes popup + DateTimeShortcuts.dismissClock(num); + event.preventDefault(); + } + }); + }, + openClock: function(num) { + const clock_box = document.getElementById(DateTimeShortcuts.clockDivName + num); + const clock_link = document.getElementById(DateTimeShortcuts.clockLinkName + num); + + // Recalculate the clockbox position + // is it left-to-right or right-to-left layout ? + if (window.getComputedStyle(document.body).direction !== 'rtl') { + clock_box.style.left = findPosX(clock_link) + 17 + 'px'; + } + else { + // since style's width is in em, it'd be tough to calculate + // px value of it. let's use an estimated px for now + clock_box.style.left = findPosX(clock_link) - 110 + 'px'; + } + clock_box.style.top = Math.max(0, findPosY(clock_link) - 30) + 'px'; + + // Show the clock box + clock_box.style.display = 'block'; + document.addEventListener('click', DateTimeShortcuts.dismissClockFunc[num]); + }, + dismissClock: function(num) { + document.getElementById(DateTimeShortcuts.clockDivName + num).style.display = 'none'; + document.removeEventListener('click', DateTimeShortcuts.dismissClockFunc[num]); + }, + handleClockQuicklink: function(num, val) { + let d; + if (val === -1) { + d = DateTimeShortcuts.now(); + } + else { + d = new Date(1970, 1, 1, val, 0, 0, 0); + } + DateTimeShortcuts.clockInputs[num].value = d.strftime(get_format('TIME_INPUT_FORMATS')[0]); + DateTimeShortcuts.clockInputs[num].focus(); + DateTimeShortcuts.dismissClock(num); + }, + // Add calendar widget to a given field. + addCalendar: function(inp) { + const num = DateTimeShortcuts.calendars.length; + + DateTimeShortcuts.calendarInputs[num] = inp; + DateTimeShortcuts.dismissCalendarFunc[num] = function() { DateTimeShortcuts.dismissCalendar(num); return true; }; + + // Shortcut links (calendar icon and "Today" link) + const shortcuts_span = document.createElement('span'); + shortcuts_span.className = DateTimeShortcuts.shortCutsClass; + inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling); + const today_link = document.createElement('a'); + today_link.href = '#'; + today_link.appendChild(document.createTextNode(gettext('Today'))); + today_link.addEventListener('click', function(e) { + e.preventDefault(); + DateTimeShortcuts.handleCalendarQuickLink(num, 0); + }); + const cal_link = document.createElement('a'); + cal_link.href = '#'; + cal_link.id = DateTimeShortcuts.calendarLinkName + num; + cal_link.addEventListener('click', function(e) { + e.preventDefault(); + // avoid triggering the document click handler to dismiss the calendar + e.stopPropagation(); + DateTimeShortcuts.openCalendar(num); + }); + quickElement( + 'span', cal_link, '', + 'class', 'date-icon', + 'title', gettext('Choose a Date') + ); + shortcuts_span.appendChild(document.createTextNode('\u00A0')); + shortcuts_span.appendChild(today_link); + shortcuts_span.appendChild(document.createTextNode('\u00A0|\u00A0')); + shortcuts_span.appendChild(cal_link); + + // Create calendarbox div. + // + // Markup looks like: + // + //
    + //

    + // + // February 2003 + //

    + //
    + // + //
    + //
    + // Yesterday | Today | Tomorrow + //
    + //

    Cancel

    + //
    + const cal_box = document.createElement('div'); + cal_box.style.display = 'none'; + cal_box.style.position = 'absolute'; + cal_box.className = 'calendarbox module'; + cal_box.id = DateTimeShortcuts.calendarDivName1 + num; + document.body.appendChild(cal_box); + cal_box.addEventListener('click', function(e) { e.stopPropagation(); }); + + // next-prev links + const cal_nav = quickElement('div', cal_box); + const cal_nav_prev = quickElement('a', cal_nav, '<', 'href', '#'); + cal_nav_prev.className = 'calendarnav-previous'; + cal_nav_prev.addEventListener('click', function(e) { + e.preventDefault(); + DateTimeShortcuts.drawPrev(num); + }); + + const cal_nav_next = quickElement('a', cal_nav, '>', 'href', '#'); + cal_nav_next.className = 'calendarnav-next'; + cal_nav_next.addEventListener('click', function(e) { + e.preventDefault(); + DateTimeShortcuts.drawNext(num); + }); + + // main box + const cal_main = quickElement('div', cal_box, '', 'id', DateTimeShortcuts.calendarDivName2 + num); + cal_main.className = 'calendar'; + DateTimeShortcuts.calendars[num] = new Calendar(DateTimeShortcuts.calendarDivName2 + num, DateTimeShortcuts.handleCalendarCallback(num)); + DateTimeShortcuts.calendars[num].drawCurrent(); + + // calendar shortcuts + const shortcuts = quickElement('div', cal_box); + shortcuts.className = 'calendar-shortcuts'; + let day_link = quickElement('a', shortcuts, gettext('Yesterday'), 'href', '#'); + day_link.addEventListener('click', function(e) { + e.preventDefault(); + DateTimeShortcuts.handleCalendarQuickLink(num, -1); + }); + shortcuts.appendChild(document.createTextNode('\u00A0|\u00A0')); + day_link = quickElement('a', shortcuts, gettext('Today'), 'href', '#'); + day_link.addEventListener('click', function(e) { + e.preventDefault(); + DateTimeShortcuts.handleCalendarQuickLink(num, 0); + }); + shortcuts.appendChild(document.createTextNode('\u00A0|\u00A0')); + day_link = quickElement('a', shortcuts, gettext('Tomorrow'), 'href', '#'); + day_link.addEventListener('click', function(e) { + e.preventDefault(); + DateTimeShortcuts.handleCalendarQuickLink(num, +1); + }); + + // cancel bar + const cancel_p = quickElement('p', cal_box); + cancel_p.className = 'calendar-cancel'; + const cancel_link = quickElement('a', cancel_p, gettext('Cancel'), 'href', '#'); + cancel_link.addEventListener('click', function(e) { + e.preventDefault(); + DateTimeShortcuts.dismissCalendar(num); + }); + document.addEventListener('keyup', function(event) { + if (event.which === 27) { + // ESC key closes popup + DateTimeShortcuts.dismissCalendar(num); + event.preventDefault(); + } + }); + }, + openCalendar: function(num) { + const cal_box = document.getElementById(DateTimeShortcuts.calendarDivName1 + num); + const cal_link = document.getElementById(DateTimeShortcuts.calendarLinkName + num); + const inp = DateTimeShortcuts.calendarInputs[num]; + + // Determine if the current value in the input has a valid date. + // If so, draw the calendar with that date's year and month. + if (inp.value) { + const format = get_format('DATE_INPUT_FORMATS')[0]; + const selected = inp.value.strptime(format); + const year = selected.getUTCFullYear(); + const month = selected.getUTCMonth() + 1; + const re = /\d{4}/; + if (re.test(year.toString()) && month >= 1 && month <= 12) { + DateTimeShortcuts.calendars[num].drawDate(month, year, selected); + } + } + + // Recalculate the clockbox position + // is it left-to-right or right-to-left layout ? + if (window.getComputedStyle(document.body).direction !== 'rtl') { + cal_box.style.left = findPosX(cal_link) + 17 + 'px'; + } + else { + // since style's width is in em, it'd be tough to calculate + // px value of it. let's use an estimated px for now + cal_box.style.left = findPosX(cal_link) - 180 + 'px'; + } + cal_box.style.top = Math.max(0, findPosY(cal_link) - 75) + 'px'; + + cal_box.style.display = 'block'; + document.addEventListener('click', DateTimeShortcuts.dismissCalendarFunc[num]); + }, + dismissCalendar: function(num) { + document.getElementById(DateTimeShortcuts.calendarDivName1 + num).style.display = 'none'; + document.removeEventListener('click', DateTimeShortcuts.dismissCalendarFunc[num]); + }, + drawPrev: function(num) { + DateTimeShortcuts.calendars[num].drawPreviousMonth(); + }, + drawNext: function(num) { + DateTimeShortcuts.calendars[num].drawNextMonth(); + }, + handleCalendarCallback: function(num) { + const format = get_format('DATE_INPUT_FORMATS')[0]; + return function(y, m, d) { + DateTimeShortcuts.calendarInputs[num].value = new Date(y, m - 1, d).strftime(format); + DateTimeShortcuts.calendarInputs[num].focus(); + document.getElementById(DateTimeShortcuts.calendarDivName1 + num).style.display = 'none'; + }; + }, + handleCalendarQuickLink: function(num, offset) { + const d = DateTimeShortcuts.now(); + d.setDate(d.getDate() + offset); + DateTimeShortcuts.calendarInputs[num].value = d.strftime(get_format('DATE_INPUT_FORMATS')[0]); + DateTimeShortcuts.calendarInputs[num].focus(); + DateTimeShortcuts.dismissCalendar(num); + } + }; + + window.addEventListener('load', DateTimeShortcuts.init); + window.DateTimeShortcuts = DateTimeShortcuts; +} diff --git a/static/admin/js/admin/RelatedObjectLookups.js b/static/admin/js/admin/RelatedObjectLookups.js new file mode 100644 index 0000000..afb6b66 --- /dev/null +++ b/static/admin/js/admin/RelatedObjectLookups.js @@ -0,0 +1,238 @@ +/*global SelectBox, interpolate*/ +// Handles related-objects functionality: lookup link for raw_id_fields +// and Add Another links. +'use strict'; +{ + const $ = django.jQuery; + let popupIndex = 0; + const relatedWindows = []; + + function dismissChildPopups() { + relatedWindows.forEach(function(win) { + if(!win.closed) { + win.dismissChildPopups(); + win.close(); + } + }); + } + + function setPopupIndex() { + if(document.getElementsByName("_popup").length > 0) { + const index = window.name.lastIndexOf("__") + 2; + popupIndex = parseInt(window.name.substring(index)); + } else { + popupIndex = 0; + } + } + + function addPopupIndex(name) { + return name + "__" + (popupIndex + 1); + } + + function removePopupIndex(name) { + return name.replace(new RegExp("__" + (popupIndex + 1) + "$"), ''); + } + + function showAdminPopup(triggeringLink, name_regexp, add_popup) { + const name = addPopupIndex(triggeringLink.id.replace(name_regexp, '')); + const href = new URL(triggeringLink.href); + if (add_popup) { + href.searchParams.set('_popup', 1); + } + const win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes'); + relatedWindows.push(win); + win.focus(); + return false; + } + + function showRelatedObjectLookupPopup(triggeringLink) { + return showAdminPopup(triggeringLink, /^lookup_/, true); + } + + function dismissRelatedLookupPopup(win, chosenId) { + const name = removePopupIndex(win.name); + const elem = document.getElementById(name); + if (elem.classList.contains('vManyToManyRawIdAdminField') && elem.value) { + elem.value += ',' + chosenId; + } else { + document.getElementById(name).value = chosenId; + } + const index = relatedWindows.indexOf(win); + if (index > -1) { + relatedWindows.splice(index, 1); + } + win.close(); + } + + function showRelatedObjectPopup(triggeringLink) { + return showAdminPopup(triggeringLink, /^(change|add|delete)_/, false); + } + + function updateRelatedObjectLinks(triggeringLink) { + const $this = $(triggeringLink); + const siblings = $this.nextAll('.view-related, .change-related, .delete-related'); + if (!siblings.length) { + return; + } + const value = $this.val(); + if (value) { + siblings.each(function() { + const elm = $(this); + elm.attr('href', elm.attr('data-href-template').replace('__fk__', value)); + }); + } else { + siblings.removeAttr('href'); + } + } + + function updateRelatedSelectsOptions(currentSelect, win, objId, newRepr, newId) { + // After create/edit a model from the options next to the current + // select (+ or :pencil:) update ForeignKey PK of the rest of selects + // in the page. + + const path = win.location.pathname; + // Extract the model from the popup url '...//add/' or + // '...///change/' depending the action (add or change). + const modelName = path.split('/')[path.split('/').length - (objId ? 4 : 3)]; + // Exclude autocomplete selects. + const selectsRelated = document.querySelectorAll(`[data-model-ref="${modelName}"] select:not(.admin-autocomplete)`); + + selectsRelated.forEach(function(select) { + if (currentSelect === select) { + return; + } + + let option = select.querySelector(`option[value="${objId}"]`); + + if (!option) { + option = new Option(newRepr, newId); + select.options.add(option); + return; + } + + option.textContent = newRepr; + option.value = newId; + }); + } + + function dismissAddRelatedObjectPopup(win, newId, newRepr) { + const name = removePopupIndex(win.name); + const elem = document.getElementById(name); + if (elem) { + const elemName = elem.nodeName.toUpperCase(); + if (elemName === 'SELECT') { + elem.options[elem.options.length] = new Option(newRepr, newId, true, true); + updateRelatedSelectsOptions(elem, win, null, newRepr, newId); + } else if (elemName === 'INPUT') { + if (elem.classList.contains('vManyToManyRawIdAdminField') && elem.value) { + elem.value += ',' + newId; + } else { + elem.value = newId; + } + } + // Trigger a change event to update related links if required. + $(elem).trigger('change'); + } else { + const toId = name + "_to"; + const o = new Option(newRepr, newId); + SelectBox.add_to_cache(toId, o); + SelectBox.redisplay(toId); + } + const index = relatedWindows.indexOf(win); + if (index > -1) { + relatedWindows.splice(index, 1); + } + win.close(); + } + + function dismissChangeRelatedObjectPopup(win, objId, newRepr, newId) { + const id = removePopupIndex(win.name.replace(/^edit_/, '')); + const selectsSelector = interpolate('#%s, #%s_from, #%s_to', [id, id, id]); + const selects = $(selectsSelector); + selects.find('option').each(function() { + if (this.value === objId) { + this.textContent = newRepr; + this.value = newId; + } + }).trigger('change'); + updateRelatedSelectsOptions(selects[0], win, objId, newRepr, newId); + selects.next().find('.select2-selection__rendered').each(function() { + // The element can have a clear button as a child. + // Use the lastChild to modify only the displayed value. + this.lastChild.textContent = newRepr; + this.title = newRepr; + }); + const index = relatedWindows.indexOf(win); + if (index > -1) { + relatedWindows.splice(index, 1); + } + win.close(); + } + + function dismissDeleteRelatedObjectPopup(win, objId) { + const id = removePopupIndex(win.name.replace(/^delete_/, '')); + const selectsSelector = interpolate('#%s, #%s_from, #%s_to', [id, id, id]); + const selects = $(selectsSelector); + selects.find('option').each(function() { + if (this.value === objId) { + $(this).remove(); + } + }).trigger('change'); + const index = relatedWindows.indexOf(win); + if (index > -1) { + relatedWindows.splice(index, 1); + } + win.close(); + } + + window.showRelatedObjectLookupPopup = showRelatedObjectLookupPopup; + window.dismissRelatedLookupPopup = dismissRelatedLookupPopup; + window.showRelatedObjectPopup = showRelatedObjectPopup; + window.updateRelatedObjectLinks = updateRelatedObjectLinks; + window.dismissAddRelatedObjectPopup = dismissAddRelatedObjectPopup; + window.dismissChangeRelatedObjectPopup = dismissChangeRelatedObjectPopup; + window.dismissDeleteRelatedObjectPopup = dismissDeleteRelatedObjectPopup; + window.dismissChildPopups = dismissChildPopups; + + // Kept for backward compatibility + window.showAddAnotherPopup = showRelatedObjectPopup; + window.dismissAddAnotherPopup = dismissAddRelatedObjectPopup; + + window.addEventListener('unload', function(evt) { + window.dismissChildPopups(); + }); + + $(document).ready(function() { + setPopupIndex(); + $("a[data-popup-opener]").on('click', function(event) { + event.preventDefault(); + opener.dismissRelatedLookupPopup(window, $(this).data("popup-opener")); + }); + $('body').on('click', '.related-widget-wrapper-link[data-popup="yes"]', function(e) { + e.preventDefault(); + if (this.href) { + const event = $.Event('django:show-related', {href: this.href}); + $(this).trigger(event); + if (!event.isDefaultPrevented()) { + showRelatedObjectPopup(this); + } + } + }); + $('body').on('change', '.related-widget-wrapper select', function(e) { + const event = $.Event('django:update-related'); + $(this).trigger(event); + if (!event.isDefaultPrevented()) { + updateRelatedObjectLinks(this); + } + }); + $('.related-widget-wrapper select').trigger('change'); + $('body').on('click', '.related-lookup', function(e) { + e.preventDefault(); + const event = $.Event('django:lookup-related'); + $(this).trigger(event); + if (!event.isDefaultPrevented()) { + showRelatedObjectLookupPopup(this); + } + }); + }); +} diff --git a/static/admin/js/autocomplete.js b/static/admin/js/autocomplete.js new file mode 100644 index 0000000..d3daeab --- /dev/null +++ b/static/admin/js/autocomplete.js @@ -0,0 +1,33 @@ +'use strict'; +{ + const $ = django.jQuery; + + $.fn.djangoAdminSelect2 = function() { + $.each(this, function(i, element) { + $(element).select2({ + ajax: { + data: (params) => { + return { + term: params.term, + page: params.page, + app_label: element.dataset.appLabel, + model_name: element.dataset.modelName, + field_name: element.dataset.fieldName + }; + } + } + }); + }); + return this; + }; + + $(function() { + // Initialize all autocomplete widgets except the one in the template + // form used when a new formset is added. + $('.admin-autocomplete').not('[name*=__prefix__]').djangoAdminSelect2(); + }); + + document.addEventListener('formset:added', (event) => { + $(event.target).find('.admin-autocomplete').djangoAdminSelect2(); + }); +} diff --git a/static/admin/js/calendar.js b/static/admin/js/calendar.js new file mode 100644 index 0000000..a62d10a --- /dev/null +++ b/static/admin/js/calendar.js @@ -0,0 +1,221 @@ +/*global gettext, pgettext, get_format, quickElement, removeChildren*/ +/* +calendar.js - Calendar functions by Adrian Holovaty +depends on core.js for utility functions like removeChildren or quickElement +*/ +'use strict'; +{ + // CalendarNamespace -- Provides a collection of HTML calendar-related helper functions + const CalendarNamespace = { + monthsOfYear: [ + gettext('January'), + gettext('February'), + gettext('March'), + gettext('April'), + gettext('May'), + gettext('June'), + gettext('July'), + gettext('August'), + gettext('September'), + gettext('October'), + gettext('November'), + gettext('December') + ], + monthsOfYearAbbrev: [ + pgettext('abbrev. month January', 'Jan'), + pgettext('abbrev. month February', 'Feb'), + pgettext('abbrev. month March', 'Mar'), + pgettext('abbrev. month April', 'Apr'), + pgettext('abbrev. month May', 'May'), + pgettext('abbrev. month June', 'Jun'), + pgettext('abbrev. month July', 'Jul'), + pgettext('abbrev. month August', 'Aug'), + pgettext('abbrev. month September', 'Sep'), + pgettext('abbrev. month October', 'Oct'), + pgettext('abbrev. month November', 'Nov'), + pgettext('abbrev. month December', 'Dec') + ], + daysOfWeek: [ + pgettext('one letter Sunday', 'S'), + pgettext('one letter Monday', 'M'), + pgettext('one letter Tuesday', 'T'), + pgettext('one letter Wednesday', 'W'), + pgettext('one letter Thursday', 'T'), + pgettext('one letter Friday', 'F'), + pgettext('one letter Saturday', 'S') + ], + firstDayOfWeek: parseInt(get_format('FIRST_DAY_OF_WEEK')), + isLeapYear: function(year) { + return (((year % 4) === 0) && ((year % 100) !== 0 ) || ((year % 400) === 0)); + }, + getDaysInMonth: function(month, year) { + let days; + if (month === 1 || month === 3 || month === 5 || month === 7 || month === 8 || month === 10 || month === 12) { + days = 31; + } + else if (month === 4 || month === 6 || month === 9 || month === 11) { + days = 30; + } + else if (month === 2 && CalendarNamespace.isLeapYear(year)) { + days = 29; + } + else { + days = 28; + } + return days; + }, + draw: function(month, year, div_id, callback, selected) { // month = 1-12, year = 1-9999 + const today = new Date(); + const todayDay = today.getDate(); + const todayMonth = today.getMonth() + 1; + const todayYear = today.getFullYear(); + let todayClass = ''; + + // Use UTC functions here because the date field does not contain time + // and using the UTC function variants prevent the local time offset + // from altering the date, specifically the day field. For example: + // + // ``` + // var x = new Date('2013-10-02'); + // var day = x.getDate(); + // ``` + // + // The day variable above will be 1 instead of 2 in, say, US Pacific time + // zone. + let isSelectedMonth = false; + if (typeof selected !== 'undefined') { + isSelectedMonth = (selected.getUTCFullYear() === year && (selected.getUTCMonth() + 1) === month); + } + + month = parseInt(month); + year = parseInt(year); + const calDiv = document.getElementById(div_id); + removeChildren(calDiv); + const calTable = document.createElement('table'); + quickElement('caption', calTable, CalendarNamespace.monthsOfYear[month - 1] + ' ' + year); + const tableBody = quickElement('tbody', calTable); + + // Draw days-of-week header + let tableRow = quickElement('tr', tableBody); + for (let i = 0; i < 7; i++) { + quickElement('th', tableRow, CalendarNamespace.daysOfWeek[(i + CalendarNamespace.firstDayOfWeek) % 7]); + } + + const startingPos = new Date(year, month - 1, 1 - CalendarNamespace.firstDayOfWeek).getDay(); + const days = CalendarNamespace.getDaysInMonth(month, year); + + let nonDayCell; + + // Draw blanks before first of month + tableRow = quickElement('tr', tableBody); + for (let i = 0; i < startingPos; i++) { + nonDayCell = quickElement('td', tableRow, ' '); + nonDayCell.className = "nonday"; + } + + function calendarMonth(y, m) { + function onClick(e) { + e.preventDefault(); + callback(y, m, this.textContent); + } + return onClick; + } + + // Draw days of month + let currentDay = 1; + for (let i = startingPos; currentDay <= days; i++) { + if (i % 7 === 0 && currentDay !== 1) { + tableRow = quickElement('tr', tableBody); + } + if ((currentDay === todayDay) && (month === todayMonth) && (year === todayYear)) { + todayClass = 'today'; + } else { + todayClass = ''; + } + + // use UTC function; see above for explanation. + if (isSelectedMonth && currentDay === selected.getUTCDate()) { + if (todayClass !== '') { + todayClass += " "; + } + todayClass += "selected"; + } + + const cell = quickElement('td', tableRow, '', 'class', todayClass); + const link = quickElement('a', cell, currentDay, 'href', '#'); + link.addEventListener('click', calendarMonth(year, month)); + currentDay++; + } + + // Draw blanks after end of month (optional, but makes for valid code) + while (tableRow.childNodes.length < 7) { + nonDayCell = quickElement('td', tableRow, ' '); + nonDayCell.className = "nonday"; + } + + calDiv.appendChild(calTable); + } + }; + + // Calendar -- A calendar instance + function Calendar(div_id, callback, selected) { + // div_id (string) is the ID of the element in which the calendar will + // be displayed + // callback (string) is the name of a JavaScript function that will be + // called with the parameters (year, month, day) when a day in the + // calendar is clicked + this.div_id = div_id; + this.callback = callback; + this.today = new Date(); + this.currentMonth = this.today.getMonth() + 1; + this.currentYear = this.today.getFullYear(); + if (typeof selected !== 'undefined') { + this.selected = selected; + } + } + Calendar.prototype = { + drawCurrent: function() { + CalendarNamespace.draw(this.currentMonth, this.currentYear, this.div_id, this.callback, this.selected); + }, + drawDate: function(month, year, selected) { + this.currentMonth = month; + this.currentYear = year; + + if(selected) { + this.selected = selected; + } + + this.drawCurrent(); + }, + drawPreviousMonth: function() { + if (this.currentMonth === 1) { + this.currentMonth = 12; + this.currentYear--; + } + else { + this.currentMonth--; + } + this.drawCurrent(); + }, + drawNextMonth: function() { + if (this.currentMonth === 12) { + this.currentMonth = 1; + this.currentYear++; + } + else { + this.currentMonth++; + } + this.drawCurrent(); + }, + drawPreviousYear: function() { + this.currentYear--; + this.drawCurrent(); + }, + drawNextYear: function() { + this.currentYear++; + this.drawCurrent(); + } + }; + window.Calendar = Calendar; + window.CalendarNamespace = CalendarNamespace; +} diff --git a/static/admin/js/cancel.js b/static/admin/js/cancel.js new file mode 100644 index 0000000..3069c6f --- /dev/null +++ b/static/admin/js/cancel.js @@ -0,0 +1,29 @@ +'use strict'; +{ + // Call function fn when the DOM is loaded and ready. If it is already + // loaded, call the function now. + // http://youmightnotneedjquery.com/#ready + function ready(fn) { + if (document.readyState !== 'loading') { + fn(); + } else { + document.addEventListener('DOMContentLoaded', fn); + } + } + + ready(function() { + function handleClick(event) { + event.preventDefault(); + const params = new URLSearchParams(window.location.search); + if (params.has('_popup')) { + window.close(); // Close the popup. + } else { + window.history.back(); // Otherwise, go back. + } + } + + document.querySelectorAll('.cancel-link').forEach(function(el) { + el.addEventListener('click', handleClick); + }); + }); +} diff --git a/static/admin/js/change_form.js b/static/admin/js/change_form.js new file mode 100644 index 0000000..96a4c62 --- /dev/null +++ b/static/admin/js/change_form.js @@ -0,0 +1,16 @@ +'use strict'; +{ + const inputTags = ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA']; + const modelName = document.getElementById('django-admin-form-add-constants').dataset.modelName; + if (modelName) { + const form = document.getElementById(modelName + '_form'); + for (const element of form.elements) { + // HTMLElement.offsetParent returns null when the element is not + // rendered. + if (inputTags.includes(element.tagName) && !element.disabled && element.offsetParent) { + element.focus(); + break; + } + } + } +} diff --git a/static/admin/js/collapse.js b/static/admin/js/collapse.js new file mode 100644 index 0000000..c6c7b0f --- /dev/null +++ b/static/admin/js/collapse.js @@ -0,0 +1,43 @@ +/*global gettext*/ +'use strict'; +{ + window.addEventListener('load', function() { + // Add anchor tag for Show/Hide link + const fieldsets = document.querySelectorAll('fieldset.collapse'); + for (const [i, elem] of fieldsets.entries()) { + // Don't hide if fields in this fieldset have errors + if (elem.querySelectorAll('div.errors, ul.errorlist').length === 0) { + elem.classList.add('collapsed'); + const h2 = elem.querySelector('h2'); + const link = document.createElement('a'); + link.id = 'fieldsetcollapser' + i; + link.className = 'collapse-toggle'; + link.href = '#'; + link.textContent = gettext('Show'); + h2.appendChild(document.createTextNode(' (')); + h2.appendChild(link); + h2.appendChild(document.createTextNode(')')); + } + } + // Add toggle to hide/show anchor tag + const toggleFunc = function(ev) { + if (ev.target.matches('.collapse-toggle')) { + ev.preventDefault(); + ev.stopPropagation(); + const fieldset = ev.target.closest('fieldset'); + if (fieldset.classList.contains('collapsed')) { + // Show + ev.target.textContent = gettext('Hide'); + fieldset.classList.remove('collapsed'); + } else { + // Hide + ev.target.textContent = gettext('Show'); + fieldset.classList.add('collapsed'); + } + } + }; + document.querySelectorAll('fieldset.module').forEach(function(el) { + el.addEventListener('click', toggleFunc); + }); + }); +} diff --git a/static/admin/js/core.js b/static/admin/js/core.js new file mode 100644 index 0000000..0344a13 --- /dev/null +++ b/static/admin/js/core.js @@ -0,0 +1,170 @@ +// Core JavaScript helper functions +'use strict'; + +// quickElement(tagType, parentReference [, textInChildNode, attribute, attributeValue ...]); +function quickElement() { + const obj = document.createElement(arguments[0]); + if (arguments[2]) { + const textNode = document.createTextNode(arguments[2]); + obj.appendChild(textNode); + } + const len = arguments.length; + for (let i = 3; i < len; i += 2) { + obj.setAttribute(arguments[i], arguments[i + 1]); + } + arguments[1].appendChild(obj); + return obj; +} + +// "a" is reference to an object +function removeChildren(a) { + while (a.hasChildNodes()) { + a.removeChild(a.lastChild); + } +} + +// ---------------------------------------------------------------------------- +// Find-position functions by PPK +// See https://www.quirksmode.org/js/findpos.html +// ---------------------------------------------------------------------------- +function findPosX(obj) { + let curleft = 0; + if (obj.offsetParent) { + while (obj.offsetParent) { + curleft += obj.offsetLeft - obj.scrollLeft; + obj = obj.offsetParent; + } + } else if (obj.x) { + curleft += obj.x; + } + return curleft; +} + +function findPosY(obj) { + let curtop = 0; + if (obj.offsetParent) { + while (obj.offsetParent) { + curtop += obj.offsetTop - obj.scrollTop; + obj = obj.offsetParent; + } + } else if (obj.y) { + curtop += obj.y; + } + return curtop; +} + +//----------------------------------------------------------------------------- +// Date object extensions +// ---------------------------------------------------------------------------- +{ + Date.prototype.getTwelveHours = function() { + return this.getHours() % 12 || 12; + }; + + Date.prototype.getTwoDigitMonth = function() { + return (this.getMonth() < 9) ? '0' + (this.getMonth() + 1) : (this.getMonth() + 1); + }; + + Date.prototype.getTwoDigitDate = function() { + return (this.getDate() < 10) ? '0' + this.getDate() : this.getDate(); + }; + + Date.prototype.getTwoDigitTwelveHour = function() { + return (this.getTwelveHours() < 10) ? '0' + this.getTwelveHours() : this.getTwelveHours(); + }; + + Date.prototype.getTwoDigitHour = function() { + return (this.getHours() < 10) ? '0' + this.getHours() : this.getHours(); + }; + + Date.prototype.getTwoDigitMinute = function() { + return (this.getMinutes() < 10) ? '0' + this.getMinutes() : this.getMinutes(); + }; + + Date.prototype.getTwoDigitSecond = function() { + return (this.getSeconds() < 10) ? '0' + this.getSeconds() : this.getSeconds(); + }; + + Date.prototype.getAbbrevMonthName = function() { + return typeof window.CalendarNamespace === "undefined" + ? this.getTwoDigitMonth() + : window.CalendarNamespace.monthsOfYearAbbrev[this.getMonth()]; + }; + + Date.prototype.getFullMonthName = function() { + return typeof window.CalendarNamespace === "undefined" + ? this.getTwoDigitMonth() + : window.CalendarNamespace.monthsOfYear[this.getMonth()]; + }; + + Date.prototype.strftime = function(format) { + const fields = { + b: this.getAbbrevMonthName(), + B: this.getFullMonthName(), + c: this.toString(), + d: this.getTwoDigitDate(), + H: this.getTwoDigitHour(), + I: this.getTwoDigitTwelveHour(), + m: this.getTwoDigitMonth(), + M: this.getTwoDigitMinute(), + p: (this.getHours() >= 12) ? 'PM' : 'AM', + S: this.getTwoDigitSecond(), + w: '0' + this.getDay(), + x: this.toLocaleDateString(), + X: this.toLocaleTimeString(), + y: ('' + this.getFullYear()).substr(2, 4), + Y: '' + this.getFullYear(), + '%': '%' + }; + let result = '', i = 0; + while (i < format.length) { + if (format.charAt(i) === '%') { + result += fields[format.charAt(i + 1)]; + ++i; + } + else { + result += format.charAt(i); + } + ++i; + } + return result; + }; + + // ---------------------------------------------------------------------------- + // String object extensions + // ---------------------------------------------------------------------------- + String.prototype.strptime = function(format) { + const split_format = format.split(/[.\-/]/); + const date = this.split(/[.\-/]/); + let i = 0; + let day, month, year; + while (i < split_format.length) { + switch (split_format[i]) { + case "%d": + day = date[i]; + break; + case "%m": + month = date[i] - 1; + break; + case "%Y": + year = date[i]; + break; + case "%y": + // A %y value in the range of [00, 68] is in the current + // century, while [69, 99] is in the previous century, + // according to the Open Group Specification. + if (parseInt(date[i], 10) >= 69) { + year = date[i]; + } else { + year = (new Date(Date.UTC(date[i], 0))).getUTCFullYear() + 100; + } + break; + } + ++i; + } + // Create Date object from UTC since the parsed value is supposed to be + // in UTC, not local time. Also, the calendar uses UTC functions for + // date extraction. + return new Date(Date.UTC(year, month, day)); + }; +} diff --git a/static/admin/js/filters.js b/static/admin/js/filters.js new file mode 100644 index 0000000..f5536eb --- /dev/null +++ b/static/admin/js/filters.js @@ -0,0 +1,30 @@ +/** + * Persist changelist filters state (collapsed/expanded). + */ +'use strict'; +{ + // Init filters. + let filters = JSON.parse(sessionStorage.getItem('django.admin.filtersState')); + + if (!filters) { + filters = {}; + } + + Object.entries(filters).forEach(([key, value]) => { + const detailElement = document.querySelector(`[data-filter-title='${CSS.escape(key)}']`); + + // Check if the filter is present, it could be from other view. + if (detailElement) { + value ? detailElement.setAttribute('open', '') : detailElement.removeAttribute('open'); + } + }); + + // Save filter state when clicks. + const details = document.querySelectorAll('details'); + details.forEach(detail => { + detail.addEventListener('toggle', event => { + filters[`${event.target.dataset.filterTitle}`] = detail.open; + sessionStorage.setItem('django.admin.filtersState', JSON.stringify(filters)); + }); + }); +} diff --git a/static/admin/js/inlines.js b/static/admin/js/inlines.js new file mode 100644 index 0000000..e9a1dfe --- /dev/null +++ b/static/admin/js/inlines.js @@ -0,0 +1,359 @@ +/*global DateTimeShortcuts, SelectFilter*/ +/** + * Django admin inlines + * + * Based on jQuery Formset 1.1 + * @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com) + * @requires jQuery 1.2.6 or later + * + * Copyright (c) 2009, Stanislaus Madueke + * All rights reserved. + * + * Spiced up with Code from Zain Memon's GSoC project 2009 + * and modified for Django by Jannis Leidel, Travis Swicegood and Julien Phalip. + * + * Licensed under the New BSD License + * See: https://opensource.org/licenses/bsd-license.php + */ +'use strict'; +{ + const $ = django.jQuery; + $.fn.formset = function(opts) { + const options = $.extend({}, $.fn.formset.defaults, opts); + const $this = $(this); + const $parent = $this.parent(); + const updateElementIndex = function(el, prefix, ndx) { + const id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))"); + const replacement = prefix + "-" + ndx; + if ($(el).prop("for")) { + $(el).prop("for", $(el).prop("for").replace(id_regex, replacement)); + } + if (el.id) { + el.id = el.id.replace(id_regex, replacement); + } + if (el.name) { + el.name = el.name.replace(id_regex, replacement); + } + }; + const totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").prop("autocomplete", "off"); + let nextIndex = parseInt(totalForms.val(), 10); + const maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").prop("autocomplete", "off"); + const minForms = $("#id_" + options.prefix + "-MIN_NUM_FORMS").prop("autocomplete", "off"); + let addButton; + + /** + * The "Add another MyModel" button below the inline forms. + */ + const addInlineAddButton = function() { + if (addButton === null) { + if ($this.prop("tagName") === "TR") { + // If forms are laid out as table rows, insert the + // "add" button in a new table row: + const numCols = $this.eq(-1).children().length; + $parent.append('' + options.addText + ""); + addButton = $parent.find("tr:last a"); + } else { + // Otherwise, insert it immediately after the last form: + $this.filter(":last").after('"); + addButton = $this.filter(":last").next().find("a"); + } + } + addButton.on('click', addInlineClickHandler); + }; + + const addInlineClickHandler = function(e) { + e.preventDefault(); + const template = $("#" + options.prefix + "-empty"); + const row = template.clone(true); + row.removeClass(options.emptyCssClass) + .addClass(options.formCssClass) + .attr("id", options.prefix + "-" + nextIndex); + addInlineDeleteButton(row); + row.find("*").each(function() { + updateElementIndex(this, options.prefix, totalForms.val()); + }); + // Insert the new form when it has been fully edited. + row.insertBefore($(template)); + // Update number of total forms. + $(totalForms).val(parseInt(totalForms.val(), 10) + 1); + nextIndex += 1; + // Hide the add button if there's a limit and it's been reached. + if ((maxForms.val() !== '') && (maxForms.val() - totalForms.val()) <= 0) { + addButton.parent().hide(); + } + // Show the remove buttons if there are more than min_num. + toggleDeleteButtonVisibility(row.closest('.inline-group')); + + // Pass the new form to the post-add callback, if provided. + if (options.added) { + options.added(row); + } + row.get(0).dispatchEvent(new CustomEvent("formset:added", { + bubbles: true, + detail: { + formsetName: options.prefix + } + })); + }; + + /** + * The "X" button that is part of every unsaved inline. + * (When saved, it is replaced with a "Delete" checkbox.) + */ + const addInlineDeleteButton = function(row) { + if (row.is("tr")) { + // If the forms are laid out in table rows, insert + // the remove button into the last table cell: + row.children(":last").append('"); + } else if (row.is("ul") || row.is("ol")) { + // If they're laid out as an ordered/unordered list, + // insert an
  • after the last list item: + row.append('
  • ' + options.deleteText + "
  • "); + } else { + // Otherwise, just insert the remove button as the + // last child element of the form's container: + row.children(":first").append('' + options.deleteText + ""); + } + // Add delete handler for each row. + row.find("a." + options.deleteCssClass).on('click', inlineDeleteHandler.bind(this)); + }; + + const inlineDeleteHandler = function(e1) { + e1.preventDefault(); + const deleteButton = $(e1.target); + const row = deleteButton.closest('.' + options.formCssClass); + const inlineGroup = row.closest('.inline-group'); + // Remove the parent form containing this button, + // and also remove the relevant row with non-field errors: + const prevRow = row.prev(); + if (prevRow.length && prevRow.hasClass('row-form-errors')) { + prevRow.remove(); + } + row.remove(); + nextIndex -= 1; + // Pass the deleted form to the post-delete callback, if provided. + if (options.removed) { + options.removed(row); + } + document.dispatchEvent(new CustomEvent("formset:removed", { + detail: { + formsetName: options.prefix + } + })); + // Update the TOTAL_FORMS form count. + const forms = $("." + options.formCssClass); + $("#id_" + options.prefix + "-TOTAL_FORMS").val(forms.length); + // Show add button again once below maximum number. + if ((maxForms.val() === '') || (maxForms.val() - forms.length) > 0) { + addButton.parent().show(); + } + // Hide the remove buttons if at min_num. + toggleDeleteButtonVisibility(inlineGroup); + // Also, update names and ids for all remaining form controls so + // they remain in sequence: + let i, formCount; + const updateElementCallback = function() { + updateElementIndex(this, options.prefix, i); + }; + for (i = 0, formCount = forms.length; i < formCount; i++) { + updateElementIndex($(forms).get(i), options.prefix, i); + $(forms.get(i)).find("*").each(updateElementCallback); + } + }; + + const toggleDeleteButtonVisibility = function(inlineGroup) { + if ((minForms.val() !== '') && (minForms.val() - totalForms.val()) >= 0) { + inlineGroup.find('.inline-deletelink').hide(); + } else { + inlineGroup.find('.inline-deletelink').show(); + } + }; + + $this.each(function(i) { + $(this).not("." + options.emptyCssClass).addClass(options.formCssClass); + }); + + // Create the delete buttons for all unsaved inlines: + $this.filter('.' + options.formCssClass + ':not(.has_original):not(.' + options.emptyCssClass + ')').each(function() { + addInlineDeleteButton($(this)); + }); + toggleDeleteButtonVisibility($this); + + // Create the add button, initially hidden. + addButton = options.addButton; + addInlineAddButton(); + + // Show the add button if allowed to add more items. + // Note that max_num = None translates to a blank string. + const showAddButton = maxForms.val() === '' || (maxForms.val() - totalForms.val()) > 0; + if ($this.length && showAddButton) { + addButton.parent().show(); + } else { + addButton.parent().hide(); + } + + return this; + }; + + /* Setup plugin defaults */ + $.fn.formset.defaults = { + prefix: "form", // The form prefix for your django formset + addText: "add another", // Text for the add link + deleteText: "remove", // Text for the delete link + addCssClass: "add-row", // CSS class applied to the add link + deleteCssClass: "delete-row", // CSS class applied to the delete link + emptyCssClass: "empty-row", // CSS class applied to the empty row + formCssClass: "dynamic-form", // CSS class applied to each form in a formset + added: null, // Function called each time a new form is added + removed: null, // Function called each time a form is deleted + addButton: null // Existing add button to use + }; + + + // Tabular inlines --------------------------------------------------------- + $.fn.tabularFormset = function(selector, options) { + const $rows = $(this); + + const reinitDateTimeShortCuts = function() { + // Reinitialize the calendar and clock widgets by force + if (typeof DateTimeShortcuts !== "undefined") { + $(".datetimeshortcuts").remove(); + DateTimeShortcuts.init(); + } + }; + + const updateSelectFilter = function() { + // If any SelectFilter widgets are a part of the new form, + // instantiate a new SelectFilter instance for it. + if (typeof SelectFilter !== 'undefined') { + $('.selectfilter').each(function(index, value) { + SelectFilter.init(value.id, this.dataset.fieldName, false); + }); + $('.selectfilterstacked').each(function(index, value) { + SelectFilter.init(value.id, this.dataset.fieldName, true); + }); + } + }; + + const initPrepopulatedFields = function(row) { + row.find('.prepopulated_field').each(function() { + const field = $(this), + input = field.find('input, select, textarea'), + dependency_list = input.data('dependency_list') || [], + dependencies = []; + $.each(dependency_list, function(i, field_name) { + dependencies.push('#' + row.find('.field-' + field_name).find('input, select, textarea').attr('id')); + }); + if (dependencies.length) { + input.prepopulate(dependencies, input.attr('maxlength')); + } + }); + }; + + $rows.formset({ + prefix: options.prefix, + addText: options.addText, + formCssClass: "dynamic-" + options.prefix, + deleteCssClass: "inline-deletelink", + deleteText: options.deleteText, + emptyCssClass: "empty-form", + added: function(row) { + initPrepopulatedFields(row); + reinitDateTimeShortCuts(); + updateSelectFilter(); + }, + addButton: options.addButton + }); + + return $rows; + }; + + // Stacked inlines --------------------------------------------------------- + $.fn.stackedFormset = function(selector, options) { + const $rows = $(this); + const updateInlineLabel = function(row) { + $(selector).find(".inline_label").each(function(i) { + const count = i + 1; + $(this).html($(this).html().replace(/(#\d+)/g, "#" + count)); + }); + }; + + const reinitDateTimeShortCuts = function() { + // Reinitialize the calendar and clock widgets by force, yuck. + if (typeof DateTimeShortcuts !== "undefined") { + $(".datetimeshortcuts").remove(); + DateTimeShortcuts.init(); + } + }; + + const updateSelectFilter = function() { + // If any SelectFilter widgets were added, instantiate a new instance. + if (typeof SelectFilter !== "undefined") { + $(".selectfilter").each(function(index, value) { + SelectFilter.init(value.id, this.dataset.fieldName, false); + }); + $(".selectfilterstacked").each(function(index, value) { + SelectFilter.init(value.id, this.dataset.fieldName, true); + }); + } + }; + + const initPrepopulatedFields = function(row) { + row.find('.prepopulated_field').each(function() { + const field = $(this), + input = field.find('input, select, textarea'), + dependency_list = input.data('dependency_list') || [], + dependencies = []; + $.each(dependency_list, function(i, field_name) { + // Dependency in a fieldset. + let field_element = row.find('.form-row .field-' + field_name); + // Dependency without a fieldset. + if (!field_element.length) { + field_element = row.find('.form-row.field-' + field_name); + } + dependencies.push('#' + field_element.find('input, select, textarea').attr('id')); + }); + if (dependencies.length) { + input.prepopulate(dependencies, input.attr('maxlength')); + } + }); + }; + + $rows.formset({ + prefix: options.prefix, + addText: options.addText, + formCssClass: "dynamic-" + options.prefix, + deleteCssClass: "inline-deletelink", + deleteText: options.deleteText, + emptyCssClass: "empty-form", + removed: updateInlineLabel, + added: function(row) { + initPrepopulatedFields(row); + reinitDateTimeShortCuts(); + updateSelectFilter(); + updateInlineLabel(row); + }, + addButton: options.addButton + }); + + return $rows; + }; + + $(document).ready(function() { + $(".js-inline-admin-formset").each(function() { + const data = $(this).data(), + inlineOptions = data.inlineFormset; + let selector; + switch(data.inlineType) { + case "stacked": + selector = inlineOptions.name + "-group .inline-related"; + $(selector).stackedFormset(selector, inlineOptions.options); + break; + case "tabular": + selector = inlineOptions.name + "-group .tabular.inline-related tbody:first > tr.form-row"; + $(selector).tabularFormset(selector, inlineOptions.options); + break; + } + }); + }); +} diff --git a/static/admin/js/jquery.init.js b/static/admin/js/jquery.init.js new file mode 100644 index 0000000..f40b27f --- /dev/null +++ b/static/admin/js/jquery.init.js @@ -0,0 +1,8 @@ +/*global jQuery:false*/ +'use strict'; +/* Puts the included jQuery into our own namespace using noConflict and passing + * it 'true'. This ensures that the included jQuery doesn't pollute the global + * namespace (i.e. this preserves pre-existing values for both window.$ and + * window.jQuery). + */ +window.django = {jQuery: jQuery.noConflict(true)}; diff --git a/static/admin/js/nav_sidebar.js b/static/admin/js/nav_sidebar.js new file mode 100644 index 0000000..7e735db --- /dev/null +++ b/static/admin/js/nav_sidebar.js @@ -0,0 +1,79 @@ +'use strict'; +{ + const toggleNavSidebar = document.getElementById('toggle-nav-sidebar'); + if (toggleNavSidebar !== null) { + const navSidebar = document.getElementById('nav-sidebar'); + const main = document.getElementById('main'); + let navSidebarIsOpen = localStorage.getItem('django.admin.navSidebarIsOpen'); + if (navSidebarIsOpen === null) { + navSidebarIsOpen = 'true'; + } + main.classList.toggle('shifted', navSidebarIsOpen === 'true'); + navSidebar.setAttribute('aria-expanded', navSidebarIsOpen); + + toggleNavSidebar.addEventListener('click', function() { + if (navSidebarIsOpen === 'true') { + navSidebarIsOpen = 'false'; + } else { + navSidebarIsOpen = 'true'; + } + localStorage.setItem('django.admin.navSidebarIsOpen', navSidebarIsOpen); + main.classList.toggle('shifted'); + navSidebar.setAttribute('aria-expanded', navSidebarIsOpen); + }); + } + + function initSidebarQuickFilter() { + const options = []; + const navSidebar = document.getElementById('nav-sidebar'); + if (!navSidebar) { + return; + } + navSidebar.querySelectorAll('th[scope=row] a').forEach((container) => { + options.push({title: container.innerHTML, node: container}); + }); + + function checkValue(event) { + let filterValue = event.target.value; + if (filterValue) { + filterValue = filterValue.toLowerCase(); + } + if (event.key === 'Escape') { + filterValue = ''; + event.target.value = ''; // clear input + } + let matches = false; + for (const o of options) { + let displayValue = ''; + if (filterValue) { + if (o.title.toLowerCase().indexOf(filterValue) === -1) { + displayValue = 'none'; + } else { + matches = true; + } + } + // show/hide parent + o.node.parentNode.parentNode.style.display = displayValue; + } + if (!filterValue || matches) { + event.target.classList.remove('no-results'); + } else { + event.target.classList.add('no-results'); + } + sessionStorage.setItem('django.admin.navSidebarFilterValue', filterValue); + } + + const nav = document.getElementById('nav-filter'); + nav.addEventListener('change', checkValue, false); + nav.addEventListener('input', checkValue, false); + nav.addEventListener('keyup', checkValue, false); + + const storedValue = sessionStorage.getItem('django.admin.navSidebarFilterValue'); + if (storedValue) { + nav.value = storedValue; + checkValue({target: nav, key: ''}); + } + } + window.initSidebarQuickFilter = initSidebarQuickFilter; + initSidebarQuickFilter(); +} diff --git a/static/admin/js/popup_response.js b/static/admin/js/popup_response.js new file mode 100644 index 0000000..2b1d3dd --- /dev/null +++ b/static/admin/js/popup_response.js @@ -0,0 +1,16 @@ +/*global opener */ +'use strict'; +{ + const initData = JSON.parse(document.getElementById('django-admin-popup-response-constants').dataset.popupResponse); + switch(initData.action) { + case 'change': + opener.dismissChangeRelatedObjectPopup(window, initData.value, initData.obj, initData.new_value); + break; + case 'delete': + opener.dismissDeleteRelatedObjectPopup(window, initData.value); + break; + default: + opener.dismissAddRelatedObjectPopup(window, initData.value, initData.obj); + break; + } +} diff --git a/static/admin/js/prepopulate.js b/static/admin/js/prepopulate.js new file mode 100644 index 0000000..89e95ab --- /dev/null +++ b/static/admin/js/prepopulate.js @@ -0,0 +1,43 @@ +/*global URLify*/ +'use strict'; +{ + const $ = django.jQuery; + $.fn.prepopulate = function(dependencies, maxLength, allowUnicode) { + /* + Depends on urlify.js + Populates a selected field with the values of the dependent fields, + URLifies and shortens the string. + dependencies - array of dependent fields ids + maxLength - maximum length of the URLify'd string + allowUnicode - Unicode support of the URLify'd string + */ + return this.each(function() { + const prepopulatedField = $(this); + + const populate = function() { + // Bail if the field's value has been changed by the user + if (prepopulatedField.data('_changed')) { + return; + } + + const values = []; + $.each(dependencies, function(i, field) { + field = $(field); + if (field.val().length > 0) { + values.push(field.val()); + } + }); + prepopulatedField.val(URLify(values.join(' '), maxLength, allowUnicode)); + }; + + prepopulatedField.data('_changed', false); + prepopulatedField.on('change', function() { + prepopulatedField.data('_changed', true); + }); + + if (!prepopulatedField.val()) { + $(dependencies.join(',')).on('keyup change focus', populate); + } + }); + }; +} diff --git a/static/admin/js/prepopulate_init.js b/static/admin/js/prepopulate_init.js new file mode 100644 index 0000000..a58841f --- /dev/null +++ b/static/admin/js/prepopulate_init.js @@ -0,0 +1,15 @@ +'use strict'; +{ + const $ = django.jQuery; + const fields = $('#django-admin-prepopulated-fields-constants').data('prepopulatedFields'); + $.each(fields, function(index, field) { + $( + '.empty-form .form-row .field-' + field.name + + ', .empty-form.form-row .field-' + field.name + + ', .empty-form .form-row.field-' + field.name + ).addClass('prepopulated_field'); + $(field.id).data('dependency_list', field.dependency_list).prepopulate( + field.dependency_ids, field.maxLength, field.allowUnicode + ); + }); +} diff --git a/static/admin/js/theme.js b/static/admin/js/theme.js new file mode 100644 index 0000000..794cd15 --- /dev/null +++ b/static/admin/js/theme.js @@ -0,0 +1,56 @@ +'use strict'; +{ + window.addEventListener('load', function(e) { + + function setTheme(mode) { + if (mode !== "light" && mode !== "dark" && mode !== "auto") { + console.error(`Got invalid theme mode: ${mode}. Resetting to auto.`); + mode = "auto"; + } + document.documentElement.dataset.theme = mode; + localStorage.setItem("theme", mode); + } + + function cycleTheme() { + const currentTheme = localStorage.getItem("theme") || "auto"; + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + + if (prefersDark) { + // Auto (dark) -> Light -> Dark + if (currentTheme === "auto") { + setTheme("light"); + } else if (currentTheme === "light") { + setTheme("dark"); + } else { + setTheme("auto"); + } + } else { + // Auto (light) -> Dark -> Light + if (currentTheme === "auto") { + setTheme("dark"); + } else if (currentTheme === "dark") { + setTheme("light"); + } else { + setTheme("auto"); + } + } + } + + function initTheme() { + // set theme defined in localStorage if there is one, or fallback to auto mode + const currentTheme = localStorage.getItem("theme"); + currentTheme ? setTheme(currentTheme) : setTheme("auto"); + } + + function setupTheme() { + // Attach event handlers for toggling themes + const buttons = document.getElementsByClassName("theme-toggle"); + Array.from(buttons).forEach((btn) => { + btn.addEventListener("click", cycleTheme); + }); + initTheme(); + } + + setupTheme(); + }); +} diff --git a/static/admin/js/urlify.js b/static/admin/js/urlify.js new file mode 100644 index 0000000..9fc0409 --- /dev/null +++ b/static/admin/js/urlify.js @@ -0,0 +1,169 @@ +/*global XRegExp*/ +'use strict'; +{ + const LATIN_MAP = { + 'À': 'A', 'Á': 'A', 'Â': 'A', 'Ã': 'A', 'Ä': 'A', 'Å': 'A', 'Æ': 'AE', + 'Ç': 'C', 'È': 'E', 'É': 'E', 'Ê': 'E', 'Ë': 'E', 'Ì': 'I', 'Í': 'I', + 'Î': 'I', 'Ï': 'I', 'Ð': 'D', 'Ñ': 'N', 'Ò': 'O', 'Ó': 'O', 'Ô': 'O', + 'Õ': 'O', 'Ö': 'O', 'Ő': 'O', 'Ø': 'O', 'Ù': 'U', 'Ú': 'U', 'Û': 'U', + 'Ü': 'U', 'Ű': 'U', 'Ý': 'Y', 'Þ': 'TH', 'Ÿ': 'Y', 'ß': 'ss', 'à': 'a', + 'á': 'a', 'â': 'a', 'ã': 'a', 'ä': 'a', 'å': 'a', 'æ': 'ae', 'ç': 'c', + 'è': 'e', 'é': 'e', 'ê': 'e', 'ë': 'e', 'ì': 'i', 'í': 'i', 'î': 'i', + 'ï': 'i', 'ð': 'd', 'ñ': 'n', 'ò': 'o', 'ó': 'o', 'ô': 'o', 'õ': 'o', + 'ö': 'o', 'ő': 'o', 'ø': 'o', 'ù': 'u', 'ú': 'u', 'û': 'u', 'ü': 'u', + 'ű': 'u', 'ý': 'y', 'þ': 'th', 'ÿ': 'y' + }; + const LATIN_SYMBOLS_MAP = { + '©': '(c)' + }; + const GREEK_MAP = { + 'α': 'a', 'β': 'b', 'γ': 'g', 'δ': 'd', 'ε': 'e', 'ζ': 'z', 'η': 'h', + 'θ': '8', 'ι': 'i', 'κ': 'k', 'λ': 'l', 'μ': 'm', 'ν': 'n', 'ξ': '3', + 'ο': 'o', 'π': 'p', 'ρ': 'r', 'σ': 's', 'τ': 't', 'υ': 'y', 'φ': 'f', + 'χ': 'x', 'ψ': 'ps', 'ω': 'w', 'ά': 'a', 'έ': 'e', 'ί': 'i', 'ό': 'o', + 'ύ': 'y', 'ή': 'h', 'ώ': 'w', 'ς': 's', 'ϊ': 'i', 'ΰ': 'y', 'ϋ': 'y', + 'ΐ': 'i', 'Α': 'A', 'Β': 'B', 'Γ': 'G', 'Δ': 'D', 'Ε': 'E', 'Ζ': 'Z', + 'Η': 'H', 'Θ': '8', 'Ι': 'I', 'Κ': 'K', 'Λ': 'L', 'Μ': 'M', 'Ν': 'N', + 'Ξ': '3', 'Ο': 'O', 'Π': 'P', 'Ρ': 'R', 'Σ': 'S', 'Τ': 'T', 'Υ': 'Y', + 'Φ': 'F', 'Χ': 'X', 'Ψ': 'PS', 'Ω': 'W', 'Ά': 'A', 'Έ': 'E', 'Ί': 'I', + 'Ό': 'O', 'Ύ': 'Y', 'Ή': 'H', 'Ώ': 'W', 'Ϊ': 'I', 'Ϋ': 'Y' + }; + const TURKISH_MAP = { + 'ş': 's', 'Ş': 'S', 'ı': 'i', 'İ': 'I', 'ç': 'c', 'Ç': 'C', 'ü': 'u', + 'Ü': 'U', 'ö': 'o', 'Ö': 'O', 'ğ': 'g', 'Ğ': 'G' + }; + const ROMANIAN_MAP = { + 'ă': 'a', 'î': 'i', 'ș': 's', 'ț': 't', 'â': 'a', + 'Ă': 'A', 'Î': 'I', 'Ș': 'S', 'Ț': 'T', 'Â': 'A' + }; + const RUSSIAN_MAP = { + 'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo', + 'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'j', 'к': 'k', 'л': 'l', 'м': 'm', + 'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u', + 'ф': 'f', 'х': 'h', 'ц': 'c', 'ч': 'ch', 'ш': 'sh', 'щ': 'sh', 'ъ': '', + 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya', + 'А': 'A', 'Б': 'B', 'В': 'V', 'Г': 'G', 'Д': 'D', 'Е': 'E', 'Ё': 'Yo', + 'Ж': 'Zh', 'З': 'Z', 'И': 'I', 'Й': 'J', 'К': 'K', 'Л': 'L', 'М': 'M', + 'Н': 'N', 'О': 'O', 'П': 'P', 'Р': 'R', 'С': 'S', 'Т': 'T', 'У': 'U', + 'Ф': 'F', 'Х': 'H', 'Ц': 'C', 'Ч': 'Ch', 'Ш': 'Sh', 'Щ': 'Sh', 'Ъ': '', + 'Ы': 'Y', 'Ь': '', 'Э': 'E', 'Ю': 'Yu', 'Я': 'Ya' + }; + const UKRAINIAN_MAP = { + 'Є': 'Ye', 'І': 'I', 'Ї': 'Yi', 'Ґ': 'G', 'є': 'ye', 'і': 'i', + 'ї': 'yi', 'ґ': 'g' + }; + const CZECH_MAP = { + 'č': 'c', 'ď': 'd', 'ě': 'e', 'ň': 'n', 'ř': 'r', 'š': 's', 'ť': 't', + 'ů': 'u', 'ž': 'z', 'Č': 'C', 'Ď': 'D', 'Ě': 'E', 'Ň': 'N', 'Ř': 'R', + 'Š': 'S', 'Ť': 'T', 'Ů': 'U', 'Ž': 'Z' + }; + const SLOVAK_MAP = { + 'á': 'a', 'ä': 'a', 'č': 'c', 'ď': 'd', 'é': 'e', 'í': 'i', 'ľ': 'l', + 'ĺ': 'l', 'ň': 'n', 'ó': 'o', 'ô': 'o', 'ŕ': 'r', 'š': 's', 'ť': 't', + 'ú': 'u', 'ý': 'y', 'ž': 'z', + 'Á': 'a', 'Ä': 'A', 'Č': 'C', 'Ď': 'D', 'É': 'E', 'Í': 'I', 'Ľ': 'L', + 'Ĺ': 'L', 'Ň': 'N', 'Ó': 'O', 'Ô': 'O', 'Ŕ': 'R', 'Š': 'S', 'Ť': 'T', + 'Ú': 'U', 'Ý': 'Y', 'Ž': 'Z' + }; + const POLISH_MAP = { + 'ą': 'a', 'ć': 'c', 'ę': 'e', 'ł': 'l', 'ń': 'n', 'ó': 'o', 'ś': 's', + 'ź': 'z', 'ż': 'z', + 'Ą': 'A', 'Ć': 'C', 'Ę': 'E', 'Ł': 'L', 'Ń': 'N', 'Ó': 'O', 'Ś': 'S', + 'Ź': 'Z', 'Ż': 'Z' + }; + const LATVIAN_MAP = { + 'ā': 'a', 'č': 'c', 'ē': 'e', 'ģ': 'g', 'ī': 'i', 'ķ': 'k', 'ļ': 'l', + 'ņ': 'n', 'š': 's', 'ū': 'u', 'ž': 'z', + 'Ā': 'A', 'Č': 'C', 'Ē': 'E', 'Ģ': 'G', 'Ī': 'I', 'Ķ': 'K', 'Ļ': 'L', + 'Ņ': 'N', 'Š': 'S', 'Ū': 'U', 'Ž': 'Z' + }; + const ARABIC_MAP = { + 'أ': 'a', 'ب': 'b', 'ت': 't', 'ث': 'th', 'ج': 'g', 'ح': 'h', 'خ': 'kh', 'د': 'd', + 'ذ': 'th', 'ر': 'r', 'ز': 'z', 'س': 's', 'ش': 'sh', 'ص': 's', 'ض': 'd', 'ط': 't', + 'ظ': 'th', 'ع': 'aa', 'غ': 'gh', 'ف': 'f', 'ق': 'k', 'ك': 'k', 'ل': 'l', 'م': 'm', + 'ن': 'n', 'ه': 'h', 'و': 'o', 'ي': 'y' + }; + const LITHUANIAN_MAP = { + 'ą': 'a', 'č': 'c', 'ę': 'e', 'ė': 'e', 'į': 'i', 'š': 's', 'ų': 'u', + 'ū': 'u', 'ž': 'z', + 'Ą': 'A', 'Č': 'C', 'Ę': 'E', 'Ė': 'E', 'Į': 'I', 'Š': 'S', 'Ų': 'U', + 'Ū': 'U', 'Ž': 'Z' + }; + const SERBIAN_MAP = { + 'ђ': 'dj', 'ј': 'j', 'љ': 'lj', 'њ': 'nj', 'ћ': 'c', 'џ': 'dz', + 'đ': 'dj', 'Ђ': 'Dj', 'Ј': 'j', 'Љ': 'Lj', 'Њ': 'Nj', 'Ћ': 'C', + 'Џ': 'Dz', 'Đ': 'Dj' + }; + const AZERBAIJANI_MAP = { + 'ç': 'c', 'ə': 'e', 'ğ': 'g', 'ı': 'i', 'ö': 'o', 'ş': 's', 'ü': 'u', + 'Ç': 'C', 'Ə': 'E', 'Ğ': 'G', 'İ': 'I', 'Ö': 'O', 'Ş': 'S', 'Ü': 'U' + }; + const GEORGIAN_MAP = { + 'ა': 'a', 'ბ': 'b', 'გ': 'g', 'დ': 'd', 'ე': 'e', 'ვ': 'v', 'ზ': 'z', + 'თ': 't', 'ი': 'i', 'კ': 'k', 'ლ': 'l', 'მ': 'm', 'ნ': 'n', 'ო': 'o', + 'პ': 'p', 'ჟ': 'j', 'რ': 'r', 'ს': 's', 'ტ': 't', 'უ': 'u', 'ფ': 'f', + 'ქ': 'q', 'ღ': 'g', 'ყ': 'y', 'შ': 'sh', 'ჩ': 'ch', 'ც': 'c', 'ძ': 'dz', + 'წ': 'w', 'ჭ': 'ch', 'ხ': 'x', 'ჯ': 'j', 'ჰ': 'h' + }; + + const ALL_DOWNCODE_MAPS = [ + LATIN_MAP, + LATIN_SYMBOLS_MAP, + GREEK_MAP, + TURKISH_MAP, + ROMANIAN_MAP, + RUSSIAN_MAP, + UKRAINIAN_MAP, + CZECH_MAP, + SLOVAK_MAP, + POLISH_MAP, + LATVIAN_MAP, + ARABIC_MAP, + LITHUANIAN_MAP, + SERBIAN_MAP, + AZERBAIJANI_MAP, + GEORGIAN_MAP + ]; + + const Downcoder = { + 'Initialize': function() { + if (Downcoder.map) { // already made + return; + } + Downcoder.map = {}; + for (const lookup of ALL_DOWNCODE_MAPS) { + Object.assign(Downcoder.map, lookup); + } + Downcoder.regex = new RegExp(Object.keys(Downcoder.map).join('|'), 'g'); + } + }; + + function downcode(slug) { + Downcoder.Initialize(); + return slug.replace(Downcoder.regex, function(m) { + return Downcoder.map[m]; + }); + } + + + function URLify(s, num_chars, allowUnicode) { + // changes, e.g., "Petty theft" to "petty-theft" + if (!allowUnicode) { + s = downcode(s); + } + s = s.toLowerCase(); // convert to lowercase + // if downcode doesn't hit, the char will be stripped here + if (allowUnicode) { + // Keep Unicode letters including both lowercase and uppercase + // characters, whitespace, and dash; remove other characters. + s = XRegExp.replace(s, XRegExp('[^-_\\p{L}\\p{N}\\s]', 'g'), ''); + } else { + s = s.replace(/[^-\w\s]/g, ''); // remove unneeded chars + } + s = s.replace(/^\s+|\s+$/g, ''); // trim leading/trailing spaces + s = s.replace(/[-\s]+/g, '-'); // convert spaces to hyphens + s = s.substring(0, num_chars); // trim to first num_chars chars + return s.replace(/-+$/g, ''); // trim any trailing hyphens + } + window.URLify = URLify; +} diff --git a/static/admin/js/vendor/jquery/LICENSE.txt b/static/admin/js/vendor/jquery/LICENSE.txt new file mode 100644 index 0000000..f642c3f --- /dev/null +++ b/static/admin/js/vendor/jquery/LICENSE.txt @@ -0,0 +1,20 @@ +Copyright OpenJS Foundation and other contributors, https://openjsf.org/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/static/admin/js/vendor/jquery/jquery.js b/static/admin/js/vendor/jquery/jquery.js new file mode 100644 index 0000000..7f35c11 --- /dev/null +++ b/static/admin/js/vendor/jquery/jquery.js @@ -0,0 +1,10965 @@ +/*! + * jQuery JavaScript Library v3.6.4 + * https://jquery.com/ + * + * Includes Sizzle.js + * https://sizzlejs.com/ + * + * Copyright OpenJS Foundation and other contributors + * Released under the MIT license + * https://jquery.org/license + * + * Date: 2023-03-08T15:28Z + */ +( function( global, factory ) { + + "use strict"; + + if ( typeof module === "object" && typeof module.exports === "object" ) { + + // For CommonJS and CommonJS-like environments where a proper `window` + // is present, execute the factory and get jQuery. + // For environments that do not have a `window` with a `document` + // (such as Node.js), expose a factory as module.exports. + // This accentuates the need for the creation of a real `window`. + // e.g. var jQuery = require("jquery")(window); + // See ticket trac-14549 for more info. + module.exports = global.document ? + factory( global, true ) : + function( w ) { + if ( !w.document ) { + throw new Error( "jQuery requires a window with a document" ); + } + return factory( w ); + }; + } else { + factory( global ); + } + +// Pass this if window is not defined yet +} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) { + +// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1 +// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode +// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common +// enough that all such attempts are guarded in a try block. +"use strict"; + +var arr = []; + +var getProto = Object.getPrototypeOf; + +var slice = arr.slice; + +var flat = arr.flat ? function( array ) { + return arr.flat.call( array ); +} : function( array ) { + return arr.concat.apply( [], array ); +}; + + +var push = arr.push; + +var indexOf = arr.indexOf; + +var class2type = {}; + +var toString = class2type.toString; + +var hasOwn = class2type.hasOwnProperty; + +var fnToString = hasOwn.toString; + +var ObjectFunctionString = fnToString.call( Object ); + +var support = {}; + +var isFunction = function isFunction( obj ) { + + // Support: Chrome <=57, Firefox <=52 + // In some browsers, typeof returns "function" for HTML elements + // (i.e., `typeof document.createElement( "object" ) === "function"`). + // We don't want to classify *any* DOM node as a function. + // Support: QtWeb <=3.8.5, WebKit <=534.34, wkhtmltopdf tool <=0.12.5 + // Plus for old WebKit, typeof returns "function" for HTML collections + // (e.g., `typeof document.getElementsByTagName("div") === "function"`). (gh-4756) + return typeof obj === "function" && typeof obj.nodeType !== "number" && + typeof obj.item !== "function"; + }; + + +var isWindow = function isWindow( obj ) { + return obj != null && obj === obj.window; + }; + + +var document = window.document; + + + + var preservedScriptAttributes = { + type: true, + src: true, + nonce: true, + noModule: true + }; + + function DOMEval( code, node, doc ) { + doc = doc || document; + + var i, val, + script = doc.createElement( "script" ); + + script.text = code; + if ( node ) { + for ( i in preservedScriptAttributes ) { + + // Support: Firefox 64+, Edge 18+ + // Some browsers don't support the "nonce" property on scripts. + // On the other hand, just using `getAttribute` is not enough as + // the `nonce` attribute is reset to an empty string whenever it + // becomes browsing-context connected. + // See https://github.com/whatwg/html/issues/2369 + // See https://html.spec.whatwg.org/#nonce-attributes + // The `node.getAttribute` check was added for the sake of + // `jQuery.globalEval` so that it can fake a nonce-containing node + // via an object. + val = node[ i ] || node.getAttribute && node.getAttribute( i ); + if ( val ) { + script.setAttribute( i, val ); + } + } + } + doc.head.appendChild( script ).parentNode.removeChild( script ); + } + + +function toType( obj ) { + if ( obj == null ) { + return obj + ""; + } + + // Support: Android <=2.3 only (functionish RegExp) + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call( obj ) ] || "object" : + typeof obj; +} +/* global Symbol */ +// Defining this global in .eslintrc.json would create a danger of using the global +// unguarded in another place, it seems safer to define global only for this module + + + +var + version = "3.6.4", + + // Define a local copy of jQuery + jQuery = function( selector, context ) { + + // The jQuery object is actually just the init constructor 'enhanced' + // Need init if jQuery is called (just allow error to be thrown if not included) + return new jQuery.fn.init( selector, context ); + }; + +jQuery.fn = jQuery.prototype = { + + // The current version of jQuery being used + jquery: version, + + constructor: jQuery, + + // The default length of a jQuery object is 0 + length: 0, + + toArray: function() { + return slice.call( this ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + + // Return all the elements in a clean array + if ( num == null ) { + return slice.call( this ); + } + + // Return just the one element from the set + return num < 0 ? this[ num + this.length ] : this[ num ]; + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems ) { + + // Build a new jQuery matched element set + var ret = jQuery.merge( this.constructor(), elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + each: function( callback ) { + return jQuery.each( this, callback ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map( this, function( elem, i ) { + return callback.call( elem, i, elem ); + } ) ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ) ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + even: function() { + return this.pushStack( jQuery.grep( this, function( _elem, i ) { + return ( i + 1 ) % 2; + } ) ); + }, + + odd: function() { + return this.pushStack( jQuery.grep( this, function( _elem, i ) { + return i % 2; + } ) ); + }, + + eq: function( i ) { + var len = this.length, + j = +i + ( i < 0 ? len : 0 ); + return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); + }, + + end: function() { + return this.prevObject || this.constructor(); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: arr.sort, + splice: arr.splice +}; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[ 0 ] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + + // Skip the boolean and the target + target = arguments[ i ] || {}; + i++; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !isFunction( target ) ) { + target = {}; + } + + // Extend jQuery itself if only one argument is passed + if ( i === length ) { + target = this; + i--; + } + + for ( ; i < length; i++ ) { + + // Only deal with non-null/undefined values + if ( ( options = arguments[ i ] ) != null ) { + + // Extend the base object + for ( name in options ) { + copy = options[ name ]; + + // Prevent Object.prototype pollution + // Prevent never-ending loop + if ( name === "__proto__" || target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject( copy ) || + ( copyIsArray = Array.isArray( copy ) ) ) ) { + src = target[ name ]; + + // Ensure proper type for the source value + if ( copyIsArray && !Array.isArray( src ) ) { + clone = []; + } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) { + clone = {}; + } else { + clone = src; + } + copyIsArray = false; + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend( { + + // Unique for each copy of jQuery on the page + expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), + + // Assume jQuery is ready without the ready module + isReady: true, + + error: function( msg ) { + throw new Error( msg ); + }, + + noop: function() {}, + + isPlainObject: function( obj ) { + var proto, Ctor; + + // Detect obvious negatives + // Use toString instead of jQuery.type to catch host objects + if ( !obj || toString.call( obj ) !== "[object Object]" ) { + return false; + } + + proto = getProto( obj ); + + // Objects with no prototype (e.g., `Object.create( null )`) are plain + if ( !proto ) { + return true; + } + + // Objects with prototype are plain iff they were constructed by a global Object function + Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor; + return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString; + }, + + isEmptyObject: function( obj ) { + var name; + + for ( name in obj ) { + return false; + } + return true; + }, + + // Evaluates a script in a provided context; falls back to the global one + // if not specified. + globalEval: function( code, options, doc ) { + DOMEval( code, { nonce: options && options.nonce }, doc ); + }, + + each: function( obj, callback ) { + var length, i = 0; + + if ( isArrayLike( obj ) ) { + length = obj.length; + for ( ; i < length; i++ ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } else { + for ( i in obj ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } + + return obj; + }, + + // results is for internal usage only + makeArray: function( arr, results ) { + var ret = results || []; + + if ( arr != null ) { + if ( isArrayLike( Object( arr ) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); + } else { + push.call( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + return arr == null ? -1 : indexOf.call( arr, elem, i ); + }, + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + merge: function( first, second ) { + var len = +second.length, + j = 0, + i = first.length; + + for ( ; j < len; j++ ) { + first[ i++ ] = second[ j ]; + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, invert ) { + var callbackInverse, + matches = [], + i = 0, + length = elems.length, + callbackExpect = !invert; + + // Go through the array, only saving the items + // that pass the validator function + for ( ; i < length; i++ ) { + callbackInverse = !callback( elems[ i ], i ); + if ( callbackInverse !== callbackExpect ) { + matches.push( elems[ i ] ); + } + } + + return matches; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var length, value, + i = 0, + ret = []; + + // Go through the array, translating each of the items to their new values + if ( isArrayLike( elems ) ) { + length = elems.length; + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + + // Go through every key on the object, + } else { + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + } + + // Flatten any nested arrays + return flat( ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // jQuery.support is not used in Core but other projects attach their + // properties to it so it needs to exist. + support: support +} ); + +if ( typeof Symbol === "function" ) { + jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; +} + +// Populate the class2type map +jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), + function( _i, name ) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); + } ); + +function isArrayLike( obj ) { + + // Support: real iOS 8.2 only (not reproducible in simulator) + // `in` check used to prevent JIT error (gh-2145) + // hasOwn isn't used here due to false negatives + // regarding Nodelist length in IE + var length = !!obj && "length" in obj && obj.length, + type = toType( obj ); + + if ( isFunction( obj ) || isWindow( obj ) ) { + return false; + } + + return type === "array" || length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj; +} +var Sizzle = +/*! + * Sizzle CSS Selector Engine v2.3.10 + * https://sizzlejs.com/ + * + * Copyright JS Foundation and other contributors + * Released under the MIT license + * https://js.foundation/ + * + * Date: 2023-02-14 + */ +( function( window ) { +var i, + support, + Expr, + getText, + isXML, + tokenize, + compile, + select, + outermostContext, + sortInput, + hasDuplicate, + + // Local document vars + setDocument, + document, + docElem, + documentIsHTML, + rbuggyQSA, + rbuggyMatches, + matches, + contains, + + // Instance-specific data + expando = "sizzle" + 1 * new Date(), + preferredDoc = window.document, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + nonnativeSelectorCache = createCache(), + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + } + return 0; + }, + + // Instance methods + hasOwn = ( {} ).hasOwnProperty, + arr = [], + pop = arr.pop, + pushNative = arr.push, + push = arr.push, + slice = arr.slice, + + // Use a stripped-down indexOf as it's faster than native + // https://jsperf.com/thor-indexof-vs-for/5 + indexOf = function( list, elem ) { + var i = 0, + len = list.length; + for ( ; i < len; i++ ) { + if ( list[ i ] === elem ) { + return i; + } + } + return -1; + }, + + booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|" + + "ismap|loop|multiple|open|readonly|required|scoped", + + // Regular expressions + + // http://www.w3.org/TR/css3-selectors/#whitespace + whitespace = "[\\x20\\t\\r\\n\\f]", + + // https://www.w3.org/TR/css-syntax-3/#ident-token-diagram + identifier = "(?:\\\\[\\da-fA-F]{1,6}" + whitespace + + "?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+", + + // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors + attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + + + // Operator (capture 2) + "*([*^$|!~]?=)" + whitespace + + + // "Attribute values must be CSS identifiers [capture 5] + // or strings [capture 3 or capture 4]" + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + + whitespace + "*\\]", + + pseudos = ":(" + identifier + ")(?:\\((" + + + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: + // 1. quoted (capture 3; capture 4 or capture 5) + "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + + // 2. simple (capture 6) + "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + + // 3. anything else (capture 2) + ".*" + + ")\\)|)", + + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rwhitespace = new RegExp( whitespace + "+", "g" ), + rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + + whitespace + "+$", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rleadingCombinator = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + + "*" ), + rdescend = new RegExp( whitespace + "|>" ), + + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), + + matchExpr = { + "ID": new RegExp( "^#(" + identifier + ")" ), + "CLASS": new RegExp( "^\\.(" + identifier + ")" ), + "TAG": new RegExp( "^(" + identifier + "|[*])" ), + "ATTR": new RegExp( "^" + attributes ), + "PSEUDO": new RegExp( "^" + pseudos ), + "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + + whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + + whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), + + // For use in libraries implementing .is() + // We use this for POS matching in `select` + "needsContext": new RegExp( "^" + whitespace + + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, + + rhtml = /HTML$/i, + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + + rnative = /^[^{]+\{\s*\[native \w/, + + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, + + rsibling = /[+~]/, + + // CSS escapes + // http://www.w3.org/TR/CSS21/syndata.html#escaped-characters + runescape = new RegExp( "\\\\[\\da-fA-F]{1,6}" + whitespace + "?|\\\\([^\\r\\n\\f])", "g" ), + funescape = function( escape, nonHex ) { + var high = "0x" + escape.slice( 1 ) - 0x10000; + + return nonHex ? + + // Strip the backslash prefix from a non-hex escape sequence + nonHex : + + // Replace a hexadecimal escape sequence with the encoded Unicode code point + // Support: IE <=11+ + // For values outside the Basic Multilingual Plane (BMP), manually construct a + // surrogate pair + high < 0 ? + String.fromCharCode( high + 0x10000 ) : + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }, + + // CSS string/identifier serialization + // https://drafts.csswg.org/cssom/#common-serializing-idioms + rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g, + fcssescape = function( ch, asCodePoint ) { + if ( asCodePoint ) { + + // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER + if ( ch === "\0" ) { + return "\uFFFD"; + } + + // Control characters and (dependent upon position) numbers get escaped as code points + return ch.slice( 0, -1 ) + "\\" + + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; + } + + // Other potentially-special ASCII characters get backslash-escaped + return "\\" + ch; + }, + + // Used for iframes + // See setDocument() + // Removing the function wrapper causes a "Permission Denied" + // error in IE + unloadHandler = function() { + setDocument(); + }, + + inDisabledFieldset = addCombinator( + function( elem ) { + return elem.disabled === true && elem.nodeName.toLowerCase() === "fieldset"; + }, + { dir: "parentNode", next: "legend" } + ); + +// Optimize for push.apply( _, NodeList ) +try { + push.apply( + ( arr = slice.call( preferredDoc.childNodes ) ), + preferredDoc.childNodes + ); + + // Support: Android<4.0 + // Detect silently failing push.apply + // eslint-disable-next-line no-unused-expressions + arr[ preferredDoc.childNodes.length ].nodeType; +} catch ( e ) { + push = { apply: arr.length ? + + // Leverage slice if possible + function( target, els ) { + pushNative.apply( target, slice.call( els ) ); + } : + + // Support: IE<9 + // Otherwise append directly + function( target, els ) { + var j = target.length, + i = 0; + + // Can't trust NodeList.length + while ( ( target[ j++ ] = els[ i++ ] ) ) {} + target.length = j - 1; + } + }; +} + +function Sizzle( selector, context, results, seed ) { + var m, i, elem, nid, match, groups, newSelector, + newContext = context && context.ownerDocument, + + // nodeType defaults to 9, since context defaults to document + nodeType = context ? context.nodeType : 9; + + results = results || []; + + // Return early from calls with invalid selector or context + if ( typeof selector !== "string" || !selector || + nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { + + return results; + } + + // Try to shortcut find operations (as opposed to filters) in HTML documents + if ( !seed ) { + setDocument( context ); + context = context || document; + + if ( documentIsHTML ) { + + // If the selector is sufficiently simple, try using a "get*By*" DOM method + // (excepting DocumentFragment context, where the methods don't exist) + if ( nodeType !== 11 && ( match = rquickExpr.exec( selector ) ) ) { + + // ID selector + if ( ( m = match[ 1 ] ) ) { + + // Document context + if ( nodeType === 9 ) { + if ( ( elem = context.getElementById( m ) ) ) { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { + return results; + } + + // Element context + } else { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( newContext && ( elem = newContext.getElementById( m ) ) && + contains( context, elem ) && + elem.id === m ) { + + results.push( elem ); + return results; + } + } + + // Type selector + } else if ( match[ 2 ] ) { + push.apply( results, context.getElementsByTagName( selector ) ); + return results; + + // Class selector + } else if ( ( m = match[ 3 ] ) && support.getElementsByClassName && + context.getElementsByClassName ) { + + push.apply( results, context.getElementsByClassName( m ) ); + return results; + } + } + + // Take advantage of querySelectorAll + if ( support.qsa && + !nonnativeSelectorCache[ selector + " " ] && + ( !rbuggyQSA || !rbuggyQSA.test( selector ) ) && + + // Support: IE 8 only + // Exclude object elements + ( nodeType !== 1 || context.nodeName.toLowerCase() !== "object" ) ) { + + newSelector = selector; + newContext = context; + + // qSA considers elements outside a scoping root when evaluating child or + // descendant combinators, which is not what we want. + // In such cases, we work around the behavior by prefixing every selector in the + // list with an ID selector referencing the scope context. + // The technique has to be used as well when a leading combinator is used + // as such selectors are not recognized by querySelectorAll. + // Thanks to Andrew Dupont for this technique. + if ( nodeType === 1 && + ( rdescend.test( selector ) || rleadingCombinator.test( selector ) ) ) { + + // Expand context for sibling selectors + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || + context; + + // We can use :scope instead of the ID hack if the browser + // supports it & if we're not changing the context. + if ( newContext !== context || !support.scope ) { + + // Capture the context ID, setting it first if necessary + if ( ( nid = context.getAttribute( "id" ) ) ) { + nid = nid.replace( rcssescape, fcssescape ); + } else { + context.setAttribute( "id", ( nid = expando ) ); + } + } + + // Prefix every selector in the list + groups = tokenize( selector ); + i = groups.length; + while ( i-- ) { + groups[ i ] = ( nid ? "#" + nid : ":scope" ) + " " + + toSelector( groups[ i ] ); + } + newSelector = groups.join( "," ); + } + + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch ( qsaError ) { + nonnativeSelectorCache( selector, true ); + } finally { + if ( nid === expando ) { + context.removeAttribute( "id" ); + } + } + } + } + } + + // All others + return select( selector.replace( rtrim, "$1" ), context, results, seed ); +} + +/** + * Create key-value caches of limited size + * @returns {function(string, object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ +function createCache() { + var keys = []; + + function cache( key, value ) { + + // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) + if ( keys.push( key + " " ) > Expr.cacheLength ) { + + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return ( cache[ key + " " ] = value ); + } + return cache; +} + +/** + * Mark a function for special use by Sizzle + * @param {Function} fn The function to mark + */ +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} + +/** + * Support testing using an element + * @param {Function} fn Passed the created element and returns a boolean result + */ +function assert( fn ) { + var el = document.createElement( "fieldset" ); + + try { + return !!fn( el ); + } catch ( e ) { + return false; + } finally { + + // Remove from its parent by default + if ( el.parentNode ) { + el.parentNode.removeChild( el ); + } + + // release memory in IE + el = null; + } +} + +/** + * Adds the same handler for all of the specified attrs + * @param {String} attrs Pipe-separated list of attributes + * @param {Function} handler The method that will be applied + */ +function addHandle( attrs, handler ) { + var arr = attrs.split( "|" ), + i = arr.length; + + while ( i-- ) { + Expr.attrHandle[ arr[ i ] ] = handler; + } +} + +/** + * Checks document order of two siblings + * @param {Element} a + * @param {Element} b + * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b + */ +function siblingCheck( a, b ) { + var cur = b && a, + diff = cur && a.nodeType === 1 && b.nodeType === 1 && + a.sourceIndex - b.sourceIndex; + + // Use IE sourceIndex if available on both nodes + if ( diff ) { + return diff; + } + + // Check if b follows a + if ( cur ) { + while ( ( cur = cur.nextSibling ) ) { + if ( cur === b ) { + return -1; + } + } + } + + return a ? 1 : -1; +} + +/** + * Returns a function to use in pseudos for input types + * @param {String} type + */ +function createInputPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for buttons + * @param {String} type + */ +function createButtonPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return ( name === "input" || name === "button" ) && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for :enabled/:disabled + * @param {Boolean} disabled true for :disabled; false for :enabled + */ +function createDisabledPseudo( disabled ) { + + // Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable + return function( elem ) { + + // Only certain elements can match :enabled or :disabled + // https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled + // https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled + if ( "form" in elem ) { + + // Check for inherited disabledness on relevant non-disabled elements: + // * listed form-associated elements in a disabled fieldset + // https://html.spec.whatwg.org/multipage/forms.html#category-listed + // https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled + // * option elements in a disabled optgroup + // https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled + // All such elements have a "form" property. + if ( elem.parentNode && elem.disabled === false ) { + + // Option elements defer to a parent optgroup if present + if ( "label" in elem ) { + if ( "label" in elem.parentNode ) { + return elem.parentNode.disabled === disabled; + } else { + return elem.disabled === disabled; + } + } + + // Support: IE 6 - 11 + // Use the isDisabled shortcut property to check for disabled fieldset ancestors + return elem.isDisabled === disabled || + + // Where there is no isDisabled, check manually + /* jshint -W018 */ + elem.isDisabled !== !disabled && + inDisabledFieldset( elem ) === disabled; + } + + return elem.disabled === disabled; + + // Try to winnow out elements that can't be disabled before trusting the disabled property. + // Some victims get caught in our net (label, legend, menu, track), but it shouldn't + // even exist on them, let alone have a boolean value. + } else if ( "label" in elem ) { + return elem.disabled === disabled; + } + + // Remaining elements are neither :enabled nor :disabled + return false; + }; +} + +/** + * Returns a function to use in pseudos for positionals + * @param {Function} fn + */ +function createPositionalPseudo( fn ) { + return markFunction( function( argument ) { + argument = +argument; + return markFunction( function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + // Match elements found at the specified indexes + while ( i-- ) { + if ( seed[ ( j = matchIndexes[ i ] ) ] ) { + seed[ j ] = !( matches[ j ] = seed[ j ] ); + } + } + } ); + } ); +} + +/** + * Checks a node for validity as a Sizzle context + * @param {Element|Object=} context + * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value + */ +function testContext( context ) { + return context && typeof context.getElementsByTagName !== "undefined" && context; +} + +// Expose support vars for convenience +support = Sizzle.support = {}; + +/** + * Detects XML nodes + * @param {Element|Object} elem An element or a document + * @returns {Boolean} True iff elem is a non-HTML XML node + */ +isXML = Sizzle.isXML = function( elem ) { + var namespace = elem && elem.namespaceURI, + docElem = elem && ( elem.ownerDocument || elem ).documentElement; + + // Support: IE <=8 + // Assume HTML when documentElement doesn't yet exist, such as inside loading iframes + // https://bugs.jquery.com/ticket/4833 + return !rhtml.test( namespace || docElem && docElem.nodeName || "HTML" ); +}; + +/** + * Sets document-related variables once based on the current document + * @param {Element|Object} [doc] An element or document object to use to set the document + * @returns {Object} Returns the current document + */ +setDocument = Sizzle.setDocument = function( node ) { + var hasCompare, subWindow, + doc = node ? node.ownerDocument || node : preferredDoc; + + // Return early if doc is invalid or already selected + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( doc == document || doc.nodeType !== 9 || !doc.documentElement ) { + return document; + } + + // Update global variables + document = doc; + docElem = document.documentElement; + documentIsHTML = !isXML( document ); + + // Support: IE 9 - 11+, Edge 12 - 18+ + // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936) + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( preferredDoc != document && + ( subWindow = document.defaultView ) && subWindow.top !== subWindow ) { + + // Support: IE 11, Edge + if ( subWindow.addEventListener ) { + subWindow.addEventListener( "unload", unloadHandler, false ); + + // Support: IE 9 - 10 only + } else if ( subWindow.attachEvent ) { + subWindow.attachEvent( "onunload", unloadHandler ); + } + } + + // Support: IE 8 - 11+, Edge 12 - 18+, Chrome <=16 - 25 only, Firefox <=3.6 - 31 only, + // Safari 4 - 5 only, Opera <=11.6 - 12.x only + // IE/Edge & older browsers don't support the :scope pseudo-class. + // Support: Safari 6.0 only + // Safari 6.0 supports :scope but it's an alias of :root there. + support.scope = assert( function( el ) { + docElem.appendChild( el ).appendChild( document.createElement( "div" ) ); + return typeof el.querySelectorAll !== "undefined" && + !el.querySelectorAll( ":scope fieldset div" ).length; + } ); + + // Support: Chrome 105 - 110+, Safari 15.4 - 16.3+ + // Make sure the the `:has()` argument is parsed unforgivingly. + // We include `*` in the test to detect buggy implementations that are + // _selectively_ forgiving (specifically when the list includes at least + // one valid selector). + // Note that we treat complete lack of support for `:has()` as if it were + // spec-compliant support, which is fine because use of `:has()` in such + // environments will fail in the qSA path and fall back to jQuery traversal + // anyway. + support.cssHas = assert( function() { + try { + document.querySelector( ":has(*,:jqfake)" ); + return false; + } catch ( e ) { + return true; + } + } ); + + /* Attributes + ---------------------------------------------------------------------- */ + + // Support: IE<8 + // Verify that getAttribute really returns attributes and not properties + // (excepting IE8 booleans) + support.attributes = assert( function( el ) { + el.className = "i"; + return !el.getAttribute( "className" ); + } ); + + /* getElement(s)By* + ---------------------------------------------------------------------- */ + + // Check if getElementsByTagName("*") returns only elements + support.getElementsByTagName = assert( function( el ) { + el.appendChild( document.createComment( "" ) ); + return !el.getElementsByTagName( "*" ).length; + } ); + + // Support: IE<9 + support.getElementsByClassName = rnative.test( document.getElementsByClassName ); + + // Support: IE<10 + // Check if getElementById returns elements by name + // The broken getElementById methods don't pick up programmatically-set names, + // so use a roundabout getElementsByName test + support.getById = assert( function( el ) { + docElem.appendChild( el ).id = expando; + return !document.getElementsByName || !document.getElementsByName( expando ).length; + } ); + + // ID filter and find + if ( support.getById ) { + Expr.filter[ "ID" ] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + return elem.getAttribute( "id" ) === attrId; + }; + }; + Expr.find[ "ID" ] = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var elem = context.getElementById( id ); + return elem ? [ elem ] : []; + } + }; + } else { + Expr.filter[ "ID" ] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== "undefined" && + elem.getAttributeNode( "id" ); + return node && node.value === attrId; + }; + }; + + // Support: IE 6 - 7 only + // getElementById is not reliable as a find shortcut + Expr.find[ "ID" ] = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var node, i, elems, + elem = context.getElementById( id ); + + if ( elem ) { + + // Verify the id attribute + node = elem.getAttributeNode( "id" ); + if ( node && node.value === id ) { + return [ elem ]; + } + + // Fall back on getElementsByName + elems = context.getElementsByName( id ); + i = 0; + while ( ( elem = elems[ i++ ] ) ) { + node = elem.getAttributeNode( "id" ); + if ( node && node.value === id ) { + return [ elem ]; + } + } + } + + return []; + } + }; + } + + // Tag + Expr.find[ "TAG" ] = support.getElementsByTagName ? + function( tag, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( tag ); + + // DocumentFragment nodes don't have gEBTN + } else if ( support.qsa ) { + return context.querySelectorAll( tag ); + } + } : + + function( tag, context ) { + var elem, + tmp = [], + i = 0, + + // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too + results = context.getElementsByTagName( tag ); + + // Filter out possible comments + if ( tag === "*" ) { + while ( ( elem = results[ i++ ] ) ) { + if ( elem.nodeType === 1 ) { + tmp.push( elem ); + } + } + + return tmp; + } + return results; + }; + + // Class + Expr.find[ "CLASS" ] = support.getElementsByClassName && function( className, context ) { + if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { + return context.getElementsByClassName( className ); + } + }; + + /* QSA/matchesSelector + ---------------------------------------------------------------------- */ + + // QSA and matchesSelector support + + // matchesSelector(:active) reports false when true (IE9/Opera 11.5) + rbuggyMatches = []; + + // qSa(:focus) reports false when true (Chrome 21) + // We allow this because of a bug in IE8/9 that throws an error + // whenever `document.activeElement` is accessed on an iframe + // So, we allow :focus to pass through QSA all the time to avoid the IE error + // See https://bugs.jquery.com/ticket/13378 + rbuggyQSA = []; + + if ( ( support.qsa = rnative.test( document.querySelectorAll ) ) ) { + + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert( function( el ) { + + var input; + + // Select is set to empty string on purpose + // This is to test IE's treatment of not explicitly + // setting a boolean content attribute, + // since its presence should be enough + // https://bugs.jquery.com/ticket/12359 + docElem.appendChild( el ).innerHTML = "" + + ""; + + // Support: IE8, Opera 11-12.16 + // Nothing should be selected when empty strings follow ^= or $= or *= + // The test attribute must be unknown in Opera but "safe" for WinRT + // https://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section + if ( el.querySelectorAll( "[msallowcapture^='']" ).length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); + } + + // Support: IE8 + // Boolean attributes and "value" are not treated correctly + if ( !el.querySelectorAll( "[selected]" ).length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); + } + + // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+ + if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) { + rbuggyQSA.push( "~=" ); + } + + // Support: IE 11+, Edge 15 - 18+ + // IE 11/Edge don't find elements on a `[name='']` query in some cases. + // Adding a temporary attribute to the document before the selection works + // around the issue. + // Interestingly, IE 10 & older don't seem to have the issue. + input = document.createElement( "input" ); + input.setAttribute( "name", "" ); + el.appendChild( input ); + if ( !el.querySelectorAll( "[name='']" ).length ) { + rbuggyQSA.push( "\\[" + whitespace + "*name" + whitespace + "*=" + + whitespace + "*(?:''|\"\")" ); + } + + // Webkit/Opera - :checked should return selected option elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + // IE8 throws error here and will not see later tests + if ( !el.querySelectorAll( ":checked" ).length ) { + rbuggyQSA.push( ":checked" ); + } + + // Support: Safari 8+, iOS 8+ + // https://bugs.webkit.org/show_bug.cgi?id=136851 + // In-page `selector#id sibling-combinator selector` fails + if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) { + rbuggyQSA.push( ".#.+[+~]" ); + } + + // Support: Firefox <=3.6 - 5 only + // Old Firefox doesn't throw on a badly-escaped identifier. + el.querySelectorAll( "\\\f" ); + rbuggyQSA.push( "[\\r\\n\\f]" ); + } ); + + assert( function( el ) { + el.innerHTML = "" + + ""; + + // Support: Windows 8 Native Apps + // The type and name attributes are restricted during .innerHTML assignment + var input = document.createElement( "input" ); + input.setAttribute( "type", "hidden" ); + el.appendChild( input ).setAttribute( "name", "D" ); + + // Support: IE8 + // Enforce case-sensitivity of name attribute + if ( el.querySelectorAll( "[name=d]" ).length ) { + rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); + } + + // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) + // IE8 throws error here and will not see later tests + if ( el.querySelectorAll( ":enabled" ).length !== 2 ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Support: IE9-11+ + // IE's :disabled selector does not pick up the children of disabled fieldsets + docElem.appendChild( el ).disabled = true; + if ( el.querySelectorAll( ":disabled" ).length !== 2 ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Support: Opera 10 - 11 only + // Opera 10-11 does not throw on post-comma invalid pseudos + el.querySelectorAll( "*,:x" ); + rbuggyQSA.push( ",.*:" ); + } ); + } + + if ( ( support.matchesSelector = rnative.test( ( matches = docElem.matches || + docElem.webkitMatchesSelector || + docElem.mozMatchesSelector || + docElem.oMatchesSelector || + docElem.msMatchesSelector ) ) ) ) { + + assert( function( el ) { + + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9) + support.disconnectedMatch = matches.call( el, "*" ); + + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( el, "[s!='']:x" ); + rbuggyMatches.push( "!=", pseudos ); + } ); + } + + if ( !support.cssHas ) { + + // Support: Chrome 105 - 110+, Safari 15.4 - 16.3+ + // Our regular `try-catch` mechanism fails to detect natively-unsupported + // pseudo-classes inside `:has()` (such as `:has(:contains("Foo"))`) + // in browsers that parse the `:has()` argument as a forgiving selector list. + // https://drafts.csswg.org/selectors/#relational now requires the argument + // to be parsed unforgivingly, but browsers have not yet fully adjusted. + rbuggyQSA.push( ":has" ); + } + + rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join( "|" ) ); + rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join( "|" ) ); + + /* Contains + ---------------------------------------------------------------------- */ + hasCompare = rnative.test( docElem.compareDocumentPosition ); + + // Element contains another + // Purposefully self-exclusive + // As in, an element does not contain itself + contains = hasCompare || rnative.test( docElem.contains ) ? + function( a, b ) { + + // Support: IE <9 only + // IE doesn't have `contains` on `document` so we need to check for + // `documentElement` presence. + // We need to fall back to `a` when `documentElement` is missing + // as `ownerDocument` of elements within `