Skip to content

wip: first naive version of map templates #2660

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion umap/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def clean_center(self):
return self.cleaned_data["center"]

class Meta:
fields = ("settings", "name", "center", "slug", "tags")
fields = ("settings", "name", "center", "slug", "tags", "is_template")
model = Map


Expand Down
31 changes: 29 additions & 2 deletions umap/managers.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,37 @@
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)
)

def starred_by_staff(self):
from .models import Star, User

staff = User.objects.filter(is_staff=True)
stars = Star.objects.filter(by__in=staff).values("map")
return self.get_queryset().filter(pk__in=stars)


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 changes: 21 additions & 0 deletions umap/migrations/0028_map_is_template.py
Original file line number Diff line number Diff line change
@@ -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",
),
),
]
17 changes: 13 additions & 4 deletions umap/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
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


Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -289,14 +295,17 @@ def delete(self, **kwargs):
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"].pop("is_template", None)
umapjson["uri"] = request.build_absolute_uri(self.get_absolute_url())
datalayers = []
for datalayer in self.datalayers:
with datalayer.geojson.open("rb") as f:
layer = json.loads(f.read())
layer = {}
if include_data:
with datalayer.geojson.open("rb") as f:
layer = json.loads(f.read())
if datalayer.settings:
datalayer.settings.pop("id", None)
layer["_umap_options"] = datalayer.settings
Expand Down
2 changes: 1 addition & 1 deletion umap/static/umap/content.css
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,6 @@ h2.tabs a:hover {
min-height: var(--map-fragment-height);
}
.tag-list {
margin-top: var(--text-margin);
margin-bottom: var(--text-margin);
display: flex;
flex-wrap: wrap;
Expand Down Expand Up @@ -205,6 +204,7 @@ h2.tabs a:hover {
margin-bottom: 0;
flex-grow: 1;
gap: var(--gutter);
margin-top: var(--text-margin);
}
.card h3 {
margin-bottom: 0;
Expand Down
7 changes: 5 additions & 2 deletions umap/static/umap/css/dialog.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@
margin-top: 100px;
width: var(--dialog-width);
max-width: 100vw;
max-height: 50vh;
max-height: 80vh;
padding: 20px;
border: 1px solid #222;
background-color: var(--background-color);
color: var(--text-color);
border-radius: 5px;
overflow-y: auto;
height: fit-content;
max-height: 90vh;
}
.umap-dialog ul + h4 {
margin-top: var(--box-margin);
}
.umap-dialog .body {
max-height: 50vh;
overflow-y: auto;
}
:where([data-component="no-dialog"]:not([hidden])) {
display: block;
position: relative;
Expand Down
2 changes: 1 addition & 1 deletion umap/static/umap/js/modules/form/fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -725,7 +725,7 @@ Fields.IconUrl = class extends Fields.BlurInput {
<button class="flat tab-url" data-ref=url>${translate('URL')}</button>
</div>
`)
this.tabs.appendChild(root)
;[recent, symbols, chars, url].forEach((node) => this.tabs.appendChild(node))
if (Icon.RECENT.length) {
recent.addEventListener('click', (event) => {
event.stopPropagation()
Expand Down
3 changes: 3 additions & 0 deletions umap/static/umap/js/modules/importer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
Expand Down
95 changes: 95 additions & 0 deletions umap/static/umap/js/modules/importers/templates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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 TEMPLATE = `
<div>
<h3>${translate('Load map template')}</h3>
<p>${translate('Use a template to initialize your map')}.</p>
<div class="formbox">
<div class="flat-tabs" data-ref="tabs">
<button type="button" class="flat" data-value="mine" data-ref="mine">${translate('My templates')}</button>
<button type="button" class="flat" data-value="staff">${translate('Staff templates')}</button>
<button type="button" class="flat" data-value="community">${translate('Community templates')}</button>
</div>
<div data-ref="body" class="body"></div>
<label>
<input type="checkbox" name="include_data" />
${translate('Include template data, if any')}
</label>
</div>
</div>
`

export class Importer {
constructor(umap, options = {}) {
this.umap = umap
this.name = options.name || 'Templates'
this.id = 'templates'
}

async open(importer) {
const [root, { tabs, include_data, body, mine }] =
Utils.loadTemplateWithRefs(TEMPLATE)
const uri = this.umap.urls.get('template_list')
const userIsAuth = Boolean(this.umap.properties.user?.id)
const defaultTab = userIsAuth ? 'mine' : 'staff'
mine.hidden = !userIsAuth

const loadTemplates = async (source) => {
const [data, response, error] = await this.umap.server.get(
`${uri}?source=${source}`
)
if (!error) {
body.innerHTML = ''
for (const template of data.templates) {
const item = Utils.loadTemplate(
`<dl>
<dt><label><input type="radio" value="${template.id}" name="template" />${template.name}</label></dt>
<dd>${template.description}&nbsp;<a href="${template.url}" target="_blank">${translate('Open')}</a></dd>
</dl>`
)
body.appendChild(item)
}
tabs.querySelectorAll('button').forEach((el) => el.classList.remove('on'))
tabs.querySelector(`[data-value="${source}"]`).classList.add('on')
} else {
console.error(response)
}
}
loadTemplates(defaultTab)
tabs
.querySelectorAll('button')
.forEach((el) =>
el.addEventListener('click', () => loadTemplates(el.dataset.value))
)
const confirm = (form) => {
console.log(form)
if (!form.template) {
Alert.error(translate('You must select a template.'))
return false
}
let url = this.umap.urls.get('map_download', {
map_id: form.template,
})
if (!form.include_data) {
url = `${url}?include_data=0`
}
importer.url = url
importer.format = 'umap'
importer.submit()
this.umap.editPanel.close()
}

importer.dialog
.open({
template: root,
className: `${this.id} importer dark`,
accept: translate('Use this template'),
cancel: false,
})
.then(confirm)
}
}
6 changes: 6 additions & 0 deletions umap/static/umap/js/modules/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
8 changes: 6 additions & 2 deletions umap/static/umap/js/modules/ui/bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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>`
Expand Down Expand Up @@ -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()
}
}
Expand Down
7 changes: 6 additions & 1 deletion umap/static/umap/js/modules/umap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down Expand Up @@ -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))
Expand Down
9 changes: 9 additions & 0 deletions umap/templates/umap/design_system.html
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,15 @@ <h3><i class="icon icon-16 icon-settings" title=""></i><span>Titre avec icône</
</form>
</fieldset>
</details>
<details open>
<summary>With tabs</summary>
<div class="flat-tabs" data-ref="tabs">
<button class="flat on" data-ref="recent">Récents</button>
<button class="flat" data-ref="symbols">Symbole</button>
<button class="flat" data-ref="chars">Emoji &amp; texte</button>
<button class="flat" data-ref="url">URL</button>
</div>
</details>
</div>
<h4>Importers</h4>
<div class="umap-dialog window importers dark">
Expand Down
4 changes: 2 additions & 2 deletions umap/templates/umap/map_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
{% endfor %}
</ul>
{% endif %}
<h3>{{ map_inst.name }}</h3>
<h3>{% if map_inst.is_template %}<mark class="template-map">[{% trans "template" %}]</mark>{% endif %} {{ map_inst.name }}</h3>
{% with author=map_inst.get_author %}
{% if author %}
<p>{% trans "by" %} <a href="{{ author.get_url }}">{{ author }}</a></p>
{% endif %}
{% endwith %}
</div>
<a class="main" href="{{ map_inst.get_absolute_url }}">{% translate "See the map" %}</a>
<a class="main" href="{{ map_inst.get_absolute_url }}">{% if map_inst.is_template %}{% translate "See the template" %}{% else %}{% translate "See the map" %}{% endif %}</a>
</hgroup>
</div>
{% endfor %}
Expand Down
Loading
Loading