mirror of
https://github.com/umap-project/umap.git
synced 2025-04-28 19:42:36 +02:00
wip: first naive version of map templates
Co-authored-by: David Larlet <david@larlet.fr>
This commit is contained in:
parent
efaa765b82
commit
b4fcbd1541
11 changed files with 182 additions and 23 deletions
|
@ -91,7 +91,7 @@ class MapSettingsForm(forms.ModelForm):
|
|||
return self.cleaned_data["center"]
|
||||
|
||||
class Meta:
|
||||
fields = ("settings", "name", "center", "slug", "tags")
|
||||
fields = ("settings", "name", "center", "slug", "tags", "is_template")
|
||||
model = Map
|
||||
|
||||
|
||||
|
|
|
@ -1,10 +1,30 @@
|
|||
from django.db.models import Manager
|
||||
from django.db import models
|
||||
|
||||
|
||||
class PublicManager(Manager):
|
||||
class PublicManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return (
|
||||
super(PublicManager, self)
|
||||
.get_queryset()
|
||||
.filter(share_status=self.model.PUBLIC)
|
||||
)
|
||||
|
||||
|
||||
class PrivateQuerySet(models.QuerySet):
|
||||
def for_user(self, user):
|
||||
qs = self.exclude(share_status__in=[self.model.DELETED, self.model.BLOCKED])
|
||||
teams = user.teams.all()
|
||||
qs = (
|
||||
qs.filter(owner=user)
|
||||
.union(qs.filter(editors=user))
|
||||
.union(qs.filter(team__in=teams))
|
||||
)
|
||||
return qs
|
||||
|
||||
|
||||
class PrivateManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return PrivateQuerySet(self.model, using=self._db)
|
||||
|
||||
def for_user(self, user):
|
||||
return self.get_queryset().for_user(user)
|
||||
|
|
21
umap/migrations/0028_map_is_template.py
Normal file
21
umap/migrations/0028_map_is_template.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 5.1.7 on 2025-04-17 09:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("umap", "0027_map_tags"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="map",
|
||||
name="is_template",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="This map is a template map.",
|
||||
verbose_name="save as template",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -12,7 +12,7 @@ from django.urls import reverse
|
|||
from django.utils.functional import classproperty
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .managers import PublicManager
|
||||
from .managers import PrivateManager, PublicManager
|
||||
from .utils import _urls_for_js
|
||||
|
||||
|
||||
|
@ -238,9 +238,15 @@ class Map(NamedModel):
|
|||
blank=True, null=True, verbose_name=_("settings"), default=dict
|
||||
)
|
||||
tags = ArrayField(models.CharField(max_length=200), blank=True, default=list)
|
||||
is_template = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("save as template"),
|
||||
help_text=_("This map is a template map."),
|
||||
)
|
||||
|
||||
objects = models.Manager()
|
||||
public = PublicManager()
|
||||
private = PrivateManager()
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
|
@ -289,12 +295,15 @@ class Map(NamedModel):
|
|||
datalayer.delete()
|
||||
return super().delete(**kwargs)
|
||||
|
||||
def generate_umapjson(self, request):
|
||||
def generate_umapjson(self, request, include_data=True):
|
||||
umapjson = self.settings
|
||||
umapjson["type"] = "umap"
|
||||
umapjson["properties"]["is_template"] = False
|
||||
umapjson["uri"] = request.build_absolute_uri(self.get_absolute_url())
|
||||
datalayers = []
|
||||
for datalayer in self.datalayers:
|
||||
layer = {}
|
||||
if include_data:
|
||||
with datalayer.geojson.open("rb") as f:
|
||||
layer = json.loads(f.read())
|
||||
if datalayer.settings:
|
||||
|
|
|
@ -97,6 +97,9 @@ export default class Importer extends Utils.WithTemplate {
|
|||
case 'banfr':
|
||||
import('./importers/banfr.js').then(register)
|
||||
break
|
||||
case 'templates':
|
||||
import('./importers/templates.js').then(register)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
77
umap/static/umap/js/modules/importers/templates.js
Normal file
77
umap/static/umap/js/modules/importers/templates.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
import { DomEvent, DomUtil } from '../../../vendors/leaflet/leaflet-src.esm.js'
|
||||
import { uMapAlert as Alert } from '../../components/alerts/alert.js'
|
||||
import { BaseAjax, SingleMixin } from '../autocomplete.js'
|
||||
import { translate } from '../i18n.js'
|
||||
import * as Utils from '../utils.js'
|
||||
|
||||
const BOUNDARY_TYPES = {
|
||||
admin_6: 'département',
|
||||
admin_7: 'pays (loi Voynet)',
|
||||
admin_8: 'commune',
|
||||
admin_9: 'quartier, hameau, arrondissement',
|
||||
political: 'canton',
|
||||
local_authority: 'EPCI',
|
||||
}
|
||||
|
||||
const TEMPLATE = `
|
||||
<h3>${translate('Load template')}</h3>
|
||||
<p>${translate('GeoDataMine: thematic data from OpenStreetMap')}.</p>
|
||||
<div class="formbox">
|
||||
<select name="theme">
|
||||
<option value="">${translate('Choose a template')}</option>
|
||||
</select>
|
||||
<label>
|
||||
<input type="checkbox" name="include_data" />
|
||||
${translate('Include template data, if any')}
|
||||
</label>
|
||||
</div>
|
||||
`
|
||||
|
||||
export class Importer {
|
||||
constructor(umap, options = {}) {
|
||||
this.umap = umap
|
||||
this.name = options.name || 'Templates'
|
||||
this.id = 'templates'
|
||||
}
|
||||
|
||||
async open(importer) {
|
||||
const container = DomUtil.create('div')
|
||||
container.innerHTML = TEMPLATE
|
||||
const select = container.querySelector('select')
|
||||
const uri = this.umap.urls.get('template_list')
|
||||
const [data, response, error] = await this.umap.server.get(uri)
|
||||
if (!error) {
|
||||
for (const template of data.templates) {
|
||||
DomUtil.element({
|
||||
tagName: 'option',
|
||||
value: template.id,
|
||||
textContent: template.name,
|
||||
parent: select,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
console.error(response)
|
||||
}
|
||||
const confirm = (form) => {
|
||||
let url = this.umap.urls.get('map_download', {
|
||||
map_id: select.options[select.selectedIndex].value,
|
||||
})
|
||||
if (!container.querySelector('[name=include_data]').checked) {
|
||||
url = `${url}?include_data=0`
|
||||
}
|
||||
importer.url = url
|
||||
importer.format = 'umap'
|
||||
importer.submit()
|
||||
this.umap.editPanel.close()
|
||||
}
|
||||
|
||||
importer.dialog
|
||||
.open({
|
||||
template: container,
|
||||
className: `${this.id} importer dark`,
|
||||
accept: translate('Use this template'),
|
||||
cancel: false,
|
||||
})
|
||||
.then(confirm)
|
||||
}
|
||||
}
|
|
@ -253,6 +253,12 @@ export const SCHEMA = {
|
|||
inheritable: true,
|
||||
default: true,
|
||||
},
|
||||
is_template: {
|
||||
type: Boolean,
|
||||
impacts: ['ui'],
|
||||
label: translate('This map is a template'),
|
||||
default: false,
|
||||
},
|
||||
labelDirection: {
|
||||
type: String,
|
||||
impacts: ['data'],
|
||||
|
|
|
@ -37,6 +37,7 @@ const TOP_BAR_TEMPLATE = `
|
|||
<i class="icon icon-16 icon-save-disabled"></i>
|
||||
<span hidden data-ref="saveLabel">${translate('Save')}</span>
|
||||
<span hidden data-ref="saveDraftLabel">${translate('Save draft')}</span>
|
||||
<span hidden data-ref="saveTemplateLabel">${translate('Save template')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>`
|
||||
|
@ -167,8 +168,11 @@ export class TopBar extends WithTemplate {
|
|||
const syncEnabled = this._umap.getProperty('syncEnabled')
|
||||
this.elements.peers.hidden = !syncEnabled
|
||||
this.elements.view.disabled = this._umap.sync._undoManager.isDirty()
|
||||
this.elements.saveLabel.hidden = this._umap.permissions.isDraft()
|
||||
this.elements.saveDraftLabel.hidden = !this._umap.permissions.isDraft()
|
||||
const isDraft = this._umap.permissions.isDraft()
|
||||
const isTemplate = this._umap.getProperty('is_template')
|
||||
this.elements.saveLabel.hidden = isDraft || isTemplate
|
||||
this.elements.saveDraftLabel.hidden = !isDraft || isTemplate
|
||||
this.elements.saveTemplateLabel.hidden = !isTemplate
|
||||
this._umap.sync._undoManager.toggleState()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -768,7 +768,11 @@ export default class Umap {
|
|||
if (!this.editEnabled) return
|
||||
if (this.properties.editMode !== 'advanced') return
|
||||
const container = DomUtil.create('div')
|
||||
const metadataFields = ['properties.name', 'properties.description']
|
||||
const metadataFields = [
|
||||
'properties.name',
|
||||
'properties.description',
|
||||
'properties.is_template',
|
||||
]
|
||||
|
||||
DomUtil.createTitle(container, translate('Edit map details'), 'icon-caption')
|
||||
const builder = new MutatingForm(this, metadataFields, {
|
||||
|
@ -1197,6 +1201,7 @@ export default class Umap {
|
|||
}
|
||||
const formData = new FormData()
|
||||
formData.append('name', this.properties.name)
|
||||
formData.append('is_template', Boolean(this.properties.is_template))
|
||||
formData.append('center', JSON.stringify(this.geometry()))
|
||||
formData.append('tags', this.properties.tags || [])
|
||||
formData.append('settings', JSON.stringify(geojson))
|
||||
|
|
|
@ -73,6 +73,11 @@ i18n_urls = [
|
|||
views.PictogramJSONList.as_view(),
|
||||
name="pictogram_list_json",
|
||||
),
|
||||
re_path(
|
||||
r"^templates/json/$",
|
||||
views.TemplateList.as_view(),
|
||||
name="template_list",
|
||||
),
|
||||
]
|
||||
i18n_urls += decorated_patterns(
|
||||
[can_view_map, cache_control(must_revalidate=True)],
|
||||
|
|
|
@ -332,10 +332,10 @@ class TeamMaps(PaginatorMixin, DetailView):
|
|||
|
||||
|
||||
class SearchMixin:
|
||||
def get_search_queryset(self, **kwargs):
|
||||
def get_search_queryset(self, qs=None, **kwargs):
|
||||
q = self.request.GET.get("q")
|
||||
tags = [t for t in self.request.GET.getlist("tags") if t]
|
||||
qs = Map.objects.all()
|
||||
qs = qs or Map.public.all()
|
||||
if q:
|
||||
vector = SearchVector("name", config=settings.UMAP_SEARCH_CONFIGURATION)
|
||||
query = SearchQuery(
|
||||
|
@ -382,14 +382,8 @@ class UserDashboard(PaginatorMixin, DetailView, SearchMixin):
|
|||
return self.get_queryset().get(pk=self.request.user.pk)
|
||||
|
||||
def get_maps(self):
|
||||
qs = self.get_search_queryset() or Map.objects.all()
|
||||
qs = qs.exclude(share_status__in=[Map.DELETED, Map.BLOCKED])
|
||||
teams = self.object.teams.all()
|
||||
qs = (
|
||||
qs.filter(owner=self.object)
|
||||
.union(qs.filter(editors=self.object))
|
||||
.union(qs.filter(team__in=teams))
|
||||
)
|
||||
qs = Map.private.for_user(self.object)
|
||||
qs = self.get_search_queryset(qs) or qs
|
||||
return qs.order_by("-modified_at")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
@ -408,8 +402,8 @@ class UserDownload(DetailView, SearchMixin):
|
|||
return self.get_queryset().get(pk=self.request.user.pk)
|
||||
|
||||
def get_maps(self):
|
||||
qs = Map.objects.filter(id__in=self.request.GET.getlist("map_id"))
|
||||
qs = qs.filter(owner=self.object).union(qs.filter(editors=self.object))
|
||||
qs = Map.private.for_user(self.object)
|
||||
qs = qs.filter(id__in=self.request.GET.getlist("map_id"))
|
||||
return qs.order_by("-modified_at")
|
||||
|
||||
def render_to_response(self, context, *args, **kwargs):
|
||||
|
@ -802,7 +796,10 @@ class MapDownload(DetailView):
|
|||
return reverse("map_download", args=(self.object.pk,))
|
||||
|
||||
def render_to_response(self, context, *args, **kwargs):
|
||||
umapjson = self.object.generate_umapjson(self.request)
|
||||
include_data = self.request.GET.get("include_data") != "0"
|
||||
umapjson = self.object.generate_umapjson(
|
||||
self.request, include_data=include_data
|
||||
)
|
||||
response = simple_json_response(**umapjson)
|
||||
response["Content-Disposition"] = (
|
||||
f'attachment; filename="umap_backup_{self.object.slug}.umap"'
|
||||
|
@ -1456,3 +1453,15 @@ class LoginPopupEnd(TemplateView):
|
|||
if backend in settings.DEPRECATED_AUTHENTICATION_BACKENDS:
|
||||
return HttpResponseRedirect(reverse("user_profile"))
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
|
||||
class TemplateList(ListView):
|
||||
model = Map
|
||||
|
||||
def render_to_response(self, context, **response_kwargs):
|
||||
if self.request.user.is_authenticated:
|
||||
qs = Map.private.filter(is_template=True).for_user(self.request.user)
|
||||
else:
|
||||
qs = Map.public.filter(is_template=True)
|
||||
templates = [{"id": m.id, "name": m.name} for m in qs]
|
||||
return simple_json_response(templates=templates)
|
||||
|
|
Loading…
Reference in a new issue