+
+[![Alt text](http://geotrek.fr/assets/img/screen-1.png "Interface de Geotrek-admin")](http://geotrek.fr)
+
+## Admin
+
+**Geotrek-admin** is a web application designed to manage, centralize, and structure geographical and touristic information for your territory. It is the back-office application of the Geotrek ecosystem.
+
+With Geotrek-admin, you can:
+- Manage treks, touristic information, and related content (media, descriptions, etc.).
+- Organize your data with maps, layers, and metadata.
+- Export content to various public interfaces, such as Geotrek-rando or printed topoguides.
+
+You can explore Geotrek-admin in action through the demonstration website:
+- [https://demo-admin.geotrek.fr/](https://demo-admin.geotrek.fr/) (demo / demo) :
+
+Geotrek-admin is built on Django and leverages a PostGIS database for handling geographical data. It serves as the data source for Geotrek-rando, Geotrek-widget and other tools of the Geotrek ecosystem.
+
+Learn more about Geotrek-admin in the [general documentation (French)](https://geotrek.readthedocs.io/fr/latest/about/geotrek.html).
+
+## Features
+
+Geotrek-Admin is a powerful web mapping application designed for managing trekking, outdoor and tourism data with ease. Tailored to support GIS features and extensive customization, it empowers organizations to manage, maintain, and publish their outdoor assets seamlessly:
+
+- **Management tool**: manage paths, interventions, signage, treks, POIs, touristic events, and much more.
+- **Maintenance tracking**: track the maintenance of equipment and infrastructures with precision.
+- **Advanced GIS capabilities**: control objects by district, protected areas, physical and legal status of paths, and compute 3D attributes using DEM draping.
+- **Data synchronization**: interconnect with external platforms like Suricate, Apidae, and Tourinsoft for real-time data updates.
+- **Publishing tools**:
+ - create public websites with [Geotrek-rando](https://github.com/GeotrekCE/Geotrek-rando-v3) (e.g., [Destination Écrins](https://rando.ecrins-parcnational.fr), [Alpes Rando](https://alpesrando.net/)).
+ - embed trek information into existing websites with [Geotrek-widget](https://github.com/GeotrekCE/Geotrek-rando-widget) for flexible and lightweight integration (e.g., [Sidobre Vallée Tourisme](https://sidobre-vallees-tourisme.com/type_activite/balades-et-randonnees-sidobre-vallees/), [la Toscane Occitane](https://www.la-toscane-occitane.com/a-voir-a-faire/balades-randonnees))..
+ - deploy mobile applications with [Geotrek-mobile](https://github.com/GeotrekCE/Geotrek-mobile) (e.g., [Grand Carcasssonne](https://play.google.com/store/apps/details?id=io.geotrek.grandcarcassonne), [Jura outdoor](https://apps.apple.com/app/jura-outdoor/id6446137384)).
+- **Customizable outputs**: export data in various formats (PDF, GPX, KML) for offline use and tailored user experiences.
+- **Interactive mapping**: enable users to visualize and explore data-rich maps with detailed elevation profiles.
+- **Documentation and support**: access comprehensive documentation, best practices and community support for all your needs in the ([official documentation](https://geotrek.readthedocs.io/en/2.111.0/usage/overview.html)).
+
+Learn more on the [Geotrek product website](http://geotrek.fr).
+
+## User manual (french)
+
+- [Presentation](https://geotrek.readthedocs.io/fr/latest/usage/overview.html)
+- [Management modules](https://geotrek.readthedocs.io/fr/latest/usage/management-modules.html)
+- [Touristic modules](https://geotrek.readthedocs.io/fr/latest/usage/touristic-modules.html)
+
+## Installation and configuration
+
+- [Installation](https://geotrek.readthedocs.io/fr/latest/install/installation.html)
+- [Configuration](https://geotrek.readthedocs.io/fr/latest/install/configuration.html)
+- [Advanced configuration](https://geotrek.readthedocs.io/fr/latest/install/advanced-configuration.html)
+
+## Support
+
+- To report bugs or suggest features, please [submit a ticket](https://github.com/GeotrekCE/Geotrek-admin/issues).
+- Join our community to stay updated and share your experience! Connect on [Matrix](https://matrix.to/#/%23geotrek:matrix.org) for real-time discussions, or connect through the [Google Group](https://groups.google.com/g/geotrek-fr) to exchange ideas and insights.
+
+## Contribution
+
+Interested in contributing? See our [Contributing Guide](https://geotrek.readthedocs.io/en/latest/contribute/contributing.html
+). You can help in many ways, the ability to code is not necessary.
+
+## Thanks to all contributors ❤
+
+
+
+
+
+Made with [contrib.rocks](https://contrib.rocks).
+
+## License
+
+This project is under the BSD License. See the [LICENSE](Geotrek-admin/blob/main/LICENSE) for details.
+
+- OpenSource - BSD
+- Copyright (c) 2012-2024 - Makina Corpus Territoires / Parc national des Ecrins - Parc National du Mercantour - Parco delle Alpi Marittime
+
+
+[![Alt text](https://geotrek.fr/assets/img/logo_autonomens-h120m.png "Logo Autonomens")](https://datatheca.com/)
+
+----
+
+[![Alt text](http://geotrek.fr/assets/img/parc_ecrins.png "Logo du Parc national des Ecrins")](http://www.ecrins-parcnational.fr)
+[![Alt text](http://geotrek.fr/assets/img/parc_mercantour.png "Logo du Parc national du Mercantour")](http://www.mercantour.eu)
+[![Alt text](http://geotrek.fr/assets/img/alpi_maritime.png "Logo du Parc naturel des Alpes maritimes")](http://www.parcoalpimarittime.it)
diff --git a/README.rst b/README.rst
deleted file mode 100644
index 71279fa2b7..0000000000
--- a/README.rst
+++ /dev/null
@@ -1,112 +0,0 @@
-**Geotrek**, *paths* management for *National Parks* and *Tourism organizations*.
-
-.. image:: http://geotrek.fr/assets/img/logo.svg
-
-:master: |master-status| |master-coverage| |master-e2e| |master-rtd|
-
-.. |master-status| image::
- https://github.com/GeotrekCE/Geotrek-admin/actions/workflows/test.yml/badge.svg
- :alt: CI Status
- :target: https://github.com/GeotrekCE/Geotrek-admin/actions/workflows/test.yml
-
-.. |master-coverage| image::
- https://codecov.io/gh/GeotrekCE/Geotrek-admin/branch/master/graph/badge.svg
- :alt: Coverage
- :target: https://codecov.io/gh/GeotrekCE/Geotrek-admin
-
-.. |master-e2e| image::
- https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/simple/ktpy7v/master&style=flat&logo=cypress
- :alt: End to End
- :target: https://dashboard.cypress.io/projects/ktpy7v/runs
-
-.. |master-rtd| image::
- https://readthedocs.org/projects/geotrek/badge/?version=latest&style=flat
- :alt: Documentation
- :target: https://geotrek.readthedocs.io
-
-
-In brief
---------
-
-* Web mapping application offering GIS features
-* Manage paths, interventions, signage, treks, POIs, touristic events and so much more
-* Track maintenance of equipments and infrastructures
-* Control objets by district, protected areas, physical and legal status of paths
-* Compute 3D attributes using DEM draping
-* Allow to interconnect with multiple applications to synchronize data (Suricate, Apidae, Tourinsoft, etc.)
-* Publish a public website with `Geotrek-rando `_ (e.g. `PNE `_, `PNM-PNAM `_)
-* Publish a public mobile application with `Geotrek-mobile `_ (e.g. `OTGC `_, `CD39 `_)
-
-.. image:: http://geotrek.fr/assets/img/screen-1.png
-
-More information on product website http://geotrek.fr
-
-Documentation
--------------
-
-* `User manual (in french) `_
-* `Installation and configuration instructions `_
-* Help us translate `on Weblate `_
-
-
-Contribution
-------------
-
-* `Contributing guide `_
-* `Development documentation `_
-
-
-License
--------
-
-* OpenSource - BSD
-* Copyright (c) 2012-2023 - Makina Corpus / Parc national des Ecrins - Parc National du Mercantour - Parco delle Alpi Marittime
-
-.. image:: https://geotrek.fr/assets/img/logo_makina.svg
- :target: https://territoires.makina-corpus.com/
- :width: 170
-
-.. image:: https://geotrek.fr/assets/img/logo_autonomens-h120m.png
- :target: https://datatheca.com
-
-----
-
-.. image:: http://geotrek.fr/assets/img/parc_ecrins.png
- :target: http://www.ecrins-parcnational.fr
-
-
-.. image:: http://geotrek.fr/assets/img/parc_mercantour.png
- :target: http://www.mercantour.eu
-
-
-.. image:: http://geotrek.fr/assets/img/alpi_maritime.png
- :target: http://www.parcoalpimarittime.it
-
-
-Status of sub-projects
-----------------------
-
-* |django-mapentity| `django-mapentity `_
-* |django-leaflet| `django-leaflet `_
-* |convertit| `ConvertIt `_
-* |Leaflet.GeometryUtil| `Leaflet.GeometryUtil `_
-* |Leaflet.FileLayer| `Leaflet.FileLayer `_
-* |Leaflet.AlmostOver| `Leaflet.AlmostOver `_
-
-.. |django-mapentity| image:: https://github.com/makinacorpus/django-mapentity/actions/workflows/python-ci.yml/badge.svg
- :target: https://github.com/makinacorpus/django-mapentity/actions/workflows/python-ci.yml
-
-.. |django-leaflet| image:: https://github.com/makinacorpus/django-leaflet/actions/workflows/python-app.yml/badge.svg
- :target: https://github.com/makinacorpus/django-leaflet/actions/workflows/python-app.yml
-
-.. |convertit| image:: https://circleci.com/gh/makinacorpus/convertit.svg?style=shield
- :target: https://circleci.com/gh/makinacorpus/convertit
-
-.. |Leaflet.GeometryUtil| image:: https://travis-ci.org/makinacorpus/Leaflet.GeometryUtil.png?branch=master
- :target: https://travis-ci.org/makinacorpus/Leaflet.GeometryUtil?branch=master
-
-.. |Leaflet.FileLayer| image:: https://travis-ci.org/makinacorpus/Leaflet.FileLayer.png?branch=master
- :target: https://travis-ci.org/makinacorpus/Leaflet.FileLayer?branch=master
-
-.. |Leaflet.AlmostOver| image:: https://travis-ci.org/makinacorpus/Leaflet.GeometryUtil.png?branch=master
- :target: https://travis-ci.org/makinacorpus/Leaflet.AlmostOver?branch=master
diff --git a/VERSION b/VERSION
index 15e9bd1664..41ddfd50ac 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.110.0+dev
+2.111.0+dev
diff --git a/cypress/integration/nav_tabs.js b/cypress/integration/nav_tabs.js
index 26c0014233..34e48c58f5 100644
--- a/cypress/integration/nav_tabs.js
+++ b/cypress/integration/nav_tabs.js
@@ -22,9 +22,9 @@ describe('Nav tabs properties/attachments', () => {
cy.visit(href);
});
cy.get("a#tab-properties").should('have.class', 'active');
- cy.get("a#tab-attachments-accessibility").should('not.have.class', 'active');
- cy.get("a#tab-attachments-accessibility").click();
- cy.get("a#tab-attachments-accessibility").should('have.class', 'active');
+ cy.get("a#tab-related-objects").should('not.have.class', 'active');
+ cy.get("a#tab-related-objects").click();
+ cy.get("a#tab-related-objects").should('have.class', 'active');
cy.get("a#tab-properties").should('not.have.class', 'active');
});
});
diff --git a/cypress/package-lock.json b/cypress/package-lock.json
index 66abd3b86c..c65edd1d41 100644
--- a/cypress/package-lock.json
+++ b/cypress/package-lock.json
@@ -475,9 +475,9 @@
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"node_modules/cross-spawn": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
- "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz",
+ "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -2117,9 +2117,9 @@
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"cross-spawn": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
- "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz",
+ "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==",
"requires": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
diff --git a/debian/changelog b/debian/changelog
index 2682ce2014..c5d83430d4 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,8 +1,14 @@
-geotrek-admin (2.110.0+dev) UNRELEASED; urgency=medium
+geotrek-admin (2.111.0+dev) UNRELEASED; urgency=medium
- *
+ *
- -- Justine Fricou Wed, 13 Nov 2024 11:09:31 +0100
+ -- Célia Prat Thu, 05 Dec 2024 14:29:50 +0100
+
+geotrek-admin (2.111.0) RELEASED; urgency=medium
+
+ * New package release
+
+ -- Célia Prat Thu, 05 Dec 2024 14:26:01 +0100
geotrek-admin (2.110.0) RELEASED; urgency=medium
diff --git a/dev-requirements.txt b/dev-requirements.txt
index c8a5720596..57bab4db87 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -20,7 +20,7 @@ click==8.1.3
# pip-tools
coverage==7.6.1
# via -r dev-requirements.in
-django==4.2.16
+django==4.2.17
# via
# -c requirements.txt
# django-debug-toolbar
@@ -29,15 +29,15 @@ django-debug-toolbar==4.3.0
# via -r dev-requirements.in
django-extensions==3.2.3
# via -r dev-requirements.in
-factory-boy==3.3.0
+factory-boy==3.3.1
# via -r dev-requirements.in
faker==19.3.1
# via factory-boy
-flake8==7.1.0
+flake8==7.1.1
# via -r dev-requirements.in
freezegun==1.5.1
# via -r dev-requirements.in
-importlib-metadata==6.8.0
+importlib-metadata==8.5.0
# via
# -c requirements.txt
# build
@@ -66,11 +66,11 @@ python-dateutil==2.9.0.post0
# -c requirements.txt
# faker
# freezegun
-six==1.16.0
+six==1.17.0
# via
# -c requirements.txt
# python-dateutil
-sqlparse==0.5.1
+sqlparse==0.5.3
# via
# -c requirements.txt
# django
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 8620518639..a54e197eed 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -2,6 +2,17 @@ ARG BASE_IMAGE=jammy
FROM ubuntu:${BASE_IMAGE} AS base
+# add labels
+LABEL org.opencontainers.image.authors="Makina Corpus"
+LABEL org.opencontainers.image.source="https://github.com/GeotrekCE/Geotrek-admin/"
+LABEL org.opencontainers.image.documentation="https://geotrek.readthedocs.io/"
+LABEL org.opencontainers.image.vendor="Makina Corpus"
+LABEL org.opencontainers.image.licenses="BSD-2-Clause"
+LABEL org.opencontainers.image.url="https://geotrek.fr"
+LABEL org.opencontainers.image.title="Geotrek-admin"
+LABEL org.opencontainers.image.description="Manage and promote your trails and tourist content and activities."
+
+
ENV PYTHONBUFFERED=1
ENV DEBIAN_FRONTEND=noninteractive
ENV ENV=prod
diff --git a/docs/_static/geotrek-admin.png b/docs/_static/geotrek-admin.png
new file mode 100644
index 0000000000..58dfa890f8
Binary files /dev/null and b/docs/_static/geotrek-admin.png differ
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 7276ee370f..1d650a65c3 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -2,9 +2,48 @@
CHANGELOG
=========
-2.110.0+dev (XXXX-XX-XX)
+2.111.0+dev (XXXX-XX-XX)
----------------------------
+**Performances**
+
+- Delay loading filter form in List Views (refs #2967)
+
+**UI/UX**
+
+- Move the related objects from the properties tab into their own tab, on objects details pages (refs #2967)
+- Move Treks' accessibility pictures into the attached files tab (refs #2967)
+- Removes the display of an object's structure in its properties tab title
+
+**Documentation**
+
+- Update theme color
+- Fix typo in documentation
+- Update and homogenize README.rst
+
+
+2.111.0 (2024-12-05)
+----------------------------
+
+**Features**
+
+- Add `CirkwiParser` to retrieve Treks and Touristic Contents from Cirkwi (refs #3947)
+
+**Improvements**
+
+- Remove overriding of SchemaRandonneeParser's filetype_name attribute (#4022)
+- Improve sync mobile and import views with current bootstrap style.
+- Docker image is now mirrored on github registry
+
+**Bug fixes**
+
+- Fix missing Dockerfile path on make build scripts
+- Fix SchemaRandonneeParser url update when description is null or was not updated (#4022)
+
+**Documentation**
+
+- Update documentation for release and update obsolete example
+- Add note about certbot ssl configuration in nginx
2.110.0 (2024-11-13)
@@ -937,7 +976,7 @@ In preparation for HD Views developments (PR #3298)
!!!! Clear cache after update. You can do this by going to admin panel, "clearcache" section, then delete default / fat and api_v2 !!!!
-**Improvements**
+**Improvments**
- Cache API v2 Detail endpoints and themes list endpoint
- Sensitive areas are now computed with buffered geometries with settings SENSITIVE_AREA_INTERSECTION_MARGIN. Use ST_INTERSECTS on it is faster.
diff --git a/docs/conf.py b/docs/conf.py
index 25f52dce5f..a662cfdb75 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -57,8 +57,8 @@
{
"media": "(prefers-color-scheme: light)",
"scheme": "default",
- "primary": "green",
- "accent": "light green",
+ "primary": "red",
+ "accent": "deep-orange",
"toggle": {
"icon": "material/weather-night",
"name": "Switch to dark mode",
@@ -67,8 +67,8 @@
{
"media": "(prefers-color-scheme: dark)",
"scheme": "slate",
- "primary": "green",
- "accent": "light green",
+ "primary": "red",
+ "accent": "deep-orange",
"toggle": {
"icon": "material/weather-sunny",
"name": "Switch to light mode",
diff --git a/docs/contribute/contributing.rst b/docs/contribute/contributing.rst
index 5b19cec4d3..5e228dd845 100644
--- a/docs/contribute/contributing.rst
+++ b/docs/contribute/contributing.rst
@@ -105,14 +105,14 @@ On master branch:
* Update files *VERSION* and *docs/changelog.rst* to remove ``+dev`` suffix and increment version (please use semver rules)
* Run ``dch -r -D RELEASED``, update version in the same way and save
-* Commit with message 'Release x.y.z' and push to ``master``
-* Create new release on Github, with tag X.Y.Z, click on "Generate release notes"
+* Commit with message 'Release X.Y.Z' and push to ``master``
+* Create new release with name 'Geotrek-admin X.Y.Z' on Github, with tag X.Y.Z, click on "Generate release notes"
* Wait for release to be published through CI
* Update files *VERSION* and *docs/changelog.rst* to add ``+dev`` suffix
* Run ``dch -v +dev --no-force-save-on-release`` and save
* Commit with message 'Back to development' and push to ```master``
-* When creating a new release 'x.y.z' on github, Github actions will generate the .deb package file, and publish it on https://packages.geotrek.fr (see ``.github/workflows/test.yml`` file for details)
+* When creating a new release 'X.Y.Z' on github, Github actions will generate the .deb package file, and publish it on https://packages.geotrek.fr (see ``.github/workflows/test.yml`` file for details)
Other ways to contribute
-------------------------
diff --git a/docs/install/configuration.rst b/docs/install/configuration.rst
index f5cd365e75..099239becb 100644
--- a/docs/install/configuration.rst
+++ b/docs/install/configuration.rst
@@ -71,11 +71,13 @@ After this, edit ``nginx.conf.in`` to add your certificate.
If you generate it with letsencrypt :
You can use certbot to add the certificate in your configuration.
-But you will have to move the configuration automatically added into ``nginx.conf``, to the file ``nginx.conf.in``
-in ``/opt/geotrek-admin/var/conf/`` directory
+But you will have to move the configuration automatically added into ``nginx.conf``, to the file ``nginx.conf.in`` in ``/opt/geotrek-admin/var/conf/`` directory.
-You have to move the configuration to the file ``nginx.conf.in`` because ``nginx.conf`` is automatically
-changed during command ``dpkg-reconfigure geotrek-admin``.
+You have to move the configuration to the file ``nginx.conf.in`` because ``nginx.conf`` is automatically changed during command ``dpkg-reconfigure geotrek-admin``.
+
+.. note::
+
+ You need to replace the ``$`` from Certbot with ``${DOLLAR}`` everywhere in the ``nginx.conf.in`` file, then run the command ``sudo dpkg-reconfigure geotrek-admin`` to regenerate the file.
Mandatory settings
diff --git a/docs/install/maintenance.rst b/docs/install/maintenance.rst
index 990c2073c0..471560069c 100644
--- a/docs/install/maintenance.rst
+++ b/docs/install/maintenance.rst
@@ -34,11 +34,9 @@ Restore
If you restore Geotrek-admin on a new server, you will have to install PostgreSQL and PostGIS and create a database user first.
Otherwise go directly to the database creation step.
-Example for Ubuntu:
-
.. code-block:: bash
- sudo apt install postgis
+ sudo apt install postgresql-14 postgresql-14-postgis-3
sudo -u postgres psql -c "CREATE USER geotrek WITH ENCRYPTED PASSWORD 'geotrek';"
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 2630f0cf0c..4b63ae0b3c 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -30,7 +30,7 @@ imagesize==1.4.1
# via sphinx
importlib-metadata==6.8.0
# via sphinx
-jinja2==3.1.4
+jinja2==3.1.5
# via sphinx
livereload==2.7.0
# via sphinx-autobuild
@@ -50,7 +50,7 @@ pydantic-extra-types==2.10.0
# via sphinx-immaterial
pygments==2.18.0
# via sphinx
-pytz==2023.3.post1
+pytz==2024.2
# via babel
requests==2.32.3
# via
@@ -82,7 +82,7 @@ sphinxcontrib-qthelp==1.0.3
# via sphinx
sphinxcontrib-serializinghtml==1.1.5
# via sphinx
-tornado==6.4.1
+tornado==6.4.2
# via livereload
typing-extensions==4.12.2
# via
diff --git a/docs/usage/apis.rst b/docs/usage/apis.rst
index 00c8bff79b..1159d3628f 100644
--- a/docs/usage/apis.rst
+++ b/docs/usage/apis.rst
@@ -27,7 +27,7 @@ APIs externes
Geotrek et IGNrando'
--------------------
-Geotrek-admin est capable de produire un flux des itinéraires et POIs présents dans sa BDD au format Cirkwi pour pouvoir les importer directement dans IGNrando' (https://makina-corpus.com/sig-webmapping/geotrek-et-lign-ca-fonctionne).
+Geotrek-admin est capable de produire un flux des itinéraires et POIs présents dans sa BDD au format Cirkwi pour pouvoir les importer directement dans IGNrando' `(voir cet article) `_.
Exemple des randonnées et POIs du Parc national des Ecrins publiées sur IGNrando' depuis Geotrek-admin : https://ignrando.fr/fr/communautes/parc-national-des-ecrins
diff --git a/docs/usage/configuration-ttw.rst b/docs/usage/configuration-ttw.rst
index 48ec32a766..b1ba1315aa 100644
--- a/docs/usage/configuration-ttw.rst
+++ b/docs/usage/configuration-ttw.rst
@@ -53,10 +53,12 @@ Exemple : ajouter une étiquette
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Les étiquettes sont des encarts "pré-configurés" pouvant être réutilisés sur de multiples itinéraires. Elles présentent plusieurs avantages :
+
- ne pas avoir à saisir à chaque itinéraire les même informations
- permet de filtrer les itinéraires dans la vue liste (catégorie "Autres") sur Geotrek-Rando.
Pour les configurer, vous devez :
+
- vous rendre dans le module de configuration
- dans la section Étiquettes du groupe **COMMUN** cliquer sur :guilabel:`+ Ajouter`
@@ -285,7 +287,7 @@ Geotrek permet de configurer un ou plusieurs portails. Ce terme est utilisé pou
Ainsi, il est possible d'avoir plusieurs Geotrek-Rando branchés sur un seul Geotrek-Admin. Grâce à leur distinction sous forme de portail, il sera alors aisé de choisir sur quel Geotrek-Rando on souhaite faire apparaitre une information.
-Avec le widget Geotrek (https://github.com/GeotrekCE/geotrek-rando-widget) il est également possible d'utiliser cette fonctionnalité pour distinguer les contenus à afficher dans un widget ou dans un autre (https://makina-corpus.com/logiciel-libre/developpement-geotrek-widget-finance-parc-naturel-regional-haut-jura).
+Avec `Geotrek-widget `_ il est également possible d'utiliser cette fonctionnalité pour distinguer les contenus à afficher dans un widget ou dans un autre `(voir cet article) `_.
Pour configurer un ou plusieurs portails, il faut se rendre dans le module de configuration sur la section "Portails cibles".
@@ -314,9 +316,9 @@ Intégration des fonds de cartes
Il est possible d'intégrer dans Geotrek différents fonds de carte comme :
-* OpenStreetMap : https://www.openstreetmap.org/#map=6/46.449/2.210
-* OpenTopoMap : https://opentopomap.org/#map=6.49.000/10.000
-* ou les données IGN : https://geoservices.ign.fr/services-geoplateforme-diffusion
+* `OpenStreetMap `_
+* `OpenTopoMap `_
+* `ou les données IGN `_
Pour configurer l'ajout de fonds de plan, référez vous à cette section :ref:`Map settings `
@@ -345,16 +347,16 @@ Afin de s'intégrer au mieux dans le design standard, les couleurs suivantes son
Voici quelques ressources en ligne proposant des pictogrammes (sous licence libre) :
-- `https://pictogrammers.com/library/mdi/ `_
-- `https://thenounproject.com/ `_
-- `http://map-icons.com/ `_
-- `https://www.opensymbols.org/ `_
-- `https://www.svgrepo.com/ `_
-- `http://www.entypo.com/ `_
-- `https://icons.getbootstrap.com/ `_
-- `https://icongr.am/ `_
-- `https://cocomaterial.com/ `_
-- `https://icofont.com/ `_
-- `https://fontello.com/ `_
-- `https://iconmonstr.com/ `_
-- `https://fontawesome.com/icons `_
+- `Pictogrammers `_
+- `The Noun Project `_
+- `Map icons `_
+- `Opensymbols `_
+- `SVG repo `_
+- `Entypo `_
+- `Icons Getbootstrap `_
+- `Icongr `_
+- `Cocomaterial `_
+- `Icofont `_
+- `Fontello `_
+- `Iconmonstr `_
+- `Fontawesome `_
diff --git a/geotrek/api/templates/mobile/sync_mobile.html b/geotrek/api/templates/mobile/sync_mobile.html
index f689781660..9b6425e0dd 100644
--- a/geotrek/api/templates/mobile/sync_mobile.html
+++ b/geotrek/api/templates/mobile/sync_mobile.html
@@ -32,29 +32,29 @@
has_progress = true;
disable_sync_button(false);
- $('#progress-value').show();
- $("#progress-value").removeClass('bar-danger');
- $("#progress-value").parent().addClass("active");
+ $("#progress-bar").removeClass('bg-danger');
+ $("#progress-bar").parent().addClass("active");
if (this.result.current) {
- $("#progress-value").css("width", this.result.current+'%');
+ $("#progress-bar").css("width", this.result.current + '%');
- if (this.result.current == 100){
- $("#progress-value").parent().removeClass("active");
- $("#progress-value").addClass('bar-success');
- }
+ if (this.result.current == 100) {
+ $("#progress-bar").parent().removeClass("active");
+ $("#progress-bar").addClass('bg-success');
+ }
}
if (this.result.infos) {
- $("#progress-text").text(this.result.infos);
- }
+ $("#progress-bar").text(this.result.infos);
+ }
}
else {
if (this.status == 'FAILURE'){
// case of exception in task
- $("#progress-text").text("{% trans 'An error occured' %}");
- $('#exception-message').text(this.result.exc_type + ' : ' + this.result.exc_message)
- $("#progress-value").addClass('bar-danger');
- $("#progress-value").parent().removeClass("active");
+ $("#progress-bar").text("{% trans 'An error occured' %}");
+ $('#exception-message').text(this.result.exc_type + ' : ' + this.result.exc_message)
+ $('#exception-message').show();
+ $("#progress-bar").addClass('bg-danger');
+ $("#progress-value").parent().removeClass("active");
}
}
});
@@ -70,11 +70,12 @@
get_sync_infos();
$('#btn-confirm')[0].addEventListener('click', function(evt) {
- $("#progress-value").css("width", '0%');
- $("#progress-text").text('');
- $("#progress-value").parent().addClass("active");
- $("#progress-value").removeClass('bar-success');
- $("#progress-value").removeClass('bar-danger');
+ $('#exception-message').hide();
+ $("#progress-bar").css("width", '0%');
+ $("#progress-bar").text('');
+ $("#progress-bar").parent().addClass("active");
+ $("#progress-bar").removeClass('bg-success');
+ $("#progress-bar").removeClass('bg-danger');
$.post(
$('#form-sync').attr('action'),
@@ -87,10 +88,10 @@
window.setInterval(function(){
get_sync_infos();
- }, 500);
+ }, 1000);
});
-
+
{% endblock extrahead %}
{% block toolbar %}
@@ -98,22 +99,31 @@
{% block mainpanel %}
-
diff --git a/geotrek/authent/templates/authent/authent_propertiestab_fragment.html b/geotrek/authent/templates/authent/authent_propertiestab_fragment.html
deleted file mode 100644
index 9bf0c109e6..0000000000
--- a/geotrek/authent/templates/authent/authent_propertiestab_fragment.html
+++ /dev/null
@@ -1,4 +0,0 @@
-{% load i18n %}
-{% if object.structure %}
- {{ object.structure }}
-{% endif %}
\ No newline at end of file
diff --git a/geotrek/cirkwi/locale/de/LC_MESSAGES/django.po b/geotrek/cirkwi/locale/de/LC_MESSAGES/django.po
index 790b8f65d0..edaee0eb4f 100644
--- a/geotrek/cirkwi/locale/de/LC_MESSAGES/django.po
+++ b/geotrek/cirkwi/locale/de/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-04-03 15:39+0000\n"
+"POT-Creation-Date: 2025-01-06 15:41+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -44,3 +44,28 @@ msgstr ""
msgid "Cirkwi POI categories"
msgstr ""
+
+#, python-brace-format
+msgid ""
+"{model} '{val}' did not exist in Geotrek-Admin and was automatically created"
+msgstr ""
+
+#, python-brace-format
+msgid "{model} '{val}' does not exists in Geotrek-Admin. Please add it"
+msgstr ""
+
+#, python-brace-format
+msgid ""
+"No Practice matching Cirkwi Locomotion '{type}' (id: '{id}') was found. "
+"Please add it"
+msgstr ""
+
+msgid "Difficulty Level with Cirkwi Level"
+msgstr ""
+
+msgid "Address"
+msgstr ""
+
+#, python-brace-format
+msgid "Type 1 '{type}' does not exist for category '{cat}'. Please add it"
+msgstr ""
diff --git a/geotrek/cirkwi/locale/en/LC_MESSAGES/django.po b/geotrek/cirkwi/locale/en/LC_MESSAGES/django.po
index 790b8f65d0..edaee0eb4f 100644
--- a/geotrek/cirkwi/locale/en/LC_MESSAGES/django.po
+++ b/geotrek/cirkwi/locale/en/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-04-03 15:39+0000\n"
+"POT-Creation-Date: 2025-01-06 15:41+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -44,3 +44,28 @@ msgstr ""
msgid "Cirkwi POI categories"
msgstr ""
+
+#, python-brace-format
+msgid ""
+"{model} '{val}' did not exist in Geotrek-Admin and was automatically created"
+msgstr ""
+
+#, python-brace-format
+msgid "{model} '{val}' does not exists in Geotrek-Admin. Please add it"
+msgstr ""
+
+#, python-brace-format
+msgid ""
+"No Practice matching Cirkwi Locomotion '{type}' (id: '{id}') was found. "
+"Please add it"
+msgstr ""
+
+msgid "Difficulty Level with Cirkwi Level"
+msgstr ""
+
+msgid "Address"
+msgstr ""
+
+#, python-brace-format
+msgid "Type 1 '{type}' does not exist for category '{cat}'. Please add it"
+msgstr ""
diff --git a/geotrek/cirkwi/locale/es/LC_MESSAGES/django.po b/geotrek/cirkwi/locale/es/LC_MESSAGES/django.po
index 790b8f65d0..edaee0eb4f 100644
--- a/geotrek/cirkwi/locale/es/LC_MESSAGES/django.po
+++ b/geotrek/cirkwi/locale/es/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-04-03 15:39+0000\n"
+"POT-Creation-Date: 2025-01-06 15:41+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -44,3 +44,28 @@ msgstr ""
msgid "Cirkwi POI categories"
msgstr ""
+
+#, python-brace-format
+msgid ""
+"{model} '{val}' did not exist in Geotrek-Admin and was automatically created"
+msgstr ""
+
+#, python-brace-format
+msgid "{model} '{val}' does not exists in Geotrek-Admin. Please add it"
+msgstr ""
+
+#, python-brace-format
+msgid ""
+"No Practice matching Cirkwi Locomotion '{type}' (id: '{id}') was found. "
+"Please add it"
+msgstr ""
+
+msgid "Difficulty Level with Cirkwi Level"
+msgstr ""
+
+msgid "Address"
+msgstr ""
+
+#, python-brace-format
+msgid "Type 1 '{type}' does not exist for category '{cat}'. Please add it"
+msgstr ""
diff --git a/geotrek/cirkwi/locale/fr/LC_MESSAGES/django.po b/geotrek/cirkwi/locale/fr/LC_MESSAGES/django.po
index 3e11d6484c..224ed19959 100644
--- a/geotrek/cirkwi/locale/fr/LC_MESSAGES/django.po
+++ b/geotrek/cirkwi/locale/fr/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-04-03 15:39+0000\n"
+"POT-Creation-Date: 2025-01-06 15:41+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -43,3 +43,34 @@ msgstr "Catégorie de POI Cirkwi"
msgid "Cirkwi POI categories"
msgstr "Catégories de POI Cirkwi"
+
+#, python-brace-format
+msgid ""
+"{model} '{val}' did not exist in Geotrek-Admin and was automatically created"
+msgstr ""
+"{model} '{val}' n'existait pas dans Geotrek-Admin. Il a été créé "
+"automatiquement"
+
+#, python-brace-format
+msgid "{model} '{val}' does not exists in Geotrek-Admin. Please add it"
+msgstr "{model} '{val}' n'existe pas dans Geotrek-Admin. Merci de l'ajouter"
+
+#, python-brace-format
+msgid ""
+"No Practice matching Cirkwi Locomotion '{type}' (id: '{id}') was found. "
+"Please add it"
+msgstr ""
+"Aucune Pratique ne correspond à la Locomotion Cirkwi '{type}' (id: '{id}'). "
+"Merci de l'ajouter"
+
+msgid "Difficulty Level with Cirkwi Level"
+msgstr "Niveau de Difficulté ayant le niveau Cirkwi"
+
+msgid "Address"
+msgstr "Adresse"
+
+#, python-brace-format
+msgid "Type 1 '{type}' does not exist for category '{cat}'. Please add it"
+msgstr ""
+"Le type 1 \"{type}\" n'existe pas pour la catégorie \"{cat}\". Vous devez "
+"l'ajouter."
diff --git a/geotrek/cirkwi/locale/it/LC_MESSAGES/django.po b/geotrek/cirkwi/locale/it/LC_MESSAGES/django.po
index 790b8f65d0..edaee0eb4f 100644
--- a/geotrek/cirkwi/locale/it/LC_MESSAGES/django.po
+++ b/geotrek/cirkwi/locale/it/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-04-03 15:39+0000\n"
+"POT-Creation-Date: 2025-01-06 15:41+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -44,3 +44,28 @@ msgstr ""
msgid "Cirkwi POI categories"
msgstr ""
+
+#, python-brace-format
+msgid ""
+"{model} '{val}' did not exist in Geotrek-Admin and was automatically created"
+msgstr ""
+
+#, python-brace-format
+msgid "{model} '{val}' does not exists in Geotrek-Admin. Please add it"
+msgstr ""
+
+#, python-brace-format
+msgid ""
+"No Practice matching Cirkwi Locomotion '{type}' (id: '{id}') was found. "
+"Please add it"
+msgstr ""
+
+msgid "Difficulty Level with Cirkwi Level"
+msgstr ""
+
+msgid "Address"
+msgstr ""
+
+#, python-brace-format
+msgid "Type 1 '{type}' does not exist for category '{cat}'. Please add it"
+msgstr ""
diff --git a/geotrek/cirkwi/locale/nl/LC_MESSAGES/django.po b/geotrek/cirkwi/locale/nl/LC_MESSAGES/django.po
index 790b8f65d0..edaee0eb4f 100644
--- a/geotrek/cirkwi/locale/nl/LC_MESSAGES/django.po
+++ b/geotrek/cirkwi/locale/nl/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-04-03 15:39+0000\n"
+"POT-Creation-Date: 2025-01-06 15:41+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -44,3 +44,28 @@ msgstr ""
msgid "Cirkwi POI categories"
msgstr ""
+
+#, python-brace-format
+msgid ""
+"{model} '{val}' did not exist in Geotrek-Admin and was automatically created"
+msgstr ""
+
+#, python-brace-format
+msgid "{model} '{val}' does not exists in Geotrek-Admin. Please add it"
+msgstr ""
+
+#, python-brace-format
+msgid ""
+"No Practice matching Cirkwi Locomotion '{type}' (id: '{id}') was found. "
+"Please add it"
+msgstr ""
+
+msgid "Difficulty Level with Cirkwi Level"
+msgstr ""
+
+msgid "Address"
+msgstr ""
+
+#, python-brace-format
+msgid "Type 1 '{type}' does not exist for category '{cat}'. Please add it"
+msgstr ""
diff --git a/geotrek/cirkwi/parsers.py b/geotrek/cirkwi/parsers.py
new file mode 100644
index 0000000000..81b147cb7e
--- /dev/null
+++ b/geotrek/cirkwi/parsers.py
@@ -0,0 +1,312 @@
+import xml.etree.ElementTree as ET
+
+from django.conf import settings
+from django.contrib.gis.geos import Point, MultiPoint, GEOSGeometry
+from django.utils.translation import gettext as _
+
+from geotrek.common.utils.parsers import get_geom_from_gpx
+from geotrek.trekking.models import DifficultyLevel
+from geotrek.cirkwi.models import CirkwiLocomotion
+from geotrek.common.parsers import AttachmentParserMixin, Parser, RowImportError
+from geotrek.tourism.models import TouristicContent, TouristicContentType1
+from geotrek.trekking.models import Trek, Practice
+
+
+class CirkwiParser(AttachmentParserMixin, Parser):
+ """
+ auth: Allows to configure HTTP auth on parser requests
+ create: Create a Cirkwi Locomotion from label, before trying to map it to a Trek Practice with same label
+ delete: Delete old objects that are now missing from flux (based on 'get_to_delete_kwargs')
+ default_language: Allows to define which language this parser will populate by default
+ eid: Field to use as Cirwki id
+ provider: Allows to differentiate multiple Parser for the same model
+ rows: Quantity of objects to load per page (Cirkwi pagination)
+ update_only: Do not delete previous objects, and query Cirkwi API with most recent date_update
+ """
+ auth = ()
+ create = False
+ delete = False
+ default_language = settings.MODELTRANSLATION_DEFAULT_LANGUAGE
+ eid = 'eid'
+ provider = "Cirkwi"
+ rows = 10
+ update_only = False
+
+ field_options = {
+ "geom": {"required": True},
+ "name": {"required": True},
+ }
+ constant_fields = {
+ 'published': True,
+ }
+ non_fields = {
+ 'attachments': "informations/information[@langue='']/medias/images/image/*"
+ }
+
+ def __init__(self, *args, **kwargs):
+ # Extract URL parameter to use to retrieve updates only (will be used in 'next_row' method)
+ self.updated_after = None
+ if self.update_only and self.model.objects.filter(provider__exact=self.provider).exists():
+ last_update_timestamp = self.model.objects.filter(provider__exact=self.provider).latest(
+ 'date_update').date_update.timestamp()
+ self.updated_after = str(int(last_update_timestamp))
+ super().__init__(*args, **kwargs)
+
+ def normalize_field_name(self, name):
+ return name
+
+ def next_row(self):
+ # Get data from local file :
+ if self.filename:
+ with open(self.filename) as f:
+ xml_root = ET.fromstring(f.read())
+ # Yield objects given XML path in 'results_path'
+ entries = xml_root.findall(self.results_path)
+ self.nb = len(entries)
+ for row in entries:
+ yield row
+
+ # Get data from API URL :
+ else:
+ # Make first query to retrieve objects count
+ # We don't need the objects yet, just need to access 'nb_objects', so set params to 0
+ params = {
+ 'first': 0,
+ 'rows': 0,
+ }
+ if self.updated_after:
+ params['end-time'] = self.updated_after
+ response = self.request_or_retry(self.url, params=params, auth=self.auth)
+ # Save objects count
+ self.nb = int(ET.fromstring(response.content).find("listing_ids", {}).attrib['nb_objects'])
+
+ # Make several requests, using Cirkwi pagination parameters, until all objects are downloaded
+ first = 0
+ while first <= self.nb:
+ params['first'] = first
+ params['rows'] = self.rows
+ response = self.request_or_retry(self.url, params=params, auth=self.auth)
+ xml_root = ET.fromstring(response.content)
+ # Yield objects given XML path in 'results_path'
+ entries = xml_root.findall(self.results_path)
+ for row in entries:
+ yield row
+ first += self.rows
+
+ def get_part(self, dst, src, val):
+ """
+ The CirkwiParser class handles some XML-related specificities for the fields mapping:
+
+ - `src` values should follow an XML path syntax so the separator is '/' instead of '.'
+ - `src` value "locomotions/locomotion" gets the text value of the first XML element encountered (inside a element at the row's root level)
+ - "informations/information[@langue='fr']/titre" means we get the text value of the first element having a parent with attribute `langue` equals "fr"
+ - the "" marker is replaced with the `default_language` of the parser
+ - a '@@' sequence at the end followed by a attribute name indicates the fetched value should be the value of the attribute (for instance "locomotions/locomotion@@type" will get the value of the 'type' attribute of the element)
+ - finally the '/*' sequence at the end of a `src` value indicates a list of all XML elements matching the path should be returned (so "locomotions/locomotion/*" has the same meaning than "locomotions.*.locomotion" from the base Parser class)
+ """
+ # Recursively extract XML attributes
+ if '@@' in src and src[:2] != '@@':
+ part, attrib = src.split('@@', 1)
+ return self.get_part(dst, f"@@{attrib}", val.find(part))
+ # Extract XML attributes
+ elif src.startswith('@@'):
+ return val.attrib[src[2:]]
+ else:
+ # Replace language attribute
+ if "''" in src:
+ src = src.replace("", self.default_language)
+ # Return a list of XML elements
+ if src.endswith('/*'):
+ return val.findall(src[:-2])
+ # Return inner text if XML element exists
+ if val.find(src) is None:
+ return None
+ return val.find(src).text
+
+ def filter_attachments(self, src, val):
+ attachments = []
+ for attachment in val:
+ legend = attachment.find('legende')
+ if legend is not None:
+ legend = legend.text
+ url = attachment.find('url').text
+ author = attachment.find('credit')
+ if author is not None:
+ author = author.text
+ attachments.append([url, legend, author])
+ return attachments
+
+
+class CirkwiTrekParser(CirkwiParser):
+ model = Trek
+ results_path = 'circuit'
+ fields = {
+ "eid": "@@id_circuit",
+ "name": "informations/information[@langue='']/titre",
+ "description_teaser": "informations/information[@langue='']/description",
+ "description": ("informations/information[@langue='']/informations_complementaires/information_complementaire/titre",
+ "informations/information[@langue='']/informations_complementaires/information_complementaire/description",
+ "infos_parcours/info_parcours/informations/information[@langue='']/description/*"),
+ "points_reference": ("infos_parcours/info_parcours/adresse/position/lat/*",
+ "infos_parcours/info_parcours/adresse/position/lng/*"),
+ "geom": "fichier_trace@@url",
+ "practice": ("locomotions/locomotion@@type", "locomotions/locomotion@@id_locomotion"),
+ "difficulty": "locomotions/locomotion@@difficulte",
+ "duration": "locomotions/locomotion@@duree",
+ }
+
+ def filter_geom(self, src, val):
+ response = self.request_or_retry(url=val)
+ return get_geom_from_gpx(response.content)
+
+ def filter_practice(self, src, val):
+ """
+ We try to :
+ 1 - Find matching Cirkwi Locomotion, OR create it if `create` is set to `True`
+ 2 - If 1 was successful, find Trek Practice matching this Cirkwi Locomotion by label. We do not create extra Practices automatically.
+ """
+ label, eid = val
+ try:
+ cirkwi_locomotion = CirkwiLocomotion.objects.get(name=label, eid=eid)
+ except CirkwiLocomotion.DoesNotExist:
+ if self.create:
+ cirkwi_locomotion = CirkwiLocomotion.objects.create(name=label, eid=eid)
+ self.add_warning(
+ _("{model} '{val}' did not exist in Geotrek-Admin and was automatically created").format(
+ model='Cirkwi Locomotion', val=label))
+ else:
+ self.add_warning(_("{model} '{val}' does not exists in Geotrek-Admin. Please add it").format(
+ model='Cirkwi Locomotion', val=val))
+ raise RowImportError
+ try:
+ practice = Practice.objects.get(cirkwi=cirkwi_locomotion)
+ except Practice.DoesNotExist:
+ try:
+ practice = Practice.objects.get(name=label, cirkwi__isnull=True)
+ practice.cirkwi = cirkwi_locomotion
+ practice.save()
+ except Practice.DoesNotExist:
+ self.add_warning(
+ _("No Practice matching Cirkwi Locomotion '{type}' (id: '{id}') was found. Please add it").format(
+ type=label,
+ id=eid
+ )
+ )
+ raise RowImportError
+ return practice
+
+ def filter_duration(self, src, val):
+ if val != "0":
+ return int(val) / 3600
+ return None
+
+ def filter_difficulty(self, src, val):
+ """
+ We try to find matching Difficulty Level through its Cirkwi id.
+ We do not create extra Difficulty Levels automatically.
+ """
+ difficulty = None
+ if val != '0':
+ try:
+ difficulty = DifficultyLevel.objects.get(cirkwi_level=int(val))
+ except DifficultyLevel.DoesNotExist:
+ self.add_warning(_("{model} '{val}' does not exists in Geotrek-Admin. Please add it").format(
+ model=_('Difficulty Level with Cirkwi Level'), val=val))
+ return difficulty
+
+ def filter_description(self, src, val):
+ desc = ""
+ compl_title, compl_descr, step_descriptions = val
+ if compl_title and compl_descr:
+ desc += f"{compl_title}: {compl_descr}"
+ # Extract text from all XML Elements, and build ordered list for 'points_reference'
+ step_descriptions = list(map(lambda x: x.text, step_descriptions))
+ if step_descriptions:
+ desc += "\r\n"
+ for step_description in step_descriptions:
+ desc += f"
{step_description}
"
+ desc += ""
+ return desc
+
+ def filter_points_reference(self, src, val):
+ step_lats, step_longs = val
+ step_lats = list(map(lambda x: float(x.text), step_lats))
+ step_longs = list(map(lambda x: float(x.text), step_longs))
+ steps = MultiPoint([Point(x, y, srid=4326) for x, y in zip(step_longs, step_lats)])
+ geom = GEOSGeometry(steps, srid=4326)
+ return geom.transform(settings.SRID, clone=True)
+
+
+class CirkwiTouristicContentParser(CirkwiParser):
+ model = TouristicContent
+ results_path = 'poi'
+ fields = {
+ "eid": "@@id_poi",
+ "name": "informations/information[@langue='']/titre",
+ "description": "informations/information[@langue='']/description",
+ "geom": ("adresse/position/lng", "adresse/position/lat"),
+ "practical_info": ("adresse/num", "adresse/rue", "adresse/cp", "adresse/ville", "informations/information[@langue='']/informations_complementaires/information_complementaire/*"),
+ "category": "categories/categorie/*",
+ }
+ m2m_fields = {
+ "type1": "categories/categorie/*",
+ }
+ field_options = {
+ "geom": {"required": True},
+ "name": {"required": True},
+ 'category': {'create': True},
+ 'type1': {'create': True},
+ }
+ natural_keys = {
+ 'category': 'label',
+ 'type1': 'label',
+ }
+
+ def filter_practical_info(self, src, val):
+ num, street, code, city, other_infos = val
+ infos = ''
+ if (num and street) or (code and city):
+ address = _("Address")
+ infos += f'{address} : '
+ if num and street:
+ infos += f"{num} {street} "
+ if code and city:
+ infos += f"{code} {city} "
+ for other_info in other_infos:
+ infos += f" {other_info.find('titre').text} : "
+ infos += f"{other_info.find('description').text} "
+ return infos
+
+ def filter_category(self, src, val):
+ # val[0] is category
+ # val[1] is type1
+ name = val[0].attrib["nom"]
+ return self.apply_filter('category', src, name)
+
+ def filter_type1(self, src, val):
+ """
+ We try to find matching TouristicContentType1 through its label and category,
+ OR create it if `create` is set to `True` for 'type1' in mapping 'field_options'
+ """
+ # val[0] is category
+ # val[1] is type1
+ if val is None or len(val) < 2:
+ return []
+ label = val[1].attrib["nom"]
+ if self.field_options.get("type1", {}).get("create", False):
+ type1, __ = TouristicContentType1.objects.get_or_create(category=self.obj.category, label=label)
+ else:
+ try:
+ type1 = TouristicContentType1.objects.get(category=self.obj.category, label=label)
+ except TouristicContentType1.DoesNotExist:
+ self.add_warning(
+ _("Type 1 '{type}' does not exist for category '{cat}'. Please add it").format(
+ type=label, cat=self.obj.category.label))
+ return []
+ return [type1]
+
+ def filter_geom(self, src, val):
+ lng, lat = val
+ geom = Point(float(lng), float(lat), srid=4326) # WGS84
+ geom.transform(settings.SRID)
+ return geom
diff --git a/geotrek/cirkwi/tests/data/circuits.xml b/geotrek/cirkwi/tests/data/circuits.xml
new file mode 100644
index 0000000000..7e2e802714
--- /dev/null
+++ b/geotrek/cirkwi/tests/data/circuits.xml
@@ -0,0 +1,268 @@
+
+
+ 10925
+ 10926
+
+
+
+
+ Le patrimoine de Plancoët
+ Laissez-vous guider par ce chemin
+
+
+
+ Le patrimoine de Plancoët
+ https://example.net/a_picture.jpg
+ Manon
+
+
+ https://example.net/a_picture.jpg
+
+
+
+
+
+
+
+ https://www.cirkwi.com/mp3/6302_fr.mp3
+ Valentin
+
+
+
+
+
+ Horaires
+ Tous les jours sauf le Dimanche
+
+
+
+
+
+
+
+ https://www.mon-site.com/lien_vers_mon_pdf.pdf
+
+
+
+ Title en
+ Description en
+
+
+
+ Le patrimoine de Plancoët
+ https://example.net/a_picture.jpg
+ Manon
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Étape 1
+
+ Au départ du terminal des car-ferrys.
+
+
+
+
+
+ 62193
+
+ 50.9647654
+ 1.8614236
+
+ 5.54
+
+
+
+
+
+
+
+
+ Étape 2
+
+ Virer à droite, direction la plage-corniche de la Côte d'Opale.
+
+
+
+
+
+ 62193
+
+ 50.9588189
+ 1.8521179
+
+ 4.37
+
+
+
+
+
+
+ 74
+ chemin du sars barras
+ Plancoët
+ 22130
+ France
+
+ 48.5223295900644
+ -2.23556272792814
+
+
+ 3400
+ 354
+ -354
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Tour de lancienne église Sainte-Gertrude
+
+ A Tenneville, ce site reposant vous fera découvrir une église
+ classée dont il ne reste qu’une partie du bâtiment originel. Nous
+ vous laissons le plaisir de...\r\nClassé le 1985\r\n
+
+
+
+
+ Le patrimoine de Plancoët
+ https://example.net/a_picture.jpg
+
+
+
+
+
+
+ Belgique
+
+ 48.7125946
+ 6.1442926
+
+ 468.0
+
+
+
+ 1365123600
+ 1365130800
+
+
+ 1365123601
+ 1365130802
+
+
+
+
+
+
+
+
+
+
+
+
+ Tour de lancienne église Sainte-Gertrude
+
+ A Tenneville, ce site reposant vous fera découvrir une église
+ classée dont il ne reste qu’une partie du bâtiment originel. Nous
+ vous laissons le plaisir de...\r\nClassé le 1985\r\n
+
+
+
+
+ https://example.net/a_picture.jpg
+
+
+
+
+
+
+ 1
+ route de Bastogne
+ Tenneville
+ 6970
+ Belgique
+
+ 48.7125946
+ 6.1442926
+
+ 468.0
+
+
+
+ 1365123600
+ 1365130800
+
+
+ 1365123601
+ 1365130802
+
+
+
+ https://ws.cirkwi.com/link/jek3dd35kn
+
+
+
+
+
+
+ 1365123600
+ 1365130800
+
+
+ 1365123601
+ 1365130802
+
+
+
+ https://ws.cirkwi.com/link/g54rzr72zve
+
+
+
+
+
+
+
+ Le patrimoine de Plancoët à vélo
+ Laissez-vous guider par ce chemin
+
+
+
+
+
+
+
+
diff --git a/geotrek/cirkwi/tests/data/circuits_updated.xml b/geotrek/cirkwi/tests/data/circuits_updated.xml
new file mode 100644
index 0000000000..6d4ec3fa8c
--- /dev/null
+++ b/geotrek/cirkwi/tests/data/circuits_updated.xml
@@ -0,0 +1,20 @@
+
+
+ 10926
+
+
+
+
+ Le patrimoine de Plancoët à VTT
+ Laissez-vous guider par cette route
+
+
+
+
+
+
+
+
diff --git a/geotrek/cirkwi/tests/data/circuits_wrong_locomotion_and_difficulty.xml b/geotrek/cirkwi/tests/data/circuits_wrong_locomotion_and_difficulty.xml
new file mode 100644
index 0000000000..34f31b8b14
--- /dev/null
+++ b/geotrek/cirkwi/tests/data/circuits_wrong_locomotion_and_difficulty.xml
@@ -0,0 +1,284 @@
+
+
+ 10925
+ 10926
+ 10926
+
+
+
+
+ Le patrimoine de Plancoët
+ Laissez-vous guider par ce chemin
+
+
+
+ Le patrimoine de Plancoët
+ https://example.net/a_picture.jpg
+ Manon
+
+
+ https://example.net/a_picture.jpg
+
+
+
+
+
+
+
+ https://www.cirkwi.com/mp3/6302_fr.mp3
+ Valentin
+
+
+
+
+
+ Horaires
+ Tous les jours sauf le Dimanche
+
+
+
+
+
+
+
+ https://www.mon-site.com/lien_vers_mon_pdf.pdf
+
+
+
+ Title en
+ Description en
+
+
+
+ Le patrimoine de Plancoët
+ https://example.net/a_picture.jpg
+ Manon
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Étape 1
+
+ Au départ du terminal des car-ferrys.
+
+
+
+
+
+ 62193
+
+ 50.9647654
+ 1.8614236
+
+ 5.54
+
+
+
+
+
+
+
+
+ Étape 2
+
+ Virer à droite, direction la plage-corniche de la Côte d'Opale.
+
+
+
+
+
+ 62193
+
+ 50.9588189
+ 1.8521179
+
+ 4.37
+
+
+
+
+
+
+ 74
+ chemin du sars barras
+ Plancoët
+ 22130
+ France
+
+ 48.5223295900644
+ -2.23556272792814
+
+
+ 3400
+ 354
+ -354
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Tour de lancienne église Sainte-Gertrude
+
+ A Tenneville, ce site reposant vous fera découvrir une église
+ classée dont il ne reste qu’une partie du bâtiment originel. Nous
+ vous laissons le plaisir de...\r\nClassé le 1985\r\n
+
+
+
+
+ Le patrimoine de Plancoët
+ https://example.net/a_picture.jpg
+
+
+
+
+
+
+ Belgique
+
+ 48.7125946
+ 6.1442926
+
+ 468.0
+
+
+
+ 1365123600
+ 1365130800
+
+
+ 1365123601
+ 1365130802
+
+
+
+
+
+
+
+
+
+
+
+
+ Tour de lancienne église Sainte-Gertrude
+
+ A Tenneville, ce site reposant vous fera découvrir une église
+ classée dont il ne reste qu’une partie du bâtiment originel. Nous
+ vous laissons le plaisir de...\r\nClassé le 1985\r\n
+
+
+
+
+ https://example.net/a_picture.jpg
+
+
+
+
+
+
+ 1
+ route de Bastogne
+ Tenneville
+ 6970
+ Belgique
+
+ 48.7125946
+ 6.1442926
+
+ 468.0
+
+
+
+ 1365123600
+ 1365130800
+
+
+ 1365123601
+ 1365130802
+
+
+
+ https://ws.cirkwi.com/link/jek3dd35kn
+
+
+
+
+
+
+ 1365123600
+ 1365130800
+
+
+ 1365123601
+ 1365130802
+
+
+
+ https://ws.cirkwi.com/link/g54rzr72zve
+
+
+
+
+
+
+
+ Le patrimoine de Plancoët à vélo
+ Laissez-vous guider par ce chemin
+
+
+
+
+
+
+
+
+
+
+ Le patrimoine de Plancoët à vélo, difficulté inconnue
+ Laissez-vous guider par ce chemin
+
+
+
+
+
+
+
+
diff --git a/geotrek/cirkwi/tests/data/poi.xml b/geotrek/cirkwi/tests/data/poi.xml
new file mode 100644
index 0000000000..b1c500df7d
--- /dev/null
+++ b/geotrek/cirkwi/tests/data/poi.xml
@@ -0,0 +1,127 @@
+
+
+ 357
+ 358
+
+
+
+
+
+
+
+
+
+
+
+
+ Tour de lancienne église Sainte-Gertrude
+ A Tenneville, ce site reposant vous fera découvrir
+
+
+
+ Le patrimoine de Plancoët
+ https://example.net/a_picture.jpg
+ Manon
+
+
+ https://example.net/a_picture.jpg
+
+
+
+
+
+
+
+ https://www.cirkwi.com/mp3/6302_fr.mp3
+ Valentin
+
+
+
+
+
+ Horaire
+ Ouvert du Lundi au Vendredi de 8h à 19h
+
+
+ Contact
+ Téléphone: 01 02 03 04 05
+
+
+
+
+ Titre en anglais
+ Description en anglais
+
+
+
+ 1
+ route de Bastogne
+ Tenneville
+ 6970
+ Belgique
+
+ 48.7125946
+ 6.1442926
+
+ 468.0
+
+
+
+ 1365123600
+ 1365130800
+
+
+ 1365123601
+ 1365130802
+
+
+
+ https://ws.cirkwi.com/ul/p/357
+
+
+
+
+
+
+
+
+
+ Tour de lancienne église Sainte-Gertrude 2
+ A Tenneville, ce site reposant vous fera découvrir
+
+
+ Horaire
+ Ouvert du Lundi au Vendredi de 8h à 19h
+
+
+ Contact
+ Téléphone: 01 02 03 04 05
+
+
+
+
+ Titre en anglais
+ Description en anglais
+
+
+
+ 1
+ route de Bastogne
+ Tenneville
+ 6970
+ Belgique
+
+ 48.7125946
+ 6.1442926
+
+ 468.0
+
+
+
+
diff --git a/geotrek/cirkwi/tests/data/trek.gpx b/geotrek/cirkwi/tests/data/trek.gpx
new file mode 100644
index 0000000000..f7396e06d3
--- /dev/null
+++ b/geotrek/cirkwi/tests/data/trek.gpx
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/geotrek/cirkwi/tests/factories.py b/geotrek/cirkwi/tests/factories.py
new file mode 100644
index 0000000000..8cccb0e039
--- /dev/null
+++ b/geotrek/cirkwi/tests/factories.py
@@ -0,0 +1,10 @@
+import factory
+
+from .. import models
+
+
+class CirkwiLocomotionFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = models.CirkwiLocomotion
+
+ name = factory.Sequence(lambda n: "Cirkwi Locomotion %s" % n)
diff --git a/geotrek/cirkwi/tests/test_parsers.py b/geotrek/cirkwi/tests/test_parsers.py
new file mode 100644
index 0000000000..e670758d7e
--- /dev/null
+++ b/geotrek/cirkwi/tests/test_parsers.py
@@ -0,0 +1,197 @@
+import io
+import os
+from copy import copy
+from unittest import mock, skipIf
+from unittest.mock import Mock
+
+from django.conf import settings
+from django.core.management import call_command
+from django.test import TestCase
+
+from geotrek.cirkwi.parsers import (CirkwiTouristicContentParser,
+ CirkwiTrekParser)
+from geotrek.cirkwi.tests.factories import CirkwiLocomotionFactory
+from geotrek.common.models import FileType
+from geotrek.common.utils import testdata
+from geotrek.tourism.models import TouristicContent
+from geotrek.trekking.models import Trek
+from geotrek.trekking.tests.factories import (DifficultyLevelFactory,
+ PracticeFactory)
+
+
+class TestCirkwiTrekParserFr(CirkwiTrekParser):
+ url = 'https://example.net/'
+ create = True
+ default_language = 'fr'
+
+
+class TestCirkwiTrekParserFrNoCreate(CirkwiTrekParser):
+ url = 'https://example.net/'
+ create = False
+ default_language = 'fr'
+
+
+class TestCirkwiTrekParserFrUpdateOnly(CirkwiTrekParser):
+ url = 'https://example.net/'
+ create = False
+ update_only = True
+ default_language = 'fr'
+
+
+class TestCirkwiTrekParserEn(CirkwiTrekParser):
+ url = 'https://example.net/'
+ default_language = 'en'
+ # English parser must not delete attachments created by French parser
+ delete_attachments = False
+
+
+class TestCirkwiTouristicContentParserFr(CirkwiTouristicContentParser):
+ url = 'https://example.net/'
+ default_language = 'fr'
+
+
+class TestCirkwiTouristicContentDoNotCreateParserEn(CirkwiTouristicContentParser):
+ # Also tests using filename instead of url
+ filename = "geotrek/cirkwi/tests/data/poi.xml"
+ url = 'https://example.net/'
+ default_language = 'en'
+ # English parser must not delete attachments created by French parser
+ delete_attachments = False
+ field_options = {
+ "geom": {"required": True},
+ "name": {"required": True},
+ 'category': {'create': True},
+ }
+
+
+class TestCirkwiTouristicContentParserEn(CirkwiTouristicContentParser):
+ url = 'https://example.net/'
+ default_language = 'en'
+ # English parser must not delete attachments created by French parser
+ delete_attachments = False
+
+
+@skipIf(settings.TREKKING_TOPOLOGY_ENABLED, 'Test without dynamic segmentation only')
+class CirkwiParserTests(TestCase):
+
+ @classmethod
+ def setUpTestData(cls):
+ cls.filetype = FileType.objects.create(type="Photographie")
+ cls.locomotion = CirkwiLocomotionFactory(eid=2, name="Marche")
+ cls.practice = PracticeFactory(name="Pédestre", cirkwi=cls.locomotion)
+ PracticeFactory.create(name_fr="Vélo", name="Vélo")
+ cls.difficulty = DifficultyLevelFactory(cirkwi_level=5)
+
+ def make_dummy_get(self, data_filename):
+ def dummy_get(url, *args, **kwargs):
+ rv = Mock()
+ rv.status_code = 200
+ if ".jpg" in url:
+ rv.content = copy(testdata.IMG_FILE)
+ elif ".gpx" in url:
+ filename = "geotrek/cirkwi/tests/data/trek.gpx"
+ with open(filename, 'r') as f:
+ geodata = f.read()
+ rv.content = bytes(geodata, 'utf-8')
+ else:
+ filename = os.path.join("geotrek/cirkwi/tests/data/", data_filename)
+ with open(filename, 'r') as f:
+ rv.content = f.read()
+ return rv
+
+ return dummy_get
+
+ @mock.patch('requests.get')
+ def test_create_treks(self, mocked_get):
+
+ mocked_get.side_effect = self.make_dummy_get('circuits.xml')
+ output = io.StringIO()
+ call_command('import', 'geotrek.cirkwi.tests.test_parsers.TestCirkwiTrekParserFr', verbosity=2, stdout=output)
+ call_command('import', 'geotrek.cirkwi.tests.test_parsers.TestCirkwiTrekParserEn', verbosity=0)
+ self.assertEqual(Trek.objects.count(), 2)
+
+ # Test values for french parsing
+ t = Trek.objects.get(name_fr="Le patrimoine de Plancoët")
+ self.assertEqual(t.eid, '10925')
+ self.assertEqual(t.name_en, "Title en")
+ self.assertEqual(t.practice, self.practice)
+ self.assertEqual(t.description_teaser_fr, 'Laissez-vous guider par ce chemin')
+ self.assertEqual(t.description_teaser_en, "Description en")
+ self.assertIn("Horaires: Tous les jours sauf le Dimanche", t.description_fr)
+ self.assertIn("Au départ du terminal des car-ferrys.", t.description_fr)
+ self.assertIn("Virer à droite, direction la plage-corniche de la Côte d'Opale.", t.description_fr)
+ self.assertAlmostEqual(t.geom[0][0], 977776.9692000002)
+ self.assertAlmostEqual(t.geom[0][1], 6547354.842799998)
+ attachement = t.attachments.last()
+ self.assertEqual(attachement.title, '')
+ self.assertEqual(attachement.legend, 'Le patrimoine de Plancoët')
+ self.assertEqual(attachement.author, 'Manon')
+ self.assertEqual(attachement.attachment_file.size, len(testdata.IMG_FILE))
+ self.assertEqual(t.duration, 2.0)
+
+ # Test values for english parsing
+ t = Trek.objects.get(name_fr="Le patrimoine de Plancoët à vélo")
+ self.assertEqual(t.eid, '10926')
+ self.assertEqual(t.practice.name, "Vélo")
+ # Assert created Cirkwi Locomotion and mapped it to Practice
+ self.assertEqual(t.practice.cirkwi.name, "Vélo")
+ self.assertEqual(t.practice.cirkwi.eid, 3)
+ self.assertEqual(t.description_teaser_fr, 'Laissez-vous guider par ce chemin')
+ self.assertIn("Cirkwi Locomotion 'Vélo' n'existait pas dans Geotrek-Admin. Il a été créé automatiquement,", output.getvalue())
+
+ # Test update
+ mocked_get.side_effect = self.make_dummy_get('circuits_updated.xml')
+ call_command('import', 'geotrek.cirkwi.tests.test_parsers.TestCirkwiTrekParserFrUpdateOnly', verbosity=0)
+ self.assertEqual(Trek.objects.count(), 2)
+ t = Trek.objects.get(name_fr="Le patrimoine de Plancoët à VTT")
+ self.assertEqual(t.eid, '10926')
+ self.assertEqual(t.description_teaser_fr, 'Laissez-vous guider par cette route')
+ t = Trek.objects.get(name_fr="Le patrimoine de Plancoët")
+ self.assertFalse(t.deleted)
+
+ @mock.patch('requests.get')
+ def test_create_touristic_content_no_type(self, mocked_get):
+ output = io.StringIO()
+ mocked_get.side_effect = self.make_dummy_get('poi.xml')
+ call_command('import', 'geotrek.cirkwi.tests.test_parsers.TestCirkwiTouristicContentDoNotCreateParserEn', verbosity=2, stdout=output)
+ self.assertIn("Type 1 'Eglise' does not exist for category 'Monuments et Architecture'. Please add it",
+ output.getvalue())
+
+ @mock.patch('requests.get')
+ def test_create_trek_with_missing_locomotion_and_difficulty(self, mocked_get):
+ output = io.StringIO()
+ mocked_get.side_effect = self.make_dummy_get('circuits_wrong_locomotion_and_difficulty.xml')
+ # Test Locomotion does not exist
+ call_command('import', 'geotrek.cirkwi.tests.test_parsers.TestCirkwiTrekParserFrNoCreate', verbosity=2, stdout=output)
+ self.assertIn("Cirkwi Locomotion '['Aviron', '8']' n'existe pas dans Geotrek-Admin. Merci de l'ajouter,",
+ output.getvalue())
+ # Test Locomotion exists but no related Practice is found
+ CirkwiLocomotionFactory(name="Aviron", eid=8)
+ call_command('import', 'geotrek.cirkwi.tests.test_parsers.TestCirkwiTrekParserFrNoCreate', verbosity=2,
+ stdout=output)
+ self.assertIn("Aucune Pratique ne correspond à la Locomotion Cirkwi 'Aviron' (id: '8'). Merci de l'ajouter,",
+ output.getvalue())
+ self.assertIn("Niveau de Difficulté ayant le niveau Cirkwi '3' n'existe pas dans Geotrek-Admin. Merci de l'ajouter,",
+ output.getvalue())
+
+ @mock.patch('requests.get')
+ def test_create_touristic_content(self, mocked_get):
+ mocked_get.side_effect = self.make_dummy_get('poi.xml')
+ call_command('import', 'geotrek.cirkwi.tests.test_parsers.TestCirkwiTouristicContentParserFr', verbosity=0)
+ call_command('import', 'geotrek.cirkwi.tests.test_parsers.TestCirkwiTouristicContentParserEn', verbosity=0)
+ self.assertEqual(TouristicContent.objects.count(), 2)
+ tc = TouristicContent.objects.get(name_fr="Tour de lancienne église Sainte-Gertrude")
+ self.assertEqual(tc.name_en, "Titre en anglais")
+ self.assertEqual(tc.description_fr, 'A Tenneville, ce site reposant vous fera découvrir')
+ self.assertEqual(tc.description_en, "Description en anglais")
+ self.assertEqual(tc.practical_info_fr, "Adresse : 1 route de Bastogne 6970 Tenneville
Horaire : Ouvert du Lundi au Vendredi de 8h à 19h
Contact : Téléphone: 01 02 03 04 05 ")
+ self.assertEqual(str(tc.category), "Monuments et Architecture")
+ self.assertEqual(str(tc.type1.first()), "Eglise")
+ self.assertAlmostEqual(tc.geom.x, 931284.9680097454)
+ self.assertAlmostEqual(tc.geom.y, 6850434.12975747)
+ self.assertEqual(tc.attachments.count(), 2)
+ attachement = tc.attachments.last()
+ self.assertEqual(attachement.title, '')
+ self.assertEqual(attachement.legend, 'Le patrimoine de Plancoët')
+ self.assertEqual(attachement.author, 'Manon')
+ self.assertEqual(attachement.attachment_file.size, len(testdata.IMG_FILE))
diff --git a/geotrek/common/forms.py b/geotrek/common/forms.py
index c163145f98..6ed0860623 100644
--- a/geotrek/common/forms.py
+++ b/geotrek/common/forms.py
@@ -388,7 +388,7 @@ def __init__(self, request, *args, **kwargs):
self.helper.form_style = "default"
self.helper.label_class = 'col-md-3'
self.helper.field_class = 'col-md-9'
- self.fields['next'].initial = f"{self._object.get_detail_url()}?tab=attachments-accessibility"
+ self.fields['next'].initial = f"{self._object.get_detail_url()}?tab=attachments"
if not self.instance.pk:
form_actions = [
@@ -423,7 +423,7 @@ class Meta:
def success_url(self):
obj = self._object
- return f"{obj.get_detail_url()}?tab=attachments-accessibility"
+ return f"{obj.get_detail_url()}?tab=attachments"
def clean_attachment_accessibility_file(self):
uploaded_image = self.cleaned_data.get("attachment_accessibility_file", False)
diff --git a/geotrek/common/locale/de/LC_MESSAGES/django.po b/geotrek/common/locale/de/LC_MESSAGES/django.po
index 242d48b728..5993469057 100644
--- a/geotrek/common/locale/de/LC_MESSAGES/django.po
+++ b/geotrek/common/locale/de/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-08-06 08:26+0000\n"
+"POT-Creation-Date: 2025-01-07 14:35+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -695,6 +695,16 @@ msgctxt "West"
msgid "W"
msgstr ""
+msgid ""
+"Feature geometry cannot be converted to a single continuous LineString "
+"feature"
+msgstr ""
+
+msgid ""
+"Geometries from various features cannot be converted to a single continuous "
+"LineString feature"
+msgstr ""
+
msgid "Reports"
msgstr ""
diff --git a/geotrek/common/locale/en/LC_MESSAGES/django.po b/geotrek/common/locale/en/LC_MESSAGES/django.po
index 242d48b728..5993469057 100644
--- a/geotrek/common/locale/en/LC_MESSAGES/django.po
+++ b/geotrek/common/locale/en/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-08-06 08:26+0000\n"
+"POT-Creation-Date: 2025-01-07 14:35+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -695,6 +695,16 @@ msgctxt "West"
msgid "W"
msgstr ""
+msgid ""
+"Feature geometry cannot be converted to a single continuous LineString "
+"feature"
+msgstr ""
+
+msgid ""
+"Geometries from various features cannot be converted to a single continuous "
+"LineString feature"
+msgstr ""
+
msgid "Reports"
msgstr ""
diff --git a/geotrek/common/locale/es/LC_MESSAGES/django.po b/geotrek/common/locale/es/LC_MESSAGES/django.po
index cfdf07737a..bbbe3118bb 100644
--- a/geotrek/common/locale/es/LC_MESSAGES/django.po
+++ b/geotrek/common/locale/es/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-08-06 08:26+0000\n"
+"POT-Creation-Date: 2025-01-07 14:35+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Olivia Duval \n"
"Language-Team: LANGUAGE \n"
@@ -695,6 +695,16 @@ msgctxt "West"
msgid "W"
msgstr ""
+msgid ""
+"Feature geometry cannot be converted to a single continuous LineString "
+"feature"
+msgstr ""
+
+msgid ""
+"Geometries from various features cannot be converted to a single continuous "
+"LineString feature"
+msgstr ""
+
msgid "Reports"
msgstr ""
diff --git a/geotrek/common/locale/fr/LC_MESSAGES/django.po b/geotrek/common/locale/fr/LC_MESSAGES/django.po
index b3f66c1095..4bbb275daa 100644
--- a/geotrek/common/locale/fr/LC_MESSAGES/django.po
+++ b/geotrek/common/locale/fr/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-08-06 08:26+0000\n"
+"POT-Creation-Date: 2025-01-07 14:35+0000\n"
"PO-Revision-Date: 2020-09-23 07:10+0000\n"
"Last-Translator: Emmanuelle Helly \n"
"Language-Team: French \n"
"Language-Team: LANGUAGE \n"
@@ -695,6 +695,16 @@ msgctxt "West"
msgid "W"
msgstr ""
+msgid ""
+"Feature geometry cannot be converted to a single continuous LineString "
+"feature"
+msgstr ""
+
+msgid ""
+"Geometries from various features cannot be converted to a single continuous "
+"LineString feature"
+msgstr ""
+
msgid "Reports"
msgstr ""
diff --git a/geotrek/common/locale/nl/LC_MESSAGES/django.po b/geotrek/common/locale/nl/LC_MESSAGES/django.po
index 242d48b728..5993469057 100644
--- a/geotrek/common/locale/nl/LC_MESSAGES/django.po
+++ b/geotrek/common/locale/nl/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-08-06 08:26+0000\n"
+"POT-Creation-Date: 2025-01-07 14:35+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -695,6 +695,16 @@ msgctxt "West"
msgid "W"
msgstr ""
+msgid ""
+"Feature geometry cannot be converted to a single continuous LineString "
+"feature"
+msgstr ""
+
+msgid ""
+"Geometries from various features cannot be converted to a single continuous "
+"LineString feature"
+msgstr ""
+
msgid "Reports"
msgstr ""
diff --git a/geotrek/common/parsers.py b/geotrek/common/parsers.py
index f5de624a2c..5683d07dad 100644
--- a/geotrek/common/parsers.py
+++ b/geotrek/common/parsers.py
@@ -43,7 +43,6 @@
from geotrek.common.utils.parsers import add_http_prefix
from geotrek.common.utils.translation import get_translated_fields
-
if 'modeltranslation' in settings.INSTALLED_APPS:
from modeltranslation.fields import TranslationField
@@ -72,9 +71,14 @@ class DownloadImportError(ImportError):
class Parser:
"""
- provider: Allow to differentiate multiple Parser for the same model
- default_language: Allow to define which language this parser will populate by default
- headers: Allow to configure headers on parser requests
+ provider: Allows to differentiate multiple Parser for the same model
+ default_language: Allows to define which language this parser will populate by default
+ headers: Allows to configure headers on parser requests
+ default_language: Allows to define which language this parser will populate by default
+ eid: Field to use as external id
+ provider: A label that should include the data's source, it allows using multiple Parsers for the same model without concurrency
+ delete: Delete old objects that are now missing from flux (based on 'get_to_delete_kwargs' including 'provider')
+ update_only: Do not delete previous objects, and should query remote API with most recent 'date_update' timestamp
"""
label = None
model = None
diff --git a/geotrek/common/static/common/css/import.css b/geotrek/common/static/common/css/import.css
index 0a46011a5f..65ba3fc801 100644
--- a/geotrek/common/static/common/css/import.css
+++ b/geotrek/common/static/common/css/import.css
@@ -9,12 +9,10 @@
}
#progress-bars div.alert {
- margin-left: 45px;
display: none;
}
#progress-bars div.description {
- margin-left: 45px;
}
#progress-bars span.parser{
diff --git a/geotrek/trekking/static/trekking/css/sync_trek.css b/geotrek/common/static/common/css/sync.css
similarity index 100%
rename from geotrek/trekking/static/trekking/css/sync_trek.css
rename to geotrek/common/static/common/css/sync.css
diff --git a/geotrek/common/static/common/js/import.js b/geotrek/common/static/common/js/import.js
index 3dfe34ca34..a1ebf63d65 100644
--- a/geotrek/common/static/common/js/import.js
+++ b/geotrek/common/static/common/js/import.js
@@ -2,38 +2,42 @@ function updateImportProgressBars() {
$.getJSON('/commands/import-update.json', function(json) {
parent = document.querySelector('#progress-bars');
json.forEach(function(row) {
- var local_percent = row.result.current + "%";
- var status_class = 'pogress-info';
-
- if (row.status == 'SUCCESS') {
- local_percent = "100%";
- status_class = "progress-success";
- } else if (row.status == 'FAILURE') {
- local_percent = "100%";
- status_class = "progress-danger";
+ var local_percent = row.result.current;
+ var status_class = '';
+
+ if (row.status === 'SUCCESS') {
+ local_percent = "100";
+ status_class = "bg-success";
+ } else if (row.status === 'FAILURE') {
+ local_percent = "100";
+ status_class = "bg-danger";
}
// Update element if exists
if (element = document.getElementById(row.id)) {
- element.querySelector('.bar').style.width = local_percent;
- element.querySelector('.pull-left').innerHTML = local_percent;
+ element.querySelector('.progress-bar').setAttribute('style', 'width: ' + local_percent + '% ;');
+ element.querySelector('.progress-bar').setAttribute('aria-valuenow', local_percent);
+ element.querySelector('.progress-bar').innerHTML = local_percent + "%";
if(!element.querySelector('.alert').classList.contains('alert-success')) {
// Add report if success.
if (row.result.report) {
alert = element.querySelector('.alert');
alert.classList.add('alert-success');
- alert.innerHTML = row.result.report;
+ message = element.querySelector('.message');
+ message.innerHTML = row.result.report;
alert.style.display = 'block';
}
}
} else { //Create element in dom.
element = document.createElement('div');
- element.innerHTML = document.querySelector('#import-template').innerHTML
+ element.innerHTML = document.querySelector('#import-template').innerHTML;
element.id = row.id;
- element.querySelector('.bar').style.width = local_percent + ' : ';
- element.querySelector('.pull-left').innerHTML = local_percent;
+ element.querySelector('.progress-bar').setAttribute('style', 'width: ' + local_percent + '% ;');
+ element.querySelector('.progress-bar').setAttribute('aria-valuenow', local_percent);
+ element.querySelector('.progress-bar').innerHTML = local_percent + "%";
+
element.querySelector('.parser').innerHTML = row.result.parser;
element.querySelector('.filename').innerHTML = row.result.filename;
@@ -43,13 +47,13 @@ function updateImportProgressBars() {
// Handle errors if any.
if (row.result.exc_message) {
element.querySelector('.alert').classList.add('alert-danger');
- element.querySelector('.alert span').innerHTML = row.result.exc_type + " : " + row.result.exc_message;
+ element.querySelector('.message span').innerHTML = row.result.exc_type + " : " + row.result.exc_message;
element.querySelector('.alert').style.display = 'block';
}
// Add class on status change.
- if (!element.querySelector('.progress').classList.contains(status_class)) {
- element.querySelector('.progress').classList.add(status_class);
+ if (!element.querySelector('.progress-bar').classList.contains(status_class)) {
+ element.querySelector('.progress-bar').classList.add(status_class);
}
});
});
diff --git a/geotrek/common/static/common/main.js b/geotrek/common/static/common/main.js
deleted file mode 100644
index 5028853b65..0000000000
--- a/geotrek/common/static/common/main.js
+++ /dev/null
@@ -1,5 +0,0 @@
-$(window).on('entity:view:list', function () {
- // Move all topology-filters to separate tab
- $('#mainfilter .right-filter').parent('p')
- .detach().appendTo('#mainfilter > .right');
-});
diff --git a/geotrek/common/templates/common/common_extrabody_fragment.html b/geotrek/common/templates/common/common_extrabody_fragment.html
deleted file mode 100644
index ec8c160130..0000000000
--- a/geotrek/common/templates/common/common_extrabody_fragment.html
+++ /dev/null
@@ -1,2 +0,0 @@
-{% load static %}
-
diff --git a/geotrek/common/templates/common/hdviewpoint_detail.html b/geotrek/common/templates/common/hdviewpoint_detail.html
index 7f7a02b2ad..bdeebbd334 100644
--- a/geotrek/common/templates/common/hdviewpoint_detail.html
+++ b/geotrek/common/templates/common/hdviewpoint_detail.html
@@ -42,7 +42,6 @@
- {% block mainform %}
- {% if form %}
- {% crispy form form.helper %}
- {% endif %}
- {% if encoding_error %}
-
- {% trans "Decoding error. Please check encoding and use only ASCII in file names." %}
-
- {% endif %}
- {% if form_without_file %}
- {% crispy form_without_file form_without_file.helper%}
- {% endif %}
- {% if form_suricate %}
- {% crispy form_suricate form_suricate.helper%}
- {% endif %}
- {% if not form and not form_without_file and not form_suricate %}
-
{% trans "No parser available." %}
- {% endif %}
- {% endblock mainform %}
-
-
-
+
+ {% block mainform %}
+ {% if form %}
+ {% crispy form form.helper %}
+ {% endif %}
+ {% if encoding_error %}
+
+ {% trans "Decoding error. Please check encoding and use only ASCII in file names." %}
+
+ {% endif %}
+ {% if form_without_file %}
+ {% crispy form_without_file form_without_file.helper %}
+ {% endif %}
+ {% if form_suricate %}
+ {% crispy form_suricate form_suricate.helper %}
+ {% endif %}
+ {% if not form and not form_without_file and not form_suricate %}
+
{% trans "No parser available." %}
+ {% endif %}
+ {% endblock mainform %}
+
+
+
-
+
{% endblock mainpanel %}
diff --git a/geotrek/common/templates/common/sync_rando.html b/geotrek/common/templates/common/sync_rando.html
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/geotrek/common/tests/test_attachments.py b/geotrek/common/tests/test_attachments.py
index db17c15482..7878a8091c 100644
--- a/geotrek/common/tests/test_attachments.py
+++ b/geotrek/common/tests/test_attachments.py
@@ -230,7 +230,7 @@ def attachmentPostData(self):
'legend': "A legend",
'attachment_accessibility_file': get_dummy_uploaded_image(name='face.png'),
'info_accessibility': 'slope',
- 'next': f"{self.object.get_detail_url()}?tab=attachments-accessibility"
+ 'next': f"{self.object.get_detail_url()}?tab=attachments"
}
return data
@@ -238,7 +238,7 @@ def test_upload_redirects_to_trek_detail_url(self):
response = self.client.post(add_url_for_obj(self.object),
data=self.attachmentPostData())
self.assertEqual(response.status_code, 302)
- self.assertEqual(response['location'], f"{self.object.get_detail_url()}?tab=attachments-accessibility")
+ self.assertEqual(response['location'], f"{self.object.get_detail_url()}?tab=attachments")
self.assertEqual(3, AccessibilityAttachment.objects.count())
self.client.force_login(user=self.user)
@@ -278,7 +278,7 @@ def attachmentPostData(self):
'legend': "A legend",
'attachment_accessibility_file': get_dummy_uploaded_image(name='face.png'),
'info_accessibility': 'slope',
- 'next': f"{self.object.get_detail_url()}?tab=attachments-accessibility"
+ 'next': f"{self.object.get_detail_url()}?tab=attachments"
}
return data
@@ -300,7 +300,7 @@ def test_post_update_url(self):
data=self.attachmentPostData(),
)
self.assertEqual(response.status_code, 302)
- self.assertEqual(response['location'], f"{self.object.get_detail_url()}?tab=attachments-accessibility")
+ self.assertEqual(response['location'], f"{self.object.get_detail_url()}?tab=attachments")
self.attachment.refresh_from_db()
self.assertEqual(self.attachment.legend, "A legend")
self.client.force_login(user=self.user)
@@ -318,7 +318,7 @@ def test_get_delete_with_perms_url(self):
self.client.force_login(user=self.superuser)
response = self.client.get(delete_url_for_obj(self.attachment))
self.assertEqual(response.status_code, 302)
- self.assertEqual(response['location'], f"{self.object.get_detail_url()}?tab=attachments-accessibility")
+ self.assertEqual(response['location'], f"{self.object.get_detail_url()}?tab=attachments")
self.assertEqual(1, AccessibilityAttachment.objects.count())
self.client.force_login(user=self.user)
@@ -334,7 +334,7 @@ def user_perms(p, obj=None):
self.client.force_login(user=self.user)
response = self.client.get(delete_url_for_obj(self.attachment))
self.assertEqual(response.status_code, 302)
- self.assertEqual(response['location'], f"{self.object.get_detail_url()}?tab=attachments-accessibility")
+ self.assertEqual(response['location'], f"{self.object.get_detail_url()}?tab=attachments")
self.assertEqual(2, AccessibilityAttachment.objects.count())
response = self.client.get(delete_url_for_obj(self.attachment), follow=True)
self.assertIn(b'You are not allowed to delete this attachment.', response.content)
@@ -446,7 +446,7 @@ def test_attachment_is_larger_max_size(self):
'attachment_accessibility_file': SimpleUploadedFile(file.name, file.read(), content_type='image/png'),
'author': "newauthor",
'info_accessibility': 'slope',
- 'next': f"{self.object.get_detail_url()}?tab=attachments-accessibility"
+ 'next': f"{self.object.get_detail_url()}?tab=attachments"
}
)
self.assertEqual(response.status_code, 302)
@@ -462,7 +462,7 @@ def test_attachment_is_larger_max_size(self):
'attachment_accessibility_file': big_image,
'author': "newauthor",
'info_accessibility': 'slope',
- 'next': f"{self.object.get_detail_url()}?tab=attachments-accessibility"
+ 'next': f"{self.object.get_detail_url()}?tab=attachments"
},
follow=True
)
@@ -488,7 +488,7 @@ def test_attachment_is_not_wide_enough(self):
'author': "newauthor",
'legend': "A legend",
'info_accessibility': 'slope',
- 'next': f"{self.object.get_detail_url()}?tab=attachments-accessibility"
+ 'next': f"{self.object.get_detail_url()}?tab=attachments"
}
)
self.assertEqual(response.status_code, 302)
@@ -508,7 +508,7 @@ def test_attachment_is_not_wide_enough(self):
'attachment_accessibility_file': SimpleUploadedFile(small_file.name, small_file.read(), content_type='image/png'),
'author': "newauthor",
'info_accessibility': 'slope',
- 'next': f"{self.object.get_detail_url()}?tab=attachments-accessibility"
+ 'next': f"{self.object.get_detail_url()}?tab=attachments"
},
follow=True
)
@@ -535,7 +535,7 @@ def test_attachment_is_not_tall_enough(self):
'attachment_accessibility_file': SimpleUploadedFile(file.name, file.read(), content_type='image/png'),
'author': "newauthor",
'info_accessibility': 'slope',
- 'next': f"{self.object.get_detail_url()}?tab=attachments-accessibility"
+ 'next': f"{self.object.get_detail_url()}?tab=attachments"
}
)
self.assertEqual(response.status_code, 302)
@@ -555,7 +555,7 @@ def test_attachment_is_not_tall_enough(self):
'author': "newauthor",
'legend': "A legend",
'info_accessibility': 'slope',
- 'next': f"{self.object.get_detail_url()}?tab=attachments-accessibility"
+ 'next': f"{self.object.get_detail_url()}?tab=attachments"
},
follow=True
)
@@ -577,7 +577,7 @@ def test_attachment_deleted(self):
'author': "newauthor",
'legend': "A legend",
'info_accessibility': 'slope',
- 'next': f"{self.object.get_detail_url()}?tab=attachments-accessibility"
+ 'next': f"{self.object.get_detail_url()}?tab=attachments"
}
)
self.assertEqual(response.status_code, 302)
diff --git a/geotrek/common/tests/test_utils.py b/geotrek/common/tests/test_utils.py
index d2250c0532..55fdbf4c72 100644
--- a/geotrek/common/tests/test_utils.py
+++ b/geotrek/common/tests/test_utils.py
@@ -1,13 +1,18 @@
import os
+from shutil import copy as copyfile
from django.conf import settings
from django.contrib.gis.geos import Point
-from django.test import SimpleTestCase, TestCase, override_settings
+from django.test import SimpleTestCase, TestCase
+from django.test.utils import override_settings
from ..parsers import Parser
-from ..utils import uniquify, format_coordinates, spatial_reference, simplify_coords
+from ..utils import (format_coordinates, simplify_coords, spatial_reference,
+ uniquify)
+from ..utils.file_infos import get_encoding_file
from ..utils.import_celery import create_tmp_destination, subclasses
-from ..utils.parsers import add_http_prefix
+from ..utils.parsers import (add_http_prefix, get_geom_from_gpx,
+ get_geom_from_kml, maybe_fix_encoding_to_utf8, GeomValueError)
class UtilsTest(TestCase):
@@ -100,3 +105,144 @@ def test_add_http_prefix_without_prefix(self):
def test_add_http_prefix_with_prefix(self):
self.assertEqual('http://test.com', add_http_prefix('http://test.com'))
+
+
+class GpxToGeomTests(SimpleTestCase):
+
+ @staticmethod
+ def _get_gpx_from(filename):
+ with open(filename, 'r') as f:
+ gpx = f.read()
+ return bytes(gpx, 'utf-8')
+
+ def test_gpx_with_waypoint_can_be_converted(self):
+ gpx = self._get_gpx_from('geotrek/trekking/tests/data/apidae_trek_parser/apidae_test_trek.gpx')
+
+ geom = get_geom_from_gpx(gpx)
+
+ self.assertEqual(geom.srid, 2154)
+ self.assertEqual(geom.geom_type, 'LineString')
+ self.assertEqual(len(geom.coords), 13)
+ first_point = geom.coords[0]
+ self.assertAlmostEqual(first_point[0], 977776.9, delta=0.1)
+ self.assertAlmostEqual(first_point[1], 6547354.8, delta=0.1)
+
+ def test_gpx_with_route_points_can_be_converted(self):
+ gpx = self._get_gpx_from('geotrek/trekking/tests/data/apidae_trek_parser/trace_with_route_points.gpx')
+
+ geom = get_geom_from_gpx(gpx)
+
+ self.assertEqual(geom.srid, 2154)
+ self.assertEqual(geom.geom_type, 'LineString')
+ self.assertEqual(len(geom.coords), 13)
+ first_point = geom.coords[0]
+ self.assertAlmostEqual(first_point[0], 977776.9, delta=0.1)
+ self.assertAlmostEqual(first_point[1], 6547354.8, delta=0.1)
+
+ def test_it_raises_an_error_on_not_continuous_segments(self):
+ gpx = self._get_gpx_from('geotrek/trekking/tests/data/apidae_trek_parser/trace_with_not_continuous_segments.gpx')
+
+ with self.assertRaises(GeomValueError):
+ get_geom_from_gpx(gpx)
+
+ def test_it_handles_segment_with_single_point(self):
+ gpx = self._get_gpx_from(
+ 'geotrek/trekking/tests/data/apidae_trek_parser/trace_with_single_point_segment.gpx'
+ )
+ geom = get_geom_from_gpx(gpx)
+
+ self.assertEqual(geom.srid, 2154)
+ self.assertEqual(geom.geom_type, 'LineString')
+ self.assertEqual(len(geom.coords), 13)
+
+ def test_it_raises_an_error_when_no_linestring(self):
+ gpx = self._get_gpx_from('geotrek/trekking/tests/data/apidae_trek_parser/trace_with_no_feature.gpx')
+
+ with self.assertRaises(GeomValueError):
+ get_geom_from_gpx(gpx)
+
+ def test_it_handles_multiple_continuous_features(self):
+ gpx = self._get_gpx_from('geotrek/trekking/tests/data/apidae_trek_parser/trace_with_multiple_continuous_features.gpx')
+ geom = get_geom_from_gpx(gpx)
+
+ self.assertEqual(geom.srid, 2154)
+ self.assertEqual(geom.geom_type, 'LineString')
+ self.assertEqual(len(geom.coords), 12)
+ first_point = geom.coords[0]
+ self.assertAlmostEqual(first_point[0], 977776.9, delta=0.1)
+ self.assertAlmostEqual(first_point[1], 6547354.8, delta=0.1)
+
+ def test_it_handles_multiple_continuous_features_with_one_empty(self):
+ gpx = self._get_gpx_from('geotrek/trekking/tests/data/apidae_trek_parser/trace_with_multiple_continuous_features_and_one_empty.gpx')
+ geom = get_geom_from_gpx(gpx)
+
+ self.assertEqual(geom.srid, 2154)
+ self.assertEqual(geom.geom_type, 'LineString')
+ self.assertEqual(len(geom.coords), 12)
+ first_point = geom.coords[0]
+ self.assertAlmostEqual(first_point[0], 977776.9, delta=0.1)
+ self.assertAlmostEqual(first_point[1], 6547354.8, delta=0.1)
+
+ def test_it_raises_error_on_multiple_not_continuous_features(self):
+ gpx = self._get_gpx_from('geotrek/trekking/tests/data/apidae_trek_parser/trace_with_multiple_not_continuous_features.gpx')
+ with self.assertRaises(GeomValueError):
+ get_geom_from_gpx(gpx)
+
+
+class KmlToGeomTests(SimpleTestCase):
+
+ @staticmethod
+ def _get_kml_from(filename):
+ with open(filename, 'r') as f:
+ kml = f.read()
+ return bytes(kml, 'utf-8')
+
+ def test_kml_can_be_converted(self):
+ kml = self._get_kml_from('geotrek/trekking/tests/data/apidae_trek_parser/trace.kml')
+
+ geom = get_geom_from_kml(kml)
+
+ self.assertEqual(geom.srid, 2154)
+ self.assertEqual(geom.geom_type, 'LineString')
+ self.assertEqual(len(geom.coords), 61)
+ first_point = geom.coords[0]
+ self.assertAlmostEqual(first_point[0], 973160.8, delta=0.1)
+ self.assertAlmostEqual(first_point[1], 6529320.1, delta=0.1)
+
+ def test_it_raises_exception_when_no_linear_data(self):
+ kml = self._get_kml_from('geotrek/trekking/tests/data/apidae_trek_parser/trace_with_no_line.kml')
+
+ with self.assertRaises(GeomValueError):
+ get_geom_from_kml(kml)
+
+
+class TestConvertEncodingFiles(TestCase):
+ data_dir = "geotrek/trekking/tests/data"
+
+ def setUp(self):
+ if not os.path.exists(settings.TMP_DIR):
+ os.mkdir(settings.TMP_DIR)
+
+ def test_fix_encoding_to_utf8(self):
+ file_name = f'{settings.TMP_DIR}/file_bad_encoding_tmp.kml'
+ copyfile(f'{self.data_dir}/file_bad_encoding.kml', file_name)
+
+ encoding = get_encoding_file(file_name)
+ self.assertNotEqual(encoding, "utf-8")
+
+ new_file_name = maybe_fix_encoding_to_utf8(file_name)
+
+ encoding = get_encoding_file(new_file_name)
+ self.assertEqual(encoding, "utf-8")
+
+ def test_not_fix_encoding_to_utf8(self):
+ file_name = f'{settings.TMP_DIR}/file_good_encoding_tmp.kml'
+ copyfile(f'{self.data_dir}/file_good_encoding.kml', file_name)
+
+ encoding = get_encoding_file(file_name)
+ self.assertEqual(encoding, "utf-8")
+
+ new_file_name = maybe_fix_encoding_to_utf8(file_name)
+
+ encoding = get_encoding_file(new_file_name)
+ self.assertEqual(encoding, "utf-8")
diff --git a/geotrek/common/tests/test_views.py b/geotrek/common/tests/test_views.py
index 7ebb8bfd6a..eb14352edc 100644
--- a/geotrek/common/tests/test_views.py
+++ b/geotrek/common/tests/test_views.py
@@ -30,6 +30,7 @@
from geotrek.core.models import Path
from geotrek.trekking.models import Trek
from geotrek.trekking.tests.factories import TrekFactory
+import geotrek.trekking.parsers # noqa
class DocumentPublicPortalTest(TestCase):
@@ -108,6 +109,7 @@ class ViewsTest(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = UserFactory.create()
+ cls.super_user = SuperUserFactory()
def setUp(self):
self.client.force_login(user=self.user)
@@ -118,14 +120,19 @@ def test_settings_json(self):
self.assertEqual(response.status_code, 200)
def test_admin_check_extents(self):
+ """ Admin can access to extents view"""
url = reverse('common:check_extents')
- response = self.client.get(url)
- self.assertEqual(response.status_code, 302)
- self.user.is_superuser = True
- self.user.save()
+ self.client.force_login(self.super_user)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
+ def test_simple_user_check_extents(self):
+ """ Simple user can't access to extents view"""
+ url = reverse('common:check_extents')
+ self.client.force_login(self.user)
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 302)
+
@override_settings(COLUMNS_LISTS={})
@mock.patch('geotrek.common.mixins.views.logger')
def test_custom_columns_mixin_error_log(self, mock_logger):
@@ -145,6 +152,9 @@ def setUpTestData(cls):
cls.user = UserFactory()
cls.super_user = SuperUserFactory()
+ def setUp(self):
+ self.client.force_login(user=self.user)
+
def test_import_form_access(self):
self.client.force_login(user=self.user)
url = reverse('common:import_dataset')
diff --git a/geotrek/common/utils/parsers.py b/geotrek/common/utils/parsers.py
index 319ff6aaa9..0e2859a17b 100644
--- a/geotrek/common/utils/parsers.py
+++ b/geotrek/common/utils/parsers.py
@@ -1,5 +1,130 @@
+import codecs
+import os
+from datetime import datetime
+from tempfile import NamedTemporaryFile
+
+from django.conf import settings
+from django.contrib.gis.gdal import DataSource
+from django.contrib.gis.geos import MultiLineString
+from django.utils.translation import gettext as _
+
+from geotrek.common.utils.file_infos import get_encoding_file
+
+
+class GeomValueError(Exception):
+ pass
+
+
def add_http_prefix(url):
if url.startswith('http'):
return url
else:
return 'http://' + url
+
+
+def maybe_fix_encoding_to_utf8(file_name):
+ encoding = get_encoding_file(file_name)
+
+ # If not utf-8, convert file to utf-8
+ if encoding != "utf-8":
+ tmp_file_path = os.path.join(settings.TMP_DIR, 'fileNameTmp_' + str(datetime.now().timestamp()))
+ BLOCKSIZE = 9_048_576
+ with codecs.open(file_name, "r", encoding) as sourceFile:
+ with codecs.open(tmp_file_path, "w", "utf-8") as targetFile:
+ while True:
+ contents = sourceFile.read(BLOCKSIZE)
+ if not contents:
+ break
+ targetFile.write(contents)
+ os.replace(tmp_file_path, file_name)
+ return file_name
+
+
+def get_geom_from_gpx(data):
+ def convert_to_geos(geom):
+ # FIXME: is it right to try to correct input geometries?
+ # FIXME: how to log that info/spread errors?
+ if geom.geom_type == 'MultiLineString' and any([ls for ls in geom if ls.num_points == 1]):
+ # Handles that framework conversion fails when there are LineStrings of length 1
+ geos_mls = MultiLineString([ls.geos for ls in geom if ls.num_points > 1])
+ geos_mls.srid = geom.srid
+ return geos_mls
+
+ return geom.geos
+
+ def get_layer(datasource, layer_name):
+ for layer in datasource:
+ if layer.name == layer_name:
+ return layer
+
+ def maybe_get_linestring_from_layer(layer):
+ if layer.num_feat == 0:
+ return None
+ geoms = []
+ for feat in layer:
+ if feat.geom.num_coords == 0:
+ continue
+ geos = convert_to_geos(feat.geom)
+ if geos.geom_type == 'MultiLineString':
+ geos = geos.merged # If possible we merge the MultiLineString into a LineString
+ if geos.geom_type == 'MultiLineString':
+ raise GeomValueError(
+ _("Feature geometry cannot be converted to a single continuous LineString feature"))
+ geoms.append(geos)
+
+ full_geom = MultiLineString(geoms)
+ full_geom.srid = geoms[0].srid
+ full_geom = full_geom.merged # If possible we merge the MultiLineString into a LineString
+ if full_geom.geom_type == 'MultiLineString':
+ raise GeomValueError(
+ _("Geometries from various features cannot be converted to a single continuous LineString feature"))
+
+ return full_geom
+
+ """Given GPX data as bytes it returns a geom."""
+ # FIXME: is there another way than the temporary file? It seems not. `DataSource` really expects a filename.
+ with NamedTemporaryFile(mode='w+b', dir=settings.TMP_DIR) as ntf:
+ ntf.write(data)
+ ntf.flush()
+
+ file_path = maybe_fix_encoding_to_utf8(ntf.name)
+ ds = DataSource(file_path)
+ for layer_name in ('tracks', 'routes'):
+ layer = get_layer(ds, layer_name)
+ geos = maybe_get_linestring_from_layer(layer)
+ if geos:
+ break
+ else:
+ raise GeomValueError("No LineString feature found in GPX layers tracks or routes")
+ geos.transform(settings.SRID)
+ return geos
+
+
+def get_geom_from_kml(data):
+ """Given KML data as bytes it returns a geom."""
+
+ def get_geos_linestring(datasource):
+ layer = datasource[0]
+ geom = get_first_geom_with_type_in(types=['MultiLineString', 'LineString'], geoms=layer.get_geoms())
+ geom.coord_dim = 2
+ geos = geom.geos
+ if geos.geom_type == 'MultiLineString':
+ geos = geos.merged
+ return geos
+
+ def get_first_geom_with_type_in(types, geoms):
+ for g in geoms:
+ for t in types:
+ if g.geom_type.name.startswith(t):
+ return g
+ raise GeomValueError('The attached KML geometry does not have any LineString or MultiLineString data')
+
+ with NamedTemporaryFile(mode='w+b', dir=settings.TMP_DIR) as ntf:
+ ntf.write(data)
+ ntf.flush()
+
+ file_path = maybe_fix_encoding_to_utf8(ntf.name)
+ ds = DataSource(file_path)
+ geos = get_geos_linestring(ds)
+ geos.transform(settings.SRID)
+ return geos
diff --git a/geotrek/common/views.py b/geotrek/common/views.py
index f6b4852bce..76824e0b9b 100644
--- a/geotrek/common/views.py
+++ b/geotrek/common/views.py
@@ -1,26 +1,27 @@
import ast
import json
+import logging
import mimetypes
import os
import re
from datetime import timedelta
from zipfile import ZipFile, is_zipfile
-import logging
import redis
from django.apps import apps
from django.conf import settings
from django.contrib import messages
from django.contrib.admin.models import CHANGE, LogEntry
-from django.contrib.auth.decorators import (login_required,
- permission_required,
- user_passes_test)
+from django.contrib.auth.decorators import (
+ login_required,
+ permission_required,
+ user_passes_test,
+)
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.gis.db.models import Extent, GeometryField
from django.core.exceptions import PermissionDenied
from django.db.models.functions import Cast
-from django.http import (Http404, HttpResponse, HttpResponseRedirect,
- JsonResponse)
+from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils import timezone
@@ -28,40 +29,51 @@
from django.utils.encoding import force_str
from django.utils.translation import gettext as _
from django.views import static
-from django.views.defaults import page_not_found
from django.views.decorators.http import require_http_methods, require_POST
+from django.views.defaults import page_not_found
from django.views.generic import TemplateView, UpdateView, View
from django_celery_results.models import TaskResult
from django_large_image.rest import LargeImageFileDetailMixin
-from geotrek.common.filters import HDViewPointFilterSet
+from geotrek import __version__
+from geotrek.altimetry.models import Dem
+from geotrek.celery import app as celery_app
+from geotrek.core.models import Path
+from geotrek.feedback.parsers import SuricateParser
from large_image import config
from mapentity import views as mapentity_views
from mapentity.helpers import api_bbox
from mapentity.registry import app_settings, registry
-from mapentity.views import MapEntityList
+from mapentity.views import MapEntityList, MapEntityFilter
from paperclip import settings as settings_paperclip
from paperclip.views import _handle_attachment_form
-from rest_framework import mixins
-from rest_framework import viewsets
-
-from geotrek import __version__
-from geotrek.celery import app as celery_app
-from geotrek.common.viewsets import GeotrekMapentityViewSet
-from geotrek.feedback.parsers import SuricateParser
-
-from ..altimetry.models import Dem
-from ..core.models import Path
-from .forms import (AttachmentAccessibilityForm, HDViewPointAnnotationForm,
- HDViewPointForm, ImportDatasetForm,
- ImportDatasetFormWithFile, ImportSuricateForm)
-from .mixins.views import BookletMixin, CompletenessMixin, DocumentPortalMixin, DocumentPublicMixin
+from rest_framework import mixins, viewsets
+
+from .filters import HDViewPointFilterSet
+from .forms import (
+ AttachmentAccessibilityForm,
+ HDViewPointAnnotationForm,
+ HDViewPointForm,
+ ImportDatasetForm,
+ ImportDatasetFormWithFile,
+ ImportSuricateForm,
+)
+from .mixins.views import (
+ BookletMixin,
+ CompletenessMixin,
+ DocumentPortalMixin,
+ DocumentPublicMixin,
+)
from .models import AccessibilityAttachment, HDViewPoint
from .permissions import PublicOrReadPermMixin, RelatedPublishedPermission
-from .serializers import HDViewPointGeoJSONSerializer, HDViewPointSerializer, HDViewPointAPISerializer
+from .serializers import (
+ HDViewPointAPISerializer,
+ HDViewPointGeoJSONSerializer,
+ HDViewPointSerializer,
+)
from .tasks import import_datas, import_datas_from_web
from .utils import leaflet_bounds
-from .utils.import_celery import (create_tmp_destination,
- discover_available_parsers)
+from .utils.import_celery import create_tmp_destination, discover_available_parsers
+from .viewsets import GeotrekMapentityViewSet
logger = logging.getLogger(__name__)
@@ -280,14 +292,19 @@ def import_update_json(request):
class HDViewPointList(MapEntityList):
queryset = HDViewPoint.objects.all()
- filterform = HDViewPointFilterSet
columns = ['id', 'title']
+class HDViewPointFilter(MapEntityFilter):
+ model = HDViewPoint
+ filterset_class = HDViewPointFilterSet
+
+
class HDViewPointViewSet(GeotrekMapentityViewSet):
model = HDViewPoint
serializer_class = HDViewPointSerializer
geojson_serializer_class = HDViewPointGeoJSONSerializer
+ filterset_class = HDViewPointFilterSet
mapentity_list_class = HDViewPointList
def get_queryset(self):
@@ -470,7 +487,7 @@ def delete_attachment_accessibility(request, attachment_pk):
else:
error_msg = _('You are not allowed to delete this attachment.')
messages.error(request, error_msg)
- return HttpResponseRedirect(f"{obj.get_detail_url()}?tab=attachments-accessibility")
+ return HttpResponseRedirect(f"{obj.get_detail_url()}?tab=attachments")
home = last_list
diff --git a/geotrek/core/templates/core/path_detail_attributes.html b/geotrek/core/templates/core/path_detail_attributes.html
index 6736e81e65..eecf9b27a6 100644
--- a/geotrek/core/templates/core/path_detail_attributes.html
+++ b/geotrek/core/templates/core/path_detail_attributes.html
@@ -3,7 +3,6 @@
{% block attributes %}
-
- {% include "outdoor/recursive_courses_tree.html" with sites_at_level=course.all_hierarchy_roots original_course=object %}
+ {% include "outdoor/recursive_courses_tree.html" with sites_at_level=course.all_hierarchy_roots original_course=object %}
-
{% site_as_list site.get_root as root_as_list %}
{% include "outdoor/recursive_sites_tree.html" with sites_at_level=root_as_list original_site=object %}
-