diff --git a/.env.example b/.env.example index bd257180a..414e9e774 100644 --- a/.env.example +++ b/.env.example @@ -163,6 +163,21 @@ WEB_HTTPS_PORT=443 # DEFAULT: error NGINX_ERROR_LOG_LEVEL=error +# Sets the maximum allowed size of the client request body. +# DEFAULT: 10g +NGINX_CLIENT_MAX_BODY_SIZE=10g + +# Timeout for establishing a connection with the proxied server +# DEFAULT: 5s +NGINX_PROXY_CONNECT_TIMEOUT=5s + +# Timeout for reading a response from the proxied server. +# DEFAULT: 300s +NGINX_PROXY_READ_TIMEOUT=300s + +# Timeout for transmitting a request to the proxied server. +# DEFAULT: 300s +NGINX_PROXY_SEND_TIMEOUT=300s ################## # Gunicorn settings @@ -449,8 +464,8 @@ SMTP4DEV_IMAP_PORT=143 # see this gist: https://gist.github.com/gounux/2c0779fcb22e512cbdc613eb78200571 # Migration to a newer database version is a risky operation to your data, so prepare and test the backup of the `postgres_data` volume. # NOTE: Ignored if `db` is not used. -# DEFAULT: 13-3.1-alpine -POSTGIS_IMAGE_VERSION=13-3.1-alpine +# DEFAULT: 17-3.5-alpine +POSTGIS_IMAGE_VERSION=17-3.5-alpine # Local admin username configuration for minio storage in local and standalone instances. # NOTE: Ignored if `minio` is not used. @@ -520,9 +535,9 @@ DEBUG_WORKER_WRAPPER_DEBUGPY_PORT=5679 # DEFAULT: "" DEBUG_QGIS_DEBUGPY_PORT="" -# Host path which will be mounted by the `worker_wrapper` into the `worker` containers to facilitate development and debugging pythons files. -# Will mount `qfc_worker`, `entrypoint.py`, and if existent - `qfieldcloud-sdk` and `libqfieldsync`. -# If empty value or invalid value, the copied by docker or pip installed versions will be used. +# Host path which will be mounted by the `worker_wrapper` into the `worker` containers to facilitate development and debugging python files. +# Will mount `qfc_worker`, `entrypoint.py`, and if exists - `qfieldcloud-sdk` and `libqfieldsync`. +# If empty or invalid value, the original code from the docker image (either copied or pip installed) will be used. # DEFAULT: "" DEBUG_QGIS_WORKER_HOST_PATH="" diff --git a/.github/workflows/build_and_push.yml b/.github/workflows/build_and_push.yml index 0ea65cfcc..344eb100b 100644 --- a/.github/workflows/build_and_push.yml +++ b/.github/workflows/build_and_push.yml @@ -1,6 +1,7 @@ name: Build and Push on: push: + pull_request: release: types: [published] @@ -12,61 +13,53 @@ jobs: fail-fast: true matrix: services: - - - service_name: app + - service_name: app dockerfile: docker-app/Dockerfile docker_context: docker-app docker_target: webserver_runtime enable_dockerhub: true - - - service_name: app-test + - service_name: app-test dockerfile: docker-app/Dockerfile docker_context: docker-app docker_target: webserver_test enable_dockerhub: false - - - service_name: worker-wrapper + - service_name: worker-wrapper dockerfile: docker-app/Dockerfile docker_context: docker-app docker_target: worker_wrapper_runtime enable_dockerhub: true - - - service_name: qgis + - service_name: qgis dockerfile: docker-qgis/Dockerfile docker_context: docker-qgis enable_dockerhub: true - - - service_name: nginx + - service_name: nginx dockerfile: docker-nginx/Dockerfile docker_context: docker-nginx enable_dockerhub: true - - - service_name: createbuckets + - service_name: createbuckets dockerfile: docker-createbuckets/Dockerfile docker_context: docker-createbuckets enable_dockerhub: true steps: - - - name: Checkout + - name: Checkout uses: actions/checkout@v4 - - - name: Docker Metadata + - name: Docker Metadata id: docker_metadata uses: docker/metadata-action@v5 with: images: | - name=ghcr.io/opengisch/qfieldcloud-${{ matrix.services.service_name }},enable=true - name=opengisch/qfieldcloud-${{ matrix.services.service_name }},enable=${{ matrix.services.enable_dockerhub && (github.event_name == 'release' || github.ref_name == 'master') }} + name=ghcr.io/${{ github.repository_owner }}/qfieldcloud-${{ matrix.services.service_name }},enable=true + name=${{ secrets.DOCKER_HUB_USERNAME }}/qfieldcloud-${{ matrix.services.service_name }},enable=${{ matrix.services.enable_dockerhub && (github.event_name == 'release' || github.ref_name == 'master' || (github.event_name == 'pull_request' && github.base_ref == 'master')) }} flavor: | - latest=auto + latest=${{ github.event_name == 'release' || github.ref_name == 'master' || (github.event_name == 'pull_request' && github.base_ref == 'master') }} prefix=,onlatest=false suffix= @@ -76,36 +69,33 @@ jobs: type=match,pattern=v(.*),group=1 type=edge,branch=master - - - name: Login to GitHub Container Repository (ghcr.io) + - name: Login to GitHub Container Repository (ghcr.io) uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - - name: Login to Docker Hub + - name: Login to Docker Hub if: ${{ matrix.services.enable_dockerhub && (github.event_name == 'release' || github.ref_name == 'master') }} uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - - - name: Set up Docker Buildx + - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@v3 - - - name: Docker Build ${{ matrix.services.service_name }} + - name: Docker Build ${{ matrix.services.service_name }} id: docker_test_application uses: docker/build-push-action@v6 with: file: ${{ matrix.services.dockerfile }} context: ${{ matrix.services.docker_context }} + platforms: linux/amd64,linux/arm64/v8 push: ${{ matrix.services.enable_dockerhub && (github.event_name == 'release' || github.ref_name == 'master') }} target: ${{ matrix.services.docker_target }} tags: ${{ steps.docker_metadata.outputs.tags }} - cache-from: type=registry,ref=ghcr.io/opengisch/qfieldcloud-${{ matrix.services.service_name }}:buildcache - cache-to: type=registry,ref=ghcr.io/opengisch/qfieldcloud-${{ matrix.services.service_name }}:buildcache,mode=max + cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/qfieldcloud-${{ matrix.services.service_name }}:buildcache + cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/qfieldcloud-${{ matrix.services.service_name }}:buildcache,mode=max diff --git a/.github/workflows/sync_translations.yml b/.github/workflows/sync_translations.yml index 11e454c9d..3281315f5 100644 --- a/.github/workflows/sync_translations.yml +++ b/.github/workflows/sync_translations.yml @@ -1,14 +1,12 @@ name: 🌎 Synchronize translations with Transifex on: - schedule: - - cron: "0 2 * * *" - workflow_dispatch: + schedule: + - cron: "0 2 * * *" + workflow_dispatch: jobs: - sync_translations: - name: Synchronize Transifex translations runs-on: ubuntu-latest @@ -16,16 +14,28 @@ jobs: TX_TOKEN: ${{ secrets.TX_TOKEN }} steps: - - name: Checkout code uses: actions/checkout@v4 with: token: ${{ secrets.NYUKI_TOKEN }} - - name: Install Transifex CLI + - name: Install transifex client run: | + # Detect architecture and download appropriate binary + ARCH=$(uname -m) + if [ "$ARCH" = "x86_64" ]; then curl -OL https://github.com/transifex/cli/releases/download/v1.6.17/tx-linux-amd64.tar.gz tar -xvzf tx-linux-amd64.tar.gz + elif [ "$ARCH" = "aarch64" ]; then + curl -OL https://github.com/transifex/cli/releases/download/v1.6.17/tx-linux-arm64.tar.gz + tar -xvzf tx-linux-arm64.tar.gz + else + echo "Unsupported architecture: $ARCH" && exit 1 + fi + sudo mv tx /usr/local/bin/tx + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - name: Copy .env file run: | @@ -33,18 +43,18 @@ jobs: - name: Perform docker pre operations run: | - docker compose pull - docker compose build + docker compose pull + docker compose build - name: Generate Django translation po files run: | - docker compose run --user root app python manage.py makemessages -l es + docker compose run --user root app python manage.py makemessages -l es - name: Push translation files to Transifex - run: ./tx push --source + run: tx push --source - name: Pull from Transifex - run: ./tx pull --all --minimum-perc 0 --force + run: tx pull --all --minimum-perc 0 --force - name: Add and commit new translations uses: EndBug/add-and-commit@v9 diff --git a/.gitignore b/.gitignore index b4e6712aa..224d3d6d8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ __pycache__/ .coverage .vscode .env +.env.test docker-compose.override.yml client/projects conf/mkcert/* @@ -16,6 +17,7 @@ Pipfile* **/site-packages docker-qgis/libqfieldsync docker-qgis/qfieldcloud-sdk-python +.venv # compiled translations docker-app/qfieldcloud/locale/**/*.mo diff --git a/README.md b/README.md index 48e8de7cf..bed797912 100644 --- a/README.md +++ b/README.md @@ -274,7 +274,7 @@ Do not forget to copy the site packages every time any of the `requirements*.txt QFieldCloud will automatically generate a certificate and its root certificate in `./conf/nginx/certs`. However, you need to trust the root certificate first, -so other programs (e.g. curl) can create secure connection to the local QFieldCloud instance. +so other programs (e.g. `curl`) can create secure connection to the local QFieldCloud instance. On Debian/Ubuntu, copy the root certificate to the directory with trusted certificates. Note the extension has been changed to `.crt`: @@ -307,7 +307,7 @@ Now connecting with `curl` should fail with a similar error: ## Code style -Code style done with [`precommit`](https://pre-commit.com): +Code style done with [`pre-commit`](https://pre-commit.com): pip install pre-commit # install pre-commit hook @@ -402,7 +402,7 @@ For great `nginx` logs, use: ### Storage -You can use either the integrated `minio` object storage, or use an external provider (e. g. S3) with versioning enabled. Check the corresponding `STORAGE_*` environment variables for more info. +You can use either the integrated `minio` object storage, or use an external provider (e.g. S3) with versioning enabled. Check the corresponding `STORAGE_*` environment variables for more info. ### Database diff --git a/docker-app/Dockerfile b/docker-app/Dockerfile index 50b873115..617d33811 100644 --- a/docker-app/Dockerfile +++ b/docker-app/Dockerfile @@ -3,6 +3,7 @@ ########################## # pull a builder image, the same as the base +# This base image supports both AMD64 and ARM64 architectures FROM python:3.10-slim-bookworm AS build # Disable annoying pip version check, we don't care if pip is slightly older @@ -11,17 +12,33 @@ ARG PIP_DISABLE_PIP_VERSION_CHECK=1 # Do not create and use redundant cache dir in the current user home ARG PIP_NO_CACHE_DIR=1 -# install psycopg2 requirements +# install psycopg2 and GDAL requirements for building packages like fiona RUN apt-get update \ && apt-get install -y \ libpq-dev \ python3-dev \ gcc \ + g++ \ +# GDAL development headers needed for building fiona and other geospatial packages + libgdal-dev \ + gdal-bin \ + git \ +# GDAL development headers needed for building fiona and other geospatial packages on ARM64 + g++ \ +# GDAL development headers needed for building fiona and other geospatial packages + libgdal-dev \ + gdal-bin \ && rm -rf /var/lib/apt/lists/* # install `pip-compile` (as part of `pip-tools`) RUN pip3 install pip-tools +# Set GDAL environment variables for building packages like fiona +ENV GDAL_CONFIG=/usr/bin/gdal-config + +# Set GDAL environment variables for building packages like fiona +ENV GDAL_CONFIG=/usr/bin/gdal-config + # install pip dependencies COPY ./requirements/requirements.txt /requirements/requirements.txt RUN pip3 install -r requirements/requirements.txt \ @@ -35,6 +52,7 @@ RUN ls -Q /usr/local/lib/python3.10/site-packages/botocore/data | grep -xv "endp ########################## # pull official base image +# This base image supports both AMD64 and ARM64 architectures FROM python:3.10-slim-bookworm AS base # set work directory @@ -47,12 +65,24 @@ ENV PYTHONUNBUFFERED=1 # NOTE while using ARG would be more appropriate, the following vars would have to be redifined for each build stage. ENV PIP_DISABLE_PIP_VERSION_CHECK=1 ENV PIP_NO_CACHE_DIR=1 +# GDAL configuration for building geospatial packages +ENV GDAL_CONFIG=/usr/bin/gdal-config +# GDAL configuration for building geospatial packages +ENV GDAL_CONFIG=/usr/bin/gdal-config # install dependencies RUN apt-get update \ && apt-get install -y \ # GeoDjango as recommended at https://docs.djangoproject.com/en/4.1/ref/contrib/gis/install/geolibs/#installing-geospatial-libraries binutils libproj-dev gdal-bin \ +# GDAL development headers needed for building packages like fiona in test environments + libgdal-dev \ +# Build tools needed for compiling Python packages with C++ extensions + gcc g++ python3-dev \ +# GDAL development headers needed for building packages like fiona in test environments + libgdal-dev \ +# Build tools needed for compiling Python packages with C++ extensions + gcc g++ python3-dev \ # needed for Django's `manage.py makemessages` gettext \ # for development purposes only (optional dependency for `django-extensions`) diff --git a/docker-app/qfieldcloud/core/admin.py b/docker-app/qfieldcloud/core/admin.py index f14c2c4eb..8245087f9 100644 --- a/docker-app/qfieldcloud/core/admin.py +++ b/docker-app/qfieldcloud/core/admin.py @@ -25,6 +25,7 @@ from django.contrib.admin.templatetags.admin_urls import admin_urlname from django.contrib.admin.views.main import ChangeList from django.contrib.auth.models import Group +from django.contrib.auth.views import redirect_to_login from django.core.exceptions import PermissionDenied, ValidationError from django.db.models import Q, QuerySet from django.db.models.fields.json import JSONField @@ -84,6 +85,22 @@ def __init__(self, *args, **kwargs) -> None: for _model, model_admin in self._registry.items(): model_admin.admin_site = self + def login( + self, request: HttpRequest, extra_context: dict[str, Any] | None = None + ) -> HttpResponse: + """Override the default Django admin login view to redirect to the Allauth's login view.""" + if request.method == "GET" and self.has_permission(request): + # Already logged-in, redirect to admin index + index_path = reverse("admin:index", current_app=self.name) + return HttpResponseRedirect(index_path) + + # If the user is not authenticated, redirect to the accounts login page, but keep the query string that has the original request URL. + return redirect_to_login( + request.GET.get("next", ""), login_url=reverse(settings.LOGIN_URL) + ) + + # TODO consider adding a logout view to redirect to the Allauth's logout view, but then we lose the nice template we have right now. + qfc_admin_site = QfcAdminSite(name="qfc_admin_site") @@ -874,6 +891,7 @@ class ProjectAdmin(QFieldCloudModelAdmin): "created_at", "updated_at", "data_last_updated_at", + "restricted_data_last_updated_at", "data_last_packaged_at", "project_details__pre", "locked_at", @@ -892,6 +910,7 @@ class ProjectAdmin(QFieldCloudModelAdmin): "created_at", "updated_at", "data_last_updated_at", + "restricted_data_last_updated_at", "data_last_packaged_at", "project_details__pre", "locked_at", diff --git a/docker-app/qfieldcloud/core/context_processors.py b/docker-app/qfieldcloud/core/context_processors.py new file mode 100644 index 000000000..7ba3e9b18 --- /dev/null +++ b/docker-app/qfieldcloud/core/context_processors.py @@ -0,0 +1,12 @@ +from typing import Any + +from allauth.account.utils import get_adapter +from django.http import HttpRequest + + +def signup_open(request: HttpRequest) -> dict[str, Any]: + adapter = get_adapter() + + return { + "is_signup_open": adapter.is_open_for_signup(request), + } diff --git a/docker-app/qfieldcloud/core/management/commands/dequeue.py b/docker-app/qfieldcloud/core/management/commands/dequeue.py index 87357c70a..40098fb22 100644 --- a/docker-app/qfieldcloud/core/management/commands/dequeue.py +++ b/docker-app/qfieldcloud/core/management/commands/dequeue.py @@ -9,7 +9,7 @@ from django.db import connection, transaction from django.db.models import Q from qfieldcloud.core.models import Job -from worker_wrapper.wrapper import ( +from worker_wrapper.factory import ( ApplyDeltaJobRun, JobRun, PackageJobRun, diff --git a/docker-app/qfieldcloud/core/management/commands/dequeue_k8s.py b/docker-app/qfieldcloud/core/management/commands/dequeue_k8s.py new file mode 100644 index 000000000..596ab6b9a --- /dev/null +++ b/docker-app/qfieldcloud/core/management/commands/dequeue_k8s.py @@ -0,0 +1,145 @@ +import logging +import signal +from time import sleep +from typing import Any + +from django.conf import settings +from django.contrib.contenttypes.models import ContentType +from django.core.management.base import BaseCommand, CommandParser +from django.db import connection, transaction +from django.db.models import Q +from qfieldcloud.core.models import Job + +# Import based on configured backend +if getattr(settings, "QFIELDCLOUD_WORKER_BACKEND", "docker") in ["kubernetes", "k8s"]: + from worker_wrapper.k8s_wrapper import ( + K8sApplyDeltaJobRun as ApplyDeltaJobRun, + K8sJobRun as JobRun, + K8sPackageJobRun as PackageJobRun, + K8sProcessProjectfileJobRun as ProcessProjectfileJobRun, + cancel_orphaned_k8s_workers as cancel_orphaned_workers, + ) +else: + from worker_wrapper.wrapper import ( + ApplyDeltaJobRun, + JobRun, + PackageJobRun, + ProcessProjectfileJobRun, + cancel_orphaned_workers, + ) + +SECONDS = 5 + + +class GracefulKiller: + alive = True + + def __init__(self) -> None: + signal.signal(signal.SIGINT, self._kill) + signal.signal(signal.SIGTERM, self._kill) + + def _kill(self, *_args: Any) -> None: + self.alive = False + + +class Command(BaseCommand): + help = "Dequeue QFieldCloud Jobs from the DB" + + def add_arguments(self, parser: CommandParser) -> None: + parser.add_argument( + "--single-shot", action="store_true", help="Don't run infinite loop." + ) + + def handle( + self, + *args: Any, + single_shot: bool | None = None, + **kwargs: Any, + ) -> None: + backend = getattr(settings, "QFIELDCLOUD_WORKER_BACKEND", "docker") + logging.info(f"Dequeue QFieldCloud Jobs from the DB using {backend} backend") + killer = GracefulKiller() + + while killer.alive: + # the worker-wrapper caches outdated ContentType ids during tests since + # the worker-wrapper and the tests reside in different containers + if settings.DATABASES["default"]["NAME"].startswith("test_"): + ContentType.objects.clear_cache() + + cancel_orphaned_workers() + + with connection.cursor() as cursor: + # NOTE `pg_is_in_recovery` returns `FALSE` if connected to the master node + cursor.execute("SELECT pg_is_in_recovery()") + # there is no way `cursor.fetchone()` returns no rows, therefore ignore the type warning + if cursor.fetchone()[0]: # type: ignore + raise Exception( + "Expected `worker_wrapper` to be connected to the master DB node!" + ) + + queued_job = None + + with transaction.atomic(): + with connection.cursor() as cursor: + cursor.execute("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ") + + busy_projects_ids_qs = Job.objects.filter( + status__in=[ + Job.Status.QUEUED, + Job.Status.STARTED, + ] + ).values("project_id") + + # select all the pending jobs, that their project has no other active job or `locked_at` is not null + jobs_qs = ( + Job.objects.select_for_update(skip_locked=True) + .filter(status=Job.Status.PENDING) + .exclude( + Q(project_id__in=busy_projects_ids_qs) + # skip all projects that are currently locked, most probably because of file transfer + | Q(project__locked_at__isnull=False), + ) + .order_by("created_at") + ) + + # each `worker_wrapper` or `dequeue.py` script can handle only one job and we handle the oldest + queued_job = jobs_qs.first() + + # there might be no jobs in the queue + if queued_job: + logging.info(f"Dequeued job {queued_job.id}, run!") + queued_job.status = Job.Status.QUEUED + queued_job.save(update_fields=["status"]) + + if queued_job: + self._run(queued_job) + queued_job = None + else: + if single_shot: + break + + for _i in range(SECONDS): + if killer.alive: + cancel_orphaned_workers() + sleep(1) + + if single_shot: + break + + def get_job_mapping(self) -> dict[Job.Type, type[JobRun]]: + return { + Job.Type.PACKAGE: PackageJobRun, + Job.Type.DELTA_APPLY: ApplyDeltaJobRun, + Job.Type.PROCESS_PROJECTFILE: ProcessProjectfileJobRun, + } + + def _run(self, job: Job) -> None: + job_run_classes = self.get_job_mapping() + + if job.type in job_run_classes: + job_run_class = job_run_classes[job.type] + else: + raise NotImplementedError(f"Unknown job type {job.type}") + + job_run = job_run_class(job.id) + job_run.run() diff --git a/docker-app/qfieldcloud/core/migrations/0092_project_restricted_data_last_updated_at.py b/docker-app/qfieldcloud/core/migrations/0092_project_restricted_data_last_updated_at.py new file mode 100644 index 000000000..8be1689d5 --- /dev/null +++ b/docker-app/qfieldcloud/core/migrations/0092_project_restricted_data_last_updated_at.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.25 on 2025-11-10 12:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0091_remove_geodb"), + ] + + operations = [ + migrations.AddField( + model_name="project", + name="restricted_data_last_updated_at", + field=models.DateTimeField( + blank=True, + editable=False, + null=True, + verbose_name="Restricted data last updated at", + ), + ), + ] diff --git a/docker-app/qfieldcloud/core/models.py b/docker-app/qfieldcloud/core/models.py index b6595654d..f89321fba 100644 --- a/docker-app/qfieldcloud/core/models.py +++ b/docker-app/qfieldcloud/core/models.py @@ -1262,6 +1262,13 @@ class Meta: ), ) + restricted_data_last_updated_at = models.DateTimeField( + _("Restricted data last updated at"), + blank=True, + null=True, + editable=False, + ) + @cached_property def shared_datasets_project(self) -> Project | None: """ @@ -1797,6 +1804,7 @@ def owner_can_create_job(self): def save(self, recompute_storage=False, *args, **kwargs): self.clean() logger.debug(f"Saving project {self}...") + additional_update_fields = set() if recompute_storage: # TODO Delete with QF-4963 Drop support for legacy storage @@ -1809,16 +1817,25 @@ def save(self, recompute_storage=False, *args, **kwargs): file_storage_bytes=Sum("versions__size", default=0) )["file_storage_bytes"] + additional_update_fields.add("file_storage_bytes") + # Ensure that the Project's storage_keep_versions is at least 1, and reflects the plan's default storage_keep_versions value. if not self.storage_keep_versions: self.storage_keep_versions = ( self.owner.useraccount.current_subscription.plan.storage_keep_versions ) + additional_update_fields.add("storage_keep_versions") + assert self.storage_keep_versions >= 1, ( "If 0, storage_keep_versions mean that all file versions are deleted!" ) + if kwargs.get("update_fields") is not None: + kwargs["update_fields"] = list( + set(kwargs["update_fields"]) | additional_update_fields + ) + super().save(*args, **kwargs) def get_file(self, filename: str) -> File: diff --git a/docker-app/qfieldcloud/core/serializers.py b/docker-app/qfieldcloud/core/serializers.py index 85751719f..6267f9828 100644 --- a/docker-app/qfieldcloud/core/serializers.py +++ b/docker-app/qfieldcloud/core/serializers.py @@ -147,6 +147,7 @@ class Meta: "updated_at", "data_last_packaged_at", "data_last_updated_at", + "restricted_data_last_updated_at", "can_repackage", "needs_repackaging", "status", @@ -163,6 +164,7 @@ class Meta: "updated_at", "data_last_packaged_at", "data_last_updated_at", + "restricted_data_last_updated_at", "can_repackage", "needs_repackaging", "status", diff --git a/docker-app/qfieldcloud/core/staticfiles/css/qfieldcloud.css b/docker-app/qfieldcloud/core/staticfiles/css/qfieldcloud.css new file mode 100644 index 000000000..aae99ec17 --- /dev/null +++ b/docker-app/qfieldcloud/core/staticfiles/css/qfieldcloud.css @@ -0,0 +1,71 @@ +:root { + --bs-gray: #6c757d; + --bs-gray-dark: #343a40; + --bs-gray-light: #f5f5f5; + --bs-primary: #0d6efd; + --bs-secondary: #6c757d; + --bs-success: #87af87; + --bs-info: #0dcaf0; + --bs-warning: #9e6a03; + --bs-danger: #dc3545; +} + +h1, h2, h3, h4, h5, h6 { + overflow-wrap: anywhere; + word-break: normal; +} + +.container { + width: auto; + max-width: 1024px; + padding: 0 15px; +} + +.footer { + background-color: #f5f5f5; +} + +@media (max-width: 768px) { + .w-sm-100 { + width: 100% !important; + } + + .modal-dialog { + margin: 0.5rem; + } +} + +.qfc-header-logo { + height: 1.5rem; +} + +.ml-5rem { + margin-left: 5rem; +} + +.toasts { + position: absolute; + top: 5rem; + right: 1rem; + z-index: 10000; +} + +.toast-logo { + max-height: 2rem; + max-width: 2rem; +} + +.qfc-cookie-banner { + position: fixed; + bottom: 0; + left: 0; + z-index: 2147483645; + box-sizing: border-box; + width: 100%; + background-color: rgba(230, 236, 234, 0.5); +} + +.required label::after { + content: ' *'; + color: red; +} diff --git a/docker-app/qfieldcloud/core/staticfiles/css/vendor.css b/docker-app/qfieldcloud/core/staticfiles/css/vendor.css new file mode 100644 index 000000000..23555e0eb --- /dev/null +++ b/docker-app/qfieldcloud/core/staticfiles/css/vendor.css @@ -0,0 +1,9977 @@ +@charset "UTF-8"; +/* basic bootstrap colors +$colors: ( + "blue": $blue, + "indigo": $indigo, + "purple": $purple, + "pink": $pink, + "red": $red, + "orange": $orange, + "yellow": $yellow, + "green": $green, + "teal": $teal, + "cyan": $cyan, + "white": $white, + "gray": $gray-600, + "gray-dark": $gray-800 +) */ +/* bootstrap theme colors +$theme-colors: map-merge(( + "primary": $primary, + "secondary": $secondary, + "success": $success, + "info": $info, + "warning": $warning, + "danger": $danger, + "light": $light, + "dark": $dark +), $theme-colors); +*/ +/*! + * Bootstrap v4.6.1 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +:root { + --blue: #4a6fae; + --indigo: #6610f2; + --purple: #6f42c1; + --pink: #e83e8c; + --red: red; + --orange: #fd7e14; + --yellow: #ffc107; + --green: #80cc28; + --teal: #20c997; + --cyan: #17a2b8; + --white: #fff; + --gray: #6c757d; + --gray-dark: #343a40; + --primary: #4a6fae; + --secondary: #6c757d; + --success: #80cc28; + --info: #17a2b8; + --warning: #ffc107; + --danger: red; + --light: #f5f5f5; + --dark: #343a40; + --breakpoint-xs: 0; + --breakpoint-sm: 576px; + --breakpoint-md: 768px; + --breakpoint-lg: 992px; + --breakpoint-xl: 1200px; + --font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + line-height: 1.15; + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { + display: block; +} + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #212529; + text-align: left; + background-color: #fff; +} + +[tabindex="-1"]:focus:not(:focus-visible) { + outline: 0 !important; +} + +hr { + box-sizing: content-box; + height: 0; + overflow: visible; +} + +h1, h2, h3, h4, h5, h6 { + margin-top: 0; + margin-bottom: 0.5rem; +} + +p { + margin-top: 0; + margin-bottom: 1rem; +} + +abbr[title], +abbr[data-original-title] { + text-decoration: underline; + text-decoration: underline dotted; + cursor: help; + border-bottom: 0; + text-decoration-skip-ink: none; +} + +address { + margin-bottom: 1rem; + font-style: normal; + line-height: inherit; +} + +ol, +ul, +dl { + margin-top: 0; + margin-bottom: 1rem; +} + +ol ol, +ul ul, +ol ul, +ul ol { + margin-bottom: 0; +} + +dt { + font-weight: 700; +} + +dd { + margin-bottom: 0.5rem; + margin-left: 0; +} + +blockquote { + margin: 0 0 1rem; +} + +b, +strong { + font-weight: bolder; +} + +small { + font-size: 80%; +} + +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +a { + color: #4a6fae; + text-decoration: none; + background-color: transparent; +} +a:hover { + color: #334d78; + text-decoration: underline; +} + +a:not([href]):not([class]) { + color: inherit; + text-decoration: none; +} +a:not([href]):not([class]):hover { + color: inherit; + text-decoration: none; +} + +pre, +code, +kbd, +samp { + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-size: 1em; +} + +pre { + margin-top: 0; + margin-bottom: 1rem; + overflow: auto; + -ms-overflow-style: scrollbar; +} + +figure { + margin: 0 0 1rem; +} + +img { + vertical-align: middle; + border-style: none; +} + +svg { + overflow: hidden; + vertical-align: middle; +} + +table { + border-collapse: collapse; +} + +caption { + padding-top: 0.75rem; + padding-bottom: 0.75rem; + color: #6c757d; + text-align: left; + caption-side: bottom; +} + +th { + text-align: inherit; + text-align: -webkit-match-parent; +} + +label { + display: inline-block; + margin-bottom: 0.5rem; +} + +button { + border-radius: 0; +} + +button:focus:not(:focus-visible) { + outline: 0; +} + +input, +button, +select, +optgroup, +textarea { + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +button, +input { + overflow: visible; +} + +button, +select { + text-transform: none; +} + +[role=button] { + cursor: pointer; +} + +select { + word-wrap: normal; +} + +button, +[type=button], +[type=reset], +[type=submit] { + -webkit-appearance: button; +} + +button:not(:disabled), +[type=button]:not(:disabled), +[type=reset]:not(:disabled), +[type=submit]:not(:disabled) { + cursor: pointer; +} + +button::-moz-focus-inner, +[type=button]::-moz-focus-inner, +[type=reset]::-moz-focus-inner, +[type=submit]::-moz-focus-inner { + padding: 0; + border-style: none; +} + +input[type=radio], +input[type=checkbox] { + box-sizing: border-box; + padding: 0; +} + +textarea { + overflow: auto; + resize: vertical; +} + +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} + +legend { + display: block; + width: 100%; + max-width: 100%; + padding: 0; + margin-bottom: 0.5rem; + font-size: 1.5rem; + line-height: inherit; + color: inherit; + white-space: normal; +} + +progress { + vertical-align: baseline; +} + +[type=number]::-webkit-inner-spin-button, +[type=number]::-webkit-outer-spin-button { + height: auto; +} + +[type=search] { + outline-offset: -2px; + -webkit-appearance: none; +} + +[type=search]::-webkit-search-decoration { + -webkit-appearance: none; +} + +::-webkit-file-upload-button { + font: inherit; + -webkit-appearance: button; +} + +output { + display: inline-block; +} + +summary { + display: list-item; + cursor: pointer; +} + +template { + display: none; +} + +[hidden] { + display: none !important; +} + +h1, h2, h3, h4, h5, h6, +.h1, .h2, .h3, .h4, .h5, .h6 { + margin-bottom: 0.5rem; + font-weight: 500; + line-height: 1.2; +} + +h1, .h1 { + font-size: 2.5rem; +} + +h2, .h2 { + font-size: 2rem; +} + +h3, .h3 { + font-size: 1.75rem; +} + +h4, .h4 { + font-size: 1.5rem; +} + +h5, .h5 { + font-size: 1.25rem; +} + +h6, .h6 { + font-size: 1rem; +} + +.lead { + font-size: 1.25rem; + font-weight: 300; +} + +.display-1 { + font-size: 6rem; + font-weight: 300; + line-height: 1.2; +} + +.display-2 { + font-size: 5.5rem; + font-weight: 300; + line-height: 1.2; +} + +.display-3 { + font-size: 4.5rem; + font-weight: 300; + line-height: 1.2; +} + +.display-4 { + font-size: 3.5rem; + font-weight: 300; + line-height: 1.2; +} + +hr { + margin-top: 1rem; + margin-bottom: 1rem; + border: 0; + border-top: 1px solid rgba(0, 0, 0, 0.1); +} + +small, +.small { + font-size: 80%; + font-weight: 400; +} + +mark, +.mark { + padding: 0.2em; + background-color: #fcf8e3; +} + +.list-unstyled { + padding-left: 0; + list-style: none; +} + +.list-inline { + padding-left: 0; + list-style: none; +} + +.list-inline-item { + display: inline-block; +} +.list-inline-item:not(:last-child) { + margin-right: 0.5rem; +} + +.initialism { + font-size: 90%; + text-transform: uppercase; +} + +.blockquote { + margin-bottom: 1rem; + font-size: 1.25rem; +} + +.blockquote-footer { + display: block; + font-size: 80%; + color: #6c757d; +} +.blockquote-footer::before { + content: "— "; +} + +.img-fluid { + max-width: 100%; + height: auto; +} + +.img-thumbnail { + padding: 0.25rem; + background-color: #fff; + border: 1px solid #dee2e6; + border-radius: 0.25rem; + max-width: 100%; + height: auto; +} + +.figure { + display: inline-block; +} + +.figure-img { + margin-bottom: 0.5rem; + line-height: 1; +} + +.figure-caption { + font-size: 90%; + color: #6c757d; +} + +code { + font-size: 87.5%; + color: #e83e8c; + word-wrap: break-word; +} +a > code { + color: inherit; +} + +kbd { + padding: 0.2rem 0.4rem; + font-size: 87.5%; + color: #fff; + background-color: #212529; + border-radius: 0.2rem; +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: 700; +} + +pre { + display: block; + font-size: 87.5%; + color: #212529; +} +pre code { + font-size: inherit; + color: inherit; + word-break: normal; +} + +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} + +.container, +.container-fluid, +.container-xl, +.container-lg, +.container-md, +.container-sm { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + .container-sm, .container { + max-width: 540px; + } +} +@media (min-width: 768px) { + .container-md, .container-sm, .container { + max-width: 720px; + } +} +@media (min-width: 992px) { + .container-lg, .container-md, .container-sm, .container { + max-width: 960px; + } +} +@media (min-width: 1200px) { + .container-xl, .container-lg, .container-md, .container-sm, .container { + max-width: 1140px; + } +} +.row { + display: flex; + flex-wrap: wrap; + margin-right: -15px; + margin-left: -15px; +} + +.no-gutters { + margin-right: 0; + margin-left: 0; +} +.no-gutters > .col, +.no-gutters > [class*=col-] { + padding-right: 0; + padding-left: 0; +} + +.col-xl, +.col-xl-auto, .col-xl-12, .col-xl-11, .col-xl-10, .col-xl-9, .col-xl-8, .col-xl-7, .col-xl-6, .col-xl-5, .col-xl-4, .col-xl-3, .col-xl-2, .col-xl-1, .col-lg, +.col-lg-auto, .col-lg-12, .col-lg-11, .col-lg-10, .col-lg-9, .col-lg-8, .col-lg-7, .col-lg-6, .col-lg-5, .col-lg-4, .col-lg-3, .col-lg-2, .col-lg-1, .col-md, +.col-md-auto, .col-md-12, .col-md-11, .col-md-10, .col-md-9, .col-md-8, .col-md-7, .col-md-6, .col-md-5, .col-md-4, .col-md-3, .col-md-2, .col-md-1, .col-sm, +.col-sm-auto, .col-sm-12, .col-sm-11, .col-sm-10, .col-sm-9, .col-sm-8, .col-sm-7, .col-sm-6, .col-sm-5, .col-sm-4, .col-sm-3, .col-sm-2, .col-sm-1, .col, +.col-auto, .col-12, .col-11, .col-10, .col-9, .col-8, .col-7, .col-6, .col-5, .col-4, .col-3, .col-2, .col-1 { + position: relative; + width: 100%; + padding-right: 15px; + padding-left: 15px; +} + +.col { + flex-basis: 0; + flex-grow: 1; + max-width: 100%; +} + +.row-cols-1 > * { + flex: 0 0 100%; + max-width: 100%; +} + +.row-cols-2 > * { + flex: 0 0 50%; + max-width: 50%; +} + +.row-cols-3 > * { + flex: 0 0 33.3333333333%; + max-width: 33.3333333333%; +} + +.row-cols-4 > * { + flex: 0 0 25%; + max-width: 25%; +} + +.row-cols-5 > * { + flex: 0 0 20%; + max-width: 20%; +} + +.row-cols-6 > * { + flex: 0 0 16.6666666667%; + max-width: 16.6666666667%; +} + +.col-auto { + flex: 0 0 auto; + width: auto; + max-width: 100%; +} + +.col-1 { + flex: 0 0 8.33333333%; + max-width: 8.33333333%; +} + +.col-2 { + flex: 0 0 16.66666667%; + max-width: 16.66666667%; +} + +.col-3 { + flex: 0 0 25%; + max-width: 25%; +} + +.col-4 { + flex: 0 0 33.33333333%; + max-width: 33.33333333%; +} + +.col-5 { + flex: 0 0 41.66666667%; + max-width: 41.66666667%; +} + +.col-6 { + flex: 0 0 50%; + max-width: 50%; +} + +.col-7 { + flex: 0 0 58.33333333%; + max-width: 58.33333333%; +} + +.col-8 { + flex: 0 0 66.66666667%; + max-width: 66.66666667%; +} + +.col-9 { + flex: 0 0 75%; + max-width: 75%; +} + +.col-10 { + flex: 0 0 83.33333333%; + max-width: 83.33333333%; +} + +.col-11 { + flex: 0 0 91.66666667%; + max-width: 91.66666667%; +} + +.col-12 { + flex: 0 0 100%; + max-width: 100%; +} + +.order-first { + order: -1; +} + +.order-last { + order: 13; +} + +.order-0 { + order: 0; +} + +.order-1 { + order: 1; +} + +.order-2 { + order: 2; +} + +.order-3 { + order: 3; +} + +.order-4 { + order: 4; +} + +.order-5 { + order: 5; +} + +.order-6 { + order: 6; +} + +.order-7 { + order: 7; +} + +.order-8 { + order: 8; +} + +.order-9 { + order: 9; +} + +.order-10 { + order: 10; +} + +.order-11 { + order: 11; +} + +.order-12 { + order: 12; +} + +.offset-1 { + margin-left: 8.33333333%; +} + +.offset-2 { + margin-left: 16.66666667%; +} + +.offset-3 { + margin-left: 25%; +} + +.offset-4 { + margin-left: 33.33333333%; +} + +.offset-5 { + margin-left: 41.66666667%; +} + +.offset-6 { + margin-left: 50%; +} + +.offset-7 { + margin-left: 58.33333333%; +} + +.offset-8 { + margin-left: 66.66666667%; +} + +.offset-9 { + margin-left: 75%; +} + +.offset-10 { + margin-left: 83.33333333%; +} + +.offset-11 { + margin-left: 91.66666667%; +} + +@media (min-width: 576px) { + .col-sm { + flex-basis: 0; + flex-grow: 1; + max-width: 100%; + } + + .row-cols-sm-1 > * { + flex: 0 0 100%; + max-width: 100%; + } + + .row-cols-sm-2 > * { + flex: 0 0 50%; + max-width: 50%; + } + + .row-cols-sm-3 > * { + flex: 0 0 33.3333333333%; + max-width: 33.3333333333%; + } + + .row-cols-sm-4 > * { + flex: 0 0 25%; + max-width: 25%; + } + + .row-cols-sm-5 > * { + flex: 0 0 20%; + max-width: 20%; + } + + .row-cols-sm-6 > * { + flex: 0 0 16.6666666667%; + max-width: 16.6666666667%; + } + + .col-sm-auto { + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + + .col-sm-1 { + flex: 0 0 8.33333333%; + max-width: 8.33333333%; + } + + .col-sm-2 { + flex: 0 0 16.66666667%; + max-width: 16.66666667%; + } + + .col-sm-3 { + flex: 0 0 25%; + max-width: 25%; + } + + .col-sm-4 { + flex: 0 0 33.33333333%; + max-width: 33.33333333%; + } + + .col-sm-5 { + flex: 0 0 41.66666667%; + max-width: 41.66666667%; + } + + .col-sm-6 { + flex: 0 0 50%; + max-width: 50%; + } + + .col-sm-7 { + flex: 0 0 58.33333333%; + max-width: 58.33333333%; + } + + .col-sm-8 { + flex: 0 0 66.66666667%; + max-width: 66.66666667%; + } + + .col-sm-9 { + flex: 0 0 75%; + max-width: 75%; + } + + .col-sm-10 { + flex: 0 0 83.33333333%; + max-width: 83.33333333%; + } + + .col-sm-11 { + flex: 0 0 91.66666667%; + max-width: 91.66666667%; + } + + .col-sm-12 { + flex: 0 0 100%; + max-width: 100%; + } + + .order-sm-first { + order: -1; + } + + .order-sm-last { + order: 13; + } + + .order-sm-0 { + order: 0; + } + + .order-sm-1 { + order: 1; + } + + .order-sm-2 { + order: 2; + } + + .order-sm-3 { + order: 3; + } + + .order-sm-4 { + order: 4; + } + + .order-sm-5 { + order: 5; + } + + .order-sm-6 { + order: 6; + } + + .order-sm-7 { + order: 7; + } + + .order-sm-8 { + order: 8; + } + + .order-sm-9 { + order: 9; + } + + .order-sm-10 { + order: 10; + } + + .order-sm-11 { + order: 11; + } + + .order-sm-12 { + order: 12; + } + + .offset-sm-0 { + margin-left: 0; + } + + .offset-sm-1 { + margin-left: 8.33333333%; + } + + .offset-sm-2 { + margin-left: 16.66666667%; + } + + .offset-sm-3 { + margin-left: 25%; + } + + .offset-sm-4 { + margin-left: 33.33333333%; + } + + .offset-sm-5 { + margin-left: 41.66666667%; + } + + .offset-sm-6 { + margin-left: 50%; + } + + .offset-sm-7 { + margin-left: 58.33333333%; + } + + .offset-sm-8 { + margin-left: 66.66666667%; + } + + .offset-sm-9 { + margin-left: 75%; + } + + .offset-sm-10 { + margin-left: 83.33333333%; + } + + .offset-sm-11 { + margin-left: 91.66666667%; + } +} +@media (min-width: 768px) { + .col-md { + flex-basis: 0; + flex-grow: 1; + max-width: 100%; + } + + .row-cols-md-1 > * { + flex: 0 0 100%; + max-width: 100%; + } + + .row-cols-md-2 > * { + flex: 0 0 50%; + max-width: 50%; + } + + .row-cols-md-3 > * { + flex: 0 0 33.3333333333%; + max-width: 33.3333333333%; + } + + .row-cols-md-4 > * { + flex: 0 0 25%; + max-width: 25%; + } + + .row-cols-md-5 > * { + flex: 0 0 20%; + max-width: 20%; + } + + .row-cols-md-6 > * { + flex: 0 0 16.6666666667%; + max-width: 16.6666666667%; + } + + .col-md-auto { + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + + .col-md-1 { + flex: 0 0 8.33333333%; + max-width: 8.33333333%; + } + + .col-md-2 { + flex: 0 0 16.66666667%; + max-width: 16.66666667%; + } + + .col-md-3 { + flex: 0 0 25%; + max-width: 25%; + } + + .col-md-4 { + flex: 0 0 33.33333333%; + max-width: 33.33333333%; + } + + .col-md-5 { + flex: 0 0 41.66666667%; + max-width: 41.66666667%; + } + + .col-md-6 { + flex: 0 0 50%; + max-width: 50%; + } + + .col-md-7 { + flex: 0 0 58.33333333%; + max-width: 58.33333333%; + } + + .col-md-8 { + flex: 0 0 66.66666667%; + max-width: 66.66666667%; + } + + .col-md-9 { + flex: 0 0 75%; + max-width: 75%; + } + + .col-md-10 { + flex: 0 0 83.33333333%; + max-width: 83.33333333%; + } + + .col-md-11 { + flex: 0 0 91.66666667%; + max-width: 91.66666667%; + } + + .col-md-12 { + flex: 0 0 100%; + max-width: 100%; + } + + .order-md-first { + order: -1; + } + + .order-md-last { + order: 13; + } + + .order-md-0 { + order: 0; + } + + .order-md-1 { + order: 1; + } + + .order-md-2 { + order: 2; + } + + .order-md-3 { + order: 3; + } + + .order-md-4 { + order: 4; + } + + .order-md-5 { + order: 5; + } + + .order-md-6 { + order: 6; + } + + .order-md-7 { + order: 7; + } + + .order-md-8 { + order: 8; + } + + .order-md-9 { + order: 9; + } + + .order-md-10 { + order: 10; + } + + .order-md-11 { + order: 11; + } + + .order-md-12 { + order: 12; + } + + .offset-md-0 { + margin-left: 0; + } + + .offset-md-1 { + margin-left: 8.33333333%; + } + + .offset-md-2 { + margin-left: 16.66666667%; + } + + .offset-md-3 { + margin-left: 25%; + } + + .offset-md-4 { + margin-left: 33.33333333%; + } + + .offset-md-5 { + margin-left: 41.66666667%; + } + + .offset-md-6 { + margin-left: 50%; + } + + .offset-md-7 { + margin-left: 58.33333333%; + } + + .offset-md-8 { + margin-left: 66.66666667%; + } + + .offset-md-9 { + margin-left: 75%; + } + + .offset-md-10 { + margin-left: 83.33333333%; + } + + .offset-md-11 { + margin-left: 91.66666667%; + } +} +@media (min-width: 992px) { + .col-lg { + flex-basis: 0; + flex-grow: 1; + max-width: 100%; + } + + .row-cols-lg-1 > * { + flex: 0 0 100%; + max-width: 100%; + } + + .row-cols-lg-2 > * { + flex: 0 0 50%; + max-width: 50%; + } + + .row-cols-lg-3 > * { + flex: 0 0 33.3333333333%; + max-width: 33.3333333333%; + } + + .row-cols-lg-4 > * { + flex: 0 0 25%; + max-width: 25%; + } + + .row-cols-lg-5 > * { + flex: 0 0 20%; + max-width: 20%; + } + + .row-cols-lg-6 > * { + flex: 0 0 16.6666666667%; + max-width: 16.6666666667%; + } + + .col-lg-auto { + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + + .col-lg-1 { + flex: 0 0 8.33333333%; + max-width: 8.33333333%; + } + + .col-lg-2 { + flex: 0 0 16.66666667%; + max-width: 16.66666667%; + } + + .col-lg-3 { + flex: 0 0 25%; + max-width: 25%; + } + + .col-lg-4 { + flex: 0 0 33.33333333%; + max-width: 33.33333333%; + } + + .col-lg-5 { + flex: 0 0 41.66666667%; + max-width: 41.66666667%; + } + + .col-lg-6 { + flex: 0 0 50%; + max-width: 50%; + } + + .col-lg-7 { + flex: 0 0 58.33333333%; + max-width: 58.33333333%; + } + + .col-lg-8 { + flex: 0 0 66.66666667%; + max-width: 66.66666667%; + } + + .col-lg-9 { + flex: 0 0 75%; + max-width: 75%; + } + + .col-lg-10 { + flex: 0 0 83.33333333%; + max-width: 83.33333333%; + } + + .col-lg-11 { + flex: 0 0 91.66666667%; + max-width: 91.66666667%; + } + + .col-lg-12 { + flex: 0 0 100%; + max-width: 100%; + } + + .order-lg-first { + order: -1; + } + + .order-lg-last { + order: 13; + } + + .order-lg-0 { + order: 0; + } + + .order-lg-1 { + order: 1; + } + + .order-lg-2 { + order: 2; + } + + .order-lg-3 { + order: 3; + } + + .order-lg-4 { + order: 4; + } + + .order-lg-5 { + order: 5; + } + + .order-lg-6 { + order: 6; + } + + .order-lg-7 { + order: 7; + } + + .order-lg-8 { + order: 8; + } + + .order-lg-9 { + order: 9; + } + + .order-lg-10 { + order: 10; + } + + .order-lg-11 { + order: 11; + } + + .order-lg-12 { + order: 12; + } + + .offset-lg-0 { + margin-left: 0; + } + + .offset-lg-1 { + margin-left: 8.33333333%; + } + + .offset-lg-2 { + margin-left: 16.66666667%; + } + + .offset-lg-3 { + margin-left: 25%; + } + + .offset-lg-4 { + margin-left: 33.33333333%; + } + + .offset-lg-5 { + margin-left: 41.66666667%; + } + + .offset-lg-6 { + margin-left: 50%; + } + + .offset-lg-7 { + margin-left: 58.33333333%; + } + + .offset-lg-8 { + margin-left: 66.66666667%; + } + + .offset-lg-9 { + margin-left: 75%; + } + + .offset-lg-10 { + margin-left: 83.33333333%; + } + + .offset-lg-11 { + margin-left: 91.66666667%; + } +} +@media (min-width: 1200px) { + .col-xl { + flex-basis: 0; + flex-grow: 1; + max-width: 100%; + } + + .row-cols-xl-1 > * { + flex: 0 0 100%; + max-width: 100%; + } + + .row-cols-xl-2 > * { + flex: 0 0 50%; + max-width: 50%; + } + + .row-cols-xl-3 > * { + flex: 0 0 33.3333333333%; + max-width: 33.3333333333%; + } + + .row-cols-xl-4 > * { + flex: 0 0 25%; + max-width: 25%; + } + + .row-cols-xl-5 > * { + flex: 0 0 20%; + max-width: 20%; + } + + .row-cols-xl-6 > * { + flex: 0 0 16.6666666667%; + max-width: 16.6666666667%; + } + + .col-xl-auto { + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + + .col-xl-1 { + flex: 0 0 8.33333333%; + max-width: 8.33333333%; + } + + .col-xl-2 { + flex: 0 0 16.66666667%; + max-width: 16.66666667%; + } + + .col-xl-3 { + flex: 0 0 25%; + max-width: 25%; + } + + .col-xl-4 { + flex: 0 0 33.33333333%; + max-width: 33.33333333%; + } + + .col-xl-5 { + flex: 0 0 41.66666667%; + max-width: 41.66666667%; + } + + .col-xl-6 { + flex: 0 0 50%; + max-width: 50%; + } + + .col-xl-7 { + flex: 0 0 58.33333333%; + max-width: 58.33333333%; + } + + .col-xl-8 { + flex: 0 0 66.66666667%; + max-width: 66.66666667%; + } + + .col-xl-9 { + flex: 0 0 75%; + max-width: 75%; + } + + .col-xl-10 { + flex: 0 0 83.33333333%; + max-width: 83.33333333%; + } + + .col-xl-11 { + flex: 0 0 91.66666667%; + max-width: 91.66666667%; + } + + .col-xl-12 { + flex: 0 0 100%; + max-width: 100%; + } + + .order-xl-first { + order: -1; + } + + .order-xl-last { + order: 13; + } + + .order-xl-0 { + order: 0; + } + + .order-xl-1 { + order: 1; + } + + .order-xl-2 { + order: 2; + } + + .order-xl-3 { + order: 3; + } + + .order-xl-4 { + order: 4; + } + + .order-xl-5 { + order: 5; + } + + .order-xl-6 { + order: 6; + } + + .order-xl-7 { + order: 7; + } + + .order-xl-8 { + order: 8; + } + + .order-xl-9 { + order: 9; + } + + .order-xl-10 { + order: 10; + } + + .order-xl-11 { + order: 11; + } + + .order-xl-12 { + order: 12; + } + + .offset-xl-0 { + margin-left: 0; + } + + .offset-xl-1 { + margin-left: 8.33333333%; + } + + .offset-xl-2 { + margin-left: 16.66666667%; + } + + .offset-xl-3 { + margin-left: 25%; + } + + .offset-xl-4 { + margin-left: 33.33333333%; + } + + .offset-xl-5 { + margin-left: 41.66666667%; + } + + .offset-xl-6 { + margin-left: 50%; + } + + .offset-xl-7 { + margin-left: 58.33333333%; + } + + .offset-xl-8 { + margin-left: 66.66666667%; + } + + .offset-xl-9 { + margin-left: 75%; + } + + .offset-xl-10 { + margin-left: 83.33333333%; + } + + .offset-xl-11 { + margin-left: 91.66666667%; + } +} +.table { + width: 100%; + margin-bottom: 1rem; + color: #212529; +} +.table th, +.table td { + padding: 0.75rem; + vertical-align: top; + border-top: 1px solid #dee2e6; +} +.table thead th { + vertical-align: bottom; + border-bottom: 2px solid #dee2e6; +} +.table tbody + tbody { + border-top: 2px solid #dee2e6; +} + +.table-sm th, +.table-sm td { + padding: 0.3rem; +} + +.table-bordered { + border: 1px solid #dee2e6; +} +.table-bordered th, +.table-bordered td { + border: 1px solid #dee2e6; +} +.table-bordered thead th, +.table-bordered thead td { + border-bottom-width: 2px; +} + +.table-borderless th, +.table-borderless td, +.table-borderless thead th, +.table-borderless tbody + tbody { + border: 0; +} + +.table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(0, 0, 0, 0.05); +} + +.table-hover tbody tr:hover { + color: #212529; + background-color: rgba(0, 0, 0, 0.075); +} + +.table-primary, +.table-primary > th, +.table-primary > td { + background-color: #ccd7e8; +} +.table-primary th, +.table-primary td, +.table-primary thead th, +.table-primary tbody + tbody { + border-color: #a1b4d5; +} + +.table-hover .table-primary:hover { + background-color: #bac9e0; +} +.table-hover .table-primary:hover > td, +.table-hover .table-primary:hover > th { + background-color: #bac9e0; +} + +.table-secondary, +.table-secondary > th, +.table-secondary > td { + background-color: #d6d8db; +} +.table-secondary th, +.table-secondary td, +.table-secondary thead th, +.table-secondary tbody + tbody { + border-color: #b3b7bb; +} + +.table-hover .table-secondary:hover { + background-color: #c8cbcf; +} +.table-hover .table-secondary:hover > td, +.table-hover .table-secondary:hover > th { + background-color: #c8cbcf; +} + +.table-success, +.table-success > th, +.table-success > td { + background-color: #dbf1c3; +} +.table-success th, +.table-success td, +.table-success thead th, +.table-success tbody + tbody { + border-color: #bde48f; +} + +.table-hover .table-success:hover { + background-color: #cfecae; +} +.table-hover .table-success:hover > td, +.table-hover .table-success:hover > th { + background-color: #cfecae; +} + +.table-info, +.table-info > th, +.table-info > td { + background-color: #bee5eb; +} +.table-info th, +.table-info td, +.table-info thead th, +.table-info tbody + tbody { + border-color: #86cfda; +} + +.table-hover .table-info:hover { + background-color: #abdde5; +} +.table-hover .table-info:hover > td, +.table-hover .table-info:hover > th { + background-color: #abdde5; +} + +.table-warning, +.table-warning > th, +.table-warning > td { + background-color: #ffeeba; +} +.table-warning th, +.table-warning td, +.table-warning thead th, +.table-warning tbody + tbody { + border-color: #ffdf7e; +} + +.table-hover .table-warning:hover { + background-color: #ffe8a1; +} +.table-hover .table-warning:hover > td, +.table-hover .table-warning:hover > th { + background-color: #ffe8a1; +} + +.table-danger, +.table-danger > th, +.table-danger > td { + background-color: #ffb8b8; +} +.table-danger th, +.table-danger td, +.table-danger thead th, +.table-danger tbody + tbody { + border-color: #ff7a7a; +} + +.table-hover .table-danger:hover { + background-color: #ff9f9f; +} +.table-hover .table-danger:hover > td, +.table-hover .table-danger:hover > th { + background-color: #ff9f9f; +} + +.table-light, +.table-light > th, +.table-light > td { + background-color: #fcfcfc; +} +.table-light th, +.table-light td, +.table-light thead th, +.table-light tbody + tbody { + border-color: #fafafa; +} + +.table-hover .table-light:hover { + background-color: #efefef; +} +.table-hover .table-light:hover > td, +.table-hover .table-light:hover > th { + background-color: #efefef; +} + +.table-dark, +.table-dark > th, +.table-dark > td { + background-color: #c6c8ca; +} +.table-dark th, +.table-dark td, +.table-dark thead th, +.table-dark tbody + tbody { + border-color: #95999c; +} + +.table-hover .table-dark:hover { + background-color: #b9bbbe; +} +.table-hover .table-dark:hover > td, +.table-hover .table-dark:hover > th { + background-color: #b9bbbe; +} + +.table-active, +.table-active > th, +.table-active > td { + background-color: rgba(0, 0, 0, 0.075); +} + +.table-hover .table-active:hover { + background-color: rgba(0, 0, 0, 0.075); +} +.table-hover .table-active:hover > td, +.table-hover .table-active:hover > th { + background-color: rgba(0, 0, 0, 0.075); +} + +.table .thead-dark th { + color: #fff; + background-color: #343a40; + border-color: #454d55; +} +.table .thead-light th { + color: #495057; + background-color: #e9ecef; + border-color: #dee2e6; +} + +.table-dark { + color: #fff; + background-color: #343a40; +} +.table-dark th, +.table-dark td, +.table-dark thead th { + border-color: #454d55; +} +.table-dark.table-bordered { + border: 0; +} +.table-dark.table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(255, 255, 255, 0.05); +} +.table-dark.table-hover tbody tr:hover { + color: #fff; + background-color: rgba(255, 255, 255, 0.075); +} + +@media (max-width: 575.98px) { + .table-responsive-sm { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + .table-responsive-sm > .table-bordered { + border: 0; + } +} +@media (max-width: 767.98px) { + .table-responsive-md { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + .table-responsive-md > .table-bordered { + border: 0; + } +} +@media (max-width: 991.98px) { + .table-responsive-lg { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + .table-responsive-lg > .table-bordered { + border: 0; + } +} +@media (max-width: 1199.98px) { + .table-responsive-xl { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + .table-responsive-xl > .table-bordered { + border: 0; + } +} + +.table-responsive { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: auto; + scrollbar-color: rgba(120, 120, 120, 0.7) transparent; +} + +.table-responsive > .table { + width: 100%; +} + +.form-control { + display: block; + width: 100%; + height: calc(1.5em + 0.75rem + 2px); + padding: 0.375rem 0.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #495057; + background-color: #fff; + background-clip: padding-box; + border: 1px solid #ced4da; + border-radius: 0.25rem; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-control { + transition: none; + } +} +.form-control::-ms-expand { + background-color: transparent; + border: 0; +} +.form-control:focus { + color: #495057; + background-color: #fff; + border-color: #a1b5d7; + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(74, 111, 174, 0.25); +} +.form-control::placeholder { + color: #6c757d; + opacity: 1; +} +.form-control:disabled, .form-control[readonly] { + background-color: #e9ecef; + opacity: 1; +} + +input[type=date].form-control, +input[type=time].form-control, +input[type=datetime-local].form-control, +input[type=month].form-control { + appearance: none; +} + +select.form-control:-moz-focusring { + color: transparent; + text-shadow: 0 0 0 #495057; +} +select.form-control:focus::-ms-value { + color: #495057; + background-color: #fff; +} + +.form-control-file, +.form-control-range { + display: block; + width: 100%; +} + +.col-form-label { + padding-top: calc(0.375rem + 1px); + padding-bottom: calc(0.375rem + 1px); + margin-bottom: 0; + font-size: inherit; + line-height: 1.5; +} + +.col-form-label-lg { + padding-top: calc(0.5rem + 1px); + padding-bottom: calc(0.5rem + 1px); + font-size: 1.25rem; + line-height: 1.5; +} + +.col-form-label-sm { + padding-top: calc(0.25rem + 1px); + padding-bottom: calc(0.25rem + 1px); + font-size: 0.875rem; + line-height: 1.5; +} + +.form-control-plaintext { + display: block; + width: 100%; + padding: 0.375rem 0; + margin-bottom: 0; + font-size: 1rem; + line-height: 1.5; + color: #212529; + background-color: transparent; + border: solid transparent; + border-width: 1px 0; +} +.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg { + padding-right: 0; + padding-left: 0; +} + +.form-control-sm { + height: calc(1.5em + 0.5rem + 2px); + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; +} + +.form-control-lg { + height: calc(1.5em + 1rem + 2px); + padding: 0.5rem 1rem; + font-size: 1.25rem; + line-height: 1.5; + border-radius: 0.3rem; +} + +select.form-control[size], select.form-control[multiple] { + height: auto; +} + +textarea.form-control { + height: auto; +} + +.form-group { + margin-bottom: 1rem; +} + +.form-text { + display: block; + margin-top: 0.25rem; +} + +.form-row { + display: flex; + flex-wrap: wrap; + margin-right: -5px; + margin-left: -5px; +} +.form-row > .col, +.form-row > [class*=col-] { + padding-right: 5px; + padding-left: 5px; +} + +.form-check { + position: relative; + display: block; + padding-left: 1.25rem; +} + +.form-check-input { + position: absolute; + margin-top: 0.3rem; + margin-left: -1.25rem; +} +.form-check-input[disabled] ~ .form-check-label, .form-check-input:disabled ~ .form-check-label { + color: #6c757d; +} + +.form-check-label { + margin-bottom: 0; +} + +.form-check-inline { + display: inline-flex; + align-items: center; + padding-left: 0; + margin-right: 0.75rem; +} +.form-check-inline .form-check-input { + position: static; + margin-top: 0; + margin-right: 0.3125rem; + margin-left: 0; +} + +.valid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 80%; + color: #80cc28; +} + +.valid-tooltip { + position: absolute; + top: 100%; + left: 0; + z-index: 5; + display: none; + max-width: 100%; + padding: 0.25rem 0.5rem; + margin-top: 0.1rem; + font-size: 0.875rem; + line-height: 1.5; + color: #212529; + background-color: rgba(128, 204, 40, 0.9); + border-radius: 0.25rem; +} +.form-row > .col > .valid-tooltip, .form-row > [class*=col-] > .valid-tooltip { + left: 5px; +} + +.was-validated :valid ~ .valid-feedback, +.was-validated :valid ~ .valid-tooltip, +.is-valid ~ .valid-feedback, +.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .form-control:valid, .form-control.is-valid { + border-color: #80cc28; + padding-right: calc(1.5em + 0.75rem) !important; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2380cc28' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right calc(0.375em + 0.1875rem) center; + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.was-validated .form-control:valid:focus, .form-control.is-valid:focus { + border-color: #80cc28; + box-shadow: 0 0 0 0.2rem rgba(128, 204, 40, 0.25); +} + +.was-validated select.form-control:valid, select.form-control.is-valid { + padding-right: 3rem !important; + background-position: right 1.5rem center; +} + +.was-validated textarea.form-control:valid, textarea.form-control.is-valid { + padding-right: calc(1.5em + 0.75rem); + background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); +} + +.was-validated .custom-select:valid, .custom-select.is-valid { + border-color: #80cc28; + padding-right: calc(0.75em + 2.3125rem) !important; + background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right 0.75rem center/8px 10px no-repeat, #fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2380cc28' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem) no-repeat; +} +.was-validated .custom-select:valid:focus, .custom-select.is-valid:focus { + border-color: #80cc28; + box-shadow: 0 0 0 0.2rem rgba(128, 204, 40, 0.25); +} + +.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label { + color: #80cc28; +} +.was-validated .form-check-input:valid ~ .valid-feedback, +.was-validated .form-check-input:valid ~ .valid-tooltip, .form-check-input.is-valid ~ .valid-feedback, +.form-check-input.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .custom-control-input:valid ~ .custom-control-label, .custom-control-input.is-valid ~ .custom-control-label { + color: #80cc28; +} +.was-validated .custom-control-input:valid ~ .custom-control-label::before, .custom-control-input.is-valid ~ .custom-control-label::before { + border-color: #80cc28; +} +.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before, .custom-control-input.is-valid:checked ~ .custom-control-label::before { + border-color: #99dc4b; + background-color: #99dc4b; +} +.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before, .custom-control-input.is-valid:focus ~ .custom-control-label::before { + box-shadow: 0 0 0 0.2rem rgba(128, 204, 40, 0.25); +} +.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before { + border-color: #80cc28; +} + +.was-validated .custom-file-input:valid ~ .custom-file-label, .custom-file-input.is-valid ~ .custom-file-label { + border-color: #80cc28; +} +.was-validated .custom-file-input:valid:focus ~ .custom-file-label, .custom-file-input.is-valid:focus ~ .custom-file-label { + border-color: #80cc28; + box-shadow: 0 0 0 0.2rem rgba(128, 204, 40, 0.25); +} + +.invalid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 80%; + color: red; +} + +.invalid-tooltip { + position: absolute; + top: 100%; + left: 0; + z-index: 5; + display: none; + max-width: 100%; + padding: 0.25rem 0.5rem; + margin-top: 0.1rem; + font-size: 0.875rem; + line-height: 1.5; + color: #fff; + background-color: rgba(255, 0, 0, 0.9); + border-radius: 0.25rem; +} +.form-row > .col > .invalid-tooltip, .form-row > [class*=col-] > .invalid-tooltip { + left: 5px; +} + +.was-validated :invalid ~ .invalid-feedback, +.was-validated :invalid ~ .invalid-tooltip, +.is-invalid ~ .invalid-feedback, +.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .form-control:invalid, .form-control.is-invalid { + border-color: red; + padding-right: calc(1.5em + 0.75rem) !important; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='red' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='red' stroke='none'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right calc(0.375em + 0.1875rem) center; + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus { + border-color: red; + box-shadow: 0 0 0 0.2rem rgba(255, 0, 0, 0.25); +} + +.was-validated select.form-control:invalid, select.form-control.is-invalid { + padding-right: 3rem !important; + background-position: right 1.5rem center; +} + +.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid { + padding-right: calc(1.5em + 0.75rem); + background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); +} + +.was-validated .custom-select:invalid, .custom-select.is-invalid { + border-color: red; + padding-right: calc(0.75em + 2.3125rem) !important; + background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right 0.75rem center/8px 10px no-repeat, #fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='red' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='red' stroke='none'/%3e%3c/svg%3e") center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem) no-repeat; +} +.was-validated .custom-select:invalid:focus, .custom-select.is-invalid:focus { + border-color: red; + box-shadow: 0 0 0 0.2rem rgba(255, 0, 0, 0.25); +} + +.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label { + color: red; +} +.was-validated .form-check-input:invalid ~ .invalid-feedback, +.was-validated .form-check-input:invalid ~ .invalid-tooltip, .form-check-input.is-invalid ~ .invalid-feedback, +.form-check-input.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .custom-control-input:invalid ~ .custom-control-label, .custom-control-input.is-invalid ~ .custom-control-label { + color: red; +} +.was-validated .custom-control-input:invalid ~ .custom-control-label::before, .custom-control-input.is-invalid ~ .custom-control-label::before { + border-color: red; +} +.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before, .custom-control-input.is-invalid:checked ~ .custom-control-label::before { + border-color: #ff3333; + background-color: #ff3333; +} +.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before, .custom-control-input.is-invalid:focus ~ .custom-control-label::before { + box-shadow: 0 0 0 0.2rem rgba(255, 0, 0, 0.25); +} +.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before { + border-color: red; +} + +.was-validated .custom-file-input:invalid ~ .custom-file-label, .custom-file-input.is-invalid ~ .custom-file-label { + border-color: red; +} +.was-validated .custom-file-input:invalid:focus ~ .custom-file-label, .custom-file-input.is-invalid:focus ~ .custom-file-label { + border-color: red; + box-shadow: 0 0 0 0.2rem rgba(255, 0, 0, 0.25); +} + +.form-inline { + display: flex; + flex-flow: row wrap; + align-items: center; +} +.form-inline .form-check { + width: 100%; +} +@media (min-width: 576px) { + .form-inline label { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 0; + } + .form-inline .form-group { + display: flex; + flex: 0 0 auto; + flex-flow: row wrap; + align-items: center; + margin-bottom: 0; + } + .form-inline .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .form-inline .form-control-plaintext { + display: inline-block; + } + .form-inline .input-group, +.form-inline .custom-select { + width: auto; + } + .form-inline .form-check { + display: flex; + align-items: center; + justify-content: center; + width: auto; + padding-left: 0; + } + .form-inline .form-check-input { + position: relative; + flex-shrink: 0; + margin-top: 0; + margin-right: 0.25rem; + margin-left: 0; + } + .form-inline .custom-control { + align-items: center; + justify-content: center; + } + .form-inline .custom-control-label { + margin-bottom: 0; + } +} + +.btn { + display: inline-block; + font-weight: 400; + color: #212529; + text-align: center; + vertical-align: middle; + user-select: none; + background-color: transparent; + border: 1px solid transparent; + padding: 0.375rem 0.75rem; + font-size: 1rem; + line-height: 1.5; + border-radius: 0.25rem; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .btn { + transition: none; + } +} +.btn:hover { + color: #212529; + text-decoration: none; +} +.btn:focus, .btn.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(74, 111, 174, 0.25); +} +.btn.disabled, .btn:disabled { + opacity: 0.65; +} +.btn:not(:disabled):not(.disabled) { + cursor: pointer; +} +a.btn.disabled, +fieldset:disabled a.btn { + pointer-events: none; +} + +.btn-primary { + color: #fff; + background-color: #4a6fae; + border-color: #4a6fae; +} +.btn-primary:hover { + color: #fff; + background-color: #3f5e93; + border-color: #3b588a; +} +.btn-primary:focus, .btn-primary.focus { + color: #fff; + background-color: #3f5e93; + border-color: #3b588a; + box-shadow: 0 0 0 0.2rem rgba(101, 133, 186, 0.5); +} +.btn-primary.disabled, .btn-primary:disabled { + color: #fff; + background-color: #4a6fae; + border-color: #4a6fae; +} +.btn-primary:not(:disabled):not(.disabled):active, .btn-primary:not(:disabled):not(.disabled).active, .show > .btn-primary.dropdown-toggle { + color: #fff; + background-color: #3b588a; + border-color: #375281; +} +.btn-primary:not(:disabled):not(.disabled):active:focus, .btn-primary:not(:disabled):not(.disabled).active:focus, .show > .btn-primary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(101, 133, 186, 0.5); +} + +.btn-secondary { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; +} +.btn-secondary:hover { + color: #fff; + background-color: #5a6268; + border-color: #545b62; +} +.btn-secondary:focus, .btn-secondary.focus { + color: #fff; + background-color: #5a6268; + border-color: #545b62; + box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5); +} +.btn-secondary.disabled, .btn-secondary:disabled { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; +} +.btn-secondary:not(:disabled):not(.disabled):active, .btn-secondary:not(:disabled):not(.disabled).active, .show > .btn-secondary.dropdown-toggle { + color: #fff; + background-color: #545b62; + border-color: #4e555b; +} +.btn-secondary:not(:disabled):not(.disabled):active:focus, .btn-secondary:not(:disabled):not(.disabled).active:focus, .show > .btn-secondary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5); +} + +.btn-success { + color: #212529; + background-color: #80cc28; + border-color: #80cc28; +} +.btn-success:hover { + color: #fff; + background-color: #6cac22; + border-color: #65a120; +} +.btn-success:focus, .btn-success.focus { + color: #fff; + background-color: #6cac22; + border-color: #65a120; + box-shadow: 0 0 0 0.2rem rgba(114, 179, 40, 0.5); +} +.btn-success.disabled, .btn-success:disabled { + color: #212529; + background-color: #80cc28; + border-color: #80cc28; +} +.btn-success:not(:disabled):not(.disabled):active, .btn-success:not(:disabled):not(.disabled).active, .show > .btn-success.dropdown-toggle { + color: #fff; + background-color: #65a120; + border-color: #5f971e; +} +.btn-success:not(:disabled):not(.disabled):active:focus, .btn-success:not(:disabled):not(.disabled).active:focus, .show > .btn-success.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(114, 179, 40, 0.5); +} + +.btn-info { + color: #fff; + background-color: #17a2b8; + border-color: #17a2b8; +} +.btn-info:hover { + color: #fff; + background-color: #138496; + border-color: #117a8b; +} +.btn-info:focus, .btn-info.focus { + color: #fff; + background-color: #138496; + border-color: #117a8b; + box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5); +} +.btn-info.disabled, .btn-info:disabled { + color: #fff; + background-color: #17a2b8; + border-color: #17a2b8; +} +.btn-info:not(:disabled):not(.disabled):active, .btn-info:not(:disabled):not(.disabled).active, .show > .btn-info.dropdown-toggle { + color: #fff; + background-color: #117a8b; + border-color: #10707f; +} +.btn-info:not(:disabled):not(.disabled):active:focus, .btn-info:not(:disabled):not(.disabled).active:focus, .show > .btn-info.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5); +} + +.btn-warning { + color: #212529; + background-color: #ffc107; + border-color: #ffc107; +} +.btn-warning:hover { + color: #212529; + background-color: #e0a800; + border-color: #d39e00; +} +.btn-warning:focus, .btn-warning.focus { + color: #212529; + background-color: #e0a800; + border-color: #d39e00; + box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5); +} +.btn-warning.disabled, .btn-warning:disabled { + color: #212529; + background-color: #ffc107; + border-color: #ffc107; +} +.btn-warning:not(:disabled):not(.disabled):active, .btn-warning:not(:disabled):not(.disabled).active, .show > .btn-warning.dropdown-toggle { + color: #212529; + background-color: #d39e00; + border-color: #c69500; +} +.btn-warning:not(:disabled):not(.disabled):active:focus, .btn-warning:not(:disabled):not(.disabled).active:focus, .show > .btn-warning.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5); +} + +.btn-danger { + color: #fff; + background-color: red; + border-color: red; +} +.btn-danger:hover { + color: #fff; + background-color: #d90000; + border-color: #cc0000; +} +.btn-danger:focus, .btn-danger.focus { + color: #fff; + background-color: #d90000; + border-color: #cc0000; + box-shadow: 0 0 0 0.2rem rgba(255, 38, 38, 0.5); +} +.btn-danger.disabled, .btn-danger:disabled { + color: #fff; + background-color: red; + border-color: red; +} +.btn-danger:not(:disabled):not(.disabled):active, .btn-danger:not(:disabled):not(.disabled).active, .show > .btn-danger.dropdown-toggle { + color: #fff; + background-color: #cc0000; + border-color: #bf0000; +} +.btn-danger:not(:disabled):not(.disabled):active:focus, .btn-danger:not(:disabled):not(.disabled).active:focus, .show > .btn-danger.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(255, 38, 38, 0.5); +} + +.btn-light { + color: #212529; + background-color: #f5f5f5; + border-color: #f5f5f5; +} +.btn-light:hover { + color: #212529; + background-color: #e2e2e2; + border-color: gainsboro; +} +.btn-light:focus, .btn-light.focus { + color: #212529; + background-color: #e2e2e2; + border-color: gainsboro; + box-shadow: 0 0 0 0.2rem rgba(213, 214, 214, 0.5); +} +.btn-light.disabled, .btn-light:disabled { + color: #212529; + background-color: #f5f5f5; + border-color: #f5f5f5; +} +.btn-light:not(:disabled):not(.disabled):active, .btn-light:not(:disabled):not(.disabled).active, .show > .btn-light.dropdown-toggle { + color: #212529; + background-color: gainsboro; + border-color: #d5d5d5; +} +.btn-light:not(:disabled):not(.disabled):active:focus, .btn-light:not(:disabled):not(.disabled).active:focus, .show > .btn-light.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(213, 214, 214, 0.5); +} + +.btn-dark { + color: #fff; + background-color: #343a40; + border-color: #343a40; +} +.btn-dark:hover { + color: #fff; + background-color: #23272b; + border-color: #1d2124; +} +.btn-dark:focus, .btn-dark.focus { + color: #fff; + background-color: #23272b; + border-color: #1d2124; + box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5); +} +.btn-dark.disabled, .btn-dark:disabled { + color: #fff; + background-color: #343a40; + border-color: #343a40; +} +.btn-dark:not(:disabled):not(.disabled):active, .btn-dark:not(:disabled):not(.disabled).active, .show > .btn-dark.dropdown-toggle { + color: #fff; + background-color: #1d2124; + border-color: #171a1d; +} +.btn-dark:not(:disabled):not(.disabled):active:focus, .btn-dark:not(:disabled):not(.disabled).active:focus, .show > .btn-dark.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5); +} + +.btn-outline-primary { + color: #4a6fae; + border-color: #4a6fae; +} +.btn-outline-primary:hover { + color: #fff; + background-color: #4a6fae; + border-color: #4a6fae; +} +.btn-outline-primary:focus, .btn-outline-primary.focus { + box-shadow: 0 0 0 0.2rem rgba(74, 111, 174, 0.5); +} +.btn-outline-primary.disabled, .btn-outline-primary:disabled { + color: #4a6fae; + background-color: transparent; +} +.btn-outline-primary:not(:disabled):not(.disabled):active, .btn-outline-primary:not(:disabled):not(.disabled).active, .show > .btn-outline-primary.dropdown-toggle { + color: #fff; + background-color: #4a6fae; + border-color: #4a6fae; +} +.btn-outline-primary:not(:disabled):not(.disabled):active:focus, .btn-outline-primary:not(:disabled):not(.disabled).active:focus, .show > .btn-outline-primary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(74, 111, 174, 0.5); +} + +.btn-outline-secondary { + color: #6c757d; + border-color: #6c757d; +} +.btn-outline-secondary:hover { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; +} +.btn-outline-secondary:focus, .btn-outline-secondary.focus { + box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); +} +.btn-outline-secondary.disabled, .btn-outline-secondary:disabled { + color: #6c757d; + background-color: transparent; +} +.btn-outline-secondary:not(:disabled):not(.disabled):active, .btn-outline-secondary:not(:disabled):not(.disabled).active, .show > .btn-outline-secondary.dropdown-toggle { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; +} +.btn-outline-secondary:not(:disabled):not(.disabled):active:focus, .btn-outline-secondary:not(:disabled):not(.disabled).active:focus, .show > .btn-outline-secondary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); +} + +.btn-outline-success { + color: #80cc28; + border-color: #80cc28; +} +.btn-outline-success:hover { + color: #212529; + background-color: #80cc28; + border-color: #80cc28; +} +.btn-outline-success:focus, .btn-outline-success.focus { + box-shadow: 0 0 0 0.2rem rgba(128, 204, 40, 0.5); +} +.btn-outline-success.disabled, .btn-outline-success:disabled { + color: #80cc28; + background-color: transparent; +} +.btn-outline-success:not(:disabled):not(.disabled):active, .btn-outline-success:not(:disabled):not(.disabled).active, .show > .btn-outline-success.dropdown-toggle { + color: #212529; + background-color: #80cc28; + border-color: #80cc28; +} +.btn-outline-success:not(:disabled):not(.disabled):active:focus, .btn-outline-success:not(:disabled):not(.disabled).active:focus, .show > .btn-outline-success.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(128, 204, 40, 0.5); +} + +.btn-outline-info { + color: #17a2b8; + border-color: #17a2b8; +} +.btn-outline-info:hover { + color: #fff; + background-color: #17a2b8; + border-color: #17a2b8; +} +.btn-outline-info:focus, .btn-outline-info.focus { + box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); +} +.btn-outline-info.disabled, .btn-outline-info:disabled { + color: #17a2b8; + background-color: transparent; +} +.btn-outline-info:not(:disabled):not(.disabled):active, .btn-outline-info:not(:disabled):not(.disabled).active, .show > .btn-outline-info.dropdown-toggle { + color: #fff; + background-color: #17a2b8; + border-color: #17a2b8; +} +.btn-outline-info:not(:disabled):not(.disabled):active:focus, .btn-outline-info:not(:disabled):not(.disabled).active:focus, .show > .btn-outline-info.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); +} + +.btn-outline-warning { + color: #ffc107; + border-color: #ffc107; +} +.btn-outline-warning:hover { + color: #212529; + background-color: #ffc107; + border-color: #ffc107; +} +.btn-outline-warning:focus, .btn-outline-warning.focus { + box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); +} +.btn-outline-warning.disabled, .btn-outline-warning:disabled { + color: #ffc107; + background-color: transparent; +} +.btn-outline-warning:not(:disabled):not(.disabled):active, .btn-outline-warning:not(:disabled):not(.disabled).active, .show > .btn-outline-warning.dropdown-toggle { + color: #212529; + background-color: #ffc107; + border-color: #ffc107; +} +.btn-outline-warning:not(:disabled):not(.disabled):active:focus, .btn-outline-warning:not(:disabled):not(.disabled).active:focus, .show > .btn-outline-warning.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); +} + +.btn-outline-danger { + color: red; + border-color: red; +} +.btn-outline-danger:hover { + color: #fff; + background-color: red; + border-color: red; +} +.btn-outline-danger:focus, .btn-outline-danger.focus { + box-shadow: 0 0 0 0.2rem rgba(255, 0, 0, 0.5); +} +.btn-outline-danger.disabled, .btn-outline-danger:disabled { + color: red; + background-color: transparent; +} +.btn-outline-danger:not(:disabled):not(.disabled):active, .btn-outline-danger:not(:disabled):not(.disabled).active, .show > .btn-outline-danger.dropdown-toggle { + color: #fff; + background-color: red; + border-color: red; +} +.btn-outline-danger:not(:disabled):not(.disabled):active:focus, .btn-outline-danger:not(:disabled):not(.disabled).active:focus, .show > .btn-outline-danger.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(255, 0, 0, 0.5); +} + +.btn-outline-light { + color: #f5f5f5; + border-color: #f5f5f5; +} +.btn-outline-light:hover { + color: #212529; + background-color: #f5f5f5; + border-color: #f5f5f5; +} +.btn-outline-light:focus, .btn-outline-light.focus { + box-shadow: 0 0 0 0.2rem rgba(245, 245, 245, 0.5); +} +.btn-outline-light.disabled, .btn-outline-light:disabled { + color: #f5f5f5; + background-color: transparent; +} +.btn-outline-light:not(:disabled):not(.disabled):active, .btn-outline-light:not(:disabled):not(.disabled).active, .show > .btn-outline-light.dropdown-toggle { + color: #212529; + background-color: #f5f5f5; + border-color: #f5f5f5; +} +.btn-outline-light:not(:disabled):not(.disabled):active:focus, .btn-outline-light:not(:disabled):not(.disabled).active:focus, .show > .btn-outline-light.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(245, 245, 245, 0.5); +} + +.btn-outline-dark { + color: #343a40; + border-color: #343a40; +} +.btn-outline-dark:hover { + color: #fff; + background-color: #343a40; + border-color: #343a40; +} +.btn-outline-dark:focus, .btn-outline-dark.focus { + box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); +} +.btn-outline-dark.disabled, .btn-outline-dark:disabled { + color: #343a40; + background-color: transparent; +} +.btn-outline-dark:not(:disabled):not(.disabled):active, .btn-outline-dark:not(:disabled):not(.disabled).active, .show > .btn-outline-dark.dropdown-toggle { + color: #fff; + background-color: #343a40; + border-color: #343a40; +} +.btn-outline-dark:not(:disabled):not(.disabled):active:focus, .btn-outline-dark:not(:disabled):not(.disabled).active:focus, .show > .btn-outline-dark.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); +} + +.btn-link { + font-weight: 400; + color: #4a6fae; + text-decoration: none; +} +.btn-link:hover { + color: #334d78; + text-decoration: underline; +} +.btn-link:focus, .btn-link.focus { + text-decoration: underline; +} +.btn-link:disabled, .btn-link.disabled { + color: #6c757d; + pointer-events: none; +} + +.btn-lg, .btn-group-lg > .btn { + padding: 0.5rem 1rem; + font-size: 1.25rem; + line-height: 1.5; + border-radius: 0.3rem; +} + +.btn-sm, .btn-group-sm > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; +} + +.btn-block { + display: block; + width: 100%; +} +.btn-block + .btn-block { + margin-top: 0.5rem; +} + +input[type=submit].btn-block, +input[type=reset].btn-block, +input[type=button].btn-block { + width: 100%; +} + +.fade { + transition: opacity 0.15s linear; +} +@media (prefers-reduced-motion: reduce) { + .fade { + transition: none; + } +} +.fade:not(.show) { + opacity: 0; +} + +.collapse:not(.show) { + display: none; +} + +.collapsing { + position: relative; + height: 0; + overflow: hidden; + transition: height 0.35s ease; +} +@media (prefers-reduced-motion: reduce) { + .collapsing { + transition: none; + } +} + +.dropup, +.dropright, +.dropdown, +.dropleft { + position: relative; +} + +.dropdown-toggle { + white-space: nowrap; +} +.dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid; + border-right: 0.3em solid transparent; + border-bottom: 0; + border-left: 0.3em solid transparent; +} +.dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 10rem; + padding: 0.5rem 0; + margin: 0.125rem 0 0; + font-size: 1rem; + color: #212529; + text-align: left; + list-style: none; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 0.25rem; +} + +.dropdown-menu-left { + right: auto; + left: 0; +} + +.dropdown-menu-right { + right: 0; + left: auto; +} + +@media (min-width: 576px) { + .dropdown-menu-sm-left { + right: auto; + left: 0; + } + + .dropdown-menu-sm-right { + right: 0; + left: auto; + } +} +@media (min-width: 768px) { + .dropdown-menu-md-left { + right: auto; + left: 0; + } + + .dropdown-menu-md-right { + right: 0; + left: auto; + } +} +@media (min-width: 992px) { + .dropdown-menu-lg-left { + right: auto; + left: 0; + } + + .dropdown-menu-lg-right { + right: 0; + left: auto; + } +} +@media (min-width: 1200px) { + .dropdown-menu-xl-left { + right: auto; + left: 0; + } + + .dropdown-menu-xl-right { + right: 0; + left: auto; + } +} +.dropup .dropdown-menu { + top: auto; + bottom: 100%; + margin-top: 0; + margin-bottom: 0.125rem; +} +.dropup .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0; + border-right: 0.3em solid transparent; + border-bottom: 0.3em solid; + border-left: 0.3em solid transparent; +} +.dropup .dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropright .dropdown-menu { + top: 0; + right: auto; + left: 100%; + margin-top: 0; + margin-left: 0.125rem; +} +.dropright .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid transparent; + border-right: 0; + border-bottom: 0.3em solid transparent; + border-left: 0.3em solid; +} +.dropright .dropdown-toggle:empty::after { + margin-left: 0; +} +.dropright .dropdown-toggle::after { + vertical-align: 0; +} + +.dropleft .dropdown-menu { + top: 0; + right: 100%; + left: auto; + margin-top: 0; + margin-right: 0.125rem; +} +.dropleft .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; +} +.dropleft .dropdown-toggle::after { + display: none; +} +.dropleft .dropdown-toggle::before { + display: inline-block; + margin-right: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid transparent; + border-right: 0.3em solid; + border-bottom: 0.3em solid transparent; +} +.dropleft .dropdown-toggle:empty::after { + margin-left: 0; +} +.dropleft .dropdown-toggle::before { + vertical-align: 0; +} + +.dropdown-menu[x-placement^=top], .dropdown-menu[x-placement^=right], .dropdown-menu[x-placement^=bottom], .dropdown-menu[x-placement^=left] { + right: auto; + bottom: auto; +} + +.dropdown-divider { + height: 0; + margin: 0.5rem 0; + overflow: hidden; + border-top: 1px solid #e9ecef; +} + +.dropdown-item { + display: block; + width: 100%; + padding: 0.25rem 1.5rem; + clear: both; + font-weight: 400; + color: #212529; + text-align: inherit; + white-space: nowrap; + background-color: transparent; + border: 0; +} +.dropdown-item:hover, .dropdown-item:focus { + color: #16181b; + text-decoration: none; + background-color: #e9ecef; +} +.dropdown-item.active, .dropdown-item:active { + color: #fff; + text-decoration: none; + background-color: #4a6fae; +} +.dropdown-item.disabled, .dropdown-item:disabled { + color: #adb5bd; + pointer-events: none; + background-color: transparent; +} + +.dropdown-menu.show { + display: block; +} + +.dropdown-header { + display: block; + padding: 0.5rem 1.5rem; + margin-bottom: 0; + font-size: 0.875rem; + color: #6c757d; + white-space: nowrap; +} + +.dropdown-item-text { + display: block; + padding: 0.25rem 1.5rem; + color: #212529; +} + +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-flex; + vertical-align: middle; +} +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + flex: 1 1 auto; +} +.btn-group > .btn:hover, +.btn-group-vertical > .btn:hover { + z-index: 1; +} +.btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active, +.btn-group-vertical > .btn:focus, +.btn-group-vertical > .btn:active, +.btn-group-vertical > .btn.active { + z-index: 1; +} + +.btn-toolbar { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; +} +.btn-toolbar .input-group { + width: auto; +} + +.btn-group > .btn:not(:first-child), +.btn-group > .btn-group:not(:first-child) { + margin-left: -1px; +} +.btn-group > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group > .btn-group:not(:last-child) > .btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.btn-group > .btn:not(:first-child), +.btn-group > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.dropdown-toggle-split { + padding-right: 0.5625rem; + padding-left: 0.5625rem; +} +.dropdown-toggle-split::after, .dropup .dropdown-toggle-split::after, .dropright .dropdown-toggle-split::after { + margin-left: 0; +} +.dropleft .dropdown-toggle-split::before { + margin-right: 0; +} + +.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split { + padding-right: 0.375rem; + padding-left: 0.375rem; +} + +.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split { + padding-right: 0.75rem; + padding-left: 0.75rem; +} + +.btn-group-vertical { + flex-direction: column; + align-items: flex-start; + justify-content: center; +} +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group { + width: 100%; +} +.btn-group-vertical > .btn:not(:first-child), +.btn-group-vertical > .btn-group:not(:first-child) { + margin-top: -1px; +} +.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group-vertical > .btn-group:not(:last-child) > .btn { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn:not(:first-child), +.btn-group-vertical > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.btn-group-toggle > .btn, +.btn-group-toggle > .btn-group > .btn { + margin-bottom: 0; +} +.btn-group-toggle > .btn input[type=radio], +.btn-group-toggle > .btn input[type=checkbox], +.btn-group-toggle > .btn-group > .btn input[type=radio], +.btn-group-toggle > .btn-group > .btn input[type=checkbox] { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; +} + +.input-group { + position: relative; + display: flex; + flex-wrap: wrap; + align-items: stretch; + width: 100%; +} +.input-group > .form-control, +.input-group > .form-control-plaintext, +.input-group > .custom-select, +.input-group > .custom-file { + position: relative; + flex: 1 1 auto; + width: 1%; + min-width: 0; + margin-bottom: 0; +} +.input-group > .form-control + .form-control, +.input-group > .form-control + .custom-select, +.input-group > .form-control + .custom-file, +.input-group > .form-control-plaintext + .form-control, +.input-group > .form-control-plaintext + .custom-select, +.input-group > .form-control-plaintext + .custom-file, +.input-group > .custom-select + .form-control, +.input-group > .custom-select + .custom-select, +.input-group > .custom-select + .custom-file, +.input-group > .custom-file + .form-control, +.input-group > .custom-file + .custom-select, +.input-group > .custom-file + .custom-file { + margin-left: -1px; +} +.input-group > .form-control:focus, +.input-group > .custom-select:focus, +.input-group > .custom-file .custom-file-input:focus ~ .custom-file-label { + z-index: 3; +} +.input-group > .custom-file .custom-file-input:focus { + z-index: 4; +} +.input-group > .form-control:not(:first-child), +.input-group > .custom-select:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.input-group > .custom-file { + display: flex; + align-items: center; +} +.input-group > .custom-file:not(:last-child) .custom-file-label, .input-group > .custom-file:not(:last-child) .custom-file-label::after { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.input-group > .custom-file:not(:first-child) .custom-file-label { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.input-group:not(.has-validation) > .form-control:not(:last-child), +.input-group:not(.has-validation) > .custom-select:not(:last-child), +.input-group:not(.has-validation) > .custom-file:not(:last-child) .custom-file-label, +.input-group:not(.has-validation) > .custom-file:not(:last-child) .custom-file-label::after { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.input-group.has-validation > .form-control:nth-last-child(n+3), +.input-group.has-validation > .custom-select:nth-last-child(n+3), +.input-group.has-validation > .custom-file:nth-last-child(n+3) .custom-file-label, +.input-group.has-validation > .custom-file:nth-last-child(n+3) .custom-file-label::after { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group-prepend, +.input-group-append { + display: flex; +} +.input-group-prepend .btn, +.input-group-append .btn { + position: relative; + z-index: 2; +} +.input-group-prepend .btn:focus, +.input-group-append .btn:focus { + z-index: 3; +} +.input-group-prepend .btn + .btn, +.input-group-prepend .btn + .input-group-text, +.input-group-prepend .input-group-text + .input-group-text, +.input-group-prepend .input-group-text + .btn, +.input-group-append .btn + .btn, +.input-group-append .btn + .input-group-text, +.input-group-append .input-group-text + .input-group-text, +.input-group-append .input-group-text + .btn { + margin-left: -1px; +} + +.input-group-prepend { + margin-right: -1px; +} + +.input-group-append { + margin-left: -1px; +} + +.input-group-text { + display: flex; + align-items: center; + padding: 0.375rem 0.75rem; + margin-bottom: 0; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #495057; + text-align: center; + white-space: nowrap; + background-color: #e9ecef; + border: 1px solid #ced4da; + border-radius: 0.25rem; +} +.input-group-text input[type=radio], +.input-group-text input[type=checkbox] { + margin-top: 0; +} + +.input-group-lg > .form-control:not(textarea), +.input-group-lg > .custom-select { + height: calc(1.5em + 1rem + 2px); +} + +.input-group-lg > .form-control, +.input-group-lg > .custom-select, +.input-group-lg > .input-group-prepend > .input-group-text, +.input-group-lg > .input-group-append > .input-group-text, +.input-group-lg > .input-group-prepend > .btn, +.input-group-lg > .input-group-append > .btn { + padding: 0.5rem 1rem; + font-size: 1.25rem; + line-height: 1.5; + border-radius: 0.3rem; +} + +.input-group-sm > .form-control:not(textarea), +.input-group-sm > .custom-select { + height: calc(1.5em + 0.5rem + 2px); +} + +.input-group-sm > .form-control, +.input-group-sm > .custom-select, +.input-group-sm > .input-group-prepend > .input-group-text, +.input-group-sm > .input-group-append > .input-group-text, +.input-group-sm > .input-group-prepend > .btn, +.input-group-sm > .input-group-append > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; +} + +.input-group-lg > .custom-select, +.input-group-sm > .custom-select { + padding-right: 1.75rem; +} + +.input-group > .input-group-prepend > .btn, +.input-group > .input-group-prepend > .input-group-text, +.input-group:not(.has-validation) > .input-group-append:not(:last-child) > .btn, +.input-group:not(.has-validation) > .input-group-append:not(:last-child) > .input-group-text, +.input-group.has-validation > .input-group-append:nth-last-child(n+3) > .btn, +.input-group.has-validation > .input-group-append:nth-last-child(n+3) > .input-group-text, +.input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle), +.input-group > .input-group-append:last-child > .input-group-text:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group > .input-group-append > .btn, +.input-group > .input-group-append > .input-group-text, +.input-group > .input-group-prepend:not(:first-child) > .btn, +.input-group > .input-group-prepend:not(:first-child) > .input-group-text, +.input-group > .input-group-prepend:first-child > .btn:not(:first-child), +.input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.custom-control { + position: relative; + z-index: 1; + display: block; + min-height: 1.5rem; + padding-left: 1.5rem; + color-adjust: exact; +} + +.custom-control-inline { + display: inline-flex; + margin-right: 1rem; +} + +.custom-control-input { + position: absolute; + left: 0; + z-index: -1; + width: 1rem; + height: 1.25rem; + opacity: 0; +} +.custom-control-input:checked ~ .custom-control-label::before { + color: #fff; + border-color: #4a6fae; + background-color: #4a6fae; +} +.custom-control-input:focus ~ .custom-control-label::before { + box-shadow: 0 0 0 0.2rem rgba(74, 111, 174, 0.25); +} +.custom-control-input:focus:not(:checked) ~ .custom-control-label::before { + border-color: #a1b5d7; +} +.custom-control-input:not(:disabled):active ~ .custom-control-label::before { + color: #fff; + background-color: #c4d1e6; + border-color: #c4d1e6; +} +.custom-control-input[disabled] ~ .custom-control-label, .custom-control-input:disabled ~ .custom-control-label { + color: #6c757d; +} +.custom-control-input[disabled] ~ .custom-control-label::before, .custom-control-input:disabled ~ .custom-control-label::before { + background-color: #e9ecef; +} + +.custom-control-label { + position: relative; + margin-bottom: 0; + vertical-align: top; +} +.custom-control-label::before { + position: absolute; + top: 0.25rem; + left: -1.5rem; + display: block; + width: 1rem; + height: 1rem; + pointer-events: none; + content: ""; + background-color: #fff; + border: #adb5bd solid 1px; +} +.custom-control-label::after { + position: absolute; + top: 0.25rem; + left: -1.5rem; + display: block; + width: 1rem; + height: 1rem; + content: ""; + background: 50%/50% 50% no-repeat; +} + +.custom-checkbox .custom-control-label::before { + border-radius: 0.25rem; +} +.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e"); +} +.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before { + border-color: #4a6fae; + background-color: #4a6fae; +} +.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e"); +} +.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before { + background-color: rgba(74, 111, 174, 0.5); +} +.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before { + background-color: rgba(74, 111, 174, 0.5); +} + +.custom-radio .custom-control-label::before { + border-radius: 50%; +} +.custom-radio .custom-control-input:checked ~ .custom-control-label::after { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e"); +} +.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before { + background-color: rgba(74, 111, 174, 0.5); +} + +.custom-switch { + padding-left: 2.25rem; +} +.custom-switch .custom-control-label::before { + left: -2.25rem; + width: 1.75rem; + pointer-events: all; + border-radius: 0.5rem; +} +.custom-switch .custom-control-label::after { + top: calc(0.25rem + 2px); + left: calc(-2.25rem + 2px); + width: calc(1rem - 4px); + height: calc(1rem - 4px); + background-color: #adb5bd; + border-radius: 0.5rem; + transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .custom-switch .custom-control-label::after { + transition: none; + } +} +.custom-switch .custom-control-input:checked ~ .custom-control-label::after { + background-color: #fff; + transform: translateX(0.75rem); +} +.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before { + background-color: rgba(74, 111, 174, 0.5); +} + +.custom-select { + display: inline-block; + width: 100%; + height: calc(1.5em + 0.75rem + 2px); + padding: 0.375rem 1.75rem 0.375rem 0.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #495057; + vertical-align: middle; + background: #fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right 0.75rem center/8px 10px no-repeat; + border: 1px solid #ced4da; + border-radius: 0.25rem; + appearance: none; +} +.custom-select:focus { + border-color: #a1b5d7; + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(74, 111, 174, 0.25); +} +.custom-select:focus::-ms-value { + color: #495057; + background-color: #fff; +} +.custom-select[multiple], .custom-select[size]:not([size="1"]) { + height: auto; + padding-right: 0.75rem; + background-image: none; +} +.custom-select:disabled { + color: #6c757d; + background-color: #e9ecef; +} +.custom-select::-ms-expand { + display: none; +} +.custom-select:-moz-focusring { + color: transparent; + text-shadow: 0 0 0 #495057; +} + +.custom-select-sm { + height: calc(1.5em + 0.5rem + 2px); + padding-top: 0.25rem; + padding-bottom: 0.25rem; + padding-left: 0.5rem; + font-size: 0.875rem; +} + +.custom-select-lg { + height: calc(1.5em + 1rem + 2px); + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 1rem; + font-size: 1.25rem; +} + +.custom-file { + position: relative; + display: inline-block; + width: 100%; + height: calc(1.5em + 0.75rem + 2px); + margin-bottom: 0; +} + +.custom-file-input { + position: relative; + z-index: 2; + width: 100%; + height: calc(1.5em + 0.75rem + 2px); + margin: 0; + overflow: hidden; + opacity: 0; +} +.custom-file-input:focus ~ .custom-file-label { + border-color: #a1b5d7; + box-shadow: 0 0 0 0.2rem rgba(74, 111, 174, 0.25); +} +.custom-file-input[disabled] ~ .custom-file-label, .custom-file-input:disabled ~ .custom-file-label { + background-color: #e9ecef; +} +.custom-file-input:lang(en) ~ .custom-file-label::after { + content: "Browse"; +} +.custom-file-input ~ .custom-file-label[data-browse]::after { + content: attr(data-browse); +} + +.custom-file-label { + position: absolute; + top: 0; + right: 0; + left: 0; + z-index: 1; + height: calc(1.5em + 0.75rem + 2px); + padding: 0.375rem 0.75rem; + overflow: hidden; + font-weight: 400; + line-height: 1.5; + color: #495057; + background-color: #fff; + border: 1px solid #ced4da; + border-radius: 0.25rem; +} +.custom-file-label::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + z-index: 3; + display: block; + height: calc(1.5em + 0.75rem); + padding: 0.375rem 0.75rem; + line-height: 1.5; + color: #495057; + content: "Browse"; + background-color: #e9ecef; + border-left: inherit; + border-radius: 0 0.25rem 0.25rem 0; +} + +.custom-range { + width: 100%; + height: 1.4rem; + padding: 0; + background-color: transparent; + appearance: none; +} +.custom-range:focus { + outline: 0; +} +.custom-range:focus::-webkit-slider-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(74, 111, 174, 0.25); +} +.custom-range:focus::-moz-range-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(74, 111, 174, 0.25); +} +.custom-range:focus::-ms-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(74, 111, 174, 0.25); +} +.custom-range::-moz-focus-outer { + border: 0; +} +.custom-range::-webkit-slider-thumb { + width: 1rem; + height: 1rem; + margin-top: -0.25rem; + background-color: #4a6fae; + border: 0; + border-radius: 1rem; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + appearance: none; +} +@media (prefers-reduced-motion: reduce) { + .custom-range::-webkit-slider-thumb { + transition: none; + } +} +.custom-range::-webkit-slider-thumb:active { + background-color: #c4d1e6; +} +.custom-range::-webkit-slider-runnable-track { + width: 100%; + height: 0.5rem; + color: transparent; + cursor: pointer; + background-color: #dee2e6; + border-color: transparent; + border-radius: 1rem; +} +.custom-range::-moz-range-thumb { + width: 1rem; + height: 1rem; + background-color: #4a6fae; + border: 0; + border-radius: 1rem; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + appearance: none; +} +@media (prefers-reduced-motion: reduce) { + .custom-range::-moz-range-thumb { + transition: none; + } +} +.custom-range::-moz-range-thumb:active { + background-color: #c4d1e6; +} +.custom-range::-moz-range-track { + width: 100%; + height: 0.5rem; + color: transparent; + cursor: pointer; + background-color: #dee2e6; + border-color: transparent; + border-radius: 1rem; +} +.custom-range::-ms-thumb { + width: 1rem; + height: 1rem; + margin-top: 0; + margin-right: 0.2rem; + margin-left: 0.2rem; + background-color: #4a6fae; + border: 0; + border-radius: 1rem; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + appearance: none; +} +@media (prefers-reduced-motion: reduce) { + .custom-range::-ms-thumb { + transition: none; + } +} +.custom-range::-ms-thumb:active { + background-color: #c4d1e6; +} +.custom-range::-ms-track { + width: 100%; + height: 0.5rem; + color: transparent; + cursor: pointer; + background-color: transparent; + border-color: transparent; + border-width: 0.5rem; +} +.custom-range::-ms-fill-lower { + background-color: #dee2e6; + border-radius: 1rem; +} +.custom-range::-ms-fill-upper { + margin-right: 15px; + background-color: #dee2e6; + border-radius: 1rem; +} +.custom-range:disabled::-webkit-slider-thumb { + background-color: #adb5bd; +} +.custom-range:disabled::-webkit-slider-runnable-track { + cursor: default; +} +.custom-range:disabled::-moz-range-thumb { + background-color: #adb5bd; +} +.custom-range:disabled::-moz-range-track { + cursor: default; +} +.custom-range:disabled::-ms-thumb { + background-color: #adb5bd; +} + +.custom-control-label::before, +.custom-file-label, +.custom-select { + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .custom-control-label::before, +.custom-file-label, +.custom-select { + transition: none; + } +} + +.nav { + display: flex; + flex-wrap: wrap; + padding-left: 0; + margin-bottom: 0; + list-style: none; +} + +.nav-link { + display: block; + padding: 0.5rem 1rem; +} +.nav-link:hover, .nav-link:focus { + text-decoration: none; +} +.nav-link.disabled { + color: #6c757d; + pointer-events: none; + cursor: default; +} + +.nav-tabs { + border-bottom: 1px solid #dee2e6; +} +.nav-tabs .nav-link { + margin-bottom: -1px; + border: 1px solid transparent; + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} +.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus { + border-color: #e9ecef #e9ecef #dee2e6; +} +.nav-tabs .nav-link.disabled { + color: #6c757d; + background-color: transparent; + border-color: transparent; +} +.nav-tabs .nav-link.active, +.nav-tabs .nav-item.show .nav-link { + color: #495057; + background-color: #fff; + border-color: #dee2e6 #dee2e6 #fff; +} +.nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.nav-pills .nav-link { + border-radius: 0.25rem; +} +.nav-pills .nav-link.active, +.nav-pills .show > .nav-link { + color: #fff; + background-color: #4a6fae; +} + +.nav-fill > .nav-link, +.nav-fill .nav-item { + flex: 1 1 auto; + text-align: center; +} + +.nav-justified > .nav-link, +.nav-justified .nav-item { + flex-basis: 0; + flex-grow: 1; + text-align: center; +} + +.tab-content > .tab-pane { + display: none; +} +.tab-content > .active { + display: block; +} + +.navbar { + position: relative; + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + padding: 0.5rem 1rem; +} +.navbar .container, +.navbar .container-fluid, +.navbar .container-sm, +.navbar .container-md, +.navbar .container-lg, +.navbar .container-xl { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; +} +.navbar-brand { + display: inline-block; + padding-top: 0.3125rem; + padding-bottom: 0.3125rem; + margin-right: 1rem; + font-size: 1.25rem; + line-height: inherit; + white-space: nowrap; +} +.navbar-brand:hover, .navbar-brand:focus { + text-decoration: none; +} + +.navbar-nav { + display: flex; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; + list-style: none; +} +.navbar-nav .nav-link { + padding-right: 0; + padding-left: 0; +} +.navbar-nav .dropdown-menu { + position: static; + float: none; +} + +.navbar-text { + display: inline-block; + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.navbar-collapse { + flex-basis: 100%; + flex-grow: 1; + align-items: center; +} + +.navbar-toggler { + padding: 0.25rem 0.75rem; + font-size: 1.25rem; + line-height: 1; + background-color: transparent; + border: 1px solid transparent; + border-radius: 0.25rem; +} +.navbar-toggler:hover, .navbar-toggler:focus { + text-decoration: none; +} + +.navbar-toggler-icon { + display: inline-block; + width: 1.5em; + height: 1.5em; + vertical-align: middle; + content: ""; + background: 50%/100% 100% no-repeat; +} + +.navbar-nav-scroll { + max-height: 75vh; + overflow-y: auto; +} + +@media (max-width: 575.98px) { + .navbar-expand-sm > .container, +.navbar-expand-sm > .container-fluid, +.navbar-expand-sm > .container-sm, +.navbar-expand-sm > .container-md, +.navbar-expand-sm > .container-lg, +.navbar-expand-sm > .container-xl { + padding-right: 0; + padding-left: 0; + } +} +@media (min-width: 576px) { + .navbar-expand-sm { + flex-flow: row nowrap; + justify-content: flex-start; + } + .navbar-expand-sm .navbar-nav { + flex-direction: row; + } + .navbar-expand-sm .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-sm .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-sm > .container, +.navbar-expand-sm > .container-fluid, +.navbar-expand-sm > .container-sm, +.navbar-expand-sm > .container-md, +.navbar-expand-sm > .container-lg, +.navbar-expand-sm > .container-xl { + flex-wrap: nowrap; + } + .navbar-expand-sm .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-sm .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-sm .navbar-toggler { + display: none; + } +} +@media (max-width: 767.98px) { + .navbar-expand-md > .container, +.navbar-expand-md > .container-fluid, +.navbar-expand-md > .container-sm, +.navbar-expand-md > .container-md, +.navbar-expand-md > .container-lg, +.navbar-expand-md > .container-xl { + padding-right: 0; + padding-left: 0; + } +} +@media (min-width: 768px) { + .navbar-expand-md { + flex-flow: row nowrap; + justify-content: flex-start; + } + .navbar-expand-md .navbar-nav { + flex-direction: row; + } + .navbar-expand-md .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-md .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-md > .container, +.navbar-expand-md > .container-fluid, +.navbar-expand-md > .container-sm, +.navbar-expand-md > .container-md, +.navbar-expand-md > .container-lg, +.navbar-expand-md > .container-xl { + flex-wrap: nowrap; + } + .navbar-expand-md .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-md .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-md .navbar-toggler { + display: none; + } +} +@media (max-width: 991.98px) { + .navbar-expand-lg > .container, +.navbar-expand-lg > .container-fluid, +.navbar-expand-lg > .container-sm, +.navbar-expand-lg > .container-md, +.navbar-expand-lg > .container-lg, +.navbar-expand-lg > .container-xl { + padding-right: 0; + padding-left: 0; + } +} +@media (min-width: 992px) { + .navbar-expand-lg { + flex-flow: row nowrap; + justify-content: flex-start; + } + .navbar-expand-lg .navbar-nav { + flex-direction: row; + } + .navbar-expand-lg .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-lg .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-lg > .container, +.navbar-expand-lg > .container-fluid, +.navbar-expand-lg > .container-sm, +.navbar-expand-lg > .container-md, +.navbar-expand-lg > .container-lg, +.navbar-expand-lg > .container-xl { + flex-wrap: nowrap; + } + .navbar-expand-lg .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-lg .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-lg .navbar-toggler { + display: none; + } +} +@media (max-width: 1199.98px) { + .navbar-expand-xl > .container, +.navbar-expand-xl > .container-fluid, +.navbar-expand-xl > .container-sm, +.navbar-expand-xl > .container-md, +.navbar-expand-xl > .container-lg, +.navbar-expand-xl > .container-xl { + padding-right: 0; + padding-left: 0; + } +} +@media (min-width: 1200px) { + .navbar-expand-xl { + flex-flow: row nowrap; + justify-content: flex-start; + } + .navbar-expand-xl .navbar-nav { + flex-direction: row; + } + .navbar-expand-xl .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-xl .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-xl > .container, +.navbar-expand-xl > .container-fluid, +.navbar-expand-xl > .container-sm, +.navbar-expand-xl > .container-md, +.navbar-expand-xl > .container-lg, +.navbar-expand-xl > .container-xl { + flex-wrap: nowrap; + } + .navbar-expand-xl .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-xl .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-xl .navbar-toggler { + display: none; + } +} +.navbar-expand { + flex-flow: row nowrap; + justify-content: flex-start; +} +.navbar-expand > .container, +.navbar-expand > .container-fluid, +.navbar-expand > .container-sm, +.navbar-expand > .container-md, +.navbar-expand > .container-lg, +.navbar-expand > .container-xl { + padding-right: 0; + padding-left: 0; +} +.navbar-expand .navbar-nav { + flex-direction: row; +} +.navbar-expand .navbar-nav .dropdown-menu { + position: absolute; +} +.navbar-expand .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; +} +.navbar-expand > .container, +.navbar-expand > .container-fluid, +.navbar-expand > .container-sm, +.navbar-expand > .container-md, +.navbar-expand > .container-lg, +.navbar-expand > .container-xl { + flex-wrap: nowrap; +} +.navbar-expand .navbar-nav-scroll { + overflow: visible; +} +.navbar-expand .navbar-collapse { + display: flex !important; + flex-basis: auto; +} +.navbar-expand .navbar-toggler { + display: none; +} + +.navbar-light .navbar-brand { + color: rgba(0, 0, 0, 0.9); +} +.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus { + color: rgba(0, 0, 0, 0.9); +} +.navbar-light .navbar-nav .nav-link { + color: rgba(0, 0, 0, 0.5); +} +.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus { + color: rgba(0, 0, 0, 0.7); +} +.navbar-light .navbar-nav .nav-link.disabled { + color: rgba(0, 0, 0, 0.3); +} +.navbar-light .navbar-nav .show > .nav-link, +.navbar-light .navbar-nav .active > .nav-link, +.navbar-light .navbar-nav .nav-link.show, +.navbar-light .navbar-nav .nav-link.active { + color: rgba(0, 0, 0, 0.9); +} +.navbar-light .navbar-toggler { + color: rgba(0, 0, 0, 0.5); + border-color: rgba(0, 0, 0, 0.1); +} +.navbar-light .navbar-toggler-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); +} +.navbar-light .navbar-text { + color: rgba(0, 0, 0, 0.5); +} +.navbar-light .navbar-text a { + color: rgba(0, 0, 0, 0.9); +} +.navbar-light .navbar-text a:hover, .navbar-light .navbar-text a:focus { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-dark .navbar-brand { + color: #fff; +} +.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus { + color: #fff; +} +.navbar-dark .navbar-nav .nav-link { + color: rgba(255, 255, 255, 0.5); +} +.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus { + color: rgba(255, 255, 255, 0.75); +} +.navbar-dark .navbar-nav .nav-link.disabled { + color: rgba(255, 255, 255, 0.25); +} +.navbar-dark .navbar-nav .show > .nav-link, +.navbar-dark .navbar-nav .active > .nav-link, +.navbar-dark .navbar-nav .nav-link.show, +.navbar-dark .navbar-nav .nav-link.active { + color: #fff; +} +.navbar-dark .navbar-toggler { + color: rgba(255, 255, 255, 0.5); + border-color: rgba(255, 255, 255, 0.1); +} +.navbar-dark .navbar-toggler-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); +} +.navbar-dark .navbar-text { + color: rgba(255, 255, 255, 0.5); +} +.navbar-dark .navbar-text a { + color: #fff; +} +.navbar-dark .navbar-text a:hover, .navbar-dark .navbar-text a:focus { + color: #fff; +} + +.card { + position: relative; + display: flex; + flex-direction: column; + min-width: 0; + word-wrap: break-word; + background-color: #fff; + background-clip: border-box; + border: 1px solid rgba(0, 0, 0, 0.125); + border-radius: 0.25rem; +} +.card > hr { + margin-right: 0; + margin-left: 0; +} +.card > .list-group { + border-top: inherit; + border-bottom: inherit; +} +.card > .list-group:first-child { + border-top-width: 0; + border-top-left-radius: calc(0.25rem - 1px); + border-top-right-radius: calc(0.25rem - 1px); +} +.card > .list-group:last-child { + border-bottom-width: 0; + border-bottom-right-radius: calc(0.25rem - 1px); + border-bottom-left-radius: calc(0.25rem - 1px); +} +.card > .card-header + .list-group, +.card > .list-group + .card-footer { + border-top: 0; +} + +.card-body { + flex: 1 1 auto; + min-height: 1px; + padding: 1.25rem; +} + +.card-title { + margin-bottom: 0.75rem; +} + +.card-subtitle { + margin-top: -0.375rem; + margin-bottom: 0; +} + +.card-text:last-child { + margin-bottom: 0; +} + +.card-link:hover { + text-decoration: none; +} +.card-link + .card-link { + margin-left: 1.25rem; +} + +.card-header { + padding: 0.75rem 1.25rem; + margin-bottom: 0; + background-color: rgba(0, 0, 0, 0.03); + border-bottom: 1px solid rgba(0, 0, 0, 0.125); +} +.card-header:first-child { + border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0; +} + +.card-footer { + padding: 0.75rem 1.25rem; + background-color: rgba(0, 0, 0, 0.03); + border-top: 1px solid rgba(0, 0, 0, 0.125); +} +.card-footer:last-child { + border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px); +} + +.card-header-tabs { + margin-right: -0.625rem; + margin-bottom: -0.75rem; + margin-left: -0.625rem; + border-bottom: 0; +} + +.card-header-pills { + margin-right: -0.625rem; + margin-left: -0.625rem; +} + +.card-img-overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding: 1.25rem; + border-radius: calc(0.25rem - 1px); +} + +.card-img, +.card-img-top, +.card-img-bottom { + flex-shrink: 0; + width: 100%; +} + +.card-img, +.card-img-top { + border-top-left-radius: calc(0.25rem - 1px); + border-top-right-radius: calc(0.25rem - 1px); +} + +.card-img, +.card-img-bottom { + border-bottom-right-radius: calc(0.25rem - 1px); + border-bottom-left-radius: calc(0.25rem - 1px); +} + +.card-deck .card { + margin-bottom: 15px; +} +@media (min-width: 576px) { + .card-deck { + display: flex; + flex-flow: row wrap; + margin-right: -15px; + margin-left: -15px; + } + .card-deck .card { + flex: 1 0 0%; + margin-right: 15px; + margin-bottom: 0; + margin-left: 15px; + } +} + +.card-group > .card { + margin-bottom: 15px; +} +@media (min-width: 576px) { + .card-group { + display: flex; + flex-flow: row wrap; + } + .card-group > .card { + flex: 1 0 0%; + margin-bottom: 0; + } + .card-group > .card + .card { + margin-left: 0; + border-left: 0; + } + .card-group > .card:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + .card-group > .card:not(:last-child) .card-img-top, +.card-group > .card:not(:last-child) .card-header { + border-top-right-radius: 0; + } + .card-group > .card:not(:last-child) .card-img-bottom, +.card-group > .card:not(:last-child) .card-footer { + border-bottom-right-radius: 0; + } + .card-group > .card:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + .card-group > .card:not(:first-child) .card-img-top, +.card-group > .card:not(:first-child) .card-header { + border-top-left-radius: 0; + } + .card-group > .card:not(:first-child) .card-img-bottom, +.card-group > .card:not(:first-child) .card-footer { + border-bottom-left-radius: 0; + } +} + +.card-columns .card { + margin-bottom: 0.75rem; +} +@media (min-width: 576px) { + .card-columns { + column-count: 3; + column-gap: 1.25rem; + orphans: 1; + widows: 1; + } + .card-columns .card { + display: inline-block; + width: 100%; + } +} + +.accordion { + overflow-anchor: none; +} +.accordion > .card { + overflow: hidden; +} +.accordion > .card:not(:last-of-type) { + border-bottom: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.accordion > .card:not(:first-of-type) { + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.accordion > .card > .card-header { + border-radius: 0; + margin-bottom: -1px; +} + +.breadcrumb { + display: flex; + flex-wrap: wrap; + padding: 0.75rem 1rem; + margin-bottom: 1rem; + list-style: none; + background-color: #e9ecef; + border-radius: 0.25rem; +} + +.breadcrumb-item + .breadcrumb-item { + padding-left: 0.5rem; +} +.breadcrumb-item + .breadcrumb-item::before { + float: left; + padding-right: 0.5rem; + color: #6c757d; + content: "/"; +} +.breadcrumb-item + .breadcrumb-item:hover::before { + text-decoration: underline; +} +.breadcrumb-item + .breadcrumb-item:hover::before { + text-decoration: none; +} +.breadcrumb-item.active { + color: #6c757d; +} + +.pagination { + display: flex; + padding-left: 0; + list-style: none; + border-radius: 0.25rem; +} + +.page-link { + position: relative; + display: block; + padding: 0.5rem 0.75rem; + margin-left: -1px; + line-height: 1.25; + color: #4a6fae; + background-color: #fff; + border: 1px solid #dee2e6; +} +.page-link:hover { + z-index: 2; + color: #334d78; + text-decoration: none; + background-color: #e9ecef; + border-color: #dee2e6; +} +.page-link:focus { + z-index: 3; + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(74, 111, 174, 0.25); +} + +.page-item:first-child .page-link { + margin-left: 0; + border-top-left-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} +.page-item:last-child .page-link { + border-top-right-radius: 0.25rem; + border-bottom-right-radius: 0.25rem; +} +.page-item.active .page-link { + z-index: 3; + color: #fff; + background-color: #4a6fae; + border-color: #4a6fae; +} +.page-item.disabled .page-link { + color: #6c757d; + pointer-events: none; + cursor: auto; + background-color: #fff; + border-color: #dee2e6; +} + +.pagination-lg .page-link { + padding: 0.75rem 1.5rem; + font-size: 1.25rem; + line-height: 1.5; +} +.pagination-lg .page-item:first-child .page-link { + border-top-left-radius: 0.3rem; + border-bottom-left-radius: 0.3rem; +} +.pagination-lg .page-item:last-child .page-link { + border-top-right-radius: 0.3rem; + border-bottom-right-radius: 0.3rem; +} + +.pagination-sm .page-link { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; +} +.pagination-sm .page-item:first-child .page-link { + border-top-left-radius: 0.2rem; + border-bottom-left-radius: 0.2rem; +} +.pagination-sm .page-item:last-child .page-link { + border-top-right-radius: 0.2rem; + border-bottom-right-radius: 0.2rem; +} + +.badge { + display: inline-block; + padding: 0.25em 0.4em; + font-size: 75%; + font-weight: 700; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.25rem; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .badge { + transition: none; + } +} +a.badge:hover, a.badge:focus { + text-decoration: none; +} + +.badge:empty { + display: none; +} + +.btn .badge { + position: relative; + top: -1px; +} + +.badge-pill { + padding-right: 0.6em; + padding-left: 0.6em; + border-radius: 10rem; +} + +.badge-primary { + color: #fff; + background-color: #4a6fae; +} +a.badge-primary:hover, a.badge-primary:focus { + color: #fff; + background-color: #3b588a; +} +a.badge-primary:focus, a.badge-primary.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(74, 111, 174, 0.5); +} + +.badge-secondary { + color: #fff; + background-color: #6c757d; +} +a.badge-secondary:hover, a.badge-secondary:focus { + color: #fff; + background-color: #545b62; +} +a.badge-secondary:focus, a.badge-secondary.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); +} + +.badge-success { + color: #212529; + background-color: #80cc28; +} +a.badge-success:hover, a.badge-success:focus { + color: #212529; + background-color: #65a120; +} +a.badge-success:focus, a.badge-success.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(128, 204, 40, 0.5); +} + +.badge-info { + color: #fff; + background-color: #17a2b8; +} +a.badge-info:hover, a.badge-info:focus { + color: #fff; + background-color: #117a8b; +} +a.badge-info:focus, a.badge-info.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); +} + +.badge-warning { + color: #212529; + background-color: #ffc107; +} +a.badge-warning:hover, a.badge-warning:focus { + color: #212529; + background-color: #d39e00; +} +a.badge-warning:focus, a.badge-warning.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); +} + +.badge-danger { + color: #fff; + background-color: red; +} +a.badge-danger:hover, a.badge-danger:focus { + color: #fff; + background-color: #cc0000; +} +a.badge-danger:focus, a.badge-danger.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(255, 0, 0, 0.5); +} + +.badge-light { + color: #212529; + background-color: #f5f5f5; +} +a.badge-light:hover, a.badge-light:focus { + color: #212529; + background-color: gainsboro; +} +a.badge-light:focus, a.badge-light.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(245, 245, 245, 0.5); +} + +.badge-dark { + color: #fff; + background-color: #343a40; +} +a.badge-dark:hover, a.badge-dark:focus { + color: #fff; + background-color: #1d2124; +} +a.badge-dark:focus, a.badge-dark.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); +} + +.jumbotron { + padding: 2rem 1rem; + margin-bottom: 2rem; + background-color: #e9ecef; + border-radius: 0.3rem; +} +@media (min-width: 576px) { + .jumbotron { + padding: 4rem 2rem; + } +} + +.jumbotron-fluid { + padding-right: 0; + padding-left: 0; + border-radius: 0; +} + +.alert { + position: relative; + padding: 0.75rem 1.25rem; + margin-bottom: 1rem; + border: 1px solid transparent; + border-radius: 0.25rem; +} + +.alert-heading { + color: inherit; +} + +.alert-link { + font-weight: 700; +} + +.alert-dismissible { + padding-right: 4rem; +} +.alert-dismissible .close { + position: absolute; + top: 0; + right: 0; + z-index: 2; + padding: 0.75rem 1.25rem; + color: inherit; +} + +.alert-primary { + color: #263a5a; + background-color: #dbe2ef; + border-color: #ccd7e8; +} +.alert-primary hr { + border-top-color: #bac9e0; +} +.alert-primary .alert-link { + color: #172336; +} + +.alert-secondary { + color: #383d41; + background-color: #e2e3e5; + border-color: #d6d8db; +} +.alert-secondary hr { + border-top-color: #c8cbcf; +} +.alert-secondary .alert-link { + color: #202326; +} + +.alert-success { + color: #436a15; + background-color: #e6f5d4; + border-color: #dbf1c3; +} +.alert-success hr { + border-top-color: #cfecae; +} +.alert-success .alert-link { + color: #283f0d; +} + +.alert-info { + color: #0c5460; + background-color: #d1ecf1; + border-color: #bee5eb; +} +.alert-info hr { + border-top-color: #abdde5; +} +.alert-info .alert-link { + color: #062c33; +} + +.alert-warning { + color: #856404; + background-color: #fff3cd; + border-color: #ffeeba; +} +.alert-warning hr { + border-top-color: #ffe8a1; +} +.alert-warning .alert-link { + color: #533f03; +} + +.alert-danger { + color: #850000; + background-color: #ffcccc; + border-color: #ffb8b8; +} +.alert-danger hr { + border-top-color: #ff9f9f; +} +.alert-danger .alert-link { + color: #520000; +} + +.alert-light { + color: #7f7f7f; + background-color: #fdfdfd; + border-color: #fcfcfc; +} +.alert-light hr { + border-top-color: #efefef; +} +.alert-light .alert-link { + color: #666666; +} + +.alert-dark { + color: #1b1e21; + background-color: #d6d8d9; + border-color: #c6c8ca; +} +.alert-dark hr { + border-top-color: #b9bbbe; +} +.alert-dark .alert-link { + color: #040505; +} + +@keyframes progress-bar-stripes { + from { + background-position: 1rem 0; + } + to { + background-position: 0 0; + } +} +.progress { + display: flex; + height: 1rem; + overflow: hidden; + line-height: 0; + font-size: 0.75rem; + background-color: #e9ecef; + border-radius: 0.25rem; +} + +.progress-bar { + display: flex; + flex-direction: column; + justify-content: center; + overflow: hidden; + color: #fff; + text-align: center; + white-space: nowrap; + background-color: #4a6fae; + transition: width 0.6s ease; +} +@media (prefers-reduced-motion: reduce) { + .progress-bar { + transition: none; + } +} + +.progress-bar-striped { + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-size: 1rem 1rem; +} + +.progress-bar-animated { + animation: 1s linear infinite progress-bar-stripes; +} +@media (prefers-reduced-motion: reduce) { + .progress-bar-animated { + animation: none; + } +} + +.media { + display: flex; + align-items: flex-start; +} + +.media-body { + flex: 1; +} + +.list-group { + display: flex; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; + border-radius: 0.25rem; +} + +.list-group-item-action { + width: 100%; + color: #495057; + text-align: inherit; +} +.list-group-item-action:hover, .list-group-item-action:focus { + z-index: 1; + color: #495057; + text-decoration: none; + background-color: #f8f9fa; +} +.list-group-item-action:active { + color: #212529; + background-color: #e9ecef; +} + +.list-group-item { + position: relative; + display: block; + padding: 0.75rem 1.25rem; + background-color: #fff; + border: 1px solid rgba(0, 0, 0, 0.125); +} +.list-group-item:first-child { + border-top-left-radius: inherit; + border-top-right-radius: inherit; +} +.list-group-item:last-child { + border-bottom-right-radius: inherit; + border-bottom-left-radius: inherit; +} +.list-group-item.disabled, .list-group-item:disabled { + color: #6c757d; + pointer-events: none; + background-color: #fff; +} +.list-group-item.active { + z-index: 2; + color: #fff; + background-color: #4a6fae; + border-color: #4a6fae; +} +.list-group-item + .list-group-item { + border-top-width: 0; +} +.list-group-item + .list-group-item.active { + margin-top: -1px; + border-top-width: 1px; +} + +.list-group-horizontal { + flex-direction: row; +} +.list-group-horizontal > .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; +} +.list-group-horizontal > .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; +} +.list-group-horizontal > .list-group-item.active { + margin-top: 0; +} +.list-group-horizontal > .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; +} +.list-group-horizontal > .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; +} + +@media (min-width: 576px) { + .list-group-horizontal-sm { + flex-direction: row; + } + .list-group-horizontal-sm > .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-sm > .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; + } + .list-group-horizontal-sm > .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-sm > .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; + } + .list-group-horizontal-sm > .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; + } +} +@media (min-width: 768px) { + .list-group-horizontal-md { + flex-direction: row; + } + .list-group-horizontal-md > .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-md > .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; + } + .list-group-horizontal-md > .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-md > .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; + } + .list-group-horizontal-md > .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; + } +} +@media (min-width: 992px) { + .list-group-horizontal-lg { + flex-direction: row; + } + .list-group-horizontal-lg > .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-lg > .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; + } + .list-group-horizontal-lg > .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-lg > .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; + } + .list-group-horizontal-lg > .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; + } +} +@media (min-width: 1200px) { + .list-group-horizontal-xl { + flex-direction: row; + } + .list-group-horizontal-xl > .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-xl > .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; + } + .list-group-horizontal-xl > .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-xl > .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; + } + .list-group-horizontal-xl > .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; + } +} +.list-group-flush { + border-radius: 0; +} +.list-group-flush > .list-group-item { + border-width: 0 0 1px; +} +.list-group-flush > .list-group-item:last-child { + border-bottom-width: 0; +} + +.list-group-item-primary { + color: #263a5a; + background-color: #ccd7e8; +} +.list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus { + color: #263a5a; + background-color: #bac9e0; +} +.list-group-item-primary.list-group-item-action.active { + color: #fff; + background-color: #263a5a; + border-color: #263a5a; +} + +.list-group-item-secondary { + color: #383d41; + background-color: #d6d8db; +} +.list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus { + color: #383d41; + background-color: #c8cbcf; +} +.list-group-item-secondary.list-group-item-action.active { + color: #fff; + background-color: #383d41; + border-color: #383d41; +} + +.list-group-item-success { + color: #436a15; + background-color: #dbf1c3; +} +.list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus { + color: #436a15; + background-color: #cfecae; +} +.list-group-item-success.list-group-item-action.active { + color: #fff; + background-color: #436a15; + border-color: #436a15; +} + +.list-group-item-info { + color: #0c5460; + background-color: #bee5eb; +} +.list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus { + color: #0c5460; + background-color: #abdde5; +} +.list-group-item-info.list-group-item-action.active { + color: #fff; + background-color: #0c5460; + border-color: #0c5460; +} + +.list-group-item-warning { + color: #856404; + background-color: #ffeeba; +} +.list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus { + color: #856404; + background-color: #ffe8a1; +} +.list-group-item-warning.list-group-item-action.active { + color: #fff; + background-color: #856404; + border-color: #856404; +} + +.list-group-item-danger { + color: #850000; + background-color: #ffb8b8; +} +.list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus { + color: #850000; + background-color: #ff9f9f; +} +.list-group-item-danger.list-group-item-action.active { + color: #fff; + background-color: #850000; + border-color: #850000; +} + +.list-group-item-light { + color: #7f7f7f; + background-color: #fcfcfc; +} +.list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus { + color: #7f7f7f; + background-color: #efefef; +} +.list-group-item-light.list-group-item-action.active { + color: #fff; + background-color: #7f7f7f; + border-color: #7f7f7f; +} + +.list-group-item-dark { + color: #1b1e21; + background-color: #c6c8ca; +} +.list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus { + color: #1b1e21; + background-color: #b9bbbe; +} +.list-group-item-dark.list-group-item-action.active { + color: #fff; + background-color: #1b1e21; + border-color: #1b1e21; +} + +.close { + float: right; + font-size: 1.5rem; + font-weight: 700; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + opacity: 0.5; +} +.close:hover { + color: #000; + text-decoration: none; +} +.close:not(:disabled):not(.disabled):hover, .close:not(:disabled):not(.disabled):focus { + opacity: 0.75; +} + +button.close { + padding: 0; + background-color: transparent; + border: 0; +} + +a.close.disabled { + pointer-events: none; +} + +.toast { + flex-basis: 350px; + max-width: 350px; + font-size: 0.875rem; + background-color: rgba(255, 255, 255, 0.85); + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.1); + box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1); + opacity: 0; + border-radius: 0.25rem; +} +.toast:not(:last-child) { + margin-bottom: 0.75rem; +} +.toast.showing { + opacity: 1; +} +.toast.show { + display: block; + opacity: 1; +} +.toast.hide { + display: none; +} + +.toast-header { + display: flex; + align-items: center; + padding: 0.25rem 0.75rem; + color: #6c757d; + background-color: rgba(255, 255, 255, 0.85); + background-clip: padding-box; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); + border-top-left-radius: calc(0.25rem - 1px); + border-top-right-radius: calc(0.25rem - 1px); +} + +.toast-body { + padding: 0.75rem; +} + +.modal-open { + overflow: hidden; +} +.modal-open .modal { + overflow-x: hidden; + overflow-y: auto; +} + +.modal { + position: fixed; + top: 0; + left: 0; + z-index: 1050; + display: none; + width: 100%; + height: 100%; + overflow: hidden; + outline: 0; +} + +.modal-dialog { + position: relative; + width: auto; + margin: 0.5rem; + pointer-events: none; +} +.modal.fade .modal-dialog { + transition: transform 0.3s ease-out; + transform: translate(0, -50px); +} +@media (prefers-reduced-motion: reduce) { + .modal.fade .modal-dialog { + transition: none; + } +} +.modal.show .modal-dialog { + transform: none; +} +.modal.modal-static .modal-dialog { + transform: scale(1.02); +} + +.modal-dialog-scrollable { + display: flex; + max-height: calc(100% - 1rem); +} +.modal-dialog-scrollable .modal-content { + max-height: calc(100vh - 1rem); + overflow: hidden; +} +.modal-dialog-scrollable .modal-header, +.modal-dialog-scrollable .modal-footer { + flex-shrink: 0; +} +.modal-dialog-scrollable .modal-body { + overflow-y: auto; +} + +.modal-dialog-centered { + display: flex; + align-items: center; + min-height: calc(100% - 1rem); +} +.modal-dialog-centered::before { + display: block; + height: calc(100vh - 1rem); + height: min-content; + content: ""; +} +.modal-dialog-centered.modal-dialog-scrollable { + flex-direction: column; + justify-content: center; + height: 100%; +} +.modal-dialog-centered.modal-dialog-scrollable .modal-content { + max-height: none; +} +.modal-dialog-centered.modal-dialog-scrollable::before { + content: none; +} + +.modal-content { + position: relative; + display: flex; + flex-direction: column; + width: 100%; + pointer-events: auto; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0.3rem; + outline: 0; +} + +.modal-backdrop { + position: fixed; + top: 0; + left: 0; + z-index: 1040; + width: 100vw; + height: 100vh; + background-color: #000; +} +.modal-backdrop.fade { + opacity: 0; +} +.modal-backdrop.show { + opacity: 0.5; +} + +.modal-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: 1rem 1rem; + border-bottom: 1px solid #dee2e6; + border-top-left-radius: calc(0.3rem - 1px); + border-top-right-radius: calc(0.3rem - 1px); +} +.modal-header .close { + padding: 1rem 1rem; + margin: -1rem -1rem -1rem auto; +} + +.modal-title { + margin-bottom: 0; + line-height: 1.5; +} + +.modal-body { + position: relative; + flex: 1 1 auto; + padding: 1rem; +} + +.modal-footer { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: flex-end; + padding: 0.75rem; + border-top: 1px solid #dee2e6; + border-bottom-right-radius: calc(0.3rem - 1px); + border-bottom-left-radius: calc(0.3rem - 1px); +} +.modal-footer > * { + margin: 0.25rem; +} + +.modal-scrollbar-measure { + position: absolute; + top: -9999px; + width: 50px; + height: 50px; + overflow: scroll; +} + +@media (min-width: 576px) { + .modal-dialog { + max-width: 500px; + margin: 1.75rem auto; + } + + .modal-dialog-scrollable { + max-height: calc(100% - 3.5rem); + } + .modal-dialog-scrollable .modal-content { + max-height: calc(100vh - 3.5rem); + } + + .modal-dialog-centered { + min-height: calc(100% - 3.5rem); + } + .modal-dialog-centered::before { + height: calc(100vh - 3.5rem); + height: min-content; + } + + .modal-sm { + max-width: 300px; + } +} +@media (min-width: 992px) { + .modal-lg, +.modal-xl { + max-width: 800px; + } +} +@media (min-width: 1200px) { + .modal-xl { + max-width: 1140px; + } +} +.tooltip { + position: absolute; + z-index: 1070; + display: block; + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-style: normal; + font-weight: 400; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + white-space: normal; + line-break: auto; + font-size: 0.875rem; + word-wrap: break-word; + opacity: 0; +} +.tooltip.show { + opacity: 0.9; +} +.tooltip .arrow { + position: absolute; + display: block; + width: 0.8rem; + height: 0.4rem; +} +.tooltip .arrow::before { + position: absolute; + content: ""; + border-color: transparent; + border-style: solid; +} + +.bs-tooltip-top, .bs-tooltip-auto[x-placement^=top] { + padding: 0.4rem 0; +} +.bs-tooltip-top .arrow, .bs-tooltip-auto[x-placement^=top] .arrow { + bottom: 0; +} +.bs-tooltip-top .arrow::before, .bs-tooltip-auto[x-placement^=top] .arrow::before { + top: 0; + border-width: 0.4rem 0.4rem 0; + border-top-color: #000; +} + +.bs-tooltip-right, .bs-tooltip-auto[x-placement^=right] { + padding: 0 0.4rem; +} +.bs-tooltip-right .arrow, .bs-tooltip-auto[x-placement^=right] .arrow { + left: 0; + width: 0.4rem; + height: 0.8rem; +} +.bs-tooltip-right .arrow::before, .bs-tooltip-auto[x-placement^=right] .arrow::before { + right: 0; + border-width: 0.4rem 0.4rem 0.4rem 0; + border-right-color: #000; +} + +.bs-tooltip-bottom, .bs-tooltip-auto[x-placement^=bottom] { + padding: 0.4rem 0; +} +.bs-tooltip-bottom .arrow, .bs-tooltip-auto[x-placement^=bottom] .arrow { + top: 0; +} +.bs-tooltip-bottom .arrow::before, .bs-tooltip-auto[x-placement^=bottom] .arrow::before { + bottom: 0; + border-width: 0 0.4rem 0.4rem; + border-bottom-color: #000; +} + +.bs-tooltip-left, .bs-tooltip-auto[x-placement^=left] { + padding: 0 0.4rem; +} +.bs-tooltip-left .arrow, .bs-tooltip-auto[x-placement^=left] .arrow { + right: 0; + width: 0.4rem; + height: 0.8rem; +} +.bs-tooltip-left .arrow::before, .bs-tooltip-auto[x-placement^=left] .arrow::before { + left: 0; + border-width: 0.4rem 0 0.4rem 0.4rem; + border-left-color: #000; +} + +.tooltip-inner { + max-width: 200px; + padding: 0.25rem 0.5rem; + color: #fff; + text-align: center; + background-color: #000; + border-radius: 0.25rem; +} + +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1060; + display: block; + max-width: 276px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-style: normal; + font-weight: 400; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + white-space: normal; + line-break: auto; + font-size: 0.875rem; + word-wrap: break-word; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0.3rem; +} +.popover .arrow { + position: absolute; + display: block; + width: 1rem; + height: 0.5rem; + margin: 0 0.3rem; +} +.popover .arrow::before, .popover .arrow::after { + position: absolute; + display: block; + content: ""; + border-color: transparent; + border-style: solid; +} + +.bs-popover-top, .bs-popover-auto[x-placement^=top] { + margin-bottom: 0.5rem; +} +.bs-popover-top > .arrow, .bs-popover-auto[x-placement^=top] > .arrow { + bottom: calc(-0.5rem - 1px); +} +.bs-popover-top > .arrow::before, .bs-popover-auto[x-placement^=top] > .arrow::before { + bottom: 0; + border-width: 0.5rem 0.5rem 0; + border-top-color: rgba(0, 0, 0, 0.25); +} +.bs-popover-top > .arrow::after, .bs-popover-auto[x-placement^=top] > .arrow::after { + bottom: 1px; + border-width: 0.5rem 0.5rem 0; + border-top-color: #fff; +} + +.bs-popover-right, .bs-popover-auto[x-placement^=right] { + margin-left: 0.5rem; +} +.bs-popover-right > .arrow, .bs-popover-auto[x-placement^=right] > .arrow { + left: calc(-0.5rem - 1px); + width: 0.5rem; + height: 1rem; + margin: 0.3rem 0; +} +.bs-popover-right > .arrow::before, .bs-popover-auto[x-placement^=right] > .arrow::before { + left: 0; + border-width: 0.5rem 0.5rem 0.5rem 0; + border-right-color: rgba(0, 0, 0, 0.25); +} +.bs-popover-right > .arrow::after, .bs-popover-auto[x-placement^=right] > .arrow::after { + left: 1px; + border-width: 0.5rem 0.5rem 0.5rem 0; + border-right-color: #fff; +} + +.bs-popover-bottom, .bs-popover-auto[x-placement^=bottom] { + margin-top: 0.5rem; +} +.bs-popover-bottom > .arrow, .bs-popover-auto[x-placement^=bottom] > .arrow { + top: calc(-0.5rem - 1px); +} +.bs-popover-bottom > .arrow::before, .bs-popover-auto[x-placement^=bottom] > .arrow::before { + top: 0; + border-width: 0 0.5rem 0.5rem 0.5rem; + border-bottom-color: rgba(0, 0, 0, 0.25); +} +.bs-popover-bottom > .arrow::after, .bs-popover-auto[x-placement^=bottom] > .arrow::after { + top: 1px; + border-width: 0 0.5rem 0.5rem 0.5rem; + border-bottom-color: #fff; +} +.bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^=bottom] .popover-header::before { + position: absolute; + top: 0; + left: 50%; + display: block; + width: 1rem; + margin-left: -0.5rem; + content: ""; + border-bottom: 1px solid #f7f7f7; +} + +.bs-popover-left, .bs-popover-auto[x-placement^=left] { + margin-right: 0.5rem; +} +.bs-popover-left > .arrow, .bs-popover-auto[x-placement^=left] > .arrow { + right: calc(-0.5rem - 1px); + width: 0.5rem; + height: 1rem; + margin: 0.3rem 0; +} +.bs-popover-left > .arrow::before, .bs-popover-auto[x-placement^=left] > .arrow::before { + right: 0; + border-width: 0.5rem 0 0.5rem 0.5rem; + border-left-color: rgba(0, 0, 0, 0.25); +} +.bs-popover-left > .arrow::after, .bs-popover-auto[x-placement^=left] > .arrow::after { + right: 1px; + border-width: 0.5rem 0 0.5rem 0.5rem; + border-left-color: #fff; +} + +.popover-header { + padding: 0.5rem 0.75rem; + margin-bottom: 0; + font-size: 1rem; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-top-left-radius: calc(0.3rem - 1px); + border-top-right-radius: calc(0.3rem - 1px); +} +.popover-header:empty { + display: none; +} + +.popover-body { + padding: 0.5rem 0.75rem; + color: #212529; +} + +.carousel { + position: relative; +} + +.carousel.pointer-event { + touch-action: pan-y; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} +.carousel-inner::after { + display: block; + clear: both; + content: ""; +} + +.carousel-item { + position: relative; + display: none; + float: left; + width: 100%; + margin-right: -100%; + backface-visibility: hidden; + transition: transform 0.6s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .carousel-item { + transition: none; + } +} + +.carousel-item.active, +.carousel-item-next, +.carousel-item-prev { + display: block; +} + +.carousel-item-next:not(.carousel-item-left), +.active.carousel-item-right { + transform: translateX(100%); +} + +.carousel-item-prev:not(.carousel-item-right), +.active.carousel-item-left { + transform: translateX(-100%); +} + +.carousel-fade .carousel-item { + opacity: 0; + transition-property: opacity; + transform: none; +} +.carousel-fade .carousel-item.active, +.carousel-fade .carousel-item-next.carousel-item-left, +.carousel-fade .carousel-item-prev.carousel-item-right { + z-index: 1; + opacity: 1; +} +.carousel-fade .active.carousel-item-left, +.carousel-fade .active.carousel-item-right { + z-index: 0; + opacity: 0; + transition: opacity 0s 0.6s; +} +@media (prefers-reduced-motion: reduce) { + .carousel-fade .active.carousel-item-left, +.carousel-fade .active.carousel-item-right { + transition: none; + } +} + +.carousel-control-prev, +.carousel-control-next { + position: absolute; + top: 0; + bottom: 0; + z-index: 1; + display: flex; + align-items: center; + justify-content: center; + width: 15%; + padding: 0; + color: #fff; + text-align: center; + background: none; + border: 0; + opacity: 0.5; + transition: opacity 0.15s ease; +} +@media (prefers-reduced-motion: reduce) { + .carousel-control-prev, +.carousel-control-next { + transition: none; + } +} +.carousel-control-prev:hover, .carousel-control-prev:focus, +.carousel-control-next:hover, +.carousel-control-next:focus { + color: #fff; + text-decoration: none; + outline: 0; + opacity: 0.9; +} + +.carousel-control-prev { + left: 0; +} + +.carousel-control-next { + right: 0; +} + +.carousel-control-prev-icon, +.carousel-control-next-icon { + display: inline-block; + width: 20px; + height: 20px; + background: 50%/100% 100% no-repeat; +} + +.carousel-control-prev-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e"); +} + +.carousel-control-next-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e"); +} + +.carousel-indicators { + position: absolute; + right: 0; + bottom: 0; + left: 0; + z-index: 15; + display: flex; + justify-content: center; + padding-left: 0; + margin-right: 15%; + margin-left: 15%; + list-style: none; +} +.carousel-indicators li { + box-sizing: content-box; + flex: 0 1 auto; + width: 30px; + height: 3px; + margin-right: 3px; + margin-left: 3px; + text-indent: -999px; + cursor: pointer; + background-color: #fff; + background-clip: padding-box; + border-top: 10px solid transparent; + border-bottom: 10px solid transparent; + opacity: 0.5; + transition: opacity 0.6s ease; +} +@media (prefers-reduced-motion: reduce) { + .carousel-indicators li { + transition: none; + } +} +.carousel-indicators .active { + opacity: 1; +} + +.carousel-caption { + position: absolute; + right: 15%; + bottom: 20px; + left: 15%; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: #fff; + text-align: center; +} + +@keyframes spinner-border { + to { + transform: rotate(360deg); + } +} +.spinner-border { + display: inline-block; + width: 2rem; + height: 2rem; + vertical-align: -0.125em; + border: 0.25em solid currentColor; + border-right-color: transparent; + border-radius: 50%; + animation: 0.75s linear infinite spinner-border; +} + +.spinner-border-sm { + width: 1rem; + height: 1rem; + border-width: 0.2em; +} + +@keyframes spinner-grow { + 0% { + transform: scale(0); + } + 50% { + opacity: 1; + transform: none; + } +} +.spinner-grow { + display: inline-block; + width: 2rem; + height: 2rem; + vertical-align: -0.125em; + background-color: currentColor; + border-radius: 50%; + opacity: 0; + animation: 0.75s linear infinite spinner-grow; +} + +.spinner-grow-sm { + width: 1rem; + height: 1rem; +} + +@media (prefers-reduced-motion: reduce) { + .spinner-border, +.spinner-grow { + animation-duration: 1.5s; + } +} +.align-baseline { + vertical-align: baseline !important; +} + +.align-top { + vertical-align: top !important; +} + +.align-middle { + vertical-align: middle !important; +} + +.align-bottom { + vertical-align: bottom !important; +} + +.align-text-bottom { + vertical-align: text-bottom !important; +} + +.align-text-top { + vertical-align: text-top !important; +} + +.bg-primary { + background-color: #4a6fae !important; +} + +a.bg-primary:hover, a.bg-primary:focus, +button.bg-primary:hover, +button.bg-primary:focus { + background-color: #3b588a !important; +} + +.bg-secondary { + background-color: #6c757d !important; +} + +a.bg-secondary:hover, a.bg-secondary:focus, +button.bg-secondary:hover, +button.bg-secondary:focus { + background-color: #545b62 !important; +} + +.bg-success { + background-color: #80cc28 !important; +} + +a.bg-success:hover, a.bg-success:focus, +button.bg-success:hover, +button.bg-success:focus { + background-color: #65a120 !important; +} + +.bg-info { + background-color: #17a2b8 !important; +} + +a.bg-info:hover, a.bg-info:focus, +button.bg-info:hover, +button.bg-info:focus { + background-color: #117a8b !important; +} + +.bg-warning { + background-color: #ffc107 !important; +} + +a.bg-warning:hover, a.bg-warning:focus, +button.bg-warning:hover, +button.bg-warning:focus { + background-color: #d39e00 !important; +} + +.bg-danger { + background-color: red !important; +} + +a.bg-danger:hover, a.bg-danger:focus, +button.bg-danger:hover, +button.bg-danger:focus { + background-color: #cc0000 !important; +} + +.bg-light { + background-color: #f5f5f5 !important; +} + +a.bg-light:hover, a.bg-light:focus, +button.bg-light:hover, +button.bg-light:focus { + background-color: gainsboro !important; +} + +.bg-dark { + background-color: #343a40 !important; +} + +a.bg-dark:hover, a.bg-dark:focus, +button.bg-dark:hover, +button.bg-dark:focus { + background-color: #1d2124 !important; +} + +.bg-white { + background-color: #fff !important; +} + +.bg-transparent { + background-color: transparent !important; +} + +.border { + border: 1px solid #dee2e6 !important; +} + +.border-top { + border-top: 1px solid #dee2e6 !important; +} + +.border-right { + border-right: 1px solid #dee2e6 !important; +} + +.border-bottom { + border-bottom: 1px solid #dee2e6 !important; +} + +.border-left { + border-left: 1px solid #dee2e6 !important; +} + +.border-0 { + border: 0 !important; +} + +.border-top-0 { + border-top: 0 !important; +} + +.border-right-0 { + border-right: 0 !important; +} + +.border-bottom-0 { + border-bottom: 0 !important; +} + +.border-left-0 { + border-left: 0 !important; +} + +.border-primary { + border-color: #4a6fae !important; +} + +.border-secondary { + border-color: #6c757d !important; +} + +.border-success { + border-color: #80cc28 !important; +} + +.border-info { + border-color: #17a2b8 !important; +} + +.border-warning { + border-color: #ffc107 !important; +} + +.border-danger { + border-color: red !important; +} + +.border-light { + border-color: #f5f5f5 !important; +} + +.border-dark { + border-color: #343a40 !important; +} + +.border-white { + border-color: #fff !important; +} + +.rounded-sm { + border-radius: 0.2rem !important; +} + +.rounded { + border-radius: 0.25rem !important; +} + +.rounded-top { + border-top-left-radius: 0.25rem !important; + border-top-right-radius: 0.25rem !important; +} + +.rounded-right { + border-top-right-radius: 0.25rem !important; + border-bottom-right-radius: 0.25rem !important; +} + +.rounded-bottom { + border-bottom-right-radius: 0.25rem !important; + border-bottom-left-radius: 0.25rem !important; +} + +.rounded-left { + border-top-left-radius: 0.25rem !important; + border-bottom-left-radius: 0.25rem !important; +} + +.rounded-lg { + border-radius: 0.3rem !important; +} + +.rounded-circle { + border-radius: 50% !important; +} + +.rounded-pill { + border-radius: 50rem !important; +} + +.rounded-0 { + border-radius: 0 !important; +} + +.clearfix::after { + display: block; + clear: both; + content: ""; +} + +.d-none { + display: none !important; +} + +.d-inline { + display: inline !important; +} + +.d-inline-block { + display: inline-block !important; +} + +.d-block { + display: block !important; +} + +.d-table { + display: table !important; +} + +.d-table-row { + display: table-row !important; +} + +.d-table-cell { + display: table-cell !important; +} + +.d-flex { + display: flex !important; +} + +.d-inline-flex { + display: inline-flex !important; +} + +@media (min-width: 576px) { + .d-sm-none { + display: none !important; + } + + .d-sm-inline { + display: inline !important; + } + + .d-sm-inline-block { + display: inline-block !important; + } + + .d-sm-block { + display: block !important; + } + + .d-sm-table { + display: table !important; + } + + .d-sm-table-row { + display: table-row !important; + } + + .d-sm-table-cell { + display: table-cell !important; + } + + .d-sm-flex { + display: flex !important; + } + + .d-sm-inline-flex { + display: inline-flex !important; + } +} +@media (min-width: 768px) { + .d-md-none { + display: none !important; + } + + .d-md-inline { + display: inline !important; + } + + .d-md-inline-block { + display: inline-block !important; + } + + .d-md-block { + display: block !important; + } + + .d-md-table { + display: table !important; + } + + .d-md-table-row { + display: table-row !important; + } + + .d-md-table-cell { + display: table-cell !important; + } + + .d-md-flex { + display: flex !important; + } + + .d-md-inline-flex { + display: inline-flex !important; + } +} +@media (min-width: 992px) { + .d-lg-none { + display: none !important; + } + + .d-lg-inline { + display: inline !important; + } + + .d-lg-inline-block { + display: inline-block !important; + } + + .d-lg-block { + display: block !important; + } + + .d-lg-table { + display: table !important; + } + + .d-lg-table-row { + display: table-row !important; + } + + .d-lg-table-cell { + display: table-cell !important; + } + + .d-lg-flex { + display: flex !important; + } + + .d-lg-inline-flex { + display: inline-flex !important; + } +} +@media (min-width: 1200px) { + .d-xl-none { + display: none !important; + } + + .d-xl-inline { + display: inline !important; + } + + .d-xl-inline-block { + display: inline-block !important; + } + + .d-xl-block { + display: block !important; + } + + .d-xl-table { + display: table !important; + } + + .d-xl-table-row { + display: table-row !important; + } + + .d-xl-table-cell { + display: table-cell !important; + } + + .d-xl-flex { + display: flex !important; + } + + .d-xl-inline-flex { + display: inline-flex !important; + } +} +@media print { + .d-print-none { + display: none !important; + } + + .d-print-inline { + display: inline !important; + } + + .d-print-inline-block { + display: inline-block !important; + } + + .d-print-block { + display: block !important; + } + + .d-print-table { + display: table !important; + } + + .d-print-table-row { + display: table-row !important; + } + + .d-print-table-cell { + display: table-cell !important; + } + + .d-print-flex { + display: flex !important; + } + + .d-print-inline-flex { + display: inline-flex !important; + } +} +.embed-responsive { + position: relative; + display: block; + width: 100%; + padding: 0; + overflow: hidden; +} +.embed-responsive::before { + display: block; + content: ""; +} +.embed-responsive .embed-responsive-item, +.embed-responsive iframe, +.embed-responsive embed, +.embed-responsive object, +.embed-responsive video { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + border: 0; +} + +.embed-responsive-21by9::before { + padding-top: 42.85714286%; +} + +.embed-responsive-16by9::before { + padding-top: 56.25%; +} + +.embed-responsive-4by3::before { + padding-top: 75%; +} + +.embed-responsive-1by1::before { + padding-top: 100%; +} + +.flex-row { + flex-direction: row !important; +} + +.flex-column { + flex-direction: column !important; +} + +.flex-row-reverse { + flex-direction: row-reverse !important; +} + +.flex-column-reverse { + flex-direction: column-reverse !important; +} + +.flex-wrap { + flex-wrap: wrap !important; +} + +.flex-nowrap { + flex-wrap: nowrap !important; +} + +.flex-wrap-reverse { + flex-wrap: wrap-reverse !important; +} + +.flex-fill { + flex: 1 1 auto !important; +} + +.flex-grow-0 { + flex-grow: 0 !important; +} + +.flex-grow-1 { + flex-grow: 1 !important; +} + +.flex-shrink-0 { + flex-shrink: 0 !important; +} + +.flex-shrink-1 { + flex-shrink: 1 !important; +} + +.justify-content-start { + justify-content: flex-start !important; +} + +.justify-content-end { + justify-content: flex-end !important; +} + +.justify-content-center { + justify-content: center !important; +} + +.justify-content-between { + justify-content: space-between !important; +} + +.justify-content-around { + justify-content: space-around !important; +} + +.align-items-start { + align-items: flex-start !important; +} + +.align-items-end { + align-items: flex-end !important; +} + +.align-items-center { + align-items: center !important; +} + +.align-items-baseline { + align-items: baseline !important; +} + +.align-items-stretch { + align-items: stretch !important; +} + +.align-content-start { + align-content: flex-start !important; +} + +.align-content-end { + align-content: flex-end !important; +} + +.align-content-center { + align-content: center !important; +} + +.align-content-between { + align-content: space-between !important; +} + +.align-content-around { + align-content: space-around !important; +} + +.align-content-stretch { + align-content: stretch !important; +} + +.align-self-auto { + align-self: auto !important; +} + +.align-self-start { + align-self: flex-start !important; +} + +.align-self-end { + align-self: flex-end !important; +} + +.align-self-center { + align-self: center !important; +} + +.align-self-baseline { + align-self: baseline !important; +} + +.align-self-stretch { + align-self: stretch !important; +} + +@media (min-width: 576px) { + .flex-sm-row { + flex-direction: row !important; + } + + .flex-sm-column { + flex-direction: column !important; + } + + .flex-sm-row-reverse { + flex-direction: row-reverse !important; + } + + .flex-sm-column-reverse { + flex-direction: column-reverse !important; + } + + .flex-sm-wrap { + flex-wrap: wrap !important; + } + + .flex-sm-nowrap { + flex-wrap: nowrap !important; + } + + .flex-sm-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + + .flex-sm-fill { + flex: 1 1 auto !important; + } + + .flex-sm-grow-0 { + flex-grow: 0 !important; + } + + .flex-sm-grow-1 { + flex-grow: 1 !important; + } + + .flex-sm-shrink-0 { + flex-shrink: 0 !important; + } + + .flex-sm-shrink-1 { + flex-shrink: 1 !important; + } + + .justify-content-sm-start { + justify-content: flex-start !important; + } + + .justify-content-sm-end { + justify-content: flex-end !important; + } + + .justify-content-sm-center { + justify-content: center !important; + } + + .justify-content-sm-between { + justify-content: space-between !important; + } + + .justify-content-sm-around { + justify-content: space-around !important; + } + + .align-items-sm-start { + align-items: flex-start !important; + } + + .align-items-sm-end { + align-items: flex-end !important; + } + + .align-items-sm-center { + align-items: center !important; + } + + .align-items-sm-baseline { + align-items: baseline !important; + } + + .align-items-sm-stretch { + align-items: stretch !important; + } + + .align-content-sm-start { + align-content: flex-start !important; + } + + .align-content-sm-end { + align-content: flex-end !important; + } + + .align-content-sm-center { + align-content: center !important; + } + + .align-content-sm-between { + align-content: space-between !important; + } + + .align-content-sm-around { + align-content: space-around !important; + } + + .align-content-sm-stretch { + align-content: stretch !important; + } + + .align-self-sm-auto { + align-self: auto !important; + } + + .align-self-sm-start { + align-self: flex-start !important; + } + + .align-self-sm-end { + align-self: flex-end !important; + } + + .align-self-sm-center { + align-self: center !important; + } + + .align-self-sm-baseline { + align-self: baseline !important; + } + + .align-self-sm-stretch { + align-self: stretch !important; + } +} +@media (min-width: 768px) { + .flex-md-row { + flex-direction: row !important; + } + + .flex-md-column { + flex-direction: column !important; + } + + .flex-md-row-reverse { + flex-direction: row-reverse !important; + } + + .flex-md-column-reverse { + flex-direction: column-reverse !important; + } + + .flex-md-wrap { + flex-wrap: wrap !important; + } + + .flex-md-nowrap { + flex-wrap: nowrap !important; + } + + .flex-md-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + + .flex-md-fill { + flex: 1 1 auto !important; + } + + .flex-md-grow-0 { + flex-grow: 0 !important; + } + + .flex-md-grow-1 { + flex-grow: 1 !important; + } + + .flex-md-shrink-0 { + flex-shrink: 0 !important; + } + + .flex-md-shrink-1 { + flex-shrink: 1 !important; + } + + .justify-content-md-start { + justify-content: flex-start !important; + } + + .justify-content-md-end { + justify-content: flex-end !important; + } + + .justify-content-md-center { + justify-content: center !important; + } + + .justify-content-md-between { + justify-content: space-between !important; + } + + .justify-content-md-around { + justify-content: space-around !important; + } + + .align-items-md-start { + align-items: flex-start !important; + } + + .align-items-md-end { + align-items: flex-end !important; + } + + .align-items-md-center { + align-items: center !important; + } + + .align-items-md-baseline { + align-items: baseline !important; + } + + .align-items-md-stretch { + align-items: stretch !important; + } + + .align-content-md-start { + align-content: flex-start !important; + } + + .align-content-md-end { + align-content: flex-end !important; + } + + .align-content-md-center { + align-content: center !important; + } + + .align-content-md-between { + align-content: space-between !important; + } + + .align-content-md-around { + align-content: space-around !important; + } + + .align-content-md-stretch { + align-content: stretch !important; + } + + .align-self-md-auto { + align-self: auto !important; + } + + .align-self-md-start { + align-self: flex-start !important; + } + + .align-self-md-end { + align-self: flex-end !important; + } + + .align-self-md-center { + align-self: center !important; + } + + .align-self-md-baseline { + align-self: baseline !important; + } + + .align-self-md-stretch { + align-self: stretch !important; + } +} +@media (min-width: 992px) { + .flex-lg-row { + flex-direction: row !important; + } + + .flex-lg-column { + flex-direction: column !important; + } + + .flex-lg-row-reverse { + flex-direction: row-reverse !important; + } + + .flex-lg-column-reverse { + flex-direction: column-reverse !important; + } + + .flex-lg-wrap { + flex-wrap: wrap !important; + } + + .flex-lg-nowrap { + flex-wrap: nowrap !important; + } + + .flex-lg-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + + .flex-lg-fill { + flex: 1 1 auto !important; + } + + .flex-lg-grow-0 { + flex-grow: 0 !important; + } + + .flex-lg-grow-1 { + flex-grow: 1 !important; + } + + .flex-lg-shrink-0 { + flex-shrink: 0 !important; + } + + .flex-lg-shrink-1 { + flex-shrink: 1 !important; + } + + .justify-content-lg-start { + justify-content: flex-start !important; + } + + .justify-content-lg-end { + justify-content: flex-end !important; + } + + .justify-content-lg-center { + justify-content: center !important; + } + + .justify-content-lg-between { + justify-content: space-between !important; + } + + .justify-content-lg-around { + justify-content: space-around !important; + } + + .align-items-lg-start { + align-items: flex-start !important; + } + + .align-items-lg-end { + align-items: flex-end !important; + } + + .align-items-lg-center { + align-items: center !important; + } + + .align-items-lg-baseline { + align-items: baseline !important; + } + + .align-items-lg-stretch { + align-items: stretch !important; + } + + .align-content-lg-start { + align-content: flex-start !important; + } + + .align-content-lg-end { + align-content: flex-end !important; + } + + .align-content-lg-center { + align-content: center !important; + } + + .align-content-lg-between { + align-content: space-between !important; + } + + .align-content-lg-around { + align-content: space-around !important; + } + + .align-content-lg-stretch { + align-content: stretch !important; + } + + .align-self-lg-auto { + align-self: auto !important; + } + + .align-self-lg-start { + align-self: flex-start !important; + } + + .align-self-lg-end { + align-self: flex-end !important; + } + + .align-self-lg-center { + align-self: center !important; + } + + .align-self-lg-baseline { + align-self: baseline !important; + } + + .align-self-lg-stretch { + align-self: stretch !important; + } +} +@media (min-width: 1200px) { + .flex-xl-row { + flex-direction: row !important; + } + + .flex-xl-column { + flex-direction: column !important; + } + + .flex-xl-row-reverse { + flex-direction: row-reverse !important; + } + + .flex-xl-column-reverse { + flex-direction: column-reverse !important; + } + + .flex-xl-wrap { + flex-wrap: wrap !important; + } + + .flex-xl-nowrap { + flex-wrap: nowrap !important; + } + + .flex-xl-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + + .flex-xl-fill { + flex: 1 1 auto !important; + } + + .flex-xl-grow-0 { + flex-grow: 0 !important; + } + + .flex-xl-grow-1 { + flex-grow: 1 !important; + } + + .flex-xl-shrink-0 { + flex-shrink: 0 !important; + } + + .flex-xl-shrink-1 { + flex-shrink: 1 !important; + } + + .justify-content-xl-start { + justify-content: flex-start !important; + } + + .justify-content-xl-end { + justify-content: flex-end !important; + } + + .justify-content-xl-center { + justify-content: center !important; + } + + .justify-content-xl-between { + justify-content: space-between !important; + } + + .justify-content-xl-around { + justify-content: space-around !important; + } + + .align-items-xl-start { + align-items: flex-start !important; + } + + .align-items-xl-end { + align-items: flex-end !important; + } + + .align-items-xl-center { + align-items: center !important; + } + + .align-items-xl-baseline { + align-items: baseline !important; + } + + .align-items-xl-stretch { + align-items: stretch !important; + } + + .align-content-xl-start { + align-content: flex-start !important; + } + + .align-content-xl-end { + align-content: flex-end !important; + } + + .align-content-xl-center { + align-content: center !important; + } + + .align-content-xl-between { + align-content: space-between !important; + } + + .align-content-xl-around { + align-content: space-around !important; + } + + .align-content-xl-stretch { + align-content: stretch !important; + } + + .align-self-xl-auto { + align-self: auto !important; + } + + .align-self-xl-start { + align-self: flex-start !important; + } + + .align-self-xl-end { + align-self: flex-end !important; + } + + .align-self-xl-center { + align-self: center !important; + } + + .align-self-xl-baseline { + align-self: baseline !important; + } + + .align-self-xl-stretch { + align-self: stretch !important; + } +} +.float-left { + float: left !important; +} + +.float-right { + float: right !important; +} + +.float-none { + float: none !important; +} + +@media (min-width: 576px) { + .float-sm-left { + float: left !important; + } + + .float-sm-right { + float: right !important; + } + + .float-sm-none { + float: none !important; + } +} +@media (min-width: 768px) { + .float-md-left { + float: left !important; + } + + .float-md-right { + float: right !important; + } + + .float-md-none { + float: none !important; + } +} +@media (min-width: 992px) { + .float-lg-left { + float: left !important; + } + + .float-lg-right { + float: right !important; + } + + .float-lg-none { + float: none !important; + } +} +@media (min-width: 1200px) { + .float-xl-left { + float: left !important; + } + + .float-xl-right { + float: right !important; + } + + .float-xl-none { + float: none !important; + } +} +.user-select-all { + user-select: all !important; +} + +.user-select-auto { + user-select: auto !important; +} + +.user-select-none { + user-select: none !important; +} + +.overflow-auto { + overflow: auto !important; +} + +.overflow-hidden { + overflow: hidden !important; +} + +.position-static { + position: static !important; +} + +.position-relative { + position: relative !important; +} + +.position-absolute { + position: absolute !important; +} + +.position-fixed { + position: fixed !important; +} + +.position-sticky { + position: sticky !important; +} + +.fixed-top { + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 1030; +} + +.fixed-bottom { + position: fixed; + right: 0; + bottom: 0; + left: 0; + z-index: 1030; +} + +@supports (position: sticky) { + .sticky-top { + position: sticky; + top: 0; + z-index: 1020; + } +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.sr-only-focusable:active, .sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + overflow: visible; + clip: auto; + white-space: normal; +} + +.shadow-sm { + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; +} + +.shadow { + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; +} + +.shadow-lg { + box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important; +} + +.shadow-none { + box-shadow: none !important; +} + +.w-25 { + width: 25% !important; +} + +.w-50 { + width: 50% !important; +} + +.w-75 { + width: 75% !important; +} + +.w-100 { + width: 100% !important; +} + +.w-auto { + width: auto !important; +} + +.h-25 { + height: 25% !important; +} + +.h-50 { + height: 50% !important; +} + +.h-75 { + height: 75% !important; +} + +.h-100 { + height: 100% !important; +} + +.h-auto { + height: auto !important; +} + +.mw-100 { + max-width: 100% !important; +} + +.mh-100 { + max-height: 100% !important; +} + +.min-vw-100 { + min-width: 100vw !important; +} + +.min-vh-100 { + min-height: 100vh !important; +} + +.vw-100 { + width: 100vw !important; +} + +.vh-100 { + height: 100vh !important; +} + +.m-0 { + margin: 0 !important; +} + +.mt-0, +.my-0 { + margin-top: 0 !important; +} + +.mr-0, +.mx-0 { + margin-right: 0 !important; +} + +.mb-0, +.my-0 { + margin-bottom: 0 !important; +} + +.ml-0, +.mx-0 { + margin-left: 0 !important; +} + +.m-1 { + margin: 0.25rem !important; +} + +.mt-1, +.my-1 { + margin-top: 0.25rem !important; +} + +.mr-1, +.mx-1 { + margin-right: 0.25rem !important; +} + +.mb-1, +.my-1 { + margin-bottom: 0.25rem !important; +} + +.ml-1, +.mx-1 { + margin-left: 0.25rem !important; +} + +.m-2 { + margin: 0.5rem !important; +} + +.mt-2, +.my-2 { + margin-top: 0.5rem !important; +} + +.mr-2, +.mx-2 { + margin-right: 0.5rem !important; +} + +.mb-2, +.my-2 { + margin-bottom: 0.5rem !important; +} + +.ml-2, +.mx-2 { + margin-left: 0.5rem !important; +} + +.m-3 { + margin: 1rem !important; +} + +.mt-3, +.my-3 { + margin-top: 1rem !important; +} + +.mr-3, +.mx-3 { + margin-right: 1rem !important; +} + +.mb-3, +.my-3 { + margin-bottom: 1rem !important; +} + +.ml-3, +.mx-3 { + margin-left: 1rem !important; +} + +.m-4 { + margin: 1.5rem !important; +} + +.mt-4, +.my-4 { + margin-top: 1.5rem !important; +} + +.mr-4, +.mx-4 { + margin-right: 1.5rem !important; +} + +.mb-4, +.my-4 { + margin-bottom: 1.5rem !important; +} + +.ml-4, +.mx-4 { + margin-left: 1.5rem !important; +} + +.m-5 { + margin: 3rem !important; +} + +.mt-5, +.my-5 { + margin-top: 3rem !important; +} + +.mr-5, +.mx-5 { + margin-right: 3rem !important; +} + +.mb-5, +.my-5 { + margin-bottom: 3rem !important; +} + +.ml-5, +.mx-5 { + margin-left: 3rem !important; +} + +.p-0 { + padding: 0 !important; +} + +.pt-0, +.py-0 { + padding-top: 0 !important; +} + +.pr-0, +.px-0 { + padding-right: 0 !important; +} + +.pb-0, +.py-0 { + padding-bottom: 0 !important; +} + +.pl-0, +.px-0 { + padding-left: 0 !important; +} + +.p-1 { + padding: 0.25rem !important; +} + +.pt-1, +.py-1 { + padding-top: 0.25rem !important; +} + +.pr-1, +.px-1 { + padding-right: 0.25rem !important; +} + +.pb-1, +.py-1 { + padding-bottom: 0.25rem !important; +} + +.pl-1, +.px-1 { + padding-left: 0.25rem !important; +} + +.p-2 { + padding: 0.5rem !important; +} + +.pt-2, +.py-2 { + padding-top: 0.5rem !important; +} + +.pr-2, +.px-2 { + padding-right: 0.5rem !important; +} + +.pb-2, +.py-2 { + padding-bottom: 0.5rem !important; +} + +.pl-2, +.px-2 { + padding-left: 0.5rem !important; +} + +.p-3 { + padding: 1rem !important; +} + +.pt-3, +.py-3 { + padding-top: 1rem !important; +} + +.pr-3, +.px-3 { + padding-right: 1rem !important; +} + +.pb-3, +.py-3 { + padding-bottom: 1rem !important; +} + +.pl-3, +.px-3 { + padding-left: 1rem !important; +} + +.p-4 { + padding: 1.5rem !important; +} + +.pt-4, +.py-4 { + padding-top: 1.5rem !important; +} + +.pr-4, +.px-4 { + padding-right: 1.5rem !important; +} + +.pb-4, +.py-4 { + padding-bottom: 1.5rem !important; +} + +.pl-4, +.px-4 { + padding-left: 1.5rem !important; +} + +.p-5 { + padding: 3rem !important; +} + +.pt-5, +.py-5 { + padding-top: 3rem !important; +} + +.pr-5, +.px-5 { + padding-right: 3rem !important; +} + +.pb-5, +.py-5 { + padding-bottom: 3rem !important; +} + +.pl-5, +.px-5 { + padding-left: 3rem !important; +} + +.m-n1 { + margin: -0.25rem !important; +} + +.mt-n1, +.my-n1 { + margin-top: -0.25rem !important; +} + +.mr-n1, +.mx-n1 { + margin-right: -0.25rem !important; +} + +.mb-n1, +.my-n1 { + margin-bottom: -0.25rem !important; +} + +.ml-n1, +.mx-n1 { + margin-left: -0.25rem !important; +} + +.m-n2 { + margin: -0.5rem !important; +} + +.mt-n2, +.my-n2 { + margin-top: -0.5rem !important; +} + +.mr-n2, +.mx-n2 { + margin-right: -0.5rem !important; +} + +.mb-n2, +.my-n2 { + margin-bottom: -0.5rem !important; +} + +.ml-n2, +.mx-n2 { + margin-left: -0.5rem !important; +} + +.m-n3 { + margin: -1rem !important; +} + +.mt-n3, +.my-n3 { + margin-top: -1rem !important; +} + +.mr-n3, +.mx-n3 { + margin-right: -1rem !important; +} + +.mb-n3, +.my-n3 { + margin-bottom: -1rem !important; +} + +.ml-n3, +.mx-n3 { + margin-left: -1rem !important; +} + +.m-n4 { + margin: -1.5rem !important; +} + +.mt-n4, +.my-n4 { + margin-top: -1.5rem !important; +} + +.mr-n4, +.mx-n4 { + margin-right: -1.5rem !important; +} + +.mb-n4, +.my-n4 { + margin-bottom: -1.5rem !important; +} + +.ml-n4, +.mx-n4 { + margin-left: -1.5rem !important; +} + +.m-n5 { + margin: -3rem !important; +} + +.mt-n5, +.my-n5 { + margin-top: -3rem !important; +} + +.mr-n5, +.mx-n5 { + margin-right: -3rem !important; +} + +.mb-n5, +.my-n5 { + margin-bottom: -3rem !important; +} + +.ml-n5, +.mx-n5 { + margin-left: -3rem !important; +} + +.m-auto { + margin: auto !important; +} + +.mt-auto, +.my-auto { + margin-top: auto !important; +} + +.mr-auto, +.mx-auto { + margin-right: auto !important; +} + +.mb-auto, +.my-auto { + margin-bottom: auto !important; +} + +.ml-auto, +.mx-auto { + margin-left: auto !important; +} + +@media (min-width: 576px) { + .m-sm-0 { + margin: 0 !important; + } + + .mt-sm-0, +.my-sm-0 { + margin-top: 0 !important; + } + + .mr-sm-0, +.mx-sm-0 { + margin-right: 0 !important; + } + + .mb-sm-0, +.my-sm-0 { + margin-bottom: 0 !important; + } + + .ml-sm-0, +.mx-sm-0 { + margin-left: 0 !important; + } + + .m-sm-1 { + margin: 0.25rem !important; + } + + .mt-sm-1, +.my-sm-1 { + margin-top: 0.25rem !important; + } + + .mr-sm-1, +.mx-sm-1 { + margin-right: 0.25rem !important; + } + + .mb-sm-1, +.my-sm-1 { + margin-bottom: 0.25rem !important; + } + + .ml-sm-1, +.mx-sm-1 { + margin-left: 0.25rem !important; + } + + .m-sm-2 { + margin: 0.5rem !important; + } + + .mt-sm-2, +.my-sm-2 { + margin-top: 0.5rem !important; + } + + .mr-sm-2, +.mx-sm-2 { + margin-right: 0.5rem !important; + } + + .mb-sm-2, +.my-sm-2 { + margin-bottom: 0.5rem !important; + } + + .ml-sm-2, +.mx-sm-2 { + margin-left: 0.5rem !important; + } + + .m-sm-3 { + margin: 1rem !important; + } + + .mt-sm-3, +.my-sm-3 { + margin-top: 1rem !important; + } + + .mr-sm-3, +.mx-sm-3 { + margin-right: 1rem !important; + } + + .mb-sm-3, +.my-sm-3 { + margin-bottom: 1rem !important; + } + + .ml-sm-3, +.mx-sm-3 { + margin-left: 1rem !important; + } + + .m-sm-4 { + margin: 1.5rem !important; + } + + .mt-sm-4, +.my-sm-4 { + margin-top: 1.5rem !important; + } + + .mr-sm-4, +.mx-sm-4 { + margin-right: 1.5rem !important; + } + + .mb-sm-4, +.my-sm-4 { + margin-bottom: 1.5rem !important; + } + + .ml-sm-4, +.mx-sm-4 { + margin-left: 1.5rem !important; + } + + .m-sm-5 { + margin: 3rem !important; + } + + .mt-sm-5, +.my-sm-5 { + margin-top: 3rem !important; + } + + .mr-sm-5, +.mx-sm-5 { + margin-right: 3rem !important; + } + + .mb-sm-5, +.my-sm-5 { + margin-bottom: 3rem !important; + } + + .ml-sm-5, +.mx-sm-5 { + margin-left: 3rem !important; + } + + .p-sm-0 { + padding: 0 !important; + } + + .pt-sm-0, +.py-sm-0 { + padding-top: 0 !important; + } + + .pr-sm-0, +.px-sm-0 { + padding-right: 0 !important; + } + + .pb-sm-0, +.py-sm-0 { + padding-bottom: 0 !important; + } + + .pl-sm-0, +.px-sm-0 { + padding-left: 0 !important; + } + + .p-sm-1 { + padding: 0.25rem !important; + } + + .pt-sm-1, +.py-sm-1 { + padding-top: 0.25rem !important; + } + + .pr-sm-1, +.px-sm-1 { + padding-right: 0.25rem !important; + } + + .pb-sm-1, +.py-sm-1 { + padding-bottom: 0.25rem !important; + } + + .pl-sm-1, +.px-sm-1 { + padding-left: 0.25rem !important; + } + + .p-sm-2 { + padding: 0.5rem !important; + } + + .pt-sm-2, +.py-sm-2 { + padding-top: 0.5rem !important; + } + + .pr-sm-2, +.px-sm-2 { + padding-right: 0.5rem !important; + } + + .pb-sm-2, +.py-sm-2 { + padding-bottom: 0.5rem !important; + } + + .pl-sm-2, +.px-sm-2 { + padding-left: 0.5rem !important; + } + + .p-sm-3 { + padding: 1rem !important; + } + + .pt-sm-3, +.py-sm-3 { + padding-top: 1rem !important; + } + + .pr-sm-3, +.px-sm-3 { + padding-right: 1rem !important; + } + + .pb-sm-3, +.py-sm-3 { + padding-bottom: 1rem !important; + } + + .pl-sm-3, +.px-sm-3 { + padding-left: 1rem !important; + } + + .p-sm-4 { + padding: 1.5rem !important; + } + + .pt-sm-4, +.py-sm-4 { + padding-top: 1.5rem !important; + } + + .pr-sm-4, +.px-sm-4 { + padding-right: 1.5rem !important; + } + + .pb-sm-4, +.py-sm-4 { + padding-bottom: 1.5rem !important; + } + + .pl-sm-4, +.px-sm-4 { + padding-left: 1.5rem !important; + } + + .p-sm-5 { + padding: 3rem !important; + } + + .pt-sm-5, +.py-sm-5 { + padding-top: 3rem !important; + } + + .pr-sm-5, +.px-sm-5 { + padding-right: 3rem !important; + } + + .pb-sm-5, +.py-sm-5 { + padding-bottom: 3rem !important; + } + + .pl-sm-5, +.px-sm-5 { + padding-left: 3rem !important; + } + + .m-sm-n1 { + margin: -0.25rem !important; + } + + .mt-sm-n1, +.my-sm-n1 { + margin-top: -0.25rem !important; + } + + .mr-sm-n1, +.mx-sm-n1 { + margin-right: -0.25rem !important; + } + + .mb-sm-n1, +.my-sm-n1 { + margin-bottom: -0.25rem !important; + } + + .ml-sm-n1, +.mx-sm-n1 { + margin-left: -0.25rem !important; + } + + .m-sm-n2 { + margin: -0.5rem !important; + } + + .mt-sm-n2, +.my-sm-n2 { + margin-top: -0.5rem !important; + } + + .mr-sm-n2, +.mx-sm-n2 { + margin-right: -0.5rem !important; + } + + .mb-sm-n2, +.my-sm-n2 { + margin-bottom: -0.5rem !important; + } + + .ml-sm-n2, +.mx-sm-n2 { + margin-left: -0.5rem !important; + } + + .m-sm-n3 { + margin: -1rem !important; + } + + .mt-sm-n3, +.my-sm-n3 { + margin-top: -1rem !important; + } + + .mr-sm-n3, +.mx-sm-n3 { + margin-right: -1rem !important; + } + + .mb-sm-n3, +.my-sm-n3 { + margin-bottom: -1rem !important; + } + + .ml-sm-n3, +.mx-sm-n3 { + margin-left: -1rem !important; + } + + .m-sm-n4 { + margin: -1.5rem !important; + } + + .mt-sm-n4, +.my-sm-n4 { + margin-top: -1.5rem !important; + } + + .mr-sm-n4, +.mx-sm-n4 { + margin-right: -1.5rem !important; + } + + .mb-sm-n4, +.my-sm-n4 { + margin-bottom: -1.5rem !important; + } + + .ml-sm-n4, +.mx-sm-n4 { + margin-left: -1.5rem !important; + } + + .m-sm-n5 { + margin: -3rem !important; + } + + .mt-sm-n5, +.my-sm-n5 { + margin-top: -3rem !important; + } + + .mr-sm-n5, +.mx-sm-n5 { + margin-right: -3rem !important; + } + + .mb-sm-n5, +.my-sm-n5 { + margin-bottom: -3rem !important; + } + + .ml-sm-n5, +.mx-sm-n5 { + margin-left: -3rem !important; + } + + .m-sm-auto { + margin: auto !important; + } + + .mt-sm-auto, +.my-sm-auto { + margin-top: auto !important; + } + + .mr-sm-auto, +.mx-sm-auto { + margin-right: auto !important; + } + + .mb-sm-auto, +.my-sm-auto { + margin-bottom: auto !important; + } + + .ml-sm-auto, +.mx-sm-auto { + margin-left: auto !important; + } +} +@media (min-width: 768px) { + .m-md-0 { + margin: 0 !important; + } + + .mt-md-0, +.my-md-0 { + margin-top: 0 !important; + } + + .mr-md-0, +.mx-md-0 { + margin-right: 0 !important; + } + + .mb-md-0, +.my-md-0 { + margin-bottom: 0 !important; + } + + .ml-md-0, +.mx-md-0 { + margin-left: 0 !important; + } + + .m-md-1 { + margin: 0.25rem !important; + } + + .mt-md-1, +.my-md-1 { + margin-top: 0.25rem !important; + } + + .mr-md-1, +.mx-md-1 { + margin-right: 0.25rem !important; + } + + .mb-md-1, +.my-md-1 { + margin-bottom: 0.25rem !important; + } + + .ml-md-1, +.mx-md-1 { + margin-left: 0.25rem !important; + } + + .m-md-2 { + margin: 0.5rem !important; + } + + .mt-md-2, +.my-md-2 { + margin-top: 0.5rem !important; + } + + .mr-md-2, +.mx-md-2 { + margin-right: 0.5rem !important; + } + + .mb-md-2, +.my-md-2 { + margin-bottom: 0.5rem !important; + } + + .ml-md-2, +.mx-md-2 { + margin-left: 0.5rem !important; + } + + .m-md-3 { + margin: 1rem !important; + } + + .mt-md-3, +.my-md-3 { + margin-top: 1rem !important; + } + + .mr-md-3, +.mx-md-3 { + margin-right: 1rem !important; + } + + .mb-md-3, +.my-md-3 { + margin-bottom: 1rem !important; + } + + .ml-md-3, +.mx-md-3 { + margin-left: 1rem !important; + } + + .m-md-4 { + margin: 1.5rem !important; + } + + .mt-md-4, +.my-md-4 { + margin-top: 1.5rem !important; + } + + .mr-md-4, +.mx-md-4 { + margin-right: 1.5rem !important; + } + + .mb-md-4, +.my-md-4 { + margin-bottom: 1.5rem !important; + } + + .ml-md-4, +.mx-md-4 { + margin-left: 1.5rem !important; + } + + .m-md-5 { + margin: 3rem !important; + } + + .mt-md-5, +.my-md-5 { + margin-top: 3rem !important; + } + + .mr-md-5, +.mx-md-5 { + margin-right: 3rem !important; + } + + .mb-md-5, +.my-md-5 { + margin-bottom: 3rem !important; + } + + .ml-md-5, +.mx-md-5 { + margin-left: 3rem !important; + } + + .p-md-0 { + padding: 0 !important; + } + + .pt-md-0, +.py-md-0 { + padding-top: 0 !important; + } + + .pr-md-0, +.px-md-0 { + padding-right: 0 !important; + } + + .pb-md-0, +.py-md-0 { + padding-bottom: 0 !important; + } + + .pl-md-0, +.px-md-0 { + padding-left: 0 !important; + } + + .p-md-1 { + padding: 0.25rem !important; + } + + .pt-md-1, +.py-md-1 { + padding-top: 0.25rem !important; + } + + .pr-md-1, +.px-md-1 { + padding-right: 0.25rem !important; + } + + .pb-md-1, +.py-md-1 { + padding-bottom: 0.25rem !important; + } + + .pl-md-1, +.px-md-1 { + padding-left: 0.25rem !important; + } + + .p-md-2 { + padding: 0.5rem !important; + } + + .pt-md-2, +.py-md-2 { + padding-top: 0.5rem !important; + } + + .pr-md-2, +.px-md-2 { + padding-right: 0.5rem !important; + } + + .pb-md-2, +.py-md-2 { + padding-bottom: 0.5rem !important; + } + + .pl-md-2, +.px-md-2 { + padding-left: 0.5rem !important; + } + + .p-md-3 { + padding: 1rem !important; + } + + .pt-md-3, +.py-md-3 { + padding-top: 1rem !important; + } + + .pr-md-3, +.px-md-3 { + padding-right: 1rem !important; + } + + .pb-md-3, +.py-md-3 { + padding-bottom: 1rem !important; + } + + .pl-md-3, +.px-md-3 { + padding-left: 1rem !important; + } + + .p-md-4 { + padding: 1.5rem !important; + } + + .pt-md-4, +.py-md-4 { + padding-top: 1.5rem !important; + } + + .pr-md-4, +.px-md-4 { + padding-right: 1.5rem !important; + } + + .pb-md-4, +.py-md-4 { + padding-bottom: 1.5rem !important; + } + + .pl-md-4, +.px-md-4 { + padding-left: 1.5rem !important; + } + + .p-md-5 { + padding: 3rem !important; + } + + .pt-md-5, +.py-md-5 { + padding-top: 3rem !important; + } + + .pr-md-5, +.px-md-5 { + padding-right: 3rem !important; + } + + .pb-md-5, +.py-md-5 { + padding-bottom: 3rem !important; + } + + .pl-md-5, +.px-md-5 { + padding-left: 3rem !important; + } + + .m-md-n1 { + margin: -0.25rem !important; + } + + .mt-md-n1, +.my-md-n1 { + margin-top: -0.25rem !important; + } + + .mr-md-n1, +.mx-md-n1 { + margin-right: -0.25rem !important; + } + + .mb-md-n1, +.my-md-n1 { + margin-bottom: -0.25rem !important; + } + + .ml-md-n1, +.mx-md-n1 { + margin-left: -0.25rem !important; + } + + .m-md-n2 { + margin: -0.5rem !important; + } + + .mt-md-n2, +.my-md-n2 { + margin-top: -0.5rem !important; + } + + .mr-md-n2, +.mx-md-n2 { + margin-right: -0.5rem !important; + } + + .mb-md-n2, +.my-md-n2 { + margin-bottom: -0.5rem !important; + } + + .ml-md-n2, +.mx-md-n2 { + margin-left: -0.5rem !important; + } + + .m-md-n3 { + margin: -1rem !important; + } + + .mt-md-n3, +.my-md-n3 { + margin-top: -1rem !important; + } + + .mr-md-n3, +.mx-md-n3 { + margin-right: -1rem !important; + } + + .mb-md-n3, +.my-md-n3 { + margin-bottom: -1rem !important; + } + + .ml-md-n3, +.mx-md-n3 { + margin-left: -1rem !important; + } + + .m-md-n4 { + margin: -1.5rem !important; + } + + .mt-md-n4, +.my-md-n4 { + margin-top: -1.5rem !important; + } + + .mr-md-n4, +.mx-md-n4 { + margin-right: -1.5rem !important; + } + + .mb-md-n4, +.my-md-n4 { + margin-bottom: -1.5rem !important; + } + + .ml-md-n4, +.mx-md-n4 { + margin-left: -1.5rem !important; + } + + .m-md-n5 { + margin: -3rem !important; + } + + .mt-md-n5, +.my-md-n5 { + margin-top: -3rem !important; + } + + .mr-md-n5, +.mx-md-n5 { + margin-right: -3rem !important; + } + + .mb-md-n5, +.my-md-n5 { + margin-bottom: -3rem !important; + } + + .ml-md-n5, +.mx-md-n5 { + margin-left: -3rem !important; + } + + .m-md-auto { + margin: auto !important; + } + + .mt-md-auto, +.my-md-auto { + margin-top: auto !important; + } + + .mr-md-auto, +.mx-md-auto { + margin-right: auto !important; + } + + .mb-md-auto, +.my-md-auto { + margin-bottom: auto !important; + } + + .ml-md-auto, +.mx-md-auto { + margin-left: auto !important; + } +} +@media (min-width: 992px) { + .m-lg-0 { + margin: 0 !important; + } + + .mt-lg-0, +.my-lg-0 { + margin-top: 0 !important; + } + + .mr-lg-0, +.mx-lg-0 { + margin-right: 0 !important; + } + + .mb-lg-0, +.my-lg-0 { + margin-bottom: 0 !important; + } + + .ml-lg-0, +.mx-lg-0 { + margin-left: 0 !important; + } + + .m-lg-1 { + margin: 0.25rem !important; + } + + .mt-lg-1, +.my-lg-1 { + margin-top: 0.25rem !important; + } + + .mr-lg-1, +.mx-lg-1 { + margin-right: 0.25rem !important; + } + + .mb-lg-1, +.my-lg-1 { + margin-bottom: 0.25rem !important; + } + + .ml-lg-1, +.mx-lg-1 { + margin-left: 0.25rem !important; + } + + .m-lg-2 { + margin: 0.5rem !important; + } + + .mt-lg-2, +.my-lg-2 { + margin-top: 0.5rem !important; + } + + .mr-lg-2, +.mx-lg-2 { + margin-right: 0.5rem !important; + } + + .mb-lg-2, +.my-lg-2 { + margin-bottom: 0.5rem !important; + } + + .ml-lg-2, +.mx-lg-2 { + margin-left: 0.5rem !important; + } + + .m-lg-3 { + margin: 1rem !important; + } + + .mt-lg-3, +.my-lg-3 { + margin-top: 1rem !important; + } + + .mr-lg-3, +.mx-lg-3 { + margin-right: 1rem !important; + } + + .mb-lg-3, +.my-lg-3 { + margin-bottom: 1rem !important; + } + + .ml-lg-3, +.mx-lg-3 { + margin-left: 1rem !important; + } + + .m-lg-4 { + margin: 1.5rem !important; + } + + .mt-lg-4, +.my-lg-4 { + margin-top: 1.5rem !important; + } + + .mr-lg-4, +.mx-lg-4 { + margin-right: 1.5rem !important; + } + + .mb-lg-4, +.my-lg-4 { + margin-bottom: 1.5rem !important; + } + + .ml-lg-4, +.mx-lg-4 { + margin-left: 1.5rem !important; + } + + .m-lg-5 { + margin: 3rem !important; + } + + .mt-lg-5, +.my-lg-5 { + margin-top: 3rem !important; + } + + .mr-lg-5, +.mx-lg-5 { + margin-right: 3rem !important; + } + + .mb-lg-5, +.my-lg-5 { + margin-bottom: 3rem !important; + } + + .ml-lg-5, +.mx-lg-5 { + margin-left: 3rem !important; + } + + .p-lg-0 { + padding: 0 !important; + } + + .pt-lg-0, +.py-lg-0 { + padding-top: 0 !important; + } + + .pr-lg-0, +.px-lg-0 { + padding-right: 0 !important; + } + + .pb-lg-0, +.py-lg-0 { + padding-bottom: 0 !important; + } + + .pl-lg-0, +.px-lg-0 { + padding-left: 0 !important; + } + + .p-lg-1 { + padding: 0.25rem !important; + } + + .pt-lg-1, +.py-lg-1 { + padding-top: 0.25rem !important; + } + + .pr-lg-1, +.px-lg-1 { + padding-right: 0.25rem !important; + } + + .pb-lg-1, +.py-lg-1 { + padding-bottom: 0.25rem !important; + } + + .pl-lg-1, +.px-lg-1 { + padding-left: 0.25rem !important; + } + + .p-lg-2 { + padding: 0.5rem !important; + } + + .pt-lg-2, +.py-lg-2 { + padding-top: 0.5rem !important; + } + + .pr-lg-2, +.px-lg-2 { + padding-right: 0.5rem !important; + } + + .pb-lg-2, +.py-lg-2 { + padding-bottom: 0.5rem !important; + } + + .pl-lg-2, +.px-lg-2 { + padding-left: 0.5rem !important; + } + + .p-lg-3 { + padding: 1rem !important; + } + + .pt-lg-3, +.py-lg-3 { + padding-top: 1rem !important; + } + + .pr-lg-3, +.px-lg-3 { + padding-right: 1rem !important; + } + + .pb-lg-3, +.py-lg-3 { + padding-bottom: 1rem !important; + } + + .pl-lg-3, +.px-lg-3 { + padding-left: 1rem !important; + } + + .p-lg-4 { + padding: 1.5rem !important; + } + + .pt-lg-4, +.py-lg-4 { + padding-top: 1.5rem !important; + } + + .pr-lg-4, +.px-lg-4 { + padding-right: 1.5rem !important; + } + + .pb-lg-4, +.py-lg-4 { + padding-bottom: 1.5rem !important; + } + + .pl-lg-4, +.px-lg-4 { + padding-left: 1.5rem !important; + } + + .p-lg-5 { + padding: 3rem !important; + } + + .pt-lg-5, +.py-lg-5 { + padding-top: 3rem !important; + } + + .pr-lg-5, +.px-lg-5 { + padding-right: 3rem !important; + } + + .pb-lg-5, +.py-lg-5 { + padding-bottom: 3rem !important; + } + + .pl-lg-5, +.px-lg-5 { + padding-left: 3rem !important; + } + + .m-lg-n1 { + margin: -0.25rem !important; + } + + .mt-lg-n1, +.my-lg-n1 { + margin-top: -0.25rem !important; + } + + .mr-lg-n1, +.mx-lg-n1 { + margin-right: -0.25rem !important; + } + + .mb-lg-n1, +.my-lg-n1 { + margin-bottom: -0.25rem !important; + } + + .ml-lg-n1, +.mx-lg-n1 { + margin-left: -0.25rem !important; + } + + .m-lg-n2 { + margin: -0.5rem !important; + } + + .mt-lg-n2, +.my-lg-n2 { + margin-top: -0.5rem !important; + } + + .mr-lg-n2, +.mx-lg-n2 { + margin-right: -0.5rem !important; + } + + .mb-lg-n2, +.my-lg-n2 { + margin-bottom: -0.5rem !important; + } + + .ml-lg-n2, +.mx-lg-n2 { + margin-left: -0.5rem !important; + } + + .m-lg-n3 { + margin: -1rem !important; + } + + .mt-lg-n3, +.my-lg-n3 { + margin-top: -1rem !important; + } + + .mr-lg-n3, +.mx-lg-n3 { + margin-right: -1rem !important; + } + + .mb-lg-n3, +.my-lg-n3 { + margin-bottom: -1rem !important; + } + + .ml-lg-n3, +.mx-lg-n3 { + margin-left: -1rem !important; + } + + .m-lg-n4 { + margin: -1.5rem !important; + } + + .mt-lg-n4, +.my-lg-n4 { + margin-top: -1.5rem !important; + } + + .mr-lg-n4, +.mx-lg-n4 { + margin-right: -1.5rem !important; + } + + .mb-lg-n4, +.my-lg-n4 { + margin-bottom: -1.5rem !important; + } + + .ml-lg-n4, +.mx-lg-n4 { + margin-left: -1.5rem !important; + } + + .m-lg-n5 { + margin: -3rem !important; + } + + .mt-lg-n5, +.my-lg-n5 { + margin-top: -3rem !important; + } + + .mr-lg-n5, +.mx-lg-n5 { + margin-right: -3rem !important; + } + + .mb-lg-n5, +.my-lg-n5 { + margin-bottom: -3rem !important; + } + + .ml-lg-n5, +.mx-lg-n5 { + margin-left: -3rem !important; + } + + .m-lg-auto { + margin: auto !important; + } + + .mt-lg-auto, +.my-lg-auto { + margin-top: auto !important; + } + + .mr-lg-auto, +.mx-lg-auto { + margin-right: auto !important; + } + + .mb-lg-auto, +.my-lg-auto { + margin-bottom: auto !important; + } + + .ml-lg-auto, +.mx-lg-auto { + margin-left: auto !important; + } +} +@media (min-width: 1200px) { + .m-xl-0 { + margin: 0 !important; + } + + .mt-xl-0, +.my-xl-0 { + margin-top: 0 !important; + } + + .mr-xl-0, +.mx-xl-0 { + margin-right: 0 !important; + } + + .mb-xl-0, +.my-xl-0 { + margin-bottom: 0 !important; + } + + .ml-xl-0, +.mx-xl-0 { + margin-left: 0 !important; + } + + .m-xl-1 { + margin: 0.25rem !important; + } + + .mt-xl-1, +.my-xl-1 { + margin-top: 0.25rem !important; + } + + .mr-xl-1, +.mx-xl-1 { + margin-right: 0.25rem !important; + } + + .mb-xl-1, +.my-xl-1 { + margin-bottom: 0.25rem !important; + } + + .ml-xl-1, +.mx-xl-1 { + margin-left: 0.25rem !important; + } + + .m-xl-2 { + margin: 0.5rem !important; + } + + .mt-xl-2, +.my-xl-2 { + margin-top: 0.5rem !important; + } + + .mr-xl-2, +.mx-xl-2 { + margin-right: 0.5rem !important; + } + + .mb-xl-2, +.my-xl-2 { + margin-bottom: 0.5rem !important; + } + + .ml-xl-2, +.mx-xl-2 { + margin-left: 0.5rem !important; + } + + .m-xl-3 { + margin: 1rem !important; + } + + .mt-xl-3, +.my-xl-3 { + margin-top: 1rem !important; + } + + .mr-xl-3, +.mx-xl-3 { + margin-right: 1rem !important; + } + + .mb-xl-3, +.my-xl-3 { + margin-bottom: 1rem !important; + } + + .ml-xl-3, +.mx-xl-3 { + margin-left: 1rem !important; + } + + .m-xl-4 { + margin: 1.5rem !important; + } + + .mt-xl-4, +.my-xl-4 { + margin-top: 1.5rem !important; + } + + .mr-xl-4, +.mx-xl-4 { + margin-right: 1.5rem !important; + } + + .mb-xl-4, +.my-xl-4 { + margin-bottom: 1.5rem !important; + } + + .ml-xl-4, +.mx-xl-4 { + margin-left: 1.5rem !important; + } + + .m-xl-5 { + margin: 3rem !important; + } + + .mt-xl-5, +.my-xl-5 { + margin-top: 3rem !important; + } + + .mr-xl-5, +.mx-xl-5 { + margin-right: 3rem !important; + } + + .mb-xl-5, +.my-xl-5 { + margin-bottom: 3rem !important; + } + + .ml-xl-5, +.mx-xl-5 { + margin-left: 3rem !important; + } + + .p-xl-0 { + padding: 0 !important; + } + + .pt-xl-0, +.py-xl-0 { + padding-top: 0 !important; + } + + .pr-xl-0, +.px-xl-0 { + padding-right: 0 !important; + } + + .pb-xl-0, +.py-xl-0 { + padding-bottom: 0 !important; + } + + .pl-xl-0, +.px-xl-0 { + padding-left: 0 !important; + } + + .p-xl-1 { + padding: 0.25rem !important; + } + + .pt-xl-1, +.py-xl-1 { + padding-top: 0.25rem !important; + } + + .pr-xl-1, +.px-xl-1 { + padding-right: 0.25rem !important; + } + + .pb-xl-1, +.py-xl-1 { + padding-bottom: 0.25rem !important; + } + + .pl-xl-1, +.px-xl-1 { + padding-left: 0.25rem !important; + } + + .p-xl-2 { + padding: 0.5rem !important; + } + + .pt-xl-2, +.py-xl-2 { + padding-top: 0.5rem !important; + } + + .pr-xl-2, +.px-xl-2 { + padding-right: 0.5rem !important; + } + + .pb-xl-2, +.py-xl-2 { + padding-bottom: 0.5rem !important; + } + + .pl-xl-2, +.px-xl-2 { + padding-left: 0.5rem !important; + } + + .p-xl-3 { + padding: 1rem !important; + } + + .pt-xl-3, +.py-xl-3 { + padding-top: 1rem !important; + } + + .pr-xl-3, +.px-xl-3 { + padding-right: 1rem !important; + } + + .pb-xl-3, +.py-xl-3 { + padding-bottom: 1rem !important; + } + + .pl-xl-3, +.px-xl-3 { + padding-left: 1rem !important; + } + + .p-xl-4 { + padding: 1.5rem !important; + } + + .pt-xl-4, +.py-xl-4 { + padding-top: 1.5rem !important; + } + + .pr-xl-4, +.px-xl-4 { + padding-right: 1.5rem !important; + } + + .pb-xl-4, +.py-xl-4 { + padding-bottom: 1.5rem !important; + } + + .pl-xl-4, +.px-xl-4 { + padding-left: 1.5rem !important; + } + + .p-xl-5 { + padding: 3rem !important; + } + + .pt-xl-5, +.py-xl-5 { + padding-top: 3rem !important; + } + + .pr-xl-5, +.px-xl-5 { + padding-right: 3rem !important; + } + + .pb-xl-5, +.py-xl-5 { + padding-bottom: 3rem !important; + } + + .pl-xl-5, +.px-xl-5 { + padding-left: 3rem !important; + } + + .m-xl-n1 { + margin: -0.25rem !important; + } + + .mt-xl-n1, +.my-xl-n1 { + margin-top: -0.25rem !important; + } + + .mr-xl-n1, +.mx-xl-n1 { + margin-right: -0.25rem !important; + } + + .mb-xl-n1, +.my-xl-n1 { + margin-bottom: -0.25rem !important; + } + + .ml-xl-n1, +.mx-xl-n1 { + margin-left: -0.25rem !important; + } + + .m-xl-n2 { + margin: -0.5rem !important; + } + + .mt-xl-n2, +.my-xl-n2 { + margin-top: -0.5rem !important; + } + + .mr-xl-n2, +.mx-xl-n2 { + margin-right: -0.5rem !important; + } + + .mb-xl-n2, +.my-xl-n2 { + margin-bottom: -0.5rem !important; + } + + .ml-xl-n2, +.mx-xl-n2 { + margin-left: -0.5rem !important; + } + + .m-xl-n3 { + margin: -1rem !important; + } + + .mt-xl-n3, +.my-xl-n3 { + margin-top: -1rem !important; + } + + .mr-xl-n3, +.mx-xl-n3 { + margin-right: -1rem !important; + } + + .mb-xl-n3, +.my-xl-n3 { + margin-bottom: -1rem !important; + } + + .ml-xl-n3, +.mx-xl-n3 { + margin-left: -1rem !important; + } + + .m-xl-n4 { + margin: -1.5rem !important; + } + + .mt-xl-n4, +.my-xl-n4 { + margin-top: -1.5rem !important; + } + + .mr-xl-n4, +.mx-xl-n4 { + margin-right: -1.5rem !important; + } + + .mb-xl-n4, +.my-xl-n4 { + margin-bottom: -1.5rem !important; + } + + .ml-xl-n4, +.mx-xl-n4 { + margin-left: -1.5rem !important; + } + + .m-xl-n5 { + margin: -3rem !important; + } + + .mt-xl-n5, +.my-xl-n5 { + margin-top: -3rem !important; + } + + .mr-xl-n5, +.mx-xl-n5 { + margin-right: -3rem !important; + } + + .mb-xl-n5, +.my-xl-n5 { + margin-bottom: -3rem !important; + } + + .ml-xl-n5, +.mx-xl-n5 { + margin-left: -3rem !important; + } + + .m-xl-auto { + margin: auto !important; + } + + .mt-xl-auto, +.my-xl-auto { + margin-top: auto !important; + } + + .mr-xl-auto, +.mx-xl-auto { + margin-right: auto !important; + } + + .mb-xl-auto, +.my-xl-auto { + margin-bottom: auto !important; + } + + .ml-xl-auto, +.mx-xl-auto { + margin-left: auto !important; + } +} +.stretched-link::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1; + pointer-events: auto; + content: ""; + background-color: rgba(0, 0, 0, 0); +} + +.text-monospace { + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important; +} + +.text-justify { + text-align: justify !important; +} + +.text-wrap { + white-space: normal !important; +} + +.text-nowrap { + white-space: nowrap !important; +} + +.text-truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.text-left { + text-align: left !important; +} + +.text-right { + text-align: right !important; +} + +.text-center { + text-align: center !important; +} + +@media (min-width: 576px) { + .text-sm-left { + text-align: left !important; + } + + .text-sm-right { + text-align: right !important; + } + + .text-sm-center { + text-align: center !important; + } +} +@media (min-width: 768px) { + .text-md-left { + text-align: left !important; + } + + .text-md-right { + text-align: right !important; + } + + .text-md-center { + text-align: center !important; + } +} +@media (min-width: 992px) { + .text-lg-left { + text-align: left !important; + } + + .text-lg-right { + text-align: right !important; + } + + .text-lg-center { + text-align: center !important; + } +} +@media (min-width: 1200px) { + .text-xl-left { + text-align: left !important; + } + + .text-xl-right { + text-align: right !important; + } + + .text-xl-center { + text-align: center !important; + } +} +.text-lowercase { + text-transform: lowercase !important; +} + +.text-uppercase { + text-transform: uppercase !important; +} + +.text-capitalize { + text-transform: capitalize !important; +} + +.font-weight-light { + font-weight: 300 !important; +} + +.font-weight-lighter { + font-weight: lighter !important; +} + +.font-weight-normal { + font-weight: 400 !important; +} + +.font-weight-bold { + font-weight: 700 !important; +} + +.font-weight-bolder { + font-weight: bolder !important; +} + +.font-italic { + font-style: italic !important; +} + +.text-white { + color: #fff !important; +} + +.text-primary { + color: #4a6fae !important; +} + +a.text-primary:hover, a.text-primary:focus { + color: #334d78 !important; +} + +.text-secondary { + color: #6c757d !important; +} + +a.text-secondary:hover, a.text-secondary:focus { + color: #494f54 !important; +} + +.text-success { + color: #80cc28 !important; +} + +a.text-success:hover, a.text-success:focus { + color: #588c1b !important; +} + +.text-info { + color: #17a2b8 !important; +} + +a.text-info:hover, a.text-info:focus { + color: #0f6674 !important; +} + +.text-warning { + color: #ffc107 !important; +} + +a.text-warning:hover, a.text-warning:focus { + color: #ba8b00 !important; +} + +.text-danger { + color: red !important; +} + +a.text-danger:hover, a.text-danger:focus { + color: #b30000 !important; +} + +.text-light { + color: #f5f5f5 !important; +} + +a.text-light:hover, a.text-light:focus { + color: #cfcfcf !important; +} + +.text-dark { + color: #343a40 !important; +} + +a.text-dark:hover, a.text-dark:focus { + color: #121416 !important; +} + +.text-body { + color: #212529 !important; +} + +.text-muted { + color: #6c757d !important; +} + +.text-black-50 { + color: rgba(0, 0, 0, 0.5) !important; +} + +.text-white-50 { + color: rgba(255, 255, 255, 0.5) !important; +} + +.text-hide { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.text-decoration-none { + text-decoration: none !important; +} + +.text-break { + word-break: break-word !important; + word-wrap: break-word !important; +} + +.text-reset { + color: inherit !important; +} + +.visible { + visibility: visible !important; +} + +.invisible { + visibility: hidden !important; +} + +@media print { + *, +*::before, +*::after { + text-shadow: none !important; + box-shadow: none !important; + } + + a:not(.btn) { + text-decoration: underline; + } + + abbr[title]::after { + content: " (" attr(title) ")"; + } + + pre { + white-space: pre-wrap !important; + } + + pre, +blockquote { + border: 1px solid #adb5bd; + page-break-inside: avoid; + } + + tr, +img { + page-break-inside: avoid; + } + + p, +h2, +h3 { + orphans: 3; + widows: 3; + } + + h2, +h3 { + page-break-after: avoid; + } + + @page { + size: a3; + } + body { + min-width: 992px !important; + } + + .container { + min-width: 992px !important; + } + + .navbar { + display: none; + } + + .badge { + border: 1px solid #000; + } + + .table { + border-collapse: collapse !important; + } + .table td, +.table th { + background-color: #fff !important; + } + + .table-bordered th, +.table-bordered td { + border: 1px solid #dee2e6 !important; + } + + .table-dark { + color: inherit; + } + .table-dark th, +.table-dark td, +.table-dark thead th, +.table-dark tbody + tbody { + border-color: #dee2e6; + } + + .table .thead-dark th { + color: inherit; + border-color: #dee2e6; + } +} diff --git a/docker-app/qfieldcloud/core/staticfiles/js/vendor/bootstrap.min.js b/docker-app/qfieldcloud/core/staticfiles/js/vendor/bootstrap.min.js new file mode 100644 index 000000000..4e07ef4c9 --- /dev/null +++ b/docker-app/qfieldcloud/core/staticfiles/js/vendor/bootstrap.min.js @@ -0,0 +1,6 @@ +/*! + * Bootstrap v4.6.1 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap={},t.jQuery,t.Popper)}(this,(function(t,e,n){"use strict";function i(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var o=i(e),a=i(n);function s(t,e){for(var n=0;n=4)throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}};d.jQueryDetection(),o.default.fn.emulateTransitionEnd=function(t){var e=this,n=!1;return o.default(this).one(d.TRANSITION_END,(function(){n=!0})),setTimeout((function(){n||d.triggerTransitionEnd(e)}),t),this},o.default.event.special[d.TRANSITION_END]={bindType:f,delegateType:f,handle:function(t){if(o.default(t.target).is(this))return t.handleObj.handler.apply(this,arguments)}};var c="bs.alert",h=o.default.fn.alert,g=function(){function t(t){this._element=t}var e=t.prototype;return e.close=function(t){var e=this._element;t&&(e=this._getRootElement(t)),this._triggerCloseEvent(e).isDefaultPrevented()||this._removeElement(e)},e.dispose=function(){o.default.removeData(this._element,c),this._element=null},e._getRootElement=function(t){var e=d.getSelectorFromElement(t),n=!1;return e&&(n=document.querySelector(e)),n||(n=o.default(t).closest(".alert")[0]),n},e._triggerCloseEvent=function(t){var e=o.default.Event("close.bs.alert");return o.default(t).trigger(e),e},e._removeElement=function(t){var e=this;if(o.default(t).removeClass("show"),o.default(t).hasClass("fade")){var n=d.getTransitionDurationFromElement(t);o.default(t).one(d.TRANSITION_END,(function(n){return e._destroyElement(t,n)})).emulateTransitionEnd(n)}else this._destroyElement(t)},e._destroyElement=function(t){o.default(t).detach().trigger("closed.bs.alert").remove()},t._jQueryInterface=function(e){return this.each((function(){var n=o.default(this),i=n.data(c);i||(i=new t(this),n.data(c,i)),"close"===e&&i[e](this)}))},t._handleDismiss=function(t){return function(e){e&&e.preventDefault(),t.close(this)}},l(t,null,[{key:"VERSION",get:function(){return"4.6.1"}}]),t}();o.default(document).on("click.bs.alert.data-api",'[data-dismiss="alert"]',g._handleDismiss(new g)),o.default.fn.alert=g._jQueryInterface,o.default.fn.alert.Constructor=g,o.default.fn.alert.noConflict=function(){return o.default.fn.alert=h,g._jQueryInterface};var m="bs.button",p=o.default.fn.button,_="active",v='[data-toggle^="button"]',y='input:not([type="hidden"])',b=".btn",E=function(){function t(t){this._element=t,this.shouldAvoidTriggerChange=!1}var e=t.prototype;return e.toggle=function(){var t=!0,e=!0,n=o.default(this._element).closest('[data-toggle="buttons"]')[0];if(n){var i=this._element.querySelector(y);if(i){if("radio"===i.type)if(i.checked&&this._element.classList.contains(_))t=!1;else{var a=n.querySelector(".active");a&&o.default(a).removeClass(_)}t&&("checkbox"!==i.type&&"radio"!==i.type||(i.checked=!this._element.classList.contains(_)),this.shouldAvoidTriggerChange||o.default(i).trigger("change")),i.focus(),e=!1}}this._element.hasAttribute("disabled")||this._element.classList.contains("disabled")||(e&&this._element.setAttribute("aria-pressed",!this._element.classList.contains(_)),t&&o.default(this._element).toggleClass(_))},e.dispose=function(){o.default.removeData(this._element,m),this._element=null},t._jQueryInterface=function(e,n){return this.each((function(){var i=o.default(this),a=i.data(m);a||(a=new t(this),i.data(m,a)),a.shouldAvoidTriggerChange=n,"toggle"===e&&a[e]()}))},l(t,null,[{key:"VERSION",get:function(){return"4.6.1"}}]),t}();o.default(document).on("click.bs.button.data-api",v,(function(t){var e=t.target,n=e;if(o.default(e).hasClass("btn")||(e=o.default(e).closest(b)[0]),!e||e.hasAttribute("disabled")||e.classList.contains("disabled"))t.preventDefault();else{var i=e.querySelector(y);if(i&&(i.hasAttribute("disabled")||i.classList.contains("disabled")))return void t.preventDefault();"INPUT"!==n.tagName&&"LABEL"===e.tagName||E._jQueryInterface.call(o.default(e),"toggle","INPUT"===n.tagName)}})).on("focus.bs.button.data-api blur.bs.button.data-api",v,(function(t){var e=o.default(t.target).closest(b)[0];o.default(e).toggleClass("focus",/^focus(in)?$/.test(t.type))})),o.default(window).on("load.bs.button.data-api",(function(){for(var t=[].slice.call(document.querySelectorAll('[data-toggle="buttons"] .btn')),e=0,n=t.length;e0,this._pointerEvent=Boolean(window.PointerEvent||window.MSPointerEvent),this._addEventListeners()}var e=t.prototype;return e.next=function(){this._isSliding||this._slide(N)},e.nextWhenVisible=function(){var t=o.default(this._element);!document.hidden&&t.is(":visible")&&"hidden"!==t.css("visibility")&&this.next()},e.prev=function(){this._isSliding||this._slide(D)},e.pause=function(t){t||(this._isPaused=!0),this._element.querySelector(".carousel-item-next, .carousel-item-prev")&&(d.triggerTransitionEnd(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null},e.cycle=function(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config.interval&&!this._isPaused&&(this._updateInterval(),this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))},e.to=function(t){var e=this;this._activeElement=this._element.querySelector(I);var n=this._getItemIndex(this._activeElement);if(!(t>this._items.length-1||t<0))if(this._isSliding)o.default(this._element).one(A,(function(){return e.to(t)}));else{if(n===t)return this.pause(),void this.cycle();var i=t>n?N:D;this._slide(i,this._items[t])}},e.dispose=function(){o.default(this._element).off(".bs.carousel"),o.default.removeData(this._element,w),this._items=null,this._config=null,this._element=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null},e._getConfig=function(t){return t=r({},k,t),d.typeCheckConfig(T,t,O),t},e._handleSwipe=function(){var t=Math.abs(this.touchDeltaX);if(!(t<=40)){var e=t/this.touchDeltaX;this.touchDeltaX=0,e>0&&this.prev(),e<0&&this.next()}},e._addEventListeners=function(){var t=this;this._config.keyboard&&o.default(this._element).on("keydown.bs.carousel",(function(e){return t._keydown(e)})),"hover"===this._config.pause&&o.default(this._element).on("mouseenter.bs.carousel",(function(e){return t.pause(e)})).on("mouseleave.bs.carousel",(function(e){return t.cycle(e)})),this._config.touch&&this._addTouchEventListeners()},e._addTouchEventListeners=function(){var t=this;if(this._touchSupported){var e=function(e){t._pointerEvent&&j[e.originalEvent.pointerType.toUpperCase()]?t.touchStartX=e.originalEvent.clientX:t._pointerEvent||(t.touchStartX=e.originalEvent.touches[0].clientX)},n=function(e){t._pointerEvent&&j[e.originalEvent.pointerType.toUpperCase()]&&(t.touchDeltaX=e.originalEvent.clientX-t.touchStartX),t._handleSwipe(),"hover"===t._config.pause&&(t.pause(),t.touchTimeout&&clearTimeout(t.touchTimeout),t.touchTimeout=setTimeout((function(e){return t.cycle(e)}),500+t._config.interval))};o.default(this._element.querySelectorAll(".carousel-item img")).on("dragstart.bs.carousel",(function(t){return t.preventDefault()})),this._pointerEvent?(o.default(this._element).on("pointerdown.bs.carousel",(function(t){return e(t)})),o.default(this._element).on("pointerup.bs.carousel",(function(t){return n(t)})),this._element.classList.add("pointer-event")):(o.default(this._element).on("touchstart.bs.carousel",(function(t){return e(t)})),o.default(this._element).on("touchmove.bs.carousel",(function(e){return function(e){t.touchDeltaX=e.originalEvent.touches&&e.originalEvent.touches.length>1?0:e.originalEvent.touches[0].clientX-t.touchStartX}(e)})),o.default(this._element).on("touchend.bs.carousel",(function(t){return n(t)})))}},e._keydown=function(t){if(!/input|textarea/i.test(t.target.tagName))switch(t.which){case 37:t.preventDefault(),this.prev();break;case 39:t.preventDefault(),this.next()}},e._getItemIndex=function(t){return this._items=t&&t.parentNode?[].slice.call(t.parentNode.querySelectorAll(".carousel-item")):[],this._items.indexOf(t)},e._getItemByDirection=function(t,e){var n=t===N,i=t===D,o=this._getItemIndex(e),a=this._items.length-1;if((i&&0===o||n&&o===a)&&!this._config.wrap)return e;var s=(o+(t===D?-1:1))%this._items.length;return-1===s?this._items[this._items.length-1]:this._items[s]},e._triggerSlideEvent=function(t,e){var n=this._getItemIndex(t),i=this._getItemIndex(this._element.querySelector(I)),a=o.default.Event("slide.bs.carousel",{relatedTarget:t,direction:e,from:i,to:n});return o.default(this._element).trigger(a),a},e._setActiveIndicatorElement=function(t){if(this._indicatorsElement){var e=[].slice.call(this._indicatorsElement.querySelectorAll(".active"));o.default(e).removeClass(S);var n=this._indicatorsElement.children[this._getItemIndex(t)];n&&o.default(n).addClass(S)}},e._updateInterval=function(){var t=this._activeElement||this._element.querySelector(I);if(t){var e=parseInt(t.getAttribute("data-interval"),10);e?(this._config.defaultInterval=this._config.defaultInterval||this._config.interval,this._config.interval=e):this._config.interval=this._config.defaultInterval||this._config.interval}},e._slide=function(t,e){var n,i,a,s=this,l=this._element.querySelector(I),r=this._getItemIndex(l),u=e||l&&this._getItemByDirection(t,l),f=this._getItemIndex(u),c=Boolean(this._interval);if(t===N?(n="carousel-item-left",i="carousel-item-next",a="left"):(n="carousel-item-right",i="carousel-item-prev",a="right"),u&&o.default(u).hasClass(S))this._isSliding=!1;else if(!this._triggerSlideEvent(u,a).isDefaultPrevented()&&l&&u){this._isSliding=!0,c&&this.pause(),this._setActiveIndicatorElement(u),this._activeElement=u;var h=o.default.Event(A,{relatedTarget:u,direction:a,from:r,to:f});if(o.default(this._element).hasClass("slide")){o.default(u).addClass(i),d.reflow(u),o.default(l).addClass(n),o.default(u).addClass(n);var g=d.getTransitionDurationFromElement(l);o.default(l).one(d.TRANSITION_END,(function(){o.default(u).removeClass(n+" "+i).addClass(S),o.default(l).removeClass("active "+i+" "+n),s._isSliding=!1,setTimeout((function(){return o.default(s._element).trigger(h)}),0)})).emulateTransitionEnd(g)}else o.default(l).removeClass(S),o.default(u).addClass(S),this._isSliding=!1,o.default(this._element).trigger(h);c&&this.cycle()}},t._jQueryInterface=function(e){return this.each((function(){var n=o.default(this).data(w),i=r({},k,o.default(this).data());"object"==typeof e&&(i=r({},i,e));var a="string"==typeof e?e:i.slide;if(n||(n=new t(this,i),o.default(this).data(w,n)),"number"==typeof e)n.to(e);else if("string"==typeof a){if("undefined"==typeof n[a])throw new TypeError('No method named "'+a+'"');n[a]()}else i.interval&&i.ride&&(n.pause(),n.cycle())}))},t._dataApiClickHandler=function(e){var n=d.getSelectorFromElement(this);if(n){var i=o.default(n)[0];if(i&&o.default(i).hasClass("carousel")){var a=r({},o.default(i).data(),o.default(this).data()),s=this.getAttribute("data-slide-to");s&&(a.interval=!1),t._jQueryInterface.call(o.default(i),a),s&&o.default(i).data(w).to(s),e.preventDefault()}}},l(t,null,[{key:"VERSION",get:function(){return"4.6.1"}},{key:"Default",get:function(){return k}}]),t}();o.default(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",P._dataApiClickHandler),o.default(window).on("load.bs.carousel.data-api",(function(){for(var t=[].slice.call(document.querySelectorAll('[data-ride="carousel"]')),e=0,n=t.length;e0&&(this._selector=s,this._triggerArray.push(a))}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}var e=t.prototype;return e.toggle=function(){o.default(this._element).hasClass(q)?this.hide():this.show()},e.show=function(){var e,n,i=this;if(!(this._isTransitioning||o.default(this._element).hasClass(q)||(this._parent&&0===(e=[].slice.call(this._parent.querySelectorAll(".show, .collapsing")).filter((function(t){return"string"==typeof i._config.parent?t.getAttribute("data-parent")===i._config.parent:t.classList.contains(F)}))).length&&(e=null),e&&(n=o.default(e).not(this._selector).data(R))&&n._isTransitioning))){var a=o.default.Event("show.bs.collapse");if(o.default(this._element).trigger(a),!a.isDefaultPrevented()){e&&(t._jQueryInterface.call(o.default(e).not(this._selector),"hide"),n||o.default(e).data(R,null));var s=this._getDimension();o.default(this._element).removeClass(F).addClass(Q),this._element.style[s]=0,this._triggerArray.length&&o.default(this._triggerArray).removeClass(B).attr("aria-expanded",!0),this.setTransitioning(!0);var l="scroll"+(s[0].toUpperCase()+s.slice(1)),r=d.getTransitionDurationFromElement(this._element);o.default(this._element).one(d.TRANSITION_END,(function(){o.default(i._element).removeClass(Q).addClass("collapse show"),i._element.style[s]="",i.setTransitioning(!1),o.default(i._element).trigger("shown.bs.collapse")})).emulateTransitionEnd(r),this._element.style[s]=this._element[l]+"px"}}},e.hide=function(){var t=this;if(!this._isTransitioning&&o.default(this._element).hasClass(q)){var e=o.default.Event("hide.bs.collapse");if(o.default(this._element).trigger(e),!e.isDefaultPrevented()){var n=this._getDimension();this._element.style[n]=this._element.getBoundingClientRect()[n]+"px",d.reflow(this._element),o.default(this._element).addClass(Q).removeClass("collapse show");var i=this._triggerArray.length;if(i>0)for(var a=0;a0},e._getOffset=function(){var t=this,e={};return"function"==typeof this._config.offset?e.fn=function(e){return e.offsets=r({},e.offsets,t._config.offset(e.offsets,t._element)),e}:e.offset=this._config.offset,e},e._getPopperConfig=function(){var t={placement:this._getPlacement(),modifiers:{offset:this._getOffset(),flip:{enabled:this._config.flip},preventOverflow:{boundariesElement:this._config.boundary}}};return"static"===this._config.display&&(t.modifiers.applyStyle={enabled:!1}),r({},t,this._config.popperConfig)},t._jQueryInterface=function(e){return this.each((function(){var n=o.default(this).data(K);if(n||(n=new t(this,"object"==typeof e?e:null),o.default(this).data(K,n)),"string"==typeof e){if("undefined"==typeof n[e])throw new TypeError('No method named "'+e+'"');n[e]()}}))},t._clearMenus=function(e){if(!e||3!==e.which&&("keyup"!==e.type||9===e.which))for(var n=[].slice.call(document.querySelectorAll(it)),i=0,a=n.length;i0&&s--,40===e.which&&sdocument.documentElement.clientHeight;n||(this._element.style.overflowY="hidden"),this._element.classList.add(ht);var i=d.getTransitionDurationFromElement(this._dialog);o.default(this._element).off(d.TRANSITION_END),o.default(this._element).one(d.TRANSITION_END,(function(){t._element.classList.remove(ht),n||o.default(t._element).one(d.TRANSITION_END,(function(){t._element.style.overflowY=""})).emulateTransitionEnd(t._element,i)})).emulateTransitionEnd(i),this._element.focus()}},e._showElement=function(t){var e=this,n=o.default(this._element).hasClass(dt),i=this._dialog?this._dialog.querySelector(".modal-body"):null;this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.appendChild(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),o.default(this._dialog).hasClass("modal-dialog-scrollable")&&i?i.scrollTop=0:this._element.scrollTop=0,n&&d.reflow(this._element),o.default(this._element).addClass(ct),this._config.focus&&this._enforceFocus();var a=o.default.Event("shown.bs.modal",{relatedTarget:t}),s=function(){e._config.focus&&e._element.focus(),e._isTransitioning=!1,o.default(e._element).trigger(a)};if(n){var l=d.getTransitionDurationFromElement(this._dialog);o.default(this._dialog).one(d.TRANSITION_END,s).emulateTransitionEnd(l)}else s()},e._enforceFocus=function(){var t=this;o.default(document).off(pt).on(pt,(function(e){document!==e.target&&t._element!==e.target&&0===o.default(t._element).has(e.target).length&&t._element.focus()}))},e._setEscapeEvent=function(){var t=this;this._isShown?o.default(this._element).on(yt,(function(e){t._config.keyboard&&27===e.which?(e.preventDefault(),t.hide()):t._config.keyboard||27!==e.which||t._triggerBackdropTransition()})):this._isShown||o.default(this._element).off(yt)},e._setResizeEvent=function(){var t=this;this._isShown?o.default(window).on(_t,(function(e){return t.handleUpdate(e)})):o.default(window).off(_t)},e._hideModal=function(){var t=this;this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._showBackdrop((function(){o.default(document.body).removeClass(ft),t._resetAdjustments(),t._resetScrollbar(),o.default(t._element).trigger(gt)}))},e._removeBackdrop=function(){this._backdrop&&(o.default(this._backdrop).remove(),this._backdrop=null)},e._showBackdrop=function(t){var e=this,n=o.default(this._element).hasClass(dt)?dt:"";if(this._isShown&&this._config.backdrop){if(this._backdrop=document.createElement("div"),this._backdrop.className="modal-backdrop",n&&this._backdrop.classList.add(n),o.default(this._backdrop).appendTo(document.body),o.default(this._element).on(vt,(function(t){e._ignoreBackdropClick?e._ignoreBackdropClick=!1:t.target===t.currentTarget&&("static"===e._config.backdrop?e._triggerBackdropTransition():e.hide())})),n&&d.reflow(this._backdrop),o.default(this._backdrop).addClass(ct),!t)return;if(!n)return void t();var i=d.getTransitionDurationFromElement(this._backdrop);o.default(this._backdrop).one(d.TRANSITION_END,t).emulateTransitionEnd(i)}else if(!this._isShown&&this._backdrop){o.default(this._backdrop).removeClass(ct);var a=function(){e._removeBackdrop(),t&&t()};if(o.default(this._element).hasClass(dt)){var s=d.getTransitionDurationFromElement(this._backdrop);o.default(this._backdrop).one(d.TRANSITION_END,a).emulateTransitionEnd(s)}else a()}else t&&t()},e._adjustDialog=function(){var t=this._element.scrollHeight>document.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},e._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},e._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=Math.round(t.left+t.right)
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",customClass:"",sanitize:!0,sanitizeFn:null,whiteList:{"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},popperConfig:null},Ut={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(number|string|function)",container:"(string|element|boolean)",fallbackPlacement:"(string|array)",boundary:"(string|element)",customClass:"(string|function)",sanitize:"boolean",sanitizeFn:"(null|function)",whiteList:"object",popperConfig:"(null|object)"},Mt={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"},Wt=function(){function t(t,e){if("undefined"==typeof a.default)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var e=t.prototype;return e.enable=function(){this._isEnabled=!0},e.disable=function(){this._isEnabled=!1},e.toggleEnabled=function(){this._isEnabled=!this._isEnabled},e.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=o.default(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),o.default(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(o.default(this.getTipElement()).hasClass(Rt))return void this._leave(null,this);this._enter(null,this)}},e.dispose=function(){clearTimeout(this._timeout),o.default.removeData(this.element,this.constructor.DATA_KEY),o.default(this.element).off(this.constructor.EVENT_KEY),o.default(this.element).closest(".modal").off("hide.bs.modal",this._hideModalHandler),this.tip&&o.default(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},e.show=function(){var t=this;if("none"===o.default(this.element).css("display"))throw new Error("Please use show on visible elements");var e=o.default.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){o.default(this.element).trigger(e);var n=d.findShadowRoot(this.element),i=o.default.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(e.isDefaultPrevented()||!i)return;var s=this.getTipElement(),l=d.getUID(this.constructor.NAME);s.setAttribute("id",l),this.element.setAttribute("aria-describedby",l),this.setContent(),this.config.animation&&o.default(s).addClass(Lt);var r="function"==typeof this.config.placement?this.config.placement.call(this,s,this.element):this.config.placement,u=this._getAttachment(r);this.addAttachmentClass(u);var f=this._getContainer();o.default(s).data(this.constructor.DATA_KEY,this),o.default.contains(this.element.ownerDocument.documentElement,this.tip)||o.default(s).appendTo(f),o.default(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new a.default(this.element,s,this._getPopperConfig(u)),o.default(s).addClass(Rt),o.default(s).addClass(this.config.customClass),"ontouchstart"in document.documentElement&&o.default(document.body).children().on("mouseover",null,o.default.noop);var c=function(){t.config.animation&&t._fixTransition();var e=t._hoverState;t._hoverState=null,o.default(t.element).trigger(t.constructor.Event.SHOWN),e===qt&&t._leave(null,t)};if(o.default(this.tip).hasClass(Lt)){var h=d.getTransitionDurationFromElement(this.tip);o.default(this.tip).one(d.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},e.hide=function(t){var e=this,n=this.getTipElement(),i=o.default.Event(this.constructor.Event.HIDE),a=function(){e._hoverState!==xt&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),o.default(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(o.default(this.element).trigger(i),!i.isDefaultPrevented()){if(o.default(n).removeClass(Rt),"ontouchstart"in document.documentElement&&o.default(document.body).children().off("mouseover",null,o.default.noop),this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1,o.default(this.tip).hasClass(Lt)){var s=d.getTransitionDurationFromElement(n);o.default(n).one(d.TRANSITION_END,a).emulateTransitionEnd(s)}else a();this._hoverState=""}},e.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},e.isWithContent=function(){return Boolean(this.getTitle())},e.addAttachmentClass=function(t){o.default(this.getTipElement()).addClass("bs-tooltip-"+t)},e.getTipElement=function(){return this.tip=this.tip||o.default(this.config.template)[0],this.tip},e.setContent=function(){var t=this.getTipElement();this.setElementContent(o.default(t.querySelectorAll(".tooltip-inner")),this.getTitle()),o.default(t).removeClass("fade show")},e.setElementContent=function(t,e){"object"!=typeof e||!e.nodeType&&!e.jquery?this.config.html?(this.config.sanitize&&(e=At(e,this.config.whiteList,this.config.sanitizeFn)),t.html(e)):t.text(e):this.config.html?o.default(e).parent().is(t)||t.empty().append(e):t.text(o.default(e).text())},e.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},e._getPopperConfig=function(t){var e=this;return r({},{placement:t,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:".arrow"},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}},this.config.popperConfig)},e._getOffset=function(){var t=this,e={};return"function"==typeof this.config.offset?e.fn=function(e){return e.offsets=r({},e.offsets,t.config.offset(e.offsets,t.element)),e}:e.offset=this.config.offset,e},e._getContainer=function(){return!1===this.config.container?document.body:d.isElement(this.config.container)?o.default(this.config.container):o.default(document).find(this.config.container)},e._getAttachment=function(t){return Bt[t.toUpperCase()]},e._setListeners=function(){var t=this;this.config.trigger.split(" ").forEach((function(e){if("click"===e)o.default(t.element).on(t.constructor.Event.CLICK,t.config.selector,(function(e){return t.toggle(e)}));else if("manual"!==e){var n=e===Ft?t.constructor.Event.MOUSEENTER:t.constructor.Event.FOCUSIN,i=e===Ft?t.constructor.Event.MOUSELEAVE:t.constructor.Event.FOCUSOUT;o.default(t.element).on(n,t.config.selector,(function(e){return t._enter(e)})).on(i,t.config.selector,(function(e){return t._leave(e)}))}})),this._hideModalHandler=function(){t.element&&t.hide()},o.default(this.element).closest(".modal").on("hide.bs.modal",this._hideModalHandler),this.config.selector?this.config=r({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},e._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},e._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||o.default(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),o.default(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Qt:Ft]=!0),o.default(e.getTipElement()).hasClass(Rt)||e._hoverState===xt?e._hoverState=xt:(clearTimeout(e._timeout),e._hoverState=xt,e.config.delay&&e.config.delay.show?e._timeout=setTimeout((function(){e._hoverState===xt&&e.show()}),e.config.delay.show):e.show())},e._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||o.default(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),o.default(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Qt:Ft]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=qt,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout((function(){e._hoverState===qt&&e.hide()}),e.config.delay.hide):e.hide())},e._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},e._getConfig=function(t){var e=o.default(this.element).data();return Object.keys(e).forEach((function(t){-1!==Pt.indexOf(t)&&delete e[t]})),"number"==typeof(t=r({},this.constructor.Default,e,"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),d.typeCheckConfig(It,t,this.constructor.DefaultType),t.sanitize&&(t.template=At(t.template,t.whiteList,t.sanitizeFn)),t},e._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},e._cleanTipClass=function(){var t=o.default(this.getTipElement()),e=t.attr("class").match(jt);null!==e&&e.length&&t.removeClass(e.join(""))},e._handlePopperPlacementChange=function(t){this.tip=t.instance.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},e._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(o.default(t).removeClass(Lt),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},t._jQueryInterface=function(e){return this.each((function(){var n=o.default(this),i=n.data(kt),a="object"==typeof e&&e;if((i||!/dispose|hide/.test(e))&&(i||(i=new t(this,a),n.data(kt,i)),"string"==typeof e)){if("undefined"==typeof i[e])throw new TypeError('No method named "'+e+'"');i[e]()}}))},l(t,null,[{key:"VERSION",get:function(){return"4.6.1"}},{key:"Default",get:function(){return Ht}},{key:"NAME",get:function(){return It}},{key:"DATA_KEY",get:function(){return kt}},{key:"Event",get:function(){return Mt}},{key:"EVENT_KEY",get:function(){return".bs.tooltip"}},{key:"DefaultType",get:function(){return Ut}}]),t}();o.default.fn.tooltip=Wt._jQueryInterface,o.default.fn.tooltip.Constructor=Wt,o.default.fn.tooltip.noConflict=function(){return o.default.fn.tooltip=Ot,Wt._jQueryInterface};var Vt="bs.popover",zt=o.default.fn.popover,Kt=new RegExp("(^|\\s)bs-popover\\S+","g"),Xt=r({},Wt.Default,{placement:"right",trigger:"click",content:"",template:''}),Yt=r({},Wt.DefaultType,{content:"(string|element|function)"}),$t={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"},Jt=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),e.prototype.constructor=e,u(e,n);var a=i.prototype;return a.isWithContent=function(){return this.getTitle()||this._getContent()},a.addAttachmentClass=function(t){o.default(this.getTipElement()).addClass("bs-popover-"+t)},a.getTipElement=function(){return this.tip=this.tip||o.default(this.config.template)[0],this.tip},a.setContent=function(){var t=o.default(this.getTipElement());this.setElementContent(t.find(".popover-header"),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(".popover-body"),e),t.removeClass("fade show")},a._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},a._cleanTipClass=function(){var t=o.default(this.getTipElement()),e=t.attr("class").match(Kt);null!==e&&e.length>0&&t.removeClass(e.join(""))},i._jQueryInterface=function(t){return this.each((function(){var e=o.default(this).data(Vt),n="object"==typeof t?t:null;if((e||!/dispose|hide/.test(t))&&(e||(e=new i(this,n),o.default(this).data(Vt,e)),"string"==typeof t)){if("undefined"==typeof e[t])throw new TypeError('No method named "'+t+'"');e[t]()}}))},l(i,null,[{key:"VERSION",get:function(){return"4.6.1"}},{key:"Default",get:function(){return Xt}},{key:"NAME",get:function(){return"popover"}},{key:"DATA_KEY",get:function(){return Vt}},{key:"Event",get:function(){return $t}},{key:"EVENT_KEY",get:function(){return".bs.popover"}},{key:"DefaultType",get:function(){return Yt}}]),i}(Wt);o.default.fn.popover=Jt._jQueryInterface,o.default.fn.popover.Constructor=Jt,o.default.fn.popover.noConflict=function(){return o.default.fn.popover=zt,Jt._jQueryInterface};var Gt="scrollspy",Zt="bs.scrollspy",te=o.default.fn[Gt],ee="active",ne="position",ie=".nav, .list-group",oe={offset:10,method:"auto",target:""},ae={offset:"number",method:"string",target:"(string|element)"},se=function(){function t(t,e){var n=this;this._element=t,this._scrollElement="BODY"===t.tagName?window:t,this._config=this._getConfig(e),this._selector=this._config.target+" .nav-link,"+this._config.target+" .list-group-item,"+this._config.target+" .dropdown-item",this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,o.default(this._scrollElement).on("scroll.bs.scrollspy",(function(t){return n._process(t)})),this.refresh(),this._process()}var e=t.prototype;return e.refresh=function(){var t=this,e=this._scrollElement===this._scrollElement.window?"offset":ne,n="auto"===this._config.method?e:this._config.method,i=n===ne?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),[].slice.call(document.querySelectorAll(this._selector)).map((function(t){var e,a=d.getSelectorFromElement(t);if(a&&(e=document.querySelector(a)),e){var s=e.getBoundingClientRect();if(s.width||s.height)return[o.default(e)[n]().top+i,a]}return null})).filter((function(t){return t})).sort((function(t,e){return t[0]-e[0]})).forEach((function(e){t._offsets.push(e[0]),t._targets.push(e[1])}))},e.dispose=function(){o.default.removeData(this._element,Zt),o.default(this._scrollElement).off(".bs.scrollspy"),this._element=null,this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null},e._getConfig=function(t){if("string"!=typeof(t=r({},oe,"object"==typeof t&&t?t:{})).target&&d.isElement(t.target)){var e=o.default(t.target).attr("id");e||(e=d.getUID(Gt),o.default(t.target).attr("id",e)),t.target="#"+e}return d.typeCheckConfig(Gt,t,ae),t},e._getScrollTop=function(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop},e._getScrollHeight=function(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)},e._getOffsetHeight=function(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height},e._process=function(){var t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),n=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=n){var i=this._targets[this._targets.length-1];this._activeTarget!==i&&this._activate(i)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(var o=this._offsets.length;o--;)this._activeTarget!==this._targets[o]&&t>=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t li > .active",ge=function(){function t(t){this._element=t}var e=t.prototype;return e.show=function(){var t=this;if(!(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&o.default(this._element).hasClass(ue)||o.default(this._element).hasClass("disabled"))){var e,n,i=o.default(this._element).closest(".nav, .list-group")[0],a=d.getSelectorFromElement(this._element);if(i){var s="UL"===i.nodeName||"OL"===i.nodeName?he:ce;n=(n=o.default.makeArray(o.default(i).find(s)))[n.length-1]}var l=o.default.Event("hide.bs.tab",{relatedTarget:this._element}),r=o.default.Event("show.bs.tab",{relatedTarget:n});if(n&&o.default(n).trigger(l),o.default(this._element).trigger(r),!r.isDefaultPrevented()&&!l.isDefaultPrevented()){a&&(e=document.querySelector(a)),this._activate(this._element,i);var u=function(){var e=o.default.Event("hidden.bs.tab",{relatedTarget:t._element}),i=o.default.Event("shown.bs.tab",{relatedTarget:n});o.default(n).trigger(e),o.default(t._element).trigger(i)};e?this._activate(e,e.parentNode,u):u()}}},e.dispose=function(){o.default.removeData(this._element,le),this._element=null},e._activate=function(t,e,n){var i=this,a=(!e||"UL"!==e.nodeName&&"OL"!==e.nodeName?o.default(e).children(ce):o.default(e).find(he))[0],s=n&&a&&o.default(a).hasClass(fe),l=function(){return i._transitionComplete(t,a,n)};if(a&&s){var r=d.getTransitionDurationFromElement(a);o.default(a).removeClass(de).one(d.TRANSITION_END,l).emulateTransitionEnd(r)}else l()},e._transitionComplete=function(t,e,n){if(e){o.default(e).removeClass(ue);var i=o.default(e.parentNode).find("> .dropdown-menu .active")[0];i&&o.default(i).removeClass(ue),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}o.default(t).addClass(ue),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),d.reflow(t),t.classList.contains(fe)&&t.classList.add(de);var a=t.parentNode;if(a&&"LI"===a.nodeName&&(a=a.parentNode),a&&o.default(a).hasClass("dropdown-menu")){var s=o.default(t).closest(".dropdown")[0];if(s){var l=[].slice.call(s.querySelectorAll(".dropdown-toggle"));o.default(l).addClass(ue)}t.setAttribute("aria-expanded",!0)}n&&n()},t._jQueryInterface=function(e){return this.each((function(){var n=o.default(this),i=n.data(le);if(i||(i=new t(this),n.data(le,i)),"string"==typeof e){if("undefined"==typeof i[e])throw new TypeError('No method named "'+e+'"');i[e]()}}))},l(t,null,[{key:"VERSION",get:function(){return"4.6.1"}}]),t}();o.default(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]',(function(t){t.preventDefault(),ge._jQueryInterface.call(o.default(this),"show")})),o.default.fn.tab=ge._jQueryInterface,o.default.fn.tab.Constructor=ge,o.default.fn.tab.noConflict=function(){return o.default.fn.tab=re,ge._jQueryInterface};var me="bs.toast",pe=o.default.fn.toast,_e="hide",ve="show",ye="showing",be="click.dismiss.bs.toast",Ee={animation:!0,autohide:!0,delay:500},Te={animation:"boolean",autohide:"boolean",delay:"number"},we=function(){function t(t,e){this._element=t,this._config=this._getConfig(e),this._timeout=null,this._setListeners()}var e=t.prototype;return e.show=function(){var t=this,e=o.default.Event("show.bs.toast");if(o.default(this._element).trigger(e),!e.isDefaultPrevented()){this._clearTimeout(),this._config.animation&&this._element.classList.add("fade");var n=function(){t._element.classList.remove(ye),t._element.classList.add(ve),o.default(t._element).trigger("shown.bs.toast"),t._config.autohide&&(t._timeout=setTimeout((function(){t.hide()}),t._config.delay))};if(this._element.classList.remove(_e),d.reflow(this._element),this._element.classList.add(ye),this._config.animation){var i=d.getTransitionDurationFromElement(this._element);o.default(this._element).one(d.TRANSITION_END,n).emulateTransitionEnd(i)}else n()}},e.hide=function(){if(this._element.classList.contains(ve)){var t=o.default.Event("hide.bs.toast");o.default(this._element).trigger(t),t.isDefaultPrevented()||this._close()}},e.dispose=function(){this._clearTimeout(),this._element.classList.contains(ve)&&this._element.classList.remove(ve),o.default(this._element).off(be),o.default.removeData(this._element,me),this._element=null,this._config=null},e._getConfig=function(t){return t=r({},Ee,o.default(this._element).data(),"object"==typeof t&&t?t:{}),d.typeCheckConfig("toast",t,this.constructor.DefaultType),t},e._setListeners=function(){var t=this;o.default(this._element).on(be,'[data-dismiss="toast"]',(function(){return t.hide()}))},e._close=function(){var t=this,e=function(){t._element.classList.add(_e),o.default(t._element).trigger("hidden.bs.toast")};if(this._element.classList.remove(ve),this._config.animation){var n=d.getTransitionDurationFromElement(this._element);o.default(this._element).one(d.TRANSITION_END,e).emulateTransitionEnd(n)}else e()},e._clearTimeout=function(){clearTimeout(this._timeout),this._timeout=null},t._jQueryInterface=function(e){return this.each((function(){var n=o.default(this),i=n.data(me);if(i||(i=new t(this,"object"==typeof e&&e),n.data(me,i)),"string"==typeof e){if("undefined"==typeof i[e])throw new TypeError('No method named "'+e+'"');i[e](this)}}))},l(t,null,[{key:"VERSION",get:function(){return"4.6.1"}},{key:"DefaultType",get:function(){return Te}},{key:"Default",get:function(){return Ee}}]),t}();o.default.fn.toast=we._jQueryInterface,o.default.fn.toast.Constructor=we,o.default.fn.toast.noConflict=function(){return o.default.fn.toast=pe,we._jQueryInterface},t.Alert=g,t.Button=E,t.Carousel=P,t.Collapse=V,t.Dropdown=lt,t.Modal=Ct,t.Popover=Jt,t.Scrollspy=se,t.Tab=ge,t.Toast=we,t.Tooltip=Wt,t.Util=d,Object.defineProperty(t,"__esModule",{value:!0})})); diff --git a/docker-app/qfieldcloud/core/staticfiles/js/vendor/jquery.js b/docker-app/qfieldcloud/core/staticfiles/js/vendor/jquery.js new file mode 100644 index 000000000..c4c6022f2 --- /dev/null +++ b/docker-app/qfieldcloud/core/staticfiles/js/vendor/jquery.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0=o.clientWidth&&n>=o.clientHeight}),l=0a[e]&&!t.escapeWithReference&&(n=Q(f[o],a[e]-('right'===e?f.width:f.height))),ae({},o,n)}};return l.forEach(function(e){var t=-1===['left','top'].indexOf(e)?'secondary':'primary';f=le({},f,m[t](e))}),e.offsets.popper=f,e},priority:['left','right','top','bottom'],padding:5,boundariesElement:'scrollParent'},keepTogether:{order:400,enabled:!0,fn:function(e){var t=e.offsets,o=t.popper,n=t.reference,i=e.placement.split('-')[0],r=Z,p=-1!==['top','bottom'].indexOf(i),s=p?'right':'bottom',d=p?'left':'top',a=p?'width':'height';return o[s]r(n[s])&&(e.offsets.popper[d]=r(n[s])),e}},arrow:{order:500,enabled:!0,fn:function(e,o){var n;if(!K(e.instance.modifiers,'arrow','keepTogether'))return e;var i=o.element;if('string'==typeof i){if(i=e.instance.popper.querySelector(i),!i)return e;}else if(!e.instance.popper.contains(i))return console.warn('WARNING: `arrow.element` must be child of its popper element!'),e;var r=e.placement.split('-')[0],p=e.offsets,s=p.popper,d=p.reference,a=-1!==['left','right'].indexOf(r),l=a?'height':'width',f=a?'Top':'Left',m=f.toLowerCase(),h=a?'left':'top',c=a?'bottom':'right',u=S(i)[l];d[c]-us[c]&&(e.offsets.popper[m]+=d[m]+u-s[c]),e.offsets.popper=g(e.offsets.popper);var b=d[m]+d[l]/2-u/2,w=t(e.instance.popper),y=parseFloat(w['margin'+f]),E=parseFloat(w['border'+f+'Width']),v=b-e.offsets.popper[m]-y-E;return v=ee(Q(s[l]-u,v),0),e.arrowElement=i,e.offsets.arrow=(n={},ae(n,m,$(v)),ae(n,h,''),n),e},element:'[x-arrow]'},flip:{order:600,enabled:!0,fn:function(e,t){if(W(e.instance.modifiers,'inner'))return e;if(e.flipped&&e.placement===e.originalPlacement)return e;var o=v(e.instance.popper,e.instance.reference,t.padding,t.boundariesElement,e.positionFixed),n=e.placement.split('-')[0],i=T(n),r=e.placement.split('-')[1]||'',p=[];switch(t.behavior){case ce.FLIP:p=[n,i];break;case ce.CLOCKWISE:p=G(n);break;case ce.COUNTERCLOCKWISE:p=G(n,!0);break;default:p=t.behavior;}return p.forEach(function(s,d){if(n!==s||p.length===d+1)return e;n=e.placement.split('-')[0],i=T(n);var a=e.offsets.popper,l=e.offsets.reference,f=Z,m='left'===n&&f(a.right)>f(l.left)||'right'===n&&f(a.left)f(l.top)||'bottom'===n&&f(a.top)f(o.right),g=f(a.top)f(o.bottom),b='left'===n&&h||'right'===n&&c||'top'===n&&g||'bottom'===n&&u,w=-1!==['top','bottom'].indexOf(n),y=!!t.flipVariations&&(w&&'start'===r&&h||w&&'end'===r&&c||!w&&'start'===r&&g||!w&&'end'===r&&u),E=!!t.flipVariationsByContent&&(w&&'start'===r&&c||w&&'end'===r&&h||!w&&'start'===r&&u||!w&&'end'===r&&g),v=y||E;(m||b||v)&&(e.flipped=!0,(m||b)&&(n=p[d+1]),v&&(r=z(r)),e.placement=n+(r?'-'+r:''),e.offsets.popper=le({},e.offsets.popper,C(e.instance.popper,e.offsets.reference,e.placement)),e=P(e.instance.modifiers,e,'flip'))}),e},behavior:'flip',padding:5,boundariesElement:'viewport',flipVariations:!1,flipVariationsByContent:!1},inner:{order:700,enabled:!1,fn:function(e){var t=e.placement,o=t.split('-')[0],n=e.offsets,i=n.popper,r=n.reference,p=-1!==['left','right'].indexOf(o),s=-1===['top','left'].indexOf(o);return i[p?'left':'top']=r[o]-(s?i[p?'width':'height']:0),e.placement=T(t),e.offsets.popper=g(i),e}},hide:{order:800,enabled:!0,fn:function(e){if(!K(e.instance.modifiers,'hide','preventOverflow'))return e;var t=e.offsets.reference,o=D(e.instance.modifiers,function(e){return'preventOverflow'===e.name}).boundaries;if(t.bottomo.right||t.top>o.bottom||t.rightwindow.devicePixelRatio||!fe),c='bottom'===o?'top':'bottom',g='right'===n?'left':'right',b=B('transform');if(d='bottom'==c?'HTML'===l.nodeName?-l.clientHeight+h.bottom:-f.height+h.bottom:h.top,s='right'==g?'HTML'===l.nodeName?-l.clientWidth+h.right:-f.width+h.right:h.left,a&&b)m[b]='translate3d('+s+'px, '+d+'px, 0)',m[c]=0,m[g]=0,m.willChange='transform';else{var w='bottom'==c?-1:1,y='right'==g?-1:1;m[c]=d*w,m[g]=s*y,m.willChange=c+', '+g}var E={"x-placement":e.placement};return e.attributes=le({},E,e.attributes),e.styles=le({},m,e.styles),e.arrowStyles=le({},e.offsets.arrow,e.arrowStyles),e},gpuAcceleration:!0,x:'bottom',y:'right'},applyStyle:{order:900,enabled:!0,fn:function(e){return V(e.instance.popper,e.styles),j(e.instance.popper,e.attributes),e.arrowElement&&Object.keys(e.arrowStyles).length&&V(e.arrowElement,e.arrowStyles),e},onLoad:function(e,t,o,n,i){var r=L(i,t,e,o.positionFixed),p=O(o.placement,r,t,e,o.modifiers.flip.boundariesElement,o.modifiers.flip.padding);return t.setAttribute('x-placement',p),V(t,{position:o.positionFixed?'fixed':'absolute'}),o},gpuAcceleration:void 0}}},ge}); diff --git a/docker-app/qfieldcloud/core/staticfiles/sso/microsoft.svg b/docker-app/qfieldcloud/core/staticfiles/sso/microsoft.svg new file mode 100644 index 000000000..e8f27e44f --- /dev/null +++ b/docker-app/qfieldcloud/core/staticfiles/sso/microsoft.svg @@ -0,0 +1 @@ +MS-SymbolLockup diff --git a/docker-app/qfieldcloud/core/templates/account/base.html b/docker-app/qfieldcloud/core/templates/account/base.html new file mode 100644 index 000000000..e80b53fff --- /dev/null +++ b/docker-app/qfieldcloud/core/templates/account/base.html @@ -0,0 +1,74 @@ +{% load i18n sri static %} + + + + + + + + + {% sri_static 'css/vendor.css' %} + {% sri_static 'css/qfieldcloud.css' %} + + + + {% block title %} + {% block title_contents %}{{ title }}{% endblock title_contents %} | {{ site_title|default:_('QFieldCloud') }} + {% endblock title %} + + + {% block extrahead %}{% endblock extrahead %} + + + + + + + + +
+
+
+
+
+

+ QFieldCloud +

+ + {% block content %}{% endblock content %} + +
+
+
+
+
+ + + +
+
+ +
+
+ + {% sri_static 'js/vendor/jquery.js' %} + {% sri_static 'js/vendor/popper.min.js' %} + {% sri_static 'js/vendor/bootstrap.min.js' %} + + {% block additional_js %}{% endblock additional_js %} + + diff --git a/docker-app/qfieldcloud/core/templates/account/email_confirm.html b/docker-app/qfieldcloud/core/templates/account/email_confirm.html new file mode 100644 index 000000000..acf5b87e3 --- /dev/null +++ b/docker-app/qfieldcloud/core/templates/account/email_confirm.html @@ -0,0 +1,49 @@ +{% extends 'account/base.html' %} +{% load i18n %} +{% load account %} + + +{% block title_contents %}{% trans 'Confirm Email' %}{% endblock %} + + +{% block content %} + +
+
+
{% trans 'Confirm Email' %}
+

+ + {% if confirmation %} + + {% user_display confirmation.email_address.user as user_display %} + {% blocktrans with confirmation.email_address.email as email %} + Please confirm that + {{ email }} + is an email address for user + {{ user_display }}. + {% endblocktrans %} + + {% else %} + + {% blocktrans %} + This email confirmation link expired or is invalid. + Please contact support to get your email confirmation done. + {% endblocktrans %} + + {% endif %} +

+ + {% if confirmation and confirmation.key %} +
+ {% csrf_token %} + +
+ +
+
+ {% endif %} + +
+
+ +{% endblock content %} diff --git a/docker-app/qfieldcloud/core/templates/account/login.html b/docker-app/qfieldcloud/core/templates/account/login.html new file mode 100644 index 000000000..de89feacf --- /dev/null +++ b/docker-app/qfieldcloud/core/templates/account/login.html @@ -0,0 +1,43 @@ +{% extends 'account/base.html' %} +{% load i18n %} +{% load bootstrap4 %} + +{% block title_contents %}{% trans 'Sign-In' %}{% endblock %} + +{% block extrahead %} + +{% endblock extrahead %} + + +{% block content %} + +
+
+
{% trans 'Sign-In' %}
+ +
+ {% csrf_token %} + + {% bootstrap_form form %} + + {% if redirect_field_value %} + + {% endif %} + +
+ {% trans 'Forgot Password?' %} + + {% if is_signup_open %} + {% trans 'Register' %} + {% endif %} + + +
+
+ +
+
+ +{% include "socialaccount/snippets/login.html" %} + +{% endblock content %} diff --git a/docker-app/qfieldcloud/core/templates/account/password_reset.html b/docker-app/qfieldcloud/core/templates/account/password_reset.html new file mode 100644 index 000000000..a132cce9c --- /dev/null +++ b/docker-app/qfieldcloud/core/templates/account/password_reset.html @@ -0,0 +1,40 @@ +{% extends 'account/base.html' %} +{% load i18n %} +{% load bootstrap4 %} + +{% block title_contents %}{% trans 'Reset Password' %}{% endblock %} + + +{% block content %} + +
+
+
{% trans 'Password Reset' %}
+

+ {% url 'account_login' as login_url %} + {% blocktrans with login_url=login_url %} + Forgot your password? + Enter your email address below, and we will send you an email allowing you to reset it. + If you happened to remember your old password, please go back to the sign-in form. + {% endblocktrans %} +

+ +
+ {% csrf_token %} + + {% bootstrap_form form %} + + {% if redirect_field_value %} + + {% endif %} + +
+ +
+
+ + +
+
+ +{% endblock content %} diff --git a/docker-app/qfieldcloud/core/templates/account/password_reset_done.html b/docker-app/qfieldcloud/core/templates/account/password_reset_done.html new file mode 100644 index 000000000..ae6f40ef7 --- /dev/null +++ b/docker-app/qfieldcloud/core/templates/account/password_reset_done.html @@ -0,0 +1,22 @@ +{% extends 'account/base.html' %} +{% load i18n %} +{% load bootstrap4 %} + +{% block title_contents %}{% trans 'Password Reset Email Sent' %}{% endblock %} + + +{% block content %} + +
+
+
{% trans 'Password Reset Email Sent' %}
+

+ {% url 'account_login' as login_url %} + {% blocktrans with login_url=login_url %} + We have sent you an email. Please contact us if you do not receive it within a few minutes. Proceed to the sign-in form. + {% endblocktrans %} +

+
+
+ +{% endblock content %} diff --git a/docker-app/qfieldcloud/core/templates/account/password_reset_from_key.html b/docker-app/qfieldcloud/core/templates/account/password_reset_from_key.html index 80278da06..ccabe199a 100644 --- a/docker-app/qfieldcloud/core/templates/account/password_reset_from_key.html +++ b/docker-app/qfieldcloud/core/templates/account/password_reset_from_key.html @@ -1,55 +1,56 @@ -{% extends 'admin/base_site.html' %} -{% load i18n static %} +{% extends 'account/base.html' %} +{% load i18n %} {% load bootstrap4 %} -{% block title %} - {% trans 'Change Password' %} -{% endblock %} +{% block title_contents %}{% trans 'Change Password' %}{% endblock %} -{% block content %} -
-
-
- {% if token_fail %} - {% trans 'Bad Token' %} - {% else %} - {% trans 'Change Password' %} - {% endif %} -
+{% block content %} +
+
+
{% if token_fail %} - + {% trans "Bad Token" %} + {% else %} + {% trans "Change Password" %} + {% endif %} +
+ {% if token_fail %} +

+ {% url 'account_reset_password' as passwd_reset_url %} + {% blocktrans %} + The password reset link was invalid, possibly because it has already been used. Please request a new password reset. + {% endblocktrans %} +

+ {% else %} + {% if form %}

- {% url 'account_reset_password' as passwd_reset_url %} - {% blocktrans %} - The password reset link was invalid, possibly because it has already been used. Please request a new password reset. + {% url 'account_login' as login_url %} + {% blocktrans with login_url=login_url %} + Please enter your newly chosen secure password in the form below. + If you happened to remember your old password, please go back to the sign-in form. {% endblocktrans %}

- {% else %} - {% if form %} -

- {% url 'account_login' as login_url %} - {% blocktrans with login_url=login_url %} - Please enter the newly chosen secure password in the form below.
- If you happened to remember the old password, please go back to the sign-in form. - {% endblocktrans %} -

-
- {% csrf_token %} - - {% bootstrap_form form %} - - -
- - {% else %} -

- {% trans 'Your password is now changed.' %} -

- {% endif %} +
+ {% csrf_token %} + {% bootstrap_form form %} + + {% if redirect_field_value %} + + {% endif %} + +
+ +
+
+ {% else %} +

{% trans 'Your password is now changed.' %}

{% endif %} -
+ {% endif %} +
-{% endblock %} +
+ +{% endblock content %} diff --git a/docker-app/qfieldcloud/core/templates/account/password_reset_from_key_done.html b/docker-app/qfieldcloud/core/templates/account/password_reset_from_key_done.html index ac6d046e7..2b1466719 100644 --- a/docker-app/qfieldcloud/core/templates/account/password_reset_from_key_done.html +++ b/docker-app/qfieldcloud/core/templates/account/password_reset_from_key_done.html @@ -1,20 +1,23 @@ -{% extends 'admin/base_site.html' %} -{% load i18n static %} +{% extends 'account/base.html' %} +{% load i18n %} +{% load bootstrap4 %} + +{% block title_contents %}{% trans 'Change Password' %}{% endblock %} -{% block title %} - {% trans 'Change Password Complete' %} -{% endblock %} {% block content %} -
-
-
{% trans 'Change Password' %}
-

- {% url 'account_login' as login_url %} - {% blocktrans with login_url=login_url %} - The password is now changed. Use the password on the sign-in form. - {% endblocktrans %} -

-
+ +
+
+
{% trans 'Change Password' %}
+

+ {% url 'account_login' as login_url %} + {% blocktrans with login_url=login_url %} + Your password is now changed. Use your new password on the sign-in form. + {% endblocktrans %} +

+
-{% endblock %} +
+ +{% endblock content %} diff --git a/docker-app/qfieldcloud/core/templates/account/signup.html b/docker-app/qfieldcloud/core/templates/account/signup.html new file mode 100644 index 000000000..629b90c37 --- /dev/null +++ b/docker-app/qfieldcloud/core/templates/account/signup.html @@ -0,0 +1,48 @@ +{% extends 'account/base.html' %} +{% load i18n %} +{% load bootstrap4 %} + +{% block title_contents %}{% trans 'Registration' %}{% endblock %} + + +{% block content %} + +
+
+
{% trans 'Registration' %}
+

+ {% url 'account_login' as login_url %} + {% blocktrans with login_url=login_url %} + Already have an account? Proceed to the sign-in form. + {% endblocktrans %} +

+
+
+
+ {% csrf_token %} + {% bootstrap_form form %} + + {% if redirect_field_value %} + + {% endif %} + +
+ +
+
+
+
+ +{% block additional_js %} + +{% endblock additional_js %} + +{% endblock content %} diff --git a/docker-app/qfieldcloud/core/templates/account/signup_closed.html b/docker-app/qfieldcloud/core/templates/account/signup_closed.html new file mode 100644 index 000000000..79faeacb8 --- /dev/null +++ b/docker-app/qfieldcloud/core/templates/account/signup_closed.html @@ -0,0 +1,32 @@ +{% extends 'account/base.html' %} +{% load i18n %} +{% load bootstrap4 %} + +{% block title_contents %}{% trans 'Registration (closed)' %}{% endblock %} + + +{% block content %} + +
+
+
{% trans 'Registration is closed' %}
+

+ {% blocktrans %} + We are sorry, but the registration is currently closed. + {% endblocktrans %} +

+

+ {% url 'account_login' as login_url %} + {% blocktrans with login_url=login_url %} + Already have an account? Then you can still sign in. + {% endblocktrans %} +

+

+ {% blocktrans %} + No account yet? Then please join the waiting list. + {% endblocktrans %} +

+
+
+ +{% endblock content %} diff --git a/docker-app/qfieldcloud/core/templates/account/verification_sent.html b/docker-app/qfieldcloud/core/templates/account/verification_sent.html new file mode 100644 index 000000000..959b8286f --- /dev/null +++ b/docker-app/qfieldcloud/core/templates/account/verification_sent.html @@ -0,0 +1,40 @@ +{% extends 'account/base.html' %} +{% load i18n %} + +{% block title_contents %}{% trans 'Verification Sent' %}{% endblock %} + +{% block content %} + +
+
+
{% trans 'Verification Email Sent' %}
+ +

+ {% if request.session.account_verified_email %} + {% blocktrans with email=request.session.account_verified_email %} + A verification email was sent to {{ email }}.
+ {% endblocktrans %} + {% else %} + {% comment %} + The following should actually never happen. + We make sure `account_verified_email` is in the session + With an assert in the view. + {% endcomment %} + {% trans 'A verification email was sent.' %} + {% endif %} +
+ {% trans 'Please confirm your email to access your QFieldCloud account.' %} +

+ +
+
+ {% csrf_token %} + + {% trans 'Sign In' %} +
+
+ +
+
+ +{% endblock content %} diff --git a/docker-app/qfieldcloud/core/templates/admin/base.html b/docker-app/qfieldcloud/core/templates/admin/base.html new file mode 100644 index 000000000..3aba79b8e --- /dev/null +++ b/docker-app/qfieldcloud/core/templates/admin/base.html @@ -0,0 +1,19 @@ +{% extends "admin/base.html" %} +{% load i18n jazzmin version %} + +{% block footer %} + {% if not is_popup %} +
+
+ {% trans 'Jazzmin version' %} {% get_jazzmin_version %} | + {% trans 'QFieldCloud version' %} {% get_qfieldcloud_version %} +
+ {% autoescape off %} + {% trans 'Copyright' %} © {% now 'Y' %} {{ jazzmin_settings.copyright }}. {% trans 'All rights reserved.' %} + {% endautoescape %} +
+ {% if jazzmin_settings.show_ui_builder %} + {% include 'jazzmin/includes/ui_builder_panel.html' %} + {% endif %} + {% endif %} +{% endblock %} diff --git a/docker-app/qfieldcloud/core/templates/axes/lockedout.html b/docker-app/qfieldcloud/core/templates/axes/lockedout.html new file mode 100644 index 000000000..f8aa92dc7 --- /dev/null +++ b/docker-app/qfieldcloud/core/templates/axes/lockedout.html @@ -0,0 +1,22 @@ +{% extends 'account/base.html' %} +{% load i18n %} +{% load bootstrap4 %} +{% load filters %} + +{% block title_contents %}{% trans 'Account locked' %}{% endblock %} + + +{% block content %} + +
+
+
{% trans 'Account Locked' %}
+

+ {% blocktrans with username=username timedelta=cooloff_timedelta|smooth_timedelta %} + Too many failed login attempts for {{ username }}! Please try again {{ timedelta }} after your last login attempt. + {% endblocktrans %} +

+
+
+ +{% endblock content %} diff --git a/docker-app/qfieldcloud/core/templatetags/filters.py b/docker-app/qfieldcloud/core/templatetags/filters.py index 54bcc7db9..c5cb50202 100644 --- a/docker-app/qfieldcloud/core/templatetags/filters.py +++ b/docker-app/qfieldcloud/core/templatetags/filters.py @@ -51,3 +51,31 @@ def filesize_number_format(value): value = "-%s" % value return avoid_wrapping(value) + + +@register.filter() +def smooth_timedelta(timedeltaobj): + """Convert a datetime.timedelta object into Days, Hours, Minutes, Seconds. + Inspired by: https://stackoverflow.com/a/46928226/1226137 + """ + secs = timedeltaobj.total_seconds() + time_str = "" + if secs > 86400: # 60sec * 60min * 24hrs + days = secs // 86400 + time_str += _("{} days").format(int(days)) + secs = secs - days * 86400 + + if secs > 3600: + hrs = secs // 3600 + time_str += _(" {} hours").format(int(hrs)) + secs = secs - hrs * 3600 + + if secs > 60: + mins = secs // 60 + time_str += _(" {} minutes").format(int(mins)) + secs = secs - mins * 60 + + if secs > 0: + time_str += _(" {} seconds").format(int(secs)) + + return time_str diff --git a/docker-app/qfieldcloud/core/templatetags/version.py b/docker-app/qfieldcloud/core/templatetags/version.py new file mode 100644 index 000000000..1bb7e82a8 --- /dev/null +++ b/docker-app/qfieldcloud/core/templatetags/version.py @@ -0,0 +1,12 @@ +from django import template +from django.conf import settings + +register = template.Library() + + +@register.simple_tag +def get_qfieldcloud_version() -> str: + """ + Get the QFieldCloud version + """ + return settings.SENTRY_RELEASE diff --git a/docker-app/qfieldcloud/core/tests/test_delta.py b/docker-app/qfieldcloud/core/tests/test_delta.py index 1b4e1592c..2b9ebc7a7 100644 --- a/docker-app/qfieldcloud/core/tests/test_delta.py +++ b/docker-app/qfieldcloud/core/tests/test_delta.py @@ -114,7 +114,7 @@ def tearDown(self): def fail(self, msg: str = "", job: Job | None = None) -> NoReturn: if job: - msg += f"\n\nJobType:{job.type}\n" + msg += f"\n\nJobType: {job.type}\n" msg += f"Output:\n================\n{job.output}\n================" if job.feedback: diff --git a/docker-app/qfieldcloud/core/tests/test_project.py b/docker-app/qfieldcloud/core/tests/test_project.py index 92789f447..0409c8b50 100644 --- a/docker-app/qfieldcloud/core/tests/test_project.py +++ b/docker-app/qfieldcloud/core/tests/test_project.py @@ -1,4 +1,7 @@ +import io import logging +import time +from io import StringIO from unittest.mock import patch from django.core.exceptions import ValidationError @@ -18,7 +21,7 @@ ) from qfieldcloud.subscription.models import Subscription -from .utils import set_subscription, setup_subscription_plans +from .utils import set_subscription, setup_subscription_plans, testdata_path logging.disable(logging.CRITICAL) @@ -804,3 +807,97 @@ def test_seamless_projects_fetching_with_public_ones(self): self.assertEqual(data[3]["owner"], "user1") self.assertEqual(data[3]["name"], "public_project_of_user1") self.assertTrue(data[3]["is_public"]) + + def test_restricted_data_last_updated_at_on_file_upload(self): + """Test that restricted_data_last_updated_at is updated when restricted files are uploaded.""" + + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token1.key) + + # Create a project + project = Project.objects.create(name="test_restricted_data", owner=self.user1) + + # Upload a non-restricted file (e.g. a `csv` file) + response = self.client.post( + f"/api/v1/files/{project.id}/data.csv/", + {"file": StringIO("c1,c2\r\nv1,v2")}, + format="multipart", + ) + self.assertTrue(status.is_success(response.status_code)) + + # Initially, `restricted_data_last_updated_at` should be None + self.assertIsNone(project.restricted_data_last_updated_at) + + # Upload a QGIS project file (restricted file) + response = self.client.post( + f"/api/v1/files/{project.id}/simple_bumblebees.qgs/", + {"file": io.FileIO(testdata_path("simple_bumblebees.qgs"), "rb")}, + format="multipart", + ) + self.assertTrue(status.is_success(response.status_code)) + + project.refresh_from_db() + # After uploading a restricted file, `restricted_data_last_updated_at` should be set + self.assertIsNotNone(project.restricted_data_last_updated_at) + first_update_time = project.restricted_data_last_updated_at + + # Upload a non-restricted file (e.g. a `csv` file) + response = self.client.post( + f"/api/v1/files/{project.id}/data.csv/", + {"file": StringIO("c1,c2\r\nv1,v2")}, + format="multipart", + ) + self.assertTrue(status.is_success(response.status_code)) + + project.refresh_from_db() + # After uploading a non-restricted file, `restricted_data_last_updated_at` should NOT change + self.assertEqual(project.restricted_data_last_updated_at, first_update_time) + + # Upload another restricted file (e.g. `.qml` file) + time.sleep(0.01) + response = self.client.post( + f"/api/v1/files/{project.id}/simple_bumblebees.qml/", + {"file": StringIO("import QtQuick")}, + format="multipart", + ) + self.assertTrue(status.is_success(response.status_code)) + + project.refresh_from_db() + # After uploading another restricted file, `restricted_data_last_updated_at` should be updated + self.assertIsNotNone(project.restricted_data_last_updated_at) + self.assertGreater(project.restricted_data_last_updated_at, first_update_time) + + def test_restricted_data_last_updated_at_on_file_delete(self): + """Test that restricted_data_last_updated_at is updated when restricted files are deleted.""" + + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token1.key) + + # Create a project + project = Project.objects.create( + name="test_restricted_data_delete", is_public=False, owner=self.user1 + ) + + # Upload restricted file (e.g. `.qgs` file) + response = self.client.post( + f"/api/v1/files/{project.id}/simple_bumblebees.qgs/", + {"file": io.FileIO(testdata_path("simple_bumblebees.qgs"), "rb")}, + format="multipart", + ) + self.assertTrue(status.is_success(response.status_code)) + + project.refresh_from_db() + first_update_time = project.restricted_data_last_updated_at + self.assertIsNotNone(first_update_time) + + # Wait a bit to ensure timestamp difference + time.sleep(0.01) + + # Delete the restricted file + response = self.client.delete( + f"/api/v1/files/{project.id}/simple_bumblebees.qgs/" + ) + self.assertTrue(status.is_success(response.status_code)) + + project.refresh_from_db() + # After deleting a restricted file, `restricted_data_last_updated_at` should be updated + self.assertIsNotNone(project.restricted_data_last_updated_at) + self.assertGreater(project.restricted_data_last_updated_at, first_update_time) diff --git a/docker-app/qfieldcloud/core/urls.py b/docker-app/qfieldcloud/core/urls.py index fd5bc4cf8..0e5c21d71 100644 --- a/docker-app/qfieldcloud/core/urls.py +++ b/docker-app/qfieldcloud/core/urls.py @@ -45,6 +45,12 @@ *filestorage_urlpatterns, path("projects/public/", projects_views.PublicProjectsListView.as_view()), path("", include(router.urls)), + # Add project overview URL pattern for get_absolute_url compatibility + path( + "projects///", + projects_views.ProjectViewSet.as_view({"get": "retrieve"}), + name="project_overview", + ), path("users/", users_views.ListUsersView.as_view()), path( "users//organizations/", diff --git a/docker-app/qfieldcloud/core/utils.py b/docker-app/qfieldcloud/core/utils.py index 94d04b576..b23791f4c 100644 --- a/docker-app/qfieldcloud/core/utils.py +++ b/docker-app/qfieldcloud/core/utils.py @@ -329,6 +329,29 @@ def get_qgis_project_file(project_id: str) -> str | None: return None +def check_legacy_s3_file_exists(key: str, should_raise: bool = True) -> bool: + """Check to see if an object exists on S3. + + Todo: + * Delete with QF-4963 Drop support for legacy storage + """ + client = get_s3_client() + try: + client.head_object( + Bucket=get_legacy_s3_credentials()["OPTIONS"]["bucket_name"], + Key=key, + ) + return True + except ClientError as e: + if e.response.get("ResponseMetadata", {}).get("HTTPStatusCode") == 404: + return False + else: + if should_raise: + raise e + else: + return False + + def check_s3_key(key: str) -> str | None: """Check to see if an object exists on S3. It it exists, the function returns the sha256 of the file from the metadata diff --git a/docker-app/qfieldcloud/core/utils2/storage.py b/docker-app/qfieldcloud/core/utils2/storage.py index 785772608..afb3fdbaa 100644 --- a/docker-app/qfieldcloud/core/utils2/storage.py +++ b/docker-app/qfieldcloud/core/utils2/storage.py @@ -19,12 +19,14 @@ from django.db import transaction from django.http import FileResponse, HttpRequest from django.http.response import HttpResponse, HttpResponseBase +from django.utils import timezone from mypy_boto3_s3.type_defs import ObjectIdentifierTypeDef import qfieldcloud.core.models import qfieldcloud.core.utils from qfieldcloud.core.utils2.audit import LogEntry, audit from qfieldcloud.filestorage.backend import QfcS3Boto3Storage +from qfieldcloud.filestorage.utils import is_admin_restricted_file logger = logging.getLogger(__name__) @@ -641,18 +643,20 @@ def delete_project_file_permanently( with transaction.atomic(): _delete_by_key_permanently(file.latest.key) - update_fields = ["file_storage_bytes"] + update_fields = ["data_last_updated_at"] + + now = timezone.now() + project.data_last_updated_at = now + + if is_admin_restricted_file(filename, project.the_qgis_file_name): + update_fields.append("restricted_data_last_updated_at") + project.restricted_data_last_updated_at = now if qfieldcloud.core.utils.is_the_qgis_file(filename): update_fields.append("the_qgis_file_name") project.the_qgis_file_name = None - file_storage_bytes = project.file_storage_bytes - sum( - [v.size for v in file.versions] - ) - project.file_storage_bytes = max(file_storage_bytes, 0) - - project.save(update_fields=update_fields) + project.save(update_fields=update_fields, recompute_storage=True) # NOTE force audits to be required when deleting files audit( @@ -722,6 +726,7 @@ def delete_project_file_version_permanently( versions_to_delete.append(file_version) with transaction.atomic(): + update_fields = ["data_last_updated_at"] for file_version in versions_to_delete: if ( not re.match( @@ -744,7 +749,14 @@ def delete_project_file_version_permanently( delete_version_permanently(file_version) - project.save(recompute_storage=True) + now = timezone.now() + project.data_last_updated_at = now + + if is_admin_restricted_file(filename, project.the_qgis_file_name): + update_fields.append("restricted_data_last_updated_at") + project.restricted_data_last_updated_at = now + + project.save(update_fields=update_fields, recompute_storage=True) return versions_to_delete diff --git a/docker-app/qfieldcloud/core/views/files_views.py b/docker-app/qfieldcloud/core/views/files_views.py index 81afaf872..17e41654a 100644 --- a/docker-app/qfieldcloud/core/views/files_views.py +++ b/docker-app/qfieldcloud/core/views/files_views.py @@ -30,6 +30,7 @@ get_attachment_dir_prefix, purge_old_file_versions_legacy, ) +from qfieldcloud.filestorage.utils import is_admin_restricted_file from rest_framework import permissions, serializers, status, views from rest_framework.exceptions import NotFound from rest_framework.parsers import DataAndFiles, MultiPartParser @@ -314,7 +315,7 @@ def post(self, request, projectid, filename, format=None): # project and update it now, it guarantees there will be no other file upload editing # the same project row. project = Project.objects.select_for_update().get(id=projectid) - update_fields = ["data_last_updated_at", "file_storage_bytes"] + update_fields = ["data_last_updated_at"] if get_attachment_dir_prefix(project, filename) == "" and ( is_the_qgis_file or project.has_the_qgis_file @@ -338,10 +339,14 @@ def post(self, request, projectid, filename, format=None): project=project, created_by=self.request.user ) - project.data_last_updated_at = timezone.now() - # NOTE just incrementing the fils_storage_bytes when uploading might make the database out of sync if a files is uploaded/deleted bypassing this function - project.file_storage_bytes += request_file.size - project.save(update_fields=update_fields) + now = timezone.now() + project.data_last_updated_at = now + + if is_admin_restricted_file(filename, project.the_qgis_file_name): + update_fields.append("restricted_data_last_updated_at") + project.restricted_data_last_updated_at = now + + project.save(update_fields=update_fields, recompute_storage=True) if old_object: audit( diff --git a/docker-app/qfieldcloud/core/views/redirect_views.py b/docker-app/qfieldcloud/core/views/redirect_views.py index 336db060b..54c827f4c 100644 --- a/docker-app/qfieldcloud/core/views/redirect_views.py +++ b/docker-app/qfieldcloud/core/views/redirect_views.py @@ -21,3 +21,15 @@ def redirect_to_admin_project_view( raise Http404() return redirect(reverse("admin:core_project_change", args=(project.id,))) + + +def redirect_to_project_api_view( + request: HttpRequest, username: str, project: str +) -> HttpResponseRedirect | HttpResponsePermanentRedirect: + """Redirect to the API endpoint for project overview. + + This provides a clean URL for Django admin's 'view on site' functionality + while redirecting to the actual API endpoint. + """ + api_url = f"/api/v1/projects/{username}/{project}/" + return redirect(api_url) diff --git a/docker-app/qfieldcloud/filestorage/helpers.py b/docker-app/qfieldcloud/filestorage/helpers.py index e3ce2ef16..d857ac619 100644 --- a/docker-app/qfieldcloud/filestorage/helpers.py +++ b/docker-app/qfieldcloud/filestorage/helpers.py @@ -21,7 +21,6 @@ def purge_old_file_versions(project: Project) -> None: logger.info(f"Cleaning up old files for {project} to {keep_count} versions") versions_to_delete_ids = [] - versions_to_delete_size = 0 for file in project.project_files: versions_to_delete = file.versions.order_by("-created_at")[keep_count:] @@ -31,14 +30,12 @@ def purge_old_file_versions(project: Project) -> None: for file_version in versions_to_delete: versions_to_delete_ids.append(file_version.id) - versions_to_delete_size += file_version.size if not versions_to_delete_ids: return with transaction.atomic(): FileVersion.objects.filter(id__in=versions_to_delete_ids).delete() - - project = Project.objects.select_for_update().get(id=project.id) - project.file_storage_bytes -= versions_to_delete_size - project.save(update_fields=["file_storage_bytes"]) + Project.objects.select_for_update().get(id=project.id).save( + recompute_storage=True + ) diff --git a/docker-app/qfieldcloud/filestorage/migrate_project_storage.py b/docker-app/qfieldcloud/filestorage/migrate_project_storage.py index 9fd55a155..bdf4a298e 100644 --- a/docker-app/qfieldcloud/filestorage/migrate_project_storage.py +++ b/docker-app/qfieldcloud/filestorage/migrate_project_storage.py @@ -8,7 +8,7 @@ from qfieldcloud.core.models import Job, Project from qfieldcloud.core.utils import ( - check_s3_key, + check_legacy_s3_file_exists, get_project_files_with_versions, get_project_package_files, ) @@ -240,7 +240,7 @@ def migrate_project_storage( f'Migrate project "{project.name}" ({str(project.id)}) thumbnail "{project.legacy_thumbnail_uri}"...' ) - if not check_s3_key(project.legacy_thumbnail_uri): + if not check_legacy_s3_file_exists(project.legacy_thumbnail_uri): logger.warning( f"Thumbnail '{project.legacy_thumbnail_uri}' does not exist in legacy storage, skip thumbnail migration!" ) diff --git a/docker-app/qfieldcloud/filestorage/view_helpers.py b/docker-app/qfieldcloud/filestorage/view_helpers.py index 2cbc3626a..6d88c5558 100644 --- a/docker-app/qfieldcloud/filestorage/view_helpers.py +++ b/docker-app/qfieldcloud/filestorage/view_helpers.py @@ -125,10 +125,10 @@ def upload_project_file_version( ) if file_type == File.FileType.PROJECT_FILE: - # Select for update the project so we can update it, especially the `file_storage_bytes` bit. + # Select for update the project so we can update it. # It guarantees there will be no other file upload editing the same project row. project = Project.objects.select_for_update().get(id=project.id) - update_fields = ["data_last_updated_at", "file_storage_bytes"] + update_fields = ["data_last_updated_at"] if get_attachment_dir_prefix(project, filename) == "" and ( is_qgis_file or project.the_qgis_file_name is not None @@ -152,9 +152,14 @@ def upload_project_file_version( project=project, created_by=request.user ) - project.data_last_updated_at = timezone.now() - project.file_storage_bytes += file_version.size - project.save(update_fields=update_fields) + now = timezone.now() + project.data_last_updated_at = now + + if is_admin_restricted_file(filename, project.the_qgis_file_name): + update_fields.append("restricted_data_last_updated_at") + project.restricted_data_last_updated_at = now + + project.save(update_fields=update_fields, recompute_storage=True) elif file_type == File.FileType.PACKAGE_FILE: # nothing to do when we upload a package file pass @@ -352,7 +357,6 @@ def delete_project_file_version( NotFound: Raised when the requested file version is not found. """ version_id = request.GET.get("version", request.headers.get("x-file-version")) - bytes_to_delete = 0 try: if version_id: @@ -368,14 +372,12 @@ def delete_project_file_version( ) object_to_delete = file_versions_qs.get(id=version_id) - bytes_to_delete = object_to_delete.size else: object_to_delete = File.objects.get( project_id=project_id, name=filename, file_type=File.FileType.PROJECT_FILE, ) - bytes_to_delete = object_to_delete.get_total_versions_size() except File.DoesNotExist: raise NotFound( detail=f"The requested {filename=} for {project_id=} does not exist!" @@ -389,7 +391,14 @@ def delete_project_file_version( with transaction.atomic(): project = Project.objects.select_for_update().get(id=project_id) - update_fields = ["file_storage_bytes"] + update_fields = ["data_last_updated_at"] + + now = timezone.now() + project.data_last_updated_at = now + + if is_admin_restricted_file(filename, project.the_qgis_file_name): + update_fields.append("restricted_data_last_updated_at") + project.restricted_data_last_updated_at = now if ( is_qgis_project_file(filename) @@ -401,6 +410,4 @@ def delete_project_file_version( project.the_qgis_file_name = None update_fields.append("the_qgis_file_name") - project.file_storage_bytes -= bytes_to_delete - - project.save(update_fields=update_fields) + project.save(update_fields=update_fields, recompute_storage=True) diff --git a/docker-app/qfieldcloud/settings.py b/docker-app/qfieldcloud/settings.py index dcf01a788..335ec01ed 100644 --- a/docker-app/qfieldcloud/settings.py +++ b/docker-app/qfieldcloud/settings.py @@ -60,7 +60,15 @@ # For example: 'DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]' ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(" ") -# A tuple representing an HTTP header/value combination that signifies a request is secure, which is important for Django’s CSRF protection. +# CSRF trusted origins for secure form submissions +# Format: 'https://domain1.com https://domain2.com' +CSRF_TRUSTED_ORIGINS = [ + origin.strip() + for origin in os.environ.get("CSRF_TRUSTED_ORIGINS", "").split(" ") + if origin.strip() +] + +# A tuple representing an HTTP header/value combination that signifies a request is secure, which is important for Django's CSRF protection. # We need to set it in QFieldCloud as we run behind a proxy. # Read more: https://docs.djangoproject.com/en/4.2/ref/settings/#secure-proxy-ssl-header SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") @@ -136,6 +144,8 @@ "constance", "django_extensions", "bootstrap4", + "sri", + # To ensure that exceptions inside other apps' signal handlers do not affect the integrity of file deletions within transactions, `django_cleanup` should be placed last in `INSTALLED_APPS`. See https://github.com/un1t/django-cleanup#configuration "django_cleanup.apps.CleanupConfig", ] @@ -187,6 +197,7 @@ "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", + "qfieldcloud.core.context_processors.signup_open", ], }, }, @@ -265,6 +276,12 @@ os.path.join(BASE_DIR, "qfieldcloud", "core", "staticfiles"), ] +BOOTSTRAP4 = { + "success_css_class": " ", + "bound_css_class": " ", + "required_css_class": "required", +} + MEDIA_URL = "/mediafiles/" MEDIA_ROOT = os.path.join(BASE_DIR, "mediafiles") @@ -344,6 +361,7 @@ # Sentry configuration SENTRY_DSN = os.environ.get("SENTRY_DSN", "") +SENTRY_RELEASE = os.environ.get("SENTRY_RELEASE", "dev") if SENTRY_DSN: SENTRY_SAMPLE_RATE = float(os.environ.get("SENTRY_SAMPLE_RATE", 1)) @@ -529,11 +547,29 @@ def before_send(event, hint): "color_text": "#E3E3E3", }, }, + "entra": { + # https://learn.microsoft.com/en-us/entra/identity-platform/howto-add-branding-in-apps + "required": True, + "light": { + "logo": "sso/microsoft.svg", + "color_fill": "#FFFFFF", + "color_stroke": "#8C8C8C", + "color_text": "#5E5E5E", + }, + "dark": { + "logo": "sso/microsoft.svg", + "color_fill": "#2F2F2F", + "color_stroke": "#2F2F2F", + "color_text": "#FFFFFF", + }, + }, } # Django axes configuration # https://django-axes.readthedocs.io/en/latest/4_configuration.html ########################### +# Template for the page shown when the user is locked out of their account for too many failed attempts. +AXES_LOCKOUT_TEMPLATE = "axes/lockedout.html" # The integer number of login attempts allowed before a record is created for the failed logins. Default: 3 AXES_FAILURE_LIMIT = 5 # Configures the limiter to handle username only (see https://django-axes.readthedocs.io/en/latest/2_installation.html#version-7-breaking-changes-and-upgrading-from-django-axes-version-6) @@ -609,6 +645,13 @@ def before_send(event, hint): QFIELDCLOUD_TOKEN_SERIALIZER = "qfieldcloud.core.serializers.TokenSerializer" QFIELDCLOUD_USER_SERIALIZER = "qfieldcloud.core.serializers.CompleteUserSerializer" +# Worker backend configuration +QFIELDCLOUD_WORKER_BACKEND = os.environ.get("QFIELDCLOUD_WORKER_BACKEND", "docker") +QFIELDCLOUD_K8S_NAMESPACE = os.environ.get("QFIELDCLOUD_K8S_NAMESPACE", "default") +QFIELDCLOUD_K8S_SERVICE_ACCOUNT = os.environ.get( + "QFIELDCLOUD_K8S_SERVICE_ACCOUNT", "default" +) + # Admin URLS which will be skipped from checking if they return HTTP 200 QFIELDCLOUD_TEST_SKIP_VIEW_ADMIN_URLS = ( "/admin/login/", @@ -732,7 +775,7 @@ def before_send(event, hint): # URL the qgis worker will use to access the running API endpoint on the app service QFIELDCLOUD_WORKER_QFIELDCLOUD_URL = os.environ["QFIELDCLOUD_WORKER_QFIELDCLOUD_URL"] -# Host path which will be mounted by the `worker_wrapper` into the `worker` containers to facilitate development and debugging pythons files. +# Host path which will be mounted by the `worker_wrapper` into the `worker` containers to facilitate development and debugging python files. DEBUG_QGIS_WORKER_HOST_PATH = os.environ.get("DEBUG_QGIS_WORKER_HOST_PATH") # Port to be used by `debugpy` to connect to the QGIS process inside the `qgis` container @@ -747,6 +790,7 @@ def before_send(event, hint): QFIELDCLOUD_DEFAULT_NETWORK = os.environ.get("QFIELDCLOUD_DEFAULT_NETWORK") # `django-auditlog` configurations, read more on https://django-auditlog.readthedocs.io/en/latest/usage.html +AUDITLOG_LOGENTRY_MODEL = "auditlog.LogEntry" AUDITLOG_INCLUDE_TRACKING_MODELS = [ # NOTE `Delta` and `Job` models are not being automatically audited, because their data changes very often and timestamps are available in their models. { diff --git a/docker-app/qfieldcloud/settings_utils.py b/docker-app/qfieldcloud/settings_utils.py index 6b6a510da..f96ddae08 100644 --- a/docker-app/qfieldcloud/settings_utils.py +++ b/docker-app/qfieldcloud/settings_utils.py @@ -43,15 +43,20 @@ def get_storages_config() -> StoragesConfig: "Envvar STORAGES should be a parsable JSON string!" ) else: + storage_options = { + "access_key": os.environ["STORAGE_ACCESS_KEY_ID"], + "secret_key": os.environ["STORAGE_SECRET_ACCESS_KEY"], + "bucket_name": os.environ["STORAGE_BUCKET_NAME"], + "endpoint_url": os.environ["STORAGE_ENDPOINT_URL"], + } + + # Only add region_name if it's set and not empty + if os.environ.get("STORAGE_REGION_NAME"): + storage_options["region_name"] = os.environ["STORAGE_REGION_NAME"] + raw_storages["default"] = { "BACKEND": "qfieldcloud.filestorage.backend.QfcS3Boto3Storage", - "OPTIONS": { - "access_key": os.environ["STORAGE_ACCESS_KEY_ID"], - "secret_key": os.environ["STORAGE_SECRET_ACCESS_KEY"], - "bucket_name": os.environ["STORAGE_BUCKET_NAME"], - "region_name": os.environ["STORAGE_REGION_NAME"], - "endpoint_url": os.environ["STORAGE_ENDPOINT_URL"], - }, + "OPTIONS": storage_options, "QFC_IS_LEGACY": True, } diff --git a/docker-app/qfieldcloud/subscription/migrations/0007_remove_plan_is_metered.py b/docker-app/qfieldcloud/subscription/migrations/0007_remove_plan_is_metered.py new file mode 100644 index 000000000..ee6d89996 --- /dev/null +++ b/docker-app/qfieldcloud/subscription/migrations/0007_remove_plan_is_metered.py @@ -0,0 +1,16 @@ +# Generated by Django 4.2.25 on 2025-11-10 11:13 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("subscription", "0006_auto_20230426_2222"), + ] + + operations = [ + migrations.RemoveField( + model_name="plan", + name="is_metered", + ), + ] diff --git a/docker-app/qfieldcloud/subscription/models.py b/docker-app/qfieldcloud/subscription/models.py index 256444835..69e64beb0 100644 --- a/docker-app/qfieldcloud/subscription/models.py +++ b/docker-app/qfieldcloud/subscription/models.py @@ -152,9 +152,6 @@ def get_plans_for_user(cls, user: User, user_type: User.Type) -> QuerySet["Plan" # the plan is set as trial is_trial = models.BooleanField(default=False) - # the plan is metered or licensed. If it metered, it is automatically post-paid. - is_metered = models.BooleanField(default=False) - # the plan is cancellable. If it True, the plan cannot be cancelled. is_cancellable = models.BooleanField(default=True) diff --git a/docker-app/qfieldcloud/urls.py b/docker-app/qfieldcloud/urls.py index 4eaed75cf..03d38a918 100644 --- a/docker-app/qfieldcloud/urls.py +++ b/docker-app/qfieldcloud/urls.py @@ -33,7 +33,10 @@ from qfieldcloud.authentication import views as auth_views from qfieldcloud.core.admin import qfc_admin_site -from qfieldcloud.core.views.redirect_views import redirect_to_admin_project_view +from qfieldcloud.core.views.redirect_views import ( + redirect_to_admin_project_view, + redirect_to_project_api_view, +) from qfieldcloud.filestorage.views import ( compatibility_file_crud_view, compatibility_file_list_view, @@ -100,9 +103,31 @@ def wrapper(request, *args, **kwargs): path("api/v1/auth/providers/", auth_views.ListProvidersView.as_view()), path("api/v1/auth/logout/", auth_views.LogoutView.as_view()), path("api/v1/", include("qfieldcloud.core.urls")), + path( + "auth/login/", + RedirectView.as_view( + url="/accounts/login/", + query_string=True, + permanent=False, + ), + ), + path( + "auth/logout/", + RedirectView.as_view( + url="/accounts/logout/", + query_string=True, + permanent=False, + ), + ), path("auth/", include("rest_framework.urls")), path("accounts/", include("allauth.urls")), path("invitations/", include("invitations.urls", namespace="invitations")), path("__debug__/", include("debug_toolbar.urls")), path("a///", redirect_to_admin_project_view), + # Add project overview URL pattern for admin "view on site" functionality + path( + "projects///", + redirect_to_project_api_view, + name="project_overview", + ), ] diff --git a/docker-app/requirements/requirements.in b/docker-app/requirements/requirements.in index 04019d3a4..feac03ed2 100644 --- a/docker-app/requirements/requirements.in +++ b/docker-app/requirements/requirements.in @@ -2,7 +2,8 @@ boto3-stubs==1.35.90 boto3==1.35.90 deprecated==1.2.18 django-allauth[socialaccount]==65.11.2 -django-auditlog==3.2.1 +# TODO: Switch this back to regular version once django-auditlog >3.3.0 is released +django-auditlog @ git+https://github.com/jazzband/django-auditlog.git@d02ed6b9 django-axes==8.0.0 django-bootstrap4==25.2 django-classy-tags==4.1.0 @@ -29,17 +30,17 @@ django-sri==0.8.0 django-storages==1.14.6 django-tables2==2.7.5 django-timezone-field==7.1 -django==4.2.25 +django==4.2.26 django-stubs==5.2.7 django-stubs-ext==5.2.7 djangorestframework==3.16.1 djangorestframework-stubs==3.16.4 -drf-spectacular==0.28.0 +drf-spectacular==0.29.0 json-log-formatter==1.1.1 mypy-boto3-s3==1.35.81 phonenumbers==9.0.15 Pillow==11.3.0 psycopg2==2.9.11 pymemcache==4.0.0 -sentry-sdk==2.42.0 +sentry-sdk==2.44.0 stripe==4.2.0 diff --git a/docker-app/requirements/requirements.txt b/docker-app/requirements/requirements.txt index 8be301ba5..7fdc5d320 100644 --- a/docker-app/requirements/requirements.txt +++ b/docker-app/requirements/requirements.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.10 # by the following command: # -# pip-compile --output-file=/requirements/requirements.txt /requirements/requirements.in +# pip-compile --no-strip-extras --output-file=/requirements/requirements.txt /requirements/requirements.in # asgiref==3.9.1 # via @@ -40,7 +40,7 @@ cryptography==45.0.6 # pyjwt deprecated==1.2.18 # via -r /requirements/requirements.in -django==4.2.25 +django==4.2.26 # via # -r /requirements/requirements.in # django-allauth @@ -73,7 +73,7 @@ django==4.2.25 # jsonfield django-allauth[socialaccount]==65.11.2 # via -r /requirements/requirements.in -django-auditlog==3.2.1 +django-auditlog @ git+https://github.com/jazzband/django-auditlog.git@d02ed6b9 # via -r /requirements/requirements.in django-axes==8.0.0 # via -r /requirements/requirements.in @@ -141,7 +141,7 @@ djangorestframework==3.16.1 # drf-spectacular djangorestframework-stubs==3.16.4 # via -r /requirements/requirements.in -drf-spectacular==0.28.0 +drf-spectacular==0.29.0 # via -r /requirements/requirements.in idna==3.10 # via requests @@ -198,7 +198,7 @@ rpds-py==0.27.0 # referencing s3transfer==0.10.4 # via boto3 -sentry-sdk==2.42.0 +sentry-sdk==2.44.0 # via -r /requirements/requirements.in six==1.17.0 # via python-dateutil diff --git a/docker-app/requirements/requirements_k8s_wrapper.in b/docker-app/requirements/requirements_k8s_wrapper.in new file mode 100644 index 000000000..b8f18c40b --- /dev/null +++ b/docker-app/requirements/requirements_k8s_wrapper.in @@ -0,0 +1,4 @@ +# Kubernetes-specific requirements for worker wrapper +kubernetes>=29.0.0 +requests>=2.32.0 +tenacity>=9.0.0 diff --git a/docker-app/requirements/requirements_worker_wrapper.in b/docker-app/requirements/requirements_worker_wrapper.in index a98ed840b..b2775ed98 100644 --- a/docker-app/requirements/requirements_worker_wrapper.in +++ b/docker-app/requirements/requirements_worker_wrapper.in @@ -1,2 +1,3 @@ docker==7.1.0 -tenacity==9.1.2 +kubernetes>=34.1.0 +tenacity>=9.1.2 diff --git a/docker-app/requirements/requirements_worker_wrapper.txt b/docker-app/requirements/requirements_worker_wrapper.txt index 977899a3f..8f78fc09e 100644 --- a/docker-app/requirements/requirements_worker_wrapper.txt +++ b/docker-app/requirements/requirements_worker_wrapper.txt @@ -5,18 +5,40 @@ # pip-compile --output-file=/requirements/requirements_worker_wrapper.txt /requirements/requirements_worker_wrapper.in # certifi==2025.8.3 - # via requests + # via + # kubernetes + # requests charset-normalizer==3.4.3 # via requests docker==7.1.0 # via -r /requirements/requirements_worker_wrapper.in idna==3.10 # via requests +kubernetes==34.1.0 + # via -r /requirements/requirements_worker_wrapper.in +oauthlib==3.2.2 + # via requests-oauthlib +python-dateutil==2.9.0.post0 + # via kubernetes +pyyaml==6.0.2 + # via kubernetes requests==2.32.5 - # via docker + # via + # docker + # kubernetes + # requests-oauthlib +requests-oauthlib==2.0.0 + # via kubernetes +six==1.17.0 + # via + # kubernetes + # python-dateutil tenacity==9.1.2 # via -r /requirements/requirements_worker_wrapper.in -urllib3==2.5.0 +urllib3==2.3.0 # via # docker + # kubernetes # requests +websocket-client==1.8.0 + # via kubernetes diff --git a/docker-app/worker_wrapper/check_dependencies.py b/docker-app/worker_wrapper/check_dependencies.py new file mode 100644 index 000000000..87c86eec3 --- /dev/null +++ b/docker-app/worker_wrapper/check_dependencies.py @@ -0,0 +1,51 @@ +""" +Script to check if all required dependencies are available for the selected backend +""" + +import sys +import importlib.util + + +def check_backend_dependencies(): + """Check if dependencies for the configured backend are available""" + backend = "docker" # Default + + try: + from django.conf import settings + + backend = getattr(settings, "QFIELDCLOUD_WORKER_BACKEND", "docker") + except Exception: + # Not in Django context, check environment + import os + + backend = os.environ.get("QFIELDCLOUD_WORKER_BACKEND", "docker") + + missing_deps = [] + + if backend in ["kubernetes", "k8s"]: + # Check Kubernetes dependencies + if importlib.util.find_spec("kubernetes") is None: + missing_deps.append("kubernetes>=29.0.0") + else: + # Check Docker dependencies + if importlib.util.find_spec("docker") is None: + missing_deps.append("docker>=7.1.0") + + # Common dependencies + if importlib.util.find_spec("tenacity") is None: + missing_deps.append("tenacity>=9.1.2") + + if missing_deps: + print(f"Missing dependencies for {backend} backend:") + for dep in missing_deps: + print(f" - {dep}") + print(f"\nInstall with: pip install {' '.join(missing_deps)}") + return False + + print(f"✅ All dependencies available for {backend} backend") + return True + + +if __name__ == "__main__": + if not check_backend_dependencies(): + sys.exit(1) diff --git a/docker-app/worker_wrapper/factory.py b/docker-app/worker_wrapper/factory.py new file mode 100644 index 000000000..379087387 --- /dev/null +++ b/docker-app/worker_wrapper/factory.py @@ -0,0 +1,113 @@ +""" +Worker factory for choosing between Docker and Kubernetes implementations +""" + +from django.conf import settings + + +def get_worker_backend(): + """Get the configured worker backend""" + backend = getattr(settings, 'QFIELDCLOUD_WORKER_BACKEND', 'docker') + + # If docker backend is selected but docker module is not available, + # fallback to kubernetes if available + if backend == 'docker': + try: + import docker + except ImportError: + try: + import kubernetes + backend = 'kubernetes' + # Log the fallback for debugging + import logging + logger = logging.getLogger(__name__) + logger.info("Docker module not available, falling back to Kubernetes backend") + except ImportError: + raise ImportError( + "Neither docker nor kubernetes Python modules are available. " + "Please install one of them or set QFIELDCLOUD_WORKER_BACKEND appropriately." + ) + + return backend + + +def create_job_run(job_id: str): + """Factory function to create the appropriate JobRun instance""" + backend = get_worker_backend() + + if backend in ["kubernetes", "k8s"]: + from .k8s_wrapper import K8sJobRun + return K8sJobRun(job_id) + else: + # Only import docker wrapper when explicitly needed + from .wrapper import JobRun + return JobRun(job_id) + + +def create_package_job_run(job_id: str): + """Factory function to create the appropriate PackageJobRun instance""" + backend = get_worker_backend() + + if backend in ["kubernetes", "k8s"]: + from .k8s_wrapper import K8sPackageJobRun + return K8sPackageJobRun(job_id) + else: + from .wrapper import PackageJobRun + return PackageJobRun(job_id) + + +def create_apply_delta_job_run(job_id: str): + """Factory function to create the appropriate ApplyDeltaJobRun instance""" + backend = get_worker_backend() + + if backend in ["kubernetes", "k8s"]: + from .k8s_wrapper import K8sApplyDeltaJobRun + return K8sApplyDeltaJobRun(job_id) + else: + from .wrapper import ApplyDeltaJobRun + return ApplyDeltaJobRun(job_id) + + +def create_process_projectfile_job_run(job_id: str): + """Factory function to create the appropriate ProcessProjectfileJobRun instance""" + backend = get_worker_backend() + + if backend in ["kubernetes", "k8s"]: + from .k8s_wrapper import K8sProcessProjectfileJobRun + return K8sProcessProjectfileJobRun(job_id) + else: + from .wrapper import ProcessProjectfileJobRun + return ProcessProjectfileJobRun(job_id) + + +def cancel_orphaned_workers(): + """Cancel orphaned workers using the appropriate backend""" + backend = get_worker_backend() + + if backend in ["kubernetes", "k8s"]: + from .k8s_wrapper import cancel_orphaned_k8s_workers + return cancel_orphaned_k8s_workers() + else: + from .wrapper import cancel_orphaned_workers as cancel_docker_workers + return cancel_docker_workers() + + +# For backwards compatibility, expose the factory functions as classes +class JobRun: + def __new__(cls, job_id: str): + return create_job_run(job_id) + + +class PackageJobRun: + def __new__(cls, job_id: str): + return create_package_job_run(job_id) + + +class ApplyDeltaJobRun: + def __new__(cls, job_id: str): + return create_apply_delta_job_run(job_id) + + +class ProcessProjectfileJobRun: + def __new__(cls, job_id: str): + return create_process_projectfile_job_run(job_id) diff --git a/docker-app/worker_wrapper/k8s_wrapper.py b/docker-app/worker_wrapper/k8s_wrapper.py new file mode 100644 index 000000000..b154c211a --- /dev/null +++ b/docker-app/worker_wrapper/k8s_wrapper.py @@ -0,0 +1,930 @@ +import json +import logging +import os +import shutil +import sys +import tempfile +import traceback +import uuid +from datetime import timedelta +from pathlib import Path +from typing import Any, Iterable +import time + +import requests +import sentry_sdk +from constance import config +from django.conf import settings +from django.core.files.base import ContentFile +from django.db import transaction +from django.forms.models import model_to_dict +from django.utils import timezone +from kubernetes import client, config as k8s_config +from kubernetes.client.rest import ApiException + +# Global kubernetes client - initialized once and reused +_k8s_batch_v1_client = None +_k8s_config_loaded = False +from qfieldcloud.authentication.models import AuthToken +from qfieldcloud.core.models import ( + ApplyJob, + ApplyJobDelta, + Delta, + Job, + PackageJob, + ProcessProjectfileJob, + Secret, +) +from qfieldcloud.core.utils import get_qgis_project_file +from qfieldcloud.core.utils2 import packages, storage +from tenacity import ( + retry, + retry_if_exception_type, + stop_after_attempt, + wait_random_exponential, +) + +logger = logging.getLogger(__name__) + +RETRY_COUNT = 5 +TIMEOUT_ERROR_EXIT_CODE = -1 +K8S_SIGKILL_EXIT_CODE = 137 +TMP_FILE = Path("/tmp") + + +class QgisException(Exception): + pass + + +class K8sJobRun: + container_timeout_secs = config.WORKER_TIMEOUT_S + job_class = Job + command = [] + + def __init__(self, job_id: str) -> None: + try: + self.job_id = job_id + self.job = self.job_class.objects.select_related().get(id=job_id) + # Use shared PVC mounted at /io - job-specific directory handled by subPath + self.shared_tempdir = Path(f"/io/jobs/{job_id}") + + # Use cached Kubernetes clients + self.k8s_core_v1 = client.CoreV1Api() + self.k8s_batch_v1 = get_k8s_batch_client() + + # K8s namespace for jobs + self.namespace = getattr(settings, "QFIELDCLOUD_K8S_NAMESPACE", "default") + + # Job name for k8s (must be DNS compliant) + self.k8s_job_name = f"qfc-worker-{self.job_id}".lower().replace("_", "-") + + except Exception as err: + feedback: dict[str, Any] = {} + (_type, _value, tb) = sys.exc_info() + feedback["error"] = str(err) + feedback["error_origin"] = "worker_wrapper" + feedback["error_stack"] = traceback.format_tb(tb) + + msg = "Uncaught exception when constructing a K8sJobRun:\n" + msg += json.dumps(feedback, indent=2, sort_keys=True) + + if hasattr(self, "job") and self.job: + self.job.status = Job.Status.FAILED + self.job.feedback = feedback + self.job.save(update_fields=["status", "feedback"]) + logger.exception(msg, exc_info=err) + else: + logger.critical(msg, exc_info=err) + + self.debug_qgis_container_is_enabled = settings.DEBUG and getattr( + settings, "DEBUG_QGIS_DEBUGPY_PORT", None + ) + + if self.debug_qgis_container_is_enabled: + logger.warning( + f"Debugging is enabled for job {self.job.id}. The worker will wait for debugger to attach on port {settings.DEBUG_QGIS_DEBUGPY_PORT}." + ) + + def get_context(self) -> dict[str, Any]: + context = model_to_dict(self.job) + + for key, value in model_to_dict(self.job.project).items(): + context[f"project__{key}"] = value + + context["project__id"] = self.job.project.id + + return context + + def get_command(self) -> list[str]: + context = self.get_context() + + if self.debug_qgis_container_is_enabled: + debug_flags = [ + "-m", + "--listen", + f"0.0.0.0:{settings.DEBUG_QGIS_DEBUGPY_PORT}", + "--wait-for-client", + ] + else: + debug_flags = [] + + # entrypoint.py is relative to WORKDIR /usr/src/app in Dockerfile + return [ + p % context + for p in ["python3", *debug_flags, "entrypoint.py", *self.command] + ] + + def get_volume_mounts(self) -> list[client.V1VolumeMount]: + # Mount job-specific directory at /io so QGIS writes to correct location + # QGIS container uses absolute path /io/feedback.json + volume_mounts = [ + client.V1VolumeMount( + name="shared-io", + mount_path="/io", + sub_path=f"jobs/{self.job_id}", # Mount only the job directory + read_only=False, + ), + ] + + # Add transformation grids volume if configured + if getattr(settings, "QFIELDCLOUD_TRANSFORMATION_GRIDS_VOLUME_NAME", None): + volume_mounts.append( + client.V1VolumeMount( + name="transformation-grids", + mount_path="/transformation_grids", + read_only=True, + ) + ) + + return volume_mounts + + def get_volumes(self) -> list[client.V1Volume]: + # Use the shared PVC from the worker StatefulSet + # PVC name format for StatefulSet: {pvc-name}-{statefulset-name}-{ordinal} + pvc_name = getattr( + settings, + "QFIELDCLOUD_WORKER_SHARED_PVC", + "shared-io-qfieldcloud-worker-0", # Default for StatefulSet + ) + + volumes = [ + client.V1Volume( + name="shared-io", + persistent_volume_claim=client.V1PersistentVolumeClaimVolumeSource( + claim_name=pvc_name + ), + ) + ] + + # Add transformation grids volume if configured + if getattr(settings, "QFIELDCLOUD_TRANSFORMATION_GRIDS_VOLUME_NAME", None): + # For K8s, this could be a PVC, ConfigMap, or HostPath + # Using PVC as the most common case + volumes.append( + client.V1Volume( + name="transformation-grids", + persistent_volume_claim=client.V1PersistentVolumeClaimVolumeSource( + claim_name=settings.QFIELDCLOUD_TRANSFORMATION_GRIDS_VOLUME_NAME + ), + ) + ) + + return volumes + + def get_environment_vars(self) -> list[client.V1EnvVar]: + env_vars = [] + extra_envvars = {} + + pgservice_file_contents = "" + for secret in Secret.objects.for_user_and_project( # type:ignore + self.job.triggered_by, self.job.project + ): + if secret.type == Secret.Type.ENVVAR: + extra_envvars[secret.name] = secret.value + elif secret.type == Secret.Type.PGSERVICE: + pgservice_file_contents += f"\n{secret.value}" + else: + raise NotImplementedError(f"Unknown secret type: {secret.type}") + + token = AuthToken.objects.create( + user=self.job.created_by, + client_type=AuthToken.ClientType.WORKER, + expires_at=timezone.now() + timedelta(seconds=self.container_timeout_secs), + ) + + environment = { + **extra_envvars, + "PGSERVICE_FILE_CONTENTS": pgservice_file_contents, + "QFIELDCLOUD_EXTRA_ENVVARS": json.dumps(sorted(extra_envvars.keys())), + "QFIELDCLOUD_TOKEN": token.key, + "QFIELDCLOUD_URL": settings.QFIELDCLOUD_WORKER_QFIELDCLOUD_URL, + "QFIELDCLOUD_HOST": settings.QFIELDCLOUD_HOST, + "JOB_ID": self.job_id, + "PROJ_DOWNLOAD_DIR": "/transformation_grids", + "QT_QPA_PLATFORM": "offscreen", + } + + # Add storage environment variables for S3 access + if hasattr(settings, "STORAGES"): + # Pass through the STORAGES configuration + environment["STORAGES"] = json.dumps(settings.STORAGES) + + # Add STORAGE environment variables if available (matching Docker pattern) + for storage_var in [ + "STORAGE_ACCESS_KEY_ID", + "STORAGE_SECRET_ACCESS_KEY", + "STORAGE_BUCKET_NAME", + "STORAGE_REGION_NAME", + "STORAGE_ENDPOINT_URL", + ]: + if hasattr(settings, storage_var): + environment[storage_var] = getattr(settings, storage_var) + elif storage_var in os.environ: + environment[storage_var] = os.environ[storage_var] + + # Add additional storage-related environment variables + for storage_var in [ + "STORAGES_PROJECT_DEFAULT_STORAGE", + "MINIO_API_PORT", + "MINIO_BROWSER_PORT", + ]: + if hasattr(settings, storage_var): + environment[storage_var] = getattr(settings, storage_var) + elif storage_var in os.environ: + environment[storage_var] = os.environ[storage_var] + + for key, value in environment.items(): + env_vars.append(client.V1EnvVar(name=key, value=str(value))) + + return env_vars + + def before_k8s_run(self) -> None: + """Hook called before Kubernetes job execution""" + pass + + def after_k8s_run(self) -> None: + """Hook called after successful Kubernetes job completion""" + pass + + def after_k8s_exception(self) -> None: + """Hook called after Kubernetes job failure""" + pass + + def run(self): + """The main and first method to be called on `K8sJobRun`. + + Should not be overloaded by inheriting classes, + they should use `before_k8s_run`, `after_k8s_run` + and `after_k8s_exception` hooks. + """ + feedback = {} + + try: + self.job.status = Job.Status.STARTED + self.job.started_at = timezone.now() + self.job.save(update_fields=["status", "started_at"]) + + # # # CONCURRENCY CHECK # # # + # safety check whether there are no concurrent jobs running for that particular project + # if there are, reset the job back to `PENDING` + concurrent_jobs_count = ( + self.job.project.jobs.filter( + status__in=[Job.Status.QUEUED, Job.Status.STARTED], + ) + .exclude(pk=self.job.pk) + .count() + ) + + if concurrent_jobs_count > 0: + self.job.status = Job.Status.PENDING + self.job.started_at = None + self.job.save(update_fields=["status", "started_at"]) + logger.warning(f"Concurrent jobs occurred for job {self.job}.") + sentry_sdk.capture_message( + f"Concurrent jobs occurred for job {self.job}." + ) + return + # # # /CONCURRENCY CHECK # # # + + self.before_k8s_run() + + command = self.get_command() + + exit_code, output = self._run_k8s_job(command) + + if exit_code == K8S_SIGKILL_EXIT_CODE: + feedback["error"] = "Kubernetes job sigkill." + feedback["error_type"] = "K8S_JOB_SIGKILL" + feedback["error_class"] = "" + feedback["error_origin"] = "container" + feedback["error_stack"] = "" + + try: + self.job.refresh_from_db() + except Exception as err: + logger.error( + "Failed to update job status, probably does not exist in the database.", + exc_info=err, + ) + return + elif exit_code == TIMEOUT_ERROR_EXIT_CODE: + feedback["error"] = "Worker timeout error." + feedback["error_type"] = "TIMEOUT" + feedback["error_class"] = "" + feedback["error_origin"] = "container" + feedback["error_stack"] = "" + else: + try: + # Read feedback.json from the shared PVC + feedback_path = Path(f"/io/jobs/{self.job_id}/feedback.json") + with open(feedback_path) as f: + feedback = json.load(f) + + if feedback.get("error"): + feedback["error_origin"] = "container" + except Exception as err: + if not isinstance(feedback, dict): + feedback = {"error_feedback": feedback} + + (_type, _value, tb) = sys.exc_info() + feedback["error"] = str(err) + feedback["error_origin"] = "worker_wrapper" + feedback["error_stack"] = traceback.format_tb(tb) + + feedback["container_exit_code"] = exit_code + + self.job.output = ( + output.decode("utf-8") if isinstance(output, bytes) else output + ) + self.job.feedback = feedback + self.job.save(update_fields=["output", "feedback"]) + + if exit_code != 0 or feedback.get("error") is not None: + self.job.status = Job.Status.FAILED + self.job.save(update_fields=["status"]) + + try: + self.after_k8s_exception() + except Exception as err: + logger.error( + "Failed to run the `after_k8s_exception` handler.", + exc_info=err, + ) + + return + + # make sure we have reloaded the project, since someone might have changed it already + self.job.project.refresh_from_db() + + self.after_k8s_run() + + shutil.rmtree(str(self.shared_tempdir), ignore_errors=True) + + self.job.finished_at = timezone.now() + self.job.status = Job.Status.FINISHED + self.job.save(update_fields=["status", "finished_at"]) + + except Exception as err: + (_type, _value, tb) = sys.exc_info() + feedback["error"] = str(err) + feedback["error_origin"] = "worker_wrapper" + feedback["error_stack"] = traceback.format_tb(tb) + + if isinstance(err, requests.exceptions.ReadTimeout): + feedback["error_timeout"] = True + + logger.error( + f"Failed job run:\n{json.dumps(feedback, sort_keys=True)}", exc_info=err + ) + + try: + self.job.status = Job.Status.FAILED + self.job.feedback = feedback + self.job.finished_at = timezone.now() + + try: + self.after_k8s_exception() + except Exception as err: + logger.error( + "Failed to run the `after_k8s_exception` handler.", + exc_info=err, + ) + + self.job.save(update_fields=["status", "feedback", "finished_at"]) + except Exception as err: + logger.error( + "Failed to handle exception and update the job status", exc_info=err + ) + + def _run_k8s_job(self, command: list[str]) -> tuple[int, str]: + """Run a Kubernetes Job and wait for completion""" + assert settings.QFIELDCLOUD_WORKER_QFIELDCLOUD_URL + + volume_mounts = self.get_volume_mounts() + volumes = self.get_volumes() + env_vars = self.get_environment_vars() + + # Create container - no resource limits to avoid cgroup allocation issues + # Mount job directory at /io via subPath, so QGIS writes to /io/feedback.json correctly + container = client.V1Container( + name="qgis-worker", + image=settings.QFIELDCLOUD_QGIS_IMAGE_NAME, + command=command, + env=env_vars, + volume_mounts=volume_mounts, + ) + + # Add debug port if enabled + if self.debug_qgis_container_is_enabled: + container.ports = [ + client.V1ContainerPort( + container_port=int(settings.DEBUG_QGIS_DEBUGPY_PORT), protocol="TCP" + ) + ] + + # Pod spec with volumes and labels + pod_template = client.V1PodTemplateSpec( + metadata=client.V1ObjectMeta( + labels={ + "app": f"{getattr(settings, 'ENVIRONMENT', 'dev')}-worker", + "type": self.job.type, + "job-id": str(self.job.id), + "project-id": str(self.job.project_id), + } + ), + spec=client.V1PodSpec( + containers=[container], + volumes=volumes, + restart_policy="Never", + ), + ) + + # Create job spec with timeout + job_spec = client.V1JobSpec( + template=pod_template, + backoff_limit=0, # Don't retry failed jobs + active_deadline_seconds=self.container_timeout_secs, + ) + + # Create job object + k8s_job = client.V1Job( + api_version="batch/v1", + kind="Job", + metadata=client.V1ObjectMeta( + name=self.k8s_job_name, + namespace=self.namespace, + labels={ + "app": f"{getattr(settings, 'ENVIRONMENT', 'dev')}-worker", + "managed-by": "qfieldcloud-worker-wrapper", + }, + ), + spec=job_spec, + ) + + logger.info(f"Execute K8s Job {self.k8s_job_name}: {' '.join(command)}") + + # Start timing + self.job.docker_started_at = ( + timezone.now() + ) # Keep same field name for compatibility + self.job.save(update_fields=["docker_started_at"]) + + try: + # Create the job + if os.getenv("QFIELDCLOUD_K8S_DEBUG"): + logger.info( + f"[K8S_DEBUG] Creating job {self.k8s_job_name} in namespace: {self.namespace}" + ) + + self.k8s_batch_v1.create_namespaced_job( + namespace=self.namespace, body=k8s_job + ) + + if os.getenv("QFIELDCLOUD_K8S_DEBUG"): + logger.info(f"[K8S_DEBUG] Successfully created job {self.k8s_job_name}") + + # Store job name for tracking + self.job.container_id = ( + self.k8s_job_name + ) # Keep same field name for compatibility + self.job.save(update_fields=["container_id"]) + + logger.info(f"Starting K8s worker job {self.k8s_job_name}...") + + # Wait for job completion + exit_code, logs = self._wait_for_job_completion() + + return exit_code, logs + + except ApiException as e: + logger.error(f"Failed to create K8s job: {e}") + return TIMEOUT_ERROR_EXIT_CODE, f"Failed to create K8s job: {e}" + finally: + # End timing + self.job.docker_finished_at = ( + timezone.now() + ) # Keep same field name for compatibility + self.job.save(update_fields=["docker_finished_at"]) + + def _wait_for_job_completion(self) -> tuple[int, str]: + """Wait for the Kubernetes job to complete and retrieve logs""" + start_time = time.time() + + while time.time() - start_time < self.container_timeout_secs: + try: + # Check job status + if os.getenv("QFIELDCLOUD_K8S_DEBUG"): + logger.info( + f"[K8S_DEBUG] Reading job status for {self.k8s_job_name} in namespace: {self.namespace}" + ) + + job_status = self.k8s_batch_v1.read_namespaced_job_status( + name=self.k8s_job_name, namespace=self.namespace + ) + + if os.getenv("QFIELDCLOUD_K8S_DEBUG"): + logger.info( + f"[K8S_DEBUG] Job status - completed: {job_status.status.completion_time is not None}, failed: {job_status.status.failed}" + ) + + if job_status.status.completion_time: + # Job completed successfully + logs = self._get_job_logs() + self._cleanup_job() + return 0, logs + elif job_status.status.failed: + # Job failed + logs = self._get_job_logs() + self._cleanup_job() + return 1, logs + + # Job still running, wait a bit + time.sleep(5) + + except ApiException as e: + if os.getenv("QFIELDCLOUD_K8S_DEBUG"): + logger.error( + f"[K8S_DEBUG] Error checking job status: {type(e).__name__}: {e}" + ) + else: + logger.error(f"Error checking job status: {e}") + time.sleep(5) + + # Timeout reached + logs = self._get_job_logs() + self._cleanup_job() + timeout_msg = f"\nTimeout error! The job failed to finish within {self.container_timeout_secs} seconds!\n" + return TIMEOUT_ERROR_EXIT_CODE, logs + timeout_msg + + def _get_job_logs(self) -> str: + """Retrieve logs from the job's pod""" + try: + # Get pods for this job + if os.getenv("QFIELDCLOUD_K8S_DEBUG"): + logger.info( + f"[K8S_DEBUG] Listing pods for job {self.k8s_job_name} in namespace: {self.namespace}" + ) + + pods = self.k8s_core_v1.list_namespaced_pod( + namespace=self.namespace, label_selector=f"job-name={self.k8s_job_name}" + ) + + if os.getenv("QFIELDCLOUD_K8S_DEBUG"): + logger.info( + f"[K8S_DEBUG] Found {len(pods.items)} pod(s) for job {self.k8s_job_name}" + ) + + if not pods.items: + return "[QFC/Worker/K8s/1001] No pods found for job." + + # Get logs from the first pod + pod_name = pods.items[0].metadata.name + logs = self.k8s_core_v1.read_namespaced_pod_log( + name=pod_name, namespace=self.namespace, container="qgis-worker" + ) + + return logs + + except ApiException as e: + logger.error(f"Failed to retrieve logs: {e}") + return f"[QFC/Worker/K8s/1002] Failed to read logs: {e}" + + def _cleanup_job(self) -> None: + """Clean up the Kubernetes job and its pods""" + try: + # Delete the job (this will also delete associated pods) + self.k8s_batch_v1.delete_namespaced_job( + name=self.k8s_job_name, + namespace=self.namespace, + propagation_policy="Background", + ) + logger.info(f"Cleaned up K8s job {self.k8s_job_name}") + except ApiException as e: + logger.warning(f"Failed to cleanup job {self.k8s_job_name}: {e}") + + +# Inherit from K8sJobRun for all job types +class K8sPackageJobRun(K8sJobRun): + job_class = PackageJob + command = [ + "package", + "%(project__id)s", + "%(project__the_qgis_file_name)s", + "%(project__packaging_offliner)s", + ] + data_last_packaged_at = None + + def before_k8s_run(self) -> None: + # at the start of k8s job we assume we make the snapshot of the data + self.data_last_packaged_at = timezone.now() + + def after_k8s_run(self) -> None: + # only successfully finished packaging jobs should update the Project.data_last_packaged_at + self.job.project.data_last_packaged_at = self.data_last_packaged_at + self.job.project.save(update_fields=("data_last_packaged_at",)) + + packages.delete_obsolete_packages(projects=[self.job.project]) + + +class K8sApplyDeltaJobRun(K8sJobRun): + job_class = ApplyJob + command = ["apply_deltas", "%(project__id)s", "%(project__the_qgis_file_name)s"] + + def __init__(self, job_id: str) -> None: + super().__init__(job_id) + + if self.job.overwrite_conflicts: + self.command = [*self.command, "--overwrite-conflicts"] + + def _prepare_deltas(self, deltas: Iterable[Delta]) -> dict[str, Any]: + delta_contents = [] + delta_client_ids = [] + + for delta in deltas: + delta_contents.append(delta.content) + + if "clientId" in delta.content: + delta_client_ids.append(delta.content["clientId"]) + + local_to_remote_pk_deltas = Delta.objects.filter( + client_id__in=delta_client_ids, + last_modified_pk__isnull=False, + ).values( + "client_id", "content__localLayerId", "content__localPk", "last_modified_pk" + ) + + client_pks_map = {} + + for delta_with_modified_pk in local_to_remote_pk_deltas: + key = f"{delta_with_modified_pk['client_id']}__{delta_with_modified_pk['content__localLayerId']}__{delta_with_modified_pk['content__localPk']}" + client_pks_map[key] = delta_with_modified_pk["last_modified_pk"] + + deltafile_contents = { + "deltas": delta_contents, + "files": [], + "id": str(uuid.uuid4()), + "project": str(self.job.project.id), + "version": "1.0", + "clientPks": client_pks_map, + } + + return deltafile_contents + + @transaction.atomic() + def before_k8s_run(self) -> None: + deltas = self.job.deltas_to_apply.all() + deltafile_contents = self._prepare_deltas(deltas) + + self.delta_ids = [d.id for d in deltas] + + ApplyJobDelta.objects.filter( + apply_job_id=self.job_id, + delta_id__in=self.delta_ids, + ).update(status=Delta.Status.STARTED) + + self.job.deltas_to_apply.update(last_status=Delta.Status.STARTED) + + # Ensure the directory exists before writing (mimics Docker tempfile.mkdtemp behavior) + self.shared_tempdir.mkdir(parents=True, exist_ok=True) + + with open(self.shared_tempdir.joinpath("deltafile.json"), "w") as f: + json.dump(deltafile_contents, f) + + def after_k8s_run(self) -> None: + delta_feedback = self.job.feedback["outputs"]["apply_deltas"]["delta_feedback"] + is_data_modified = False + + for feedback in delta_feedback: + delta_id = feedback["delta_id"] + status = feedback["status"] + modified_pk = feedback["modified_pk"] + + if status == "status_applied": + status = Delta.Status.APPLIED + is_data_modified = True + elif status == "status_conflict": + status = Delta.Status.CONFLICT + elif status == "status_apply_failed": + status = Delta.Status.NOT_APPLIED + else: + status = Delta.Status.ERROR + # not certain what happened + is_data_modified = True + + Delta.objects.filter(pk=delta_id).update( + last_status=status, + last_feedback=feedback, + last_modified_pk=modified_pk, + last_apply_attempt_at=self.job.started_at, + last_apply_attempt_by=self.job.created_by, + ) + + ApplyJobDelta.objects.filter( + apply_job_id=self.job_id, + delta_id=delta_id, + ).update( + status=status, + feedback=feedback, + modified_pk=modified_pk, + ) + + if is_data_modified: + self.job.project.data_last_updated_at = timezone.now() + self.job.project.save(update_fields=("data_last_updated_at",)) + + def after_k8s_exception(self) -> None: + if hasattr(self, "delta_ids"): + Delta.objects.filter( + id__in=self.delta_ids, + ).update( + last_status=Delta.Status.ERROR, + last_feedback=None, + last_modified_pk=None, + last_apply_attempt_at=self.job.started_at, + last_apply_attempt_by=self.job.created_by, + ) + + ApplyJobDelta.objects.filter( + apply_job_id=self.job_id, + delta_id__in=self.delta_ids, + ).update( + status=Delta.Status.ERROR, + feedback=None, + modified_pk=None, + ) + + +class K8sProcessProjectfileJobRun(K8sJobRun): + job_class = ProcessProjectfileJob + command = [ + "process_projectfile", + "%(project__id)s", + "%(project__the_qgis_file_name)s", + ] + + def get_context(self, *args) -> dict[str, Any]: + context = super().get_context(*args) + + if not context.get("project__the_qgis_file_name"): + context["project__the_qgis_file_name"] = get_qgis_project_file( + context["project__id"] + ) + + return context + + def after_k8s_run(self) -> None: + project = self.job.project + project.project_details = self.job.feedback["outputs"]["project_details"][ + "project_details" + ] + + thumbnail_filename = self.shared_tempdir.joinpath("thumbnail.png") + + with open(thumbnail_filename, "rb") as f: + # TODO Delete with QF-4963 Drop support for legacy storage + if project.uses_legacy_storage: + legacy_thumbnail_uri = storage.upload_project_thumbail( + project, f, "image/png", "thumbnail" + ) + project.legacy_thumbnail_uri = ( + project.legacy_thumbnail_uri or legacy_thumbnail_uri + ) + else: + project.thumbnail = ContentFile(f.read(), "dummy_thumbnail_name.png") + + project.save( + update_fields=( + "project_details", + "legacy_thumbnail_uri", + "thumbnail", + ) + ) + + # for non-legacy storage, keep only one thumbnail version if so. + if not project.uses_legacy_storage and project.thumbnail: + storage.purge_previous_thumbnails_versions(project) + + def after_k8s_exception(self) -> None: + project = self.job.project + + if project.project_details is not None: + project.project_details = None + project.save(update_fields=("project_details",)) + + +def get_k8s_batch_client(): + """Get a cached Kubernetes BatchV1Api client, initializing it only once.""" + global _k8s_batch_v1_client, _k8s_config_loaded + + # Reload config each time to ensure fresh configuration + try: + k8s_config.load_incluster_config() + if os.getenv("QFIELDCLOUD_K8S_DEBUG"): + config = client.Configuration.get_default_copy() + logger.info( + f"Loaded in-cluster Kubernetes configuration - API host: {config.host}" + ) + else: + logger.info("Loaded in-cluster Kubernetes configuration") + _k8s_config_loaded = True + except k8s_config.ConfigException: + try: + k8s_config.load_kube_config() + logger.info("Loaded kube config from file") + _k8s_config_loaded = True + except Exception as e: + logger.error(f"Failed to load Kubernetes configuration: {e}") + raise + + # Always create a fresh client + _k8s_batch_v1_client = client.BatchV1Api() + + if os.getenv("QFIELDCLOUD_K8S_DEBUG"): + config = client.Configuration.get_default_copy() + logger.info( + f"Initialized Kubernetes BatchV1Api client with host: {config.host}" + ) + else: + logger.info("Initialized Kubernetes BatchV1Api client") + + return _k8s_batch_v1_client + + +def cancel_orphaned_k8s_workers() -> None: + """Cancel orphaned Kubernetes worker jobs that are not associated with active jobs.""" + try: + batch_v1 = get_k8s_batch_client() + namespace = getattr(settings, "QFIELDCLOUD_K8S_NAMESPACE", "default") + + if os.getenv("QFIELDCLOUD_K8S_DEBUG"): + logger.info(f"[K8S_DEBUG] Listing jobs in namespace: {namespace}") + + # Get all jobs with the qfc-worker prefix + jobs = batch_v1.list_namespaced_job(namespace=namespace) + + if os.getenv("QFIELDCLOUD_K8S_DEBUG"): + logger.info(f"[K8S_DEBUG] Successfully listed {len(jobs.items)} jobs") + + # Get active job IDs from the database + from qfieldcloud.core.models import Job + + active_job_ids = set(Job.objects.values_list("id", flat=True)) + + # Cancel jobs that are not in the active jobs list + for job in jobs.items: + # Extract job_id from the job name (format: qfc-worker-{job_id}) + job_name = job.metadata.name + if not job_name.startswith("qfc-worker-"): + continue + + job_id = job_name.replace("qfc-worker-", "").replace("-", "") + + if job_id not in active_job_ids: + if os.getenv("QFIELDCLOUD_K8S_DEBUG"): + logger.info( + f"[K8S_DEBUG] Deleting orphaned job: {job_name} in namespace: {namespace}" + ) + else: + logger.info(f"Canceling orphaned K8s job: {job_name}") + + batch_v1.delete_namespaced_job( + name=job_name, namespace=namespace, propagation_policy="Background" + ) + + if os.getenv("QFIELDCLOUD_K8S_DEBUG"): + logger.info(f"[K8S_DEBUG] Successfully deleted job: {job_name}") + except Exception as e: + if os.getenv("QFIELDCLOUD_K8S_DEBUG"): + logger.error( + f"[K8S_DEBUG] Failed to cancel orphaned K8s workers: {type(e).__name__}: {e}" + ) + else: + logger.error(f"Failed to cancel orphaned K8s workers: {e}") + + +# Compatibility aliases - use these instead of the original Docker-based classes +JobRun = K8sJobRun +PackageJobRun = K8sPackageJobRun +ApplyDeltaJobRun = K8sApplyDeltaJobRun +ProcessProjectfileJobRun = K8sProcessProjectfileJobRun +cancel_orphaned_workers = cancel_orphaned_k8s_workers diff --git a/docker-app/worker_wrapper/wrapper.py b/docker-app/worker_wrapper/wrapper.py index 1f918da23..5174ea68c 100644 --- a/docker-app/worker_wrapper/wrapper.py +++ b/docker-app/worker_wrapper/wrapper.py @@ -10,6 +10,7 @@ from typing import Any, Iterable import docker +import docker.client import docker.errors import requests import sentry_sdk @@ -49,6 +50,11 @@ TIMEOUT_ERROR_EXIT_CODE = -1 DOCKER_SIGKILL_EXIT_CODE = 137 TMP_FILE = Path("/tmp") +TRANSFORMATION_GRIDS_PATH = "/transformation_grids" +"""Path inside the worker container where the transformation grids volume `settings.QFIELDCLOUD_TRANSFORMATION_GRIDS_VOLUME_NAME` is mounted.""" + +TOKEN_EXPIRATION_TIME_BUFFER_S = 60 +"""Extra time in seconds for the dedicated worker token to keep the token valid, in addition to `JobRun.container_timeout_secs`. Useful when the worker takes longer to start.""" class QgisException(Exception): @@ -124,7 +130,7 @@ def get_command(self) -> list[str]: def get_volumes(self) -> list[str]: volumes = [ f"{str(self.shared_tempdir)}:/io/:rw", - f"{settings.QFIELDCLOUD_TRANSFORMATION_GRIDS_VOLUME_NAME}:/transformation_grids:ro", + f"{settings.QFIELDCLOUD_TRANSFORMATION_GRIDS_VOLUME_NAME}:{TRANSFORMATION_GRIDS_PATH}:ro", ] return volumes @@ -135,7 +141,6 @@ def get_ports(self) -> dict[str, int]: return ports def get_environment(self) -> dict[str, str]: - environment = {} extra_envvars = {} pgservice_file_contents = "" @@ -149,10 +154,14 @@ def get_environment(self) -> dict[str, str]: else: raise NotImplementedError(f"Unknown secret type: {secret.type}") + # expire the token a bit after the container timeout to avoid edge cases + token_expires_at = timezone.now() + timedelta( + seconds=self.container_timeout_secs + TOKEN_EXPIRATION_TIME_BUFFER_S + ) token = AuthToken.objects.create( user=self.job.created_by, client_type=AuthToken.ClientType.WORKER, - expires_at=timezone.now() + timedelta(seconds=self.container_timeout_secs), + expires_at=token_expires_at, ) environment = { @@ -162,7 +171,7 @@ def get_environment(self) -> dict[str, str]: "QFIELDCLOUD_TOKEN": token.key, "QFIELDCLOUD_URL": settings.QFIELDCLOUD_WORKER_QFIELDCLOUD_URL, "JOB_ID": self.job_id, - "PROJ_DOWNLOAD_DIR": "/transformation_grids", + "PROJ_DOWNLOAD_DIR": TRANSFORMATION_GRIDS_PATH, "QT_QPA_PLATFORM": "offscreen", } @@ -333,16 +342,15 @@ def _run_docker(self, command: list[str]) -> tuple[int, bytes]: if settings.DEBUG: if self.debug_qgis_container_is_enabled: # NOTE the `qgis` container must expose the same port as the one used by `debugpy`, - # otherwise the vscode deubgger won't be able to connect + # otherwise the vscode debugger won't be able to connect # NOTE the port must be passed here and not in the `docker-compose` file, # because the `qgis` container is started with docker in docker and the `docker-compose` # configuration is valid only for the brief moment when the stack is built and started, # but not when new `qgis` containers are started dynamically by the worker wrapper - ports.update( - { - f"{settings.DEBUG_QGIS_DEBUGPY_PORT}/tcp": settings.DEBUG_QGIS_DEBUGPY_PORT, - } - ) + ports = { + **ports, + f"{settings.DEBUG_QGIS_DEBUGPY_PORT}/tcp": settings.DEBUG_QGIS_DEBUGPY_PORT, + } logger.debug( f"Exposing ports from the qgis container for debugging: {ports=}" @@ -351,17 +359,16 @@ def _run_docker(self, command: list[str]) -> tuple[int, bytes]: if settings.DEBUG_QGIS_WORKER_HOST_PATH: debug_host_path = Path(settings.DEBUG_QGIS_WORKER_HOST_PATH) - volumes.extend( - [ - # allow local development for `docker-qgis` - f"{debug_host_path.joinpath('qfc_worker')}:/usr/src/app/qfc_worker:ro", - f"{debug_host_path.joinpath('entrypoint.py')}:/usr/src/app/entrypoint.py:ro", - # allow local development for `libqfieldsync` if host directory present; requires `PYTHONPATH=/libqfieldsync:${PYTHONPATH}` - f"{debug_host_path.joinpath('libqfieldsync')}:/libqfieldsync.py:ro", - # allow local development for `qfieldcloud-sdk-python` if host directory present; requires `PYTHONPATH=/qfieldcloud-sdk-python:${PYTHONPATH}`" - f"{debug_host_path.joinpath('qfieldcloud-sdk-python')}:/qfieldcloud-sdk-python.py:ro", - ] - ) + volumes = [ + *volumes, + # allow local development for `docker-qgis` + f"{debug_host_path.joinpath('qfc_worker')}:/usr/src/app/qfc_worker:ro", + f"{debug_host_path.joinpath('entrypoint.py')}:/usr/src/app/entrypoint.py:ro", + # allow local development for `libqfieldsync` if host directory present; requires `PYTHONPATH=/libqfieldsync:${PYTHONPATH}` within the worker container. + f"{debug_host_path.joinpath('libqfieldsync')}:/libqfieldsync:ro", + # allow local development for `qfieldcloud-sdk-python` if host directory present; requires `PYTHONPATH=/qfieldcloud-sdk-python:${PYTHONPATH}` within the worker container. + f"{debug_host_path.joinpath('qfieldcloud-sdk-python')}:/qfieldcloud-sdk-python:ro", + ] logger.debug( f"Mounting host path into qgis container for debugging: {volumes=}" diff --git a/docker-compose.override.standalone.yml b/docker-compose.override.standalone.yml index 4de133e27..4d90ecb55 100644 --- a/docker-compose.override.standalone.yml +++ b/docker-compose.override.standalone.yml @@ -1,5 +1,4 @@ services: - db: image: postgis/postgis:${POSTGIS_IMAGE_VERSION} restart: unless-stopped @@ -11,7 +10,8 @@ services: - postgres_data:/var/lib/postgresql/data/ ports: - ${HOST_POSTGRES_PORT}:5432 - command: ["postgres", "-c", "log_statement=all", "-c", "log_destination=stderr"] + command: + ["postgres", "-c", "log_statement=all", "-c", "log_destination=stderr"] smtp4dev: image: rnwood/smtp4dev:v3 @@ -24,7 +24,7 @@ services: # IMAP - ${SMTP4DEV_IMAP_PORT}:143 volumes: - - smtp4dev_data:/smtp4dev + - smtp4dev_data:/smtp4dev environment: # Specifies the server hostname. Used in auto-generated TLS certificate if enabled. - ServerOptions__HostName=smtp4dev @@ -43,17 +43,18 @@ services: MINIO_BROWSER_REDIRECT_URL: http://${QFIELDCLOUD_HOST}:${MINIO_BROWSER_PORT} command: server /data{1...4} --console-address :9001 healthcheck: - test: [ + test: + [ "CMD", "curl", "-A", "Mozilla/5.0 (X11; Linux x86_64; rv:30.0) Gecko/20100101 Firefox/30.0", "-f", - "http://localhost:9001/minio/index.html" + "http://localhost:9001/minio/index.html", ] - interval: 5s - timeout: 20s - retries: 5 + interval: 5s + timeout: 20s + retries: 5 ports: - ${MINIO_BROWSER_PORT}:9001 - ${MINIO_API_PORT}:9000 @@ -61,6 +62,9 @@ services: createbuckets: build: context: ./docker-createbuckets + platforms: + - linux/amd64 + - linux/arm64/v8 depends_on: minio: condition: service_healthy diff --git a/docker-compose.override.test.yml b/docker-compose.override.test.yml index 0368b5ce3..61d58a795 100644 --- a/docker-compose.override.test.yml +++ b/docker-compose.override.test.yml @@ -42,4 +42,4 @@ volumes: # We use a different volume, just so that the test_ database # gets created in the entrypoint. postgres_data: - name: qfieldcloud_postgres_data_test + name: ${COMPOSE_PROJECT_NAME}_postgres_data_test diff --git a/docker-compose.yml b/docker-compose.yml index f4bebfc84..6e0d5b79d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,9 @@ services: context: ./docker-app target: webserver_runtime network: host + platforms: + - linux/amd64 + - linux/arm64/v8 restart: unless-stopped command: > gunicorn @@ -89,6 +92,9 @@ services: nginx: build: context: ./docker-nginx + platforms: + - linux/amd64 + - linux/arm64/v8 restart: unless-stopped volumes: - ./conf/certbot/conf:/etc/letsencrypt:ro @@ -111,6 +117,10 @@ services: LETSENCRYPT_STAGING: ${LETSENCRYPT_STAGING} LETSENCRYPT_RSA_KEY_SIZE: ${LETSENCRYPT_RSA_KEY_SIZE} NGINX_ERROR_LOG_LEVEL: ${NGINX_ERROR_LOG_LEVEL:-error} + NGINX_CLIENT_MAX_BODY_SIZE: ${NGINX_CLIENT_MAX_BODY_SIZE:-10g} + NGINX_PROXY_CONNECT_TIMEOUT: ${NGINX_PROXY_CONNECT_TIMEOUT:-5s} + NGINX_PROXY_READ_TIMEOUT: ${NGINX_PROXY_READ_TIMEOUT:-300s} + NGINX_PROXY_SEND_TIMEOUT: ${NGINX_PROXY_SEND_TIMEOUT:-300s} logging: driver: "json-file" options: @@ -139,6 +149,9 @@ services: build: context: ./docker-qgis network: host + platforms: + - linux/amd64 + - linux/arm64/v8 args: DEBUG_BUILD: ${DEBUG} tty: true @@ -151,6 +164,9 @@ services: build: context: ./docker-app network: host + platforms: + - linux/amd64 + - linux/arm64/v8 target: worker_wrapper_runtime command: python manage.py dequeue user: root # TODO change me to least privileged docker-capable user on the host (/!\ docker users!=hosts users, use UID rather than username) diff --git a/docker-createbuckets/Dockerfile b/docker-createbuckets/Dockerfile index 3813a0a03..03cdc535b 100644 --- a/docker-createbuckets/Dockerfile +++ b/docker-createbuckets/Dockerfile @@ -6,8 +6,16 @@ RUN apt-get update \ python3 \ && rm -rf /var/lib/apt/lists/* -RUN curl https://dl.min.io/client/mc/release/linux-amd64/archive/mc.RELEASE.2025-03-12T17-29-24Z -o /usr/bin/mc -RUN chmod +x /usr/bin/mc +# Download MinIO client for the correct architecture +RUN ARCH=$(uname -m) && \ + if [ "$ARCH" = "x86_64" ]; then \ + curl https://dl.min.io/client/mc/release/linux-amd64/archive/mc.RELEASE.2025-03-12T17-29-24Z -o /usr/bin/mc; \ + elif [ "$ARCH" = "aarch64" ]; then \ + curl https://dl.min.io/client/mc/release/linux-arm64/archive/mc.RELEASE.2025-03-12T17-29-24Z -o /usr/bin/mc; \ + else \ + echo "Unsupported architecture: $ARCH" && exit 1; \ + fi && \ + chmod +x /usr/bin/mc COPY ./createbuckets.py /createbuckets.py RUN chmod +x /createbuckets.py diff --git a/docker-nginx/templates/default.conf.k8s.template b/docker-nginx/templates/default.conf.k8s.template new file mode 100644 index 000000000..16081c95d --- /dev/null +++ b/docker-nginx/templates/default.conf.k8s.template @@ -0,0 +1,191 @@ +map "$time_iso8601 # $msec" $time_iso8601_ms { + "~([^+]+)\+([\d:]+?) # \d+?\.(\d+)" "$1.$3+$2"; +} + +map "${DEBUG}" $debug_mode { + "1" "on"; + default "off"; +} + +log_format json-logger escape=json +'{' + '"ts":"$time_iso8601_ms",' + '"ip":"$remote_addr",' + '"method":"$request_method",' + '"status":$status,' + '"resp_time":$request_time,' + '"request_length":$request_length,' + '"resp_body_size":$body_bytes_sent,' + '"uri":"$request_uri",' + '"connection": "$connection",' + '"connection_requests": "$connection_requests",' + '"user_agent":"$http_user_agent",' + '"host":"$http_host",' + '"user":"$remote_user",' + '"upstream_addr":"$upstream_addr",' + '"upstream_connect_time":"$upstream_connect_time",' + '"upstream_header_time":"$upstream_header_time",' + '"upstream_response_time":"$upstream_response_time",' + '"request_id":"$request_id",' + '"source":"nginx"' +'}'; + +upstream django { + # Defines a shared memory zone between worker processes, necessary dynamic dns resolving + zone django 64k; + + # Add Docker's DNS resolver with a short TTL and IPv6 turned off + resolver 127.0.0.11 valid=1s ipv6=off; + + # the number of keepalive connections comes from the number Django workers we have + keepalive 32; + + # Use localhost since Django app is in the same pod + # `max_fails=1` and `fail_timeout=1s` ensures Nginx retries on almost every request from `loading.html` + server 127.0.0.1:8000 max_fails=1 fail_timeout=1s; +} + +server { + listen 80; + server_name ${QFIELDCLOUD_HOST}; + + access_log /var/log/nginx/access.log json-logger; + error_log /var/log/nginx/error.log ${NGINX_ERROR_LOG_LEVEL}; + + client_max_body_size 10G; + keepalive_timeout 5; + + # path for static files (only needed for serving local staticfiles) + root /var/www/html/; + + # prevent access by IP (adapted for Kubernetes - check X-Forwarded-Host from Traefik) + set $forwarded_host $http_x_forwarded_host; + if ($forwarded_host = "") { + set $forwarded_host $http_host; + } + if ($forwarded_host !~ "${QFIELDCLOUD_HOST}") { + return 444; + } + + # include additional config from the current QFieldCloud instance. This is different from the default `conf.d` directory. + include config.d/*.conf; + + # deny annoying bot + deny 34.215.13.216; + + # Default proxy settings for all locations + proxy_http_version 1.1; + proxy_set_header Connection ''; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Request-Id $request_id; + proxy_set_header Host $http_host; + + proxy_connect_timeout 5s; + proxy_read_timeout 300; + proxy_send_timeout 300; + proxy_redirect off; + proxy_intercept_errors off; + + location ^~ /api/ { + proxy_pass http://django; + } + + location ^~ /pages/ { + try_files $uri =404; + access_log off; + expires 1h; + } + + location / { + try_files $uri @proxy_to_app; + } + + location @proxy_to_app { + error_page 403 =403 /pages/403.html; + error_page 404 =404 /pages/404.html; + + # Initial loading page for upstream issues (502, 503, 504 for timeouts) + error_page 502 503 504 =503 /pages/loading.html; + + # Redirect 500-level errors to a dedicated handler location + error_page 500 501 505 = @handle_500_error; + + proxy_intercept_errors on; + proxy_pass http://django; + } + + location @handle_500_error { + if ($debug_mode = "on") { + # If debug mode is on, attempt to pass the original django 500 response (with stacktrace) + proxy_pass http://django; + } + + try_files /pages/500.html =404; + } + + location /swagger.yaml { + add_header Access-Control-Allow-Origin https://docs.qfield.org; + proxy_pass http://django; + } + + location /storage-download/ { + # Only allow internal redirects + internal; + + # used for redirecting file requests to storage. + set $redirect_uri "$upstream_http_redirect_uri"; + # webdav storage requires a HTTP auth (Basic, mostly). + set $webdav_auth "$upstream_http_webdav_auth"; + # if a Range header is provided + set $file_range "$upstream_http_file_range"; + + # Add Docker's DNS resolver with IPv6 turned off + resolver 127.0.0.11 ipv6=off; + + # Stops the local disk from being written to (just forwards data through) + proxy_max_temp_file_size 0; + proxy_buffering off; + + # Required when keepalive is used + proxy_http_version 1.1; + + # does not work with S3 otherwise + proxy_ssl_server_name on; + + # remove the authorization and the cookie headers + proxy_set_header Connection ''; + proxy_set_header Authorization $webdav_auth; + proxy_set_header Cookie ''; + proxy_set_header Content-Type ''; + proxy_set_header Accept-Encoding ''; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Range $file_range; + + # hide Object Storage related headers + proxy_hide_header Access-Control-Allow-Credentials; + proxy_hide_header Access-Control-Allow-Headers; + proxy_hide_header Access-Control-Allow-Methods; + proxy_hide_header Access-Control-Allow-Origin; + proxy_hide_header Access-Control-Expose-Headers; + proxy_hide_header X-Amz-Meta-Sha256sum; + proxy_hide_header X-Amz-Req-Time-Micros; + proxy_hide_header X-Amz-Request-Id; + proxy_hide_header A-Amz-Meta-Server-Side-Encryption; + proxy_hide_header X-Amz-Storage-Class; + proxy_hide_header X-Amz-Version-Id; + proxy_hide_header X-Amz-Id-2; + proxy_hide_header X-Amz-Server-Side-Encryption; + proxy_hide_header Set-Cookie; + proxy_ignore_headers Set-Cookie; + + proxy_intercept_errors on; + + proxy_pass $redirect_uri; + error_page 404 =404 /pages/404.html; + error_page 403 =403 /pages/403.html; + error_page 401 402 405 406 407 408 409 410 411 412 413 414 415 416 417 500 501 502 503 504 505 =500 /pages/500.html; + } + +} diff --git a/docker-nginx/templates/default.conf.template b/docker-nginx/templates/default.conf.template index 6d636787b..a9cda8bfd 100644 --- a/docker-nginx/templates/default.conf.template +++ b/docker-nginx/templates/default.conf.template @@ -40,7 +40,8 @@ upstream django { # the number of keepalive connections comes from the number Django workers we have keepalive 32; - # Use Docker DNS resolution to dynamically resolve the app backend hostname + # Use Docker DNS resolution to dynamically resolve the app backend hostname for Docker Compose + # For Kubernetes, use 127.0.0.1:8000 since containers are in the same pod # `max_fails=1` and `fail_timeout=1s` ensures Nginx retries on almost every request from `loading.html` server app:8000 max_fails=1 fail_timeout=1s resolve; } @@ -78,7 +79,7 @@ server { error_log /var/log/nginx/error.log ${NGINX_ERROR_LOG_LEVEL}; server_name ${QFIELDCLOUD_HOST} nginx; - client_max_body_size 10G; + client_max_body_size ${NGINX_CLIENT_MAX_BODY_SIZE}; keepalive_timeout 5; # path for static files (only needed for serving local staticfiles) @@ -106,9 +107,9 @@ server { proxy_set_header X-Request-Id $request_id; proxy_set_header Host $http_host; - proxy_connect_timeout 5s; - proxy_read_timeout 300; - proxy_send_timeout 300; + proxy_connect_timeout ${NGINX_PROXY_CONNECT_TIMEOUT}; + proxy_read_timeout ${NGINX_PROXY_READ_TIMEOUT}; + proxy_send_timeout ${NGINX_PROXY_SEND_TIMEOUT}; proxy_redirect off; proxy_intercept_errors off; diff --git a/docker-qgis/Dockerfile b/docker-qgis/Dockerfile index 0d1be63d3..6c9e9dab4 100644 --- a/docker-qgis/Dockerfile +++ b/docker-qgis/Dockerfile @@ -12,16 +12,16 @@ RUN apt-get update \ wget \ && rm -rf /var/lib/apt/lists/* -# Add QGIS GPG key -RUN wget -O /etc/apt/keyrings/qgis-archive-keyring.gpg https://download.qgis.org/downloads/qgis-archive-keyring.gpg - -# Add QGIS repository -RUN echo "Types: deb deb-src\n\ +# Add QGIS repository only for AMD64 (ARM64 packages not available from QGIS.org) +RUN if [ "$(uname -m)" = "x86_64" ]; then \ + wget -O /etc/apt/keyrings/qgis-archive-keyring.gpg https://download.qgis.org/downloads/qgis-archive-keyring.gpg && \ + echo "Types: deb deb-src\n\ URIs: https://qgis.org/debian\n\ Suites: noble\n\ Architectures: amd64\n\ Components: main\n\ -Signed-By: /etc/apt/keyrings/qgis-archive-keyring.gpg\n" > /etc/apt/sources.list.d/qgis.sources +Signed-By: /etc/apt/keyrings/qgis-archive-keyring.gpg\n" > /etc/apt/sources.list.d/qgis.sources; \ + fi # Disable annoying pip version check, we don't care if pip is slightly older ARG PIP_DISABLE_PIP_VERSION_CHECK=1 @@ -41,21 +41,42 @@ RUN apt-get update \ # Set QGIS version as in the debian repos -# Choose your version from here: https://debian.qgis.org/debian/dists/noble/main/binary-amd64/Packages +# Choose your version from here: +# AMD64: https://debian.qgis.org/debian/dists/noble/main/binary-amd64/Packages +# ARM64: Uses Ubuntu's default QGIS packages (QGIS.org doesn't provide ARM64 builds) ARG QGIS_VERSION=1:3.44.3+40noble -# Install QGIS dependencies -RUN apt-get update \ - # NOTE `DEBIAN_FRONTEND=noninteractive` is required to be able to install tzinfo - && DEBIAN_FRONTEND=noninteractive apt-get install -yf \ - qgis=${QGIS_VERSION} \ - qgis-dbg=${QGIS_VERSION} \ - qgis-common=${QGIS_VERSION} \ - python3-qgis=${QGIS_VERSION} \ - python3-qgis-common=${QGIS_VERSION} \ - qgis-providers=${QGIS_VERSION} \ - qgis-providers-common=${QGIS_VERSION} \ - && rm -rf /var/lib/apt/lists/* +# Install QGIS dependencies with architecture-aware package selection +RUN apt-get update && \ + if [ "$(uname -m)" = "x86_64" ]; then \ + echo "Installing QGIS from official repository for AMD64"; \ + if ! DEBIAN_FRONTEND=noninteractive apt-get install -yf \ + qgis=${QGIS_VERSION} \ + qgis-common=${QGIS_VERSION} \ + python3-qgis=${QGIS_VERSION} \ + python3-qgis-common=${QGIS_VERSION} \ + qgis-providers=${QGIS_VERSION} \ + qgis-providers-common=${QGIS_VERSION}; then \ + echo "Specific QGIS version ${QGIS_VERSION} not available, installing latest from official repo"; \ + DEBIAN_FRONTEND=noninteractive apt-get install -yf \ + qgis \ + qgis-common \ + python3-qgis \ + python3-qgis-common \ + qgis-providers \ + qgis-providers-common; \ + fi; \ + else \ + echo "Installing QGIS from Ubuntu repositories for ARM64 ($(uname -m))"; \ + DEBIAN_FRONTEND=noninteractive apt-get install -yf \ + qgis \ + qgis-common \ + python3-qgis \ + python3-qgis-common \ + qgis-providers \ + qgis-providers-common; \ + fi && \ + rm -rf /var/lib/apt/lists/* # If debug build, install `gdbserver` ARG DEBUG_BUILD @@ -69,8 +90,19 @@ RUN if [ "$DEBUG_BUILD" = "1" ]; then \ WORKDIR /usr/src/app -# crashes to STDERR -ENV LD_PRELOAD="/lib/x86_64-linux-gnu/libSegFault.so" +# crashes to STDERR - create script to set architecture-aware library path +RUN echo '#!/bin/bash\n\ +ARCH=$(uname -m)\n\ +if [ "$ARCH" = "x86_64" ]; then\n\ + export LD_PRELOAD="/lib/x86_64-linux-gnu/libSegFault.so"\n\ +elif [ "$ARCH" = "aarch64" ]; then\n\ + export LD_PRELOAD="/lib/aarch64-linux-gnu/libSegFault.so"\n\ +fi\n\ +export SEGFAULT_SIGNALS="abrt segv"\n\ +export LIBC_FATAL_STDERR_=1\n\ +exec "$@"' > /usr/local/bin/set-segfault-env.sh && \ + chmod +x /usr/local/bin/set-segfault-env.sh + ENV SEGFAULT_SIGNALS="abrt segv" ENV LIBC_FATAL_STDERR_=1 @@ -97,4 +129,4 @@ COPY schemas schemas COPY qfc_worker qfc_worker COPY entrypoint.py . -ENTRYPOINT ["/bin/sh", "-c", "/usr/bin/xvfb-run -a \"$@\"", ""] +ENTRYPOINT ["/bin/sh", "-c", "/usr/local/bin/set-segfault-env.sh /usr/bin/xvfb-run -a \"$@\"", ""] diff --git a/docker-qgis/qfc_worker/commands/apply_deltas.py b/docker-qgis/qfc_worker/commands/apply_deltas.py index 4eaff3bec..50002ecc1 100755 --- a/docker-qgis/qfc_worker/commands/apply_deltas.py +++ b/docker-qgis/qfc_worker/commands/apply_deltas.py @@ -3,6 +3,7 @@ import argparse import json import logging +import os import re from enum import Enum from functools import lru_cache @@ -14,6 +15,7 @@ # pylint: disable=no-name-in-module from qgis.core import ( + QgsDataSourceUri, QgsExpression, QgsFeature, QgsGeometry, @@ -48,6 +50,8 @@ FeaturePk = str LayerId = str +QFC_PG_EFFECTIVE_USER = os.getenv("QFC_PG_EFFECTIVE_USER") + class BaseOptions(TypedDict): cmd0: Callable @@ -423,6 +427,17 @@ def apply_deltas_without_transaction( provider_errors=layer.dataProvider().errors(), ) + # check if a PostGIS layer's session_role override is requested. + if layer.providerType() == "postgres" and QFC_PG_EFFECTIVE_USER: + logger.info( + f"Adjusting pg layer {layer.name()} with session_role={QFC_PG_EFFECTIVE_USER}" + ) + + uri = QgsDataSourceUri(layer.dataProvider().dataSourceUri()) + uri.setParam("session_role", QFC_PG_EFFECTIVE_USER) + layer.setDataSource(uri.uri(), layer.name(), "postgres") + layer.reload() + pk_attr_name = get_pk_attr_name(layer) if not pk_attr_name: raise DeltaException(f'Layer "{layer.name()}" has no primary key.') diff --git a/docker-qgis/qfc_worker/workflow.py b/docker-qgis/qfc_worker/workflow.py index 56e8d2154..8244c3a05 100644 --- a/docker-qgis/qfc_worker/workflow.py +++ b/docker-qgis/qfc_worker/workflow.py @@ -144,23 +144,24 @@ def __init__( id: str, name: str, method: Callable, - arguments: dict[str, Any] = {}, - return_names: list[str] = [], - outputs: list[str] = [], - ): + arguments: dict[str, Any] | None = None, + return_names: list[str] | None = None, + outputs: list[str] | None = None, + ) -> None: self.id = id self.name = name self.method = method - self.arguments = arguments + self.arguments = arguments or {} # names of method return values - self.return_names = return_names + self.return_names = return_names or [] # names of method return values that will be part of the outputs. They are assumed to be safe to be shown to the user. - self.outputs = outputs + self.outputs = outputs or [] + # stage of the step execution: 0 = not started, 1 = in progress, 2 = completed self.stage = 0 class StepOutput: - def __init__(self, step_id: str, return_name: str): + def __init__(self, step_id: str, return_name: str) -> None: self.step_id = step_id self.return_name = return_name diff --git a/docker-qgis/requirements_libqfieldsync.txt b/docker-qgis/requirements_libqfieldsync.txt index 84d35d89a..23e9f9ae2 100644 --- a/docker-qgis/requirements_libqfieldsync.txt +++ b/docker-qgis/requirements_libqfieldsync.txt @@ -1 +1 @@ -libqfieldsync @ git+https://github.com/opengisch/libqfieldsync@62853968a3cc98953ff7f41f8770a7f9d1b6a80e +libqfieldsync @ git+https://github.com/opengisch/libqfieldsync@043fe5ae4d9904dbba7ef6605f30f62e229b0f73