Browse Source

feat: add packaging, models and blocks with tests

main
Jérôme Lebleu 8 months ago
commit
c6beb75a46
  1. 23
      .editorconfig
  2. 24
      .gitignore
  3. 674
      LICENSE
  4. 4
      MANIFEST.in
  5. 48
      Makefile
  6. 91
      README.md
  7. 40
      pyproject.toml
  8. 55
      setup.cfg
  9. 3
      setup.py
  10. 0
      tests/__init__.py
  11. 8
      tests/conftest.py
  12. 22
      tests/factories.py
  13. 92
      tests/settings.py
  14. 89
      tests/test_admin.py
  15. 110
      tests/test_api.py
  16. 55
      tests/test_blocks.py
  17. 20
      tests/urls.py
  18. 24
      tox.ini
  19. 0
      wagtail_maps/__init__.py
  20. 41
      wagtail_maps/admin.py
  21. 0
      wagtail_maps/api/__init__.py
  22. 40
      wagtail_maps/api/serializers.py
  23. 13
      wagtail_maps/api/views.py
  24. 6
      wagtail_maps/apps.py
  25. 52
      wagtail_maps/blocks.py
  26. 63
      wagtail_maps/models.py
  27. 3
      wagtail_maps/templates/wagtail_maps/icons/map.svg
  28. 3
      wagtail_maps/templates/wagtail_maps/map_block.html
  29. 13
      wagtail_maps/templates/wagtail_maps/popup_content.html
  30. 13
      wagtail_maps/urls.py
  31. 12
      wagtail_maps/wagtail_hooks.py

23
.editorconfig

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.py]
max_line_length = 80
[*.{html,css,js,json,scss,yml}]
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

24
.gitignore vendored

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
# general things to ignore
build/
dist/
*.egg-info/
*.egg
.eggs
*.py[cod]
__pycache__/
*.sw[po]
*~
# virtual environments
venv/
.env/
# unit test / coverage reports
.coverage
.tox
nosetests.xml
htmlcov
# testing
tests/*.db
tests/var

674
LICENSE

File diff suppressed because it is too large Load Diff

4
MANIFEST.in

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
include LICENSE *.md
recursive-include wagtail_maps *
global-exclude __pycache__
global-exclude *.py[co]

48
Makefile

@ -0,0 +1,48 @@ @@ -0,0 +1,48 @@
.PHONY: clean lint format help
.DEFAULT_GOAL := help
PYTHON := venv/bin/python
help:
@echo "Please use 'make <target>' where <target> is one of"
@echo ""
@grep '^[^.#]\+:\s\+.*#' Makefile | \
sed "s/\(.\+\):\s*\(.*\) #\s*\(.*\)/`printf "\033[93m"` \1`printf "\033[0m"` \3 [\2]/" | \
expand -35
@echo ""
@echo "Check the Makefile to know exactly what each target is doing."
clean: # Remove all builds and Python artifacts
find wagtail_maps tests \
\( -name '*.py[co]' -o -name '__pycache__' \) -exec rm -rf {} +
rm -rf build dist .eggs *.egg-info
test: # Run tests quickly with the current Python
$(PYTHON) -m pytest
test-wip: # Run tests marked as wip with the current Python
$(PYTHON) -m pytest -vv -m 'wip' --pdb
test-all: # Run tests on every Python, Django and Wagtail versions
tox
coverage: # Check code coverage quickly with the current Python
$(PYTHON) -m coverage run -m pytest
$(PYTHON) -m coverage report -m
$(PYTHON) -m coverage html
@echo open htmlcov/index.html
lint: # Check the Python code syntax and style
$(PYTHON) -m flake8 wagtail_maps tests
format: # Fix the Python code syntax and imports order
$(PYTHON) -m isort wagtail_maps tests
$(PYTHON) -m black wagtail_maps tests
release: dist # Package and upload a release
twine upload dist/*
dist: clean # Build source and wheel package
$(PYTHON) setup.py sdist
$(PYTHON) setup.py bdist_wheel
ls -l dist

91
README.md

@ -0,0 +1,91 @@ @@ -0,0 +1,91 @@
# wagtail-maps
Create and display maps with points in Wagtail.
**Warning!** This project is still early on in its development lifecycle. It is
possible for breaking changes to occur between versions until reaching a stable
1.0. Feedback and pull requests are welcome.
This package extend Wagtail to add a new Map model, which is composed by one or
more points. Each point may have a title, some content and link to an internal
or external URL. Once you have configured your map from the Wagtail admin, you
will be able to display it in a page - e.g. as a StreamField block.
## Requirements
This package requires the following:
- Python (3.7, 3.8, 3.9)
- Django (2.2, 3.1, 3.2)
- Wagtail (2.11, 2.14)
Older versions of Wagtail could work too but they are not tested. The efforts
are focused in LTS and recent versions.
## Installation
1. Install using ``pip``:
```shell
pip install wagtail-maps
```
2. Add ``wagtail_maps`` to your ``INSTALLED_APPS`` setting:
```python
INSTALLED_APPS = [
# ...
'wagtail_maps',
# ...
]
```
3. Include the URL of *wagtail-maps* to your ``urls.py`` file:
```python
from wagtail_maps import urls as wagtailmaps_urls
urlpatterns = [
# ...
path('maps/', include(wagtailmaps_urls)),
# ...
]
```
4. Run ``python manage.py migrate`` to create the models
## Development
### Quick start
To set up a development environment, ensure that Python 3 is installed on your
system. Then:
1. Clone this repository
2. Create a virtual environment and activate it:
```shell
python3 -m venv venv
source venv/bin/activate
```
3. Install this package in develop mode with extra requirements:
```shell
pip install -e .[test]
```
### Contributing
The Python code is formatted and linted thanks to [flake8], [isort] and [black].
To ease the use of this tools, the following commands are available:
- `make lint`: check the Python code syntax and imports order
- `make format`: fix the Python code syntax and imports order
The tests are written with [pytest] and code coverage is measured with [coverage].
You can use the following commands for that:
- ``make test``: run the tests and output a quick report of code coverage
- ``make coverage``: run the tests and produce an HTML report of code coverage
When submitting a pull-request, please ensure that the code is well formatted
and covered, and that all the other tests pass.
[flake8]: https://flake8.pycqa.org/
[isort]: https://pycqa.github.io/isort/
[black]: https://black.readthedocs.io/
[pytest]: https://docs.pytest.org/
[coverage]: https://coverage.readthedocs.io/
## License
This extension is mainly developed by [Cliss XXI](https://www.cliss21.com) and
licensed under the [AGPLv3+](LICENSE). Any contribution is welcome!

40
pyproject.toml

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
[build-system]
requires = ["setuptools >=42", "wheel", "setuptools_scm[toml] >=3.4"]
build-backend = "setuptools.build_meta"
[tool.setuptools_scm]
[tool.black]
line-length = 80
skip-string-normalization = true
exclude = '''
/(
\.git
| venv
| migrations
)/
'''
[tool.isort]
profile = 'black'
line_length = 80
known_django = 'django'
known_wagtail = 'wagtail'
sections = [
'FUTURE', 'STDLIB', 'DJANGO', 'WAGTAIL', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER'
]
skip_glob = '**/migrations/*.py'
[tool.pytest.ini_options]
addopts = '--ds=tests.settings'
python_files = ['test_*.py']
testpaths = ['tests']
markers = ['wip: mark a test as a work in progress']
[tool.coverage.run]
branch = true
source = ['wagtail_maps']
[tool.coverage.report]
exclude_lines = ['# pragma: no cover', 'raise NotImplementedError']
show_missing = true

55
setup.cfg

@ -0,0 +1,55 @@ @@ -0,0 +1,55 @@
[metadata]
name = wagtail_maps
description = Create and display maps with points in Wagtail
long_description = file: README.md
long_description_content_type = text/markdown
author = Cliss XXI
author_email = contact@cliss21.com
license = AGPLv3+
project_urls =
Bug Tracker = https://forge.cliss21.org/cliss21/wagtail-maps/issues
Source Code = https://forge.cliss21.org/cliss21/wagtail-maps
classifiers =
Development Status :: 3 - Alpha
Environment :: Web Environment
Framework :: Django
Framework :: Wagtail
Framework :: Wagtail :: 2
Intended Audience :: Developers
License :: OSI Approved :: GNU Affero General Public License v3
Operating System :: OS Independent
Programming Language :: Python :: 3
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
keywords =
wagtail
map
leaflet
[options]
include_package_data = True
install_requires =
wagtail >=2.11
packages = wagtail_maps
python_requires = >=3.7, <4
setup_requires =
setuptools_scm
[options.extras_require]
test =
pytest
pytest-cov
pytest-django
factory-boy
; code quality
black
flake8 >=3.5
flake8-black
flake8-isort
isort >=5.0
[flake8]
exclude =
*/migrations/*
max-line-length = 80

3
setup.py

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
from setuptools import setup
setup()

0
tests/__init__.py

8
tests/conftest.py

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
from wagtail.core.models import Page
import pytest
@pytest.fixture
def root_page():
return Page.objects.filter(sites_rooted_here__is_default_site=True).get()

22
tests/factories.py

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
import factory
from factory.django import DjangoModelFactory
from wagtail_maps import models
class PointFactory(DjangoModelFactory):
title = factory.Sequence(lambda n: "Point #%d" % n)
content = "<p>Lorem ipsum.</p>"
latitude = factory.Faker('latitude')
longitude = factory.Faker('longitude')
class Meta:
model = models.Point
class MapFactory(DjangoModelFactory):
name = factory.Sequence(lambda n: "Map #%d" % n)
points = factory.RelatedFactoryList(PointFactory, 'map', size=3)
class Meta:
model = models.Map

92
tests/settings.py

@ -0,0 +1,92 @@ @@ -0,0 +1,92 @@
import os
from pathlib import Path
BASE_DIR = Path(__file__).parent
VAR_DIR = Path(__file__).parent / 'var'
DEBUG = True if os.environ.get('DEBUG', '0') == '1' else False
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': str(BASE_DIR / 'sqlite.db'),
}
}
SECRET_KEY = 'not needed'
ALLOWED_HOSTS = ['localhost', 'testserver']
ROOT_URLCONF = 'tests.urls'
STATIC_URL = '/static/'
STATICFILES_FINDERS = [
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
]
STATIC_ROOT = VAR_DIR / 'static'
MEDIA_URL = '/media/'
MEDIA_ROOT = VAR_DIR / 'media'
USE_TZ = True
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'debug': DEBUG,
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
INSTALLED_APPS = [
# django
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# wagtail
'wagtail.contrib.modeladmin',
'wagtail.sites',
'wagtail.users',
'wagtail.documents',
'wagtail.images',
'wagtail.search',
'wagtail.admin',
'wagtail.core',
'modelcluster',
'taggit',
# wagtail_maps
'wagtail_maps',
'tests',
]
PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher']
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
WAGTAIL_SITE_NAME = 'wagtail-maps test'
BASE_URL = 'http://testserver'

89
tests/test_admin.py

@ -0,0 +1,89 @@ @@ -0,0 +1,89 @@
from wagtail.contrib.modeladmin.helpers import AdminURLHelper
from wagtail.tests.utils.form_data import inline_formset, nested_form_data
import pytest
from bs4 import BeautifulSoup
from wagtail_maps.models import Map
from .factories import MapFactory
@pytest.mark.django_db
class TestMapAdminViews:
url_helper = AdminURLHelper(Map)
@property
def index_url(self):
return self.url_helper.index_url
@property
def create_url(self):
return self.url_helper.create_url
def get_edit_url(self, pk):
return self.url_helper.get_action_url('edit', instance_pk=pk)
# Tests
def test_index(self, admin_client):
MapFactory.create_batch(2)
response = admin_client.get(self.index_url)
assert response.status_code == 200
soup = BeautifulSoup(response.content, 'html5lib')
rows = soup.select('[data-object-pk]')
assert len(rows) == 2
assert rows[0].select_one('.field-points_count').text == '3'
def test_create(self, admin_client, root_page):
response = admin_client.get(self.create_url)
assert response.status_code == 200
data = nested_form_data(
{
'name': "Map example",
'points': inline_formset(
[
{
'title': "Foo",
'latitude': '50.9523',
'longitude': '1.8689',
'page_link': root_page.id,
}
]
),
}
)
response = admin_client.post(self.create_url, data)
assert response.status_code == 302
instance = Map.objects.get(name="Map example")
points = instance.points.all()
assert len(points) == 1
assert points[0].page_link == root_page
response = admin_client.get(self.get_edit_url(instance.pk))
assert response.status_code == 200
def test_create_multiple_link_error(self, admin_client, root_page):
data = nested_form_data(
{
'name': "Map example",
'points': inline_formset(
[
{
'title': "Foo",
'latitude': '50.9523',
'longitude': '1.8689',
'page_link': root_page.id,
'external_link': 'https://example.org',
}
]
),
}
)
response = admin_client.post(self.create_url, data)
assert response.status_code == 200
formset = response.context['form'].formsets['points']
assert set(formset.errors[0].keys()) == {'page_link', 'external_link'}

110
tests/test_api.py

@ -0,0 +1,110 @@ @@ -0,0 +1,110 @@
from django.urls import reverse
import pytest
from bs4 import BeautifulSoup
from rest_framework.test import APIClient
from wagtail_maps.models import Map, Point
from .factories import PointFactory
MAP_POINT_LAT = '50.9523'
MAP_POINT_LON = '1.8689'
@pytest.fixture
def map_example(root_page):
return Map.objects.create(
name="Map example",
points=[
Point(
title="Point 1",
latitude=MAP_POINT_LAT,
longitude=MAP_POINT_LON,
),
Point(
title="Point 2",
page_link=root_page,
latitude=MAP_POINT_LAT,
longitude=MAP_POINT_LON,
),
Point(
title="Point 3",
external_link='https://example.org',
latitude=MAP_POINT_LAT,
longitude=MAP_POINT_LON,
),
],
)
@pytest.mark.django_db
class TestMapsAPIViewSet:
@classmethod
def setup_class(cls):
cls.client = APIClient(enforce_csrf_checks=True)
def get_detail_url(self, item_id):
return reverse('wagtail_maps:api:map-detail', args=(item_id,))
def get_detail_response(self, item_id, data=None):
return self.client.get(self.get_detail_url(item_id), data)
# Tests
def test_detail(self, map_example):
response = self.get_detail_response(map_example.id)
assert response.status_code == 200
assert response.json() == {
'id': 1,
'name': 'Map example',
'points': [
{
'title': 'Point 1',
'content': '',
'url': '',
'latitude': MAP_POINT_LAT,
'longitude': MAP_POINT_LON,
},
{
'title': 'Point 2',
'content': '',
'url': 'http://localhost/',
'latitude': MAP_POINT_LAT,
'longitude': MAP_POINT_LON,
},
{
'title': 'Point 3',
'content': '',
'url': 'https://example.org',
'latitude': MAP_POINT_LAT,
'longitude': MAP_POINT_LON,
},
],
}
def test_detail_content_expanded(self, map_example, root_page):
PointFactory(
title='Point title',
content='<p>Lorem <a id="{}" linktype="page">ipsum</a></p>'.format(
root_page.id
),
map_id=map_example.id,
)
points = self.get_detail_response(map_example.id).json()['points']
content = BeautifulSoup(points[-1]['content'], 'html5lib')
assert content.select_one('span', text='Point title')
assert content.select_one('p', text='Lorem')
link = content.select_one('a', tex_t='ipsum')
assert link['href'] == root_page.url
def test_detail_content_with_url(self, map_example):
PointFactory(
title='Point with link',
external_link='https://example.org',
map_id=map_example.id,
)
points = self.get_detail_response(map_example.id).json()['points']
content = BeautifulSoup(points[-1]['content'], 'html5lib')
title = content.select_one('a', text='Point with link')
assert title['href'] == 'https://example.org'

55
tests/test_blocks.py

@ -0,0 +1,55 @@ @@ -0,0 +1,55 @@
import pytest
from pytest_django.asserts import assertHTMLEqual
from wagtail_maps.blocks import MapBlock
from .factories import MapFactory
@pytest.mark.django_db
class TestMapBlock:
@pytest.fixture(autouse=True)
def setup_block(self):
self.block = MapBlock()
self.block.set_name('test_mapblock')
def render(self, data):
return self.block.render(self.block.to_python(data))
def test_form_response_map(self):
maps = MapFactory.create_batch(2)
value = self.block.value_from_datadict({'p-map': maps[1].pk}, {}, 'p')
assert value['map'] == maps[1]
@pytest.mark.parametrize('value', ('', None, '10'))
def test_form_response_map_none(self, value):
MapFactory.create_batch(2)
value = self.block.value_from_datadict({'p-map': value}, {}, 'p')
assert value['map'] is None
def test_render(self):
assertHTMLEqual(
self.render({'map': MapFactory().id}),
"""
<div class="map" data-map
data-map-api-url="/maps/api/v1/1/">
</div>
""",
)
def test_render_with_attrs(self):
assertHTMLEqual(
self.render({'map': MapFactory().id, 'zoom': '1', 'height': '10'}),
"""
<div class="map" data-map
data-map-api-url="/maps/api/v1/1/"
data-map-height="10"
data-map-zoom="1">
</div>
""",
)
def test_render_unknown(self):
assert self.render({'map': '100'}).strip() == ''

20
tests/urls.py

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
from django.conf import settings
from django.urls import include, path
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.core import urls as wagtail_urls
from wagtail.documents import urls as wagtaildocs_urls
from wagtail_maps import urls as wagtailmaps_urls
urlpatterns = [
path('admin/', include(wagtailadmin_urls)),
path('documents/', include(wagtaildocs_urls)),
path('maps/', include(wagtailmaps_urls)),
path('', include(wagtail_urls)),
]
if settings.DEBUG:
from django.conf.urls.static import static
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

24
tox.ini

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
[tox]
envlist =
python{3.7,3.8}-django{2.2,3.1}-wagtail{2.11}
python{3.7,3.8,3.9}-django{3.1,3.2}-wagtail{2.14,main}
[testenv]
commands = {envpython} -m pytest --basetemp="{envtmpdir}" --cov --cov-report=term:skip-covered
basepython =
python3.7: python3.7
python3.8: python3.8
python3.9: python3.9
deps =
django2.2: Django>=2.2,<2.3
django3.1: Django>=3.1,<3.2
django3.2: Django>=3.2,<3.3
wagtail2.11: wagtail>=2.11,<2.12
wagtail2.14: wagtail>=2.14,<2.15
wagtailmain: git+https://github.com/wagtail/wagtail.git
extras = test
usedevelop = true

0
wagtail_maps/__init__.py

41
wagtail_maps/admin.py

@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
from django.utils.translation import gettext_lazy as _
from wagtail.admin.edit_handlers import (
FieldPanel,
FieldRowPanel,
InlinePanel,
PageChooserPanel,
)
from wagtail.contrib.modeladmin.options import ModelAdmin
from .models import Map
class MapAdmin(ModelAdmin):
model = Map
menu_icon = 'map'
list_display = ('name', 'points_count')
panels = [
FieldPanel('name', classname='title'),
InlinePanel(
'points',
panels=[
FieldPanel('title'),
FieldPanel('content'),
PageChooserPanel('page_link'),
FieldPanel('external_link'),
FieldRowPanel(
[FieldPanel('latitude'), FieldPanel('longitude')]
),
],
heading=_("Points"),
label=_("Point"),
min_num=1,
),
]
def points_count(self, obj):
return obj.points.count()
points_count.short_description = _("Points")

0
wagtail_maps/api/__init__.py

40
wagtail_maps/api/serializers.py

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
from django.template.loader import get_template
from rest_framework import serializers
from ..models import Map, Point
class PointSerializer(serializers.ModelSerializer):
url = serializers.SerializerMethodField()
content = serializers.SerializerMethodField()
class Meta:
model = Point
fields = ['title', 'content', 'url', 'latitude', 'longitude']
def get_url(self, obj):
if obj.page_link:
return obj.page_link.get_full_url(self.context['request'])
return obj.external_link
def get_content(self, obj):
return (
get_template('wagtail_maps/popup_content.html')
.render(
{
'title': obj.title,
'content': obj.content,
'url': self.get_url(obj),
}
)
.strip()
)
class MapSerializer(serializers.ModelSerializer):
points = PointSerializer(many=True, read_only=True)
class Meta:
model = Map
fields = ['id', 'name', 'points']

13
wagtail_maps/api/views.py

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
from rest_framework.mixins import RetrieveModelMixin
from rest_framework.viewsets import GenericViewSet
from ..models import Map
from .serializers import MapSerializer
class MapsAPIViewSet(RetrieveModelMixin, GenericViewSet):
queryset = Map.objects.all()
serializer_class = MapSerializer
map_detail = MapsAPIViewSet.as_view({'get': 'retrieve'})

6
wagtail_maps/apps.py

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
from django.apps import AppConfig
class WagtailMapsConfig(AppConfig):
name = 'wagtail_maps'
default_auto_field = 'django.db.models.AutoField'

52
wagtail_maps/blocks.py

@ -0,0 +1,52 @@ @@ -0,0 +1,52 @@
from django import forms
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from wagtail.core import blocks
from wagtail.core.utils import resolve_model_string
class MapChooserBlock(blocks.ChooserBlock):
class Meta:
label = _("Map")
@cached_property
def target_model(self):
return resolve_model_string('wagtail_maps.Map')
@cached_property
def widget(self):
return forms.Select()
def value_from_form(self, value):
if value == '':
return None
return super().value_from_form(value)
class MapBlock(blocks.StructBlock):
map = MapChooserBlock()
height = blocks.IntegerBlock(
label=_("Height (px)"),
required=False,
min_value=10,
)
zoom = blocks.IntegerBlock(
label=_("Initial zoom"),
required=False,
min_value=1,
max_value=20,
)
class Meta:
icon = 'map'
label = _("Map")
template = 'wagtail_maps/map_block.html'
def get_context(self, value, **kwargs):
context = super().get_context(value, **kwargs)
context['attrs'] = {}
for name in ('height', 'zoom'):
if value.get(name):
context['attrs'][f'data-map-{name}'] = value[name]
return context

63
wagtail_maps/models.py

@ -0,0 +1,63 @@ @@ -0,0 +1,63 @@
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
from wagtail.core.fields import RichTextField
from modelcluster.fields import ParentalKey
from modelcluster.models import ClusterableModel
class Map(ClusterableModel):
name = models.CharField(verbose_name=_("name"), max_length=30)
class Meta:
verbose_name = _("map")
verbose_name_plural = _("maps")
def __str__(self):
return self.name
class Point(models.Model):
title = models.CharField(verbose_name=_("title"), max_length=50)
content = RichTextField(
verbose_name=_("content"),
blank=True,
features=['bold', 'italic', 'ol', 'ul', 'link'],
)
page_link = models.ForeignKey(
'wagtailcore.Page',
verbose_name=_("link to a page"),
blank=True,
null=True,
related_name='+',
on_delete=models.SET_NULL,
)
external_link = models.URLField(
verbose_name=_("link to an URL"),
blank=True,
)
latitude = models.DecimalField(
verbose_name=_("latitude"),
max_digits=7,
decimal_places=4,
)
longitude = models.DecimalField(
verbose_name=_("longitude"),
max_digits=7,
decimal_places=4,
)
map = ParentalKey('Map', on_delete=models.CASCADE, related_name='points')
class Meta:
verbose_name = _("point")
verbose_name_plural = _("points")
def clean(self):
if self.page_link and self.external_link:
msg = gettext("Linking to both a page and an URL is not allowed.")
raise ValidationError({'page_link': msg, 'external_link': msg})

3
wagtail_maps/templates/wagtail_maps/icons/map.svg

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
<symbol id="icon-map" viewBox="0 0 24 24">
<path d="M2 5l7-3 6 3 6.303-2.701a.5.5 0 0 1 .697.46V19l-7 3-6-3-6.303 2.701a.5.5 0 0 1-.697-.46V5zm14 14.395l4-1.714V5.033l-4 1.714v12.648zm-2-.131V6.736l-4-2v12.528l4 2zm-6-2.011V4.605L4 6.319v12.648l4-1.714z"/>
</symbol>

3
wagtail_maps/templates/wagtail_maps/map_block.html

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
{% if self.map %}
<div class="map" data-map data-map-api-url="{% url "wagtail_maps:api:map-detail" self.map.pk %}"{% for name, value in attrs.items %}{% if value is not False %} {{ name }}{% if value is not True %}="{{ value|stringformat:'s' }}"{% endif %}{% endif %}{% endfor %}></div>
{% endif %}

13
wagtail_maps/templates/wagtail_maps/popup_content.html

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
{% load i18n wagtailcore_tags %}{% spaceless %}
{% if content %}
<h5 class="leaflet-popup-header">
{% if url %}
<a href="{{ url }}" class="flex-fill">{{ title }}</a>
{% else %}
<span class="me-auto">{{ title }}</span>
{% endif %}
<button type="button" class="btn-close" data-dismiss="popup" aria-label="{% trans "Close" %}"></button>
</h5>
<div class="leaflet-popup-body">{{ content|richtext }}</div>
{% endif %}
{% endspaceless %}

13
wagtail_maps/urls.py

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
from django.urls import include, path
from .api import views as api_views
app_name = 'wagtail_maps'
api_patterns = [
path('<int:pk>/', api_views.map_detail, name='map-detail'),
]
urlpatterns = [
path('api/v1/', include((api_patterns, 'api'))),
]

12
wagtail_maps/wagtail_hooks.py

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
from wagtail.contrib.modeladmin.options import modeladmin_register
from wagtail.core import hooks
from .admin import MapAdmin
modeladmin_register(MapAdmin)
@hooks.register('register_icons')
def register_icons(icons):
icons.append('wagtail_maps/icons/map.svg')
return icons
Map all the world