mirror of
https://github.com/umap-project/umap.git
synced 2025-05-06 14:31:50 +02:00
Compare commits
37 commits
cee6755c57
...
78762de8cd
Author | SHA1 | Date | |
---|---|---|---|
![]() |
78762de8cd | ||
![]() |
e3fbe0d55b | ||
![]() |
5084c11434 | ||
![]() |
250d8c38d6 | ||
![]() |
8f7e5c7252 | ||
![]() |
c9d532508d | ||
![]() |
6221b709f4 | ||
![]() |
2b2580fa22 | ||
![]() |
26ff82e838 | ||
![]() |
8fa26a02a2 | ||
![]() |
9a900319af | ||
![]() |
e01a526935 | ||
![]() |
8569b827ca | ||
![]() |
2dab2f23b5 | ||
![]() |
f4ff1048bd | ||
![]() |
6310cde28b | ||
![]() |
666a92ec44 | ||
![]() |
2f776dab59 | ||
![]() |
bba9487847 | ||
![]() |
995052d83e | ||
![]() |
7e53e50b9c | ||
![]() |
8e5f46eb95 | ||
![]() |
0f8ebcdf9a | ||
![]() |
177a4edc1d | ||
![]() |
31c8bf95ba | ||
![]() |
f6f42f5e6b | ||
![]() |
d9998efc0f | ||
![]() |
30d9e43cd4 | ||
![]() |
c29df404c8 | ||
![]() |
3f24563a05 | ||
![]() |
64f0926e2d | ||
![]() |
3aa0c8fc82 | ||
![]() |
9e2b207dfd | ||
![]() |
35b541f200 | ||
![]() |
8624209e1b | ||
![]() |
998bf87a0b | ||
![]() |
4bd7bd7d48 |
30 changed files with 1198 additions and 833 deletions
2
Makefile
2
Makefile
|
@ -7,7 +7,7 @@ install: ## Install the dependencies
|
||||||
|
|
||||||
.PHONY: develop
|
.PHONY: develop
|
||||||
develop: ## Install the test and dev dependencies
|
develop: ## Install the test and dev dependencies
|
||||||
python3 -m pip install -e .[test,dev,sync]
|
python3 -m pip install -e .[test,dev,sync,s3]
|
||||||
playwright install
|
playwright install
|
||||||
|
|
||||||
.PHONY: format
|
.PHONY: format
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# Force rtfd to use a recent version of mkdocs
|
# Force rtfd to use a recent version of mkdocs
|
||||||
mkdocs==1.6.1
|
mkdocs==1.6.1
|
||||||
pymdown-extensions==10.12
|
pymdown-extensions==10.12
|
||||||
mkdocs-material==9.5.44
|
mkdocs-material==9.5.47
|
||||||
mkdocs-static-i18n==1.2.3
|
mkdocs-static-i18n==1.2.3
|
||||||
|
|
|
@ -89,13 +89,6 @@ Running uMap / Django with a known SECRET_KEY defeats many of Django’s securit
|
||||||
See [Django documentation for SECRET_KEY](https://docs.djangoproject.com/en/4.2/ref/settings/#secret-key)
|
See [Django documentation for SECRET_KEY](https://docs.djangoproject.com/en/4.2/ref/settings/#secret-key)
|
||||||
|
|
||||||
|
|
||||||
#### SITE_URL
|
|
||||||
|
|
||||||
The final URL of you instance, including the protocol:
|
|
||||||
|
|
||||||
`SITE_URL=http://umap.org`
|
|
||||||
|
|
||||||
|
|
||||||
#### SHORT_SITE_URL
|
#### SHORT_SITE_URL
|
||||||
|
|
||||||
If you have a short domain for sharing links.
|
If you have a short domain for sharing links.
|
||||||
|
@ -108,6 +101,13 @@ Eg.: `SHORT_SITE_URL=https://u.umap.org`
|
||||||
The name of the site, to be used in header and HTML title.
|
The name of the site, to be used in header and HTML title.
|
||||||
|
|
||||||
|
|
||||||
|
#### SITE_URL
|
||||||
|
|
||||||
|
The final URL of you instance, including the protocol:
|
||||||
|
|
||||||
|
`SITE_URL=http://umap.org`
|
||||||
|
|
||||||
|
|
||||||
#### STATIC_ROOT
|
#### STATIC_ROOT
|
||||||
|
|
||||||
Where uMap should store static files (CSS, JS…), must be consistent with your
|
Where uMap should store static files (CSS, JS…), must be consistent with your
|
||||||
|
@ -115,6 +115,12 @@ Nginx configuration.
|
||||||
|
|
||||||
See [Django documentation for STATIC_ROOT](https://docs.djangoproject.com/en/4.2/ref/settings/#static-root)
|
See [Django documentation for STATIC_ROOT](https://docs.djangoproject.com/en/4.2/ref/settings/#static-root)
|
||||||
|
|
||||||
|
|
||||||
|
#### STORAGES
|
||||||
|
|
||||||
|
See [storage](storage.md).
|
||||||
|
|
||||||
|
|
||||||
#### USE_I18N
|
#### USE_I18N
|
||||||
|
|
||||||
Default is True. Set it to False if you don't want uMap to localize the app.
|
Default is True. Set it to False if you don't want uMap to localize the app.
|
||||||
|
|
63
docs/config/storage.md
Normal file
63
docs/config/storage.md
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
# Storage
|
||||||
|
|
||||||
|
uMap stores metadata (such as owner, permissions…) in PostgreSQL, and the data itself (the content of a layer)
|
||||||
|
in geojson format, by default on the local file system, but optionally in a S3 like server.
|
||||||
|
|
||||||
|
This can be configured through the `STORAGES` settings. uMap will use three keys:
|
||||||
|
|
||||||
|
- `default`, used only for the pictogram files, it can use whatever storage suits your needs
|
||||||
|
- `staticfiles`, used to store the static files, it can use whatever storage suits your needs,
|
||||||
|
but by default uses a custom storage that will add hash to the filenames, to be sure they
|
||||||
|
are not kept in any cache after a release
|
||||||
|
- `data`, used to store the layers data. This one should follow the uMap needs, and currently
|
||||||
|
uMap provides only two options: `umap.storage.UmapFileSystem` and `umap.storage.UmapS3`
|
||||||
|
|
||||||
|
## Default settings:
|
||||||
|
|
||||||
|
This will use the file system for everything, including the data.
|
||||||
|
|
||||||
|
```
|
||||||
|
STORAGES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"BACKEND": "umap.storage.UmapFileSystem",
|
||||||
|
},
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "umap.storage.UmapManifestStaticFilesStorage",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using S3
|
||||||
|
|
||||||
|
To use an S3 like server for the layers data, the first thing is to install
|
||||||
|
the needed dependencies: `pip install umap-project[s3]`.
|
||||||
|
|
||||||
|
Then, change the `STORAGES` settings with something like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
STORAGES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"BACKEND": "umap.storage.UmapS3",
|
||||||
|
"OPTIONS": {
|
||||||
|
"access_key": "xxx",
|
||||||
|
"secret_key": "yyy",
|
||||||
|
"bucket_name": "umap",
|
||||||
|
"region_name": "eu",
|
||||||
|
"endpoint_url": "http://127.0.0.1:9000",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "umap.storage.UmapManifestStaticFilesStorage",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In order to store old versions of a layer, the versioning should be activated in the bucket.
|
||||||
|
|
||||||
|
See more about the configuration on the [django-storages documentation](https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html).
|
|
@ -1,5 +1,5 @@
|
||||||
# Force rtfd to use a recent version of mkdocs
|
# Force rtfd to use a recent version of mkdocs
|
||||||
mkdocs==1.6.1
|
mkdocs==1.6.1
|
||||||
pymdown-extensions==10.12
|
pymdown-extensions==10.12
|
||||||
mkdocs-material==9.5.44
|
mkdocs-material==9.5.47
|
||||||
mkdocs-static-i18n==1.2.3
|
mkdocs-static-i18n==1.2.3
|
||||||
|
|
|
@ -15,6 +15,7 @@ nav:
|
||||||
- Configuration:
|
- Configuration:
|
||||||
- Settings: config/settings.md
|
- Settings: config/settings.md
|
||||||
- Customize: config/customize.md
|
- Customize: config/customize.md
|
||||||
|
- Storage: config/storage.md
|
||||||
- Icon packs: config/icons.md
|
- Icon packs: config/icons.md
|
||||||
- Deployment:
|
- Deployment:
|
||||||
- Docker: deploy/docker.md
|
- Docker: deploy/docker.md
|
||||||
|
|
|
@ -44,10 +44,10 @@ dependencies = [
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
"hatch==1.13.0",
|
"hatch==1.13.0",
|
||||||
"ruff==0.7.4",
|
"ruff==0.8.1",
|
||||||
"djlint==1.36.1",
|
"djlint==1.36.3",
|
||||||
"mkdocs==1.6.1",
|
"mkdocs==1.6.1",
|
||||||
"mkdocs-material==9.5.44",
|
"mkdocs-material==9.5.47",
|
||||||
"mkdocs-static-i18n==1.2.3",
|
"mkdocs-static-i18n==1.2.3",
|
||||||
"vermin==1.6.0",
|
"vermin==1.6.0",
|
||||||
"pymdown-extensions==10.12",
|
"pymdown-extensions==10.12",
|
||||||
|
@ -56,19 +56,23 @@ dev = [
|
||||||
test = [
|
test = [
|
||||||
"factory-boy==3.3.1",
|
"factory-boy==3.3.1",
|
||||||
"playwright>=1.39",
|
"playwright>=1.39",
|
||||||
"pytest==8.3.3",
|
"pytest==8.3.4",
|
||||||
"pytest-django==4.9.0",
|
"pytest-django==4.9.0",
|
||||||
"pytest-playwright==0.5.2",
|
"pytest-playwright==0.6.2",
|
||||||
"pytest-rerunfailures==14.0",
|
"pytest-rerunfailures==15.0",
|
||||||
"pytest-xdist>=3.5.0,<4",
|
"pytest-xdist>=3.5.0,<4",
|
||||||
|
"moto[s3]==5.0.21"
|
||||||
]
|
]
|
||||||
docker = [
|
docker = [
|
||||||
"uwsgi==2.0.28",
|
"uwsgi==2.0.28",
|
||||||
]
|
]
|
||||||
|
s3 = [
|
||||||
|
"django-storages[s3]==1.14.4",
|
||||||
|
]
|
||||||
sync = [
|
sync = [
|
||||||
"channels==4.2.0",
|
"channels==4.2.0",
|
||||||
"daphne==4.1.2",
|
"daphne==4.1.2",
|
||||||
"pydantic==2.9.2",
|
"pydantic==2.10.2",
|
||||||
"websockets==13.1",
|
"websockets==13.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
24
umap/migrations/0024_alter_datalayer_geojson.py
Normal file
24
umap/migrations/0024_alter_datalayer_geojson.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Generated by Django 5.1.2 on 2024-11-29 15:45
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import umap.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("umap", "0023_alter_datalayer_uuid"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="datalayer",
|
||||||
|
name="geojson",
|
||||||
|
field=models.FileField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
storage=umap.models.set_storage,
|
||||||
|
upload_to=umap.models.upload_to,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
131
umap/models.py
131
umap/models.py
|
@ -1,17 +1,12 @@
|
||||||
import json
|
import json
|
||||||
import operator
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import time
|
|
||||||
import uuid
|
import uuid
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.gis.db import models
|
from django.contrib.gis.db import models
|
||||||
from django.core.files.base import File
|
from django.core.files.base import File
|
||||||
|
from django.core.files.storage import storages
|
||||||
from django.core.signing import Signer
|
from django.core.signing import Signer
|
||||||
from django.template.defaultfilters import slugify
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.functional import classproperty
|
from django.utils.functional import classproperty
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
@ -270,7 +265,7 @@ class Map(NamedModel):
|
||||||
umapjson["uri"] = request.build_absolute_uri(self.get_absolute_url())
|
umapjson["uri"] = request.build_absolute_uri(self.get_absolute_url())
|
||||||
datalayers = []
|
datalayers = []
|
||||||
for datalayer in self.datalayer_set.all():
|
for datalayer in self.datalayer_set.all():
|
||||||
with open(datalayer.geojson.path, "rb") as f:
|
with datalayer.geojson.open("rb") as f:
|
||||||
layer = json.loads(f.read())
|
layer = json.loads(f.read())
|
||||||
if datalayer.settings:
|
if datalayer.settings:
|
||||||
layer["_umap_options"] = datalayer.settings
|
layer["_umap_options"] = datalayer.settings
|
||||||
|
@ -423,10 +418,11 @@ class Pictogram(NamedModel):
|
||||||
# Must be out of Datalayer for Django migration to run, because of python 2
|
# Must be out of Datalayer for Django migration to run, because of python 2
|
||||||
# serialize limitations.
|
# serialize limitations.
|
||||||
def upload_to(instance, filename):
|
def upload_to(instance, filename):
|
||||||
if instance.pk:
|
return instance.geojson.storage.make_filename(instance)
|
||||||
return instance.upload_to()
|
|
||||||
name = "%s.geojson" % slugify(instance.name)[:50] or "untitled"
|
|
||||||
return os.path.join(instance.storage_root(), name)
|
def set_storage():
|
||||||
|
return storages["data"]
|
||||||
|
|
||||||
|
|
||||||
class DataLayer(NamedModel):
|
class DataLayer(NamedModel):
|
||||||
|
@ -448,7 +444,9 @@ class DataLayer(NamedModel):
|
||||||
old_id = models.IntegerField(null=True, blank=True)
|
old_id = models.IntegerField(null=True, blank=True)
|
||||||
map = models.ForeignKey(Map, on_delete=models.CASCADE)
|
map = models.ForeignKey(Map, on_delete=models.CASCADE)
|
||||||
description = models.TextField(blank=True, null=True, verbose_name=_("description"))
|
description = models.TextField(blank=True, null=True, verbose_name=_("description"))
|
||||||
geojson = models.FileField(upload_to=upload_to, blank=True, null=True)
|
geojson = models.FileField(
|
||||||
|
upload_to=upload_to, blank=True, null=True, storage=set_storage
|
||||||
|
)
|
||||||
display_on_load = models.BooleanField(
|
display_on_load = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name=_("display on load"),
|
verbose_name=_("display on load"),
|
||||||
|
@ -467,50 +465,14 @@ class DataLayer(NamedModel):
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ("rank",)
|
ordering = ("rank",)
|
||||||
|
|
||||||
def save(self, force_insert=False, force_update=False, **kwargs):
|
def save(self, **kwargs):
|
||||||
is_new = not bool(self.pk)
|
super(DataLayer, self).save(**kwargs)
|
||||||
super(DataLayer, self).save(
|
self.geojson.storage.onDatalayerSave(self)
|
||||||
force_insert=force_insert, force_update=force_update, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
if is_new:
|
|
||||||
force_insert, force_update = False, True
|
|
||||||
filename = self.upload_to()
|
|
||||||
old_name = self.geojson.name
|
|
||||||
new_name = self.geojson.storage.save(filename, self.geojson)
|
|
||||||
self.geojson.storage.delete(old_name)
|
|
||||||
self.geojson.name = new_name
|
|
||||||
super(DataLayer, self).save(
|
|
||||||
force_insert=force_insert, force_update=force_update, **kwargs
|
|
||||||
)
|
|
||||||
self.purge_gzip()
|
|
||||||
self.purge_old_versions()
|
|
||||||
|
|
||||||
def delete(self, **kwargs):
|
def delete(self, **kwargs):
|
||||||
self.purge_gzip()
|
self.geojson.storage.onDatalayerDelete(self)
|
||||||
self.to_purgatory()
|
|
||||||
return super().delete(**kwargs)
|
return super().delete(**kwargs)
|
||||||
|
|
||||||
def to_purgatory(self):
|
|
||||||
dest = Path(settings.UMAP_PURGATORY_ROOT)
|
|
||||||
dest.mkdir(parents=True, exist_ok=True)
|
|
||||||
src = Path(self.geojson.storage.location) / self.storage_root()
|
|
||||||
for version in self.versions:
|
|
||||||
name = version["name"]
|
|
||||||
shutil.move(src / name, dest / f"{self.map.pk}_{name}")
|
|
||||||
|
|
||||||
def upload_to(self):
|
|
||||||
root = self.storage_root()
|
|
||||||
name = "%s_%s.geojson" % (self.pk, int(time.time() * 1000))
|
|
||||||
return os.path.join(root, name)
|
|
||||||
|
|
||||||
def storage_root(self):
|
|
||||||
path = ["datalayer", str(self.map.pk)[-1]]
|
|
||||||
if len(str(self.map.pk)) > 1:
|
|
||||||
path.append(str(self.map.pk)[-2])
|
|
||||||
path.append(str(self.map.pk))
|
|
||||||
return os.path.join(*path)
|
|
||||||
|
|
||||||
def metadata(self, request=None):
|
def metadata(self, request=None):
|
||||||
# Retrocompat: minimal settings for maps not saved after settings property
|
# Retrocompat: minimal settings for maps not saved after settings property
|
||||||
# has been introduced
|
# has been introduced
|
||||||
|
@ -532,74 +494,23 @@ class DataLayer(NamedModel):
|
||||||
new.pk = uuid.uuid4()
|
new.pk = uuid.uuid4()
|
||||||
if map_inst:
|
if map_inst:
|
||||||
new.map = map_inst
|
new.map = map_inst
|
||||||
new.geojson = File(new.geojson.file.file)
|
new.geojson = File(new.geojson.file.file, name="tmpname")
|
||||||
new.save()
|
new.save()
|
||||||
return new
|
return new
|
||||||
|
|
||||||
def is_valid_version(self, name):
|
|
||||||
valid_prefixes = [name.startswith("%s_" % self.pk)]
|
|
||||||
if self.old_id:
|
|
||||||
valid_prefixes.append(name.startswith("%s_" % self.old_id))
|
|
||||||
return any(valid_prefixes) and name.endswith(".geojson")
|
|
||||||
|
|
||||||
def extract_version_number(self, path):
|
|
||||||
version = path.split(".")[0]
|
|
||||||
if "_" in version:
|
|
||||||
return version.split("_")[-1]
|
|
||||||
return version
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def reference_version(self):
|
def reference_version(self):
|
||||||
return self.extract_version_number(self.geojson.path)
|
return self.geojson.storage.get_reference_version(self)
|
||||||
|
|
||||||
def version_metadata(self, name):
|
|
||||||
return {
|
|
||||||
"name": name,
|
|
||||||
"at": self.extract_version_number(name),
|
|
||||||
"size": self.geojson.storage.size(self.get_version_path(name)),
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def versions(self):
|
def versions(self):
|
||||||
root = self.storage_root()
|
return self.geojson.storage.list_versions(self)
|
||||||
names = self.geojson.storage.listdir(root)[1]
|
|
||||||
names = [name for name in names if self.is_valid_version(name)]
|
|
||||||
versions = [self.version_metadata(name) for name in names]
|
|
||||||
versions.sort(reverse=True, key=operator.itemgetter("at"))
|
|
||||||
return versions
|
|
||||||
|
|
||||||
def get_version(self, name):
|
def get_version(self, ref):
|
||||||
path = self.get_version_path(name)
|
return self.geojson.storage.get_version(ref, self)
|
||||||
with self.geojson.storage.open(path, "r") as f:
|
|
||||||
return f.read()
|
|
||||||
|
|
||||||
def get_version_path(self, name):
|
def get_version_path(self, ref):
|
||||||
return "{root}/{name}".format(root=self.storage_root(), name=name)
|
return self.geojson.storage.get_version_path(ref, self)
|
||||||
|
|
||||||
def purge_old_versions(self):
|
|
||||||
root = self.storage_root()
|
|
||||||
versions = self.versions[settings.UMAP_KEEP_VERSIONS :]
|
|
||||||
for version in versions:
|
|
||||||
name = version["name"]
|
|
||||||
# Should not be in the list, but ensure to not delete the file
|
|
||||||
# currently used in database
|
|
||||||
if self.geojson.name.endswith(name):
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
self.geojson.storage.delete(os.path.join(root, name))
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def purge_gzip(self):
|
|
||||||
root = self.storage_root()
|
|
||||||
names = self.geojson.storage.listdir(root)[1]
|
|
||||||
prefixes = [f"{self.pk}_"]
|
|
||||||
if self.old_id:
|
|
||||||
prefixes.append(f"{self.old_id}_")
|
|
||||||
prefixes = tuple(prefixes)
|
|
||||||
for name in names:
|
|
||||||
if name.startswith(prefixes) and name.endswith(".gz"):
|
|
||||||
self.geojson.storage.delete(os.path.join(root, name))
|
|
||||||
|
|
||||||
def can_edit(self, request=None):
|
def can_edit(self, request=None):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -175,6 +175,9 @@ STORAGES = {
|
||||||
"default": {
|
"default": {
|
||||||
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||||
},
|
},
|
||||||
|
"data": {
|
||||||
|
"BACKEND": "umap.storage.UmapFileSystem",
|
||||||
|
},
|
||||||
"staticfiles": {
|
"staticfiles": {
|
||||||
"BACKEND": "umap.storage.UmapManifestStaticFilesStorage",
|
"BACKEND": "umap.storage.UmapManifestStaticFilesStorage",
|
||||||
},
|
},
|
||||||
|
|
|
@ -161,609 +161,6 @@ dt {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* *********** */
|
|
||||||
/* forms */
|
|
||||||
/* *********** */
|
|
||||||
input[type="text"], input[type="password"], input[type="date"],
|
|
||||||
input[type="datetime-local"], input[type="email"], input[type="number"],
|
|
||||||
input[type="search"], input[type="tel"], input[type="time"], input[type="file"],
|
|
||||||
input[type="url"], textarea {
|
|
||||||
background-color: white;
|
|
||||||
border: 1px solid #CCCCCC;
|
|
||||||
border-radius: 2px 2px 2px 2px;
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) inset;
|
|
||||||
color: rgba(0, 0, 0, 0.75);
|
|
||||||
display: block;
|
|
||||||
font-family: inherit;
|
|
||||||
margin: 0;
|
|
||||||
margin-bottom: var(--box-margin);
|
|
||||||
padding: 7px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
input[type="range"] {
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
input[type="radio"] {
|
|
||||||
margin-inline-end: var(--text-margin);
|
|
||||||
}
|
|
||||||
input[type="checkbox"] {
|
|
||||||
margin: 0 var(--text-margin);
|
|
||||||
vertical-align: middle;
|
|
||||||
appearance: none;
|
|
||||||
}
|
|
||||||
input[type="checkbox"]:after {
|
|
||||||
display: inline-block;
|
|
||||||
content: ' ';
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border: 1px solid var(--color-lightGray);
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 0.8rem;
|
|
||||||
}
|
|
||||||
input[type=checkbox]:checked:after {
|
|
||||||
background-color: var(--color-lightCyan);
|
|
||||||
content: '✓';
|
|
||||||
color: var(--color-darkGray);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[data-modified=true] {
|
|
||||||
background-color: var(--color-lightCyan);
|
|
||||||
border: 1px solid var(--color-darkGray);
|
|
||||||
}
|
|
||||||
textarea {
|
|
||||||
height: inherit;
|
|
||||||
padding: 7px;
|
|
||||||
min-height: 15rem;
|
|
||||||
min-height: 6rlh;
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
border: 1px solid #222;
|
|
||||||
width: 100%;
|
|
||||||
height: 28px;
|
|
||||||
line-height: 28px;
|
|
||||||
margin-top: 5px;
|
|
||||||
margin-bottom: var(--box-margin);
|
|
||||||
}
|
|
||||||
.dark select {
|
|
||||||
color: #efefef;
|
|
||||||
background-color: #393F3F;
|
|
||||||
}
|
|
||||||
select[multiple="multiple"] {
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
.button,
|
|
||||||
[type="button"],
|
|
||||||
input[type="submit"] {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 14px;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 2px;
|
|
||||||
font-weight: normal;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 7px;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 32px;
|
|
||||||
line-height: 32px;
|
|
||||||
border: none;
|
|
||||||
text-decoration: none;
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
.dark .button,
|
|
||||||
.dark [type="button"] {
|
|
||||||
background-color: var(--color-darkerGray);
|
|
||||||
color: var(--text-color);
|
|
||||||
border: 1px solid #1b1f20;
|
|
||||||
}
|
|
||||||
.dark .button:hover,
|
|
||||||
.dark [type="button"]:hover,
|
|
||||||
.dark input[type="submit"]:hover {
|
|
||||||
background-color: #2e3436;
|
|
||||||
}
|
|
||||||
.dark a {
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
button.flat,
|
|
||||||
[type="button"].flat,
|
|
||||||
.dark [type="button"].flat {
|
|
||||||
border: none;
|
|
||||||
background-color: inherit;
|
|
||||||
padding: 0;
|
|
||||||
text-align: start;
|
|
||||||
min-height: inherit;
|
|
||||||
width: initial;
|
|
||||||
display: initial;
|
|
||||||
line-height: inherit;
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
button.flat:hover,
|
|
||||||
[type="button"].flat:hover,
|
|
||||||
.dark [type="button"].flat:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
.help-text, .helptext {
|
|
||||||
display: block;
|
|
||||||
padding: 7px 7px;
|
|
||||||
margin-bottom: 14px;
|
|
||||||
background: #393F3F;
|
|
||||||
color: var(--color-lightGray);
|
|
||||||
font-size: 10px;
|
|
||||||
border-radius: 0 2px;
|
|
||||||
}
|
|
||||||
.content .helptext {
|
|
||||||
background-color: #eee;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
input + .help-text {
|
|
||||||
margin-top: -14px;
|
|
||||||
}
|
|
||||||
.formbox {
|
|
||||||
min-height: 36px;
|
|
||||||
margin-bottom: 14px;
|
|
||||||
}
|
|
||||||
.formbox.with-switch {
|
|
||||||
padding-top: 2px;
|
|
||||||
}
|
|
||||||
fieldset.formbox {
|
|
||||||
border: none;
|
|
||||||
border-top: 1px solid var(--color-lightGray);
|
|
||||||
}
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 21px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
label + label {
|
|
||||||
margin-top: var(--box-margin);
|
|
||||||
}
|
|
||||||
.content label {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
input[type="checkbox"] + label {
|
|
||||||
display: inline;
|
|
||||||
padding: 0 14px;
|
|
||||||
}
|
|
||||||
select + .error,
|
|
||||||
input + .error {
|
|
||||||
display: block;
|
|
||||||
padding: 7px 7px;
|
|
||||||
margin-top: -14px;
|
|
||||||
margin-bottom: 14px;
|
|
||||||
background: var(--color-lightGray);
|
|
||||||
color: #fff;
|
|
||||||
background-color: #cc0000;
|
|
||||||
font-size: 11px;
|
|
||||||
border-radius: 0 2px;
|
|
||||||
}
|
|
||||||
input[type="file"] + .error {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
input[value]:invalid {
|
|
||||||
border-color: red;
|
|
||||||
background-color: darkred;
|
|
||||||
}
|
|
||||||
.dark input, .dark textarea {
|
|
||||||
background-color: #232729;
|
|
||||||
border-color: #1b1f20;
|
|
||||||
color: #efefef;
|
|
||||||
}
|
|
||||||
details {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
border-start-start-radius: 4px;
|
|
||||||
border-start-end-radius: 4px;
|
|
||||||
}
|
|
||||||
.dark details {
|
|
||||||
border: 1px solid #222;
|
|
||||||
}
|
|
||||||
details fieldset {
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid var(--color-lightGray);
|
|
||||||
margin: 0;
|
|
||||||
padding-top: 10px;
|
|
||||||
}
|
|
||||||
details summary {
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: var(--color-lightGray);
|
|
||||||
line-height: 30px;
|
|
||||||
font-size: 1.2em;
|
|
||||||
padding: 0 5px;
|
|
||||||
}
|
|
||||||
.dark details summary {
|
|
||||||
background-color: #232729;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.dark details fieldset {
|
|
||||||
border: 1px solid var(--color-darkGray);
|
|
||||||
}
|
|
||||||
fieldset legend {
|
|
||||||
font-size: .9rem;
|
|
||||||
padding: 0 5px;
|
|
||||||
}
|
|
||||||
fieldset.separator {
|
|
||||||
border: none;
|
|
||||||
border-top: 1px solid var(--color-lightGray);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-badge] {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
[data-badge]:after {
|
|
||||||
position: absolute;
|
|
||||||
inset-inline-end: -6px;
|
|
||||||
top: -6px;
|
|
||||||
min-width: 8px;
|
|
||||||
min-height: 8px;
|
|
||||||
line-height: 8px;
|
|
||||||
padding: 2px;
|
|
||||||
font-weight: bold;
|
|
||||||
background-color: var(--color-accent);
|
|
||||||
color: var(--color-darkBlue);
|
|
||||||
text-align: center;
|
|
||||||
font-size: .75rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
content: attr(data-badge);
|
|
||||||
}
|
|
||||||
[hidden] {
|
|
||||||
display: none!important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Switch */
|
|
||||||
input.switch:empty {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
input.switch:empty ~ label {
|
|
||||||
white-space: nowrap;
|
|
||||||
position: relative;
|
|
||||||
float: inline-start;
|
|
||||||
line-height: 2em;
|
|
||||||
height: 2em;
|
|
||||||
text-indent: 6em;
|
|
||||||
margin: 0.2em 0;
|
|
||||||
cursor: pointer;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
text-shadow: 0 1px rgba(0, 0, 0, 0.1);
|
|
||||||
width: 80px;
|
|
||||||
}
|
|
||||||
input.switch:empty ~ label:before,
|
|
||||||
input.switch:empty ~ label:after {
|
|
||||||
position: absolute;
|
|
||||||
display: block;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
inset-inline-start: 0;
|
|
||||||
content: ' ';
|
|
||||||
width: 6em;
|
|
||||||
-webkit-transition: all 100ms ease-in;
|
|
||||||
transition: all 100ms ease-in;
|
|
||||||
color: #c9c9c7;
|
|
||||||
font-weight: bold;
|
|
||||||
background-color: #ededed;
|
|
||||||
}
|
|
||||||
.dark input.switch:empty ~ label:before,
|
|
||||||
.dark input.switch:empty ~ label:after {
|
|
||||||
background-color: #272c2e;
|
|
||||||
}
|
|
||||||
input.switch:empty ~ label:after {
|
|
||||||
width: 3em;
|
|
||||||
margin-inline-start: 0.1em;
|
|
||||||
background-color: #ededed;
|
|
||||||
content: "OFF";
|
|
||||||
text-indent: 3.5em;
|
|
||||||
border: 1px solid #374E75;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.dark input.switch:empty ~ label:after {
|
|
||||||
border: 1px solid #202425;
|
|
||||||
background-color: #2c3233;
|
|
||||||
}
|
|
||||||
input.switch:checked:empty ~ label:after {
|
|
||||||
content: ' ';
|
|
||||||
}
|
|
||||||
.dark input.switch:checked ~ label:before,
|
|
||||||
input.switch:checked ~ label:before {
|
|
||||||
background-color: var(--color-lightCyan);
|
|
||||||
border: 1px solid var(--color-lightGray);
|
|
||||||
color: var(--color-darkGray);
|
|
||||||
content: "ON";
|
|
||||||
text-indent: 0.7em;
|
|
||||||
text-align: start;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.dark input.switch:checked ~ label:before {
|
|
||||||
border: none;
|
|
||||||
background-color: var(--color-accent);
|
|
||||||
}
|
|
||||||
input.switch:checked ~ label:after {
|
|
||||||
margin-inline-start: 3em;
|
|
||||||
}
|
|
||||||
.button-bar, .umap-multiplechoice {
|
|
||||||
margin-top: 5px;
|
|
||||||
text-align: center;
|
|
||||||
display: grid;
|
|
||||||
width: 100%
|
|
||||||
}
|
|
||||||
.button-bar {
|
|
||||||
grid-gap: 7px;
|
|
||||||
}
|
|
||||||
.umap-multiplechoice.by2,
|
|
||||||
.button-bar.half {
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
}
|
|
||||||
.button-bar.by3,
|
|
||||||
.button-bar.by5,
|
|
||||||
.button-bar.by6,
|
|
||||||
.umap-multiplechoice.by3,
|
|
||||||
.umap-multiplechoice.by5,
|
|
||||||
.umap-multiplechoice.by6 {
|
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
|
||||||
}
|
|
||||||
.button-bar.by4,
|
|
||||||
.umap-multiplechoice.by4 {
|
|
||||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
|
||||||
}
|
|
||||||
.button-bar .button,
|
|
||||||
.button-bar [type="button"] {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
.umap-multiplechoice input[type='radio'] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.umap-multiplechoice label {
|
|
||||||
border: 1px solid #374E75;
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: #c9c9c7;
|
|
||||||
min-height: 30px;
|
|
||||||
line-height: 30px;
|
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
.dark .umap-multiplechoice label {
|
|
||||||
border: 1px solid black;
|
|
||||||
background-color: #2c3233;
|
|
||||||
}
|
|
||||||
.umap-multiplechoice input[type='radio']:checked + label {
|
|
||||||
background-color: var(--color-accent);
|
|
||||||
box-shadow: inset 0 0 6px 0px #2c3233;
|
|
||||||
color: var(--color-darkGray);
|
|
||||||
}
|
|
||||||
.inheritable .header,
|
|
||||||
.inheritable {
|
|
||||||
clear: both;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.inheritable .header {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
.inheritable .header label {
|
|
||||||
padding-top: 6px;
|
|
||||||
}
|
|
||||||
.inheritable + .inheritable {
|
|
||||||
border-top: 1px solid #222;
|
|
||||||
padding-top: 5px;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
.umap-field-iconUrl .action-button,
|
|
||||||
.inheritable .define,
|
|
||||||
.inheritable .undefine {
|
|
||||||
float: inline-end;
|
|
||||||
width: initial;
|
|
||||||
min-height: 18px;
|
|
||||||
line-height: 18px;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
.inheritable .quick-actions {
|
|
||||||
float: inline-end;
|
|
||||||
}
|
|
||||||
.inheritable .quick-actions .formbox {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
.inheritable .quick-actions input {
|
|
||||||
width: 100px;
|
|
||||||
margin-inline-end: 5px;
|
|
||||||
}
|
|
||||||
.inheritable .define,
|
|
||||||
.inheritable.undefined .undefine,
|
|
||||||
.inheritable.undefined .show-on-defined {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.inheritable.undefined .define {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
i.info {
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-image: url('./img/16.svg');
|
|
||||||
background-position: -170px -50px;
|
|
||||||
display: inline-block;
|
|
||||||
margin-inline-start: 5px;
|
|
||||||
vertical-align: middle;
|
|
||||||
width: 16px;
|
|
||||||
height: 18px;
|
|
||||||
}
|
|
||||||
.dark i.info {
|
|
||||||
background-image: url('./img/16-white.svg');
|
|
||||||
}
|
|
||||||
.with-transition {
|
|
||||||
transition: all .7s;
|
|
||||||
}
|
|
||||||
.umap-empty:before, .umap-to-polygon:before,
|
|
||||||
.umap-clone:before, .umap-edit:before, .umap-download:before,
|
|
||||||
.umap-to-polyline:before {
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
text-indent: 36px;
|
|
||||||
height: 24px;
|
|
||||||
line-height: 24px;
|
|
||||||
display: inline-block;
|
|
||||||
background-image: url('./img/24.svg');
|
|
||||||
vertical-align: bottom;
|
|
||||||
content: " ";
|
|
||||||
}
|
|
||||||
.dark .umap-empty:before,
|
|
||||||
.dark .umap-to-polygon:before,
|
|
||||||
.dark .umap-clone:before,
|
|
||||||
.dark .umap-edit:before, .dark .umap-download:before,
|
|
||||||
.dark .umap-to-polyline:before {
|
|
||||||
background-image: url('./img/24-white.svg');
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.umap-to-polygon:before {
|
|
||||||
background-position: -72px -42px;
|
|
||||||
}
|
|
||||||
.umap-to-polyline:before {
|
|
||||||
background-position: -106px -42px;
|
|
||||||
}
|
|
||||||
.umap-clone:before {
|
|
||||||
background-position: -144px -78px;
|
|
||||||
}
|
|
||||||
.umap-empty:before {
|
|
||||||
background-position: -108px -78px;
|
|
||||||
}
|
|
||||||
.umap-download:before {
|
|
||||||
background-position: -72px -78px;
|
|
||||||
}
|
|
||||||
.permissions-panel,
|
|
||||||
.umap-upload,
|
|
||||||
.umap-share,
|
|
||||||
.umap-datalayer-container,
|
|
||||||
.umap-layer-properties-container,
|
|
||||||
.umap-browse-data,
|
|
||||||
.umap-tilelayer-switcher-container {
|
|
||||||
padding: 0 10px;
|
|
||||||
}
|
|
||||||
.umap-field-datalist {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-size: 9px;
|
|
||||||
margin-top: -8px;
|
|
||||||
padding: 0 5px;
|
|
||||||
}
|
|
||||||
.flat-tabs {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
font-size: 1.2em;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
border-bottom: 1px solid #bebebe;
|
|
||||||
}
|
|
||||||
.flat-tabs button {
|
|
||||||
padding: 10px;
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: pointer;
|
|
||||||
border-bottom: 1px solid transparent;
|
|
||||||
}
|
|
||||||
.flat-tabs button:hover,
|
|
||||||
.flat-tabs .on {
|
|
||||||
font-weight: bold;
|
|
||||||
border-bottom: 1px solid #444;
|
|
||||||
}
|
|
||||||
.dark .flat-tabs button {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.dark .flat-tabs button:hover,
|
|
||||||
.dark .flat-tabs .on {
|
|
||||||
border-bottom: 1px solid #fff;
|
|
||||||
}
|
|
||||||
.umap-pictogram-category h6 {
|
|
||||||
font-size: 1.3em;
|
|
||||||
}
|
|
||||||
.umap-pictogram-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, 30px);
|
|
||||||
justify-content: space-between;
|
|
||||||
grid-gap: 5px;
|
|
||||||
}
|
|
||||||
.umap-pictogram-choice {
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
line-height: 30px;
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: #999;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
display: block;
|
|
||||||
color: black;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.umap-pictogram-choice img {
|
|
||||||
vertical-align: middle;
|
|
||||||
max-width: 24px;
|
|
||||||
}
|
|
||||||
.umap-pictogram-choice:hover,
|
|
||||||
.umap-color-picker span:hover {
|
|
||||||
background-color: #bebebe;
|
|
||||||
}
|
|
||||||
.umap-pictogram-choice.selected {
|
|
||||||
box-shadow: inset 0 0 0 1px #e9e9e9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.umap-pictogram-choice .leaflet-marker-icon {
|
|
||||||
bottom: 0;
|
|
||||||
inset-inline-start: 30px;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
.umap-color-picker {
|
|
||||||
clear: both;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
overflow: hidden;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.umap-color-picker span {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
display: block;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
float: inline-start;
|
|
||||||
}
|
|
||||||
input.blur {
|
|
||||||
width: calc(100% - 40px);
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
border-start-end-radius: 0;
|
|
||||||
border-end-end-radius: 0;
|
|
||||||
}
|
|
||||||
.blur + .button:before,
|
|
||||||
.blur + [type="button"]:before {
|
|
||||||
content: '✔';
|
|
||||||
}
|
|
||||||
.blur + .button,
|
|
||||||
.blur + [type="button"] {
|
|
||||||
width: 40px;
|
|
||||||
height: 18px;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
line-height: 18px;
|
|
||||||
border-start-start-radius: 0;
|
|
||||||
border-end-start-radius: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
input[type=hidden].blur + .button,
|
|
||||||
input[type=hidden].blur + [type="button"] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.copiable-input {
|
|
||||||
display: flex;
|
|
||||||
align-items: end;
|
|
||||||
}
|
|
||||||
.copiable-input input {
|
|
||||||
border-radius: initial;
|
|
||||||
}
|
|
||||||
.copiable-input button {
|
|
||||||
background-position: -46px -92px;
|
|
||||||
display: inline;
|
|
||||||
padding: 0 10px;
|
|
||||||
height: 32px;
|
|
||||||
width: 32px;
|
|
||||||
border: 1px solid #202425;
|
|
||||||
border-radius: initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* *********** */
|
/* *********** */
|
||||||
/* Panel */
|
/* Panel */
|
||||||
|
|
|
@ -65,7 +65,9 @@ body.login header {
|
||||||
.login-grid .login-openstreetmap-oauth2 {
|
.login-grid .login-openstreetmap-oauth2 {
|
||||||
background-image: url("./openstreetmap.png");
|
background-image: url("./openstreetmap.png");
|
||||||
}
|
}
|
||||||
|
.login-grid .login-keycloak {
|
||||||
|
background-image: url("./keycloak.png");
|
||||||
|
}
|
||||||
|
|
||||||
/* **************************** */
|
/* **************************** */
|
||||||
/* home */
|
/* home */
|
||||||
|
|
604
umap/static/umap/css/form.css
Normal file
604
umap/static/umap/css/form.css
Normal file
|
@ -0,0 +1,604 @@
|
||||||
|
input[type="text"], input[type="password"], input[type="date"],
|
||||||
|
input[type="datetime-local"], input[type="email"], input[type="number"],
|
||||||
|
input[type="search"], input[type="tel"], input[type="time"], input[type="file"],
|
||||||
|
input[type="url"], textarea {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #CCCCCC;
|
||||||
|
border-radius: 2px 2px 2px 2px;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) inset;
|
||||||
|
color: rgba(0, 0, 0, 0.75);
|
||||||
|
display: block;
|
||||||
|
font-family: inherit;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: var(--box-margin);
|
||||||
|
padding: 7px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
input[type="range"] {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
input[type="radio"] {
|
||||||
|
margin-inline-end: var(--text-margin);
|
||||||
|
}
|
||||||
|
input[type="checkbox"] {
|
||||||
|
margin: 0 var(--text-margin);
|
||||||
|
vertical-align: middle;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
input[type="checkbox"]:after {
|
||||||
|
display: inline-block;
|
||||||
|
content: ' ';
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border: 1px solid var(--color-lightGray);
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 0.8rem;
|
||||||
|
}
|
||||||
|
input[type=checkbox]:checked:after {
|
||||||
|
background-color: var(--color-lightCyan);
|
||||||
|
content: '✓';
|
||||||
|
color: var(--color-darkGray);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[data-modified=true] {
|
||||||
|
background-color: var(--color-lightCyan);
|
||||||
|
border: 1px solid var(--color-darkGray);
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
height: inherit;
|
||||||
|
padding: 7px;
|
||||||
|
min-height: 15rem;
|
||||||
|
min-height: 6rlh;
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
border: 1px solid #222;
|
||||||
|
width: 100%;
|
||||||
|
height: 28px;
|
||||||
|
line-height: 28px;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: var(--box-margin);
|
||||||
|
}
|
||||||
|
.dark select {
|
||||||
|
color: #efefef;
|
||||||
|
background-color: #393F3F;
|
||||||
|
}
|
||||||
|
select[multiple="multiple"] {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.button,
|
||||||
|
[type="button"],
|
||||||
|
input[type="submit"] {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-weight: normal;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 7px;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
border: none;
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
.dark .button,
|
||||||
|
.dark [type="button"] {
|
||||||
|
background-color: var(--color-darkerGray);
|
||||||
|
color: var(--text-color);
|
||||||
|
border: 1px solid #1b1f20;
|
||||||
|
}
|
||||||
|
.dark .button:hover,
|
||||||
|
.dark [type="button"]:hover,
|
||||||
|
.dark input[type="submit"]:hover {
|
||||||
|
background-color: #2e3436;
|
||||||
|
}
|
||||||
|
.dark a {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
button.flat,
|
||||||
|
[type="button"].flat,
|
||||||
|
.dark [type="button"].flat {
|
||||||
|
border: none;
|
||||||
|
background-color: inherit;
|
||||||
|
padding: 0;
|
||||||
|
text-align: start;
|
||||||
|
min-height: inherit;
|
||||||
|
width: initial;
|
||||||
|
display: initial;
|
||||||
|
line-height: inherit;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
button.flat:hover,
|
||||||
|
[type="button"].flat:hover,
|
||||||
|
.dark [type="button"].flat:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.help-text, .helptext {
|
||||||
|
display: block;
|
||||||
|
padding: 7px 7px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
background: #393F3F;
|
||||||
|
color: var(--color-lightGray);
|
||||||
|
font-size: 10px;
|
||||||
|
border-radius: 0 2px;
|
||||||
|
}
|
||||||
|
.content .helptext {
|
||||||
|
background-color: #eee;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
input + .help-text {
|
||||||
|
margin-top: -14px;
|
||||||
|
}
|
||||||
|
.formbox {
|
||||||
|
min-height: 36px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.formbox.with-switch {
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
|
fieldset.formbox {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--color-lightGray);
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 21px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
label + label {
|
||||||
|
margin-top: var(--box-margin);
|
||||||
|
}
|
||||||
|
.content label {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
input[type="checkbox"] + label {
|
||||||
|
display: inline;
|
||||||
|
padding: 0 14px;
|
||||||
|
}
|
||||||
|
select + .error,
|
||||||
|
input + .error {
|
||||||
|
display: block;
|
||||||
|
padding: 7px 7px;
|
||||||
|
margin-top: -14px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
background: var(--color-lightGray);
|
||||||
|
color: #fff;
|
||||||
|
background-color: #cc0000;
|
||||||
|
font-size: 11px;
|
||||||
|
border-radius: 0 2px;
|
||||||
|
}
|
||||||
|
input[type="file"] + .error {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
input[value]:invalid {
|
||||||
|
border-color: red;
|
||||||
|
background-color: darkred;
|
||||||
|
}
|
||||||
|
.dark input, .dark textarea {
|
||||||
|
background-color: #232729;
|
||||||
|
border-color: #1b1f20;
|
||||||
|
color: #efefef;
|
||||||
|
}
|
||||||
|
details {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
border-start-start-radius: 4px;
|
||||||
|
border-start-end-radius: 4px;
|
||||||
|
}
|
||||||
|
.dark details {
|
||||||
|
border: 1px solid #222;
|
||||||
|
}
|
||||||
|
details fieldset {
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--color-lightGray);
|
||||||
|
margin: 0;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
details summary {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--color-lightGray);
|
||||||
|
line-height: 30px;
|
||||||
|
font-size: 1.2em;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
.dark details summary {
|
||||||
|
background-color: #232729;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.dark details fieldset {
|
||||||
|
border: 1px solid var(--color-darkGray);
|
||||||
|
}
|
||||||
|
fieldset legend {
|
||||||
|
font-size: .9rem;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
fieldset.separator {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--color-lightGray);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-badge] {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
[data-badge]:after {
|
||||||
|
position: absolute;
|
||||||
|
inset-inline-end: -6px;
|
||||||
|
top: -6px;
|
||||||
|
min-width: 8px;
|
||||||
|
min-height: 8px;
|
||||||
|
line-height: 8px;
|
||||||
|
padding: 2px;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
color: var(--color-darkBlue);
|
||||||
|
text-align: center;
|
||||||
|
font-size: .75rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
content: attr(data-badge);
|
||||||
|
}
|
||||||
|
[hidden] {
|
||||||
|
display: none!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Switch */
|
||||||
|
input.switch:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
input.switch:empty ~ label {
|
||||||
|
white-space: nowrap;
|
||||||
|
position: relative;
|
||||||
|
float: inline-start;
|
||||||
|
line-height: 2em;
|
||||||
|
height: 2em;
|
||||||
|
text-indent: 6em;
|
||||||
|
margin: 0.2em 0;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
text-shadow: 0 1px rgba(0, 0, 0, 0.1);
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
input.switch:empty ~ label:before,
|
||||||
|
input.switch:empty ~ label:after {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
inset-inline-start: 0;
|
||||||
|
content: ' ';
|
||||||
|
width: 6em;
|
||||||
|
-webkit-transition: all 100ms ease-in;
|
||||||
|
transition: all 100ms ease-in;
|
||||||
|
color: #c9c9c7;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #ededed;
|
||||||
|
}
|
||||||
|
.dark input.switch:empty ~ label:before,
|
||||||
|
.dark input.switch:empty ~ label:after {
|
||||||
|
background-color: #272c2e;
|
||||||
|
}
|
||||||
|
input.switch:empty ~ label:after {
|
||||||
|
width: 3em;
|
||||||
|
margin-inline-start: 0.1em;
|
||||||
|
background-color: #ededed;
|
||||||
|
content: "OFF";
|
||||||
|
text-indent: 3.5em;
|
||||||
|
border: 1px solid #374E75;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.dark input.switch:empty ~ label:after {
|
||||||
|
border: 1px solid #202425;
|
||||||
|
background-color: #2c3233;
|
||||||
|
}
|
||||||
|
input.switch:checked:empty ~ label:after {
|
||||||
|
content: ' ';
|
||||||
|
}
|
||||||
|
.dark input.switch:checked ~ label:before,
|
||||||
|
input.switch:checked ~ label:before {
|
||||||
|
background-color: var(--color-lightCyan);
|
||||||
|
border: 1px solid var(--color-lightGray);
|
||||||
|
color: var(--color-darkGray);
|
||||||
|
content: "ON";
|
||||||
|
text-indent: 0.7em;
|
||||||
|
text-align: start;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.dark input.switch:checked ~ label:before {
|
||||||
|
border: none;
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
input.switch:checked ~ label:after {
|
||||||
|
margin-inline-start: 3em;
|
||||||
|
}
|
||||||
|
.button-bar, .umap-multiplechoice {
|
||||||
|
margin-top: 5px;
|
||||||
|
text-align: center;
|
||||||
|
display: grid;
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
.button-bar {
|
||||||
|
grid-gap: 7px;
|
||||||
|
}
|
||||||
|
.umap-multiplechoice.by2,
|
||||||
|
.button-bar.half {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
.button-bar.by3,
|
||||||
|
.button-bar.by5,
|
||||||
|
.button-bar.by6,
|
||||||
|
.umap-multiplechoice.by3,
|
||||||
|
.umap-multiplechoice.by5,
|
||||||
|
.umap-multiplechoice.by6 {
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
.button-bar.by4,
|
||||||
|
.umap-multiplechoice.by4 {
|
||||||
|
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
.button-bar .button,
|
||||||
|
.button-bar [type="button"] {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.umap-multiplechoice input[type='radio'] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.umap-multiplechoice label {
|
||||||
|
border: 1px solid #374E75;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #c9c9c7;
|
||||||
|
min-height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.dark .umap-multiplechoice label {
|
||||||
|
border: 1px solid black;
|
||||||
|
background-color: #2c3233;
|
||||||
|
}
|
||||||
|
.umap-multiplechoice input[type='radio']:checked + label {
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
box-shadow: inset 0 0 6px 0px #2c3233;
|
||||||
|
color: var(--color-darkGray);
|
||||||
|
}
|
||||||
|
.inheritable .header,
|
||||||
|
.inheritable {
|
||||||
|
clear: both;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.inheritable .header {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.inheritable .header label {
|
||||||
|
padding-top: 6px;
|
||||||
|
}
|
||||||
|
.inheritable + .inheritable {
|
||||||
|
border-top: 1px solid #222;
|
||||||
|
padding-top: 5px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
.umap-field-iconUrl .action-button,
|
||||||
|
.inheritable .define,
|
||||||
|
.inheritable .undefine {
|
||||||
|
float: inline-end;
|
||||||
|
width: initial;
|
||||||
|
min-height: 18px;
|
||||||
|
line-height: 18px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.inheritable .quick-actions {
|
||||||
|
float: inline-end;
|
||||||
|
}
|
||||||
|
.inheritable .quick-actions .formbox {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.inheritable .quick-actions input {
|
||||||
|
width: 100px;
|
||||||
|
margin-inline-end: 5px;
|
||||||
|
}
|
||||||
|
.inheritable .define,
|
||||||
|
.inheritable.undefined .undefine,
|
||||||
|
.inheritable.undefined .show-on-defined {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.inheritable.undefined .define {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
i.info {
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-image: url('../img/16.svg');
|
||||||
|
background-position: -170px -50px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-inline-start: 5px;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 16px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
.dark i.info {
|
||||||
|
background-image: url('../img/16-white.svg');
|
||||||
|
}
|
||||||
|
.with-transition {
|
||||||
|
transition: all .7s;
|
||||||
|
}
|
||||||
|
.umap-empty:before, .umap-to-polygon:before,
|
||||||
|
.umap-clone:before, .umap-edit:before, .umap-download:before,
|
||||||
|
.umap-to-polyline:before {
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
text-indent: 36px;
|
||||||
|
height: 24px;
|
||||||
|
line-height: 24px;
|
||||||
|
display: inline-block;
|
||||||
|
background-image: url('../img/24.svg');
|
||||||
|
vertical-align: bottom;
|
||||||
|
content: " ";
|
||||||
|
}
|
||||||
|
.dark .umap-empty:before,
|
||||||
|
.dark .umap-to-polygon:before,
|
||||||
|
.dark .umap-clone:before,
|
||||||
|
.dark .umap-edit:before, .dark .umap-download:before,
|
||||||
|
.dark .umap-to-polyline:before {
|
||||||
|
background-image: url('../img/24-white.svg');
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.umap-to-polygon:before {
|
||||||
|
background-position: -72px -42px;
|
||||||
|
}
|
||||||
|
.umap-to-polyline:before {
|
||||||
|
background-position: -106px -42px;
|
||||||
|
}
|
||||||
|
.umap-clone:before {
|
||||||
|
background-position: -144px -78px;
|
||||||
|
}
|
||||||
|
.umap-empty:before {
|
||||||
|
background-position: -108px -78px;
|
||||||
|
}
|
||||||
|
.umap-download:before {
|
||||||
|
background-position: -72px -78px;
|
||||||
|
}
|
||||||
|
.permissions-panel,
|
||||||
|
.umap-upload,
|
||||||
|
.umap-share,
|
||||||
|
.umap-datalayer-container,
|
||||||
|
.umap-layer-properties-container,
|
||||||
|
.umap-browse-data,
|
||||||
|
.umap-tilelayer-switcher-container {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
.umap-field-datalist {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 9px;
|
||||||
|
margin-top: -8px;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
.flat-tabs {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
font-size: 1.2em;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #bebebe;
|
||||||
|
}
|
||||||
|
.flat-tabs button {
|
||||||
|
padding: 10px;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
}
|
||||||
|
.flat-tabs button:hover,
|
||||||
|
.flat-tabs .on {
|
||||||
|
font-weight: bold;
|
||||||
|
border-bottom: 1px solid #444;
|
||||||
|
}
|
||||||
|
.dark .flat-tabs button {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.dark .flat-tabs button:hover,
|
||||||
|
.dark .flat-tabs .on {
|
||||||
|
border-bottom: 1px solid #fff;
|
||||||
|
}
|
||||||
|
.umap-pictogram-category h6 {
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
.umap-pictogram-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, 30px);
|
||||||
|
justify-content: space-between;
|
||||||
|
grid-gap: 5px;
|
||||||
|
}
|
||||||
|
.umap-pictogram-choice {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #999;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
display: block;
|
||||||
|
color: black;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.umap-pictogram-choice img {
|
||||||
|
vertical-align: middle;
|
||||||
|
max-width: 24px;
|
||||||
|
}
|
||||||
|
.umap-pictogram-choice:hover,
|
||||||
|
.umap-color-picker span:hover {
|
||||||
|
background-color: #bebebe;
|
||||||
|
}
|
||||||
|
.umap-pictogram-choice.selected {
|
||||||
|
box-shadow: inset 0 0 0 1px #e9e9e9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.umap-pictogram-choice .leaflet-marker-icon {
|
||||||
|
bottom: 0;
|
||||||
|
inset-inline-start: 30px;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.umap-color-picker {
|
||||||
|
clear: both;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.umap-color-picker span {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
float: inline-start;
|
||||||
|
}
|
||||||
|
input.blur {
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-start-end-radius: 0;
|
||||||
|
border-end-end-radius: 0;
|
||||||
|
}
|
||||||
|
.blur + .button:before,
|
||||||
|
.blur + [type="button"]:before {
|
||||||
|
content: '✔';
|
||||||
|
}
|
||||||
|
.blur + .button,
|
||||||
|
.blur + [type="button"] {
|
||||||
|
width: 40px;
|
||||||
|
height: 18px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 18px;
|
||||||
|
border-start-start-radius: 0;
|
||||||
|
border-end-start-radius: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
input[type=hidden].blur + .button,
|
||||||
|
input[type=hidden].blur + [type="button"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.copiable-input {
|
||||||
|
display: flex;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
.copiable-input input {
|
||||||
|
border-radius: initial;
|
||||||
|
}
|
||||||
|
.copiable-input button {
|
||||||
|
background-position: -46px -92px;
|
||||||
|
display: inline;
|
||||||
|
padding: 0 10px;
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
border: 1px solid #202425;
|
||||||
|
border-radius: initial;
|
||||||
|
}
|
||||||
|
input.highlightable:not(:placeholder-shown) {
|
||||||
|
border: 1px solid var(--color-brightCyan);
|
||||||
|
}
|
|
@ -540,11 +540,11 @@ export class DataLayer extends ServerStored {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getVersionUrl(name) {
|
getVersionUrl(ref) {
|
||||||
return this._umap.urls.get('datalayer_version', {
|
return this._umap.urls.get('datalayer_version', {
|
||||||
pk: this.id,
|
pk: this.id,
|
||||||
map_id: this._umap.id,
|
map_id: this._umap.id,
|
||||||
name: name,
|
ref: ref,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -861,13 +861,7 @@ export class DataLayer extends ServerStored {
|
||||||
const date = new Date(Number.parseInt(data.at, 10))
|
const date = new Date(Number.parseInt(data.at, 10))
|
||||||
const content = `${date.toLocaleString(U.lang)} (${Number.parseInt(data.size) / 1000}Kb)`
|
const content = `${date.toLocaleString(U.lang)} (${Number.parseInt(data.size) / 1000}Kb)`
|
||||||
const el = DomUtil.create('div', 'umap-datalayer-version', versionsContainer)
|
const el = DomUtil.create('div', 'umap-datalayer-version', versionsContainer)
|
||||||
const button = DomUtil.createButton(
|
const button = DomUtil.createButton('', el, '', () => this.restore(data.ref))
|
||||||
'',
|
|
||||||
el,
|
|
||||||
'',
|
|
||||||
() => this.restore(data.name),
|
|
||||||
this
|
|
||||||
)
|
|
||||||
button.title = translate('Restore this version')
|
button.title = translate('Restore this version')
|
||||||
DomUtil.add('span', '', el, content)
|
DomUtil.add('span', '', el, content)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ const TEMPLATE = `
|
||||||
<fieldset class="formbox">
|
<fieldset class="formbox">
|
||||||
<legend class="counter">${translate('Choose data')}</legend>
|
<legend class="counter">${translate('Choose data')}</legend>
|
||||||
<input type="file" multiple autofocus onchange />
|
<input type="file" multiple autofocus onchange />
|
||||||
<input type="url" placeholder="${translate('Provide an URL here')}" onchange />
|
<input class="highlightable" type="url" placeholder="${translate('Provide an URL here')}" onchange />
|
||||||
<textarea onchange placeholder="${translate('Paste your data here')}"></textarea>
|
<textarea onchange placeholder="${translate('Paste your data here')}"></textarea>
|
||||||
<div class="importers" hidden>
|
<div class="importers" hidden>
|
||||||
<h4>${translate('Import helpers:')}</h4>
|
<h4>${translate('Import helpers:')}</h4>
|
||||||
|
|
|
@ -2,6 +2,8 @@ import { DomUtil } from '../../../vendors/leaflet/leaflet-src.esm.js'
|
||||||
import { BaseAjax, SingleMixin } from '../autocomplete.js'
|
import { BaseAjax, SingleMixin } from '../autocomplete.js'
|
||||||
import * as Util from '../utils.js'
|
import * as Util from '../utils.js'
|
||||||
import { AutocompleteCommunes } from './communesfr.js'
|
import { AutocompleteCommunes } from './communesfr.js'
|
||||||
|
import { translate } from '../i18n.js'
|
||||||
|
import { uMapAlert as Alert } from '../../components/alerts/alert.js'
|
||||||
|
|
||||||
const TEMPLATE = `
|
const TEMPLATE = `
|
||||||
<h3>Cadastre</h3>
|
<h3>Cadastre</h3>
|
||||||
|
@ -56,6 +58,8 @@ export class Importer {
|
||||||
.open({
|
.open({
|
||||||
template: container,
|
template: container,
|
||||||
className: `${this.id} importer dark`,
|
className: `${this.id} importer dark`,
|
||||||
|
cancel: false,
|
||||||
|
accept: translate('Choose this data'),
|
||||||
})
|
})
|
||||||
.then(confirm)
|
.then(confirm)
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,8 +37,8 @@ class Autocomplete extends SingleMixin(BaseAjax) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Importer {
|
export class Importer {
|
||||||
constructor(map, options = {}) {
|
constructor(umap, options = {}) {
|
||||||
this.map = map
|
this.umap = umap
|
||||||
this.name = options.name || 'GeoDataMine'
|
this.name = options.name || 'GeoDataMine'
|
||||||
this.baseUrl = options?.url || 'https://geodatamine.fr'
|
this.baseUrl = options?.url || 'https://geodatamine.fr'
|
||||||
this.id = 'geodatamine'
|
this.id = 'geodatamine'
|
||||||
|
@ -49,7 +49,7 @@ export class Importer {
|
||||||
let boundaryName = null
|
let boundaryName = null
|
||||||
const container = DomUtil.create('div')
|
const container = DomUtil.create('div')
|
||||||
container.innerHTML = TEMPLATE
|
container.innerHTML = TEMPLATE
|
||||||
const response = await importer.map.request.get(`${this.baseUrl}/themes`)
|
const response = await this.umap.request.get(`${this.baseUrl}/themes`)
|
||||||
const select = container.querySelector('select')
|
const select = container.querySelector('select')
|
||||||
if (response?.ok) {
|
if (response?.ok) {
|
||||||
const { themes } = await response.json()
|
const { themes } = await response.json()
|
||||||
|
|
|
@ -44,12 +44,12 @@ const ControlsMixin = {
|
||||||
new U.DrawToolbar({ map: this }).addTo(this)
|
new U.DrawToolbar({ map: this }).addTo(this)
|
||||||
const editActions = [
|
const editActions = [
|
||||||
U.EditCaptionAction,
|
U.EditCaptionAction,
|
||||||
U.EditPropertiesAction,
|
U.ImportAction,
|
||||||
U.EditLayersAction,
|
U.EditLayersAction,
|
||||||
U.ChangeTileLayerAction,
|
U.ChangeTileLayerAction,
|
||||||
U.UpdateExtentAction,
|
U.UpdateExtentAction,
|
||||||
U.UpdatePermsAction,
|
U.UpdatePermsAction,
|
||||||
U.ImportAction,
|
U.EditPropertiesAction,
|
||||||
]
|
]
|
||||||
if (this.options.editMode === 'advanced') {
|
if (this.options.editMode === 'advanced') {
|
||||||
new U.SettingsToolbar({ actions: editActions }).addTo(this)
|
new U.SettingsToolbar({ actions: editActions }).addTo(this)
|
||||||
|
|
|
@ -255,7 +255,10 @@ const PathMixin = {
|
||||||
if (this._map.measureTools?.enabled()) {
|
if (this._map.measureTools?.enabled()) {
|
||||||
this._map._umap.tooltip.open({ content: this.getMeasure(), anchor: this })
|
this._map._umap.tooltip.open({ content: this.getMeasure(), anchor: this })
|
||||||
} else if (this._map._umap.editEnabled && !this._map._umap.editedFeature) {
|
} else if (this._map._umap.editEnabled && !this._map._umap.editedFeature) {
|
||||||
this._map._umap.tooltip.open({ content: translate('Click to edit'), anchor: this })
|
this._map._umap.tooltip.open({
|
||||||
|
content: translate('Click to edit'),
|
||||||
|
anchor: this,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -267,7 +270,9 @@ const PathMixin = {
|
||||||
this._map.once('moveend', this.makeGeometryEditable, this)
|
this._map.once('moveend', this.makeGeometryEditable, this)
|
||||||
const pointsCount = this._parts.reduce((acc, part) => acc + part.length, 0)
|
const pointsCount = this._parts.reduce((acc, part) => acc + part.length, 0)
|
||||||
if (pointsCount > 100 && this._map.getZoom() < this._map.getMaxZoom()) {
|
if (pointsCount > 100 && this._map.getZoom() < this._map.getMaxZoom()) {
|
||||||
this._map._umap.tooltip.open({ content: L._('Please zoom in to edit the geometry') })
|
this._map._umap.tooltip.open({
|
||||||
|
content: L._('Please zoom in to edit the geometry'),
|
||||||
|
})
|
||||||
this.disableEdit()
|
this.disableEdit()
|
||||||
} else {
|
} else {
|
||||||
this.enableEdit()
|
this.enableEdit()
|
||||||
|
@ -380,8 +385,19 @@ export const LeafletPolyline = Polyline.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
getMeasure: function (shape) {
|
getMeasure: function (shape) {
|
||||||
|
let shapes
|
||||||
|
if (shape) {
|
||||||
|
shapes = [shape]
|
||||||
|
} else if (LineUtil.isFlat(this._latlngs)) {
|
||||||
|
shapes = [this._latlngs]
|
||||||
|
} else {
|
||||||
|
shapes = this._latlngs
|
||||||
|
}
|
||||||
// FIXME: compute from data in feature (with TurfJS)
|
// FIXME: compute from data in feature (with TurfJS)
|
||||||
const length = L.GeoUtil.lineLength(this._map, shape || this._defaultShape())
|
const length = shapes.reduce(
|
||||||
|
(acc, shape) => acc + L.GeoUtil.lineLength(this._map, shape),
|
||||||
|
0
|
||||||
|
)
|
||||||
return L.GeoUtil.readableDistance(length, this._map.measureTools.getMeasureUnit())
|
return L.GeoUtil.readableDistance(length, this._map.measureTools.getMeasureUnit())
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -537,10 +537,10 @@ export default class Umap extends ServerStored {
|
||||||
this._leafletMap.editTools.startPolyline()
|
this._leafletMap.editTools.startPolyline()
|
||||||
break
|
break
|
||||||
case 'i':
|
case 'i':
|
||||||
this._leafletMap.importer.open()
|
this.importer.open()
|
||||||
break
|
break
|
||||||
case 'o':
|
case 'o':
|
||||||
this._leafletMap.importer.openFiles()
|
this.importer.openFiles()
|
||||||
break
|
break
|
||||||
case 'h':
|
case 'h':
|
||||||
this.help.showGetStarted()
|
this.help.showGetStarted()
|
||||||
|
@ -596,7 +596,7 @@ export default class Umap extends ServerStored {
|
||||||
const panes = this._leafletMap.getPane('overlayPane')
|
const panes = this._leafletMap.getPane('overlayPane')
|
||||||
|
|
||||||
this.datalayersIndex = []
|
this.datalayersIndex = []
|
||||||
for (const pane of panes) {
|
for (const pane of panes.children) {
|
||||||
if (!pane.dataset || !pane.dataset.id) continue
|
if (!pane.dataset || !pane.dataset.id) continue
|
||||||
this.datalayersIndex.push(this.datalayers[pane.dataset.id])
|
this.datalayersIndex.push(this.datalayers[pane.dataset.id])
|
||||||
}
|
}
|
||||||
|
@ -1425,13 +1425,13 @@ export default class Umap extends ServerStored {
|
||||||
row.dataset.id = stamp(datalayer)
|
row.dataset.id = stamp(datalayer)
|
||||||
})
|
})
|
||||||
const onReorder = (src, dst, initialIndex, finalIndex) => {
|
const onReorder = (src, dst, initialIndex, finalIndex) => {
|
||||||
const layer = this.datalayers[src.dataset.id]
|
const movedLayer = this.datalayers[src.dataset.id]
|
||||||
const other = this.datalayers[dst.dataset.id]
|
const targetLayer = this.datalayers[dst.dataset.id]
|
||||||
const minIndex = Math.min(layer.getRank(), other.getRank())
|
const minIndex = Math.min(movedLayer.getRank(), targetLayer.getRank())
|
||||||
const maxIndex = Math.max(layer.getRank(), other.getRank())
|
const maxIndex = Math.max(movedLayer.getRank(), targetLayer.getRank())
|
||||||
if (finalIndex === 0) layer.bringToTop()
|
if (finalIndex === 0) movedLayer.bringToTop()
|
||||||
else if (finalIndex > initialIndex) layer.insertBefore(other)
|
else if (finalIndex > initialIndex) movedLayer.insertBefore(targetLayer)
|
||||||
else layer.insertAfter(other)
|
else movedLayer.insertAfter(targetLayer)
|
||||||
this.eachDataLayerReverse((datalayer) => {
|
this.eachDataLayerReverse((datalayer) => {
|
||||||
if (datalayer.getRank() >= minIndex && datalayer.getRank() <= maxIndex)
|
if (datalayer.getRank() >= minIndex && datalayer.getRank() <= maxIndex)
|
||||||
datalayer.isDirty = true
|
datalayer.isDirty = true
|
||||||
|
|
BIN
umap/static/umap/keycloak.png
Normal file
BIN
umap/static/umap/keycloak.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
155
umap/storage.py
155
umap/storage.py
|
@ -1,9 +1,16 @@
|
||||||
|
import operator
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from botocore.exceptions import ClientError
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.staticfiles.storage import ManifestStaticFilesStorage
|
from django.contrib.staticfiles.storage import ManifestStaticFilesStorage
|
||||||
|
from django.core.files.storage import FileSystemStorage
|
||||||
from rcssmin import cssmin
|
from rcssmin import cssmin
|
||||||
from rjsmin import jsmin
|
from rjsmin import jsmin
|
||||||
|
from storages.backends.s3 import S3Storage
|
||||||
|
|
||||||
|
|
||||||
class UmapManifestStaticFilesStorage(ManifestStaticFilesStorage):
|
class UmapManifestStaticFilesStorage(ManifestStaticFilesStorage):
|
||||||
|
@ -62,3 +69,151 @@ class UmapManifestStaticFilesStorage(ManifestStaticFilesStorage):
|
||||||
minified = cssmin(initial)
|
minified = cssmin(initial)
|
||||||
path.write_text(minified)
|
path.write_text(minified)
|
||||||
yield original_path, processed_path, True
|
yield original_path, processed_path, True
|
||||||
|
|
||||||
|
|
||||||
|
class UmapS3(S3Storage):
|
||||||
|
def get_reference_version(self, instance):
|
||||||
|
metadata = self.connection.meta.client.head_object(
|
||||||
|
Bucket=self.bucket_name, Key=instance.geojson.name
|
||||||
|
)
|
||||||
|
# Do not fail if bucket does not handle versioning
|
||||||
|
return metadata.get("VersionId", metadata["ETag"])
|
||||||
|
|
||||||
|
def make_filename(self, instance):
|
||||||
|
return f"{str(instance.pk)}.geojson"
|
||||||
|
|
||||||
|
def list_versions(self, instance):
|
||||||
|
response = self.connection.meta.client.list_object_versions(
|
||||||
|
Bucket=self.bucket_name, Prefix=instance.geojson.name
|
||||||
|
)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"ref": version["VersionId"],
|
||||||
|
"at": version["LastModified"].timestamp() * 1000,
|
||||||
|
"size": version["Size"],
|
||||||
|
}
|
||||||
|
for version in response["Versions"]
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_version(self, ref, instance):
|
||||||
|
try:
|
||||||
|
data = self.connection.meta.client.get_object(
|
||||||
|
Bucket=self.bucket_name,
|
||||||
|
Key=instance.geojson.name,
|
||||||
|
VersionId=ref,
|
||||||
|
)
|
||||||
|
except ClientError:
|
||||||
|
raise ValueError(f"Invalid version reference: {ref}")
|
||||||
|
return data["Body"].read()
|
||||||
|
|
||||||
|
def get_version_path(self, ref, instance):
|
||||||
|
return self.url(instance.geojson.name, parameters={"VersionId": ref})
|
||||||
|
|
||||||
|
def onDatalayerSave(self, instance):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def onDatalayerDelete(self, instance):
|
||||||
|
return self.connection.meta.client.delete_object(
|
||||||
|
Bucket=self.bucket_name,
|
||||||
|
Key=instance.geojson.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UmapFileSystem(FileSystemStorage):
|
||||||
|
def get_reference_version(self, instance):
|
||||||
|
return self._extract_version_ref(instance.geojson.name)
|
||||||
|
|
||||||
|
def make_filename(self, instance):
|
||||||
|
root = self._base_path(instance)
|
||||||
|
name = "%s_%s.geojson" % (instance.pk, int(time.time() * 1000))
|
||||||
|
return root / name
|
||||||
|
|
||||||
|
def list_versions(self, instance):
|
||||||
|
root = self._base_path(instance)
|
||||||
|
names = self.listdir(root)[1]
|
||||||
|
names = [name for name in names if self._is_valid_version(name, instance)]
|
||||||
|
versions = [self._version_metadata(name, instance) for name in names]
|
||||||
|
versions.sort(reverse=True, key=operator.itemgetter("at"))
|
||||||
|
return versions
|
||||||
|
|
||||||
|
def get_version(self, ref, instance):
|
||||||
|
with self.open(self.get_version_path(ref, instance), "r") as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
def get_version_path(self, ref, instance):
|
||||||
|
base_path = Path(settings.MEDIA_ROOT) / self._base_path(instance)
|
||||||
|
fullpath = base_path / f"{instance.pk}_{ref}.geojson"
|
||||||
|
if instance.old_id and not fullpath.exists():
|
||||||
|
fullpath = base_path / f"{instance.old_id}_{ref}.geojson"
|
||||||
|
if not fullpath.exists():
|
||||||
|
raise ValueError(f"Invalid version reference: {ref}")
|
||||||
|
return fullpath
|
||||||
|
|
||||||
|
def onDatalayerSave(self, instance):
|
||||||
|
self._purge_gzip(instance)
|
||||||
|
self._purge_old_versions(instance)
|
||||||
|
|
||||||
|
def onDatalayerDelete(self, instance):
|
||||||
|
self._purge_gzip(instance)
|
||||||
|
self._to_purgatory(instance)
|
||||||
|
|
||||||
|
def _extract_version_ref(self, path):
|
||||||
|
version = path.split(".")[0]
|
||||||
|
if "_" in version:
|
||||||
|
return version.split("_")[-1]
|
||||||
|
return version
|
||||||
|
|
||||||
|
def _base_path(self, instance):
|
||||||
|
path = ["datalayer", str(instance.map.pk)[-1]]
|
||||||
|
if len(str(instance.map.pk)) > 1:
|
||||||
|
path.append(str(instance.map.pk)[-2])
|
||||||
|
path.append(str(instance.map.pk))
|
||||||
|
return Path(os.path.join(*path))
|
||||||
|
|
||||||
|
def _is_valid_version(self, name, instance):
|
||||||
|
valid_prefixes = [name.startswith("%s_" % instance.pk)]
|
||||||
|
if instance.old_id:
|
||||||
|
valid_prefixes.append(name.startswith("%s_" % instance.old_id))
|
||||||
|
return any(valid_prefixes) and name.endswith(".geojson")
|
||||||
|
|
||||||
|
def _version_metadata(self, name, instance):
|
||||||
|
ref = self._extract_version_ref(name)
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"ref": ref,
|
||||||
|
"at": ref,
|
||||||
|
"size": self.size(self._base_path(instance) / name),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _purge_old_versions(self, instance):
|
||||||
|
root = self._base_path(instance)
|
||||||
|
versions = self.list_versions(instance)[settings.UMAP_KEEP_VERSIONS :]
|
||||||
|
for version in versions:
|
||||||
|
name = version["name"]
|
||||||
|
# Should not be in the list, but ensure to not delete the file
|
||||||
|
# currently used in database
|
||||||
|
if instance.geojson.name.endswith(name):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
self.delete(root / name)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _purge_gzip(self, instance):
|
||||||
|
root = self._base_path(instance)
|
||||||
|
names = self.listdir(root)[1]
|
||||||
|
prefixes = [f"{instance.pk}_"]
|
||||||
|
if instance.old_id:
|
||||||
|
prefixes.append(f"{instance.old_id}_")
|
||||||
|
prefixes = tuple(prefixes)
|
||||||
|
for name in names:
|
||||||
|
if name.startswith(prefixes) and name.endswith(".gz"):
|
||||||
|
self.delete(root / name)
|
||||||
|
|
||||||
|
def _to_purgatory(self, instance):
|
||||||
|
dest = Path(settings.UMAP_PURGATORY_ROOT)
|
||||||
|
dest.mkdir(parents=True, exist_ok=True)
|
||||||
|
src = Path(self.location) / self._base_path(instance)
|
||||||
|
for version in self.list_versions(instance):
|
||||||
|
name = version["name"]
|
||||||
|
shutil.move(src / name, dest / f"{instance.map.pk}_{name}")
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
<link rel="stylesheet" href="{% static 'umap/font.css' %}" />
|
<link rel="stylesheet" href="{% static 'umap/font.css' %}" />
|
||||||
<link rel="stylesheet" href="{% static 'umap/css/icon.css' %}" />
|
<link rel="stylesheet" href="{% static 'umap/css/icon.css' %}" />
|
||||||
<link rel="stylesheet" href="{% static 'umap/base.css' %}" />
|
<link rel="stylesheet" href="{% static 'umap/base.css' %}" />
|
||||||
|
<link rel="stylesheet" href="{% static 'umap/css/form.css' %}" />
|
||||||
<link rel="stylesheet" href="{% static 'umap/content.css' %}" />
|
<link rel="stylesheet" href="{% static 'umap/content.css' %}" />
|
||||||
<link rel="stylesheet" href="{% static 'umap/nav.css' %}" />
|
<link rel="stylesheet" href="{% static 'umap/nav.css' %}" />
|
||||||
<link rel="stylesheet" href="{% static 'umap/map.css' %}" />
|
<link rel="stylesheet" href="{% static 'umap/map.css' %}" />
|
||||||
|
|
|
@ -71,6 +71,7 @@ def test_umap_import_from_file(live_server, tilelayer, page):
|
||||||
expect(nonloaded).to_have_count(1)
|
expect(nonloaded).to_have_count(1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip
|
||||||
def test_umap_import_from_textarea(live_server, tilelayer, page, settings):
|
def test_umap_import_from_textarea(live_server, tilelayer, page, settings):
|
||||||
page.route("https://tile.openstreetmap.fr/hot/**", mock_tiles)
|
page.route("https://tile.openstreetmap.fr/hot/**", mock_tiles)
|
||||||
|
|
||||||
|
|
|
@ -49,3 +49,37 @@ def test_should_open_popup_on_click(live_server, map, page, bootstrap):
|
||||||
# Close popup
|
# Close popup
|
||||||
page.locator("#map").click()
|
page.locator("#map").click()
|
||||||
expect(line).to_have_attribute("stroke-opacity", "0.5")
|
expect(line).to_have_attribute("stroke-opacity", "0.5")
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_use_measure_on_name(live_server, map, page):
|
||||||
|
data = {
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": {"name": "linestring"},
|
||||||
|
"geometry": {
|
||||||
|
"type": "LineString",
|
||||||
|
"coordinates": [
|
||||||
|
[11.25, 53.585984],
|
||||||
|
[10.151367, 52.975108],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": {"name": "multilinestring"},
|
||||||
|
"geometry": {
|
||||||
|
"type": "MultiLineString",
|
||||||
|
"coordinates": [[[8, 53], [13, 52]], [[12, 51], [15, 52]]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
map.settings["properties"]["labelKey"] = "{name} ({measure})"
|
||||||
|
map.settings["properties"]["onLoadPanel"] = "databrowser"
|
||||||
|
map.save()
|
||||||
|
DataLayerFactory(map=map, data=data)
|
||||||
|
page.goto(f"{live_server.url}{map.get_absolute_url()}#6/10/50")
|
||||||
|
expect(page.get_by_text("linestring (99.7 km)")).to_be_visible()
|
||||||
|
expect(page.get_by_text("multilinestring (592 km)")).to_be_visible()
|
||||||
|
|
|
@ -22,10 +22,11 @@ def test_datalayers_should_be_ordered_by_rank(map, datalayer):
|
||||||
assert list(map.datalayer_set.all()) == [c1, c2, c3, c4, datalayer]
|
assert list(map.datalayer_set.all()) == [c1, c2, c3, c4, datalayer]
|
||||||
|
|
||||||
|
|
||||||
def test_upload_to(map, datalayer):
|
def test_upload_to(map):
|
||||||
map.pk = 302
|
map.pk = 302
|
||||||
datalayer.pk = 17
|
map.save()
|
||||||
assert datalayer.upload_to().startswith("datalayer/2/0/302/17_")
|
datalayer = DataLayerFactory(map=map)
|
||||||
|
assert datalayer.geojson.name.startswith(f"datalayer/2/0/302/{datalayer.pk}_")
|
||||||
|
|
||||||
|
|
||||||
def test_save_should_use_pk_as_name(map, datalayer):
|
def test_save_should_use_pk_as_name(map, datalayer):
|
||||||
|
@ -65,7 +66,7 @@ def test_clone_should_clone_geojson_too(datalayer):
|
||||||
def test_should_remove_old_versions_on_save(map, settings):
|
def test_should_remove_old_versions_on_save(map, settings):
|
||||||
datalayer = DataLayerFactory(uuid="0f1161c0-c07f-4ba4-86c5-8d8981d8a813", old_id=17)
|
datalayer = DataLayerFactory(uuid="0f1161c0-c07f-4ba4-86c5-8d8981d8a813", old_id=17)
|
||||||
settings.UMAP_KEEP_VERSIONS = 3
|
settings.UMAP_KEEP_VERSIONS = 3
|
||||||
root = Path(datalayer.storage_root())
|
root = Path(datalayer.geojson.storage._base_path(datalayer))
|
||||||
before = len(datalayer.geojson.storage.listdir(root)[1])
|
before = len(datalayer.geojson.storage.listdir(root)[1])
|
||||||
newer = f"{datalayer.pk}_1440924889.geojson"
|
newer = f"{datalayer.pk}_1440924889.geojson"
|
||||||
medium = f"{datalayer.pk}_1440923687.geojson"
|
medium = f"{datalayer.pk}_1440923687.geojson"
|
||||||
|
@ -275,7 +276,7 @@ def test_anonymous_can_edit_in_inherit_mode_and_map_in_public_mode(
|
||||||
def test_should_remove_all_versions_on_delete(map, settings):
|
def test_should_remove_all_versions_on_delete(map, settings):
|
||||||
settings.UMAP_PURGATORY_ROOT = tempfile.mkdtemp()
|
settings.UMAP_PURGATORY_ROOT = tempfile.mkdtemp()
|
||||||
datalayer = DataLayerFactory(uuid="0f1161c0-c07f-4ba4-86c5-8d8981d8a813", old_id=17)
|
datalayer = DataLayerFactory(uuid="0f1161c0-c07f-4ba4-86c5-8d8981d8a813", old_id=17)
|
||||||
root = Path(datalayer.storage_root())
|
root = Path(datalayer.geojson.storage._base_path(datalayer))
|
||||||
before = len(datalayer.geojson.storage.listdir(root)[1])
|
before = len(datalayer.geojson.storage.listdir(root)[1])
|
||||||
other = "123456_1440918637.geojson"
|
other = "123456_1440918637.geojson"
|
||||||
files = [
|
files = [
|
||||||
|
|
135
umap/tests/test_datalayer_s3.py
Normal file
135
umap/tests/test_datalayer_s3.py
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
import boto3
|
||||||
|
import pytest
|
||||||
|
from botocore.errorfactory import ClientError
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
from django.core.files.storage import storages
|
||||||
|
from moto import mock_aws
|
||||||
|
|
||||||
|
from umap.models import DataLayer
|
||||||
|
|
||||||
|
from .base import DataLayerFactory
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module", autouse=True)
|
||||||
|
def patch_storage():
|
||||||
|
"""Mocked AWS Credentials for moto."""
|
||||||
|
os.environ["AWS_ACCESS_KEY_ID"] = "testing"
|
||||||
|
os.environ["AWS_SECRET_ACCESS_KEY"] = "testing"
|
||||||
|
os.environ["AWS_SECURITY_TOKEN"] = "testing"
|
||||||
|
os.environ["AWS_SESSION_TOKEN"] = "testing"
|
||||||
|
os.environ["AWS_DEFAULT_REGION"] = "us-east-1"
|
||||||
|
before = DataLayer.geojson.field.storage
|
||||||
|
|
||||||
|
DataLayer.geojson.field.storage = storages.create_storage(
|
||||||
|
{
|
||||||
|
"BACKEND": "umap.storage.UmapS3",
|
||||||
|
"OPTIONS": {
|
||||||
|
"access_key": "testing",
|
||||||
|
"secret_key": "testing",
|
||||||
|
"bucket_name": "umap",
|
||||||
|
"region_name": "us-east-1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
yield
|
||||||
|
DataLayer.geojson.field.storage = before
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module", autouse=True)
|
||||||
|
def mocked_aws():
|
||||||
|
"""
|
||||||
|
Mock all AWS interactions
|
||||||
|
Requires you to create your own boto3 clients
|
||||||
|
"""
|
||||||
|
with mock_aws():
|
||||||
|
client = boto3.client("s3", region_name="us-east-1")
|
||||||
|
client.create_bucket(Bucket="umap")
|
||||||
|
client.put_bucket_versioning(
|
||||||
|
Bucket="umap", VersioningConfiguration={"Status": "Enabled"}
|
||||||
|
)
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_create_datalayer(map, datalayer):
|
||||||
|
other = DataLayerFactory(map=map)
|
||||||
|
assert datalayer.geojson.name == f"{datalayer.pk}.geojson"
|
||||||
|
assert other.geojson.name == f"{other.pk}.geojson"
|
||||||
|
|
||||||
|
|
||||||
|
def test_clone_should_return_new_instance(map, datalayer):
|
||||||
|
clone = datalayer.clone()
|
||||||
|
assert datalayer.pk != clone.pk
|
||||||
|
assert datalayer.name == clone.name
|
||||||
|
assert datalayer.map == clone.map
|
||||||
|
assert datalayer.geojson != clone.geojson
|
||||||
|
assert datalayer.geojson.name != clone.geojson.name
|
||||||
|
assert clone.geojson.name == f"{clone.pk}.geojson"
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_should_add_version(map, datalayer):
|
||||||
|
assert len(datalayer.versions) == 1
|
||||||
|
datalayer.geojson = ContentFile("{}", "foo.json")
|
||||||
|
datalayer.save()
|
||||||
|
assert len(datalayer.versions) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_version(map, datalayer):
|
||||||
|
assert len(datalayer.versions) == 1
|
||||||
|
datalayer.geojson = ContentFile('{"foo": "bar"}', "foo.json")
|
||||||
|
datalayer.save()
|
||||||
|
assert len(datalayer.versions) == 2
|
||||||
|
latest = datalayer.versions[0]["ref"]
|
||||||
|
version = datalayer.get_version(latest)
|
||||||
|
assert json.loads(version) == {"foo": "bar"}
|
||||||
|
older = datalayer.versions[1]["ref"]
|
||||||
|
version = datalayer.get_version(older)
|
||||||
|
assert json.loads(version) == {
|
||||||
|
"_umap_options": {
|
||||||
|
"browsable": True,
|
||||||
|
"displayOnLoad": True,
|
||||||
|
"name": "test datalayer",
|
||||||
|
},
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"geometry": {
|
||||||
|
"coordinates": [
|
||||||
|
14.68896484375,
|
||||||
|
48.55297816440071,
|
||||||
|
],
|
||||||
|
"type": "Point",
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"_umap_options": {
|
||||||
|
"color": "DarkCyan",
|
||||||
|
"iconClass": "Ball",
|
||||||
|
},
|
||||||
|
"description": "Da place anonymous again 755",
|
||||||
|
"name": "Here",
|
||||||
|
},
|
||||||
|
"type": "Feature",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
}
|
||||||
|
|
||||||
|
latest = datalayer.reference_version
|
||||||
|
version = datalayer.get_version(latest)
|
||||||
|
assert json.loads(version) == {"foo": "bar"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_datalayer_should_delete_all_versions(datalayer):
|
||||||
|
# create a new version
|
||||||
|
datalayer.geojson = ContentFile('{"foo": "bar"}', "foo.json")
|
||||||
|
datalayer.save()
|
||||||
|
s3_key = datalayer.geojson.name
|
||||||
|
datalayer.delete()
|
||||||
|
with pytest.raises(ClientError):
|
||||||
|
datalayer.geojson.storage.connection.meta.client.get_object(
|
||||||
|
Bucket="umap",
|
||||||
|
Key=s3_key,
|
||||||
|
)
|
|
@ -231,7 +231,7 @@ def test_optimistic_concurrency_control_with_empty_version(
|
||||||
def test_versions_should_return_versions(client, datalayer, map, settings):
|
def test_versions_should_return_versions(client, datalayer, map, settings):
|
||||||
map.share_status = Map.PUBLIC
|
map.share_status = Map.PUBLIC
|
||||||
map.save()
|
map.save()
|
||||||
root = datalayer.storage_root()
|
root = datalayer.geojson.storage._base_path(datalayer)
|
||||||
datalayer.geojson.storage.save(
|
datalayer.geojson.storage.save(
|
||||||
"%s/%s_1440924889.geojson" % (root, datalayer.pk), ContentFile("{}")
|
"%s/%s_1440924889.geojson" % (root, datalayer.pk), ContentFile("{}")
|
||||||
)
|
)
|
||||||
|
@ -248,6 +248,7 @@ def test_versions_should_return_versions(client, datalayer, map, settings):
|
||||||
"name": "%s_1440918637.geojson" % datalayer.pk,
|
"name": "%s_1440918637.geojson" % datalayer.pk,
|
||||||
"size": 2,
|
"size": 2,
|
||||||
"at": "1440918637",
|
"at": "1440918637",
|
||||||
|
"ref": "1440918637",
|
||||||
}
|
}
|
||||||
assert version in versions["versions"]
|
assert version in versions["versions"]
|
||||||
|
|
||||||
|
@ -255,7 +256,7 @@ def test_versions_should_return_versions(client, datalayer, map, settings):
|
||||||
def test_versions_can_return_old_format(client, datalayer, map, settings):
|
def test_versions_can_return_old_format(client, datalayer, map, settings):
|
||||||
map.share_status = Map.PUBLIC
|
map.share_status = Map.PUBLIC
|
||||||
map.save()
|
map.save()
|
||||||
root = datalayer.storage_root()
|
root = datalayer.geojson.storage._base_path(datalayer)
|
||||||
datalayer.old_id = 123 # old datalayer id (now replaced by uuid)
|
datalayer.old_id = 123 # old datalayer id (now replaced by uuid)
|
||||||
datalayer.save()
|
datalayer.save()
|
||||||
|
|
||||||
|
@ -279,31 +280,32 @@ def test_versions_can_return_old_format(client, datalayer, map, settings):
|
||||||
"name": old_format_version,
|
"name": old_format_version,
|
||||||
"size": 2,
|
"size": 2,
|
||||||
"at": "1440918637",
|
"at": "1440918637",
|
||||||
|
"ref": "1440918637",
|
||||||
}
|
}
|
||||||
assert version in versions["versions"]
|
assert version in versions["versions"]
|
||||||
|
|
||||||
client.get(
|
client.get(reverse("datalayer_version", args=(map.pk, datalayer.pk, "1440918637")))
|
||||||
reverse("datalayer_version", args=(map.pk, datalayer.pk, old_format_version))
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_version_should_return_one_version_geojson(client, datalayer, map):
|
def test_version_should_return_one_version_geojson(client, datalayer, map):
|
||||||
map.share_status = Map.PUBLIC
|
map.share_status = Map.PUBLIC
|
||||||
map.save()
|
map.save()
|
||||||
root = datalayer.storage_root()
|
root = datalayer.geojson.storage._base_path(datalayer)
|
||||||
name = "%s_1440924889.geojson" % datalayer.pk
|
name = "%s_1440924889.geojson" % datalayer.pk
|
||||||
datalayer.geojson.storage.save("%s/%s" % (root, name), ContentFile("{}"))
|
datalayer.geojson.storage.save("%s/%s" % (root, name), ContentFile("{}"))
|
||||||
url = reverse("datalayer_version", args=(map.pk, datalayer.pk, name))
|
url = reverse("datalayer_version", args=(map.pk, datalayer.pk, "1440924889"))
|
||||||
assert client.get(url).content.decode() == "{}"
|
resp = client.get(url)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.content.decode() == "{}"
|
||||||
|
|
||||||
|
|
||||||
def test_version_should_return_403_if_not_allowed(client, datalayer, map):
|
def test_version_should_return_403_if_not_allowed(client, datalayer, map):
|
||||||
map.share_status = Map.PRIVATE
|
map.share_status = Map.PRIVATE
|
||||||
map.save()
|
map.save()
|
||||||
root = datalayer.storage_root()
|
root = datalayer.geojson.storage._base_path(datalayer)
|
||||||
name = "%s_1440924889.geojson" % datalayer.pk
|
name = "%s_1440924889.geojson" % datalayer.pk
|
||||||
datalayer.geojson.storage.save("%s/%s" % (root, name), ContentFile("{}"))
|
datalayer.geojson.storage.save("%s/%s" % (root, name), ContentFile("{}"))
|
||||||
url = reverse("datalayer_version", args=(map.pk, datalayer.pk, name))
|
url = reverse("datalayer_version", args=(map.pk, datalayer.pk, "1440924889"))
|
||||||
assert client.get(url).status_code == 403
|
assert client.get(url).status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -87,7 +87,7 @@ i18n_urls += decorated_patterns(
|
||||||
name="datalayer_versions",
|
name="datalayer_versions",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"datalayer/<int:map_id>/<uuid:pk>/<str:name>",
|
"datalayer/<int:map_id>/<uuid:pk>/<str:ref>",
|
||||||
views.DataLayerVersion.as_view(),
|
views.DataLayerVersion.as_view(),
|
||||||
name="datalayer_version",
|
name="datalayer_version",
|
||||||
),
|
),
|
||||||
|
|
119
umap/views.py
119
umap/views.py
|
@ -1,7 +1,6 @@
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
import zipfile
|
import zipfile
|
||||||
|
@ -1105,35 +1104,8 @@ class MapAnonymousEditUrl(RedirectView):
|
||||||
# ############## #
|
# ############## #
|
||||||
|
|
||||||
|
|
||||||
class GZipMixin(object):
|
class DataLayerView(BaseDetailView):
|
||||||
EXT = ".gz"
|
model = DataLayer
|
||||||
|
|
||||||
@property
|
|
||||||
def path(self):
|
|
||||||
return Path(self.object.geojson.path)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def gzip_path(self):
|
|
||||||
return Path(f"{self.path}{self.EXT}")
|
|
||||||
|
|
||||||
def read_version(self, path):
|
|
||||||
# Remove optional .gz, then .geojson, then return the trailing version from path.
|
|
||||||
return str(path.with_suffix("").with_suffix("")).split("_")[-1]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def version(self):
|
|
||||||
# Prior to 1.3.0 we did not set gzip mtime as geojson mtime,
|
|
||||||
# but we switched from If-Match header to If-Unmodified-Since
|
|
||||||
# and when users accepts gzip their last modified value is the gzip
|
|
||||||
# (when umap is served by nginx and X-Accel-Redirect)
|
|
||||||
# one, so we need to compare with that value in that case.
|
|
||||||
# cf https://github.com/umap-project/umap/issues/1212
|
|
||||||
path = (
|
|
||||||
self.gzip_path
|
|
||||||
if self.accepts_gzip and self.gzip_path.exists()
|
|
||||||
else self.path
|
|
||||||
)
|
|
||||||
return self.read_version(path)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def accepts_gzip(self):
|
def accepts_gzip(self):
|
||||||
|
@ -1141,43 +1113,80 @@ class GZipMixin(object):
|
||||||
self.request.META.get("HTTP_ACCEPT_ENCODING", "")
|
self.request.META.get("HTTP_ACCEPT_ENCODING", "")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_s3(self):
|
||||||
|
return "S3" in settings.STORAGES["data"]["BACKEND"]
|
||||||
|
|
||||||
class DataLayerView(GZipMixin, BaseDetailView):
|
@property
|
||||||
model = DataLayer
|
def filepath(self):
|
||||||
|
return Path(self.object.geojson.path)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fileurl(self):
|
||||||
|
return self.object.geojson.url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filedata(self):
|
||||||
|
with self.object.geojson.open("rb") as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fileversion(self):
|
||||||
|
return self.object.reference_version
|
||||||
|
|
||||||
def render_to_response(self, context, **response_kwargs):
|
def render_to_response(self, context, **response_kwargs):
|
||||||
response = None
|
response = None
|
||||||
path = self.path
|
|
||||||
# Generate gzip if needed
|
# Generate gzip if needed
|
||||||
if self.accepts_gzip:
|
if not self.is_s3 and self.accepts_gzip:
|
||||||
if not self.gzip_path.exists():
|
gzip_path = Path(f"{self.filepath}.gz")
|
||||||
gzip_file(path, self.gzip_path)
|
if not gzip_path.exists():
|
||||||
|
gzip_file(self.filepath, gzip_path)
|
||||||
|
|
||||||
if getattr(settings, "UMAP_XSENDFILE_HEADER", None):
|
if getattr(settings, "UMAP_XSENDFILE_HEADER", None):
|
||||||
response = HttpResponse()
|
response = HttpResponse()
|
||||||
internal_path = str(path).replace(settings.MEDIA_ROOT, "/internal")
|
if self.is_s3:
|
||||||
|
internal_path = f"/s3/{self.fileurl}"
|
||||||
|
else:
|
||||||
|
internal_path = str(self.filepath).replace(
|
||||||
|
settings.MEDIA_ROOT, "/internal"
|
||||||
|
)
|
||||||
response[settings.UMAP_XSENDFILE_HEADER] = internal_path
|
response[settings.UMAP_XSENDFILE_HEADER] = internal_path
|
||||||
else:
|
else:
|
||||||
# Do not use in production
|
# Do not use in production
|
||||||
# (no gzip/cache-control/If-Modified-Since/If-None-Match)
|
# (no gzip/cache-control/If-Modified-Since/If-None-Match)
|
||||||
statobj = os.stat(path)
|
data = self.filedata
|
||||||
with open(path, "rb") as f:
|
response = HttpResponse(data, content_type="application/geo+json")
|
||||||
# Should not be used in production!
|
response["X-Datalayer-Version"] = self.fileversion
|
||||||
response = HttpResponse(f.read(), content_type="application/geo+json")
|
|
||||||
response["X-Datalayer-Version"] = self.version
|
|
||||||
response["Content-Length"] = statobj.st_size
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class DataLayerVersion(DataLayerView):
|
class DataLayerVersion(DataLayerView):
|
||||||
@property
|
@property
|
||||||
def path(self):
|
def filepath(self):
|
||||||
|
try:
|
||||||
return Path(settings.MEDIA_ROOT) / self.object.get_version_path(
|
return Path(settings.MEDIA_ROOT) / self.object.get_version_path(
|
||||||
self.kwargs["name"]
|
self.kwargs["ref"]
|
||||||
)
|
)
|
||||||
|
except ValueError:
|
||||||
|
raise Http404("Invalid version reference")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fileurl(self):
|
||||||
|
return self.object.get_version_path(self.kwargs["ref"])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filedata(self):
|
||||||
|
try:
|
||||||
|
return self.object.get_version(self.kwargs["ref"])
|
||||||
|
except ValueError:
|
||||||
|
raise Http404("Invalid version reference.")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fileversion(self):
|
||||||
|
return self.kwargs["ref"]
|
||||||
|
|
||||||
|
|
||||||
class DataLayerCreate(FormLessEditMixin, GZipMixin, CreateView):
|
class DataLayerCreate(FormLessEditMixin, CreateView):
|
||||||
model = DataLayer
|
model = DataLayer
|
||||||
form_class = DataLayerForm
|
form_class = DataLayerForm
|
||||||
|
|
||||||
|
@ -1196,16 +1205,16 @@ class DataLayerCreate(FormLessEditMixin, GZipMixin, CreateView):
|
||||||
# Simple response with only metadata
|
# Simple response with only metadata
|
||||||
data = self.object.metadata(self.request)
|
data = self.object.metadata(self.request)
|
||||||
response = simple_json_response(**data)
|
response = simple_json_response(**data)
|
||||||
response["X-Datalayer-Version"] = self.version
|
response["X-Datalayer-Version"] = self.object.reference_version
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class DataLayerUpdate(FormLessEditMixin, GZipMixin, UpdateView):
|
class DataLayerUpdate(FormLessEditMixin, UpdateView):
|
||||||
model = DataLayer
|
model = DataLayer
|
||||||
form_class = DataLayerForm
|
form_class = DataLayerForm
|
||||||
|
|
||||||
def has_changes_since(self, incoming_version):
|
def has_changes_since(self, incoming_version):
|
||||||
return incoming_version and self.version != incoming_version
|
return incoming_version and self.object.reference_version != incoming_version
|
||||||
|
|
||||||
def merge(self, reference_version):
|
def merge(self, reference_version):
|
||||||
"""
|
"""
|
||||||
|
@ -1217,11 +1226,9 @@ class DataLayerUpdate(FormLessEditMixin, GZipMixin, UpdateView):
|
||||||
|
|
||||||
# Use the provided info to find the correct version in our storage.
|
# Use the provided info to find the correct version in our storage.
|
||||||
for version in self.object.versions:
|
for version in self.object.versions:
|
||||||
name = version["name"]
|
ref = version["ref"]
|
||||||
path = Path(settings.MEDIA_ROOT) / self.object.get_version_path(name)
|
if reference_version == ref:
|
||||||
if reference_version == self.read_version(path):
|
reference = json.loads(self.object.get_version(ref))
|
||||||
with open(path) as f:
|
|
||||||
reference = json.loads(f.read())
|
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
# If the reference document is not found, we can't merge.
|
# If the reference document is not found, we can't merge.
|
||||||
|
@ -1230,7 +1237,7 @@ class DataLayerUpdate(FormLessEditMixin, GZipMixin, UpdateView):
|
||||||
incoming = json.loads(self.request.FILES["geojson"].read())
|
incoming = json.loads(self.request.FILES["geojson"].read())
|
||||||
|
|
||||||
# Latest known version of the data.
|
# Latest known version of the data.
|
||||||
with open(self.path) as f:
|
with self.object.geojson.open() as f:
|
||||||
latest = json.loads(f.read())
|
latest = json.loads(f.read())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -1274,7 +1281,7 @@ class DataLayerUpdate(FormLessEditMixin, GZipMixin, UpdateView):
|
||||||
data["geojson"] = json.loads(self.object.geojson.read().decode())
|
data["geojson"] = json.loads(self.object.geojson.read().decode())
|
||||||
self.request.session["needs_reload"] = False
|
self.request.session["needs_reload"] = False
|
||||||
response = simple_json_response(**data)
|
response = simple_json_response(**data)
|
||||||
response["X-Datalayer-Version"] = self.version
|
response["X-Datalayer-Version"] = self.object.reference_version
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue