feat: add packaging, models and blocks with tests
révision
c6beb75a46
|
@ -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
|
|
@ -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
|
Fichier diff supprimé car celui-ci est trop grand
Voir la Diff
|
@ -0,0 +1,4 @@
|
|||
include LICENSE *.md
|
||||
recursive-include wagtail_maps *
|
||||
global-exclude __pycache__
|
||||
global-exclude *.py[co]
|
|
@ -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
|
|
@ -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!
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
from setuptools import setup
|
||||
|
||||
setup()
|
|
@ -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()
|
|
@ -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
|
|
@ -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'
|
|
@ -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'}
|
|
@ -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'
|
|
@ -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() == ''
|
|
@ -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)
|
|
@ -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,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,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']
|
|
@ -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'})
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class WagtailMapsConfig(AppConfig):
|
||||
name = 'wagtail_maps'
|
||||
default_auto_field = 'django.db.models.AutoField'
|
|
@ -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
|
|
@ -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})
|
|
@ -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>
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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'))),
|
||||
]
|
|
@ -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
|
Référencer dans un nouveau ticket