From f08694e44f7e6c934f7c198504480acc762fe59e Mon Sep 17 00:00:00 2001 From: "arnaud.morvan@camptocamp.com" Date: Tue, 5 Jun 2018 14:35:06 +0200 Subject: [PATCH 01/92] Upgrade c2cgeoform to remove All from grid page sizes --- docker/build/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/build/requirements.txt b/docker/build/requirements.txt index 3c67611e2c..f19af29d93 100644 --- a/docker/build/requirements.txt +++ b/docker/build/requirements.txt @@ -5,7 +5,7 @@ boto3==1.7.4 # Tile generation bottle==0.12.13 # geoportal c2c.cssmin==0.7.dev6 # CGXP build c2c.template==2.1.0.dev1 # geoportal -c2cgeoform==2.0.dev20180527 # commons +c2cgeoform==2.0.dev20180605 # commons codacy-coverage==1.3.11 # Codacy send report codespell==1.12.0 # Lint colander==1.4 # commons, admin From ae293e80e49a2e3aaf680b7e0d63964cd0e39ec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Wed, 6 Jun 2018 10:28:30 +0200 Subject: [PATCH 02/92] Upgrade for the new version of python:2 --- docker/build/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/build/Dockerfile b/docker/build/Dockerfile index 2c6c2dc423..5c2de7abc4 100644 --- a/docker/build/Dockerfile +++ b/docker/build/Dockerfile @@ -3,7 +3,7 @@ LABEL maintainer Camptocamp "info@camptocamp.com" RUN \ apt-get update && \ - apt-get install --assume-yes --no-install-recommends vim tree make git curl ca-certificates apt-transport-https libpq5 libgeos-c1 gettext libpq-dev libgeos-dev libjpeg-dev gcc default-jre-headless && \ + apt-get install --assume-yes --no-install-recommends vim tree make git curl ca-certificates apt-transport-https libpq5 libgeos-c1v5 gettext libpq-dev libgeos-dev libjpeg-dev gcc default-jre-headless && \ curl --silent https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - && \ echo 'deb https://deb.nodesource.com/node_6.x jessie main' > /etc/apt/sources.list.d/nodesource.list && \ apt-get update && \ From 2df3e8e1be48fe2ade303618f2b964f37ef96f11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Wed, 6 Jun 2018 13:42:21 +0200 Subject: [PATCH 03/92] Fix tow variable with the same name --- c2cgeoportal/lib/lingua_extractor.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/c2cgeoportal/lib/lingua_extractor.py b/c2cgeoportal/lib/lingua_extractor.py index eac68e6468..7baf1b5747 100644 --- a/c2cgeoportal/lib/lingua_extractor.py +++ b/c2cgeoportal/lib/lingua_extractor.py @@ -235,7 +235,7 @@ def _collect_app_config(cls, config, filename): value.encode("ascii", errors="replace") if isinstance(value, unicode) else value) enums.append(Message(None, msgid, None, [], u"", u"", (filename, location))) - metadata = [] + metadata_list = [] defs = config["vars"]["admin_interface"]["available_metadata"] names = [e["name"] for e in defs if e.get("translate", False)] @@ -251,9 +251,11 @@ def _collect_app_config(cls, config, filename): .filter(c2cgeoportal.models.Metadata.name.in_(names)) for metadata in query.all(): location = "metadata/{}/{}".format(metadata.name, metadata.id) - metadata.append(Message(None, metadata.value, None, [], u"", u"", (filename, location))) + metadata_list.append( + Message(None, metadata.value, None, [], u"", u"", (filename, location)) + ) - return raster + enums + metadata + return raster + enums + metadata_list @classmethod def _enumerate_attributes_values(cls, dbsessions, layers, layerinfos, fieldname): From cc254d01d850274075ea65e1824ff5ae7dd622c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Thu, 7 Jun 2018 11:48:39 +0200 Subject: [PATCH 04/92] Fix travis CI on Docker project --- .../scaffolds/create/+dot+travis.yml_tmpl | 2 +- .../scaffolds/nondockercreate/+dot+travis.yml_tmpl | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/+dot+travis.yml_tmpl diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+travis.yml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+travis.yml_tmpl index 29f1c0cdb3..0154f50588 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+travis.yml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+travis.yml_tmpl @@ -8,4 +8,4 @@ addons: - python3-netifaces script: - - ./docker-run make --makefile={{package}}.mk checks + - ./docker-run make checks diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/+dot+travis.yml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/+dot+travis.yml_tmpl new file mode 100644 index 0000000000..29f1c0cdb3 --- /dev/null +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/+dot+travis.yml_tmpl @@ -0,0 +1,11 @@ +--- + +sudo: false + +addons: + apt: + packages: + - python3-netifaces + +script: + - ./docker-run make --makefile={{package}}.mk checks From 923482ccaaef4f18953b713ead30d46ccdfd3edb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Thu, 7 Jun 2018 15:27:21 +0200 Subject: [PATCH 05/92] Update the upgrade documentation --- doc/integrator/upgrade_application.rst.mako | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/integrator/upgrade_application.rst.mako b/doc/integrator/upgrade_application.rst.mako index ce8daa574d..9d4c20801c 100644 --- a/doc/integrator/upgrade_application.rst.mako +++ b/doc/integrator/upgrade_application.rst.mako @@ -87,6 +87,12 @@ Then follow the instructions. From a version 2.3 and next ~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Build the project file: + +.. prompt:: bash + + ./docker-run make project.yaml.mako + Change the version in the file ``.config`` to the wanted version. For non Docker project: From 350437c2aa07720c2fdd022b0f998fa76207e1cf Mon Sep 17 00:00:00 2001 From: Laurent Lienher Date: Thu, 7 Jun 2018 15:54:03 +0200 Subject: [PATCH 06/92] Add printNativeAngle metadata --- c2cgeoportal/scaffolds/update/CONST_vars.yaml_tmpl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/c2cgeoportal/scaffolds/update/CONST_vars.yaml_tmpl b/c2cgeoportal/scaffolds/update/CONST_vars.yaml_tmpl index 0defccd7d3..247b37e0cb 100644 --- a/c2cgeoportal/scaffolds/update/CONST_vars.yaml_tmpl +++ b/c2cgeoportal/scaffolds/update/CONST_vars.yaml_tmpl @@ -150,6 +150,8 @@ vars: # Layers group - name: isExpanded type: boolean + - name: printNativeAngle + type: boolean # Layer - name: copy_to # V1 - name: copyable From b0194869b9e524dea0af5fc8833d65ffdcaf451a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Thu, 7 Jun 2018 11:33:47 +0200 Subject: [PATCH 07/92] Use faster pgp key server --- .../c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl index f1919bca1a..4a7e03845e 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl @@ -761,7 +761,7 @@ $(WMTSCAPABILITIES_FILE): tilegeneration/config.yaml secrets.tar.bz2.gpg: tar -jcf secrets.tar.bz2 $^ - gpg --keyserver pgp.mit.edu --keyserver-options timeout=20 --recv-keys $(GPG_KEYS) + gpg --keyserver pool.sks-keyservers.net --keyserver-options timeout=20 --recv-keys $(GPG_KEYS) rm -f $@ gpg --always-trust --output $@ --encrypt $(addprefix --recipient ,$(GPG_KEYS)) secrets.tar.bz2 rm secrets.tar.bz2 From 8bdcea902a1f7f3e5ccca2afd9dbda255ac4a6e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Thu, 7 Jun 2018 11:43:14 +0200 Subject: [PATCH 08/92] Be able to get the local and S3 tile in Docker compose mode --- doc/integrator/https.rst | 3 +- doc/integrator/tilegeneration.rst | 83 ------------------- docker/build/requirements.txt | 2 +- .../scaffolds/create/+dot+dockerignore | 2 +- .../scaffolds/create/+dot+gitignore_tmpl | 3 +- .../scaffolds/create/Dockerfile | 4 +- .../scaffolds/create/docker-compose.yaml.mako | 14 +++- .../scaffolds/create/front/haproxy.cfg.tmpl | 4 +- ...l.mako_tmpl => config.yaml.tmpl.mako_tmpl} | 12 ++- .../nondockercreate/+dot+gitignore_tmpl | 1 + .../nondockercreate/nondocker-override.mk | 2 + .../nondockerupdate/+dot+upgrade.yaml_tmpl | 3 + .../scaffolds/update/+dot+upgrade.yaml_tmpl | 3 + .../scaffolds/update/CONST_Makefile_tmpl | 13 +-- .../scaffolds/update/CONST_vars.yaml_tmpl | 9 +- 15 files changed, 54 insertions(+), 104 deletions(-) rename geoportal/c2cgeoportal_geoportal/scaffolds/create/tilegeneration/{config.yaml.mako_tmpl => config.yaml.tmpl.mako_tmpl} (94%) diff --git a/doc/integrator/https.rst b/doc/integrator/https.rst index 1c28bf2a55..fe323103da 100644 --- a/doc/integrator/https.rst +++ b/doc/integrator/https.rst @@ -49,8 +49,7 @@ for all url except for some parameters that should always use http (actually all request on localhost): *url* parameter in tilegeneration configuration. If you apply ssl encryption on your application, you should take care of the -tiles url to use https scheme to avoid secure and insecure contents: change -*tiles_url* in the vars_xxx.yaml file. +tiles url to use https scheme to avoid secure and insecure contents. Finally, you should redirect all http request to https scheme. On Camptocamp's server, this should be asked to our sysadmin team. diff --git a/doc/integrator/tilegeneration.rst b/doc/integrator/tilegeneration.rst index 009ff3a037..991a409c89 100644 --- a/doc/integrator/tilegeneration.rst +++ b/doc/integrator/tilegeneration.rst @@ -190,86 +190,3 @@ using the command: export AWS_SECRET_ACCESS_KEY=YYYY If you forget it you will get an explicit message. - -Integration in c2cgeoportal -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In the ``viewer.js``, ``api/viewer.js`` and ``edit.js``: - - * Be sure that ``OpenLayers.IMAGE_RELOAD_ATTEMPTS`` is not defined. - * In ``WMTS_OPTION`` url should be ${tiles_url}. - -In the ``vars_.yaml`` define ``tiles_url`` to something like, for S3 usage: - -.. code:: yaml - - tiles_url: - - http://a.tiles.{host}/ - - http://b.tiles.{host}/ - - http://c.tiles.{host}/ - - http://d.tiles.{host}/ - -The configuration of the ``tiles`` vhost will be done by the sysadmins. - -To get your tiles URL in the ``viewer.js`` do: - -.. code:: javascript - - <% - from json import dumps - %> - var WMTS_OPTIONS = { - url: ${dumps(request.registry.settings['tiles_url']) | n}, - ... - } - -And in the ``mobile/config.js`` do: - -.. code:: javascript - - var dummy = "<% from json import dumps %>"; - jsonFormat = new OpenLayers.Format.JSON(); - try { - App.tilesURL = jsonFormat.read('${dumps(request.registry.settings["tiles_url"]) | n}'); - } - catch (e) { - App.tilesURL = ""; - } - var WMTS_OPTIONS = { - url: App.tilesURL, - ... - } - -SwitchableWMTS --------------- - -Useful tool to switch from TileCloud to MapCache. - -See: https://github.com/camptocamp/cgxp/blob/master/openlayers.addins/SwitchableWMTS/lib/OpenLayers/Layer/SwitchableWMTS.js - -Internal service ----------------- - -If you use an internal service to access to the tiles you can use sub domaine -to access to them by using that in ``WMTS_OPTION``: - -.. code:: javascript - - url: [ - '${request.route_url('', subdomain='s1')}', - '${request.route_url('', subdomain='s2')}', - '${request.route_url('', subdomain='s3')}', - '${request.route_url('', subdomain='s4')}' - ] - -With ```` the name of the view that serve the tiles. -The sub domain should obviously be define in the DNS and in the Apache -vhost. If the application is served on deferent URL and you want to use -the sub domain on only one of them you can define in the ``vars_.yaml`` -the following: - -.. code:: yaml - - # The URL template used to generate the sub domain URL - # %(sub)s will be replaced by the sub domain value. - subdomain_url_template: http://%(sub)s.{host} diff --git a/docker/build/requirements.txt b/docker/build/requirements.txt index 3c67611e2c..434d3b1838 100644 --- a/docker/build/requirements.txt +++ b/docker/build/requirements.txt @@ -65,7 +65,7 @@ Sphinx==1.7.2 # doc sphinx-prompt==1.0.0 # doc SQLAlchemy==1.2.6 tilecloud==0.5.1 # Tile generation -tilecloud-chain==1.5.2 # Tile generation +tilecloud-chain==1.6.0.dev2 # Tile generation transaction==2.2.1 # commons, geoportal transifex-client==0.12.5 # Makefile, rq.filter: >=0.14 translationstring==1.3 # admin diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+dockerignore b/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+dockerignore index afb08d0b96..d356d5d93d 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+dockerignore +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+dockerignore @@ -4,5 +4,5 @@ !mapserver/ !qgisserver/ !mapcache/ -!tilegeneration/config.yaml +!tilegeneration/ !print/print-apps/ diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+gitignore_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+gitignore_tmpl index 571344ece4..84db93d206 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+gitignore_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+gitignore_tmpl @@ -10,7 +10,8 @@ __pycache__/ /testdb/12-alembic.sql /testdb/13-alembic-static.sql /mapcache/ -/tilegeneration/config.yaml +/tilegeneration/config.yaml.tmpl +/tilegeneration/config.yaml.mako /print/print-apps/{{package}}/config.yaml.mako /mapserver/mapserver.map.tmpl /mapserver/*.map.tmpl diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/Dockerfile b/geoportal/c2cgeoportal_geoportal/scaffolds/create/Dockerfile index f4bf2b9d23..dc84b0a890 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/Dockerfile +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/Dockerfile @@ -18,8 +18,8 @@ VOLUME /etc/mapserver COPY mapcache /mapcache VOLUME /mapcache -COPY tilegeneration /etc/tilecloudchain -VOLUME /etc/tilecloudchain +COPY tilegeneration /etc/tilegeneration +VOLUME /etc/tilegeneration COPY print/print-apps /usr/local/tomcat/webapps/ROOT/print-apps VOLUME /usr/local/tomcat/webapps/ROOT/print-apps diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako index 394e26cfa3..17874c73ad 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako @@ -45,10 +45,20 @@ ${service_defaults('memcached', 11211)}\ ${service_defaults('redis', 6379)}\ tilecloudchain: - image: camptocamp/tilecloud-chain:1.5.0 + image: camptocamp/tilecloud-chain:1.6 volumes_from: - config:ro -${service_defaults('mapserver', 80)}\ +${service_defaults('tilecloudchain', 80)}\ + + tilegeneration: + image: camptocamp/tilecloud-chain:1.6 + volumes_from: + - config:ro +${service_defaults('tilecloudchain')}\ + entrypoint: + - bash + - -c + - sleep infinity geoportal: image: ${docker_base}-geoportal:${docker_tag} diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/front/haproxy.cfg.tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/create/front/haproxy.cfg.tmpl index 3f3bad4b55..2601cfafed 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/front/haproxy.cfg.tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/front/haproxy.cfg.tmpl @@ -57,7 +57,7 @@ backend geoportal backend tilecloudchain - option httpchk GET ${VISIBLE_ENTRY_POINT}tiles HTTP/1.0\r\nUser-Agent:\ healthcheck + option httpchk GET ${VISIBLE_ENTRY_POINT}c2c_tiles/health_check HTTP/1.0\r\nUser-Agent:\ healthcheck http-check expect status 200 server linked tilecloudchain:80 resolvers dns #check @@ -69,7 +69,7 @@ frontend plain http-request set-var(req.path) path # If the path starts with /tiles/, use the tilecloudchain backend - acl is_tiles var(req.path) -m beg ${VISIBLE_ENTRY_POINT}tiles/ + acl is_tiles var(req.path) -m beg ${VISIBLE_ENTRY_POINT}tiles/ ${VISIBLE_ENTRY_POINT}static_tiles/ ${VISIBLE_ENTRY_POINT}c2c_tiles/ use_backend tilecloudchain if is_tiles # Redirect all to geoportal by default diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/tilegeneration/config.yaml.mako_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/create/tilegeneration/config.yaml.tmpl.mako_tmpl similarity index 94% rename from geoportal/c2cgeoportal_geoportal/scaffolds/create/tilegeneration/config.yaml.mako_tmpl rename to geoportal/c2cgeoportal_geoportal/scaffolds/create/tilegeneration/config.yaml.tmpl.mako_tmpl index e910691a9c..f4b51af61e 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/tilegeneration/config.yaml.mako_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/tilegeneration/config.yaml.tmpl.mako_tmpl @@ -14,7 +14,7 @@ caches: folder: /var/sig/tiles wmtscapabilities_file: ${wmtscapabilities_path} # for GetCapabilities - http_url: ${web_protocol}://${host}${entry_point}tiles/ + http_url: ${web_protocol}://${host}${entry_point} s3: type: s3 bucket: tiles @@ -33,7 +33,7 @@ defaults: # The minimum resolution to seed, useful to use with mapcache, optional. # min_resolution_seed: 1 # the URL of the WMS server to used - url: http://mapserver/ + url: ${mapserver_url} # Set the headers to get the right virtual host, and don't get any cached result headers: Host: '${host}' @@ -101,8 +101,14 @@ generation: # maximum allowed consecutive errors, after it exit [default to 10] maxconsecutive_errors: 10 +server: + mapcache_base: '${mapcache_url}' + wmts_path: tiles + static_path: static_tiles + expires: 8 # 8 hours + mapcache: - config_file: mapcache/mapcache.xml + config_file: mapcache/mapcache.xml.tmpl memcache_host: localhost memcache_port: 11211 diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/+dot+gitignore_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/+dot+gitignore_tmpl index baa7b4d9e9..33cf2ee95d 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/+dot+gitignore_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/+dot+gitignore_tmpl @@ -31,6 +31,7 @@ __pycache__/ /mapserver/*.map.tmpl.mako /mapserver/tinyows.xml /tilegeneration/config.yaml +/tilegeneration/config.yaml.tmpl.mako /geoportal/jsbuild/app.cfg /geoportal/Dockerfile /geoportal/config.yaml diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-override.mk b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-override.mk index 67c70e6b66..62b059ba4a 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-override.mk +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-override.mk @@ -14,6 +14,8 @@ export MAPSERVER_URL PRINT_URL ?= http://print:8080/print/ export PRINT_URL PRINT_CONFIG_FILE ?= print/print-apps/$(PACKAGE)/config.yaml +MAPCACHE_FILE ?= mapcache/mapcache.xml +TILEGENERATION_CONFIG_FILE = tilegeneration/config.yaml VISIBLE_WEB_PROTOCOL ?= https VISIBLE_WEB_PORT ?= 443 diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockerupdate/+dot+upgrade.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockerupdate/+dot+upgrade.yaml_tmpl index 45d4e8628c..2405ebbe2e 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockerupdate/+dot+upgrade.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockerupdate/+dot+upgrade.yaml_tmpl @@ -161,3 +161,6 @@ files_to_move: - from: print/print-apps/{{package}}/config.yaml.tmpl to: print/print-apps/{{package}}/config.yaml.mako version: 2.3 + - from: tilegeneration/config.yaml.tmpl.mako + to: tilegeneration/config.yaml.mako + version: 2.3 diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl index 98b443a38b..dee0944e24 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl @@ -198,3 +198,6 @@ files_to_move: - from: print/print-apps/{{package}}/config.yaml.mako to: print/print-apps/{{package}}/config.yaml.tmpl version: 2.3 + - from: tilegeneration/config.yaml.mako + to: tilegeneration/config.yaml.tmpl.mako + version: 2.3 diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl index 4a7e03845e..044270e7c5 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl @@ -92,8 +92,9 @@ WMTSCAPABILITIES_PATH ?= 1.0.0/WMTSCapabilities-$(ENVIRONMENT).xml export WMTSCAPABILITIES_PATH ifeq ($(TILECLOUD_CHAIN), TRUE) WMTSCAPABILITIES_FILE ?= /var/sig/tiles/$(WMTSCAPABILITIES_PATH) -MAPCACHE_FILE ?= mapcache/mapcache.xml -DEFAULT_BUILD_RULES += tilegeneration/config.yaml +MAPCACHE_FILE ?= mapcache/mapcache.xml.tmpl +TILEGENERATION_CONFIG_FILE ?= tilegeneration/config.yaml.tmpl +DEFAULT_BUILD_RULES += $(TILEGENERATION_CONFIG_FILE) ifeq ($(TILECLOUD_CHAIN_LOCAL), TRUE) DEFAULT_BUILD_RULES += $(WMTSCAPABILITIES_FILE) endif @@ -747,15 +748,15 @@ upgrade%: # Tilecloud chain -$(MAPCACHE_FILE): tilegeneration/config.yaml +$(MAPCACHE_FILE): $(TILEGENERATION_CONFIG_FILE) $(PRERULE_CMD) mkdir --parent $(dir $@) - generate_controller --mapcache + generate_controller --config=$< --mapcache -$(WMTSCAPABILITIES_FILE): tilegeneration/config.yaml +$(WMTSCAPABILITIES_FILE): $(TILEGENERATION_CONFIG_FILE) $(PRERULE_CMD) mkdir --parent $(dir $@) - generate_controller --capabilities + generate_controller --config=$< --capabilities # Secrets diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl index f5d61a4620..7c64b648f0 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl @@ -36,6 +36,8 @@ vars: PGPASSWORD: www-data PGDATABASE: geomapfish GEOPORTAL_INTERNAL_URL: 'http://geoportal/' + MAPCACHE_URL: http://mapcache/ + MAPSERVER_URL: http://mapserver/ db: environment: POSTGRES_DB: geomapfish @@ -63,6 +65,7 @@ vars: environment: VISIBLE_ENTRY_POINT: '{docker_entry_point}' TILEGENERATION_CONFIGFILE: /etc/tilegeneration/config.yaml + C2C_BASE_PATH: /c2c_tiles front: port: 80 @@ -360,9 +363,11 @@ vars: # For print proxy print_url: '{PRINT_URL}' + mapserver_url: '{MAPSERVER_URL}' + mapcache_url: '{MAPCACHE_URL}' pdfreport: - print_url: '{PRINT_URL}' + print_url: '{print_url}' # For base layers tiles_url: @@ -550,6 +555,8 @@ runtime_environment: default: http://mapserver/ - name: PRINT_URL default: http://print:8080/print/ + - name: MAPCACHE_URL + default: http://mapcache/ - name: REDIS_HOST default: undefined # no error if absent - name: REDIS_PORT From 63ff0bfd06d63c4c754dda950fdd836757b46947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Thu, 7 Jun 2018 21:20:33 +0200 Subject: [PATCH 09/92] Print the diff files --- travis/test-upgrade-convert.sh | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/travis/test-upgrade-convert.sh b/travis/test-upgrade-convert.sh index 8c4df43468..3ac949db5e 100755 --- a/travis/test-upgrade-convert.sh +++ b/travis/test-upgrade-convert.sh @@ -56,6 +56,14 @@ function createv220 { cd - } +function printdiff { + for f in $(ls -1 *.diff) + do + echo "--- $f ---" + cat "$f" + done +} + if [ "$1" = "init" ] then rm --recursive --force ${WORKSPACE}/nondockerref ${WORKSPACE}/dockerref \ @@ -81,6 +89,7 @@ then ./docker-run --env=NODE_ENV make upgrade if [ ! -e .UPGRADE_SUCCESS ] then + printdiff echo "Fail to upgrade" exit 1 fi @@ -98,6 +107,7 @@ then ./docker-run --env=NODE_ENV make --makefile=testgeomapfish.mk upgrade if [ ! -e .UPGRADE_SUCCESS ] then + printdiff echo "Fail to upgrade" exit 1 fi @@ -121,6 +131,7 @@ then ./docker-run make --makefile=temp.mk upgrade if [ ! -e .UPGRADE10 ] then + printdiff echo "Fail to upgrade" exit 1 fi @@ -131,6 +142,7 @@ then ./docker-run --env=NODE_ENV make upgrade11 if [ ! -e .UPGRADE_SUCCESS ] then + printdiff echo "Fail to upgrade" exit 1 fi @@ -157,6 +169,7 @@ then ./docker-run --env=NODE_ENV make upgrade if [ ! -e .UPGRADE10 ] then + printdiff echo "Fail to upgrade" exit 1 fi @@ -168,6 +181,7 @@ then ./docker-run --env=NODE_ENV make --makefile=testgeomapfish.mk upgrade11 if [ ! -e .UPGRADE_SUCCESS ] then + printdiff echo "Fail to upgrade" exit 1 fi @@ -200,6 +214,7 @@ function v220 { fi if [ ! -e .UPGRADE8 ] then + printdiff echo "Fail to upgrade" exit 1 fi @@ -209,11 +224,7 @@ function v220 { ./docker-run make $MAKE_ARGS upgrade9 if [ ! -e .UPGRADE_SUCCESS ] then - for f in $(ls -1 *.diff) - do - echo "--- $f ---" - cat "$f" - done + printdiff echo "Fail to upgrade" exit 1 fi From dcdf0b3aa672c691d8bea0492d9fc53781f563b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Fri, 8 Jun 2018 11:22:21 +0200 Subject: [PATCH 10/92] Add the missing update path in the doc, and schema fix --- c2cgeoportal/scaffolds/update/CONST_config-schema.yaml | 3 +++ c2cgeoportal/scaffolds/update/CONST_vars.yaml_tmpl | 3 +++ doc/integrator/caching.rst | 6 ++++++ 3 files changed, 12 insertions(+) diff --git a/c2cgeoportal/scaffolds/update/CONST_config-schema.yaml b/c2cgeoportal/scaffolds/update/CONST_config-schema.yaml index 4271b22037..dd5a56658b 100644 --- a/c2cgeoportal/scaffolds/update/CONST_config-schema.yaml +++ b/c2cgeoportal/scaffolds/update/CONST_config-schema.yaml @@ -100,6 +100,9 @@ mapping: access_control_max_age: required: True type: int + cache_control_max_age: + required: True + type: int index: *header config: *header api: *header diff --git a/c2cgeoportal/scaffolds/update/CONST_vars.yaml_tmpl b/c2cgeoportal/scaffolds/update/CONST_vars.yaml_tmpl index 0defccd7d3..26927e655d 100644 --- a/c2cgeoportal/scaffolds/update/CONST_vars.yaml_tmpl +++ b/c2cgeoportal/scaffolds/update/CONST_vars.yaml_tmpl @@ -338,6 +338,7 @@ vars: # Control the HTTP headers headers: index: &header + cache_control_max_age: 600 # 10 minutes access_control_max_age: 600 # 10 minutes access_control_allow_origin: - "*" @@ -349,6 +350,7 @@ vars: exportgpxkml: *header error: *header themes: &auth_header + cache_control_max_age: 600 # 10 minutes access_control_max_age: 600 # 10 minutes access_control_allow_origin: - "{web_protocol}://{host}" @@ -361,6 +363,7 @@ vars: layers: *auth_header shortener: *auth_header login: + cache_control_max_age: 600 # 10 minutes access_control_max_age: 600 # 10 minutes access_control_allow_origin: - "{web_protocol}://{host}" diff --git a/doc/integrator/caching.rst b/doc/integrator/caching.rst index f2409ebf9a..db25e4213a 100644 --- a/doc/integrator/caching.rst +++ b/doc/integrator/caching.rst @@ -22,6 +22,12 @@ To change this value for a specific service add the following stricture in the : cache_control_max_age: + ... + + update_paths: + - headers. + + Where ```` can be: ``entry``, ``fulltextsearch``, ``mapserver``, ``print`` or ``layers`` (editing). From 0db4b0063c7c6f91351ecb2516bfcf3218fd10bb Mon Sep 17 00:00:00 2001 From: Patrick Valsecchi Date: Fri, 8 Jun 2018 13:24:52 +0200 Subject: [PATCH 11/92] Fix check collector By default it was trying to access: http://localhost///c2c/health_check?max_level=1 --- geoportal/c2cgeoportal_geoportal/lib/check_collector.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/geoportal/c2cgeoportal_geoportal/lib/check_collector.py b/geoportal/c2cgeoportal_geoportal/lib/check_collector.py index 42b0d115ad..2566c5b457 100644 --- a/geoportal/c2cgeoportal_geoportal/lib/check_collector.py +++ b/geoportal/c2cgeoportal_geoportal/lib/check_collector.py @@ -50,7 +50,9 @@ def check(request): display = host["display"] if "host" not in params or display == params["host"]: url_headers = build_url( - "check_collector", "%s/%s/health_check" % (host["url"], c2c_base), request + "check_collector", + "%s/%s/health_check" % (host["url"].rstrip("/"), c2c_base.strip("/")), + request ) r = requests.get(params={"max_level": str(host.get("max_level", max_level))}, **url_headers) r.raise_for_status() From f97f2c9d20a54a7f8e4c954ff87c34931a29f797 Mon Sep 17 00:00:00 2001 From: Patrick Valsecchi Date: Fri, 8 Jun 2018 13:27:31 +0200 Subject: [PATCH 12/92] Revert "Fix check collector" This reverts commit 0db4b0063c7c6f91351ecb2516bfcf3218fd10bb. --- geoportal/c2cgeoportal_geoportal/lib/check_collector.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/geoportal/c2cgeoportal_geoportal/lib/check_collector.py b/geoportal/c2cgeoportal_geoportal/lib/check_collector.py index 2566c5b457..42b0d115ad 100644 --- a/geoportal/c2cgeoportal_geoportal/lib/check_collector.py +++ b/geoportal/c2cgeoportal_geoportal/lib/check_collector.py @@ -50,9 +50,7 @@ def check(request): display = host["display"] if "host" not in params or display == params["host"]: url_headers = build_url( - "check_collector", - "%s/%s/health_check" % (host["url"].rstrip("/"), c2c_base.strip("/")), - request + "check_collector", "%s/%s/health_check" % (host["url"], c2c_base), request ) r = requests.get(params={"max_level": str(host.get("max_level", max_level))}, **url_headers) r.raise_for_status() From 763c90a031699a52e3dc6aa134c2f84b3667f8fd Mon Sep 17 00:00:00 2001 From: Patrick Valsecchi Date: Fri, 8 Jun 2018 13:24:52 +0200 Subject: [PATCH 13/92] Fix check collector By default it was trying to access: http://localhost///c2c/health_check?max_level=1 --- geoportal/c2cgeoportal_geoportal/lib/check_collector.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/geoportal/c2cgeoportal_geoportal/lib/check_collector.py b/geoportal/c2cgeoportal_geoportal/lib/check_collector.py index 42b0d115ad..2566c5b457 100644 --- a/geoportal/c2cgeoportal_geoportal/lib/check_collector.py +++ b/geoportal/c2cgeoportal_geoportal/lib/check_collector.py @@ -50,7 +50,9 @@ def check(request): display = host["display"] if "host" not in params or display == params["host"]: url_headers = build_url( - "check_collector", "%s/%s/health_check" % (host["url"], c2c_base), request + "check_collector", + "%s/%s/health_check" % (host["url"].rstrip("/"), c2c_base.strip("/")), + request ) r = requests.get(params={"max_level": str(host.get("max_level", max_level))}, **url_headers) r.raise_for_status() From 2d8bff1b2713cb65ffafd0dea9cca364157d9996 Mon Sep 17 00:00:00 2001 From: "arnaud.morvan@camptocamp.com" Date: Fri, 8 Jun 2018 14:40:03 +0200 Subject: [PATCH 14/92] Set is_password_changed to False when log in with temp_password Relate https://jira.camptocamp.com/browse/GEO-712 --- admin/acceptance_tests/user_test.py | 8 ++++---- admin/c2cgeoportal_admin/views/users.py | 4 +++- commons/c2cgeoportal_commons/models/static.py | 2 +- geoportal/tests/functional/test_entry.py | 9 ++++++++- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/admin/acceptance_tests/user_test.py b/admin/acceptance_tests/user_test.py index 4ef7d0b2c3..2e0fae5800 100644 --- a/admin/acceptance_tests/user_test.py +++ b/admin/acceptance_tests/user_test.py @@ -199,10 +199,7 @@ def test_submit_new(self, pw_gen_mock, smtp_mock, dbsession, test_app): 'id': '', 'username': 'new_user', 'email': 'valid@email.net', - 'role_name': 'secretary_2', - 'is_password_changed': 'false', - '_password': 'da39a3ee5e6b4b0d3255bfef95601890afd80709', - 'temp_password': ''}, + 'role_name': 'secretary_2'}, status=302) user = dbsession.query(User). \ @@ -216,6 +213,9 @@ def test_submit_new(self, pw_gen_mock, smtp_mock, dbsession, test_app): assert user.username == 'new_user' assert user.email == 'valid@email.net' assert user.role_name == 'secretary_2' + assert user.password is not None and len(user.password) + assert user.temp_password is None + assert user.is_password_changed == False parts = list(email.message_from_string(sender_mock.sendmail.mock_calls[0][1][2]).walk()) assert EXPECTED_WELCOME_MAIL.format('new_user', 'new_user', 'basile') == \ diff --git a/admin/c2cgeoportal_admin/views/users.py b/admin/c2cgeoportal_admin/views/users.py index dec1ab3c32..b00c9e1cb1 100644 --- a/admin/c2cgeoportal_admin/views/users.py +++ b/admin/c2cgeoportal_admin/views/users.py @@ -56,8 +56,10 @@ def save(self): if isinstance(response, HTTPFound): password = pwgenerator.generate() + user = self._obj - user.set_temp_password(password) + user.password = password + user.is_password_changed = False user = self._request.dbsession.merge(user) self._request.dbsession.flush() diff --git a/commons/c2cgeoportal_commons/models/static.py b/commons/c2cgeoportal_commons/models/static.py index c1794525fc..c8a607bfb6 100644 --- a/commons/c2cgeoportal_commons/models/static.py +++ b/commons/c2cgeoportal_commons/models/static.py @@ -194,7 +194,7 @@ def validate_password(self, passwd: str) -> bool: self.temp_password == self.__encrypt_password(passwd): self._password = self.temp_password self.temp_password = None - self.is_password_changed = True + self.is_password_changed = False return True return False diff --git a/geoportal/tests/functional/test_entry.py b/geoportal/tests/functional/test_entry.py index dc820b42e0..f42038cb71 100644 --- a/geoportal/tests/functional/test_entry.py +++ b/geoportal/tests/functional/test_entry.py @@ -282,6 +282,8 @@ def test_logout(self): self.assertEqual(response.body.decode("utf-8"), "true") def test_reset_password(self): + from c2cgeoportal_commons.models import DBSession + from c2cgeoportal_commons.models.static import User from c2cgeoportal_geoportal.views.entry import Entry request = self._create_request_obj(POST={ @@ -299,12 +301,17 @@ def test_reset_password(self): self.assertEqual(json.loads(response.body.decode("utf-8")), { "success": True, "username": "__test_user1", - "is_password_changed": True, + "is_password_changed": False, "role_name": "__test_role1", "role_id": self.role1_id, "functionalities": {}, }) + user = DBSession.query(User).filter(User.username == '__test_user1').first() + self.assertIsNone(user.temp_password) + self.assertIsNotNone(user.password) + self.assertNotEqual(len(user.password), 0) + # # viewer view tests # From b953086920b96180c4b153a30b40fb366a9c6a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Tue, 12 Jun 2018 10:36:00 +0200 Subject: [PATCH 15/92] Some Docker litle improvements --- docker/build/bin/eval-templates | 1 + .../scaffolds/create/Dockerfile | 2 +- .../scaffolds/create/bin/eval-templates | 1 + .../scaffolds/create/docker-compose.yaml.mako | 13 ++++++++----- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/docker/build/bin/eval-templates b/docker/build/bin/eval-templates index 187bf2300c..9bd432d7a5 100755 --- a/docker/build/bin/eval-templates +++ b/docker/build/bin/eval-templates @@ -2,6 +2,7 @@ find /app/ -name '*.tmpl' -print | while read file do + echo "Evaluate: ${file}" envsubst < ${file} > ${file%.tmpl} done diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/Dockerfile b/geoportal/c2cgeoportal_geoportal/scaffolds/create/Dockerfile index dc84b0a890..6c26ccd0c8 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/Dockerfile +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/Dockerfile @@ -3,7 +3,7 @@ LABEL maintainer Camptocamp "info@camptocamp.com" RUN \ apt-get update && \ - apt-get install --assume-yes --no-install-recommends gettext-base && \ + apt-get install --assume-yes --no-install-recommends gettext-base python3 && \ apt-get clean && \ rm --recursive --force /var/lib/apt/lists/* diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/bin/eval-templates b/geoportal/c2cgeoportal_geoportal/scaffolds/create/bin/eval-templates index 728d58115f..81a38d96ee 100755 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/bin/eval-templates +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/bin/eval-templates @@ -5,6 +5,7 @@ export VISIBLE_ENTRY_POINT_RE_ESCAPED=`python -c "print(__import__('re').escape( find /etc /mapcache /usr/local/tomcat/webapps/ -name '*.tmpl' -print | while read file do + echo "Evaluate: ${file}" envsubst < ${file} > ${file%.tmpl} done diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako index 17874c73ad..525b4fd014 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako @@ -50,15 +50,15 @@ ${service_defaults('redis', 6379)}\ - config:ro ${service_defaults('tilecloudchain', 80)}\ - tilegeneration: + tilegeneration_slave: image: camptocamp/tilecloud-chain:1.6 volumes_from: - config:ro ${service_defaults('tilecloudchain')}\ entrypoint: - - bash - - -c - - sleep infinity + - generate_tiles + - --role=slave + - --daemon geoportal: image: ${docker_base}-geoportal:${docker_tag} @@ -72,7 +72,10 @@ ${service_defaults('geoportal', 80)}\ - config:ro volumes: - /dev/log:/dev/log:rw - command: ["haproxy", "-f", "/etc/haproxy"] + command: + - haproxy + - -f + - /etc/haproxy ${service_defaults('front', 80, not docker_global_front)} %if docker_global_front: networks: From ec3f4ce14bbc0c941170fa8cc87799379d3d3844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Tue, 12 Jun 2018 11:21:29 +0200 Subject: [PATCH 16/92] Phandomjs check to level 3, activate missing, cleanup --- .../scaffolds/update/CONST_vars.yaml_tmpl | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl index 7c64b648f0..2626903f44 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl @@ -425,28 +425,18 @@ vars: - name: desktop params: no_redirect: "true" - level: 1 - # - name: mobile - # params: - # no_redirect: "true" - # level: 3 - # - name: desktop - # params: - # no_redirect: "true" - # debug: "true" - # level: 3 - # - name: mobile - # params: - # no_redirect: "true" - # debug: "true" - # level: 3 + level: 3 + - name: mobile + params: + no_redirect: "true" + level: 3 routes: disable: [] routes: - # - name: apijs - # level: 3 - # - name: xapijs - # level: 3 + - name: apijs + level: 3 + - name: xapijs + level: 3 - name: printproxy_capabilities level: 3 - name: mapserverproxy From f446367bfb312d857514bb210a5b2ca02439d6a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Tue, 12 Jun 2018 11:23:40 +0200 Subject: [PATCH 17/92] Fix the list --- doc/developer/debugging.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/developer/debugging.rst b/doc/developer/debugging.rst index 6d55b83d79..7c7111f72b 100644 --- a/doc/developer/debugging.rst +++ b/doc/developer/debugging.rst @@ -118,8 +118,8 @@ to test it). Tilecloud chain ............... -Points to check with TileCloud chain:: +Points to check with TileCloud chain: - * Disabling metatiles should be avoided. - * Make sure that``empty_metatile_detection`` and ``empty_tile_detection`` are configured correctly. - * Make sure to not generate tiles with a higher resolution than in the raster sources. +* Disabling metatiles should be avoided. +* Make sure that``empty_metatile_detection`` and ``empty_tile_detection`` are configured correctly. +* Make sure to not generate tiles with a higher resolution than in the raster sources. From 31b17513576708cf1e62640696796b6ba583df1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Tue, 12 Jun 2018 16:25:27 +0200 Subject: [PATCH 18/92] Fix the foreign key that don't ends with '_id' --- c2cgeoportal/lib/lingua_extractor.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/c2cgeoportal/lib/lingua_extractor.py b/c2cgeoportal/lib/lingua_extractor.py index 7baf1b5747..4b3b30f249 100644 --- a/c2cgeoportal/lib/lingua_extractor.py +++ b/c2cgeoportal/lib/lingua_extractor.py @@ -391,8 +391,12 @@ def _import_layer_wms(self, layer, messages): column = column_property.columns[0] if not column.primary_key and not isinstance(column.type, Geometry): if column.foreign_keys: - name = "type_" if column.name == "type_id" else \ - column.name[0:column.name.rindex("_id")] + if column.name == "type_id": + name = "type_" + elif column.name.endswith("_id"): + name = column.name[:-3] + else: + name = column.name + "_" else: name = column_property.key messages.append(Message( From 6a340d235b13dc9ec80ae73ea9a980d43ce294c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Wed, 6 Jun 2018 11:21:44 +0200 Subject: [PATCH 19/92] Upgrade c2cwsgiutils to version 2 --- Dockerfile.mako | 8 ++------ Jenkinsfile | 6 ++++-- Makefile | 4 ++-- docker/build/Dockerfile | 19 ++++++++++--------- .../scaffolds/create/docker-compose.yaml.mako | 2 +- .../scaffolds/create/front/haproxy.cfg.tmpl | 2 +- travis/docker-compose.yaml.mako | 2 +- 7 files changed, 21 insertions(+), 22 deletions(-) diff --git a/Dockerfile.mako b/Dockerfile.mako index a882d76606..2e6533aa8d 100644 --- a/Dockerfile.mako +++ b/Dockerfile.mako @@ -16,17 +16,13 @@ RUN \ ttf2eot /usr/lib/node_modules/ngeo/contribs/gmf/fonts/gmf-icons.ttf \ /usr/lib/node_modules/ngeo/contribs/gmf/fonts/gmf-icons.eot && \ ttf2woff /usr/lib/node_modules/ngeo/contribs/gmf/fonts/gmf-icons.ttf \ - /usr/lib/node_modules/ngeo/contribs/gmf/fonts/gmf-icons.woff && \ - convert /usr/lib/node_modules/ngeo/contribs/gmf/cursors/grab.png \ - /usr/lib/node_modules/ngeo/contribs/gmf/cursors/grab.cur && \ - convert /usr/lib/node_modules/ngeo/contribs/gmf/cursors/grabbing.png \ - /usr/lib/node_modules/ngeo/contribs/gmf/cursors/grabbing.cur + /usr/lib/node_modules/ngeo/contribs/gmf/fonts/gmf-icons.woff RUN \ mkdir --parents /opt/angular-locale && \ for LANG in en fr de it en-ch fr-ch de-ch it-ch; \ do \ - wget -O /opt/angular-locale/angular-locale_$LANG.js https://raw.githubusercontent.com/angular/angular.js/v`grep '"angular"' /usr/lib/node_modules/ngeo/package.json | cut --delimiter \" --fields 4 | tr --delete '\r\n'`/src/ngLocale/angular-locale_$LANG.js; \ + curl --output /opt/angular-locale/angular-locale_$LANG.js https://raw.githubusercontent.com/angular/angular.js/v`grep '"angular"' /usr/lib/node_modules/ngeo/package.json | cut --delimiter \" --fields 4 | tr --delete '\r\n'`/src/ngLocale/angular-locale_$LANG.js; \ done COPY commons /opt/c2cgeoportal_commons diff --git a/Jenkinsfile b/Jenkinsfile index 8f0793d378..618e8f2a72 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -105,8 +105,10 @@ dockerBuild { try { sh 'docker login -u "$USERNAME" -p "$PASSWORD"' sh '(cd ${HOME}/workspace/testgeomapfish/; docker-compose up --force-recreate -d)' - sh '(cd ${HOME}/workspace/testgeomapfish/; docker-compose exec -T geoportal wait-for-db)' - sh './docker-run travis/waitwsgi http://`netstat --route --numeric|grep ^0.0.0.0|awk \'{print($2)}\'`:8080/' + timeout(time: 2, unit: 'MINUTES') { + sh '(cd ${HOME}/workspace/testgeomapfish/; docker-compose exec -T geoportal wait-for-db)' + sh './docker-run travis/waitwsgi http://`netstat --route --numeric|grep ^0.0.0.0|awk \'{print($2)}\'`:8080/' + } for (path in [ 'c2c/health_check', 'c2c/health_check?max_level=9', diff --git a/Makefile b/Makefile index a5e63e0d56..5a7a46e9e4 100644 --- a/Makefile +++ b/Makefile @@ -232,11 +232,11 @@ pylint: $(BUILD_DIR)/commons.timestamp .PHONY: mypy mypy: - MYPYPATH=/usr/local/lib/python3.6/site-packages:/opt/c2cwsgiutils \ + MYPYPATH=/usr/local/lib/python3.6/dist-packages:/opt/c2cwsgiutils \ mypy --disallow-untyped-defs --strict-optional --follow-imports skip \ commons/c2cgeoportal_commons # TODO: add --disallow-untyped-defs - MYPYPATH=/usr/local/lib/python3.6/site-packages/ \ + MYPYPATH=/usr/local/lib/python3.6/dist-packages/ \ mypy --ignore-missing-imports --strict-optional --follow-imports skip \ geoportal/c2cgeoportal_geoportal \ admin/c2cgeoportal_admin \ diff --git a/docker/build/Dockerfile b/docker/build/Dockerfile index a4520b895b..6e3b8ade4c 100644 --- a/docker/build/Dockerfile +++ b/docker/build/Dockerfile @@ -1,17 +1,18 @@ -FROM camptocamp/c2cwsgiutils:1 +FROM camptocamp/c2cwsgiutils:2 LABEL maintainer Camptocamp "info@camptocamp.com" RUN \ + . /etc/os-release && \ apt-get update && \ apt-get install --assume-yes --no-install-recommends tree apt-transport-https gettext sudo && \ - echo 'deb https://deb.nodesource.com/node_6.x jessie main' > /etc/apt/sources.list.d/nodesource.list && \ + echo "deb https://deb.nodesource.com/node_6.x ${VERSION_CODENAME} main" > /etc/apt/sources.list.d/nodesource.list && \ curl --silent https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - && \ - echo "deb http://apt.dockerproject.org/repo debian-jessie main" > /etc/apt/sources.list.d/docker.list && \ - curl --silent http://apt.dockerproject.org/gpg | apt-key add - && \ - echo "deb http://http.debian.net/debian jessie-backports main" > /etc/apt/sources.list.d/backport.list && \ +# Docker source list should be like it but actually it's empty... +# echo "deb [arch=amd64] https://download.docker.com/linux/ubuntu ${VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list && \ + echo "deb [arch=amd64] https://download.docker.com/linux/debian stretch stable" > /etc/apt/sources.list.d/docker.list && \ + curl --silent https://download.docker.com/linux/ubuntu/gpg | apt-key add - && \ apt-get update && \ - apt-get install --assume-yes --no-install-recommends nodejs docker-engine && \ - apt-get install --assume-yes --no-install-recommends --target-release=jessie-backports openjdk-8-jre-headless && \ + apt-get install --assume-yes --no-install-recommends 'nodejs=6.*' docker-ce openjdk-8-jre-headless && \ apt-get clean && \ rm --recursive --force /var/lib/apt/lists/* @@ -21,8 +22,8 @@ RUN \ cd /tmp && \ pip install --disable-pip-version-check --no-cache-dir --requirement requirements.txt && \ # for mypy - touch /usr/local/lib/python3.6/site-packages/zope/__init__.py && \ - touch /usr/local/lib/python3.6/site-packages/c2c/__init__.py && \ + touch /usr/local/lib/python3.6/dist-packages/zope/__init__.py && \ + touch /usr/local/lib/python3.6/dist-packages/c2c/__init__.py && \ rm --recursive --force /tmp/* /var/tmp/* /root/.cache/* COPY sudoers /etc/ diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako index 525b4fd014..f9f8430a88 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako @@ -64,7 +64,7 @@ ${service_defaults('tilecloudchain')}\ image: ${docker_base}-geoportal:${docker_tag} volumes: - /var/sig:/var/sig:ro -${service_defaults('geoportal', 80)}\ +${service_defaults('geoportal', 8080)}\ front: image: haproxy:1.8 diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/front/haproxy.cfg.tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/create/front/haproxy.cfg.tmpl index 2601cfafed..f96a4a25c6 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/front/haproxy.cfg.tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/front/haproxy.cfg.tmpl @@ -53,7 +53,7 @@ defaults backend geoportal option httpchk GET / HTTP/1.0\r\nUser-Agent:\ healthcheck http-check expect status 200 - server linked geoportal:80 resolvers dns #check + server linked geoportal:8080 resolvers dns #check backend tilecloudchain diff --git a/travis/docker-compose.yaml.mako b/travis/docker-compose.yaml.mako index 69f250682d..42784fa63f 100644 --- a/travis/docker-compose.yaml.mako +++ b/travis/docker-compose.yaml.mako @@ -56,4 +56,4 @@ ${service_defaults('mapserver', 80)}\ geoportal: image: ${docker_base}-geoportal:${docker_tag} -${service_defaults('geoportal', 80, True)}\ +${service_defaults('geoportal', 8080, True)}\ From c262948a821dd0a1ae32f703f31c5859c0e6ce2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Tue, 12 Jun 2018 16:19:25 +0200 Subject: [PATCH 20/92] Don't request tow times the same capabilities --- geoportal/c2cgeoportal_geoportal/views/entry.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/geoportal/c2cgeoportal_geoportal/views/entry.py b/geoportal/c2cgeoportal_geoportal/views/entry.py index 4eb7ffda6c..df3f457bb3 100644 --- a/geoportal/c2cgeoportal_geoportal/views/entry.py +++ b/geoportal/c2cgeoportal_geoportal/views/entry.py @@ -217,6 +217,11 @@ def _wms_getcap(self, ogc_server=None): log.error(error, exc_info=True) return wms, errors + @cache_region.cache_on_arguments() + def get_http_cached(self, url, headers): + http = httplib2.Http() + return http.request(url, method="GET", headers=headers) + @cache_region.cache_on_arguments() def _wms_getcap_cached(self, ogc_server, _): """ _ is just for cache on the role id """ @@ -244,7 +249,6 @@ def _wms_getcap_cached(self, ogc_server, _): log.info("Get WMS GetCapabilities for url: {0!s}".format(url)) # forward request to target (without Host Header) - http = httplib2.Http() headers = dict(self.request.headers) role = None if self.request.user is None else self.request.user.role @@ -258,7 +262,7 @@ def _wms_getcap_cached(self, ogc_server, _): headers.pop("Host") try: - resp, content = http.request(url, method="GET", headers=headers) + resp, content = self.get_http_cached(url, headers) except Exception: # pragma: no cover error = "Unable to GetCapabilities from url {}".format(url) errors.add(error) @@ -1092,13 +1096,12 @@ def _wfs_types_cached(self, wfs_url): log.info("WFS GetCapabilities for base url: {0!s}".format(wfsgc_url)) # forward request to target (without Host Header) - http = httplib2.Http() headers = dict(self.request.headers) if urllib.parse.urlsplit(wfsgc_url).hostname != "localhost" and "Host" in headers: headers.pop("Host") # pragma nocover try: - resp, get_capabilities_xml = http.request(wfsgc_url, method="GET", headers=headers) + resp, get_capabilities_xml = self.get_http_cached(wfsgc_url, headers) except Exception: # pragma: no cover errors.add("Unable to GetCapabilities from url {0!s}".format(wfsgc_url)) return None, errors From c7d98f85bf159d1de199e10e27c394390777e56e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Tue, 12 Jun 2018 15:24:34 +0200 Subject: [PATCH 21/92] Add missing space --- doc/developer/debugging.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/developer/debugging.rst b/doc/developer/debugging.rst index 7c7111f72b..dcab2f79c9 100644 --- a/doc/developer/debugging.rst +++ b/doc/developer/debugging.rst @@ -121,5 +121,5 @@ Tilecloud chain Points to check with TileCloud chain: * Disabling metatiles should be avoided. -* Make sure that``empty_metatile_detection`` and ``empty_tile_detection`` are configured correctly. +* Make sure that ``empty_metatile_detection`` and ``empty_tile_detection`` are configured correctly. * Make sure to not generate tiles with a higher resolution than in the raster sources. From 9d1fa023cf334b6dac7b2d716c6eb2ace2302826 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Tue, 12 Jun 2018 10:40:22 +0200 Subject: [PATCH 22/92] Add memcached service in Docker compose environment --- docker/build/requirements.txt | 4 ++-- .../create/tilegeneration/config.yaml.tmpl.mako_tmpl | 11 +++++++++-- .../scaffolds/update/CONST_vars.yaml_tmpl | 12 ++++++++++-- travis/docker-compose.yaml.mako | 2 +- travis/v23-project.yaml | 1 + 5 files changed, 23 insertions(+), 7 deletions(-) diff --git a/docker/build/requirements.txt b/docker/build/requirements.txt index 45a963a5fd..ee168152e8 100644 --- a/docker/build/requirements.txt +++ b/docker/build/requirements.txt @@ -64,8 +64,8 @@ simplejson==3.13.2 # geoportal Sphinx==1.7.2 # doc sphinx-prompt==1.0.0 # doc SQLAlchemy==1.2.6 -tilecloud==0.5.1 # Tile generation -tilecloud-chain==1.6.0.dev2 # Tile generation +tilecloud==1.0.0 # Tile generation +tilecloud-chain==1.6.0.dev4 # Tile generation transaction==2.2.1 # commons, geoportal transifex-client==0.12.5 # Makefile, rq.filter: >=0.14 translationstring==1.3 # admin diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/tilegeneration/config.yaml.tmpl.mako_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/create/tilegeneration/config.yaml.tmpl.mako_tmpl index f4b51af61e..a0af03a93b 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/tilegeneration/config.yaml.tmpl.mako_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/tilegeneration/config.yaml.tmpl.mako_tmpl @@ -101,6 +101,12 @@ generation: # maximum allowed consecutive errors, after it exit [default to 10] maxconsecutive_errors: 10 +sqs: + # The region where the SQS queue is + region: eu-west-1 + # The SQS queue name, it should already exists + queue: + server: mapcache_base: '${mapcache_url}' wmts_path: tiles @@ -109,8 +115,9 @@ server: mapcache: config_file: mapcache/mapcache.xml.tmpl - memcache_host: localhost - memcache_port: 11211 + location: '' + memcache_host: '${memcached_host}' + memcache_port: '${memcached_port}' process: optipng_test: diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl index 2626903f44..a6a016f466 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl @@ -36,8 +36,10 @@ vars: PGPASSWORD: www-data PGDATABASE: geomapfish GEOPORTAL_INTERNAL_URL: 'http://geoportal/' - MAPCACHE_URL: http://mapcache/ + MAPCACHE_URL: http://mapcache/mapcache/ MAPSERVER_URL: http://mapserver/ + MEMCACHED_HOST: memcached + MEMCACHED_PORT: 11211 db: environment: POSTGRES_DB: geomapfish @@ -365,6 +367,8 @@ vars: print_url: '{PRINT_URL}' mapserver_url: '{MAPSERVER_URL}' mapcache_url: '{MAPCACHE_URL}' + memcached_host: '{MEMCACHED_HOST}' + memcached_port: '{MEMCACHED_PORT}' pdfreport: print_url: '{print_url}' @@ -548,9 +552,13 @@ runtime_environment: - name: MAPCACHE_URL default: http://mapcache/ - name: REDIS_HOST - default: undefined # no error if absent + default: redis - name: REDIS_PORT default: '6372' + - name: MEMCACHED_HOST + default: memcached + - name: MEMCACHED_PORT + default: '11211' runtime_postprocess: - expression: int({}) diff --git a/travis/docker-compose.yaml.mako b/travis/docker-compose.yaml.mako index 42784fa63f..15a31ba5be 100644 --- a/travis/docker-compose.yaml.mako +++ b/travis/docker-compose.yaml.mako @@ -49,7 +49,7 @@ ${service_defaults('mapserver', 11211)}\ ${service_defaults('mapserver', 6379)}\ tilecloudchain: - image: camptocamp/tilecloud-chain:1.5.0 + image: camptocamp/tilecloud-chain:1.6 volumes_from: - config:ro ${service_defaults('mapserver', 80)}\ diff --git a/travis/v23-project.yaml b/travis/v23-project.yaml index 033ca58f17..1c73188a42 100644 --- a/travis/v23-project.yaml +++ b/travis/v23-project.yaml @@ -10,6 +10,7 @@ unmanaged_files: - mapserver/mapserver\.map\.mako - mapserver/mapserver\.map\.tmpl\.mako - tilegeneration/config\.yaml\.mako + - tilegeneration/config\.yaml\.tmpl\.mako - geoportal/testgeomapfish_geoportal/static-ngeo/js/apps/desktop\.html\.ejs - geoportal/testgeomapfish_geoportal/static-ngeo/js/apps/mobile\.html\.ejs template_vars: From b7afaf7771a6f61a1321ea7694b113116809895e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Fri, 8 Jun 2018 12:51:42 +0200 Subject: [PATCH 23/92] Test the upgrade from 2.3.0 --- Jenkinsfile | 35 ++++++---- .../nondockercreate/nondocker-override.mk | 3 +- .../nondockerupdate/+dot+upgrade.yaml_tmpl | 3 + .../scaffolds/update/+dot+upgrade.yaml_tmpl | 3 + travis/from23-config | 3 + travis/{v22-project.yaml => old-project.yaml} | 10 +-- travis/test-upgrade-convert.sh | 69 ++++++++++++++++--- 7 files changed, 95 insertions(+), 31 deletions(-) create mode 100644 travis/from23-config rename travis/{v22-project.yaml => old-project.yaml} (87%) diff --git a/Jenkinsfile b/Jenkinsfile index 618e8f2a72..71f3f15c3b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -36,6 +36,8 @@ dockerBuild { stage('Build') { checkout scm sh 'make docker-build' + sh 'docker run --name geomapfish-db --env=POSTGRES_USER=www-data --env=POSTGRES_PASSWORD=www-data --env=POSTGRES_DB=geomapfish --publish=5432:5432 --detach camptocamp/geomapfish-test-db' + sh 'travis/test-upgrade-convert.sh init ${HOME}/workspace' } stage('Tests') { checkout scm @@ -164,25 +166,28 @@ dockerBuild { sh '(cd ${HOME}/workspace/testgeomapfish/; docker-compose --file=docker-compose-build.yaml down)' } sh 'rm -rf ${HOME}/workspace/testgeomapfish' - }, 'Tests upgrades': { - try { - sh 'docker run --name geomapfish-db --env=POSTGRES_USER=www-data --env=POSTGRES_PASSWORD=www-data --env=POSTGRES_DB=geomapfish --publish=5432:5432 --detach camptocamp/geomapfish-test-db' - sh 'travis/test-upgrade-convert.sh init ${HOME}/workspace' - // Test Upgrade an convert project - sh 'travis/test-upgrade-convert.sh v220-todocker ${HOME}/workspace' - sh 'travis/test-upgrade-convert.sh v220-tonondocker ${HOME}/workspace' - sh 'travis/test-upgrade-convert.sh docker ${HOME}/workspace' - sh 'travis/test-upgrade-convert.sh tonondocker ${HOME}/workspace' - sh 'travis/test-upgrade-convert.sh nondocker ${HOME}/workspace' - sh 'travis/test-upgrade-convert.sh todocker ${HOME}/workspace' - } finally { - sh 'docker stop geomapfish-db' - sh 'docker rm --volumes geomapfish-db' - } + }, 'Tests upgrades 220': { + // Test Upgrade an convert project + sh 'travis/test-upgrade-convert.sh v220-todocker ${HOME}/workspace' + sh 'travis/test-upgrade-convert.sh v220-tonondocker ${HOME}/workspace' + } + } + stage('Test Upgrade') { + parallel 'Tests upgrades Docker': { + sh 'travis/test-upgrade-convert.sh docker ${HOME}/workspace' + sh 'travis/test-upgrade-convert.sh tonondocker ${HOME}/workspace' + }, 'Tests upgrades non Docker': { + sh 'travis/test-upgrade-convert.sh nondocker ${HOME}/workspace' + sh 'travis/test-upgrade-convert.sh todocker ${HOME}/workspace' + }, 'Tests upgrades 230': { + sh 'travis/test-upgrade-convert.sh v230-docker ${HOME}/workspace' + sh 'travis/test-upgrade-convert.sh v230-nondocker ${HOME}/workspace' } } } finally { stage('Clean') { + sh 'docker stop geomapfish-db' + sh 'docker rm --volumes geomapfish-db' clean() } } diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-override.mk b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-override.mk index 62b059ba4a..b2085b50e6 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-override.mk +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-override.mk @@ -14,7 +14,7 @@ export MAPSERVER_URL PRINT_URL ?= http://print:8080/print/ export PRINT_URL PRINT_CONFIG_FILE ?= print/print-apps/$(PACKAGE)/config.yaml -MAPCACHE_FILE ?= mapcache/mapcache.xml +MAPCACHE_FILE ?= apache/mapcache.xml TILEGENERATION_CONFIG_FILE = tilegeneration/config.yaml VISIBLE_WEB_PROTOCOL ?= https @@ -43,7 +43,6 @@ DEFAULT_BUILD_RULES ?= docker-build-geoportal \ TILECLOUD_CHAIN ?= TRUE ifeq ($(TILECLOUD_CHAIN), TRUE) -MAPCACHE_FILE = apache/mapcache.xml CONF_FILES += $(MAPCACHE_FILE) endif diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockerupdate/+dot+upgrade.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockerupdate/+dot+upgrade.yaml_tmpl index 2405ebbe2e..2b3d28e034 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockerupdate/+dot+upgrade.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockerupdate/+dot+upgrade.yaml_tmpl @@ -164,3 +164,6 @@ files_to_move: - from: tilegeneration/config.yaml.tmpl.mako to: tilegeneration/config.yaml.mako version: 2.3 + - from: front/haproxy.cfg + to: front/haproxy.cfg.tmpl + version: 2.3 diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl index dee0944e24..c41e56dd88 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl @@ -201,3 +201,6 @@ files_to_move: - from: tilegeneration/config.yaml.mako to: tilegeneration/config.yaml.tmpl.mako version: 2.3 + - from: front/haproxy.cfg + to: front/haproxy.cfg.tmpl + version: 2.3 diff --git a/travis/from23-config b/travis/from23-config new file mode 100644 index 0000000000..7f3f43f02f --- /dev/null +++ b/travis/from23-config @@ -0,0 +1,3 @@ +[docker-run] +image=camptocamp/geomapfish-build +version=2.3 diff --git a/travis/v22-project.yaml b/travis/old-project.yaml similarity index 87% rename from travis/v22-project.yaml rename to travis/old-project.yaml index 24853b37f5..88d65d86fd 100644 --- a/travis/v22-project.yaml +++ b/travis/old-project.yaml @@ -1,8 +1,9 @@ --- project_folder: testgeomapfish project_package: ${package} -host: ${host} -checker_path: /${instanceid}/wsgi/check_collector? +checker_url: http://localhost/c2c/health_check? +checker_headers: + Host: ${host} managed_files: [] unmanaged_files: - \.gitignore @@ -20,8 +21,9 @@ unmanaged_files: - print/print-apps/testgeomapfish/config\.yaml\.mako - print/print-apps/testgeomapfish/config\.yaml\.tmpl - print/print-apps/testgeomapfish/.*\.jrxml - - geoportal/testgeomapfish_geoportal/locale/fr/LC_MESSAGES/testgeomapfish-client\.po - - geoportal/testgeomapfish_geoportal/locale/de/LC_MESSAGES/testgeomapfish-client\.po + - geoportal/testgeomapfish_geoportal/locale/fr/LC_MESSAGES/testgeomapfish_geoportal-client\.po + - geoportal/testgeomapfish_geoportal/locale/de/LC_MESSAGES/testgeomapfish_geoportal-client\.po + - geoportal/testgeomapfish_geoportal/locale/it/LC_MESSAGES/testgeomapfish_geoportal-client\.po - geoportal/testgeomapfish_geoportal/static-ngeo/js/apps/desktop\.html\.ejs - geoportal/testgeomapfish_geoportal/static-ngeo/js/apps/mobile\.html\.ejs - geoportal/testgeomapfish_geoportal/static-ngeo/js/apps/Controllerdesktop\.js diff --git a/travis/test-upgrade-convert.sh b/travis/test-upgrade-convert.sh index 3ac949db5e..d8d8c2b3aa 100755 --- a/travis/test-upgrade-convert.sh +++ b/travis/test-upgrade-convert.sh @@ -5,15 +5,15 @@ WORKSPACE=$2 export NODE_ENV=development function pcreate { - ./docker-run --image=camptocamp/geomapfish-build --share $1 pcreate --scaffold=$2 $1/testgeomapfish \ + ./docker-run --image=camptocamp/geomapfish-build $3 --share $1 pcreate --scaffold=$2 $1/testgeomapfish \ --overwrite --ignore-conflicting-name --package-name testgeomapfish } function only_create { rm --recursive --force $1 mkdir --parent $1 - pcreate $1 c2cgeoportal_create - pcreate $1 c2cgeoportal_update + pcreate $1 c2cgeoportal_create $2 + pcreate $1 c2cgeoportal_update $2 cd $1/testgeomapfish git init git config user.email travis@camptocamp.com @@ -23,7 +23,7 @@ function only_create { } function create { - only_create $1 + only_create $1 $2 cd $1/testgeomapfish git add --all git commit --quiet --message="Initial commit" @@ -33,8 +33,8 @@ function create { function createnondocker { only_create $1 - pcreate $1 c2cgeoportal_nondockercreate - pcreate $1 c2cgeoportal_nondockerupdate + pcreate $1 c2cgeoportal_nondockercreate $2 + pcreate $1 c2cgeoportal_nondockerupdate $2 cd $1/testgeomapfish git add --all git commit --quiet --message="Initial commit" @@ -57,6 +57,7 @@ function createv220 { } function printdiff { + ls -l .UPGRADE* for f in $(ls -1 *.diff) do echo "--- $f ---" @@ -76,6 +77,8 @@ then create ${WORKSPACE}/dockerref createnondocker ${WORKSPACE}/nondocker createnondocker ${WORKSPACE}/nondockerref + create ${WORKSPACE}/v230-docker --version=2.3.0 + createnondocker ${WORKSPACE}/v230-nondocker --version=2.3.0 unset SRID APACHE_VHOST EXTENT mkdir --parent ${WORKSPACE}/v220 ./docker-run --share=${WORKSPACE} tar --extract --bzip2 --file=travis/v220.tar.bz2 --directory=${WORKSPACE}/v220 @@ -128,7 +131,7 @@ then git rm testgeomapfish.mk git add temp.mk project.yaml.mako git commit --quiet --message="Start upgrade" - ./docker-run make --makefile=temp.mk upgrade + ./docker-run --env=NODE_ENV make --makefile=temp.mk upgrade if [ ! -e .UPGRADE10 ] then printdiff @@ -195,7 +198,7 @@ fi function v220 { cp docker-run $1/testgeomapfish - cp travis/v22-project.yaml $1/testgeomapfish/project.yaml.mako + cp travis/old-project.yaml $1/testgeomapfish/project.yaml.mako cd $1/testgeomapfish head --lines=-23 CONST_vars.yaml > CONST_vars.yaml_ mv CONST_vars.yaml{_,} @@ -221,7 +224,7 @@ function v220 { mv geoportal/testgeomapfish_geoportal/locale/en/LC_MESSAGES/testgeomapfish{,_geoportal}-client.po mv geoportal/testgeomapfish_geoportal/locale/fr/LC_MESSAGES/testgeomapfish{,_geoportal}-client.po mv geoportal/testgeomapfish_geoportal/locale/de/LC_MESSAGES/testgeomapfish{,_geoportal}-client.po - ./docker-run make $MAKE_ARGS upgrade9 + ./docker-run --env=NODE_ENV make $MAKE_ARGS upgrade9 if [ ! -e .UPGRADE_SUCCESS ] then printdiff @@ -246,9 +249,55 @@ then v220 ${WORKSPACE}/v220-tonondocker non fi +function v230 { + DOCKER_ARGS='' + MAKE_ARGS='' + if [ "$1" = 'non' ] + then + MAKE_ARGS='--makefile=testgeomapfish.mk' + DOCKER_ARGS='--env=VISIBLE_WEB_HOST=example.com --env=VISIBLE_WEB_PROTOCOL=https --env=VISIBLE_ENTRY_POINT=/' + fi + cp travis/old-project.yaml ${WORKSPACE}/v230-$1docker/testgeomapfish/project.yaml.mako + cd ${WORKSPACE}/v230-$1docker/testgeomapfish + ./docker-run ${DOCKER_ARGS} make ${MAKE_ARGS} project.yaml + cd - + cp travis/from23-config ${WORKSPACE}/v230-$1docker/testgeomapfish/.config + cd ${WORKSPACE}/v230-$1docker/testgeomapfish + git add project.yaml.mako .config + git commit --quiet --message="Start upgrade" + ./docker-run --env=NODE_ENV make ${MAKE_ARGS} upgrade + if [ -e .UPGRADE8 ] + then + ./docker-run --env=NODE_ENV make ${MAKE_ARGS} upgrade9 + fi + if [ ! -e .UPGRADE_SUCCESS ] + then + printdiff + echo "Fail to upgrade" + exit 1 + fi + ./docker-run make ${MAKE_ARGS} clean-all + rm --recursive --force .UPGRADE* \ + commons/testgeomapfish_commons.egg-info geoportal/testgeomapfish_geoportal.egg-info + cd - + find ${WORKSPACE}/v230-$1docker -type d -empty -delete + diff --recursive --exclude=.git ${WORKSPACE}/$1dockerref ${WORKSPACE}/v230-$1docker +} + +if [ "$1" = "v230-docker" ] +then + v230 '' +fi + +if [ "$1" = "v230-nondocker" ] +then + v230 'non' +fi + if [ "$1" = "cleanup" ] then rm --recursive --force ${WORKSPACE}/nondockerref ${WORKSPACE}/dockerref \ ${WORKSPACE}/nondocker ${WORKSPACE}/docker \ - ${WORKSPACE}/v220-todocker ${WORKSPACE}/v220-tonondocker + ${WORKSPACE}/v220-todocker ${WORKSPACE}/v220-tonondocker \ + ${WORKSPACE}/v230-docker ${WORKSPACE}/v230-nondocker fi From 3d443208473962b6fcbadd2a9ae8b5fa67d7aa54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Thu, 7 Jun 2018 13:54:55 +0200 Subject: [PATCH 24/92] Fix spell --- doc/developer/webservices.rst | 2 +- doc/integrator/security.rst | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/developer/webservices.rst b/doc/developer/webservices.rst index 86aeba898e..8e5b555231 100644 --- a/doc/developer/webservices.rst +++ b/doc/developer/webservices.rst @@ -251,7 +251,7 @@ Result HTTP code: * 200 Success: Success. -Annoymous JSON result +Anonymous JSON result ~~~~~~~~~~~~~~~~~~~~~ .. code:: json diff --git a/doc/integrator/security.rst b/doc/integrator/security.rst index 3c07ae232c..95bba9ce27 100644 --- a/doc/integrator/security.rst +++ b/doc/integrator/security.rst @@ -80,11 +80,11 @@ Services: - login - error -Authorized referers -------------------- +Authorized referrers +-------------------- To mitigate `CSRF `_ -attacks, the server validates the referer against a list of authorized referers. +attacks, the server validates the referer against a list of authorized referrers. By default, only the pages coming from the server are allowed. You can change that list by adding an ``authorized_referers`` list in your From 43714d882a12e2d3a14bd5142474567034ac9c6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Wed, 13 Jun 2018 11:36:24 +0200 Subject: [PATCH 25/92] Make the queue name configurable --- .../create/tilegeneration/config.yaml.tmpl.mako_tmpl | 2 +- .../scaffolds/update/CONST_vars.yaml_tmpl | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/tilegeneration/config.yaml.tmpl.mako_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/create/tilegeneration/config.yaml.tmpl.mako_tmpl index a0af03a93b..f0169fb4ef 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/tilegeneration/config.yaml.tmpl.mako_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/tilegeneration/config.yaml.tmpl.mako_tmpl @@ -105,7 +105,7 @@ sqs: # The region where the SQS queue is region: eu-west-1 # The SQS queue name, it should already exists - queue: + queue: '${tilegeneration_sqs_queue}' server: mapcache_base: '${mapcache_url}' diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl index a6a016f466..87fc204f5a 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl @@ -40,6 +40,7 @@ vars: MAPSERVER_URL: http://mapserver/ MEMCACHED_HOST: memcached MEMCACHED_PORT: 11211 + TILEGENERATION_SQS_QUEUE: '' db: environment: POSTGRES_DB: geomapfish @@ -369,6 +370,7 @@ vars: mapcache_url: '{MAPCACHE_URL}' memcached_host: '{MEMCACHED_HOST}' memcached_port: '{MEMCACHED_PORT}' + tilegeneration_sqs_queue: '{TILEGENERATION_SQS_QUEUE}' pdfreport: print_url: '{print_url}' @@ -559,6 +561,8 @@ runtime_environment: default: memcached - name: MEMCACHED_PORT default: '11211' + - name: TILEGENERATION_SQS_QUEUE + default: queue_name runtime_postprocess: - expression: int({}) From a03b350ac0d455dde8a8c54efcb2dbce5ec1f48c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Tue, 12 Jun 2018 10:32:22 +0200 Subject: [PATCH 26/92] Add Alembic upgrade service --- .travis.yml | 14 ++++++++----- Jenkinsfile | 10 +++++----- .../scaffolds/create/+dot+gitignore_tmpl | 2 +- .../scaffolds/create/docker-compose.yaml.mako | 9 +++++++++ .../create/geoportal/+dot+dockerignore_tmpl | 1 + .../create/{ => geoportal}/alembic.ini | 0 .../nondockercreate/+dot+gitignore_tmpl | 2 +- .../deploy/hooks/pre-restore-database.mako | 2 +- .../nondockercreate/nondocker-override.mk | 4 ++-- .../nondockerupdate/+dot+upgrade.yaml_tmpl | 5 ++++- .../scaffolds/update/+dot+upgrade.yaml_tmpl | 5 ++++- .../scaffolds/update/CONST_Makefile_tmpl | 20 +++++++++---------- .../scripts/c2cupgrade.py | 4 ++-- travis/create-new-nondocker-project.sh | 8 ++++---- 14 files changed, 53 insertions(+), 33 deletions(-) rename geoportal/c2cgeoportal_geoportal/scaffolds/create/{ => geoportal}/alembic.ini (100%) diff --git a/.travis.yml b/.travis.yml index 275cd5b133..c4015220dc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -98,11 +98,15 @@ script: - (cd /tmp/travis/nondockertestgeomapfish/; ./docker-run rm /build/c2ctemplate-cache.json) - "(cd /tmp/travis/nondockertestgeomapfish/; \ ./docker-run make --makefile=empty-vars.mk geoportal/config.yaml)" - - (cd /tmp/travis/nondockertestgeomapfish/; ./docker-run make --makefile=travis.mk alembic.ini) - - (cd /tmp/travis/nondockertestgeomapfish/; ./docker-run alembic --name=main upgrade head) - - (cd /tmp/travis/nondockertestgeomapfish/; ./docker-run alembic --name=static upgrade head) - - (cd /tmp/travis/nondockertestgeomapfish/; ./docker-run alembic --name=static downgrade base) - - (cd /tmp/travis/nondockertestgeomapfish/; ./docker-run alembic --name=main downgrade base) + - (cd /tmp/travis/nondockertestgeomapfish/; ./docker-run make --makefile=travis.mk geoportal/alembic.ini) + - "(cd /tmp/travis/nondockertestgeomapfish/; \ + ./docker-run alembic --config=geoportal/alembic.ini --name=main upgrade head)" + - "(cd /tmp/travis/nondockertestgeomapfish/; \ + ./docker-run alembic --config=geoportal/alembic.ini --name=static upgrade head)" + - "(cd /tmp/travis/nondockertestgeomapfish/; \ + ./docker-run alembic --config=geoportal/alembic.ini --name=static downgrade base)" + - "(cd /tmp/travis/nondockertestgeomapfish/; \ + ./docker-run alembic --config=geoportal/alembic.ini --name=main downgrade base)" ## END FOR NON DOCKER TESTS - ./docker-run make doc diff --git a/Jenkinsfile b/Jenkinsfile index 71f3f15c3b..c6b9b8a819 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -153,12 +153,12 @@ dockerBuild { -name \\*.py | xargs travis/squote''' sh '(cd ${HOME}/workspace/testgeomapfish/; ./docker-run travis/status.sh)' sh '(cd ${HOME}/workspace/testgeomapfish/; ./docker-run make --makefile=empty-vars.mk geoportal/config.yaml)' - sh '(cd ${HOME}/workspace/testgeomapfish/; ./docker-run make --makefile=travis.mk alembic.ini)' + sh '(cd ${HOME}/workspace/testgeomapfish/; ./docker-run make --makefile=travis.mk geoportal/alembic.ini)' try { - sh '(cd ${HOME}/workspace/testgeomapfish/; ./docker-compose-run alembic --name=main upgrade head)' - sh '(cd ${HOME}/workspace/testgeomapfish/; ./docker-compose-run alembic --name=static upgrade head)' - sh '(cd ${HOME}/workspace/testgeomapfish/; ./docker-compose-run alembic --name=static downgrade base)' - sh '(cd ${HOME}/workspace/testgeomapfish/; ./docker-compose-run alembic --name=main downgrade base)' + sh '(cd ${HOME}/workspace/testgeomapfish/; ./docker-compose-run alembic --config=geoportal/alembic.ini --name=main upgrade head)' + sh '(cd ${HOME}/workspace/testgeomapfish/; ./docker-compose-run alembic --config=geoportal/alembic.ini --name=static upgrade head)' + sh '(cd ${HOME}/workspace/testgeomapfish/; ./docker-compose-run alembic --config=geoportal/alembic.ini --name=static downgrade base)' + sh '(cd ${HOME}/workspace/testgeomapfish/; ./docker-compose-run alembic --config=geoportal/alembic.ini --name=main downgrade base)' } catch (Exception error) { sh '(cd ${HOME}/workspace/testgeomapfish/; docker-compose --file=docker-compose-build.yaml logs)' throw error diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+gitignore_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+gitignore_tmpl index 84db93d206..0d8e4b8469 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+gitignore_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+gitignore_tmpl @@ -5,7 +5,7 @@ __pycache__/ /project.yaml /docker-compose.yaml /docker-compose-build.yaml -/alembic.yaml +/geoportal/alembic.yaml /testdb/11-schemas.sql /testdb/12-alembic.sql /testdb/13-alembic-static.sql diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako index f9f8430a88..26749019a6 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako @@ -66,6 +66,15 @@ ${service_defaults('tilecloudchain')}\ - /var/sig:/var/sig:ro ${service_defaults('geoportal', 8080)}\ + alembic: + image: ${docker_base}-geoportal:${docker_tag} + command: + - alembic + - --name=static + - upgrade + - head +${service_defaults('geoportal', 80)}\ + front: image: haproxy:1.8 volumes_from: diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/+dot+dockerignore_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/+dot+dockerignore_tmpl index 1de7eaac11..55209ccd93 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/+dot+dockerignore_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/+dot+dockerignore_tmpl @@ -1,5 +1,6 @@ * !alembic.ini +!alembic.yaml !config.yaml !eval_templates.sh !production.ini diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/alembic.ini b/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/alembic.ini similarity index 100% rename from geoportal/c2cgeoportal_geoportal/scaffolds/create/alembic.ini rename to geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/alembic.ini diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/+dot+gitignore_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/+dot+gitignore_tmpl index 33cf2ee95d..8baa5065fe 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/+dot+gitignore_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/+dot+gitignore_tmpl @@ -7,7 +7,7 @@ __pycache__/ /docker-compose.yaml /docker-compose-build.yaml /admin/ -/alembic.yaml +/geoportal/alembic.yaml /testdb/11-schemas.sql /testdb/12-alembic.sql /testdb/13-alembic-static.sql diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/deploy/hooks/pre-restore-database.mako b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/deploy/hooks/pre-restore-database.mako index 163b0eb84c..7ad1b4d44b 100755 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/deploy/hooks/pre-restore-database.mako +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/deploy/hooks/pre-restore-database.mako @@ -37,5 +37,5 @@ psql -c 'GRANT ALL ON SCHEMA "${schema_static}" TO "${dbuser}";' ${db} psql -c 'GRANT ALL ON ALL TABLES IN SCHEMA "${schema_static}" TO "${dbuser}";' ${db} psql -c 'ALTER TABLE main_static.shorturl OWNER TO "www-data";' ${db} -./docker-run make --makefile=$TARGET.mk alembic.ini alembic.yaml +./docker-run make --makefile=$TARGET.mk geoportal/alembic.ini geoportal/alembic.yaml ./docker-run alembic --name=static upgrade head diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-override.mk b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-override.mk index b2085b50e6..32481e1ac4 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-override.mk +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-override.mk @@ -38,8 +38,8 @@ CONF_FILES += $(shell ls -1 apache/*.conf 2> /dev/null) $(CONF_FILES_MAKO:.mako= DEFAULT_BUILD_RULES ?= docker-build-geoportal \ docker-build-config \ project.yaml \ - alembic.ini \ - alembic.yaml + geoportal/alembic.ini \ + geoportal/alembic.yaml TILECLOUD_CHAIN ?= TRUE ifeq ($(TILECLOUD_CHAIN), TRUE) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockerupdate/+dot+upgrade.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockerupdate/+dot+upgrade.yaml_tmpl index 2b3d28e034..8ecbf99ae0 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockerupdate/+dot+upgrade.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockerupdate/+dot+upgrade.yaml_tmpl @@ -99,7 +99,7 @@ files_to_move: to: vars.yaml version: 2.3 - from: alembic.ini.mako - to: alembic.ini + to: geoportal/alembic.ini version: 2.3 - from: docker-compose.yml.mako to: docker-compose.yaml.mako @@ -167,3 +167,6 @@ files_to_move: - from: front/haproxy.cfg to: front/haproxy.cfg.tmpl version: 2.3 + - from: alembic.ini + to: geoportal/alembic.ini + version: 2.3 diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl index c41e56dd88..6a3cc44dee 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl @@ -142,7 +142,7 @@ files_to_move: to: Makefile version: 2.3 - from: alembic.ini.mako - to: alembic.ini + to: geoportal/alembic.ini version: 2.3 - from: docker-compose.yml.mako to: docker-compose.yaml.mako @@ -204,3 +204,6 @@ files_to_move: - from: front/haproxy.cfg to: front/haproxy.cfg.tmpl version: 2.3 + - from: alembic.ini + to: geoportal/alembic.ini + version: 2.3 diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl index 044270e7c5..fb650f57e5 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl @@ -82,8 +82,8 @@ WEB_RULE ?= $(DEFAULT_WEB_RULE) DEFAULT_BUILD_RULES ?= docker-build-geoportal \ docker-build-config \ project.yaml \ - alembic.ini \ - alembic.yaml \ + geoportal/alembic.ini \ + geoportal/alembic.yaml \ docker-compose.yaml \ docker-compose-build.yaml @@ -393,7 +393,7 @@ checks: flake8 $(CLIENT_CHECK_RULE) git-attributes yamllint spell git-attributes: git --no-pager diff --check `git log --oneline | tail -1 | cut --fields=1 --delimiter=' '` -YAML_FILES ?= $(filter-out ./tilegeneration/config.yaml ./geoportal/config.yaml ./alembic.yaml,$(shell find \ +YAML_FILES ?= $(filter-out ./tilegeneration/config.yaml ./geoportal/config.yaml ./geoportal/alembic.yaml,$(shell find \ -name .build -prune -or \ -name cgxp -prune -or \ -name node_modules -prune -or \ @@ -417,7 +417,7 @@ clean: template-clean $(OUTPUT_DIR)/ \ $(APP_OUTPUT_DIR)/ \ geoportal/$(PACKAGE)_geoportal/locale/$(PACKAGE)-*.pot \ - alembic.yaml \ + geoportal/alembic.yaml \ .UPGRADE* \ mapcache \ $(addprefix geoportal/$(PACKAGE)_geoportal/locale/, $(addsuffix /LC_MESSAGES/$(PACKAGE)_geoportal-$(L10N_CLIENT_POSTFIX).mo, $(LANGUAGES))) \ @@ -456,7 +456,7 @@ build-cgxp: $(JSBUILD_OUTPUT_FILES) $(CSS_CGXP_OUTPUT) lint-ngeo: /build/eslint.timestamp .PHONY: upgrade-db -upgrade-db: alembic.ini alembic.yaml +upgrade-db: geoportal/alembic.ini geoportal/alembic.yaml alembic --name=main upgrade head alembic --name=static upgrade head @@ -708,7 +708,7 @@ push-docker: docker push $(DOCKER_BASE)-geoportal:$(DOCKER_TAG) docker push $(DOCKER_BASE)-mapserver:$(DOCKER_TAG) -alembic.yaml: $(ALEMBIC_YAML_FILE) vars.yaml CONST_vars.yaml +geoportal/alembic.yaml: $(ALEMBIC_YAML_FILE) vars.yaml CONST_vars.yaml $(PRERULE_CMD) c2c-template --vars $< --get-config /build/_alembic.yaml srid schema schema_static sqlalchemy.url cache mv /build/_alembic.yaml $@ @@ -721,13 +721,13 @@ testdb/11-schemas.sql: $(ALEMBIC_YAML_FILE) testdb/11-schemas.sql_mako vars.yaml --files testdb/11-schemas.sql.mako rm testdb/11-schemas.sql.mako -testdb/12-alembic.sql: alembic.ini alembic.yaml $(shell ls -1 /opt/alembic/main/*.py) +testdb/12-alembic.sql: geoportal/alembic.ini geoportal/alembic.yaml $(shell ls -1 /opt/alembic/main/*.py) $(PRERULE_CMD) - alembic --name=main upgrade --sql head > $@ + alembic --config=$< --name=main upgrade --sql head > $@ -testdb/13-alembic-static.sql: alembic.ini alembic.yaml $(shell ls -1 /opt/alembic/static/*.py) +testdb/13-alembic-static.sql: geoportal/alembic.ini geoportal/alembic.yaml $(shell ls -1 /opt/alembic/static/*.py) $(PRERULE_CMD) - alembic --name=static upgrade --sql head > $@ + alembic --config=$< --name=static upgrade --sql head > $@ .PHONY: docker-build-testdb docker-build-testdb: testdb/11-schemas.sql testdb/12-alembic.sql testdb/13-alembic-static.sql \ diff --git a/geoportal/c2cgeoportal_geoportal/scripts/c2cupgrade.py b/geoportal/c2cgeoportal_geoportal/scripts/c2cupgrade.py index fd9ede953c..d6f5084dc8 100644 --- a/geoportal/c2cgeoportal_geoportal/scripts/c2cupgrade.py +++ b/geoportal/c2cgeoportal_geoportal/scripts/c2cupgrade.py @@ -645,8 +645,8 @@ def step11(self, step): check_call(["make", "--makefile=" + self.options.new_makefile, "build"]) if self.options.nondocker: - command.upgrade(Config("alembic.ini", ini_section="main"), "head") - command.upgrade(Config("alembic.ini", ini_section="static"), "head") + command.upgrade(Config("geoportal/alembic.ini", ini_section="main"), "head") + command.upgrade(Config("geoportal/alembic.ini", ini_section="static"), "head") args = " --makefile={}".format(self.options.makefile) \ if self.options.makefile != "Makefile" else "" diff --git a/travis/create-new-nondocker-project.sh b/travis/create-new-nondocker-project.sh index c2403d3e65..eb34badd12 100755 --- a/travis/create-new-nondocker-project.sh +++ b/travis/create-new-nondocker-project.sh @@ -44,14 +44,14 @@ sudo a2enmod fcgid # Minimal build ./docker-run make --makefile=travis.mk \ build \ - alembic.ini \ - alembic.yaml \ + geoportal/alembic.ini \ + geoportal/alembic.yaml \ geoportal/production.ini \ geoportal/config.yaml \ docker-compose-build.yaml docker-build-testdb FINALISE=TRUE make --makefile=travis.mk build -./docker-run alembic --name=main upgrade head -./docker-run alembic --name=static upgrade head +./docker-run alembic --config=geoportal/alembic.ini --name=main upgrade head +./docker-run alembic --config=geoportal/alembic.ini --name=static upgrade head # Create default theme ./docker-run /build/venv/bin/python /usr/local/bin/create-demo-theme ./docker-run make --makefile=travis.mk update-po From bdebec90e8d74a2fc6ed99037efdeb0cfe179f81 Mon Sep 17 00:00:00 2001 From: Patrick Valsecchi Date: Thu, 14 Jun 2018 08:18:59 +0200 Subject: [PATCH 27/92] Add health checks for the DB schema version (alembic) --- geoportal/c2cgeoportal_geoportal/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/geoportal/c2cgeoportal_geoportal/__init__.py b/geoportal/c2cgeoportal_geoportal/__init__.py index 679a37f995..ac32959bd1 100644 --- a/geoportal/c2cgeoportal_geoportal/__init__.py +++ b/geoportal/c2cgeoportal_geoportal/__init__.py @@ -30,6 +30,7 @@ import time import logging import mimetypes +import os import binascii from urllib.parse import urlsplit @@ -772,6 +773,12 @@ def init_dbsessions(settings: dict, config: Configurator, health_check: HealthCh for name, session in c2cgeoportal_commons.models.DBSessions.items(): if name == 'dbsession': health_check.add_db_session_check(session, at_least_one_model=main.Theme) + alembic_ini = os.path.join(os.path.abspath(os.path.curdir), 'alembic.ini') + if os.path.exists(alembic_ini): + health_check.add_alembic_check(session, alembic_ini_path=alembic_ini, name='main', + version_schema=settings['schema']) + health_check.add_alembic_check(session, alembic_ini_path=alembic_ini, name='static', + version_schema=settings['schema_static']) else: # pragma: no cover def check(session: Session) -> None: session.execute('SELECT 1') From 8c3348137684c7e63dcbd215e634927bff293545 Mon Sep 17 00:00:00 2001 From: Patrick Valsecchi Date: Thu, 14 Jun 2018 09:46:44 +0200 Subject: [PATCH 28/92] Freeze the haproxy version to 1.8.8 The 1.8.9 version has a bug and uses 100% CPU all the time. --- .../scaffolds/create/docker-compose.yaml.mako | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako index 26749019a6..3051f1cb78 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako @@ -76,7 +76,7 @@ ${service_defaults('geoportal', 8080)}\ ${service_defaults('geoportal', 80)}\ front: - image: haproxy:1.8 + image: haproxy:1.8.8 volumes_from: - config:ro volumes: From 8ccfbe02857a80f79a3039c399b7e74020726276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Thu, 14 Jun 2018 12:06:53 +0200 Subject: [PATCH 29/92] Fix the project upgrade file --- .../scaffolds/update/+dot+upgrade.yaml_tmpl | 2 +- travis/old-project.yaml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl index 6a3cc44dee..b097329b22 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl @@ -14,7 +14,7 @@ default_project_file: - geoportal/{{package}}_geoportal/jsbuild/.* - print/print-apps/.* - mapserver/.* - - tilegeneration/config\.yaml\.mako + - tilegeneration/config\.yaml\.tmpl\.mako - project\.yaml\.mako - docker-compose\.yaml\.mako - setup\.py diff --git a/travis/old-project.yaml b/travis/old-project.yaml index 88d65d86fd..47c640024a 100644 --- a/travis/old-project.yaml +++ b/travis/old-project.yaml @@ -16,6 +16,7 @@ unmanaged_files: - docker-compose\.yaml\.mako - testdb/11-schemas\.sql\_mako - tilegeneration/config\.yaml\.mako + - tilegeneration/config\.yaml\.tmpl\.mako - mapserver/mapserver\.map\.mako - mapserver/mapserver\.map\.tmpl\.mako - print/print-apps/testgeomapfish/config\.yaml\.mako From 6ac9df6df8de5a77d5b7b039e5e29b2f6bbed14b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Wed, 13 Jun 2018 09:53:23 +0200 Subject: [PATCH 30/92] Add Webpack dev server integration --- doc/developer/debugging.rst | 13 +++++ geoportal/c2cgeoportal_geoportal/__init__.py | 3 + .../scaffolds/create/+dot+gitignore_tmpl | 5 +- .../create/docker-compose-dev.yaml.mako | 22 ++++++++ .../scaffolds/create/docker-compose.yaml.mako | 1 + .../create/geoportal/+dot+dockerignore_tmpl | 2 + .../create/geoportal/Dockerfile.mako | 3 + .../{ => geoportal}/webpack.apps.js.mako_tmpl | 12 ++-- .../create/{ => geoportal}/webpack.config.js | 0 .../nondockercreate/+dot+gitignore_tmpl | 5 +- .../{ => geoportal}/webpack.apps.js.mako_tmpl | 6 +- .../nondockerupdate/+dot+upgrade.yaml_tmpl | 6 ++ .../scaffolds/update/+dot+upgrade.yaml_tmpl | 6 ++ .../scaffolds/update/CONST_Makefile_tmpl | 15 ++--- .../scaffolds/update/CONST_vars.yaml_tmpl | 3 + geoportal/c2cgeoportal_geoportal/views/dev.py | 56 +++++++++++++++++++ .../c2cgeoportal_geoportal/views/proxy.py | 8 +-- 17 files changed, 142 insertions(+), 24 deletions(-) create mode 100644 geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose-dev.yaml.mako rename geoportal/c2cgeoportal_geoportal/scaffolds/create/{ => geoportal}/webpack.apps.js.mako_tmpl (72%) rename geoportal/c2cgeoportal_geoportal/scaffolds/create/{ => geoportal}/webpack.config.js (100%) rename geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/{ => geoportal}/webpack.apps.js.mako_tmpl (84%) create mode 100644 geoportal/c2cgeoportal_geoportal/views/dev.py diff --git a/doc/developer/debugging.rst b/doc/developer/debugging.rst index dcab2f79c9..68b7901a1b 100644 --- a/doc/developer/debugging.rst +++ b/doc/developer/debugging.rst @@ -107,6 +107,19 @@ Add in the ``docker-compose.yaml`` file, in the ``geoportal`` service the follow - /geoportal/c2cgeoportal_geoportal:/opt/c2cgeoportal_geoportal/c2cgeoportal_geoportal - /geoportal/c2cgeoportal_admin:/opt/c2cgeoportal_geoportal/c2cgeoportal_admin +Use Webpack dev server +...................... + +In the file ``docker-compose-dev.yaml`` set the ``INTERFACE`` to the wanted value. + +Run: + +.. prompt:: bash + + docker-compose --file=docker-compose.yaml --file=docker-compose-dev.yaml up + +Open the application with on the following path: ``https:////dev/.html``. + Performance or network error ---------------------------- diff --git a/geoportal/c2cgeoportal_geoportal/__init__.py b/geoportal/c2cgeoportal_geoportal/__init__.py index 679a37f995..1aada5e074 100644 --- a/geoportal/c2cgeoportal_geoportal/__init__.py +++ b/geoportal/c2cgeoportal_geoportal/__init__.py @@ -730,6 +730,9 @@ def handle(event: InvalidateCacheEvent): # Resource proxy (load external url, useful when loading non https content) config.add_route("resourceproxy", "/resourceproxy", request_method="GET") + # Dev + config.add_route("dev", "/dev/*path", request_method="GET") + # Scan view decorator for adding routes config.scan(ignore=["c2cgeoportal_geoportal.scripts", "c2cgeoportal_geoportal.wsgi_app"]) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+gitignore_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+gitignore_tmpl index 0d8e4b8469..60bec1db09 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+gitignore_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+gitignore_tmpl @@ -5,6 +5,7 @@ __pycache__/ /project.yaml /docker-compose.yaml /docker-compose-build.yaml +/docker-compose-dev.yaml /geoportal/alembic.yaml /testdb/11-schemas.sql /testdb/12-alembic.sql @@ -31,6 +32,6 @@ __pycache__/ /geoportal/{{package}}_geoportal/static-ngeo/fonts/gmf-icons.* /geoportal/{{package}}_geoportal/static-ngeo/fonts/fontawesome-webfont.* /geoportal/{{package}}_geoportal/static/build/ -/node_modules -/webpack.apps.js +/geoportal/node_modules +/geoportal/webpack.apps.js /.upgrade.yaml diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose-dev.yaml.mako b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose-dev.yaml.mako new file mode 100644 index 0000000000..50410b0a57 --- /dev/null +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose-dev.yaml.mako @@ -0,0 +1,22 @@ +<%namespace file="CONST.mako_inc" import="service_defaults"/>\ +--- + +# The project Docker compose file for development. + +version: '2' + +services: + + webpack-dev-server: + image: ${docker_base}-geoportal:${docker_tag} + volumes: + - ${project_directory}/geoportal/${package}_geoportal/static-ngeo:/app/${package}_geoportal/static-ngeo + environment: + - INTERFACE=desktop + command: + - webpack-dev-server + - --mode=development + - --port=8080 + - --debug + - --watch + - --progress diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako index 26749019a6..210430977f 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako @@ -4,6 +4,7 @@ # The project Docker compose file for development. version: '2' + services: config: image: ${docker_base}-config:${docker_tag} diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/+dot+dockerignore_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/+dot+dockerignore_tmpl index 55209ccd93..c85e4f4d5e 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/+dot+dockerignore_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/+dot+dockerignore_tmpl @@ -1,6 +1,8 @@ * !alembic.ini !alembic.yaml +!webpack.apps.js +!webpack.config.js !config.yaml !eval_templates.sh !production.ini diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/Dockerfile.mako b/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/Dockerfile.mako index 6ace9218b4..5aa28ada1d 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/Dockerfile.mako +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/Dockerfile.mako @@ -20,6 +20,9 @@ ENV NODE_PATH=/usr/lib/node_modules \ WORKDIR /app COPY . /app +RUN mv webpack.apps.js webpack.apps.js.tmpl && \ + ln --symbolic /usr/lib/node_modules/ . + ARG GIT_HASH RUN pip install --disable-pip-version-check --no-cache-dir --no-deps --editable=/app/ && \ diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/webpack.apps.js.mako_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/webpack.apps.js.mako_tmpl similarity index 72% rename from geoportal/c2cgeoportal_geoportal/scaffolds/create/webpack.apps.js.mako_tmpl rename to geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/webpack.apps.js.mako_tmpl index 5773f553f8..fc4d3f0855 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/webpack.apps.js.mako_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/webpack.apps.js.mako_tmpl @@ -17,11 +17,11 @@ process.traceDeprecation = true; const name = process.env.INTERFACE; process.env.THEME = INTERFACE_THEME[name]; -entry[name] = path.resolve(__dirname, 'geoportal/${package}_geoportal/static-ngeo/js/apps/Controller' + name + '.js'); +entry[name] = '${package}/apps/Controller' + name + '.js'; plugins.push( new HtmlWebpackPlugin({ inject: false, - template: '/src/geoportal/${package}_geoportal/static-ngeo/js/apps/' + name + '.html.ejs', + template: path.resolve(__dirname, '${package}_geoportal/static-ngeo/js/apps/' + name + '.html.ejs'), chunksSortMode: 'manual', filename: name + '.html', chunks: [name], @@ -41,7 +41,7 @@ const babelPresets = [['env',{ // Transform code to ES2015 and annotate injectable functions with an $inject array. const projectRule = { - test: /geoportal\/{{package}}_geoportal\/static-ngeo\/js\/.*\.js$/, + test: /{{package}}_geoportal\/static-ngeo\/js\/.*\.js$/, use: { loader: 'babel-loader', options: { @@ -60,8 +60,8 @@ devServer = dev && !noDevServer; module.exports = { output: { - path: path.resolve(__dirname, 'geoportal/{{package}}_geoportal/static-ngeo/build/'), - publicPath: devServer ? '/${instance}/dev/' : '${entry_point}static-ngeo/UNUSED_CACHE_VERSION/build/' + path: path.resolve(__dirname, '{{package}}_geoportal/static-ngeo/build/'), + publicPath: devServer ? '${entry_point}dev/' : '${entry_point}static-ngeo/UNUSED_CACHE_VERSION/build/' }, entry: entry, module: { @@ -70,7 +70,7 @@ module.exports = { plugins: plugins, resolve: { alias: { - {{package}}: path.resolve(__dirname, 'geoportal/{{package}}_geoportal/static-ngeo/js'), + {{package}}: path.resolve(__dirname, '{{package}}_geoportal/static-ngeo/js'), } } }; diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/webpack.config.js b/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/webpack.config.js similarity index 100% rename from geoportal/c2cgeoportal_geoportal/scaffolds/create/webpack.config.js rename to geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/webpack.config.js diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/+dot+gitignore_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/+dot+gitignore_tmpl index 8baa5065fe..ae2cf80c48 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/+dot+gitignore_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/+dot+gitignore_tmpl @@ -6,6 +6,7 @@ __pycache__/ /project.yaml /docker-compose.yaml /docker-compose-build.yaml +/docker-compose-dev.yaml /admin/ /geoportal/alembic.yaml /testdb/11-schemas.sql @@ -54,11 +55,11 @@ __pycache__/ /geoportal/{{package}}_geoportal/static/mobile/openlayers-mobile.js /geoportal/{{package}}_geoportal/static/mobile/bootstrap.js /geoportal/{{package}}_geoportal/static/mobile/bootstrap.json +/geoportal/node_modules +/geoportal/webpack.apps.js /c2cgeoportal_commons/ /c2cgeoportal_geoportal/ /c2cgeoportal_admin/ /npm-packages -/node_modules /package-lock.json /.upgrade.yaml -/webpack.apps.js diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/webpack.apps.js.mako_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/geoportal/webpack.apps.js.mako_tmpl similarity index 84% rename from geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/webpack.apps.js.mako_tmpl rename to geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/geoportal/webpack.apps.js.mako_tmpl index 85963c988b..0bfca17a64 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/webpack.apps.js.mako_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/geoportal/webpack.apps.js.mako_tmpl @@ -17,7 +17,7 @@ process.traceDeprecation = true; const name = process.env.INTERFACE; process.env.THEME = INTERFACE_THEME[name]; -entry[name] = path.resolve(__dirname, 'geoportal/${package}_geoportal/static-ngeo/js/apps/Controller' + name + '.js'); +entry[name] = path.resolve(__dirname, '${package}_geoportal/static-ngeo/js/apps/Controller' + name + '.js'); plugins.push( new HtmlWebpackPlugin({ inject: false, @@ -38,7 +38,7 @@ const babelPresets = [['env',{ // Transform code to ES2015 and annotate injectable functions with an $inject array. const projectRule = { - test: /geoportal\/{{package}}_geoportal\/static-ngeo\/js\/.*\.js$/, + test: /{{package}}_geoportal\/static-ngeo\/js\/.*\.js$/, use: { loader: 'babel-loader', options: { @@ -55,7 +55,7 @@ const rules = [ module.exports = { output: { - path: path.resolve(__dirname, 'geoportal/{{package}}_geoportal/static-ngeo/build/'), + path: path.resolve(__dirname, '{{package}}_geoportal/static-ngeo/build/'), publicPath: dev ? '/${instanceid}/dev/' : '/${instanceid}/wsgi/static-ngeo/UNUSED_CACHE_VERSION/build/' }, entry: entry, diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockerupdate/+dot+upgrade.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockerupdate/+dot+upgrade.yaml_tmpl index 8ecbf99ae0..2c4b3d560d 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockerupdate/+dot+upgrade.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockerupdate/+dot+upgrade.yaml_tmpl @@ -170,3 +170,9 @@ files_to_move: - from: alembic.ini to: geoportal/alembic.ini version: 2.3 + - from: webpack.config.js + to: geoportal/webpack.config.js + version: 2.3 + - from: webpack.apps.js.mako + to: geoportal/webpack.apps.js.mako + version: 2.3 diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl index 6a3cc44dee..394a7587dc 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl @@ -207,3 +207,9 @@ files_to_move: - from: alembic.ini to: geoportal/alembic.ini version: 2.3 + - from: webpack.config.js + to: geoportal/webpack.config.js + version: 2.3 + - from: webpack.apps.js.mako + to: geoportal/webpack.apps.js.mako + version: 2.3 diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl index fb650f57e5..bcdbaf6e0e 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl @@ -85,7 +85,8 @@ DEFAULT_BUILD_RULES ?= docker-build-geoportal \ geoportal/alembic.ini \ geoportal/alembic.yaml \ docker-compose.yaml \ - docker-compose-build.yaml + docker-compose-build.yaml \ + docker-compose-dev.yaml # TileCloud-chain WMTSCAPABILITIES_PATH ?= 1.0.0/WMTSCapabilities-$(ENVIRONMENT).xml @@ -321,7 +322,7 @@ CONFIG_VARS += sqlalchemy.url sqlalchemy_slave.url schema schema_static enable_a tiles_url checker check_collector default_max_age jsbuild package srid \ reset_password fulltextsearch headers authorized_referers hooks stats db_chooser \ ogcproxy_enable dbsessions urllogin host_forward_host smtp c2c.base_path welcome_email host \ - lingua_extractor interfaces_config interfaces default_interface + lingua_extractor interfaces_config interfaces default_interface devserver_url MAKE_FILES = $(wildcard *.mk) CONST_Makefile # Secrets @@ -589,20 +590,20 @@ $(APP_OUTPUT_DIR)/images/: /usr/lib/node_modules/jquery-ui/themes/base/images .PRECIOUS: /build/apps.timestamp -/build/apps.timestamp: webpack.apps.js $(addprefix /build/apps., $(addsuffix .timestamp, $(NGEO_INTERFACES))) +/build/apps.timestamp: geoportal/webpack.apps.js $(addprefix /build/apps., $(addsuffix .timestamp, $(NGEO_INTERFACES))) touch $@ .PRECIOUS: /build/apps.%.timestamp /build/apps.%.timestamp: $(OL_JS_FILES) $(NGEO_JS_FILES) $(APP_JS_FILES) \ - $(APP_HTML_FILES) webpack.config.js \ + $(APP_HTML_FILES) geoportal/webpack.config.js \ $(LESS_FILES) \ $(NGEO_FONTS) \ $(APP_OUTPUT_DIR)/images/ $(PRERULE_CMD) # Workaround to make Webpack working for ol/index.js - ln --symbolic /usr/lib/node_modules/ . - INTERFACE=$* webpack $(WEBPACK_ARGS) - rm node_modules + ln --symbolic /usr/lib/node_modules/ geoportal/ + (cd geoportal; INTERFACE=$* webpack $(WEBPACK_ARGS)) + rm geoportal/node_modules mv $(APP_OUTPUT_DIR)/$*.html $(APP_OUTPUT_DIR)/$*.html.tmpl ls -1 $(APP_OUTPUT_DIR)/$*.*.css|while read file; do mv $${file} $${file}.tmpl; done touch $@ diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl index 87fc204f5a..8fc4ddc330 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl @@ -368,6 +368,7 @@ vars: print_url: '{PRINT_URL}' mapserver_url: '{MAPSERVER_URL}' mapcache_url: '{MAPCACHE_URL}' + devserver_url: '{DEVSERVER_URL}' memcached_host: '{MEMCACHED_HOST}' memcached_port: '{MEMCACHED_PORT}' tilegeneration_sqs_queue: '{TILEGENERATION_SQS_QUEUE}' @@ -553,6 +554,8 @@ runtime_environment: default: http://print:8080/print/ - name: MAPCACHE_URL default: http://mapcache/ + - name: DEVSERVER_URL + default: http://webpack-dev-server:8080 - name: REDIS_HOST default: redis - name: REDIS_PORT diff --git a/geoportal/c2cgeoportal_geoportal/views/dev.py b/geoportal/c2cgeoportal_geoportal/views/dev.py new file mode 100644 index 0000000000..99faa183b9 --- /dev/null +++ b/geoportal/c2cgeoportal_geoportal/views/dev.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2013-2018, Camptocamp SA +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: + +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# The views and conclusions contained in the software and documentation are those +# of the authors and should not be interpreted as representing official policies, +# either expressed or implied, of the FreeBSD Project. + + +from c2cgeoportal_geoportal.views.proxy import Proxy +import logging +from pyramid.httpexceptions import HTTPFound +from pyramid.view import view_config +import re + +logger = logging.getLogger(__name__) + + +class Dev(Proxy): + + THEME_RE = re.compile(r'/theme/.*$') + + def __init__(self, request): + super().__init__(request) + self.dev_url = self.request.registry.settings['devserver_url'] + + @view_config(route_name='dev') + def dev(self): + path = self.THEME_RE.sub('', self.request.path) + if self.request.path.endswith('/dynamics.js'): + return HTTPFound(location=self.request.route_url('dynamic')) + else: + return self._proxy_response('dev', "{}/{}".format( + self.dev_url.rstrip('/'), path.lstrip('/') + )) diff --git a/geoportal/c2cgeoportal_geoportal/views/proxy.py b/geoportal/c2cgeoportal_geoportal/views/proxy.py index c9945be23a..2a8e19d103 100644 --- a/geoportal/c2cgeoportal_geoportal/views/proxy.py +++ b/geoportal/c2cgeoportal_geoportal/views/proxy.py @@ -38,7 +38,7 @@ from urllib.parse import urlparse, parse_qs from pyramid.response import Response -from pyramid.httpexceptions import HTTPBadGateway, HTTPInternalServerError +from pyramid.httpexceptions import HTTPBadGateway, exception_response from c2cgeoportal_geoportal.lib.caching import get_region, \ set_common_headers, NO_CACHE, PUBLIC_CACHE, PRIVATE_CACHE @@ -127,9 +127,9 @@ def _proxy(self, url, params=None, method=None, cache=False, body=None, headers= args.append(body.decode("utf-8")) log.error("\n".join(errors), *args, exc_info=True) - raise HTTPBadGateway("Error on backend
See logs for detail") + raise HTTPBadGateway("Error on backend, See logs for detail") - if resp.status < 200 or resp.status >= 300: # pragma: no cover + if resp.status >= 300: # pragma: no cover errors = [ "Error '%s' in response of URL:", "%s", @@ -155,7 +155,7 @@ def _proxy(self, url, params=None, method=None, cache=False, body=None, headers= args.append(content.decode("utf-8")) log.error("\n".join(errors), *args) - raise HTTPInternalServerError("See logs for details") + raise exception_response(resp.status) return resp, content From d15b57a7a547d7b19872cc6551c91727dd0c8482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Wed, 13 Jun 2018 14:06:00 +0200 Subject: [PATCH 31/92] Parse the WMS capabilities less times. One time per process and OGC server, Not one time per layers. Fix the CI --- Jenkinsfile | 5 ++--- .../create/docker-compose-build.yaml.mako | 2 +- .../scaffolds/update/CONST.mako_inc | 5 +++++ .../scaffolds/update/CONST_vars.yaml_tmpl | 13 ++++++++---- .../scripts/create_demo_theme.py | 4 +++- .../c2cgeoportal_geoportal/views/entry.py | 20 +++++++++++++++++-- travis/create-new-nondocker-project.sh | 2 +- travis/create-new-project.sh | 2 -- travis/docker-compose-build.yaml.mako | 2 +- 9 files changed, 40 insertions(+), 15 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index c6b9b8a819..fa63a0ec8c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -96,8 +96,6 @@ dockerBuild { sh '(cd ${HOME}/workspace/testgeomapfish/; git commit -m "Upgrade the po files")' sh '(cd ${HOME}/workspace/testgeomapfish/; ./docker-run travis/empty-make --makefile=travis.mk help)' sh '(cd ${HOME}/workspace/testgeomapfish/; ./docker-run make --makefile=travis.mk build)' - sh 'cat ${HOME}/workspace/testgeomapfish/testdb/*.sql' - sh 'cat ${HOME}/workspace/testgeomapfish/geoportal/config.yaml' withCredentials([[ $class: 'UsernamePasswordMultiBinding', credentialsId: 'dockerhub', @@ -106,9 +104,10 @@ dockerBuild { ]]) { try { sh 'docker login -u "$USERNAME" -p "$PASSWORD"' - sh '(cd ${HOME}/workspace/testgeomapfish/; docker-compose up --force-recreate -d)' + sh '(cd ${HOME}/workspace/testgeomapfish/; docker-compose up -d)' timeout(time: 2, unit: 'MINUTES') { sh '(cd ${HOME}/workspace/testgeomapfish/; docker-compose exec -T geoportal wait-for-db)' + sh '(cd ${HOME}/workspace/testgeomapfish/; docker-compose exec -T geoportal create-demo-theme)' sh './docker-run travis/waitwsgi http://`netstat --route --numeric|grep ^0.0.0.0|awk \'{print($2)}\'`:8080/' } for (path in [ diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose-build.yaml.mako b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose-build.yaml.mako index 1295e9d3ea..b65c92e65c 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose-build.yaml.mako +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose-build.yaml.mako @@ -32,7 +32,7 @@ ${service_defaults('mapserver', 80)}\ - run links: - mapserver -${service_defaults('geoportal', 80)}\ +${service_defaults('geoportal-build', 80)}\ - HOME_DIR - USER_NAME - USER_ID diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST.mako_inc b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST.mako_inc index 445f007324..4c7b454b1f 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST.mako_inc +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST.mako_inc @@ -1,5 +1,10 @@ <%def name="service_defaults(service, inner_port=None, port_required=False)">\ % if port_required or 'port' in docker_services.get(service, {}): +<% +if 'port' not in docker_services[service]: + raise Exception("Required port is not configured for service {}.".format(service)) +endif +%> ports: - ${docker_services[service]['port']}:${inner_port} % endif diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl index 87fc204f5a..4988b46864 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl @@ -46,10 +46,8 @@ vars: POSTGRES_DB: geomapfish POSTGRES_USER: www-data POSTGRES_PASSWORD: www-data - geoportal: - environment: - VISIBLE_WEB_HOST: '{docker_host}' - VISIBLE_WEB_PROTOCOL: '{docker_web_protocol}' + geoportal-build: + environment: &geo-run-env VISIBLE_ENTRY_POINT: '{docker_entry_point}' PGHOST: 172.17.0.1 PGHOST_SLAVE: 172.17.0.1 @@ -59,11 +57,18 @@ vars: PGDATABASE: geomapfish PGSCHEMA: main PGSCHEMA_STATIC: main_static + geoportal: + environment: + <<: *geo-run-env + VISIBLE_WEB_HOST: '{docker_host}' + VISIBLE_WEB_PROTOCOL: '{docker_web_protocol}' TINYOWS_URL: http://tinyows/ MAPSERVER_URL: http://mapserver/ PRINT_URL: http://print:8080/print/ REDIS_HOST: redis REDIS_PORT: 6379 + C2C_REDIS_URL: redis://redis:6379 + C2C_BROADCAST_PREFIX: c2cgeoportal_ tilecloudchain: environment: VISIBLE_ENTRY_POINT: '{docker_entry_point}' diff --git a/geoportal/c2cgeoportal_geoportal/scripts/create_demo_theme.py b/geoportal/c2cgeoportal_geoportal/scripts/create_demo_theme.py index b3791bfd66..6b8f48d748 100644 --- a/geoportal/c2cgeoportal_geoportal/scripts/create_demo_theme.py +++ b/geoportal/c2cgeoportal_geoportal/scripts/create_demo_theme.py @@ -45,7 +45,7 @@ def main(): ) parser.add_argument( '-i', '--iniconfig', - default='geoportal/production.ini', + default='production.ini', help='project .ini config file' ) parser.add_argument( @@ -91,6 +91,8 @@ def main(): transaction.commit() + print("Successfully added the demo theme") + if __name__ == "__main__": main() diff --git a/geoportal/c2cgeoportal_geoportal/views/entry.py b/geoportal/c2cgeoportal_geoportal/views/entry.py index df3f457bb3..547b0469e6 100644 --- a/geoportal/c2cgeoportal_geoportal/views/entry.py +++ b/geoportal/c2cgeoportal_geoportal/views/entry.py @@ -49,13 +49,15 @@ from sqlalchemy.orm import subqueryload from sqlalchemy.orm.exc import NoResultFound from owslib.wms import WebMapService +from typing import Dict, Tuple, Set # noqa # pylint: disable=unused-import +import zope.event.classhandler from c2cgeoportal_commons import models from c2cgeoportal_commons.models import main, static from c2cgeoportal_geoportal.lib import get_setting, get_protected_layers_query, \ get_url2, get_url, get_typed, get_types_map, add_url_params from c2cgeoportal_geoportal.lib.cacheversion import get_cache_version -from c2cgeoportal_geoportal.lib.caching import get_region, invalidate_region, \ +from c2cgeoportal_geoportal.lib.caching import get_region, \ set_common_headers, NO_CACHE, PUBLIC_CACHE, PRIVATE_CACHE from c2cgeoportal_geoportal.lib.functionality import get_functionality, get_mapserver_substitution_params from c2cgeoportal_geoportal.lib.wmstparsing import parse_extent, TimeInformation @@ -115,6 +117,7 @@ class Entry: WFS_NS = "http://www.opengis.net/wfs" default_ogc_server = None external_ogc_server = None + server_wms_capabilities = {} # type: Dict[int, Tuple[WebMapService, Set[str]]] def __init__(self, request): self.request = request @@ -160,6 +163,13 @@ def __init__(self, request): ", ".join([i[0] for i in models.DBSession.query(main.OGCServer.name).all()]) ) + from c2cgeoportal_commons.models.main import InvalidateCacheEvent + + @zope.event.classhandler.handler(InvalidateCacheEvent) + def handle(event: InvalidateCacheEvent): + del event + Entry.server_wms_capabilities = {} + @view_config(route_name="testi18n", renderer="testi18n.html") def testi18n(self): # pragma: no cover _ = self.request.translate @@ -200,6 +210,9 @@ def _get_metadatas(self, item, errors): def _wms_getcap(self, ogc_server=None): ogc_server = (ogc_server or self.default_ogc_server) + if ogc_server.id in self.server_wms_capabilities: + return self.server_wms_capabilities[ogc_server.id] + url, content, errors = self._wms_getcap_cached( ogc_server, self._get_capabilities_cache_role_key(ogc_server) ) @@ -215,6 +228,9 @@ def _wms_getcap(self, ogc_server=None): "\nURL: {}\n{}".format(url, content) errors.add(error) log.error(error, exc_info=True) + + self.server_wms_capabilities[ogc_server.id] = (wms, errors) + return wms, errors @cache_region.cache_on_arguments() @@ -1008,7 +1024,7 @@ def _get_functionalities(theme): @staticmethod @view_config(route_name="invalidate", renderer="json") def invalidate_cache(): # pragma: no cover - invalidate_region() + main.cache_invalidate_cb() return { "success": True } diff --git a/travis/create-new-nondocker-project.sh b/travis/create-new-nondocker-project.sh index eb34badd12..7e2b08f89e 100755 --- a/travis/create-new-nondocker-project.sh +++ b/travis/create-new-nondocker-project.sh @@ -53,7 +53,7 @@ FINALISE=TRUE make --makefile=travis.mk build ./docker-run alembic --config=geoportal/alembic.ini --name=main upgrade head ./docker-run alembic --config=geoportal/alembic.ini --name=static upgrade head # Create default theme -./docker-run /build/venv/bin/python /usr/local/bin/create-demo-theme +./docker-run /build/venv/bin/python /usr/local/bin/create-demo-theme --iniconfig geoportal/production.ini ./docker-run make --makefile=travis.mk update-po git add geoportal/testgeomapfish_geoportal/locale/*/LC_MESSAGES/*.po git commit -m "Add initial localisation" diff --git a/travis/create-new-project.sh b/travis/create-new-project.sh index 6e21135ad4..4eac654dbd 100755 --- a/travis/create-new-project.sh +++ b/travis/create-new-project.sh @@ -29,7 +29,5 @@ git clean -fX # Build ./docker-run make --makefile=travis.mk build docker-build-testdb -# Create default theme -./docker-compose-run /build/venv/bin/python /usr/local/bin/create-demo-theme ./docker-compose-run make --makefile=travis.mk update-po ./docker-compose-run make --makefile=travis.mk theme2fts diff --git a/travis/docker-compose-build.yaml.mako b/travis/docker-compose-build.yaml.mako index bb0a8f635f..180e8c63b2 100644 --- a/travis/docker-compose-build.yaml.mako +++ b/travis/docker-compose-build.yaml.mako @@ -44,7 +44,7 @@ ${service_defaults('mapserver', 80)}\ - db - external-db - mapserver -${service_defaults('geoportal', 80, True)}\ +${service_defaults('geoportal-build', 80)}\ - HOME_DIR - USER_NAME - USER_ID From bc98a27562ae58687a7a8c49fdd175baa6f8ba51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Thu, 14 Jun 2018 16:16:56 +0200 Subject: [PATCH 32/92] Add some documentation to be able to have concurent instance on the same server --- doc/developer/debugging.rst | 82 ++++++++++++++++++- .../scaffolds/create/docker-compose.yaml.mako | 4 +- .../scaffolds/update/CONST.mako_inc | 4 +- 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/doc/developer/debugging.rst b/doc/developer/debugging.rst index 68b7901a1b..c28ac51180 100644 --- a/doc/developer/debugging.rst +++ b/doc/developer/debugging.rst @@ -78,6 +78,66 @@ Actually we display the running rule and why she is running (dependence update). Docker ------ +Multiple dev on one server +.......................... + +When you want to run multiple instances on the same server you should: + +- Use the global front +- Use a different docker tag for each instance +- Use a different project name for each instance + +Global front +............ + +The global front will offer a unique entry point on port 80 that provide the 'main' project on `/` and the +others on `//`. + +Activate it in the vars: + +.. code:: yaml + + vars: + docker_global_front: true + +Build the project: + +.. prompt:: bash + + ./docker-run make build + +Run the global front: + +.. prompt:: bash + + (cd global-front; docker-compose --project-name=global up --build) + + +And we should defined different instance name for the build: + +.. prompt:: bash + + INSTANCE= ./docker-run make build + + +Use a different docker tag +.......................... + +Just define an environment variable in the build: + +.. prompt:: bash + + DOCKER_TAG= ./docker-run make build + +Use a different project name +............................ + +Define the project name when you run the Docker composition: + +.. prompt:: bash + + docker-compose --project-name= ... + Run gunicorn to reload on modifications of Python files ....................................................... @@ -107,6 +167,27 @@ Add in the ``docker-compose.yaml`` file, in the ``geoportal`` service the follow - /geoportal/c2cgeoportal_geoportal:/opt/c2cgeoportal_geoportal/c2cgeoportal_geoportal - /geoportal/c2cgeoportal_admin:/opt/c2cgeoportal_geoportal/c2cgeoportal_admin +Expose a service +................ + +To expose a service out of the Docker composition you can add a port for the service in the vars, e.g.: + +.. code:: yaml + + vars: + docker_services: + : + port: 8086 + +Be careful one port can be open only one time on a server. +Within the Docker composition you can access a port of a container, you can achieve this via curl. +This way, you do not need to publish this port on the main host. + +.. prompt:: bash + + docker-compose exec geoportal bash + curl "" + Use Webpack dev server ...................... @@ -120,7 +201,6 @@ Run: Open the application with on the following path: ``https:////dev/.html``. - Performance or network error ---------------------------- diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako index e4264f3154..a85847534a 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako @@ -74,7 +74,7 @@ ${service_defaults('geoportal', 8080)}\ - --name=static - upgrade - head -${service_defaults('geoportal', 80)}\ +${service_defaults('geoportal')}\ front: image: haproxy:1.8.8 @@ -86,7 +86,7 @@ ${service_defaults('geoportal', 80)}\ - haproxy - -f - /etc/haproxy -${service_defaults('front', 80, not docker_global_front)} +${service_defaults('front', 80, True, docker_global_front)} %if docker_global_front: networks: default: {} diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST.mako_inc b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST.mako_inc index 445f007324..41481a735c 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST.mako_inc +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST.mako_inc @@ -1,8 +1,10 @@ -<%def name="service_defaults(service, inner_port=None, port_required=False)">\ +<%def name="service_defaults(service, inner_port=None, port_required=False, global_front=False)">\ +% if not global_front: % if port_required or 'port' in docker_services.get(service, {}): ports: - ${docker_services[service]['port']}:${inner_port} % endif +% endif % if 'environment' in docker_services.get(service, {}): environment: % for key, value in docker_services[service]['environment'].items(): From d006c0873443921efc9aa2ce11534f161f559ab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Fri, 15 Jun 2018 09:16:25 +0200 Subject: [PATCH 33/92] Little fix of the tilegeneration config --- .../create/tilegeneration/config.yaml.tmpl.mako_tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/tilegeneration/config.yaml.tmpl.mako_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/create/tilegeneration/config.yaml.tmpl.mako_tmpl index f0169fb4ef..9a21886033 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/tilegeneration/config.yaml.tmpl.mako_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/tilegeneration/config.yaml.tmpl.mako_tmpl @@ -20,7 +20,7 @@ caches: bucket: tiles folder: '' # for GetCapabilities - http_url: https://%(host)s/%(bucket)s/%(folder)s/ + http_url: ${web_protocol}://${host}${entry_point} cache_control: 'public, max-age=14400' hosts: - wmts0. @@ -59,7 +59,7 @@ defaults: # the meta tiles definition [default to off] meta: on # the meta tiles size [default to 8] - meta_size: 8 + meta_size: 5 # the meta tiles buffer [default to 128] meta_buffer: 128 # connexion an sql to get geometries (in column named geom) where we want to generate tiles From e27f1b9eb566d892bffc01b3c76b0e3233f68ce2 Mon Sep 17 00:00:00 2001 From: Patrick Valsecchi Date: Thu, 14 Jun 2018 13:39:03 +0200 Subject: [PATCH 34/92] Make the invalidate call broadcasted to every processes --- Makefile | 6 +++--- commons/c2cgeoportal_commons/models/main.py | 6 ++++++ geoportal/c2cgeoportal_geoportal/lib/check_collector.py | 1 + .../scaffolds/create/bin/eval-templates | 4 ++-- .../scaffolds/update/CONST_vars.yaml_tmpl | 4 +++- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 5a7a46e9e4..b14b8d151f 100644 --- a/Makefile +++ b/Makefile @@ -195,9 +195,9 @@ prepare-tests: \ .PHONY: tests tests: - py.test --color=yes --cov=commons/c2cgeoportal_commons commons/acceptance_tests - py.test --color=yes --cov-append --cov=geoportal/c2cgeoportal_geoportal geoportal/tests - py.test --color=yes --cov-append --cov=admin/c2cgeoportal_admin admin/acceptance_tests + py.test --verbose --color=yes --cov=commons/c2cgeoportal_commons commons/acceptance_tests + py.test --verbose --color=yes --cov-append --cov=geoportal/c2cgeoportal_geoportal geoportal/tests + py.test --verbose --color=yes --cov-append --cov=admin/c2cgeoportal_admin admin/acceptance_tests .PHONY: flake8 flake8: diff --git a/commons/c2cgeoportal_commons/models/main.py b/commons/c2cgeoportal_commons/models/main.py index 59c18984cc..628b62eeac 100644 --- a/commons/c2cgeoportal_commons/models/main.py +++ b/commons/c2cgeoportal_commons/models/main.py @@ -41,6 +41,7 @@ from sqlalchemy.orm import relationship, backref, Session from geoalchemy2 import Geometry from geoalchemy2.shape import to_shape +from c2cwsgiutils import broadcast import zope.event import colander @@ -63,6 +64,11 @@ class InvalidateCacheEvent: def cache_invalidate_cb(*args: List[Any]) -> None: + _cache_invalidate_cb() + + +@broadcast.decorator() +def _cache_invalidate_cb() -> None: zope.event.notify(InvalidateCacheEvent()) diff --git a/geoportal/c2cgeoportal_geoportal/lib/check_collector.py b/geoportal/c2cgeoportal_geoportal/lib/check_collector.py index 2566c5b457..907801c837 100644 --- a/geoportal/c2cgeoportal_geoportal/lib/check_collector.py +++ b/geoportal/c2cgeoportal_geoportal/lib/check_collector.py @@ -56,6 +56,7 @@ def check(request): ) r = requests.get(params={"max_level": str(host.get("max_level", max_level))}, **url_headers) r.raise_for_status() + return r.json() health_check.add_custom_check( name="check_collector_" + host["display"], diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/bin/eval-templates b/geoportal/c2cgeoportal_geoportal/scaffolds/create/bin/eval-templates index 81a38d96ee..7a15e27972 100755 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/bin/eval-templates +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/bin/eval-templates @@ -1,7 +1,7 @@ #!/bin/bash -e -export VISIBLE_WEB_HOST_RE_ESCAPED=`python -c "print(__import__('re').escape('${VISIBLE_WEB_HOST}'))"` -export VISIBLE_ENTRY_POINT_RE_ESCAPED=`python -c "print(__import__('re').escape('${VISIBLE_ENTRY_POINT}'))"` +export VISIBLE_WEB_HOST_RE_ESCAPED=`python3 -c "print(__import__('re').escape('${VISIBLE_WEB_HOST}'))"` +export VISIBLE_ENTRY_POINT_RE_ESCAPED=`python3 -c "print(__import__('re').escape('${VISIBLE_ENTRY_POINT}'))"` find /etc /mapcache /usr/local/tomcat/webapps/ -name '*.tmpl' -print | while read file do diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl index cf5f9d7784..250d0b334e 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl @@ -68,12 +68,14 @@ vars: REDIS_HOST: redis REDIS_PORT: 6379 C2C_REDIS_URL: redis://redis:6379 - C2C_BROADCAST_PREFIX: c2cgeoportal_ + C2C_BROADCAST_PREFIX: broadcast_geoportal_ tilecloudchain: environment: VISIBLE_ENTRY_POINT: '{docker_entry_point}' TILEGENERATION_CONFIGFILE: /etc/tilegeneration/config.yaml C2C_BASE_PATH: /c2c_tiles + C2C_REDIS_URL: redis://redis:6379 + C2C_BROADCAST_PREFIX: broadcast_tilecloudchain_ front: port: 80 From 3f6631d05d10e8293debbf0e495ffdb897d8fa0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Fri, 15 Jun 2018 15:51:45 +0200 Subject: [PATCH 35/92] Add tinyows service to Docker composition Remove last confd usages --- .../c2cgeoportal_geoportal/lib/__init__.py | 19 ------------------- .../scaffolds/create/+dot+gitignore_tmpl | 3 ++- .../scaffolds/create/docker-compose.yaml.mako | 6 ++++++ .../create/mapserver/demo.map.tmpl.mako_tmpl | 6 +----- .../mapserver/mapserver.map.tmpl.mako_tmpl | 6 +----- ...tinyows.xml.mako => tinyows.xml.tmpl.mako} | 2 +- .../nondockercreate/+dot+gitignore_tmpl | 1 + .../nondockerupdate/+dot+upgrade.yaml_tmpl | 4 +++- .../scaffolds/update/+dot+upgrade.yaml_tmpl | 4 +++- travis/old-project.yaml | 2 ++ travis/test-upgrade-convert.sh | 2 +- travis/v23-project.yaml | 2 ++ 12 files changed, 23 insertions(+), 34 deletions(-) rename geoportal/c2cgeoportal_geoportal/scaffolds/create/mapserver/{tinyows.xml.mako => tinyows.xml.tmpl.mako} (90%) diff --git a/geoportal/c2cgeoportal_geoportal/lib/__init__.py b/geoportal/c2cgeoportal_geoportal/lib/__init__.py index 9880704cbb..a38d64d929 100644 --- a/geoportal/c2cgeoportal_geoportal/lib/__init__.py +++ b/geoportal/c2cgeoportal_geoportal/lib/__init__.py @@ -400,22 +400,3 @@ def add(self, config, name, spec, **extra): _formatter = Formatter() - - -def confd_env(val): - """ - This is a Mako filter build used in conjunction with c2c.template to create confd TOML files. - I we put in the vars file: - ``` - vars: - host: '{HOST}' - runtime_environment: - - HOST - ``` - `${host|confd_env}`will be transformed as `{{getenv "HOST"}}` - """ - replacements = {} - for _, attr, _, _ in _formatter.parse(val): - if attr is not None: - replacements[attr] = '{{getenv "' + attr + '"}}' - return val.format(**replacements) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+gitignore_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+gitignore_tmpl index 60bec1db09..1ec84d1038 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+gitignore_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+gitignore_tmpl @@ -16,7 +16,8 @@ __pycache__/ /print/print-apps/{{package}}/config.yaml.mako /mapserver/mapserver.map.tmpl /mapserver/*.map.tmpl -/mapserver/tinyows.xml +/mapserver/tinyows.xml.mako +/mapserver/tinyows.xml.tmpl /geoportal/jsbuild/app.cfg /geoportal/Dockerfile /geoportal/development.ini diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako index a85847534a..55bccd451d 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako @@ -31,6 +31,12 @@ ${service_defaults('mapserver', 80)}\ ## - config:ro ##${service_defaults('qgisserver', 80)} + tinyows: + image: camptocamp/tinyows + volumes_from: + - config:ro +${service_defaults('tinyows', 80)}\ + mapcache: image: camptocamp/mapcache:1.6 volumes_from: diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/mapserver/demo.map.tmpl.mako_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/create/mapserver/demo.map.tmpl.mako_tmpl index 916d7315ec..e292894004 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/mapserver/demo.map.tmpl.mako_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/mapserver/demo.map.tmpl.mako_tmpl @@ -1,9 +1,5 @@ # This file is used to render the examples layers, he can be deleted. -<%! -from c2cgeoportal_geoportal.lib import confd_env -%> - # Europe Layer LAYER NAME "borders" @@ -15,7 +11,7 @@ LAYER EXTENT -31 27 45 71 # Useful for better performance but not mandatory CONNECTIONTYPE postgis PROCESSING "CLOSE_CONNECTION=DEFER" # For performance - CONNECTION "${mapserver_connection | confd_env}" + CONNECTION "${mapserver_connection}" DATA "geom FROM (SELECT geo.* FROM data.europe_borders as geo) as foo using unique gid using srid=4326" PROJECTION "init=epsg:4326" diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/mapserver/mapserver.map.tmpl.mako_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/create/mapserver/mapserver.map.tmpl.mako_tmpl index 898acc7feb..ab267134ba 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/mapserver/mapserver.map.tmpl.mako_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/mapserver/mapserver.map.tmpl.mako_tmpl @@ -13,10 +13,6 @@ # /mapserv?service=wms&version=1.1.1&request=getfeatureinfo&bbox=-180,-90,180,90&layers=countries&query_layers=countries&width=600&height=400&srs=EPSG:4326&format=image/png&x=180&y=90&info_format=application/vnd.ogc.gml # -<%! -from c2cgeoportal_geoportal.lib import confd_env -%> - MAP NAME "{{package}}" @@ -74,7 +70,7 @@ MAP METADATA "wms_title" "changeme" "wms_abstract" "changeme" - "wms_onlineresource" "http://${host | confd_env}${entry_point | confd_env}/mapserv_proxy" + "wms_onlineresource" "${web_protocol}://${host}${entry_point}mapserv_proxy" "wms_srs" "EPSG:{{srid}}" "wms_encoding" "UTF-8" "wms_enable_request" "*" diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/mapserver/tinyows.xml.mako b/geoportal/c2cgeoportal_geoportal/scaffolds/create/mapserver/tinyows.xml.tmpl.mako similarity index 90% rename from geoportal/c2cgeoportal_geoportal/scaffolds/create/mapserver/tinyows.xml.mako rename to geoportal/c2cgeoportal_geoportal/scaffolds/create/mapserver/tinyows.xml.tmpl.mako index 6754bc177f..fdad87d5b2 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/mapserver/tinyows.xml.mako +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/mapserver/tinyows.xml.tmpl.mako @@ -1,5 +1,5 @@ Date: Mon, 18 Jun 2018 09:38:31 +0200 Subject: [PATCH 36/92] Use print version 3.14 in Docker mode --- .../scaffolds/create/docker-compose.yaml.mako | 2 +- travis/docker-compose.yaml.mako | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako index a85847534a..fc90584f6e 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako @@ -11,7 +11,7 @@ services: ${service_defaults('config')}\ print: - image: camptocamp/mapfish_print:3.12.1 + image: camptocamp/mapfish_print:3.14 volumes_from: - config:ro ${service_defaults('print', 8080)}\ diff --git a/travis/docker-compose.yaml.mako b/travis/docker-compose.yaml.mako index 15a31ba5be..18d4b16634 100644 --- a/travis/docker-compose.yaml.mako +++ b/travis/docker-compose.yaml.mako @@ -17,7 +17,7 @@ ${service_defaults('db', 5432)}\ ${service_defaults('external-db', 5432)}\ print: - image: camptocamp/mapfish_print:3.12.1 + image: camptocamp/mapfish_print:3.14 volumes_from: - config:ro ${service_defaults('print', 8080)}\ From e54a08d8b02e282a86c03e9cc620289327084e8f Mon Sep 17 00:00:00 2001 From: Patrick Valsecchi Date: Mon, 18 Jun 2018 10:33:22 +0200 Subject: [PATCH 37/92] Improve perfs by using ujson We were spending the thrird of our time generting json. Now it's near 0%. --- admin/c2cgeoportal_admin/views/functionalities.py | 4 ++-- admin/c2cgeoportal_admin/views/interfaces.py | 4 ++-- admin/c2cgeoportal_admin/views/layer_groups.py | 4 ++-- admin/c2cgeoportal_admin/views/layers_v1.py | 4 ++-- admin/c2cgeoportal_admin/views/layers_wms.py | 6 +++--- admin/c2cgeoportal_admin/views/layers_wmts.py | 6 +++--- admin/c2cgeoportal_admin/views/layertree.py | 6 +++--- admin/c2cgeoportal_admin/views/ogc_servers.py | 4 ++-- admin/c2cgeoportal_admin/views/restriction_areas.py | 4 ++-- admin/c2cgeoportal_admin/views/roles.py | 4 ++-- admin/c2cgeoportal_admin/views/themes.py | 4 ++-- admin/c2cgeoportal_admin/views/users.py | 4 ++-- geoportal/c2cgeoportal_geoportal/__init__.py | 2 +- .../scaffolds/create/geoportal/webpack.apps.js.mako_tmpl | 2 +- .../nondockercreate/geoportal/webpack.apps.js.mako_tmpl | 2 +- 15 files changed, 30 insertions(+), 30 deletions(-) diff --git a/admin/c2cgeoportal_admin/views/functionalities.py b/admin/c2cgeoportal_admin/views/functionalities.py index cd71c60990..a82cd24a28 100644 --- a/admin/c2cgeoportal_admin/views/functionalities.py +++ b/admin/c2cgeoportal_admin/views/functionalities.py @@ -31,7 +31,7 @@ def index(self): return super().index() @view_config(route_name='c2cgeoform_grid', - renderer='json') + renderer='fast_json') def grid(self): return super().grid() @@ -49,7 +49,7 @@ def save(self): @view_config(route_name='c2cgeoform_item', request_method='DELETE', - renderer='json') + renderer='fast_json') def delete(self): return super().delete() diff --git a/admin/c2cgeoportal_admin/views/interfaces.py b/admin/c2cgeoportal_admin/views/interfaces.py index 3e91d7f2db..7f8ac78374 100644 --- a/admin/c2cgeoportal_admin/views/interfaces.py +++ b/admin/c2cgeoportal_admin/views/interfaces.py @@ -37,7 +37,7 @@ def index(self): return super().index() @view_config(route_name='c2cgeoform_grid', - renderer='json') + renderer='fast_json') def grid(self): return super().grid() @@ -55,7 +55,7 @@ def save(self): @view_config(route_name='c2cgeoform_item', request_method='DELETE', - renderer='json') + renderer='fast_json') def delete(self): return super().delete() diff --git a/admin/c2cgeoportal_admin/views/layer_groups.py b/admin/c2cgeoportal_admin/views/layer_groups.py index bf3a90d07d..7421c721ca 100644 --- a/admin/c2cgeoportal_admin/views/layer_groups.py +++ b/admin/c2cgeoportal_admin/views/layer_groups.py @@ -46,7 +46,7 @@ def index(self): return super().index() @view_config(route_name='c2cgeoform_grid', - renderer='json') + renderer='fast_json') def grid(self): return super().grid() @@ -64,7 +64,7 @@ def save(self): @view_config(route_name='c2cgeoform_item', request_method='DELETE', - renderer='json') + renderer='fast_json') def delete(self): return super().delete() diff --git a/admin/c2cgeoportal_admin/views/layers_v1.py b/admin/c2cgeoportal_admin/views/layers_v1.py index 7c2b7c3975..c2f1c00755 100644 --- a/admin/c2cgeoportal_admin/views/layers_v1.py +++ b/admin/c2cgeoportal_admin/views/layers_v1.py @@ -64,7 +64,7 @@ def index(self): return super().index() @view_config(route_name='c2cgeoform_grid', - renderer='json') + renderer='fast_json') def grid(self): return super().grid() @@ -82,7 +82,7 @@ def save(self): @view_config(route_name='c2cgeoform_item', request_method='DELETE', - renderer='json') + renderer='fast_json') def delete(self): return super().delete() diff --git a/admin/c2cgeoportal_admin/views/layers_wms.py b/admin/c2cgeoportal_admin/views/layers_wms.py index 5461c542d8..305ffb6a96 100644 --- a/admin/c2cgeoportal_admin/views/layers_wms.py +++ b/admin/c2cgeoportal_admin/views/layers_wms.py @@ -59,7 +59,7 @@ def index(self): return super().index() @view_config(route_name='c2cgeoform_grid', - renderer='json') + renderer='fast_json') def grid(self): return super().grid() @@ -96,7 +96,7 @@ def save(self): @view_config(route_name='c2cgeoform_item', request_method='DELETE', - renderer='json') + renderer='fast_json') def delete(self): return super().delete() @@ -108,7 +108,7 @@ def duplicate(self): @view_config(route_name='convert_to_wmts', request_method='POST', - renderer='json') + renderer='fast_json') def convert_to_wmts(self): src = self._get_object() dbsession = self._request.dbsession diff --git a/admin/c2cgeoportal_admin/views/layers_wmts.py b/admin/c2cgeoportal_admin/views/layers_wmts.py index af1520901e..3805bc4f96 100644 --- a/admin/c2cgeoportal_admin/views/layers_wmts.py +++ b/admin/c2cgeoportal_admin/views/layers_wmts.py @@ -53,7 +53,7 @@ def index(self): return super().index() @view_config(route_name='c2cgeoform_grid', - renderer='json') + renderer='fast_json') def grid(self): return super().grid() @@ -90,7 +90,7 @@ def save(self): @view_config(route_name='c2cgeoform_item', request_method='DELETE', - renderer='json') + renderer='fast_json') def delete(self): return super().delete() @@ -102,7 +102,7 @@ def duplicate(self): @view_config(route_name='convert_to_wms', request_method='POST', - renderer='json') + renderer='fast_json') def convert_to_wms(self): src = self._get_object() dbsession = self._request.dbsession diff --git a/admin/c2cgeoportal_admin/views/layertree.py b/admin/c2cgeoportal_admin/views/layertree.py index 996cc1df0f..bf89ac1dde 100644 --- a/admin/c2cgeoportal_admin/views/layertree.py +++ b/admin/c2cgeoportal_admin/views/layertree.py @@ -33,7 +33,7 @@ def index(self): return {'limit_exceeded': limit_exceeded} @view_config(route_name='layertree_children', - renderer='json') + renderer='fast_json') def children(self): group_id = self._request.params.get('group_id', None) path = self._request.params.get('path', '') @@ -143,7 +143,7 @@ def _item_actions(self, item, parent_id=None): @view_config(route_name='layertree_unlink', request_method='DELETE', - renderer='json') + renderer='fast_json') def unlink(self): group_id = self._request.matchdict.get('group_id') item_id = self._request.matchdict.get('item_id') @@ -162,7 +162,7 @@ def unlink(self): @view_config(route_name='layertree_delete', request_method='DELETE', - renderer='json') + renderer='fast_json') def delete(self): item_id = self._request.matchdict.get('item_id') item = self._request.dbsession.query(TreeItem).get(item_id) diff --git a/admin/c2cgeoportal_admin/views/ogc_servers.py b/admin/c2cgeoportal_admin/views/ogc_servers.py index 2ee8fb0eb4..460e557608 100644 --- a/admin/c2cgeoportal_admin/views/ogc_servers.py +++ b/admin/c2cgeoportal_admin/views/ogc_servers.py @@ -38,7 +38,7 @@ def index(self): return super().index() @view_config(route_name='c2cgeoform_grid', - renderer='json') + renderer='fast_json') def grid(self): return super().grid() @@ -56,7 +56,7 @@ def save(self): @view_config(route_name='c2cgeoform_item', request_method='DELETE', - renderer='json') + renderer='fast_json') def delete(self): return super().delete() diff --git a/admin/c2cgeoportal_admin/views/restriction_areas.py b/admin/c2cgeoportal_admin/views/restriction_areas.py index ceea98523f..fffdb592f8 100644 --- a/admin/c2cgeoportal_admin/views/restriction_areas.py +++ b/admin/c2cgeoportal_admin/views/restriction_areas.py @@ -52,7 +52,7 @@ def index(self): return super().index() @view_config(route_name='c2cgeoform_grid', - renderer='json') + renderer='fast_json') def grid(self): return super().grid() @@ -70,7 +70,7 @@ def save(self): @view_config(route_name='c2cgeoform_item', request_method='DELETE', - renderer='json') + renderer='fast_json') def delete(self): return super().delete() diff --git a/admin/c2cgeoportal_admin/views/roles.py b/admin/c2cgeoportal_admin/views/roles.py index 0e2d73e79e..df9c00bd1c 100644 --- a/admin/c2cgeoportal_admin/views/roles.py +++ b/admin/c2cgeoportal_admin/views/roles.py @@ -53,7 +53,7 @@ def index(self): return super().index() @view_config(route_name='c2cgeoform_grid', - renderer='json') + renderer='fast_json') def grid(self): return super().grid() @@ -71,7 +71,7 @@ def save(self): @view_config(route_name='c2cgeoform_item', request_method='DELETE', - renderer='json') + renderer='fast_json') def delete(self): return super().delete() diff --git a/admin/c2cgeoportal_admin/views/themes.py b/admin/c2cgeoportal_admin/views/themes.py index 1c10f0d074..3f36e892aa 100644 --- a/admin/c2cgeoportal_admin/views/themes.py +++ b/admin/c2cgeoportal_admin/views/themes.py @@ -74,7 +74,7 @@ def index(self): return super().index() @view_config(route_name='c2cgeoform_grid', - renderer='json') + renderer='fast_json') def grid(self): return super().grid() @@ -92,7 +92,7 @@ def save(self): @view_config(route_name='c2cgeoform_item', request_method='DELETE', - renderer='json') + renderer='fast_json') def delete(self): return super().delete() diff --git a/admin/c2cgeoportal_admin/views/users.py b/admin/c2cgeoportal_admin/views/users.py index b00c9e1cb1..ca1a2d0994 100644 --- a/admin/c2cgeoportal_admin/views/users.py +++ b/admin/c2cgeoportal_admin/views/users.py @@ -37,7 +37,7 @@ def index(self): return super().index() @view_config(route_name='c2cgeoform_grid', - renderer='json') + renderer='fast_json') def grid(self): return super().grid() @@ -76,7 +76,7 @@ def save(self): @view_config(route_name='c2cgeoform_item', request_method='DELETE', - renderer='json') + renderer='fast_json') def delete(self): return super().delete() diff --git a/geoportal/c2cgeoportal_geoportal/__init__.py b/geoportal/c2cgeoportal_geoportal/__init__.py index af0cec0770..2eee714a33 100644 --- a/geoportal/c2cgeoportal_geoportal/__init__.py +++ b/geoportal/c2cgeoportal_geoportal/__init__.py @@ -507,7 +507,7 @@ def includeme(config): settings = config.get_settings() - config.add_notfound_view(notfound, append_slash=True, renderer='json', http_cache=0) + config.add_notfound_view(notfound, append_slash=True, renderer='fast_json', http_cache=0) config.include("c2cgeoportal_commons") diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/webpack.apps.js.mako_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/webpack.apps.js.mako_tmpl index fc4d3f0855..c1173ee2ee 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/webpack.apps.js.mako_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/webpack.apps.js.mako_tmpl @@ -4,7 +4,7 @@ const webpack = require('webpack'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin'); -const INTERFACE_THEME = ${__import__('json').dumps(interfaces_theme)}; +const INTERFACE_THEME = ${__import__('ujson').dumps(interfaces_theme)}; const plugins = []; const entry = {}; diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/geoportal/webpack.apps.js.mako_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/geoportal/webpack.apps.js.mako_tmpl index 0bfca17a64..8e4068b7ae 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/geoportal/webpack.apps.js.mako_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/geoportal/webpack.apps.js.mako_tmpl @@ -4,7 +4,7 @@ const webpack = require('webpack'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin'); -const INTERFACE_THEME = ${__import__('json').dumps(interfaces_theme)}; +const INTERFACE_THEME = ${__import__('ujson').dumps(interfaces_theme)}; const plugins = []; const entry = {}; From 6a442a073c8d7c2a38abbdcde8b6c73ad70d103a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Mon, 18 Jun 2018 10:48:21 +0200 Subject: [PATCH 38/92] Limit the memory used by memcached --- .../scaffolds/create/docker-compose.yaml.mako | 3 +++ 1 file changed, 3 insertions(+) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako index a85847534a..ec15c65d96 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako @@ -39,6 +39,9 @@ ${service_defaults('mapcache', 80)}\ memcached: image: memcached:1.5 + command: + - memcached + - --memory-limit=512 ${service_defaults('memcached', 11211)}\ redis: From 25921e85ba7e2d78cc22990edecdd983df6236f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Mon, 18 Jun 2018 12:35:59 +0200 Subject: [PATCH 39/92] Fix the make serve --- doc/developer/debugging.rst | 2 +- .../nondockercreate/geoportal/webpack.apps.js.mako_tmpl | 2 +- .../scaffolds/update/CONST_Makefile_tmpl | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/developer/debugging.rst b/doc/developer/debugging.rst index c28ac51180..dbbab7f49c 100644 --- a/doc/developer/debugging.rst +++ b/doc/developer/debugging.rst @@ -20,7 +20,7 @@ Add in your makefile ``.mk`` (Each developer should have a different port, .. prompt:: bash - make serve + make serve- Open in the browser an URL like: ``https:///dev/.html``. diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/geoportal/webpack.apps.js.mako_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/geoportal/webpack.apps.js.mako_tmpl index 0bfca17a64..107a9b2780 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/geoportal/webpack.apps.js.mako_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/geoportal/webpack.apps.js.mako_tmpl @@ -21,7 +21,7 @@ entry[name] = path.resolve(__dirname, '${package}_geoportal/static-ngeo/js/apps/ plugins.push( new HtmlWebpackPlugin({ inject: false, - template: '/src/geoportal/${package}_geoportal/static-ngeo/js/apps/' + name + '.html.ejs', + template: path.resolve(__dirname, '${package}_geoportal/static-ngeo/js/apps/' + name + '.html.ejs'), chunksSortMode: 'manual', filename: name + '.html', chunks: [name], diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl index bcdbaf6e0e..41f8285f24 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl @@ -609,9 +609,9 @@ $(APP_OUTPUT_DIR)/images/: /usr/lib/node_modules/jquery-ui/themes/base/images touch $@ .PHONY: serve -serve: - node_modules/.bin/webpack-dev-server --port=$(DEV_SERVER_PORT) -d --watch --progress \ - --public=$(VISIBLE_WEB_HOST):$(VISIBLE_WEB_PORT) --mode=development --inline=false +serve-%: + (cd geoportal; INTERFACE=$* ../node_modules/.bin/webpack-dev-server --port=$(DEV_SERVER_PORT) -d --watch --progress \ + --public=$(VISIBLE_WEB_HOST):$(VISIBLE_WEB_PORT) --mode=development --inline=false) /build/eslint.timestamp: $(APP_JS_FILES) $(PRERULE_CMD) From 261c0f6ff0db67af9725e17aed56b5056dbc7773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Mon, 18 Jun 2018 11:00:52 +0200 Subject: [PATCH 40/92] Limit print memory usage --- .../scaffolds/update/CONST_vars.yaml_tmpl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl index 250d0b334e..38141c4e65 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl @@ -76,6 +76,9 @@ vars: C2C_BASE_PATH: /c2c_tiles C2C_REDIS_URL: redis://redis:6379 C2C_BROADCAST_PREFIX: broadcast_tilecloudchain_ + print: + environment: + CATALINA_OPTS: -Xmx1024m front: port: 80 From f5a23600774b9918a3291a2ac3c097c590d0c8a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Mon, 18 Jun 2018 12:55:15 +0200 Subject: [PATCH 41/92] Limit memory used by redis --- .../scaffolds/create/docker-compose.yaml.mako | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako index a85847534a..1fc9e11bc0 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako @@ -43,6 +43,16 @@ ${service_defaults('memcached', 11211)}\ redis: image: redis:3.2 + command: + - redis-server + - --save + - '' + - --appendonly + - 'no' + - --maxmemory + - 512mb + - --maxmemory-policy + - allkeys-lru ${service_defaults('redis', 6379)}\ tilecloudchain: From 355c71cdb99fed2e5a8a2fd9f0ab479976018d3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Mon, 18 Jun 2018 13:00:28 +0200 Subject: [PATCH 42/92] Removes no more used script --- .../scaffolds/create/geoportal/setup.py_tmpl | 1 - 1 file changed, 1 deletion(-) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/setup.py_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/setup.py_tmpl index 8b191b45a9..c87d186905 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/setup.py_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/setup.py_tmpl @@ -22,7 +22,6 @@ setup( 'main = {{package}}_geoportal:main', ], 'console_scripts': [ - 'create_db = {{package}}_geoportal.scripts.create_db:main', ], }, ) From 372302feeca277631e5f6a6c7bcb3b4637363575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Mon, 18 Jun 2018 15:04:12 +0200 Subject: [PATCH 43/92] Update the Docker diagrams --- doc/developer/docker.dia | Bin 3585 -> 3759 bytes doc/developer/docker.png | Bin 55114 -> 51525 bytes doc/developer/server_side.rst.mako | 2 +- doc/integrator/docker.dia | Bin 4431 -> 4873 bytes doc/integrator/docker.png | Bin 37652 -> 42337 bytes travis/docker-compose.yaml.mako | 22 ---------------------- 6 files changed, 1 insertion(+), 23 deletions(-) diff --git a/doc/developer/docker.dia b/doc/developer/docker.dia index d2926ed48452f87dd773a340448dbf71118fb548..f98149503d01d40343e95fd14b7674cb3c18e453 100644 GIT binary patch literal 3759 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 ZlaDvW1M_c|sKv*d{{y=U5(b7x0RUcFIfeiL literal 3585 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 HK_dYGe<%8I diff --git a/doc/developer/docker.png b/doc/developer/docker.png index 394ed4eff24370de19c3d0acd062ea9238a6b7e8..5c723d3353e4dcfb9182005020e9f2e25ec149c0 100644 GIT binary patch literal 51525 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%0A3tOD_+v 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 JFN0!t{{{Ca?#}=K literal 55114 zcmd43cRbep-#31!vnYy65+V@_k?e%XXh=$AWJHndy*W>-L6Ra{R4PQWcL~`dGc&7< ztjKmhKRVC*`i|@VUf1`&uixYG^G`W&9G~%izh2Mv`rJBqR$s%?;;T1KlcTD(ay^YLCRSF7<&%Me$B+_2e$rHy_9V16OU9@S33g#w_e120k z=Z;I#R#(gCke1uub4+KS3A;{w6W6Ve!H%p_rKjUw)SNsMAk?gBcE!IXoFUlc>!eRk z&*99nbk)NZx0-HiZ=fA`=K06AN8K!SVp`svSC8xK&rYwDxAle@6;6+4AMKv6tu~>R z+eRX#^90BnjLo+CUtDWm5!}qb8 z>=O8gqNPPR8ZWA@}Z_~bi{^MAg$B!SA zZ)Hlgqa~3(v*8z>A9r+|>ndPE+_PlxXM%QWJ;q*{lie^To9{-sLVWkxzK0b@;M~C-_+kX9= zHU8p4LVq=*hN9Mn*D=l2@VC zDkqUr`4|G^DfH(laTMw4>4IWnZBMN&Jl>3iFo zgxuVPA*t0IFE80ESK>@p85GoEV}vJCn?)jjti5K>KTYzk$%2TYBfJJ zG?eBF4tt|l?}s2Zb93`L$;HuEm-D=7nwpvxXUAM{5H-9jEUS|&>&ds;+S}{oJ5E02 zR)3{i6de>a8CtY7kX5vt?C0l~kT8@tUg$E~`uax8tBP4lI-YxpE(_lhQnYg#_C-*T zjO}sbJ=IPnG&V)o*3|5j;nB{X=y~hq@V!IKe)Q7Ci%mFQGjl6lU0t!>FJJa~b8&N5 zRa98n+Wz?YGj!VPk3YO=#Kgp=t`5~k4V=KCv8~lzH=+EjSbsy%5Gf+IoGzr+OIRufdva8?P`^IXYNI9 zZ6+2LDf5o(b~|>G=fXX#WX%_$I?e|U9Xg9U(w@P6_^?Ws9-H^ve4R&hSlI5JJDcJa zs15pX0Xf*&jWgOm#w%!jovwc@yLIc!Oe4t;EpEed{V%T$mX#stIq>W)=2MNGFrlI_`t3Q0DsMqkS1j(p8I1xiGn!nSjnj? zVu!-;-LDF8P#zWF+{1RCJ-6{!c+GRO)p}}@G*JPbD=gpND#uJpwIPZ~M$o|8?;I!o zV2e#D6aGN2j<};Q&*!De29cAbiJz>my1Mq0EaE4_Dn(C-pNul0u*BB9dFHwH-41*w zy}x>=4c8RO^B7S}(#@Jf&57^!tbOqB0|ILwtVJwh;iT7cjKqgoFq!Q^b)qH&`8qLhDJY^3Mca-;$Aj+qqWV*3Rv}SKa=_#r>~Kt~B+E)`z56Lia*@UDY3EPFDe~A5#oUv;RLtd)*AIG6w>Vtc|Irf>rrEY_ zve%DAIN8l0bOXtSSOt!v?@CI3e($_7!gYq9o~ojvf@3w^d1knr*(FKVZ);&;Az#sw zH7e*Gxt%O5EW3AKe|$V@(lqrLZia#u+Kcp&g~@Uj?t=$6Ze=o_N)a*dXid9%y1UTL z#@c$YI_!;M#eKKM*^D!K7ZT2zHP4brw>DFgo@?VBwY4L4QXK5;##&NhlI%=7b2A*q zyI#F|E*?ZUi#<&GB^LZpJa!9IlSxsgM0V*uQ#xx z#ab8SIgGncRd5y*6p&NVry75ZvnZS|)4r#0mub`1tsx%ErF`y>KN<@N2zb-1&Xr4b zbap1beA%z|b;g;RIuHf@kZz$iMNnwyu04CkdV4>j%cFv9C z^4q%S)alcSc*Sut?yVlK@Eud8?b{m)Tz;a9b>`R@)kcaPKYsl6>(~4D?|<>)MRauZ zrcImD0(4S}tonT8xOClS9_Hn_96$bh9M|&V&+a!yfmQ`*aCdIqI)E=89;T!Arna`W zM@kJ?+2vH;y?a-ACV3YT!k)dDDC}>=d z7k0``PEMXYb?W{REEI1V95${-YnP~*bpYK&0d=wD+Fm8Q+D`uU+qZ8r6k*I< zx2;!}oLJvCC8|fTF;lW$H8OH>bX@w;sJ!Ks1D2DHj!sxu7~Mv*Vo$O>`>|ZRk&wK+ zMcmM5XQN(nRZjGlcr1SIK6v|nP*87wKNYJ=3^yl7M1I9ZEWf+w zn1qBsUsYvgfBM;kBbhDVY_UY_7V7?YYv#L3LSL?-4a~W>ef##f_;?nl{%tlR58ZKG zfC(!jDMeeUsM7h30lviDm*!e@J(l0^P(Goc5F=_8m!H36firln67NmpT(Hs?CI(hUeyu4GkvBiwg^zS=%!fe>&WbiP61y@!{cZ_sQnw=Y=jv49YBw zzNx!%<%)Nh)xDKzsnzLDhwfueQ-&xJ157pn*t&LK-rsuiT?KkPc<9jGz(5ue^T@LaH=a43@l{n-wMC7y>MILlBxgP0 z>biuty?_7y($dmFZtn4}f*X&m@H^M9e`8shAlGQvEoAa3#bbGdGsgA&#(OxT2M<&= zG`{us_YVv_{b=kwJ35cM_P)GaT~(Eqj;?UBjFE$Fyt`->m&S+Y;>C;Og^RDO+{Y#b zU#u|fVPXpBwY1Ef=I-Fo--ZEG8w2!X4o^z|R>q;#k1m)zXOh7-L8WOJ?h zQQ7Q#!)dpkprOH0cbNjGta z@A)39D}#5+4HfazJ(8~3ckbNDnE`|=TKt};{!05BwiPzf=PZly@88W|y{36rQE?rp zv$nQ2@Yw95HRucMC@{ZGMpaFXK;T_nT~Xq8&LhcLTUjJK9?SZn(coy55*p>?%8}@qFl~b!zFSG3lp4LOHUd_9%g1k%NuS_! zGB!5GRQ!UefFX3#kbgI$~6tZU*4t_X~K`Tp6f*n7Q?M|ImJoY=0L3KUO#`pTKE*i8pzv|LV~iXxmMIwu>ga>;cnA zYj3ZYoO5cXwIsfCTZ41nIak);oDl)%GHVtZwh*XNmiSImBp>?FZxZbpDB+tqZ4}AGV zNuII4kWnddK>wP+{`)A6IKF?IIyZ3LzA{2jk{(!tb|lSp?>Bq~emdhq*L8C5ohL4UREu5{Hm}OjRwJ zC}sHX{<8M1Vu5Q+3eea$vfO#?Fr`1s)*9?z!}PIA`-Wa`B;E>FEofTs<{I@kF8ScZ zL);q@sa@EbnR4xdc?t(cmNOB5#h|99ChWfCn5@RQd$-f32mtkT_WU6awGVI!w&@yuQ^5w;u5zZTXw6e^{I`gs;6BA=%x^M5~Mm^_b^z-#a z({!Jk=tb3{-n1#lviDt~+amg0XJ_XwUQPBQ3S5IwK3z$ZPtSh`C#UD{`SGn$=BB!i z)49e9obd}%}E-lXyQ@kUHMKZt!rKSiMO}+$zYD_g5ml3`Cq>p z10bT$eD5mw?AUhE$;nAuTU$>r7JS}&(~h(^ZwCGt{F24NP8o4Qs&)e<-DqdtPtf*_ zUT^jI(M6jIpO}p_Cp#B(m6XJT`B#PVg&vunngTRyMD@g>M~mB4c4);!M@u?R=!uAA z#Vn5+l^vd(nK|aZlpP5vpvhQJ#G;G#+93k~oOXO@x~7?><-|~ohq<}jV0akQXIu(g z7lzM<*REBb87FI!TA%!Bs%Lq<=k?8`STT)h{YBuVKqfH(Ztlcq&juTwo(8(Q9mgbY z%f-u^j90#t-T~M~>rS&$R8$0|sEa!403kulX7INS8%oQ{LLNML^6Z(jvom=42Vnq_ zvH-^0ajr8>>fgVA2eh~y_hY=<152lDW-FsG4+qCHfP&OiOUNA`KYqkz^|?nYw~3N+ zajJ@Mr`#=nfB%4h0-K?;$B(~7lk=uYtLd)@E)QghiHY&R$D=Lz`S@H^SJ%4P9e$6; z^lpVH?rpkduS^?y_K~w^&ss0wEYPNQ@#(12KF!aU5EIj|-%dkwFEUbFMdh`5r?`iQ zNAS3dqT@90sj zLJ5zRe3~!AO^G$zaB;B?fkWM)Ct-n6vfjRZ+t4eRQ@PU1+UdQ=K;=%cqL(i#OG^j* zSlsz{70s}QeP#-_3P0@#j2{^pDY@`9K0?HTA$0SmP4i7D?)wvfa=mHh=jUhq4zsfx zqO+ETf(bqxNt5h72{O~^bpHJP3h#35BQ^$!T$R{hp@%NNDeFnwhUwMHFVjV|d^rQAaCMNg`dAPZQ8^xn)TXhl|2L=b5ze#a&mScT(ck32D zAOn;G$Prcm>^+5LbN8-$e~`k|)D-r+xZN;2<8QzH_Tg4_^_c+qBhEjJ`_h0dv+c*i zaeNhX1DXs}74HTGMQt;_%`5~;tvG(pG=oCs2(7dE)#alo)U4hTY~0?KR#w|&9z1*q z&Que2M4^`Fz=0svZ!^vY>c9W~dq~SgU32qSckY~Sd3AZ*dA;#LfC9!M0eGBdawqtT8rgk)FL_UA@w($Dk>_xnXib#*^%=Gj2v4yLfr0U zXsEiWsp*~MJ!TX?CrF+F8#ZoKyLhpJ%_1IZAV|dA++1~a_2{Un#sg#3tf?o3R+`Kn zSvP);2f9f~VdWg`+m=aH41tiK-3>-DZge|#uz62RPJ%0nbom+Nt0-KPOgJ00(6X8X z$`4*tQ);2a9wJh17)kg3{d*=U_x$J2jZtHm$)~H`)`b$gokKZ#bYP$fa`ercH&Go}PdGWvqhg?+<=KAQ&d7M9 zBTC@N5y%7I1_y-=KhS6h3l2eyKo!L5#KNC?^=1pB@CT`#J9pMZioH1^u3gyCfs7(&6B6g{1#{Q&A`Bv=g+x#cwGAe zMEv`XTyJc6vV>SmBr!VVeDt^9Ba8( ziC{M%HalfhFJ3GFqoJdt-n^M$1`4Nf(1Xk&X0O6>Uhk9<6&02E*qml+9^iNE(s6tnZkFY4^91zeOt|3n(%U7mRg!9fppI!5`DzzZc)){0Ha8zY1}Z=ong4au7`}oZ=b`_pC3a@qP=}MU=8<64LfO$w zR?{i9{aVjTPVR^Il5|_J#rj)aS+3a?SaI(z^D%MPIZLOHq5ebg9K@pz;cWqgWGFZ} zIa%LB`tkDe@}p+?K0M4oM;G$w5f^oMO=lSSo_kY)_bh7`}XZ) z_`Ft8t{51sPK6eIomn7vy&@VGC^puf5nR_$Z!Czt3K;{!fjIUy7Noqv4QPilQ+>Ix82 zv@(-&qdCdS-kw?1GS=!TM|sE8C88+d;FRpUPV%6Qx(TZDLELzn{Y!dd+ zw##_;_{I)~jF`kJ&-Wc_9l3U=J67x0Nl8hGUDnjhy;8b)-tkA<8>7&N50^nQWNzYi z10`3U_e@AI($HulaFTbn&Cp$KODsVpN8`6xl-@K`kMG=$V`pc_vS_pBBw$yeTVCYZ zlPA{%26;I-ltmZoK^*k6GhydlwgoHieQs0dwnNwmRUbI|$b16KAzweg7I%}I6=l{> zK~-M8IUj6rDCY4M>m+|_0TtT)Vq!UvB!q;7=*sMu<|Z>@ysj8dJ>I~n9L*$T@<1nr znu4O}9FGRQ~c}BB8)MdBX1C;7}DXaO#G|-AI11L2!$Amv&q1sjjJc zxGPPQSxZ4-2lb}J)r}WbRsErzqwS(hX=rHN*~KR;{QQWgF%Jl&XHAlJP8t?wSt!~a zK+?>_1bQU}CFPN>50n`%W9FX+#>X3A$3XDC18vIMdIiGyRt7=Em2Y?W3bUc0#&Mwo zw-^h81E8@TxmJBC&9Gu-q-CdEMRoOq&~&@8b~~s^sE|8k`UeM@kJ=}_omrThqot+& zK+dWNQxB3P#28qadGIZO+XCtsQ}s|UKp?MPy;?M1W6__X4=Qb?umPP@aqOFaX` z7i64G^J$wRroY_bBVaE&?h61FbRE~ZRGNKgvIh=)`}#2+6yf2+htHO&uKkq-C=;wU z>_aa>Mfia&yz(s}rf~85(Z0Iza3-570~i@PPFZy!yjuP1sUr9@XS!pLI(!E(1e1)# zvI5y5r)Cxu@ib1SOXei=T)sY0(wy17PC8!DNlN_~B^$KC1+;YZUeG06yR7W&OSP+= z1MMmG^RPdR*K)YijHw%}i4b)O}C@{QM9fV^wV}v!L-g zXn^2p=zMj%`L5$0Zl<8Hw6U?Vvf4yJK_F8ThnY5`nE8bT`_WbccPM?1#6s9Y&tKfx zc{!octRwsN%8u%?yL)cDdL0=S6~)W4W!KJ~Gw6*=rMq+=Os@fxVQ8GFYJYfnU43Sl z|DaZTak8(R(c@v_Tam9ni&ovR&iApg#W)YYK%;v+OV_kpx@vCqlAnBU9vvw>IR8il zwbpaSMKQMe%<8D4BK-XIbApN^K7M|D@Y1n%YiK^nmULYr4^4*@BE5_&>d^ac1M3NA z=LP8R;j^hn)8#nKLB>g9Xj4zx5Z4g#LGqQ1wQ53vR4U}FML-hT`_ZN%#n4* z$66UjdhQG3_x~eK!F08Ta`u)cCtmv$usQHDTjvJIPr1wE7urP|E&L2K<3CRHAa=sM zwU&KdGELyqjbhaG+eN9?k%~DK9Y@3CxIQ_NNRF1S0qc^RSEH@^Jzne);1Da@Q8&7g zr0O*-3h&I1uBY2+YiI}Q9J#?*d-Lyb=L5aTW$sPmL#3Z8I!+XQa3VdI9iu)hSx+Ld zOU#Kt3w-%@3q-A3-sC@7hsf%mHLZ?*X{1`SXNULUh31O7!*E(j@OXpq*XeSDl4)~X z>q#zU)Pe0nTi21EE04XqJ2~`_jAVbuyWCyK@in=^CQ>_16^q8w)&rzg-b2^d6TWeh zuLN9Q?K>P2um~W%T2u&SK6>j#UIfIId3POY^nuXM>o1#0dNI`;5|AT484G%>E_M%8 zQA;h=x3pw!SO@RIu`20#MYzyAzXqof7Fm+<*1_9iv`(brXseTqq*${zg|=&2;kgUj zqd~w?C0(4H?1n!bKtqPcs{7mbk1-~(Yf zn1H=ZgcLyNyx#Jo>*BaLPH=gDsI7$_KMW=_cD8!U>3DIGmXpgO?_@YRx`kk-|7Dqn zPd&&~iV%Jn5~7o1J%EyZ%x2IZgplS6Fn3FeuJXx~*SiasZR(EgVqqyVDBsHZ9x}Jf z%pXe#B=mIo_**w`PWo8uX=m4a@AaVpTlfaA1e7&CKK?4_pzl)^9^3P)0jc%M@eJ#w zBjT^$OkV#Rsnxe_vvQujoegVWB%lD?Fb7CN@Z5qql$^n;poI9)G$tsAc5?v%pvB<8 z(cok#;DG1ELP;eUzb@3GjQZrk#`B}&c^RpmLy$LMO$jeG^+{O6V$X7OH4F1m-6!W-;WPmD z?>eMC2bd0hwvAeHt{6fCh1FDl1$-pk)rFR#^A|2qQc_;l*0vRoqKQMJICKdO*HU_{ zy=k5q(u1|FZNBr&OIN3{cA+*iFQz@?r-y(MJjT0@iMfq5^WJby6_Bx6TcBC>YYWtS z5ZCJS(?|OADTYT$Ff}+N?%uttudk0L#&hssWktn&AIqw!asA%;1ir$#FW|hlcr}!(?`JM3&0{hz*NZop%3`BNue+n}FP~2*y8WDN_VRL=X|Cjq9gC{<*lGyPYn8 z-E`(mHMU@7W#ucK0@PmnzUhq&Pt{X&5|m@;WICX5)p@Mg>~QAa^;nF!tx;NEpR6$I zv&z^65bE~MH@4JuL}+Vy!LC4ug^mu&S-R8oH8Ck(-mw=6(bf(Q9!rzuU|Yuk;aT6K z1Gu@lk#AuDq?kpg1yHCcgNFtH{2nX`m>y`qT~8X@%XNMr6zvqh-t9cXD>hB^BgjDr z3rOk}dj_#7yaEQ=6ahGUJ1Hp%grOFo@54P-rLS*gyayjKhREBQzuFOO;hl4~S_qyM zNp;jC#r;`f;6i`t* z8sSV1rs2cODkkc%gH z?Ql{L8<-6}9+LQFtC*&QSu+#P1FSn4oBlsv)x6^uJxwILNcdo8^IF+=HY@@1B^FzW zS2Aa4Z2tO_+LB7AH4B53E>FFGh)5syu)+amgf^s>7~t;@f}MoY=~MPAwrHHH%pI{? z?!*b#xn94Sgn+5(X(0wBSfev;>!hn(CL;adk}wM>@n+r~_}$yjZ@e?FrCH^X9zXs4 z;9#>oKKqgX%fVXI*YAZLIWj(8biMIeNFXbap@2ZzKnu6Y@~+g1cCvI8hH2(hqjJCP zEQrPQm8VZ4j8qQ=?~;njVapzAE%GZT<>lppVwjkiylIeg)5v$Usw(G42 z2(Wtg5kD)Wh4 zZ!LD@A2XoU1v79MyvV8TZEP0X>fEm}?f5f5^R1&pJ3!uMdQeG!P#_aBmC_^r z=V%8KQwmXE__(;v(LRNmghg}sl|yT)K8Oh;8i4 zl%b)asp%+s$>zDRV#!^9^TxYDK6dDowv4rrge znYrX`BXf?qfjN&&O>-4p+8d*1159*>QU*gV(#kb+H~b%M456j}Asb^eE3R_?wpH}| z{w1)yUAsbA+~?kEFE#ran&Zk|Khz7Q&eGZ%K1Lw;?se^h$eXDbEj)}o#KlD;1OLB; zrR?6ldqOvw>2*Of)n8RDcyq%0vL6EAi3{2km{U;jaCJ3qS=PeB{KftI+gaatugr?f zTLX@uVhoCB-51&|bN+mTSIYBEo9aZmU2Oom#E;xq6-lbA^iEiz%XsIV5F4w9t4`e@ z8{Z{bwfORCM~Mx~)59@?q>0o^9*MHh1i`4!ojVsbf9uvQi1?KsKG4W?KmkE-E0e+< zS2}Bp?usP`I$aiu#%8UV(75|Eblr}Q4)Nuo{(ez$@rA|3s!#z>_%`UcgEz~y6`{Pr z#R!y`Qq<9jhI;@kXz1nXNfHZ!^!L#iy1AKlCJ+jcj*Y#&@a`h*(_X9^t&to3UI`cI ztRe$w0fiTk6`_w-XcREw_`L5Wc>Bn5%Wl1;E8YHLH}Zpm-hTer-ZFfi$mXTQ2~cVc=P zQ6>Rq{VEc+Soi^E8&*WejIdD&Y!?Ht)aNEz#%O0aXoI|n$8ro-f8d^@%~&NTt~e2^ zGAshpR zvx-njLS&QUYa@*FEfOv+uf`Raebzd6{=5M4P>}8*0vZ@MOdw6`j8#$!b z)=PaJemEzfjoh4^ExQFygdD5MfqL>*@6k~Qb9eW`{2S(=FjLCdd{BT#8=l^Q0Rl=* zSk8L)03pA^23Jdr5;hA@Qrp5TF}M7)JJf^7u3vYhLo8hZl^H)qXws`wvq&r?i|gpo zqX!SZ#5#&s2n^}nG;Lsf*3r=s1q<~OU&uyZ)YLpUr2L4#7YIJ;uz{~m3X*F`o@MY$ z`3}{^h|p85tgPIW zsJfy8;u~r)8(Tb_^JmXE5a)+tg43F@xM)%TKN8m3#0tKL?AtaL7G21$moCkfF=1IX zo!t!lLEylBElouptGHmBSy5@b+^$|cn4E!-D$O=m3+?->ZBVfOCK_Pw#_ZOfvt=DgI-<=9Mi_nmeS>4qc-Hbe;T@_(Rj5w6S(}9K&A_a@oQETTly=H^xk>@o-*1GG~ zG5<4BwSk>ia5WGQLG0~2xEhKqEbeaC$wyR#W``3B-Q`x{2$Akt<2ND!SN(|98|8hw z%xHI!l&Gj9;vbOM!D>)3te5^j@(7Z2?c59{jUOO%QDOAEv0ymVQDx;ixJY{>94(2CaM3{4<-k%oH=v6xWpG}FHlgB+GG zpyJt&cXfg%BB2QR^7=dH5f`oqHrT=m=m&rnAZ~@0y?@ZW!yOd{J%(b5qd@MXa5nYK zUwm53IDkQxGRSq}Ejd!!Q36+QgFc6bw!o8*kXWZwc1(gaaW71w$K@VX!d3@|pHOU( z%PW|z-ie_0rPp!peYnwGqIvK$viOwRM#`;T3+w7Yj}9PI^)u(Gq;vUO{1Rh8D8Yc+tK zX7r5SFRJ~PySqN*9=Eg0aCHJ}?+ZenX#{&4=<`uimZHZ#@R58 z9;cWD2-?i*?^W+zb2vwiqoS&0IPme+vb^1peg`r#rml`LTAyGmua{wm_&g;RHlvXf zMtv8#=N?vmYJ%aHJV<0s(^;$vi&VY9-7FlpFO5VMLwXC74 z`XD%Xx?F0-sy_DklrrH#A_ucm4yspye7qIb+r$I}g)&%Lh;6;%I(6;ZvwVt|I4h8Y zt}KffntgB^?UfoJ!o~dY$GZ8wzg4__%ZdajDkks`M1)S@qe-Xrbi~$2XGuyL6)Bnz z$e((NpG_&aBU(I97@^_ECm@(iO^I85@pf(db?RY^vQ@L+JGm|1V2f2DVN@e@;}gB+ z&3@EIB0`Rxc^=7JB!N*-%FG94^}f$Dk~~9NdN^&~cyJdV5_0nMc1ue&K=w&;=F&_iG#=BRWy7$i4WyL8AVHaoc_wK>2`Zb zqqH$H#ur;z^|v3D|Gbl#88qe#q|0ND>PB_ZdxT9}u_#&cxo=2(&?(386uuZZ(S?!S z%e?#UphPNML%(_uQsW=Uv+RYf>`qoxytT zq;-}rH`FM__xdxFq$#2sHj>Ve@6LVU6LxenH7A>@4Z5%5`(eZmc*XE&hWU&DPKgnvzkCIxC zY-f(W1<+p>$db}z!p6pifW`=7JCCi92|?;glSi2QPsK#(62Fv`Zv02M__v#LucvbV z$GFyOhu+>^m!ID=>}WaYseJqU4Yd^XD}c@rdbgwtU9icmQ zbjC461l<(Huqq61i{!*NU~i-f5k$YZs+6*7|LAkI_;QPG>QDJ$!ia!Udae z4Nrgku$5B*Pe6?)G3J2$BrG_ zv3Rwqx z^V5IDI(z;_tdselW1Uf1CH>dJM~iCzuzSQ|@&v5Fth=xirBc+KW3s}_@8-B9N!n|!KI`|5Mdkyymj0Skl7RIpwrBhH zsoJCCAHLz>DG80GzY0~AxJ79-h-zKeCPpic+K-<5E_Lp#vhpq&VrhheI_;9tMhZym z%WInR7`XEDE612IW~;BVo={NN*I&uyXU8S)U&fGdHA>;^eGIPN0bvzPeNL(pU@TjT$Auqg;xJE$`t!xbBWF0A*# zL5Tn8&wMNw-5NgrFm^GZKal;kLo4Jg`zWR(stJG zN=xM^RMgb$F&)@fDhwAYsWC98MzEzwCCa>JOZ>p25!?F_5hECYLaw5%OOOAPMu+Gg zdiqNqEwF=AcW#efOus*pzkY293Q<>+Ivy@ABRR`2e`NvwQE{;2R&Mx9ap)q)eg+ft zS4b2fp(0_eYb(aM&1#%E<4^OhygVU4zr&H`?&MZFrw{J`pg2&|EAPk@bo9qMq z1y_iWaIzj_O36e>gt6|}hu@NBKkJ(xQJU3tc`M!-&M04XSUFtqg*_+En@HdD@r4FV zPfy?Ai@?ryX@As?{?^L0N%=fZJlwWD#!!$dkDV zZY^Hd(}ikDWGF9RE`V-<3HlFKXx_$K6dg5RpsxQ$POcy)2b)hROyFs1>baB_3#hax zF%SVD;UIi+#$eI)+tbi5U%sSkj$wgq-?0Nh8*duC)fq)a@DxycIvHf%KLB8gie@Xv zNP=T^6?u3}ml_B>!A~*Kq^N|0Reg)r8u(JGj`g|!BG=T; z?~7+twDJ-o934i;e`M$m{==ly6!UgLx)KT!X_PKBy))sVc0)5mG%dNrUK><=IJR_j?N233X1((IK_H@vQ zzn#~a^73vm5lJp2;0jn$YU;#ii=vsIKfBYf2Pd;R8@JczVN?!haOfA{46pcGU#md| zB@rcgpr>cA_t%D}RhR-X+w)P@5yqjgz}$c#Roj;jZ&j zU_)D&L@hD3yJivttZ}1v>SmZcge(f7ljaLZGKlol>}>NlTNF_4Lx*6(RQs^+XJ z<@`g_o4OAn;{Nx~HyIw68We8snAL2(6g(c}Gm9|=21!>(#IFFMhKnc%*DyfctYU=XTtq&#&OL{DY6x((O@gmXtG= zk@}ireP+ER7uQ!n!7|0+d`b?5YoN6d9FmH5W=x)>j2)PKY{MdXv&s7n$=H0$cmAm- zE}EL*KEjw#iN-{9r++3neFXzA)3j$S;=a--IA|nu4iQ=%UsqfRU25eZR>oQVc+>ub?oYm zi!}Qvwt4Gid*5Neqk)F9-W(3w{cb;d&*F?--BI18V zKKVN|>3u<5l9JI;eVDJ&(pvn$7RV&wI1%O0rIBIMgya(OOd*)F@mHHnGVa`|!l?V# za+p`W;&|kcSi~P2I}GgC@iz9U=^=VKIe=M41x`e zzh2CM#^&4jBet#fdr|ig%n#>+=wrVnkj*VFYH=|a65;jm`}bvx77u=2cVaFaAOPf-Fn z)jc}QcJwI5@8@AW;9&$Hyysthl9{yh{nXm(xUw|w?fsi(F~3CE8DWo=CD;(omqXMN zk71TvCxwrj`zVrqMn(yjcpzHQ-8%8Z%E-5>a$pKH8-^qvJONQ!UDxem_SwQ`{G11R zLwY+lN6t+RZ_~xeRT>KXZm6{W`^7$bb04yAM@S^Ejq9Tq>sgwT@DwU4mc^gr)LXV( zJopgW8j_nJ;h-iU%qacm4?aZkA!1=>QN&}H5DJ%&kN^t0pshVRHl{>Tff<>D2d5?` zVnfAt($Tel{tPJqPaNo~s=5KD9dgI-$P;9o_F?oNA$Nu@z!j}7Q|}Xc>ChQ`4cBjK%507)_bK&Z={GB z4YW=0FS^S!D8UylTxh#fxG~^jri{n^j3{$23q2COt7r*u)OtQ~@$iD*jr1VDzfMZ> zr-@Tb#2D^_=xFm>8Lc&a@|L5koS@GFfT?`-?y z&tKecHc|X`!%qHl&ZDR0>vGmPTK1IUPmfyq61&1u6*KO7cE3j5NhDuhV&UOSul1eh zkmSA~ik1!n+L5Rj>DOXDq~cT`)RVQBwEpEwy8_pdeo!()FC*zy5g$f;`SM2g=1`aL zH*X~)`BJ$W;H6f{$TgHk^3*@=t;=P$lv94W}66)m|iXtX_ z|Mr-K@K!11clQuI!{4VKSo=BtS3nF6qb|AKYxgj_Y&(g>-;)!l$o@`O z0Utv8DGhq~_a{+2al8&<%h4aYky+#<$Ktg=|IgmhyLZNv_LT|V{BN!kdGQ~bT3W&P z?_+`D2_J|w_I&-ybxw5OzAKPgz33u5b92Rm6rX%FzR4>`0NZ+U`iTxd{YSsM*?D~^ zCaE3$fSQSr12zSDj<(ME^H;{Wyb)W-%E-VRGF;&ghG|({&WWpbvraaxqxQei)A+W$ z+3h~^<)tv5I~3`avEI>>etE{neF0v=QE_pMPh1(3_I|GVGBuSSkLHlU_*EVHN%9^5 zU{bNy1|vtFJBN#FnV#E7^78V+utW~zz9Rb$jx(`C;*dFn%sWJHo%k|7j+F1}3PM2X zmVl<2W}iW@ix)p`nEx_vvqR-s*elJ9m$9*!U|MgTqkSlJadDAR zr-3j}5bK1Hg7?8i(!a@zbTYYKXK1p>jSXsVZ-*8Nw*w%y zXmxqqclDx*%C;Rl`i6&zY0Gy0t0WRV%|22pz07y1eSUBC7nI7?<+GzXqaClHDuD#z zaDkH{h*Fh6cg15i&^2{2sCFn6qgcUoYFxAk`V*$Ji_0Q{mq_+t1`*+04UI5N&EUg$ zchJ(R>gv|TP)yJTJ$OfQU88)s-@0u>qH_wP?oma@@oFX0ag z3et6--(SKH^%DN>*x1;XEn84Q_j%U~z?eS+NhmRqko}O-gyIeg``edsLBO?RTPTa!6A!hPEGKb-5p&4Oz3^PD2O-&D#!h*#v?<(F-wHU1;bt2|B zMx&1(X^Yq2)u&(D(zCzb#Os`nqlT~kSL%7R;B{A_EFQM#k`PZm$JOOJtt9S-Ne`Ku zYKc!-5UN z-5ugtf(S9#AMlZRjUb_TXMMdi#f!wmBgq|>HWN=PG>VEn2@%NB4lslhT@A5-*ZUDS z6}xb4rqzyYBiRBg74O&Aq>$q88re^325(m9eM&|Q0r~?w`+H+|E2~N9bnDi>u(9L= zBVPQwh>Py_-@>iuO(r-MP^*vYAimjt`3rkV@~nNqgsrG}k(n97x3VGn6eApKTm z2|TS!2RRqC^If}kHO}J^Ubax)d}tcl+jBiu-De|BFI)&#Ip%tOs;N%kW#+SIWu>K8 zF^=`aW57B`C4JNu?CyFBstPGpgMMOSJ2TH##d)0?k?r*K&s#>&+Iy>geSI-lg~Spv zImEsV-ah!LEUxIso5mk=p8>d*-y<2F((=YHUyy{keWDaJV9!EpreK94CLVRSfZDIX z%jd2ujNbo1UWIduyOmi4lun)$gJf%9(1(Q!#%U~CcH}`&(A(Ml;-n)T*xy*J*|91a zI5RUdGrZY~#pTOU`c&AlzKx^LsfdC&t6yxe;b-RwIf`DV%-{LL&V)opB6~YH0NBnvRdrk+x zaWB~r0Kpp%j~W^n*d_y5;LjXW07s6+W9fBb;tg2mFb`u75FMRr6M9g&G1HY>m15;$ zZi$v?Rmk3+dfI?Y0)qPt0US{2Kz|{qpL#05#%2K9pdfh~BSV+rBtqGS#t0Jy6PcS> zq0B+(1n0*1i1$4_H4P;Wbrc7R84p;2b=E+eKj6E*FD?Cer_fjeMFV8xh3d1XPq)du zzjBzBc&HRU9y$B%x7ZQU&CY%#EUCM+d_At~T6C8VCiBT;2Bs4bMe;5NVr#V0 zk>4Qauhw@o*n~^v%Q7-N4BsU~Gx@~6FcAypf^7sUAbDagi7nISY)-i%k3-5fPq8`i zgt5^RB$DigLNPL)-#gz}`Wiid*RqfJW$8dm>c7Qd#r%dJTddXe|Tvh;=?GNvZHWB_zUoi%uHeAcL%F?hn>0hLuTSQzju2j(=QlE^`!Uk z)awIMt4lU$7Y`0xNZRvZugVKmc-9W{ z_U*`(y=XGQWAG4}fpTsf78@J8u;A>gk8mB5I?eU<#r)Oi*og7B&ESNO?B6e!*kmFY zju;dN2ZtsX^Y8cQeeqzJB|ITUsYEN=QXlrk<;$H=%Mm7^*}c21sR^OQVdP-&JQH7i zTXD=PZQ?aeCmwxx?b8lg_?Uv@O5M#mvRECCza9eZHDc>Z|(W!IMLdk^tWXOi( z1(OoIO;23tWytQprhd;p!ty+%m6?>BJcbAk9^*LrOofXUrHXjqqfUwkLU?$lQ5*BS zTb*wv%8TWDm4&EOzUJz&)$e?(YT>c8r+Dh!%#YT~->8TGF9FaKt^W!D$@gM7{TJYo z8vYCLoP2uzR_PbQzj^VZf}g$QYRct2d5Y_%rcYSlQp^1>=H5Im$Mx<1y^Vz^q(UMx zMM)V#gOw>%rX-<>Bt(*gQZ0!}5|z@VQnZpXR6>I?ga(O}Iixa18H%*uSH9!=uIKkW zzrBBZzxMr?vHQNS>%7kMI6l)6@JG|@m+QJVoPDvbZ*kBhF)i0^Bc*-W%ggG#y*uma z3ukD%>|?dpRhFe|HQt`q%SRB3K3_k|^J@1chPprN=DEj~HA;joP?}k{xrYm?*zU3j zBaf!^blN?f}(}=pY64E$TVm+t4iNpCwfQB~_mA zK!@=_8w4!#9D3kzbo9VT5?vI%&ZVcnf0`4W75j;&|CxcW>L-$J(vhk|a?u+^ys2K- zyE%ta#(OuURP7a1yLL)>b-nc@rrG1SOHAb0>#MzeT#H5K@(p*z+iI$*oBQVZ&9d+J zerrb>j}ZoA240!$Q9Y^kXt;$tC6{^1l0;+riMM~Nu4o{sF1RW@F$!8PsY}|GMc=8c zDYS^W#eOm3rmd;H5%7!78W=-IFzPcZyN)oU4`p3T~ZUg8f2 zi_W$Sy$;|GKOVJRdD5gN!Tb32y&IZ^I#-Q{VWFE+AFRBtCJ`+49D#m$IbzS+XUV5d z6{kAb*u+;gBJ)+e^rba=|D@eiX!3(o`&JCzb>qVKNEHqH!DVmk3l$ID!0f{z&+}EJ zWe-nK#`_mDHmlhEF6usiHVGMS?s;FWJ?~q|Ark; zK%6QoN6H9=(2T)7-5!NionfPFZmgr80K+7s0g-i|Q-@MU7S5h?-bOQ{{qXEd+pip! zR$N6XgJ_Z3aEik-k(-HU)4TP>f9x=LYD^3l`21E9x$Lbpd~>FV;e(Pg`j6}AdI7otgPSkSVW%a>;w90tVWw7 zgs))DKXjgm-^U29Yh0Y2G z8U?&>XKTw>I$t(iwiAsXDKxTdVCDJwFVM%!Zcv>zSpJAyY=24n_9C}kAEaDFr>|>E zx|Gyt{nR*Xc=FRd)`+E_lKkJb0D*@Nt#la~cyE(;5Ue(}I?+U>i=(x+sgEBY>~^Q| z=;_PJY7wbBWQqkCELW7fO+m(JtK?B+FhN1mbMLy6atA)Ex7RHB2}x80bqy_VpDvPA z;a_{~w374Q?>s$L?CZ@yu?d^z;+sgDUql5)NCcrNg98h>P*29S&%d+l`_(I54#$b6 zx<8xJQDN%aL1OpQKQ9fi7@)bM^B|9nPp!U=ZbQpF+F8D++2-B&q35)5>qY%N+w#aQ zEmN;oYYu5AO7GA?U>@bv%M+s4ddK=nx^uu=7iU5C!J@xYkdx-rFY%8}NGGv-Z zl^D@dB8l0oc_7a6{nqoZUb%7!_jvjKyyq_+GueFfmDa9ZtKK}uZ$qP4>a2FT9omBg z@0~vQM|OKrhf|4(6nX4j`*#Yh6_sXZt1JY@atHc!*VoSaW7W~Y1ARF|SfjU^EQDw+ zl>XnB>RLSPp#RiCNR8?V6efc5vucHDc)#NlE;| zF;i>In1G*4Xfn9P0RaJc;+&no@WGqsOImnE)QNX2ODgE1h^~~)sIqdBj*fZKy{^;q zP}_i74d=J2htCE(ln*&{Ly%eJ+X!}U%v|%EXs3o5gboVSr`|G2HoI3^`kT0qukX#m z!U*7Ls$L*7ApJE~*-a6I$P7Mw3lE+_%4AeX73b{A$h7{O(bZG`+k=o4$Lv#i_r3Ev z?sQF?vrXjU(fDtFn%BzL(`ZoFgc5Ya@Ih% zb6Hs{QtoppOyWw_Bo2{I$OhEh++1E>PCU{{vov2BWJE&p1_yEL=M;A;3EQY`Gc}{o zOPw8Mt+Mot5^~S@_!-G2g3ToDzWJN6W6HLF8~oB>)}T=H8K;bu2X3#8K_3e^0DB?? z=C%*4U!WY!Gd(#HVIZuKZzSA&@Hl1esNXWR-N;y*!vrD|$xzk!oJ<*?^WMou!Q zmuGtQ$%vGs`ZUFXZ{7!sM8B2|hlp8n`pJ_9O1(gLipt8)LZW~&a>?tzd~x2isS;Ho zIga9rJj-mS+mON0mM=_1a#EZBn*N&`b;c^cv(I#O?h^hpS3FBu$zEghik+IJj(H#u z)TMU9N`RGtQd(y3^r^6&7-89R@hkE7P3pcrJ~0_9_|7D91WGyLY{XUMd1T2+)3G); zgW%crL3gtKfPEZoC@U)?NoM-sQo&|W@yxZM=<8*%fhd?&&CVfxx%}|q4*cDDfuZxx zCxxaf5(JZ{tz~lUnXzQ9$C;uXLGRw~b6;UygSi?V-*9d1tb~NsP()bk%@;53qev$F z&~b-J3#Q0VBRnI7n@nWiIH(o4J=PmoMqPvc<8u8ikX477!t+QD34#2n z33BbNTQkm`Gl-lVTx=`06G`^x&z~joZ}<6FG!Z@BVkf6_O@I4(G4JB+{d6yiBnJtb zW~yfMsGUoe$hOd*X@A+fd~o28j{)wNkRu2|`Ros9@uvGZ6~+C+^Fnm)IPAW ztF1Ps48#bOJ%$DbjUPWY{b(pTZV>h<$)d$K&- zI6!B#{M5>a4^d6xXiG~^Cx=DI{wCk?oxQeePsfBIQU)o5Ym}sBr?8Xt@(G$_!xE-r zQnl&`OqVESRYK=%DW*Iy)P zEMFG%%rbupGx@FFs_Ngqbf#q1=^GCgh;r4O2$SHu@B=Yud3$+bo7`25-b_de0B#d9 z9B7MC5liXmu(P4xqg`V$@oitwV4SP}+43E~?j)d{W~UT|CC!9b>5`NK?g325Pmb3h z(n*)wEG8z#KNJ!i0B+n)>=B;aj=$F}ei`v?aGgr=c0{yv`%|zJf*7Tlsc1d>*w`pI zufWt(jFpAp8|s%ct!W74x!Jk7xq!S#t+TE-4Gyf`C8Z8hdd?+>GQv!g&a- ziKXSUApu%?bB)5(tGQROqFm$N!;L`A@&ct!rB^3il3dYAl-t?I!F8j``B1ssUMDZw zTIFy;$_PA+n!{|il4b#Ow`<;*Rjxw(k+)343^(yf1& z4x-Kjp9nmpC_ma?a?ARL0oI}x+in*Xgsd1|9#-9K+c?7YzpMO^>7h1%ea)qz#ptM% zdQ}#udn(4HrD3?ck)J;Vvnttatn&7|;j6!Vc&fYRv`}D(I%~BIRkwU++Drr-#7*vl z1r7&pRkOpD@7L28ZosnenQ=Hy_ZbY^K^@p^VH-VhPtk_!V*cVH3DX?`9X-Sbbk^$7 z#o~R3HyEPc*VcyOd`j!kGj?Gj-ZkjqE`rmai;(6#eOf*Q(#ZjEej+U?wP$r>u8F2> zK05P%zulIZIC-Z z-T7TbQSg{tBK&ynm@K8kx3H%hh$p6`SdiNX*L-YY9cT*jT>BW8Z+fxVy~a3DC$6|%;Bma< z@DPh!D&k+^enca`c#i0Q@ZgFHd^;O9ZnRjs)b7>ugvv_s&LS7n9Ur&>p=miH`&(1f zd?wMfT!?ac?t{3MUCepwR$PT_(`pinh%+Hcjlu-X2JVl%-Q2YE)yV3=w8ZvdDYqJjCl=hd+c{-%S<--w?^i^?AuOSa zbih=7iG2EwogBaXV%98M%DIdTbTJH~@{`iLMFXowIdbaMdu)&-jxpot?d#XQ_CAd` z(D6>yzRe`uve_rtkf<<5#;?xF&MuLOKy1u%-Xz4ZF3%lA(L`UsS_bYYSfyV4Zo|1r znq@CfK3((MkPcBgwi4|h&WM@M6ONwXkX0x`XTPh@d6GRmKb*E)x5HzqyQWW#Fr)zhWGDp zrGcj#cFO(_kf-w*@UjyM+2fjC+io(Ml3KUyw z$KRl-4IiqvH}bLjp6oVZQZ^-m!jU9n<;e{Y^KFtVcC|0yU=Ch{|V>e)u<1vTqDl*;-9j3e7+iwwsJ6|&9-da3J+0HSvh{fgq;jy$?{i0g^2DH0O!M{hW%#A=BbwL z6sIebs1NzO+GuOZ%X2B_21``uVMZN657B0nNZ*Ih?Z(Nl^}2-Wz)g2!M~w~+o>B4yKMksDGM3pgeWq&;C{B2>h+T@Pt>R*%)25siD!qJa4?QZh zIzQ>#9Uf?h%R068%@JekJFUMknNk$=YAvTJ9^Gy9JG&d+ZD#PGsNN&r!*)=YeVdz2 zs#xdP>XBw)?nLD}uAd=07#kmOG?ftwApADlm(>U|MG{59RZ#3oL$*FGWqSiIM>fKC z+eMMQ5ttHid)k_>ZFA|OtDjd%2ziTp{ruu&-AY~7u3HzsFp&mh?%W|>d#_sS75s;+ zX{^0#_O~5M5j||nKGyxUzA3D)cj>EV1`vjEopvN;)HPHWWUMf-S-pBEf#V>d-jV|+ zx^LoJdMNmGe%UWstZz+B=%nO1vhpv#;07(Yaii{Cu#wEwz6){25DiJRg@_V;jebrsURHQ(^O zo3CB_`q{~K^`@V~?hiV4{Y%D%>!Q?6Zcolq?RYjlHS@0CeQVsri9$v^CppG(kY}F1 zm2$5z+#$gwmhodl10(%1cYIYO)nA322g3UC}#->AaIL~%wfwGxA&Pfbh&n6 zC;;izov1me_=G?)Id8$_iBj+5M>dk#hK4qBuOKA|<6(mk5;~cqP%-gEcNakM1vuCH zWrg1zafvPsAVd)t5I`N5PbiRoASK62Qww=R*i2+(QJ5LkKaXE{_%fhCu!;irC+myb zB&mlYr``%p_!0i}$2Htl_wJ1+;T45LNlAcPX|$dBxq|>2RO2L7fR+A?ZvAoGuC?cy zWxMr=6Pp5;2*tz`Y8W~BGj-3g2DB{IXoyB@X}OXtan7u4^~;r0+ym_g?-e`r&Z)ge zRmUANq6%q^Svs#=w`%{tM_SF4{OWJMQkxG5z?W>2RJuk89SjX!6XViR&suJnx;k7{ z&baOy=^VLgY;{06m8n+K*hXoL|FecjEj{xmg^snD*1vL95gRAe2&!J9=E2?pfJxUz z0vh&ZplABxHX5o?<()*nTn`rrz*({^2+O2CJ4+|sa{f8^;dCj27L_R9zlHRRL z^xSWsV~2c@wPWJu)~fY0W^_`--{ZTJVz!s!OVouSduI*ePn0?0x0_s1(`xRq3y<*a z0L%eQ7(nX|um6wQL0#g8LnUQ+PrZt>;}GA(c2LNk+;k-G$J=ja-WoQ=?IZ;{&Kd;V z=}7jtOMw4t;$26nn?+>0=Qfa-?NfZL#4q+aYFcOd1 z_<4=J{o@>W-S5)=D2l535V`7jsuWX#(e1Ch`fo^7?AV6Lx=4L&ky{`0$>br=D9|IO#Ofn&Q9G5A#PWBqPY z@)vyhnCx9uy5J_to|K>0flgSybH!Py|E8AF54hX?FaXNEI;XdOso}t-`T-JY&n9 zny76qE-+iiSq%>#hD+f{TxM+ivZ{)Rnt9@=QqE;$AaNc*&K{C2jRdN|?cX z)U1WK#tBxL-QM1)s%9%41kZW?+&WPI?q4fjo*N{mr<*7TD+k}it*W=zbo8sFMMp<& zS3+98%(J(Jx!Zday60hd${!79mE6@J6!~+w#A4T?Ty_?#^yWYBS4Jsk!t-p@t)f=DnHKng5IBAI2 ziF>cXN8^tlUuLahxur*WTl}A;W^Vm8J%*a87;mP)gXc6!3f5P`%!422>}}uN3l6ix z?@7G+U?7t0=X$oCN6&|u7rYk*R8^(6TQ6gglNK4-<8MGiq>B9b_7V%^>P@?6XR`E= z#&>UHR@%^rwuc9{BmEbBApjHt86nE8-?PlNor39qB@gDG;$~v*{_CD%zmj8FhAFfqzNW z`JvoVB=ft5mS|U3U90w@(Ac;Ef#Q+@ojg4E@P`PaM>#}^i8{-->6R_s-ewNiw@qYe z`AdF0q1{TxZgqUrhQ*}+ZY9_4ammVCTg9wA#3WB%t{nU8wyYN~ZV=_>=KXp{m5?d? z8-)mib_LIV#(MsGlg({!()HJy_^G{W=h4oyO~YL=g10-``s;f<+&xxO)Y$viK3l#A z+Q(G<*MG6m);W5zHDA%JLzGwNdQqN$6;Zssf@sTdmcB*-DB$>W(mE7;8FDUyNp~nK zH^KV?R+R7G{Sen#?K8{Xx>aFcdS@WSnO%e@Js zJ7XA<56gGwM`Ld64H^{Dnr3*f<$7PTv~R}wV@r?wDQUaE0N?G3L%UVJ@o{@0m}&yQL1W7wED&ct^mghkl-Y`s&bq*^hAbyb8$fpl4rs7u zu=jx#?w(RlKdDRfHf(#-Vs8XezJwOe+TdRRcfJR~aX)+IT#aLypnr4l1k z#gy3YxpTWkMn^;lvX|rW@!*-WOI*DH8qDLBi(9>2bg_>!2n3$ z^FwXh1B;BBc&YZir2DsN=+h!~{>%gE6`;3z>DWZkhlB_cZnBxL?xG6T<(dL&z-!)~m z=#jWBHCCiHV*8^P9Bo2tl|N}t{HK^b3XE_BH3}sXw*SUUm(>gi3ulW4+eR)TJK8jr z)Xb{S#0S~2!q9}G7=VZ+azQIHIw_8z*ngrz1 zJgPXs)ekkD^YinV)22U|h@ILqo8&~N?Ffo6vRKQ#V%+y;C#N3({)EFUp)WTzE$F-% zkrBTv*5oWNF&QQ0voMiiSa<83wrA}`4u8(@TYXv|i=m*rLbgVw3kt$cgWuZscRUrO zk;l?)Zfe@%EX)9EAm?noon2o7pE=pT-RySGXX>u-=YA1Zq1f-}((@wbU?`wHBStRC zBngdjYxA3wZql#bL`2w+l2XW5gYw&jW}!+xJGtb(ZqddhwA}WOFWhr5YGc|IA}72X zE|K4+ls;#c9M)h+g?{hp(;Sp0@$ohQ;w+D$J>98-j%?gA&fSF^J`-y0wqKT7)A-IZO`br0>Ytg#}Dj?;|xJaMJ0>`>&1loVSz zZ&f6I)}|+8Vtz}xPpD$Z&Y(H*jfca50`>N$q>PFDK-l4vC#tCz2uwf_VSaq=Or@ak z@cxl52_FlWXzvIkeSw+PwPo6|cJ%9FW z>X@=K85z{G?W>E{_imRL82*94jmisK)&Y;vbj+Q53p?cQzg@mB6SO>&pSJ$QrFxO| z3w#23;|3*6J4RK@cH5FzCciK-C~^%T5&-nmY$;2JRPC_KyHdsSc;p|ImcpV+uM<<( zH&$M3YAV;V?AePKD_?9~@Ide-$<0AZrudR~{FPv_7M0P=xVn$efI5EhbgdHoA2&P$ z%zpNaR5-)t)93~jEKA*i_YIl5*v~nT`}O4F)&Qf46DOAWfFlz!R;*9&UfA@oM0R#c zV@xt~hoCvt5Xm~24yf!lD-5t&-T#ZOp0`!=fxxitPaYODan{gP3sXBTU5e1DIeIG7H(XTQVySSWZax7OC76HEx%!m#Iu|>=4CGGugx5ftyA8V^K+j1qJ_szer#!O6r%QFwQEK_?%12Y}XUQx7e>(-p_kqcb@#DFv|5%rt> zulzbUB!~WV#N`hEp7&B4Cne?7g+Xfy1R&R%1?L;z*`wqnVFyz#m3{?*?N{a)|3E_~4lgE)MYV zsZ6);-=+_ILdMo-y#YU;M^;u{WW$x~eQ%2?7eRXm-&obC5GCMKQ0c-s zG41Qos}a-2Y(BH&xzBK>8n3{~@c8ix4x0rFIHO@ap*Y!FpIE}u$D8g;Qrv->L}ZKa$kBJWtwe)U*naR%XZ z#=FBJN zQTY4%ZfDR7sLgeS5_m&SaAeK`HpuuR=^ZCpGICn%fe%@F3lkd|R~A>L3|=^Kb)g(p z$`@zn_=SC?q@GbdK7Y=1O*L#bp8DIzKdV|d%5%<<@Nndf0N>r;RP9U6uwV1DTw1Q) zAw+gV?=tE@*-2&Ldt`nzmAHd$62Gt$YCd|~t1tyHugF7HYnOS}%bjW$kQl77@x6KZOzF%!?TXsK3b?F%|`=lp36(v@LXij+j zZsX++i=Q}EDx8he&BgfoZzz}Llzbqry_e694IwdnPi@!Td8tVR?bzXiDQuaQ(de{> z!>`4q?#@uX0o-+iopDKB^X31;`8$J z?NnW9z~sC;Z@xZZRb#${$2+;);%&&)aRjwX?7F_pVL-jqu5j0mdM(VzB!=FgMo?V& z`}rNZL32@9R<@Zo$|<|5;Y@K`AY7s9CkoQr1Mf?By~^`lF>WRQ z^POcK*&ap<-&UC7!NFW>kYE3kTje`Fc29S(+8<1bu#|fR(-WZ6eB$ic1_Wk50ho`d z?%*e&hjCs(1$$iVeVfNYkVGT3j{w}eB+=ohCr#M9G44&>9$u$#jDJ`(?l9VjBi*7e zYpEhO0y%mK93p_b<@?)v%T>KdZ8xT5$+d|2BsG2Bo`u;cqVo1_fulNf2(8Lzz{c&v zE=N2tZM?tT`2n!9l(p^Gr@N+M70WU&d9rR+hx)HyGX$ac05sIZwil0+7&B;)hm<2# zXi}>X$4gn&i~R0|%pobQs33alroehFG488MQ;<|vSMlAUBHZ+wAb8(=P;hw%H95Sx zI&#R6UGH|V8amt~s9A82HEx^qQa50{^`wC*x6?1ewJ#m+m}T zByTX#%(B?>#@_$0h_EQPA-`L>sE4gbtAzmb?Wkl@;Qk~~t}B~g`=VIlaPX~aSiurV zoZ4Du-KAtV|KZSfRxZp_$__3KZT%6>o4{+Is$QI9(N zci*u4H;vH83T;T2<+uI^4p)VyMAvNg=rdqI(UV+_RCLd#CMGk*7bM*yGoHBz8CT45 zWg35&X$$C7ZO^|ESTRTb>?s@5$$Kah?bYN9YfB4jF$^k_Km)-_>cg8MFU`PlYJGMuWu{K&gzJ>9bFqJKy#1Pdisuz0+ACD62jyerQkU^KbzNi=6M}J zoWev?K^RY=ib{KsY^2fGyl!Jqe_&-}60qm}*e=rrhX@hv@7`&%te7!^4a&10*wL`V zw-9J9Sh8d|b}z?CJ4QMabiXjs$=P{-K!9!j`^8?a!3~S^`esh`is1T#p+Nxyn>f5w z4||J|Mt!+6^dOyJ```w9n{3r9bqJ6gGaq3vZk#-#thg|9rsssJlaHLXN^KaW6VZSF z*@J@Hix&B9NXswOrtZy`Z+B`QB^L%Pw-l>+voRv@YkE%4r~z8Ppu6K`?%(nUVnh5s zpLD>_OTUkk&E2JE8Nejf4|oRg@ikCO-@AWbRz~Jd(=QNYt(_g~5Vx7jyR`eXuCH-! zu<=oB0neQQ7GOpPQ!}mG^g1ks_3PGA60_Lc81G>>6nqOY&A&-^>wa)Hkgmy&q#-eF ziQ;@0!u04gD7kUbf_4=b7OvW5Yq9KF@$7l?P$WB3e)3ZZY(gY0WU7pMj9iIx>cNvI zr|!Q&AP!TveSH-i`uFDMe#OX8CAAypvngT_15kYG^1?rRM#0ia7DL+yhMVNgN;Jl= z^4e)KvelK>Bs>d<#uY+5^KH;fk}uU!x#rX^p3k} zxK*qw{ds<7)i>oZ;3hW6!fVK-Rq4<+*zA1Seu}1rdvE=xKYm`z*flEnCJ<48(MIBk zG1z7$ZxelPJ$f}na>x2f6GYwm7H$XtuPy&8y{^{*nh*HX^%beV4Gt9az7(CL5%yJ7 zf+!j-bF2CjK@X^mnPK+efti^wC>*b%zS4Fi3wBm|1>gr{La19`*~=5fmhuf@-rMap zMd7l&druRONs>~%MDF{uXO=!f#x+yCdS)-Dz>}2MOl58vLRXyS?D-0lo%A)`ii0`r z?9v|KcL>xUXa8Uv_fKcM%2L|EX_DGVMuS5`UBK(IXH+DO6I1>~M@xnfQi{+}InM$_ zr#!Z^#KF`aoZmD=@g*`Y3gq_VyREk=R=qnaB66vv|LW%t2aG67e?BC&N^=y`J-(6G z+(i+fPME$}v!d_Ji;Fi7sd;pH_mt$Ox^8Nl#l%E%z8GRXK2yJ8#_eCUe$ATOZ$3N8 z>xl=>iFdMGxpCt>@l6iG>*zpofK))Nc5L5HPLGBJ3fAnuP#3S5!5r|?Ii0%BjbG>- zi}IU~-ROTeAyY~(Wb{(K=AKP!-m_qq*cvblG<#ro3ya%uZvs<@zE&7wzCp$iL zxjV?it>|oA+>oq;e@OQHqFm(Z`>3eM--xlgdV@Z}_psWah@N6GeUp+yCy%>yaQJbA(%y$=6hIJ}mGc5bDCniwOdhz?-FOpWVf{-1bN=a#DxCVkH+v4yS{?18H zW)^4e>bBLap-k5DRS%tGW79V^j1x%+Kt^izFXSOE3>O3jI}DA-2GWsK4(T=It;I*Y zbIy4WA1+hswdG%MhD&G8I3m>WcK=k@v(Mb~YCD2xjye1;D@Xt;C{W=<6z!@gE2gQH0b{!jQq z`oaRuE*oviRHqwEIIUJzJo?g1jfYo$JpRpeV+Pu+=!J<|OdMwSIT#YM`rIuCb>*_w zg1+8;4;YQww^@vVyf3L%YJZ?($i62*&C=rn8^VQ3gDsT-4X00kBw*M}iUkVo zj#vRf1Xq`_pcjFK^FHH%BS?jxz|B=%e*S>e9WEie9^zsAcK8zJu9xMW&S^z|zWk!Izf~w0k?-`u zUnZ_^ha&+sSYE)y+G$=9!QG4pOJ##d#!iA(g9o>U=u6aW?5r2BXBaYp@ntM)>mp~<}rb67y^Vm7`*~0nA)0}iCbQS zGv8AT*}==hLx1Fad9Qscan$31tzECQ*E=uXPEq`~^gdljq&tr6vUY)) zl*;eRePj2UZT~~BWQ5t4Yp)CE@AHA9e;krF;JQ>zg{7%XDlvOs>l_h7t%qg(`@K$QDjNfiOD-#ow4I56) z9}zKroy_A?T49wLCmaooJKt}tK3Xbw1~hGiiV8A&7K_O^_285{dspcUJ`}S1rtCMm zaW)gM4W>1Wbu!m(HRvpon=3CL+*z)FT#H{GsVcY-j?^)53Dr}J@~;j!6QFIF zGg?f|YG09Gzgd3^bjr(H+!m$oWEZQD*cy)wHPUvZ9f_b z5U@`V`;p4ZfWHIdi?YsN4hBhwptCBRAoe!MR^2MHm-2UA-N?eOhPFRP+&eWgYR*7~ZX$LQ{sn_Y5g#{X;0*AoT^*TVZ zwQnq;DALFG14IYJo*?8KWrlC;2;Vt|hKsepa7~nYfhm)2jDZDZ*#v34iojG)!5;wla1Om<*M$tByMvOzb6q;9gd; zlyDc$pXWg(tLYv3)@#>RgUUHcCA7PLLO!8zar&r|Re6)28h=@`bXZyLnN_C}@n?K% zZl(n2*70}F?~ql-tMe8n+>}^cUc7MFttZAiJ0(Ww9^rWyx-kk!U01Cdhz1L*7sX;fI4+tL*J)TmZHNyD`rc`FXORqGg9aA;{il2 z9XK%P@U6LX=O&*#iNzxF@L`-@73JmE6qegaWiA&;3_H6f;6y6&$?lU$WPHVVRkF1Z z{EfVcIo$;j9x}$)Z{7&0gwP6KoxY<87FXn=z7B$k%^(^}pwfUx)F`|pE&{Xj_`x@F zFTm@l;g|ldW)rKvIYC9e64Ze*3;C^$dZjQZ_rU{Y+ls6B?b0$b0D(UKXlfw+iev+{ zw|5^sDpY^gFE~GF(%cz0HI^1ne(L@CZ+AoW{wfo)3P~WUXKN)ahOBSW?`&p!kL;bG zzEs^+vt zmO?-Vge>$WaF4(KE9^dKlqmk6H#B)2;Hd1}xwBvp1Y3mAV~^wRf4gPzQysQN<@ehG zsw*xSkCy4JSNo&x8aM-nNt9miFb8|gnN5sr8 zc<~W$C&`K3B_vEcFE4ToRlgrQc>L*-mkW;c@;bn%E=a`oJOcPX@+}V-7h@*}!tI#-^l+^ztD?Z5Ta)+ZU>0UU4-oJZi59P!} zW9*2KR#drZ;xpWj4f2xf*YD2p>-vBW24I@AGB7Ppm@r|#@hePZUI#eanvbh65s@dC z0@c94fa^dg_)oI0NU~oyz)R}++qWlwNNZ?lG+!fAL%L=pRv-GRc&atVj$jzSetnPI zMHq?L5~Uv;xKyNF6p?RwNwEO7lCNt<)f$^ag68v?obT@LIo#Kq1*5|xZbi8HDMRff z=j7<3&IS1yH?EQ5s4?fq;`i2cUV7p}JDb_KBFx~L&a<^4`9Ya7eG+}#h6NM^%_m%{ zyrg7ai?CNrFOvw~rMr1!j~$&xmJCd2ITZ=BfmsXCo(HD_!Veq3pVc zO`0=qmAW8Nlo?~b-rnAgjG2uaKM)(|Q$3RDlxu%hziMb&u9^`O-=_1vYpkp; zpL<|+V|w53GK-&obV0f}3ga20Oz}L?sy?`?e52a&OS>g!*;ms}=od95EC?GNRu;N? zw>qnaEU6j;?-R<4;u7;-hV9r~F-m{iPp!?pUkCJ2d`SW;NXjzAn6zuP$JdN|y7Pvn zNX;?G!rHW6R4cb9zo*Z1N>a`eUi^ZU_I6%R@s3C?Bge=CWsX^vK6?E4H1^Awm?|bt zG=A;^g+T7@9BQZfDmbV370iHd;8`TCra4>@cPy-n*8ySP#EBENNYoK$J!Go#rcGn# zIwTD0SNHCn+mCOjf!1;77%LYssR+rFJVTO5T^jGWqoDu{Phj@^{U=X! z0FSj@X0FlO;rw?kz)Wx21M)yfK457%$K0t{t2L%5Jf5CP9tH*FxJ_)Ot@kT1l?R}0hPRMbnI{FEt{2s_l! zIJ@-hYC;n4ShQb|>9_QR?z7R=YBs0NMA{AO?L9K)OwLT3Gatl1+wMmDfM1Ib%2|p3 zKp^g&4X-cHG)RzGdTq{7e{&t>8p$cur?;>6zIN>z(%lCLSl!A;%=Ko;_;#?F>hN19 zkD6kFiM+@|X4scywO3B{uq;%|3ZnQDFiKHd{V4pxGVk9?O8(&a z$_wTyA_EuJx4{VXVkE>3e4w}BE{Ce7d@*UPX+5S4{>^(~@ghS+#q?_{&nC-0-uoeK z!Dqk6hENF)QsGXxrWS0wiqksu>joSJW&~X1=uvHXa{Oh3v5AHq zdpjBcE5jjtY$(xvGnpt&kB>blubnoMord6mt_$Xa^N&HaRYl^G1)2D^Zq%xT9TZbB@7md_6>Zd)jHgzdu9l56+2aL&f9!HgKu>jgk^wszIv$&703!+gz=7 z6h?`11K90BIiND4Q}^Fs1LQ{H{{7BuG25LZL7r!!iE!1)xpWD0Ii?@d?PgSS710Nv zfMv+x;W_d2AV3CQ+pdp&CywRC1n`%oGh@b8O1nPDNZui<@?DYQ=r`JUwGLS=zOk@+ zbH55`!@Lt>5wx5H4LdQj6{rqE`ZF+Rii&CPAnqBnp* zgZ!AaW%IA%j5_RDLlb)o{<1=pfCI^vz~E|{?K+85y`hN$eDo%2ETj@B6<9G8VG}ky zazPE3P)z**^TANDJroQ2N?m*OSUvCH&_j!pOo~Fs=HI-z_E(MQw31nVY%UZg|0v-q zUQmto@86#T%I~W645?v%uf`w}6#{Hmt}LslC^$ERumSv)sAGEUdGY2Ac>N9O5Sv-I z;ck^>{JJPMijR8G+IU6qB`K^YAvF`a#9_uC# zH4aIq5sJJ*%c@i7p&IaA2zVCg%2} zrzezb?RT$;D7IdD2r6nvoujv-FJKLahKAlPFGq=&*mo8}>{aSl#Wqo1a1gd#q_!gK zY2>dW>v`!6KN*g*tzHKR7Z{|i9cgrMWN)45c~8|4mq+M3Q@VmVQ59fzquRoB@Z}{v zV{Lzg3=2D2#ipdp6R&Cuwap%N=tgMzd5SYmeLe&7HWC)uA+e;KkhpbbNNo>LwVNgE zDu{^Yk)E(_hF|ltV`FWyL3XLp)6Sg>3J%srnnz6ju_ePs_x)qw@@=`5$Gy~pZPt$U zHVaC0oFy93e|zP+%;l%@4Wh17Ns&8!Zgo+=6~oo#XDlF=2Vrkqd7b~mJ3+$qUAC6B z_V(0s8}012z3TfaSJ(}&`O(b_iQth@)NTD*M!L0m+aY%wS2Q=KLaRJ&tI(zZ@l%2} z+D>V!i{@clOVkPT=Rmt?F zY?%%Y^`IbRdy{1VWi<19+{}#|$CSxz(-M-usjJ9pLJmpfZl98V=`T#3JNlbjbl*(K z0EWDnCDxXev7gLW_8rCX5vWg2(pGd4^d+PT7fS3YT{A0nUKbnXTX(G`(nc(*-tQsX zqh*-J>hKpou7}!hvi?r89+!csuE}qvUqeF>?C005<--Js3WV`>lj8!9eLJAGnzPE~ zqj<;miN?)^9}-5@H@WIWyECM#`4mMA0x$LgHk{?EDW^`EMC3L=#6;=1T{wD_R0?_n z3~d^n6urO${`)d-#@6)Hr{Obcngq-41xn)6HH8O#&H!D~z>>I^_Qk3=|lJ<$*DFN*xO36hl3OvK@Qv(|l`yh9O<6Rgd|4p7x zA>>+(F-^@aC|JW|O`FMQW`ig!T#xv|X*;J9$LUfOn}~o8AC_&&Zh6|;@|2`yp+cIm zfV>zlPRbBLWQjnvyj-A0eBBrBbI~yvS;`zuwnF#MNfzDbrW`+BgZ@*h>u&xzd{#_A z^PMm(@nKYQ6&1G+w_m85oSnUseGIG@>8O_TTT1Sj)`mRkLv|zXChoUc6*SjKUtg1> z!N{nG+nwut4e8_OM&6$RXzz}tiU&j()&{~bs$}5!t)4TtsaMB2iU5ZY+vby;bN<2w z!BNB%qO!6@flPr&;|&cByO%wabesIb&uh25=-QQn?B@&x-G$_(YscT)KnOp5B}XSG zDB1C2$8I9kv%X&LK`V-8&?t&ChIf-p)HqO?UAA}cL+po5f|Mt=hQ|yvn~s)#ipj6n zdl5BDa?11>GhoBG%JhshnK!CGK^Z}5BIO!NPXsW;GkSc={kwPZ%{ve`LnmoQQ^ILl zvCcTmM_*~O>o+ww3WdkM&!~AQ9astou@G7aaSf)|yhiSw1I!Vr>z=(_)tzeVXZ;@C zf@(*wWQ46J9>p$KZDo&2m>10jB~b(-JgfmwGG-wU=% zaq$z0i5v}=(5jIshyJ~&2n!0eGtf@XNMG3W<5OrJb|4fH@;-R>$y2C)`<8j6^W3`? z-??)pHTA%IMpxXua|g{l$?le`R}TxGQ|;Ka4*w*&zLDuucyf?1!kZ8SnEknY86mI= z@vf!|58kS#xo9E^Fd@R60o-&U)Y}A^Qoq!2+5@3~eZPWl1HqCJ zhaB~&miygDo%&VZ2nx8_E`nIAvu8I?o7Um+@vN-Jxw+4<>cxH#Y}h0lj^3sFw$4D# z!usvJ4jzG@p8rhq-t-*f7<4l2;RU0PQ^jWfP%$oZ?-*e09z4%|^_Irg%%Q!y%#DWo z7`@^`P&QEL`l}bRPMp{)b%*3CLY5-8!m>QQ?Izu-&hixIB=e5StvN~Uo9{kn#WioR zvl}VM!L7!9vYIVm2ag{8fM>|R6E|k<_STM>i8$aLz>bkWoupIR@< zENn2&fF@HtoaEZFZzF^ucjlJ|M9}K*`56`b+*h5)kr(@i??qpWBmJ7o7E()4-{n8o zb*Z2FQG4R2=x#28w?F8U)ttoCR0nn&j{1J-nK%)9?CFi=mX3kft5*}ql%c3V1%{du z#@Aqwlwi+g~x-hKCxd#CpmYJB~@{oEC742|8JzwZHu zCPZz1gb~+mTHA*7N8uhj(TxL1cW;AL<{*y8I*+?rpo%E;N0%(#d0}S#3_OUejI5)KR8gIRSCcAVbtUkJZrx&*yYZu=H zK2ABm{dgWflg#;Y2FwGMGWrTQM5lKO3UQ7rhgF z{UFHnjIBREw8AJuv-Q+tTcb6YyERnDtr^!X()UMfxrp33a^>X_MogL0biyE!*jOZn zzR|Dy9f{htYZ~5a)?M8Y6>cg|o_g5H*x2XvR00O$>Dey2q{czBvAWn>=Wy(a`xfrn zlO|;%0EKcy$*Ho#AR%V{@eyAF7i7vBTmP}{*qS59Z@Pwdp1*A~T_25$4h!$M-!22D zc4>H~>uyKGz-ymc$d>)i?RW}4B7~u1UbLtTAT1+3ee$G9@d*h+HXE=jI%W#TyW?Jw zrrk=W7~wAR+S?lu=f|`rXH%CF`;F3*TnjyY*skpCpQvhnfLfX^T`I^w;tmJg45BQ3 z!#OzOl(g>XEI_9p@Yh1bPneK)g?_FA-J$c!?ruuoR~rrNM|0;@CTVc~Lz^{wvB@YGa6%fEZ11;Nd660MJQzZddFcKTE=;PJ zKHcm2oU)ax<==M-HV@JYGoPc$$leFN{jQP*QM!c*aW9HDE0nbBL`Na0*k`_k93uLL zaZGRCxVOBqxPIlzzH26yTDiZASzgkgJD0QK(kUVVL@IV4tL0r1xN025{6F?627KHC z1SNU|V+6fu-041Yl5HfN)ufd~Lv2Dc!jZd~F+c9DKQRN*-CfuE&3^CI=dhlw%_*&S zwhR`jUCxp^e$MV)*ggM(`*NaT5(%>1j3CHzh+nc;vg9y{nd|PW>O}K-WfG$Py76zh zO()CNu}uqLYca>Mg~hyCs1r=jsceF6;PhMrmW8zv}(iqMcgrz!70hfVDt$&C5H|im`^AOKY~q-HcGa7O&{)(f&RCu1)|45 zyOUh%pOI$IMH%U^wgp@7AYj2@sUG^G#X1$ zag|vdNBJcwDLLHh5-cu<1{v`OZmePh780R;gS2FNw;yR9yK*hM@S=QYN?WRA0&(lj@I5W0fz=H zd47Hz$p_kebOTfg1jUf|VPI&u>-XOYGMyQ}TKMVXpKYb%-Fs^1bZZJ_1e#_9w##jl z6-3Z4P6RhlTv|cvOy}sb6c|jXLD+%^ztmTG`QdG-FQft#fu;Cw5T# zJ?QC(U!dvK!^2+d^{|Y{<;M%JU&!gX;WB=>H)F5%M%;w}TlaaB@Rt(1_=+xpAtBDF z##h)1X+vr;mQYbUNR`A$No*rwa#T*>P#22PR*EctXGfa)^?zqim_KjcJjM#JCRpUI zuC80RPTbM@FD!BUtG(WUfB~{-DU{s2sjR3N19)-TE|{l{=5=sjK}Ch8np$dH+^&(m z_4KKUrcU)>$&d_dtUTaGPg1T$mfWkqL;*)fg2fsyUtU<#KqC+CN2VUe5phMV3@D9- z*}#AN!7wiaBcs)I46*hH8bKaH?e*ls7DkeLzAyX>3~z%i>;7M{K8+SPg#-g4{fN*! zwvgDm|l*tgPgU1rkG~yTT@p@a@%gH(&nR zwS?2BOLKDv4If^^)<%ra8RNcW1tBv|n!GEd58~*%!fdmX{+^8spke_rLH!r8)y*hYs;nJlbr4{$? zRlj&KEklEkal2c8y}iHog+;^u!66E9JsUTsc)t+t+R?Fsh6B_`?OoJ}!?)^&=utPP zr==}Eohgb0X;*bCSVf%z~M2xU&=H0AF}SAx2@;X0(6V?Ut6UmEGb&xFF^ zxd|07EGmkNi-YD54-c<)+{UUQ6nkzfA+BE`WzdsSA%T$V>PAc5`H%f0|A|GM0a(Qq zYcRbDipR)*1JW8|A~^3w8hq$sdzp4e(+BKE%|rYb^?gY3lP6DTUiq$RxM=F@PyF%6 zU@ucMGrIN>fObl~lEJ8v*}rWWU-S||DK?(QPoI2=0%jRD92Vxu)EmeHp}mn8?d)0m z;oP%6=v+GeQrP^rjL6>~b>Snn@g~E9e(7%BOcpkNkB{xUDR_JwtfKM`^}y@K(QSg6 zB~9~9O)JP0oHcHY@Q-BWmfUB&?Q8>s<>%J){QJ`nS~thYs9ZHjU#VZ|UEvMP*Hh0N z-N6*T6j$r?y14-q&vINYe8Q;yGT^dH%*@!c7kf@s+w%G@a4aHFwa=s?dru#HqqsN<;UMxK zeI+5JZXmP04(lrcMJ(D`{%;69^4Y?ONtlejQmn@7H&d{|Fmx0O@;ub}q{JAnmI zxAFj(j%Kbm&-IFn6Q)cF-`OandIbaw_o9?VVJJ-Uy#9i-oRD9EZ}gHnZE7&ah2qQk z{iE690!Rk+(?yZagVum{M83y$V(RYPaa^S#uxu_yf2#<7=WCEjKnQ0WHa4XHQ$YUg zv37TkWoPT?5i5TVN{p6ztbn zQ_Oow=|q=QJNEC_uU3aJ12$?eDg3cv280-#=C)CEi z-!3SKxNe@2Jk|0yaIUF?Zf{X#3srmaM6cbm zPI>pXv=<%iK&(VFlXnCP#j36B>fL`o|F`Fl9lbmh~#? zEyMGdGw2sHYkQ$B_va%QM9utId_4Hx&e0iiGBW8Hs%m`dVMTeG*(y<`aeV!O?&6rE z@N?vPwk=K)mf!_ui1(F~({%mjU}pB9V*L`OURm^)CA|qg9;Ty{&6Z1h%J%*^`e)K6j$`o;`Fr@Rr@Xg@=TcGm8r(jK7APh5j@8NdP@T3BY8OcKw2ctD{DEaU6T( z!iA2+Q&BLp))z@HW=iU_XSqLv0AN_Ddg1}sUwm}=M^o`9krsy?4a%+JVsfXXe5ON2 zy|n_$VOz#UgFJve`Cg%5#(qpHwt$_^3sav_bX>Ru`?~*C=~3e9i)--qMJm5vlBWNw zvNI2>IdA{?u|Aa~TO*YsLYiStNzr1Wgdsy#nuHSY2`irisbDeX(-|zjo@Ar0}c=J)x#3xl4-1*1A zr^Z+Z#`Wv{m4YY&DZdf& zfVW*HD-*T@cIm>x+R`##EGkQu)Q>0?DpJE46Gpr{rlNO4iYgAB8>J$e*rV<7RS=>$ z8C#us+=QRA(x7ht_pc;AZL*3&b!m!VUs`37z;ZS5tEt3y=5A_Uv!X6Yhzn<;LN&y* zro3F+a_86GWs9>zKzx-oPAGc6o7rC!7@~yYMhn5JzJY<}Jwc#OG)$eob_{eq(#IW- z4K4{*(#5y-&1OCPJ3$9{%-q00*l;yJ*2dmG60kcz_>xJ~svs}tOAkzB`XW*8L^t>0 zqK4_URaGgkBWJn7mH=;WzuD<}X=$nVy<<~Px)1FWzEP|no-El^d8x&zlPE5&Rq*TP z?q1(5A}Y#}r+P5*E_PqVsDevIk?w-WW%+U~ZS9Kt_pkS1<({sYSwdkg_w&U&cTP}~ zpOaGZPXUFF^jKyl%PgGXctIQA27AJo4`hI_cXYpJTvBLI?UqnK%B6AZh} zk)5>3>}w@Bq&}5i+sDAOuQ{ z5upz6qH^8~Mz(p@jwC~FGhu^ZMYQa!q}ZYDrQYzF0;pd=dh{S`do+?f*z|7wV&dcT zlR|8~#GdzFNzTny$+E)ILr5w7okUOfn!iVNT-pV(fka(wzj@!7o;z$Dt*gxj$It9P z!{Zx~sP6y{f$=ull}nZ{$JS4qJ55C*%zDh+&u0<+27ChIKp$UUC?SIxM+B?J&ag1{M8&4lD=RFX{5#@PCC0sO_HAF| zXYig3U-k=qg6&ij9daT=x!|8YU*8SNK+(e@Z9!ZiewiC7ob^cn-C)yXYUdfB!UPD` z3n7R?i^4K3F|u9VPR$p8R)JDxSez!&lboLGg`i~*rwhID41g!vx`(SzO4mLc6%jJc zATOo(_dCA((ljTAr-rJ%m|T5iag!23U}uGM<7x0U_eVXsgrh z^cwR+cJA48r??n!=3J!@B^iTU|6T{%=0ubv@J@7?+5-k4$G)4hQ4o-4Wc-FX4wOFg ziV&2=$%^nCjbmGlckRj-oXUL)-#{SD@EP$77A^WhUERgO;rvZg3L7aN*tnP<3u{r+ z65<#<48;#e7JR#b8r|43*ZI_i3QhzM@<-9NFgq>XehW3wMRqz$?WUH9XAg`N%hs7Y z`owgxdt6uNoaOWDL>;tn4}DCes`Bi^W_r5LIpP_6LhWi4?FXuTIk)wO%gIAX3$e)g z!Ny8I0$q>|qDv86BlvU#Y_;uNab&LC0f{Q?^h6-&=#lGN8n7PmIKY1TC#M%oXG4YgI#hQNZu;!EumPOh#-F))n@mZAZ6 z8WGW}K&=fm+cNfOuWe`;H_j)RywJ?UPF-tS?{Wyr#Prqc)(yIyVlUGVx0OjwCymU? z@3aT(?@Iu|w6wIGoFtViIe*4no~#>hx2gTdrXj0~n>FG;h_;~($}eIx9}J(-o| zW1`vgyR5x>!XSB$Uk8DMY(wLlfUkZQ7CBTb_x7fK?Ow@Yh4dpj=RU@zmoDVI_Yuaz ziN#iy`x(A-&>-i}9qH&(eQoT)?0;xurGG8Wa{Fr0)eFVN>Bx(nbqkJ|S&omcnx9_P zy&w?v_KVZ~obyXdO6HGt@tmB$psl=^Jo!}0T6Qt#&){&y3Dus($KHLNq%V0PU2HPj zXk)idB8j)q8WMH(fT_%l4S(P&wY5L6z@n*#89W+UTU=LCp&&?4or-VX72WRqy|_6( zroN*jB6Rdb;=kRpH^itmf_!;aXd`_Jun^LwL)AK->boW9VhryblXL&JlSjxUpV6br zJ=bqSp~4aV8&YpXZi3QX2#4{VP!Kq3{zyk1Ra;CNbN0;>5AhN|*^v5B|6d6G&{@+70=HwMwc)+LRX{q~Ky-Z!PvN&Vc{T}5#sp)#0r;5IoPXr1FStKv&(T%hz= zX+Z5~UdTH%*F0?U5Je>=s@gAOmwh{L-aK&U>9t{D%Iv}Ac}L*Ut!r00tMCB-+7~os_-YyVFN^i!V+F(T zr=r`a-Xo)y+}W>u!|0(B1Ztb!RDgM=7?r;@a8lW3QytOlWN$ii>lVkLu<`+&MG}$b z*utNfIwvPfBIanS3Pnt6Rk?EW+wYi<9u2IyI_qOobk_NGt!2wpkK_9pFMxntJxW#d zjIZ?-dbRz>I*Gyv;M{_})ndcTE9cKwk!3`wxV6@1%plJsI$VcUJmX`-$`#E(ZsCetM9_h{j7^QnF zD~KAb0AfXR&1h>u>r24CbO-QPMp3%oLFo0^*A zgjH*3{6ZsN(u=?h(qsT0xNO6Q4t@FRRc|Xs-03aHvk}W6Z$>&)cLTtooe0#|zatua zKZoT&@YIv1OqpP63MWCbU;TRtsPAi1+hBw+G&uj17Phlq0V3xN3QQ(nlmV^~85={3 z;c@WNFQ$>gY4iO>rHdive*;dpg7*f5j|o4RFP4cBHZeS*L+odb))|&T)d~~r6nhY2 zH%G_5G(6VumP;cm(;QqP^H(e6p)%jQS}&vPa4CwGf3{z}Z%<8wulf!+gQF%pyP4^X z_3`!cA0Dg^P67Jc$5zGAl)Jqa?Jci^bEC85T&%Z%0^Nk3gLq_A?D=k9Z@9$hD0ni|oG-8C^<_%|S9@!ALAf+gOa2oU1X z?qiEt2!5rhvIt`|TvV#Y$2l2S16(K(h=X@!WEcYevnedVqT<4Z1yEj0pP^4ac%Bf% zB_?VO9tx$-sW()rtfB&lsn369-#A^euYl_VD9PlS(62JilYNz-dJ}mJab(TH5?{ax z)Ntc1jSn9U_%coNyZ9zAo4+NtRgI0SpNQ2honm+5ueiP-^&y&KAq9iow&bn}oWmco zD4)1CHy6trOC~IXGs%;uj(0Zxqo!uv!?QnPf}$XjgO4%TdYevu^|-NP>0*{+v}$!> zEv~z!y;nVF>8*V%c!c5ql2pz?KC#NO2l3#_2Cr2hayXXzLe`S*x%(M9S*t5}JPF~! zEeABi?GB4n_g84)cy3c#S?rN{m7m7~KB5TY-hM<8Kou1RL)GSqUDCh52)hdG#4N&% z)I>~DAFI3nL!GhM>!crbHPtS0S_Q8`FSN_jX{1_z#EZo+C zu7DZG_mLnAG~B#vA&63I-+8mkjp)Iio0fGJ`K3Qp03$_tgi4D*$2$jHbR%u+Mmra@ ztZ|saO5LjN{EcO5JG`^VV~mfFkB(Fb+G}M`%lD?G5h^}Az%Iq|f->F{B6YEZL!YQ? zY0dayUau7K_9o~S3VMCvdZ}uYBSMpdRq`SY13q)NnEBKFlf+ASN*hBOCpmIE>DFj&w`)2(8(fP7wnoM6~eS+|CJCHwX4Tzp{<_moi^KK5S8wq2I(lt^9xZiy zIxT+Y`Hr3HjKpsS*i;ME9XTi~_`Kj=|M&yobC9}v8S8=FsPTHJ*|ONHjShI@xrx>^ z+lHR;E0@MzwQFAAO|)Z;re+ex_?K7h3tK{e;b24vP9JL6*)%*Jbv-Uj6iIxc4m*dZ z?%E~D_VPCM5VN*+L_fCQ#$+VI{Z z-@Qm~*)@9bY!{dHPs>f$epPBbO%`_idGcVuwjMpfzy_=r?nFxGmQ9a597%EjOJz2v zX~$KY&iQaitiJq_CFpVB7Xt-hj;@Jzmyh`S{}aG##m5SM+TTk6*~5luzE>J3_Q-9` f37C#wKiMW#wG#^Xz-qikulDxTuw z?Udsvg(0@9Sq`muOsXp1^Zg&AwCvJ=<*$F6ZHLa>X^~8JPu#T8amUfHtgV$ECOQ_eIGi4)Pst=M_YAZ3 zGA%XDR>ki3_4;PHt{t=d)dpGDCer97E>_k5R20eo{cjO}TBK3d@2%eP3UqqWxJtgg z{`9U}^3i|C<7(C3U()<}Jd28|_o?@n{CodRhky!6CZGHNj@LVEvx!g}VBuDeE%LWv zg^Z9?3>1=PvnRH3q|L@?v}vM%Rz%q}jej#kR%XP|AYlfCA<2%j7^m_4VVoD)wpci6 z%W69{uxz*P6;u?og(+@cA0ZuV&EjM_tJ;kTWbpDK++aeaKF-)EFD7xZuW1-zwQQ88 zglLJSP)_#Nk)uUgzNyy3Y?QP7`Jq7?@8|tPtSq9*WS#T3I!zo`a}|HD`g?$UqfA>* z)XI0Q+=fX0rO08HK<&12z>_=swCLs?WYeZxpzNVXr81m@Fk+s%UPlraH_DuBz!BkrHBZ8t`~6DlN}UW* zqQt-ewRvPuhKO8R8A=5ILkQA73C_7yT{w;*uIU=ddE_|qn@j0NJHh?xPB_^#$-W17 z%QtCoP@~#{0!GH%7;}&h%N)$v=^{w70U*0qw~N>{NH8LXQXs>O8c<*mulNA92u39+ImD!Ab!EJp@DJe&{zW93tAEJ!v#fj52gCZPx$aJQHY z?$((|lRJ|pMM4jbXRvOh2}YPT!iPixf7+F7#l2FdW3#%sTSl&VTfyDBEqa+niz5I2 z5{Sb)ad5QSaW^dZTW6vU;YuAGpj-ly8|{%gtPx_3u%Q6ZSb61M<3C*66}TgC#}7q3 zNy@++-k0h$5}ekcKxm+jGf{_hr4B0XhCcx>fO_N(YsDIoYRJGfT*9-|N1r8EVr&%W8f5QLYoqiW3cnvu>Z|Wi<_6s(+E8>~4f)0}vZpGZs|FK9$in zz|v&#Fw2YNpD?1LbSK7a9C1C^3@8;l9=#bUpGNi^aNIu{s}4ElFljpGpk1|>s4LOeb!BMG3cpx5a&~L(PLfe+Ymk*X(jxgzf01%d?~AV z9!T3aX-k2au#_#s>49@CEF0%=7J&rU+U^HP?lYoAAa0g+gc(waI0b!YqfY!rg1R@a z3w=QKdd+DkVS)Lk7MRD;d{O20&-)4}Weof{`2B%UfE0j96}$w6N#V*Qy>zFBn~`s zE*^2tr9m~4)<@uo`dsU>+u&+xh7UPL%D_OOUKhIDTh?g@g(5=Maxn!ixm7OtF|CHH z?0!E^tKp|0oFhm(5KKJOY6>9{WA=8N4 z46_1$@JcSO4ePi&557q_{`Gt0mT{gvCeuJH-id`G!$BodkcnZF{*)=#e~rsBnl@%# zcg{#*GYko4YTZ~QDT|7I` zV79>AF1Pyn^>2E!D4VSKq{{2O(Y?37g@GlvhF!yl$?C(r{ll*yw(*rtf>+LI5b9)T zoZHoNt%TQjRj|ag1V>32VIx*;Rs>1GvfzxdtH!I=UTj?a5lS)W24l1$0k3Whuc$p} zCkzY)${C>^@rt|P6=9bFEOi3FqC{K1S4=X`fJFqhnZKk|iVlfELI_r8|B{qmJ76{U zVnb1b7?x~#&VZ=^ST_b(L?IA16R)HQyGAWojow$m^429L9m3GlwgTS z#n_MwFiErxr&h*Xx4vra#fC5q4(JU2LLIx#0$SY|T5(_@#fkFGf-%OjeY`^}NA6ydcc zY?R#?)TGW}ZnU^=uxjhW#%)1+31OWpULLDv16qAtv;v(ib^U%B^@&!y0B1j3$;+UX z^I;Kta6=n=JWqn;@wv=yEip;zFb1vY&>jnv+OFXU!_9T$RckLc6s^O~xb=ydGQk_X zId707X0Uy~toyLO#8Nx;h*&?z3z#->HhyWiHh?;xLHI4gmmz|=&nwJ8BNIaHotK2$ zi)f5lnH9&H^@~_}QOzFquU)nE-;Sbm?Y@NedjLBZ)JNxAtQ(mZ$$GT(u1M*_u* zckp+Lo~pda;g`M=Pu~XaH)Y*D8Sa-=T*P@3`k&0iq==p$qN2#3PjnRKIQZwM47+7# zLw|8S=uffTRF#AJa_j*AJKgYAX3Wk7?K&h6_#f~;;QuA?-&_p;<@Mk{Ske~p2?OM} zyVsgA74Uyo2FuO`9kSvE{15ma@ZSypy+&H@CIEFs4uDOR$F$vt5|q>60_;@wb>2zc zy`bIQQ^y8G1ri7(5JG-#r4GBuKZLlxPA+xv%H1TIo`s^=UW(gN=VxwrIJQe2;~&Z&?yQL z?BJyxZiZ4J%?Y#DTQhOJ`m?3;+6&U1r);5DaNR52N@*lS5T0Iy=VSeN6`qfe-G%2# zj;_%|WR>3d@cBd)38E^9svxR@sJdcNRi|TP#A$1wVWqJu7vu zim7wKAgzM5+9j>F_8jj$6xA+8MO}9O zE+?r(kW@iZ1xXboRfwuOFskZCmZ=U7suJDwQrB`pR|Q?=EuzY!u4=!yfH`LYtndBM zueTKWTzI#Z7^imn3#Ivx5yb3iXqdf3UHLsLZJpS-22X_6H3%w9!o3BzcFzj<`1Exs zeB66hwy^FrDT{td^XKs_Dk?y&zvSPW5X(7josEF9*E8AF2R`A-AS%HOf%iCM_SPJ_ z%wT$?gox944il=#;=*g=YCF%#q?&CvFiMwJG%`YQ^Y%ywTeCQs&Z>4}(1CdEadEUq z(fI2W1Wz{c{q zHbUx-MzorL%(JS=xczS*uC5$=)vZJ;qR!Cev&lH^0&r=VphMSMpem>zxzj zM2zB`0}n7>Sbr1zL+(=ngB@;GcEpf^Rf*+*VMLivPB}sIx{!)NAb~{<@Z4ZRjy=lX ziBu?3uWV8qlSi`UC@&^)v9D>^-uYTTni8VntqJ91Z*z7x&g^6T=|Gg-iSqC9*!EFx zQRFq~fE{ZF0Ol_6rEk8p+PcO=jU>ATFJ&l`u%rI;z0<%vvZkGTK_vW+)kA7y#eyoqk9O+>N)KJFW|BaYX5IJ!<(#=8?hR zy_olpmJUjK?`jmmC)rUsgKU3H($sVK$azS=?yH)e`UeF0ywgzLw8n`B!&z6|{`2oa zT=*6jk|4sFw)+c&G8P2JK7r9Tz|v&#Fw2YNpD?1L)K{QgZ3aY&9gp6Olux4)s*n3e z&Iy|{4|a8MP2}+-TeU&VLnR6!Q=N%%GUEyHjoVdtAo>j!Y!}ehtGJ(kQ5G| zg~Mmz@Yz)#KC{ckAU&1Wtt^9Ny>+Gd%_N;{=(n~~;c}F4-t0`dCOK}uHtwRyCU@RA z%K0g89OY-YZxm)e<3py9uxGTaRdXBHjq=l6Hm#s0tqv&Yz(4`l!fm6yaNDRulHo1Z zI9~=A9x}KDCptLM!HK?RCz?}41)<*kXlnAT=8}HE#0@Hpg=p5 zy}WKycP^)pIl2df{`?)}aoJKx2jy|$#7ky`PWnm)eHHXo&{siUU9rBB)J7!AIz-0j z>YlV$J}vDCb0Zn@TKaBBL5ig4l=`l7!H^>_XsaFCie6ogJg!c0iqM|tlZbM!Ij5LC zMgY@VBBD8=LwgGz)^$24LW#WUGtX_ExFe0u@ky}4ndc9C<{8ETQH%@>lC0}lh0|O- zU_OjShX({e9nQozwlN@4n8{-j_Hpl5Fv* zPY~|+;>9?dB#Y^%{%`;I>7(v{_xa{elO*~h{F~;{ya%6H6!y1I{aINq|9E@*`1pv@ zry?q|9Hq$vD&pIJM`;?}LZjRM=bK*d^#vwT8QJ$%??z>rC!>cl?k%Eu{HZ^R#=lSV z>|rtKSFKj<##x%>y)RMvssG{Lz52JS%iOMAXUnyh(KH_AarArhr7^sWIposIIN!YN ze3=yqG%25!Z<;h*$NqfPX4R^IMvLj^58v|-)mK`*@p7%YFWP9NoJaXIS-iCazDX-b zP^2lP;0$53!HO&L?34Y?jZSMWJ*~O?wC0lHZkgp}9wp^llcOw4<7iP?E%S%?a682~ zN@0j?>sE(e+$Uw3?fCxpQChU=K=t;w*|zV@o#x48`@~&q(QuW=WKz!VzIt+3b@eAt zSO1a}$taEMzJ0PNk9PT|<6M68?l|;*`FO|CP+hH&9ws^#vDlv;rVq&^E_MvF^)?Mx znys4M?(+I(yS9$0-r68*XCjTB;(T@a@AEwQ?|<_6<1&pF{od*xuRy04jmzZA^KY-* zl8ycukIPkme@e5*@hr;A-pAe#+1LJy4gnRCOg{Dhiq{>s*+sY-VBs5&Ewh(lg^Z9? z3>1=P(-UuTq{+r;v}q!TUPOy&8h0IX3tYU;x_!TQAI+2W3BF{sDEhske6o-TXjXmn z=Q#Zmm&rJK4c_~IgE#Z2_C{&E{^H$?uH$SSB2mO02x_F6=w@tlDm6|gjJp{Ei921) z58JQE{W(3TWAw`7G6 z#E*GgM!WE1l8j5=lzX_p+R-iNFJucZ?vZXe!e;GtAh$!581E)qvH?e=gE5N&4?C&$ zH8$*H;yy{!7ADr$pw5>Ke~sfwJn3bN^r@HJ-#o^>#|TDrne}E_QQpk694yX2J7kOa zW|?OWezHX`a(_Q9ZO*erRxG1&{FXca47VeR^DAXeHsFZxz?w&4&E0V&?Mj^tQKH1a z0JV8!PlkxBtPB;R473#?Nc*I?&i!h`ats-{#&X6Z%aLzEWjEpp0a$y=nMJ@OU!OwA z<(oJJq631FF;~VMqL~vJ5we_ zLJtmSuy&LQMwm6iheQH<+I4Kjy;5e!ZiP@Q&`SulPDUIYt)90V7NV^aQHSWfPR9_H z10v9&xdiRj>FlzZv%-co=WkrcuLxPc5K;*m?N8Csda zVkjjN@PGRx<8rg^x_hqGvnBAG5-&5lQ}DKntQcZzyr@Iw-KjF~PQ&lcSB{}J4Kys+ zH`j6c077BQap@oA#mJ37_0WFbC}fCIq0t1OGqT0elR|hHLVqEgbyV_Leolk zU9Rrx?6r2Xi-D(Jft*j-MGw6l*CBm2c9!s4f|sOm@l=%YJkYlHBv6Hju%wMc0l}%3 zmQClJ7P~sCwN2>vTba%g#Fv#FKBNK}3JT9wr~A!q>E69I^g&>{t%dC*EHPiz67x8k zFU!pSye)xJ#=wu0k1#cpMc@_hRcWAQZN>|s*H_Sez7H))=RHp-MaXV7{#dCNwb1wKB^*THYtc?Bf#tQU+e}&MUT-l_?7q zSO;D?6|Z#OE`%AIGy=+=F`v1VV8t4tddM|mt6WOzx9YiT&)P7G-UwB&42*KcjN&q5 z0-1PEjA2MirY)Ip70BdNWI~USTB-Q}ZkqI)S*eUQLWTmyiBPtvixbb8m7V(4ZkH${ z6+h7~QGrR`dDUz2ls$&z2meF@5kCbYQApyzBd6k#&f95lj)YMzW!`6nmo>vHGfE5v zK}-%PM@o3*lFof=!zL0bLSM6q2yAk#Y*O5p%RnWkNY)oj4Jbo~2QE1kmvr7|f@&n~ zdJ{*~=OcMABUELE4>_(v6zcVHeD|KUdKym=S*Mb~B-hF$e@M&WYO~lsOv~ZNX`I

DIB9nO?^5 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 V7x*{Lqxt8X{{igqKVj!50RVMPfL;Iq diff --git a/doc/integrator/docker.png b/doc/integrator/docker.png index 99f99eb92a3d265069ef58de5b647df70d415e18..be03af64e5ad74062bab383fab1a720fe9f2f43a 100644 GIT binary patch literal 42337 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 lS`!3H%sA(~-i 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 literal 37652 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 Date: Mon, 18 Jun 2018 14:07:28 +0200 Subject: [PATCH 44/92] Remove legacy documentation, cleanup --- doc/administrator/administrate.rst | 13 +- doc/builddoc.rst | 28 ----- .../documentation_infrastructure.rst | 41 ------ doc/developer/index.rst | 1 - doc/index.rst.mako | 11 -- doc/integrator/backend_arcgis.rst | 33 +---- doc/integrator/backend_geoserver.rst | 15 +-- doc/integrator/backend_qgis.rst | 63 ++-------- doc/integrator/caching.rst | 6 +- doc/integrator/create_application.rst | 4 +- doc/integrator/docker.rst | 8 +- doc/integrator/filter.rst | 17 +-- doc/integrator/fulltext_search.rst | 15 ++- doc/integrator/functionality.rst | 107 +--------------- doc/integrator/internationalization.rst | 2 +- doc/integrator/make.rst | 6 +- doc/integrator/pdfreport.rst | 2 +- doc/integrator/print.rst | 38 +----- doc/integrator/reset_password.rst | 9 +- doc/releasenotes.rst | 119 ------------------ 20 files changed, 56 insertions(+), 482 deletions(-) delete mode 100644 doc/builddoc.rst delete mode 100644 doc/developer/documentation_infrastructure.rst delete mode 100644 doc/releasenotes.rst diff --git a/doc/administrator/administrate.rst b/doc/administrator/administrate.rst index 7cc9d6be66..1622ec3d16 100644 --- a/doc/administrator/administrate.rst +++ b/doc/administrator/administrate.rst @@ -131,14 +131,11 @@ The layers in the admin interface have the following attributes: * ``Min/Max resolution``: resolutions between which data are displayed by the given layer, used to zoom to visible scale, with WMS if it is empty we get the values from the capabilities. -* ``Disclaimer``: optional, copyright of the layer, used by - `Disclaimer `_. -* ``Identifier attribute field``: field used to identify a feature from the - layer, e.g.: 'name', used by - `FeaturesWindow `_. -* ``Restrictions area``: the areas throw witch the user can see the layer. - -On ``internal WMS`` layer we have the following specific attributes: +* ``Disclaimer``: optional, copyright of the layer. +* ``Identifier attribute field``: field used to identify a feature from the layer, e.g.: 'name'. +* ``Restrictions area``: the areas through which the user can see the layer. + +On ``internal WMS`` layers we have the following specific attributes: * ``Image type``: the type of the images. * ``Style``: the used style, can be empty. diff --git a/doc/builddoc.rst b/doc/builddoc.rst deleted file mode 100644 index f18a363c3c..0000000000 --- a/doc/builddoc.rst +++ /dev/null @@ -1,28 +0,0 @@ -.. _build_doc: - -Build this doc -============== - -.. prompt:: bash - - ./docker-run make doc - -The HTML should now be available in the ``doc/_build/html`` directory. - -Contribute to the documentation -------------------------------- - -You can contribute to the documentation by making changes to the git-managed -files and creating a pull request, just like for any change proposals to -c2cgeoportal or other git managed projects. - -To make changes to the documentation, you need to edit the ``.rst.mako`` -files where available; otherwise directly the ``.rst`` if there is no corresponding -``mako`` file. - -To verify that the syntax of your changes is OK (no trailing whitespace etc.), -you should execute the following command (in addition to the ``./docker-run make doc``): - -.. prompt:: bash - - ./docker-run make git-attributes diff --git a/doc/developer/documentation_infrastructure.rst b/doc/developer/documentation_infrastructure.rst deleted file mode 100644 index c4dacc1b6c..0000000000 --- a/doc/developer/documentation_infrastructure.rst +++ /dev/null @@ -1,41 +0,0 @@ -.. _developer_documentation_infrastructure: - -Documentation infrastructure -============================ - -c2cgeoportal ------------- - -The c2cgeoportal documentation is publicly available at -https://camptocamp.github.io/c2cgeoportal/master/. - -It is generated by Travis-ci after all commits on the concerned branches. - -ngeo ----- - -The ngeo documentation is publicly available at -https://camptocamp.github.io/ngeo/master/apidoc/. - -It is generated by Travis-ci after all commits on the concerned branches. - -CGXP ----- - -The CGXP documentation is publicly available at -http://docs.camptocamp.net/cgxp/. - -The ``docs.camptocamp.net`` server has an ``@hourly`` cronjob that runs the -``update_online.sh`` script, which is in the ``core/src/doc`` directory of -cgxp. - -To manually update the online doc you can log onto ``docs.camptocamp.net`` as -``sig``, change directory to -``/var/www/vhosts/docs.camptocamp.net/private/cgxp/core/src/doc`` and run -``update_online.sh`` from there. - -.. note:: - - The initial ``git clone`` of cgxp, and the cron configuration are managed - by puppet. Contrary to c2cgeoportal there is no need for a GitHub deploy - key, as the CGXP repository is public. diff --git a/doc/developer/index.rst b/doc/developer/index.rst index a82988a4bd..76e9864a0a 100644 --- a/doc/developer/index.rst +++ b/doc/developer/index.rst @@ -18,7 +18,6 @@ Content: cache debugging development_procedure - documentation_infrastructure ngeo principles python_packaging diff --git a/doc/index.rst.mako b/doc/index.rst.mako index 0c9fa57a7b..dc78eb3d6a 100644 --- a/doc/index.rst.mako +++ b/doc/index.rst.mako @@ -31,19 +31,8 @@ One of the primary goals of the c2cgeoportal project is sharing as much as functionality and code as possible between applications. *Do not repeat ourselves!* -:ref:`releasenotes`. - `Demo `_, `with features grid `_, to test the editing you can use the username 'demo' with the password 'demo'. `ngeo (client) documentation `_. -`CGXP (old client) documentation `_. - -.. toctree:: - :hidden: - - releasenotes - builddoc - -See the :ref:`build_doc` section to know how to build this doc. diff --git a/doc/integrator/backend_arcgis.rst b/doc/integrator/backend_arcgis.rst index 36df581a14..3f8c166a78 100644 --- a/doc/integrator/backend_arcgis.rst +++ b/doc/integrator/backend_arcgis.rst @@ -3,20 +3,12 @@ Specific configuration for Arcgis ================================= -WFS namespace -------------- - -In the ``/templates/viewer.js`` file, define the url of the WFS service which will be used -to get the namespace required to parse the WFS response:: - - cgxp.WFS_FEATURE_NS = "http://your.arcgis.server/arcgis/services/SOME/FOLDER/MapServer/WFSServer"; - Differentiating WMS and WFS services ------------------------------------ It may be necessary to differentiate the WMS and WFS services url. -In the ``vars_.yaml`` file, define: +In the ``OgcServer`` file, define: .. code:: yaml @@ -26,23 +18,6 @@ In the ``vars_.yaml`` file, define: These are the urls which respond respectively to the *WMS GetCapabilities* and *WFS GetCapabilities*. -If your project has a parent (in a parent/child architecture), -in the ``config_child.yaml.mako`` file also define the WFS service separately: - -.. code:: yaml - - external_mapserv_wfs_url: http://your.parent.arcgis.server/arcgis/services/SOME/FOLDER/MapServer/WFSServer - - -WFS geometryName ----------------- - -Also, the name of the geometry parameter in the WFS response is different in ArcGis. - -in the ``/templates/viewer.js`` file, for the *cgxp.plugins.WFSGetFeature* or *cgxp.plugins.GetFeature* plugins, add the -following parameter in the plugin configuration: - - geometryName: 'SHAPE' Note ---- @@ -50,9 +25,3 @@ Note ArcGis (even 10.2) does not understand "application/vnd.ogc.gml" for *INFO_FORMAT* in a *GetFeatureInfo* request and returns only a simple XML formatted response instead of GML. - -This kind of result can not be parsed correctly by the *cgxp.plugins.GetFeature* plugin, -meaning a WMS GetFeatureInfo result will not display correctly. - -It is recommended to use the *cgxp.plugins.WFSGetFeature* plugin that does *only* WFS -queries. diff --git a/doc/integrator/backend_geoserver.rst b/doc/integrator/backend_geoserver.rst index 2c11b3355a..cdcb92dea2 100644 --- a/doc/integrator/backend_geoserver.rst +++ b/doc/integrator/backend_geoserver.rst @@ -1,23 +1,10 @@ .. _integrator_backend_geoserver: Specific configuration for GeoServer -========================================= +==================================== -Viewer configuration --------------------- - -In the ``/templates/viewer.js`` file, make sure no namespace is added for WFS:: - - cgxp.WFS_FEATURE_NS = undefined; Application configuration ------------------------- Add an OGC server in the admin interface for Geoserver (type: geoserver, auth: Geoserver auth). - -In the ``vars_.yaml`` file, define in the ``vars`` section: - -.. code:: yaml - - mapserverproxy: - default_ogc_server: diff --git a/doc/integrator/backend_qgis.rst b/doc/integrator/backend_qgis.rst index cc54825aaa..687b7cd9c2 100644 --- a/doc/integrator/backend_qgis.rst +++ b/doc/integrator/backend_qgis.rst @@ -4,29 +4,6 @@ Specific configuration for QGIS mapserver ========================================= -Viewer configuration -==================== - -In the ``/templates/viewer.js`` file, define the url of the WFS service -which will be used to get the namespace required to parse the WFS response, -and indicate that we do not want duplicate layer name above the legend:: - - cgxp.WMS_FEATURE_NS = "http://www.qgis.org/gml"; - cgxp.LEGEND_INCLUDE_LAYER_NAME = false; - -Change the DPI value to use the same as in the server:: - - - OpenLayers.DOTS_PER_INCH = 92; - + OpenLayers.DOTS_PER_INCH = 2.54 / 100 / 0.00028; - -In the ``cgxp_print`` plugin add the following configuration: - -.. code:: javascript - - encodeLayer: { useNativeAngle: true, serverType: 'qgisserver' } - encodeExternalLayer: { useNativeAngle: true, serverType: 'qgisserver' } - - Apache configuration ==================== @@ -40,23 +17,6 @@ In the ``apache/mapserv.conf.mako`` do the following changes: - SetEnv MS_MAPFILE ${directory}/mapserver/mapserver.map + SetEnv QGIS_PROJECT_FILE ${directory}/qgisserver.qgs -Application configuration -========================= - -Add an OGC server in the admin interface for QGIS server (type: qgisserver, auth: Standard auth). - -In the ``vars_.yaml`` file, define: - -.. code:: yaml - - vars: - ... - mapserverproxy: - default_ogc_server: - update_paths: - ... - - mapserverproxy - Cleanup ======= @@ -88,13 +48,15 @@ Finally, your QGIS project layers CRS should all be in the same CSR. This is sub change. Connect to Postgres database -***************************** +**************************** This section is subject to change when the QGIS plugin is available. The way you should connect QGIS to the database is based on an external file -called. pg_service.conf located in the home directory. The content of this file -is as follow:: +called ``pg_service.conf`` located in the home directory. The content of this file +is as follows: + +.. code:: [geomapfish] host=localhost @@ -114,22 +76,19 @@ Ask to your database administrator the correct parameters. You probably just nee to change the host parameter. You can have several sections. A section start with a name with [] and -finish with a blanck line. This file should be a unix file. +finish with a blank line. This file should be a unix file. On QGIS desktop, when creating a new PostGIS connection, give it a name and use -the service name (`geomapfish` in our example) in the connection parameters +the service name (``geomapfish`` in our example) in the connection parameters form. -Copy past this file in the server, change the parameters to fit with the server -settings and add the variable environment setting in the Apache config:: +Copy-past this file in the server, change the parameters to fit with the server +settings and add the variable environment setting in the Apache config: + +.. code:: [..] SetEnv QGIS_PROJECT_FILE ${directory}/qgisserver.qgs + SetEnv PGSERVICEFILE path/to/pg_service.conf Don't forget to restart Apache. - -Deploy notes -************ - -TODO diff --git a/doc/integrator/caching.rst b/doc/integrator/caching.rst index db25e4213a..1d9be7128c 100644 --- a/doc/integrator/caching.rst +++ b/doc/integrator/caching.rst @@ -9,11 +9,11 @@ Headers By default most of the elements will be cached for 10 days by the browser. To change this value for all the application change in the -``vars_.yaml`` file the ``vars/default_max_age`` value. ``0`` will mean ``no-cache``. +``vars.yaml`` file the ``vars/default_max_age`` value. ``0`` will mean ``no-cache``. The specified value is in seconds. -To change this value for a specific service add the following stricture in the -``vars_.yaml``: +To change this value for a specific service add the following structure in the +``vars.yaml``: .. code:: yaml diff --git a/doc/integrator/create_application.rst b/doc/integrator/create_application.rst index 6fcb3a7a15..714a7ae835 100644 --- a/doc/integrator/create_application.rst +++ b/doc/integrator/create_application.rst @@ -236,14 +236,14 @@ environment variables: Configure the application ------------------------- -As the integrator you need to edit the ``vars_.yaml`` and +As the integrator you need to edit the ``vars.yaml`` and ``.mk`` files to configure the application. Do not miss to add your changes to git: .. prompt:: bash - git add vars_.yaml + git add vars.yaml git commit -m "Configure the project" git push origin master diff --git a/doc/integrator/docker.rst b/doc/integrator/docker.rst index b7d73798e0..7815ad0fd5 100644 --- a/doc/integrator/docker.rst +++ b/doc/integrator/docker.rst @@ -30,7 +30,7 @@ that: The tag is by default ``latest``, but you can change it by setting the ``DOCKER_TAG`` Makefile variable. -Edit ``vars_.yaml`` and add: +Edit ``vars.yaml`` and add: .. code:: yaml @@ -51,10 +51,10 @@ A ``docker-compose.yml.mako`` file is created as a starting point. If you want to host the database on your local machine, you must add a ``dbhost`` and ``dbhost_slave`` entry pointing to ``172.17.0.1`` (your host address for Docker -container) in your ``vars_.yaml`` file. Then you need to make sure +container) in your ``vars.yaml`` file. Then you need to make sure Postgres is configured to listen on that interface and accepts authentication. -If you want to use an external serveur for the database, just put it is address +If you want to use an external server for the database, just put its address in the ``dbhost`` and ``dbhost_slave`` entry. Run the developer composition @@ -105,7 +105,7 @@ WSGI .... To make the DB connection used by your WSGI configurable from the -composition, you can add this in your ``vars_.yaml`` file: +composition, you can add this in your ``vars.yaml`` file: .. code:: yaml diff --git a/doc/integrator/filter.rst b/doc/integrator/filter.rst index dd0b66daf1..b4221a9db1 100644 --- a/doc/integrator/filter.rst +++ b/doc/integrator/filter.rst @@ -5,6 +5,7 @@ Filter (Querier) Filterable layers (public) -------------------------- + Per default, layers are not filterable. If you wish to provide the filter functionality, proceed as follows: @@ -29,8 +30,10 @@ If you wish to provide the filter functionality, proceed as follows: - my_layer_name_1 - my_layer_name_2 + Filterable layers (private) --------------------------- + * If you wish to provide layers for filtering only for specific user roles, define this in the admin interface as follows: @@ -45,6 +48,7 @@ Filterable layers (private) functionalities that all registered users shall have access to, via the ``functionalities.registered`` variable of your vars file. + Available attributes and operators in filters --------------------------------------------- @@ -53,6 +57,7 @@ filterable attribute. If the type, and so the operator on the attribute, is not filtering, you should adapt the type in your layer definition. See :ref:`administrator_mapfile_wfs_getfeature` for more information (MapServer only). + Enumerate available attributes for a layer ------------------------------------------ @@ -103,18 +108,6 @@ Simple example: table: geodata.table country: *layers-enum-mapserver-layer-defaults -.. note:: - - If you use cgxp, make sure that the ``cgxp_querier`` plugin has - the attribute ``attributeURLs`` in the ``viewer.js`` file: - - .. code:: javascript - - { - ptype: "cgxp_querier", - attributeURLs: ${queryer_attribute_urls | n}, - ... - }, Step 2: Administration interface ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/integrator/fulltext_search.rst b/doc/integrator/fulltext_search.rst index 8d6f25d1bb..bd7bc25017 100644 --- a/doc/integrator/fulltext_search.rst +++ b/doc/integrator/fulltext_search.rst @@ -39,6 +39,7 @@ Also make sure that the db user can ``SELECT`` in the ``tsearch`` table: with ````, and ```` substituted as appropriately. + Populate the full-text search table ----------------------------------- @@ -78,6 +79,7 @@ Here is another example where rows from a ``SELECT`` are inserted: be ``fr``. In other words c2cgeoportal assumes that the database language and the application's default language match. + Populate with the themes ------------------------ @@ -87,6 +89,7 @@ A script is available to fill the full-text search table, for more information t ./docker-compose-run theme2fts --help + Security -------- @@ -121,6 +124,7 @@ available to users of the corresponding role. to_tsvector('german', regexp_replace(text, E'[\\[\\]\\(\\):&\\*]', ' ', 'g')) FROM table; + .. _integrator_fulltext_search_params: Params @@ -137,8 +141,6 @@ For instance to specify a ``floor``: Query string ``floor=1`` is then automatically appended to all WMS requests. -See `FloorSlider `_ -for more information. Actions ------- @@ -173,16 +175,19 @@ Example of ``SQL`` ``INSERT`` of ``actions`` data to add the layer "cadastre" on INSERT INTO app_schema.tsearch (..., actions) VALUES (..., '[{"action": "add_layer", "data": "cadastre"}]') + Interface --------- If the ``interface_id`` column contains a value it means that the result is only for an interface. + Lang ---- If the ``lang`` column contains a value it means that the result is only for a language. + Configuration ------------- @@ -193,6 +198,7 @@ following variables: default is 30. * ``fulltextsearch_maxlimit`` the max possible limit, default is 200. + Ranking system -------------- @@ -208,14 +214,15 @@ Ensure that the extension is created in you database: sudo -u postgres psql -c "CREATE EXTENSION pg_trgm" -Alternatively, you can use the `tsvector` and `ts_rank_cd` to rank your search +Alternatively, you can use the ``tsvector`` and ``ts_rank_cd`` to rank your search results (See: `textsearch-controls `_. These methods are useful to handle language-based strings. That means for instance that plural nouns are the same as singular nouns. This system only checks if your search word exists in the result. That means that if you search `B 12 Zug`, `B 120 Zug` has the same weight because the system only see that the `12` exists in each case. To use this system, your request must contains the -parameter `rank_system=ts_rank_cd`. +parameter ``rank_system=ts_rank_cd``. + Using the unaccent extension ---------------------------- diff --git a/doc/integrator/functionality.rst b/doc/integrator/functionality.rst index 812fa3e9ce..2a6162c769 100644 --- a/doc/integrator/functionality.rst +++ b/doc/integrator/functionality.rst @@ -7,15 +7,11 @@ the application according to the user's permissions. A functionality may be associated to users through different ways: 1. Functionalities for anonymous users are defined through the - ``functionalities:anonymous`` variable in the ``vars_.yaml`` - configuration file. + ``functionalities:anonymous`` variable in the ``vars_.yaml`` configuration file. 2. Functionalities for authenticated users are defined through the - ``functionalities:registered`` variable in the ``vars_.yaml`` - configuration file. -3. Functionalities for specific roles are defined in the database through the - administration interface. -4. Functionalities for specific users are defined in the database through the - administration interface. + ``functionalities:registered`` variable in the ``vars_.yaml`` configuration file. +3. Functionalities for specific roles are defined in the database through the administration interface. +4. Functionalities for specific users are defined in the database through the administration interface. Each level overrides the previous ones in the same order as indicated above. For example, if the user is authenticated and has associated functionalities in @@ -154,90 +150,6 @@ project's ``viewer.js`` template: defaultBaseLayerRef: "${functionality['default_basemap'][0] | n}" } -Limiting Access to some CGXP Plugins using Functionalities -.......................................................... - -Functionalities may also be used to enable some CGXP plugins only for users -with specific roles. - -To do so, add ``authorized_plugins`` to the list of functionalities that must be -available in the administration interface and to the list of functionalities -provided to the templates. Set also ``authorized_plugins`` as an empty list for -anonymous users. In ``vars_.yaml``: - -.. code:: yaml - - admin_interface: - # ... - available_functionalities: - - default_basemap - - print_template - - mapserver_substitution - - authorized_plugins - - functionalities: - # ... - anonymous: - # ... - default_basemap: - authorized_plugins: [] - - available_in_templates: [default_basemap, authorized_plugins] - -Then you may test in your project's ``viewer.js`` template if the current user -has been granted access to some protected plugins: - -.. code:: javascript - - app = new gxp.Viewer({ - // ... - tools: [{ - //... - }, - % if '' in functionality['authorized_plugins']: - { - ptype: ... - //... - }, - % endif - { - //... - }] - }); - -Using Functionalities list to configure the layers in the QueryBuilder -...................................................................... - -Add the new ``querybuilder_layer`` functionality to the list of -``available_functionalities`` in your ``vars_.yaml`` file: - -.. code:: yaml - - admin_interface: - available_functionalities: - ... - - querybuilder_layer - -Make sure that the ``dumps`` function is imported in -``/templates/viewer.js`` using: - -.. code:: python - - <% - from json import dumps - %> - -And configure your plugin like that: - -.. code:: javascript - - { - ptype: "cgxp_querier", - featureTypes: ${dumps(functionality['querybuilder_layer']) | n}, - ... - } - -This way you may assign more than one layer per role using functionalities. Using Functionalities to configure the basemap to use for each theme .................................................................... @@ -245,17 +157,6 @@ Using Functionalities to configure the basemap to use for each theme A default basemap may be automatically loaded when the user selects a given theme. -To do so make sure that the ``MapOpacitySlider`` plugin has a reference to the -layertree plugin. For instance: - -.. code:: javascript - - { - ptype: "cgxp_mapopacityslider", - layerTreeId: "layertree", - defaultBaseLayerRef: "${functionality['default_basemap'][0] | n}" - } - Then, in the administration interface, if not available yet, define a ``default_basemap`` functionality containing the basemap reference. Edit the theme and select the basemap to load in the ``default_basemap`` list. If diff --git a/doc/integrator/internationalization.rst b/doc/integrator/internationalization.rst index 454fc0d09a..8f90d34118 100644 --- a/doc/integrator/internationalization.rst +++ b/doc/integrator/internationalization.rst @@ -15,7 +15,7 @@ In the file ``.mk``, define the supported languages with (default): LANGUAGES ?= en fr de -In the file ``vars_.yaml``, define the available and default locales: +In the file ``vars.yaml``, define the available and default locales: .. code:: yaml diff --git a/doc/integrator/make.rst b/doc/integrator/make.rst index bda0535362..73bf3f1379 100644 --- a/doc/integrator/make.rst +++ b/doc/integrator/make.rst @@ -18,9 +18,9 @@ Default is: .. code:: makefile ifdef VARS_FILE - VARS_FILES += ${VARS_FILE} vars_.yaml + VARS_FILES += ${VARS_FILE} vars.yaml else - VARS_FILE = vars_.yaml + VARS_FILE = vars.yaml VARS_FILES += ${VARS_FILE} endif @@ -37,7 +37,7 @@ Default is: Vars files ---------- -The project variables are set in the ``vars_.yaml`` file, +The project variables are set in the ``vars.yaml`` file, which extends the default ``CONST_vars.yaml``. To make such variables available to the python code, for instance using diff --git a/doc/integrator/pdfreport.rst b/doc/integrator/pdfreport.rst index d99544de73..964df07d05 100644 --- a/doc/integrator/pdfreport.rst +++ b/doc/integrator/pdfreport.rst @@ -16,7 +16,7 @@ The webservice is called using the following URL schema: Configuration ------------- -The service is configured in the main ``vars_.yaml`` file of the project +The service is configured in the main ``vars.yaml`` file of the project as in the following example: .. code:: yaml diff --git a/doc/integrator/print.rst b/doc/integrator/print.rst index 1ec314ab03..9b9d03d6a8 100644 --- a/doc/integrator/print.rst +++ b/doc/integrator/print.rst @@ -6,34 +6,6 @@ Print Using MapFish Print v3 ---------------------- -Migration from the v2 -~~~~~~~~~~~~~~~~~~~~~ - -Start the migration by instantiating the default template: - -.. prompt:: bash - - cd - cp -r print ~ - git rm -r print - SRID=-1 ./docker-run pcreate --interactive --scaffold=c2cgeoportal_create --package-name= /tmp/ - ./docker-run pcreate --interactive --scaffold=c2cgeoportal_update --package-name= /tmp/ - mv /tmp//print . - rm -rf /tmp/ - -Then in ``~/print`` you will have a backup of the old template, and in the ``print`` folder you will have the new print configuration. - -In the ``.mk`` you should remove the ``PRINT_VERSION = 2`` (it is ``3`` by default). - -In the concerned viewer javascript files, in the ``cgxp_print`` plugin add the following lines: - -.. code:: javascript - - version: 3, - encodeLayer: {}, - encodeExternalLayer: {}, - additionalAttributes: [] - Template configuration ~~~~~~~~~~~~~~~~~~~~~~ @@ -47,7 +19,7 @@ The main configuration file is ``print/print-apps//config.yaml``. Using a single print server in a set of sites ----------------------------------------------- +--------------------------------------------- For memory issues it is recommended to only use a single print server for a set of sites. @@ -69,7 +41,7 @@ generated by the templating. Then we should do: PRINT_VERSION = NONE * Point to the parent print server by editing the following lines - in the ``vars_.yaml`` file: + in the ``vars.yaml`` file: .. code:: yaml @@ -80,7 +52,7 @@ generated by the templating. Then we should do: print_url: http://{host}:8080/print/pdf/ * If needed set the print templates used by anonymous users by adding the - following in the application configuration (``vars_.yaml``): + following in the application configuration (``vars.yaml``): .. code:: yaml @@ -92,10 +64,6 @@ generated by the templating. Then we should do: - 1 A4 child - 2 A3 child -.. note:: - - This system works for print v2 but must be adapted for - print v3 (although that is the same idea). Having a dedicated print instance --------------------------------- diff --git a/doc/integrator/reset_password.rst b/doc/integrator/reset_password.rst index 3097a17b34..12a7baf4c7 100644 --- a/doc/integrator/reset_password.rst +++ b/doc/integrator/reset_password.rst @@ -4,14 +4,7 @@ Reset password When a user has forgotten his/her password, a new one may be sent by email if some additional GeoMapFish configuration is provided. -In the ``/template/*.js`` files, in the ``cgxp_login`` plugin you should add the -service URL with the following configuration: - -.. code:: javascript - - loginResetPasswordURL: "${request.route_url('loginresetpassword') | n}", - -And to generate the required e-mail, in the ``vars_.yaml`` file, add the following configuration: +And to generate the required e-mail, in the ``vars.yaml`` file, add the following configuration: .. code:: yaml diff --git a/doc/releasenotes.rst b/doc/releasenotes.rst deleted file mode 100644 index 4d2a816974..0000000000 --- a/doc/releasenotes.rst +++ /dev/null @@ -1,119 +0,0 @@ -.. _releasenotes: - -Release notes -============= - -.. _releasenotes_1_5: - -1.5 ---- - -New Features -~~~~~~~~~~~~ - -* Configure a default base map for each themes. -* Be able to be authenticated in basic auth. -* Measure a polyline in the mobile application. -* Directly have a short permalink. -* Display the query results as vector features on the map. -* Multiline text editing in the edit interface. -* Update StreetView for the new API. -* Extend cache usage in all the application and improve the HTTP headers about the cache. - -Full list -~~~~~~~~~ - -* `c2cgeoportal 1.5 `_. -* `CGXP 1.5 `_. - -.. _releasenotes_1_4: - -1.4 ---- - -Main interface -~~~~~~~~~~~~~~ - -* The user is able to change his password. -* Add WMS-T support. -* Add a floor slider. -* Display distance and area in the redlining tool. -* Be able to rotate redlining geometries. -* Get the circle radius. -* Add a short permalink. -* Add the attribute name in CSV export. -* Better context management during the connection. -* Display the legend for the external WMS. -* Open by default the legend. -* Open the layer group on activation. - -Editing interface -~~~~~~~~~~~~~~~~~ - -* Add snapping capabilities. - -Routing interface -~~~~~~~~~~~~~~~~~ - -* Completely new. - -Mobile interface -~~~~~~~~~~~~~~~~ - -* Interface consolidation -* Add permalink -* Update Sencha Touch -* Query WMS group -* Measure distance -* Manage floors - -Full list -~~~~~~~~~ - -* `c2cgeoportal 1.4 `_. -* `CGXP 1.4 `_. - -Stabilisation releases -~~~~~~~~~~~~~~~~~~~~~~ - -* `c2cgeoportal 1.4.5 `_, `CGXP 1.4.5 `_. -* `c2cgeoportal 1.4.4 `_, `CGXP 1.4.4 `_. -* `c2cgeoportal 1.4.3 `_, `CGXP 1.4.3 `_. -* `c2cgeoportal 1.4.2 `_, `CGXP 1.4.2 `_. -* `c2cgeoportal 1.4.1.1 `_, `CGXP 1.4.1.1 `_. -* `c2cgeoportal 1.4.1 `_, `CGXP 1.4.1 `_. - -.. _releasenotes_1_3: - -1.3 ---- - -New Features -~~~~~~~~~~~~ - -* WMSBrowser: indicates if a layer is compatible with the map and is queryable. -* Menu: Add a menu plugin -* Query: Merge the interrogation tools, point and box (CTRL drag) interrogation without activating a tool. Query the layers added by the WMSBrowser. -* Mobile: Better redirection to the mobile application for tablet. -* Print: Add ability to put it on the top right corner. -* Redligning: Add ability to resize and to move the rectangles and circles, new attributes. -* Mobile: Add x, y and zoom in the URL. -* Mobile: Authentication to mobile application. -* Redligning: Add support of the drawn objects to the permalink. - -Bugfixs and other -~~~~~~~~~~~~~~~~~ - -* Mobile: Remove delta (issue with some devices). -* Mobile: Howto to create a second application. -* Get the toolbar mouse coordinate plugin. -* ScaleChoser: Add ability to round the scales. -* FullTextSearch: Add ability to configure the limits. -* GoogleEarth: Add KML layers in the GoogleEarth plugin. -* ScaleLine: Use geodesic measure. - -Full list -~~~~~~~~~ - -* `c2cgeoportal 1.3 `_. -* `CGXP 1.3 `_. From 6621a972fd9231b85621d1354bc6c0c37eda7c53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Mon, 18 Jun 2018 16:33:12 +0200 Subject: [PATCH 45/92] More values in update_paths for admin_interface, and uniformize --- .../scaffolds/create/vars.yaml_tmpl | 7 +++++-- .../scaffolds/nondockercreate/vars.yaml_tmpl | 13 ++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/vars.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/create/vars.yaml_tmpl index 80444744e1..1f6245d04a 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/vars.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/vars.yaml_tmpl @@ -265,7 +265,10 @@ vars: # url: {web_protocol}://{host}/child/wsgi update_paths: - - admin_interface + - admin_interface.available_functionalities + - admin_interface.available_metadata + - admin_interface.functionalities + - admin_interface.available_in_templates - authorized_referers - authtkt - check_collector.disabled @@ -273,9 +276,9 @@ update_paths: - checker.fulltextsearch - checker.lang - checker.phantomjs + - checker.print - checker.routes - checker.themes - - checker.print - docker_services.config.environment - docker_services.db.environment - docker_services.geoportal.environment diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/vars.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/vars.yaml_tmpl index ba9dd08385..e6428f2c94 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/vars.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/vars.yaml_tmpl @@ -269,13 +269,24 @@ vars: code_destination: /var/www/c2cgeoportal_mapfish/private/c2cgeoportal update_paths: - - admin_interface + - admin_interface.available_functionalities + - admin_interface.available_metadata + - admin_interface.functionalities + - admin_interface.available_in_templates - authorized_referers - authtkt - check_collector.disabled - check_collector.hosts - checker.fulltextsearch + - checker.lang + - checker.phantomjs - checker.print + - checker.routes + - checker.themes + - docker_services.config.environment + - docker_services.db.environment + - docker_services.geoportal.environment + - docker_services.tilecloudchain.environment - fulltextsearch - interfaces_theme - shortener From 4bdc414918909535c6827caf8c78f4058ab9c623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Mon, 18 Jun 2018 16:54:52 +0200 Subject: [PATCH 46/92] Update the requirements --- doc/integrator/requirements.rst | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/doc/integrator/requirements.rst b/doc/integrator/requirements.rst index 443061a54a..478c4098d3 100644 --- a/doc/integrator/requirements.rst +++ b/doc/integrator/requirements.rst @@ -7,20 +7,31 @@ To install a GeoMapFish application you need to have the following components installed on your system: * **Git** (preferably to other revision control systems) -* **Python** >= 3.4 with development files (``python-dev``), ``python3-pip``, ``python3-venv``. +* **Docker** >= 1.12 +* **Docker-compose** >= 1.8 +* **Python** >= 3.5 with development files (``python-dev``), ``python3-pip``, ``python3-venv``. * **Python-netifaces** -* **VirtualEnv** >= 1.7 * Oracle **Java** SE Development Kit 6 or 7 * **Tomcat** >= 6.0 -* **Apache** >= 2.2 +* **Apache** >= 2.4 (with ``mod_wsgi`` for Python 3) * **PostgreSQL** >= 9.1/**PostGIS** >= 2.1, with library (``libpq-dev``) * **MapServer** 7.2 (for the filters) or **QGIS**-mapserver 2.2 and upper +* **Java** >= 7.0 * **MapCache** >= 1.0.0 with memcached support +* **Memcached** >= 1.4 * **TinyOWS** >= 1.1.0 * **ImageMagick** * **GCC** GNU Compiler Collection >= 4.6 -* **Deploy** >= 0.4 * **libproj** >= 4.7 +* **gdal** >= 1.9 (with ``libgdal-dev``) +* **geos** >= 3.3 +* **zip** >= 3.0 +* **unzip** >= 6.0 +* **graphviz-dev** +* **node** >= 6.0 +* **npm** >= 3.10 +* **Deploy** >= 0.4.7 + Required Apache modules ~~~~~~~~~~~~~~~~~~~~~~~ From 6f1d765184cfaf02a641878f497a10951ce84e22 Mon Sep 17 00:00:00 2001 From: Patrick Valsecchi Date: Tue, 19 Jun 2018 14:55:17 +0200 Subject: [PATCH 47/92] Make the raster files (profile) work over http(s) Now, we can have something like that in the config: ```yaml raster: srtm: file: /vsicurl/http://url-of-some-http-service/SRTM21781/srtm.shp round: 1 ``` That http service could be S3. --- geoportal/c2cgeoportal_geoportal/views/profile.py | 1 - geoportal/c2cgeoportal_geoportal/views/raster.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/geoportal/c2cgeoportal_geoportal/views/profile.py b/geoportal/c2cgeoportal_geoportal/views/profile.py index 3507062378..fb89706850 100644 --- a/geoportal/c2cgeoportal_geoportal/views/profile.py +++ b/geoportal/c2cgeoportal_geoportal/views/profile.py @@ -47,7 +47,6 @@ class Profile(Raster): def __init__(self, request): - self.request = request Raster.__init__(self, request) @view_config(route_name="profile.json", renderer="decimaljson") diff --git a/geoportal/c2cgeoportal_geoportal/views/raster.py b/geoportal/c2cgeoportal_geoportal/views/raster.py index 78b4a97077..07d1676c8a 100644 --- a/geoportal/c2cgeoportal_geoportal/views/raster.py +++ b/geoportal/c2cgeoportal_geoportal/views/raster.py @@ -33,7 +33,7 @@ from decimal import Decimal from repoze.lru import LRUCache -import fiona +from fiona.collection import Collection import rasterio from pyramid.httpexceptions import HTTPNotFound from pyramid.view import view_config @@ -82,7 +82,7 @@ def _get_raster_value(self, layer, lon, lat): path = layer["file"] if layer.get("type", "shp_index") == "shp_index": - with fiona.open(path) as collection: + with Collection(path) as collection: tiles = [e for e in collection.filter(mask={ "type": "Point", "coordinates": [lon, lat], From b24aa946b05224261092a57d85cf4d94ebdea452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Tue, 19 Jun 2018 09:32:12 +0200 Subject: [PATCH 48/92] Fix the create project documentation Removes hard coded pass throw --- .gitignore | 1 + ...cation.rst => create_application.rst.mako} | 26 ++++++++++++------- docker-run | 19 ++++++++------ .../scaffolds/update/CONST_Makefile_tmpl | 4 +-- travis/create-new-nondocker-project.sh | 15 +++++------ travis/create-new-project.sh | 9 +++---- travis/test-upgrade-convert.sh | 8 +++--- 7 files changed, 44 insertions(+), 38 deletions(-) rename doc/integrator/{create_application.rst => create_application.rst.mako} (92%) diff --git a/.gitignore b/.gitignore index 4cefaa1ad2..17de3f79cb 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ /docker/test-mapserver/mapserver.map /doc/index.rst /doc/developer/server_side.rst +/doc/integrator/create_application.rst /doc/integrator/upgrade_application.rst /dist/ /docker-compose.yaml diff --git a/doc/integrator/create_application.rst b/doc/integrator/create_application.rst.mako similarity index 92% rename from doc/integrator/create_application.rst rename to doc/integrator/create_application.rst.mako index 714a7ae835..d1f33f44ba 100644 --- a/doc/integrator/create_application.rst +++ b/doc/integrator/create_application.rst.mako @@ -45,8 +45,9 @@ To get ``c2cgeoportal`` you first need to get the related docker image: .. prompt:: bash - docker pull camptocamp/geomapfish-build - wget https://raw.githubusercontent.com/camptocamp/c2cgeoportal/master/docker-run + docker pull camptocamp/geomapfish-build: + wget https://raw.githubusercontent.com/camptocamp/c2cgeoportal/${git_branch}/docker-run + chmod +x docker-run List existing skeletons ----------------------- @@ -58,7 +59,7 @@ c2cgeoportal application you want to create the new application from: .. prompt:: bash - ./docker-run pcreate -l + ./docker-run --image=camptocamp/geomapfish-build pcreate -l You should at least see the c2cgeoportal skeletons: @@ -80,7 +81,8 @@ To create the application first apply the ``c2cgeoportal_create`` skeleton: .. prompt:: bash - ./docker-run pcreate -s c2cgeoportal_create + ./docker-run -ti --image=camptocamp/geomapfish-build \ + pcreate -s c2cgeoportal_create .. note:: @@ -97,8 +99,9 @@ it later. .. prompt:: bash - SRID=21781 EXTENT="420000 30000 900000 350000" \ - ./docker-run pcreate -s c2cgeoportal_create --package-name + ./docker-run -ti --image=camptocamp/geomapfish-build \ + --env=SRID=21781 --env=EXTENT="420000,30000,900000,350000" + pcreate -s c2cgeoportal_create --package-name This will create a directory named ```` that will be next to the ``c2cgeoportal`` directory, or to the directory of the application you are @@ -108,7 +111,8 @@ Now apply the ``c2cgeoportal_update`` skeleton: .. prompt:: bash - SRID=21781 ./docker-run pcreate -s c2cgeoportal_update --package-name + ./docker-run -ti --env=SRID=21781 --image=camptocamp/geomapfish-build \ + pcreate -s c2cgeoportal_update --package-name .. note:: @@ -324,10 +328,12 @@ The result is also a file without the .mako. **Syntax** -In ``.mako`` files, the variable replacement syntax is as follows:: +In ``.mako`` files, the variable replacement syntax is as follows: - ${} +.. code:: mako + + ${'$'}{} for example: -* ``${directory}`` +* ``${'$'}{directory}`` diff --git a/docker-run b/docker-run index b457a22eb8..b6ca32c8bd 100755 --- a/docker-run +++ b/docker-run @@ -48,8 +48,16 @@ def main(): build_volume_name = dir_path.replace(":", "-").replace("\\", "-") \ if is_windows else dir_path[1:].replace("/", "-") - git_branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]).decode("utf-8").strip() - git_hash = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode("utf-8").strip() + try: + git_branch = subprocess.check_output([ + "git", "rev-parse", "--abbrev-ref", "HEAD" + ]).decode("utf-8").strip() + git_hash = subprocess.check_output([ + "git", "rev-parse", "HEAD" + ]).decode("utf-8").strip() + except subprocess.CalledProcessError as e: + git_branch = "unknown" + git_hash = "unknown" try: login = os.getlogin() @@ -92,12 +100,7 @@ def main(): "--env=SSH_AUTH_SOCK", ]) - for env in [ - "SRID", - "EXTENT", - "APACHE_VHOST", - "CI", - ] + options.env: + for env in ["CI"] + options.env: docker_cmd.append("--env={}".format(env)) if 'docker0' in netifaces.interfaces(): diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl index 41f8285f24..7a3436ffc1 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl @@ -690,14 +690,14 @@ docker-compose-build.yaml: /build/requirements.timestamp docker-build-config: $(shell docker-required --path .) \ $(PRINT_CONFIG_FILE) \ $(MAPCACHE_FILE) - docker build -t $(DOCKER_BASE)-config:$(DOCKER_TAG) . + docker build --tag=$(DOCKER_BASE)-config:$(DOCKER_TAG) . .PHONY: docker-build-geoportal docker-build-geoportal: $(shell docker-required --path geoportal) \ $(WEB_RULE) \ $(addprefix geoportal/$(PACKAGE)_geoportal/locale/, $(addsuffix /LC_MESSAGES/$(PACKAGE)_geoportal-$(L10N_CLIENT_POSTFIX).mo, $(LANGUAGES))) \ geoportal/config.yaml - docker build --tag=$(DOCKER_BASE)-geoportal:$(DOCKER_TAG) --build-arg="GIT_HASH=$(GIT_HASH)" geoportal + docker build --tag=$(DOCKER_BASE)-geoportal:$(DOCKER_TAG) --build-arg=GIT_HASH=$(GIT_HASH) geoportal ifdef CGXP_INTERFACES docker-build-geoportal: $(addprefix geoportal/$(PACKAGE)_geoportal/locale/, $(addsuffix /LC_MESSAGES/$(PACKAGE)_geoportal-$(L10N_SERVER_POSTFIX).mo, $(LANGUAGES))) endif diff --git a/travis/create-new-nondocker-project.sh b/travis/create-new-nondocker-project.sh index 7e2b08f89e..a681e3e787 100755 --- a/travis/create-new-nondocker-project.sh +++ b/travis/create-new-nondocker-project.sh @@ -1,14 +1,11 @@ #!/bin/bash -ex -export SRID=21781 APACHE_VHOST=test EXTENT=489246.36,78873.44,837119.76,296543.14 -./docker-run --image=camptocamp/geomapfish-build --share /tmp/travis pcreate --scaffold=c2cgeoportal_create /tmp/travis/nondockertestgeomapfish \ - --package-name testgeomapfish -./docker-run --image=camptocamp/geomapfish-build --share /tmp/travis pcreate --scaffold=c2cgeoportal_nondockercreate /tmp/travis/nondockertestgeomapfish \ - --overwrite --package-name testgeomapfish -./docker-run --image=camptocamp/geomapfish-build --share /tmp/travis pcreate --scaffold=c2cgeoportal_update /tmp/travis/nondockertestgeomapfish \ - --package-name testgeomapfish -./docker-run --image=camptocamp/geomapfish-build --share /tmp/travis pcreate --scaffold=c2cgeoportal_nondockerupdate /tmp/travis/nondockertestgeomapfish \ - --overwrite --package-name testgeomapfish +DOCKER_RUN_ARGS="--env=SRID=21781 --env=APACHE_VHOST=test --env=EXTENT=489246.36,78873.44,837119.76,296543.14 --image=camptocamp/geomapfish-build --share=/tmp/travis" +PCREATE_CMD="pcreate --overwrite --package-name testgeomapfish /tmp/travis/nondockertestgeomapfish" +./docker-run ${DOCKER_RUN_ARGS} ${PCREATE_CMD} --scaffold=c2cgeoportal_create +./docker-run ${DOCKER_RUN_ARGS} ${PCREATE_CMD} --scaffold=c2cgeoportal_nondockercreate +./docker-run ${DOCKER_RUN_ARGS} ${PCREATE_CMD} --scaffold=c2cgeoportal_update +./docker-run ${DOCKER_RUN_ARGS} ${PCREATE_CMD} --scaffold=c2cgeoportal_nondockerupdate cp travis/build-nondocker.mk /tmp/travis/nondockertestgeomapfish/travis.mk cp travis/vars-nondocker.yaml /tmp/travis/nondockertestgeomapfish/vars_travis.yaml diff --git a/travis/create-new-project.sh b/travis/create-new-project.sh index 4eac654dbd..00c72b542b 100755 --- a/travis/create-new-project.sh +++ b/travis/create-new-project.sh @@ -3,11 +3,10 @@ WORKSPACE=$1 mkdir --parent ${WORKSPACE}/testgeomapfish -export SRID=21781 EXTENT=489246.36,78873.44,837119.76,296543.14 -./docker-run --image=camptocamp/geomapfish-build --share ${WORKSPACE} pcreate --scaffold=c2cgeoportal_create \ - --ignore-conflicting-name --package-name testgeomapfish ${WORKSPACE}/testgeomapfish -./docker-run --image=camptocamp/geomapfish-build --share ${WORKSPACE} pcreate --scaffold=c2cgeoportal_update \ - --ignore-conflicting-name --package-name testgeomapfish ${WORKSPACE}/testgeomapfish +DOCKER_RUN_ARGS="--env=SRID=21781 --env=EXTENT=489246.36,78873.44,837119.76,296543.14 --image=camptocamp/geomapfish-build --share=${WORKSPACE}" +PCREATE_CMD="pcreate --ignore-conflicting-name --overwrite --package-name testgeomapfish ${WORKSPACE}/testgeomapfish" +./docker-run ${DOCKER_RUN_ARGS} ${PCREATE_CMD} --scaffold=c2cgeoportal_create +./docker-run ${DOCKER_RUN_ARGS} ${PCREATE_CMD} --scaffold=c2cgeoportal_update # Copy files for travis build and tests cp travis/build.mk ${WORKSPACE}/testgeomapfish/travis.mk diff --git a/travis/test-upgrade-convert.sh b/travis/test-upgrade-convert.sh index aa0c5aeb0a..3a21e7f207 100755 --- a/travis/test-upgrade-convert.sh +++ b/travis/test-upgrade-convert.sh @@ -2,11 +2,13 @@ WORKSPACE=$2 +DOCKER_RUN_ARGS="--env=SRID=21781 --env=APACHE_VHOST=test --env=EXTENT=489246.36,78873.44,837119.76,296543.14 --image=camptocamp/geomapfish-build" +PCREATE_ARGS="--ignore-conflicting-name --overwrite --package-name testgeomapfish" + export NODE_ENV=development function pcreate { - ./docker-run --image=camptocamp/geomapfish-build $3 --share $1 pcreate --scaffold=$2 $1/testgeomapfish \ - --overwrite --ignore-conflicting-name --package-name testgeomapfish + ./docker-run ${DOCKER_RUN_ARGS} $3 --share=$1 pcreate --scaffold=$2 $1/testgeomapfish ${PCREATE_ARGS} } function only_create { @@ -72,14 +74,12 @@ then ${WORKSPACE}/v220-todocker ${WORKSPACE}/v220-tonondocker \ ${WORKSPACE}/testgeomapfish rm --recursive --force $(find c2cgeoportal/scaffolds -name __pycache__) - export SRID=21781 APACHE_VHOST=test EXTENT=489246.36,78873.44,837119.76,296543.14 create ${WORKSPACE}/docker create ${WORKSPACE}/dockerref createnondocker ${WORKSPACE}/nondocker createnondocker ${WORKSPACE}/nondockerref create ${WORKSPACE}/v230-docker --version=2.3.0 createnondocker ${WORKSPACE}/v230-nondocker --version=2.3.0 - unset SRID APACHE_VHOST EXTENT mkdir --parent ${WORKSPACE}/v220 ./docker-run --share=${WORKSPACE} tar --extract --bzip2 --file=travis/v220.tar.bz2 --directory=${WORKSPACE}/v220 createv220 ${WORKSPACE}/v220-todocker From a1182c7ffa2bd708d8da05298c0167a8e002023c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Tue, 19 Jun 2018 14:46:08 +0200 Subject: [PATCH 49/92] Fix dev mode And also fix admin to admin/ redirect --- doc/developer/debugging.rst | 4 ++-- geoportal/c2cgeoportal_geoportal/__init__.py | 20 +++++++++---------- .../create/docker-compose-dev.yaml.mako | 5 +++-- .../geoportal/webpack.apps.js.mako_tmpl | 2 +- .../nondockercreate/apache/wsgi.conf.mako | 6 ------ .../geoportal/webpack.apps.js.mako_tmpl | 4 +++- .../nondocker-finalise.mk_tmpl | 8 ++++++++ .../nondockercreate/nondocker-override.mk | 1 + .../scaffolds/update/CONST_Makefile_tmpl | 8 +++----- .../scaffolds/update/CONST_vars.yaml_tmpl | 5 +++-- .../c2cgeoportal_geoportal/views/__init__.py | 5 +++++ geoportal/c2cgeoportal_geoportal/views/dev.py | 4 ++-- geoportal/tests/test_init.py | 10 ++++------ travis/vars-nondocker.yaml | 1 + 14 files changed, 45 insertions(+), 38 deletions(-) diff --git a/doc/developer/debugging.rst b/doc/developer/debugging.rst index dbbab7f49c..82c03df886 100644 --- a/doc/developer/debugging.rst +++ b/doc/developer/debugging.rst @@ -20,9 +20,9 @@ Add in your makefile ``.mk`` (Each developer should have a different port, .. prompt:: bash - make serve- + FINALISE=TRUE make serve- -Open in the browser an URL like: ``https:///dev/.html``. +Open in the browser an URL like: ``https:////wsgi/dev/.html``. Browser ------- diff --git a/geoportal/c2cgeoportal_geoportal/__init__.py b/geoportal/c2cgeoportal_geoportal/__init__.py index 2eee714a33..9724b4f151 100644 --- a/geoportal/c2cgeoportal_geoportal/__init__.py +++ b/geoportal/c2cgeoportal_geoportal/__init__.py @@ -56,6 +56,7 @@ from sqlalchemy.orm import Session import c2cgeoportal_commons.models +import c2cgeoportal_geoportal.views from c2cgeoportal_geoportal.lib import dbreflection, caching, \ C2CPregenerator, MultiDomainStaticURLInfo, checker, check_collector @@ -234,11 +235,13 @@ def add_static_view_ngeo(config): # pragma: no cover def add_admin_interface(config): - if config.get_settings().get("enable_admin_interface", False): + if config.get_settings().get('enable_admin_interface', False): config.add_request_method( # pylint: disable=not-callable lambda request: c2cgeoportal_commons.models.DBSession(), 'dbsession', reify=True ) + config.add_view(c2cgeoportal_geoportal.views.add_ending_slash, 'add_ending_slash') + config.add_route('add_ending_slash', '/admin', request_method='GET') config.include('c2cgeoportal_admin', route_prefix='/admin') @@ -493,13 +496,6 @@ def call_hook(settings, name, *args, **kwargs): function(*args, **kwargs) -def notfound(request): - return { - "message": request.path_info, - "status": 404, - } - - def includeme(config): """ This function returns a Pyramid WSGI application. @@ -507,8 +503,6 @@ def includeme(config): settings = config.get_settings() - config.add_notfound_view(notfound, append_slash=True, renderer='fast_json', http_cache=0) - config.include("c2cgeoportal_commons") call_hook(settings, "after_settings", settings) @@ -735,7 +729,11 @@ def handle(event: InvalidateCacheEvent): config.add_route("dev", "/dev/*path", request_method="GET") # Scan view decorator for adding routes - config.scan(ignore=["c2cgeoportal_geoportal.scripts", "c2cgeoportal_geoportal.wsgi_app"]) + config.scan(ignore=[ + "c2cgeoportal_geoportal.lib", + "c2cgeoportal_geoportal.scaffolds", + "c2cgeoportal_geoportal.scripts" + ]) if "subdomains" in settings: # pragma: no cover config.registry.registerUtility( diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose-dev.yaml.mako b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose-dev.yaml.mako index 50410b0a57..017ace5d58 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose-dev.yaml.mako +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose-dev.yaml.mako @@ -11,12 +11,13 @@ services: image: ${docker_base}-geoportal:${docker_tag} volumes: - ${project_directory}/geoportal/${package}_geoportal/static-ngeo:/app/${package}_geoportal/static-ngeo - environment: - - INTERFACE=desktop command: - webpack-dev-server - --mode=development + - --host=webpack-dev-server - --port=8080 - --debug - --watch - --progress +${service_defaults('geoportal')}\ + - INTERFACE=desktop diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/webpack.apps.js.mako_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/webpack.apps.js.mako_tmpl index c1173ee2ee..4736dfeba7 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/webpack.apps.js.mako_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/webpack.apps.js.mako_tmpl @@ -55,7 +55,7 @@ const rules = [ projectRule, ]; -const noDevServer = process.env['NO_DEV_SERVER'] != 'FALSE'; +const noDevServer = process.env['NO_DEV_SERVER'] == 'TRUE'; devServer = dev && !noDevServer; module.exports = { diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/apache/wsgi.conf.mako b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/apache/wsgi.conf.mako index bfbfb30559..ee414962cb 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/apache/wsgi.conf.mako +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/apache/wsgi.conf.mako @@ -26,12 +26,6 @@ RewriteRule ^${apache_entry_point[:-1]}$ ${apache_entry_point} [R] % endif RewriteRule ^${apache_entry_point}$ /${instanceid}/wsgi [PT] -# Dev server -RewriteRule ^${apache_entry_point}dev/dynamic.js$ http://localhost/${instanceid}/wsgi/dynamic.js [P] -RewriteRule ^${apache_entry_point}dev/(${'|'.join([re.escape(e) for e in interfaces])}).html/theme/dynamic.js$ http://localhost/${instanceid}/wsgi/dynamic.js [P] -RewriteRule ^${apache_entry_point}dev/(${'|'.join([re.escape(e) for e in interfaces])}).html/theme/.* http://127.0.0.1:8081/${instanceid}/dev/$1.html [P] -RewriteRule ^${apache_entry_point}dev/ http://127.0.0.1:${dev_server_port}$0 [P] - RewriteRule ^${apache_entry_point}dynamic.js$ /${instanceid}/wsgi/dynamic.js [R] RewriteRule ^${apache_entry_point}(${'|'.join([re.escape(e) for e in interfaces])}|theme)/dynamic.js$ /${instanceid}/wsgi/dynamic.js [R] RewriteRule ^${apache_entry_point}(${'|'.join([re.escape(e) for e in interfaces])})/theme/dynamic.js$ /${instanceid}/wsgi/dynamic.js [R] diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/geoportal/webpack.apps.js.mako_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/geoportal/webpack.apps.js.mako_tmpl index a0cc1b9197..fd9794dacd 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/geoportal/webpack.apps.js.mako_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/geoportal/webpack.apps.js.mako_tmpl @@ -52,11 +52,13 @@ const rules = [ projectRule ]; +const noDevServer = process.env['NO_DEV_SERVER'] == 'TRUE'; +devServer = dev && !noDevServer; module.exports = { output: { path: path.resolve(__dirname, '{{package}}_geoportal/static-ngeo/build/'), - publicPath: dev ? '/${instanceid}/dev/' : '/${instanceid}/wsgi/static-ngeo/UNUSED_CACHE_VERSION/build/' + publicPath: devServer ? '/${instanceid}/wsgi/dev/' : '/${instanceid}/wsgi/static-ngeo/UNUSED_CACHE_VERSION/build/' }, entry: entry, module: { diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-finalise.mk_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-finalise.mk_tmpl index 7ca7d9215d..4e2987df4c 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-finalise.mk_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-finalise.mk_tmpl @@ -181,3 +181,11 @@ endif mkdir --parent admin cat npm-packages | xargs npm install --prefix ./admin touch $@ + +.PHONY: serve-% +serve-%: + rm -f geoportal/node_modules + ln -s ../admin/node_modules geoportal + (cd geoportal; INTERFACE=$* NODE_ENV=development node_modules/.bin/webpack-dev-server --port=$(DEV_SERVER_PORT) -d --watch --progress \ + --public=$(VISIBLE_WEB_HOST):$(VISIBLE_WEB_PORT) --disable-host-check --mode=development) + rm geoportal/node_modules diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-override.mk b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-override.mk index 32481e1ac4..6377b8aa26 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-override.mk +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-override.mk @@ -1,6 +1,7 @@ # This file override and improve some rules for CONST_Makefile for non Docker production environment DEPLOY ?= TRUE +DOCKER ?= FALSE ENVIRONMENT ?= INSTANCE_ID CONFIG_VARS += instanceid MODWSGI_USER ?= www-data diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl index 41f8285f24..174dbe969a 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl @@ -8,6 +8,7 @@ export DEV_SERVER_PORT INSTANCE ?= main export INSTANCE +DOCKER ?= TRUE ifeq ($(INSTANCE), main) DOCKER_ENTRY_POINT ?= / else @@ -604,15 +605,12 @@ $(APP_OUTPUT_DIR)/images/: /usr/lib/node_modules/jquery-ui/themes/base/images ln --symbolic /usr/lib/node_modules/ geoportal/ (cd geoportal; INTERFACE=$* webpack $(WEBPACK_ARGS)) rm geoportal/node_modules +ifeq ($(DOCKER), TRUE) mv $(APP_OUTPUT_DIR)/$*.html $(APP_OUTPUT_DIR)/$*.html.tmpl ls -1 $(APP_OUTPUT_DIR)/$*.*.css|while read file; do mv $${file} $${file}.tmpl; done +endif touch $@ -.PHONY: serve -serve-%: - (cd geoportal; INTERFACE=$* ../node_modules/.bin/webpack-dev-server --port=$(DEV_SERVER_PORT) -d --watch --progress \ - --public=$(VISIBLE_WEB_HOST):$(VISIBLE_WEB_PORT) --mode=development --inline=false) - /build/eslint.timestamp: $(APP_JS_FILES) $(PRERULE_CMD) eslint $? diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl index 38141c4e65..e9ef1f93d6 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl @@ -35,7 +35,7 @@ vars: PGUSER: www-data PGPASSWORD: www-data PGDATABASE: geomapfish - GEOPORTAL_INTERNAL_URL: 'http://geoportal/' + GEOPORTAL_INTERNAL_URL: http://geoportal/ MAPCACHE_URL: http://mapcache/mapcache/ MAPSERVER_URL: http://mapserver/ MEMCACHED_HOST: memcached @@ -65,6 +65,7 @@ vars: TINYOWS_URL: http://tinyows/ MAPSERVER_URL: http://mapserver/ PRINT_URL: http://print:8080/print/ + DEVSERVER_URL: http://webpack-dev-server:8080${entry_point} REDIS_HOST: redis REDIS_PORT: 6379 C2C_REDIS_URL: redis://redis:6379 @@ -570,7 +571,7 @@ runtime_environment: - name: MAPCACHE_URL default: http://mapcache/ - name: DEVSERVER_URL - default: http://webpack-dev-server:8080 + default: http://webpack-dev-server:8080/ - name: REDIS_HOST default: redis - name: REDIS_PORT diff --git a/geoportal/c2cgeoportal_geoportal/views/__init__.py b/geoportal/c2cgeoportal_geoportal/views/__init__.py index 2a83f09eca..b450144b3d 100644 --- a/geoportal/c2cgeoportal_geoportal/views/__init__.py +++ b/geoportal/c2cgeoportal_geoportal/views/__init__.py @@ -29,12 +29,17 @@ import logging from pyramid.view import view_config +from pyramid.httpexceptions import HTTPFound from c2cgeoportal_geoportal.lib.caching import set_common_headers, NO_CACHE log = logging.getLogger(__name__) +def add_ending_slash(request): + return HTTPFound(location=request.path + '/') + + @view_config(context=Exception, renderer="json", http_cache=0) def other_error(exception, request): # pragma: no cover set_common_headers(request, "index", NO_CACHE) diff --git a/geoportal/c2cgeoportal_geoportal/views/dev.py b/geoportal/c2cgeoportal_geoportal/views/dev.py index 99faa183b9..d2640498df 100644 --- a/geoportal/c2cgeoportal_geoportal/views/dev.py +++ b/geoportal/c2cgeoportal_geoportal/views/dev.py @@ -47,8 +47,8 @@ def __init__(self, request): @view_config(route_name='dev') def dev(self): - path = self.THEME_RE.sub('', self.request.path) - if self.request.path.endswith('/dynamics.js'): + path = self.THEME_RE.sub('', self.request.path_info) + if self.request.path.endswith('/dynamic.js'): return HTTPFound(location=self.request.route_url('dynamic')) else: return self._proxy_response('dev', "{}/{}".format( diff --git a/geoportal/tests/test_init.py b/geoportal/tests/test_init.py index 4a10f4c5d6..65786cbd82 100644 --- a/geoportal/tests/test_init.py +++ b/geoportal/tests/test_init.py @@ -45,7 +45,7 @@ class TestIncludeme(TestCase): def setup_method(self, _): # the c2cgeoportal includeme function requires a number # of settings - config._config = { + self.config = testing.setUp(settings={ "sqlalchemy.url": "postgresql://www-data:www-data@db:5432/geomapfish_tests", "srid": 3857, "schema": "main", @@ -53,9 +53,8 @@ def setup_method(self, _): "default_max_age": 86400, "app.cfg": "/src/geoportal/tests/config.yaml", "package": "c2cgeoportal", - "enable_admin_interface": True, - } - self.config = testing.setUp(settings=config.get_config()) + "enable_admin_interface": False, + }) def test_set_user_validator_directive(self): self.config.include(c2cgeoportal_geoportal.includeme) @@ -79,8 +78,7 @@ def custom_validator(username, password): del password # Unused return False # pragma: no cover self.config.set_user_validator(custom_validator) - self.assertEqual(self.config.registry.validate_user, - custom_validator) + self.assertEqual(self.config.registry.validate_user, custom_validator) class TestReferer(TestCase): diff --git a/travis/vars-nondocker.yaml b/travis/vars-nondocker.yaml index 7ef9795372..4dafcca99d 100644 --- a/travis/vars-nondocker.yaml +++ b/travis/vars-nondocker.yaml @@ -12,6 +12,7 @@ vars: host_map: localhost: netloc: '{DOCKER_HOST_}' + devserver_url: http://localhost:8081/{instanceid}/wsgi/ update_paths: - layers From 2fa51f74d1b8c0c5e3766be483b113ca91c576f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Wed, 20 Jun 2018 14:51:02 +0200 Subject: [PATCH 50/92] Clearer documentation about witch template we should use --- doc/integrator/create_application.rst.mako | 45 +++++++++++++------ .../scaffolds/__init__.py | 10 ----- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/doc/integrator/create_application.rst.mako b/doc/integrator/create_application.rst.mako index d1f33f44ba..4bce4a530b 100644 --- a/doc/integrator/create_application.rst.mako +++ b/doc/integrator/create_application.rst.mako @@ -3,8 +3,8 @@ Create a new application ======================== -Creating a new c2cgeoportal application is done by applying two Paste skeletons -(a.k.a. templates and scaffolds). These skeletons are provided by the +Creating a new c2cgeoportal application is done by applying two Paste scaffolds +(a.k.a. templates and scaffolds). These scaffolds are provided by the ``c2cgeoportal`` package. So to be able to create a c2cgeoportal application the ``c2cgeoportal`` package must be installed. @@ -15,17 +15,17 @@ that is already alongside the existing c2cgeoportal application. .. note:: - Some c2cgeoportal applications provide their own skeletons. For example - a *parent* application may provide a skeleton for creating *child* - applications. In that case, the c2cgeoportal skeletons, as well as the - application skeletons, should be applied. + Some c2cgeoportal applications provide their own scaffolds. For example + a *parent* application may provide a scaffold for creating *child* + applications. In that case, the c2cgeoportal scaffolds, as well as the + application scaffolds, should be applied. Project structure ----------------- In the simple case the root directory of the application is the directory -created by the c2cgeoportal skeletons (the ``c2cgeoportal_create`` and -``c2cgeoportal_update`` skeletons). +created by the c2cgeoportal scaffolds (the ``c2cgeoportal_create`` and +``c2cgeoportal_update`` scaffolds). Install c2cgeoportal -------------------- @@ -49,10 +49,10 @@ To get ``c2cgeoportal`` you first need to get the related docker image: wget https://raw.githubusercontent.com/camptocamp/c2cgeoportal/${git_branch}/docker-run chmod +x docker-run -List existing skeletons +List existing scaffolds ----------------------- -To list the available skeletons/templates use the following command, either +To list the available scaffolds use the following command, either from the root directory of c2cgeoportal (if you have followed the instructions from the previous section), or from the root directory of the existing c2cgeoportal application you want to create the new application from: @@ -61,9 +61,11 @@ c2cgeoportal application you want to create the new application from: ./docker-run --image=camptocamp/geomapfish-build pcreate -l -You should at least see the c2cgeoportal skeletons: +You should at least see the c2cgeoportal scaffolds: * c2cgeoportal_create +* c2cgeoportal_nondockercreate +* c2cgeoportal_nondockerupdate * c2cgeoportal_update Create the new application @@ -77,13 +79,20 @@ Normally the project name should be the same name as the Git repository name. The package name should not contain an underscore (``_``) because of an issue with Pip. -To create the application first apply the ``c2cgeoportal_create`` skeleton: +To create the application first apply the ``c2cgeoportal_create`` scaffold: .. prompt:: bash ./docker-run -ti --image=camptocamp/geomapfish-build \ pcreate -s c2cgeoportal_create +And for the non Docker version, apply also the ``c2cgeoportal_nondockercreate`` scaffold: + +.. prompt:: bash + + ./docker-run -ti --image=camptocamp/geomapfish-build \ + pcreate -s c2cgeoportal_nondockercreate + .. note:: Do not add any '/' after the project name. @@ -107,19 +116,27 @@ This will create a directory named ```` that will be next to the ``c2cgeoportal`` directory, or to the directory of the application you are creating this application from. -Now apply the ``c2cgeoportal_update`` skeleton: +Now apply the ``c2cgeoportal_update`` scaffold: .. prompt:: bash ./docker-run -ti --env=SRID=21781 --image=camptocamp/geomapfish-build \ pcreate -s c2cgeoportal_update --package-name +And for the non Docker version, apply also the ``c2cgeoportal_nondockerupdate`` scaffold: + +.. prompt:: bash + + ./docker-run -ti --env=SRID=21781 --image=camptocamp/geomapfish-build \ + pcreate -s c2cgeoportal_nondockerupdate --package-name + + .. note:: Do not add any '/' after the project name. The ``c2cgeoportal_update`` scaffold is also used to update the -application. The files generated by this skeleton are prefixed with +application. The files generated by this scaffold are prefixed with ``CONST_``, which means they are *constant* files that should not be changed. Following this rule is important for easier updates. diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/__init__.py b/geoportal/c2cgeoportal_geoportal/scaffolds/__init__.py index bccf98ac60..24e7cea265 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/__init__.py +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/__init__.py @@ -39,7 +39,6 @@ from pyramid.scaffolds.template import Template from pyramid.compat import input_ -from c2cgeoportal_geoportal.lib.bashcolor import colorize, GREEN class BaseTemplate(Template): # pragma: no cover @@ -170,13 +169,6 @@ def post(self, command, output_dir, vars_): fix_executables(output_dir, ("docker-run", "docker-compose-run", "bin/*")) - self.out("\nContinue with:") - self.out(colorize( - "SRID={vars[srid]} EXTENT={vars[extent]} ./docker-run pcreate --scaffold c2cgeoportal_update " - "--ignore-conflicting-name --package-name {vars[package]} ../{vars[project]}".format(vars=vars_), - GREEN - )) - return BaseTemplate.post(self, command, output_dir, vars_) @@ -208,8 +200,6 @@ def post(self, command, output_dir, vars_): fix_executables(output_dir, ("docker-run", "docker-compose-run", "bin/*"), True) - self.out(colorize("\nWelcome to c2cgeoportal!", GREEN)) - return BaseTemplate.post(self, command, output_dir, vars_) From cfdf2a2c17d0024707cefe8170269ddc5ea8d14c Mon Sep 17 00:00:00 2001 From: "arnaud.morvan@camptocamp.com" Date: Wed, 20 Jun 2018 15:20:52 +0200 Subject: [PATCH 51/92] Fix commons packaging (add requirements.txt) --- commons/MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commons/MANIFEST.in b/commons/MANIFEST.in index 9561fb1061..24ab90c6b7 100644 --- a/commons/MANIFEST.in +++ b/commons/MANIFEST.in @@ -1 +1 @@ -include README.rst +include README.rst requirements.txt From 73e8fad96bb04625de1d7f589de87d4533922e94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Wed, 20 Jun 2018 15:59:06 +0200 Subject: [PATCH 52/92] Don't remove the generated alt applications And fix all his deployment --- Makefile | 36 +++++++++---- .../nondockerupdate/+dot+upgrade.yaml_tmpl | 14 +++++ .../scaffolds/update/+dot+upgrade.yaml_tmpl | 14 +++++ .../scripts/c2cupgrade.py | 51 ++++++++++++------- 4 files changed, 86 insertions(+), 29 deletions(-) diff --git a/Makefile b/Makefile index b14b8d151f..709dfa1f3e 100644 --- a/Makefile +++ b/Makefile @@ -65,20 +65,24 @@ ADMIN_SRC_FILES = $(shell ls -1 commons/c2cgeoportal_commons/models/*.py) \ APPS += desktop mobile APPS_PACKAGE_PATH_NONDOCKER = geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/geoportal/+package+_geoportal -APPS_HTML_FILES = $(addprefix $(APPS_PACKAGE_PATH_NONDOCKER)/static-ngeo/js/apps/, $(addsuffix .html.ejs_tmpl, $(APPS))) APPS_PACKAGE_PATH = geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/+package+_geoportal +APPS_HTML_FILES = $(addprefix $(APPS_PACKAGE_PATH_NONDOCKER)/static-ngeo/js/apps/, $(addsuffix .html.ejs_tmpl, $(APPS))) APPS_HTML_FILES += $(addprefix $(APPS_PACKAGE_PATH)/static-ngeo/js/apps/, $(addsuffix .html.ejs_tmpl, $(APPS))) APPS_JS_FILES = $(addprefix $(APPS_PACKAGE_PATH)/static-ngeo/js/apps/Controller, $(addsuffix .js_tmpl, $(APPS))) -APPS_ALT += desktop_alt mobile_alt oeedit oeview -APPS_PACKAGE_PATH_ALT = geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_create_template/geoportal/+package+_geoportal/ -APPS_HTML_FILES += $(addprefix $(APPS_PACKAGE_PATH_ALT)/static-ngeo/js/apps/, $(addsuffix .html.ejs_tmpl, $(APPS_ALT))) -APPS_JS_FILES += $(addprefix $(APPS_PACKAGE_PATH_ALT)/static-ngeo/js/apps/Controller, $(addsuffix .js_tmpl, $(APPS_ALT))) APPS_FILES = $(APPS_HTML_FILES) $(APPS_JS_FILES) \ $(APPS_PACKAGE_PATH)/static-ngeo/js/apps/contextualdata.html \ $(APPS_PACKAGE_PATH)/static-ngeo/js/apps/image/background-layer-button.png \ $(APPS_PACKAGE_PATH)/static-ngeo/js/apps/image/favicon.ico \ $(APPS_PACKAGE_PATH)/static-ngeo/js/apps/image/logo.png +APPS_ALT += desktop_alt mobile_alt oeedit oeview +APPS_PACKAGE_PATH_ALT_NONDOCKER = geoportal/c2cgeoportal_geoportal/scaffolds/nondockerupdate/CONST_create_template/geoportal/+package+_geoportal +APPS_PACKAGE_PATH_ALT = geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_create_template/geoportal/+package+_geoportal +APPS_HTML_FILES_ALT = $(addprefix $(APPS_PACKAGE_PATH_ALT_NONDOCKER)/static-ngeo/js/apps/, $(addsuffix .html.ejs_tmpl, $(APPS_ALT))) +APPS_HTML_FILES_ALT += $(addprefix $(APPS_PACKAGE_PATH_ALT)/static-ngeo/js/apps/, $(addsuffix .html.ejs_tmpl, $(APPS_ALT))) +APPS_JS_FILES_ALT += $(addprefix $(APPS_PACKAGE_PATH_ALT)/static-ngeo/js/apps/Controller, $(addsuffix .js_tmpl, $(APPS_ALT))) +APPS_FILES_ALT = $(APPS_HTML_FILES_ALT) $(APPS_JS_FILES_ALT) + .PHONY: help help: @echo "Usage: $(MAKE) " @@ -122,7 +126,7 @@ clean: rm --force geoportal/c2cgeoportal_admin/locale/en/LC_MESSAGES/c2cgeoportal_admin.po rm --recursive --force geoportal/c2cgeoportal_geoportal/static/build rm --force $(MAKO_FILES:.mako=) - rm --force $(APPS_FILES) + rm --force $(APPS_FILES) $(APPS_FILES_ALT) rm --force geoportal/tests/functional/alembic.yaml .PHONY: clean-all @@ -177,7 +181,8 @@ docker-build-build: $(shell docker-required --path . --replace-pattern='^test(.* geoportal/c2cgeoportal_geoportal/scaffolds/nondockerupdate/CONST_create_template/ \ $(MO_FILES) \ $(L10N_PO_FILES) \ - $(APPS_FILES) + $(APPS_FILES) \ + $(APPS_FILES_ALT) docker build --build-arg=VERSION=$(VERSION) --tag=$(DOCKER_BASE)-build:$(MAJOR_VERSION) . .PHONY: prepare-tests @@ -315,7 +320,7 @@ transifex-init: $(TX_DEPENDENCIES) \ # Import ngeo templates .PHONY: import-ngeo-apps -import-ngeo-apps: $(APPS_FILES) +import-ngeo-apps: $(APPS_FILES) $(APPS_FILES_ALT) .PRECIOUS: ngeo ngeo: @@ -348,12 +353,23 @@ $(APPS_PACKAGE_PATH)/static-ngeo/js/apps/Controller%.js_tmpl: ngeo/contribs/gmf/ mkdir --parent $(dir $@) import-ngeo-apps --js $* $< $@ -$(APPS_PACKAGE_PATH_ALT)/static-ngeo/js/apps/%.html.ejs_tmpl: ngeo/contribs/gmf/apps/%/index.html.ejs +$(APPS_PACKAGE_PATH_ALT)/static-ngeo/js/apps/%.html.ejs_tmpl: \ + ngeo/contribs/gmf/apps/%/index.html.ejs \ + geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_create_template/ $(PRERULE_CMD) mkdir --parent $(dir $@) import-ngeo-apps --html $* $< $@ -$(APPS_PACKAGE_PATH_ALT)/static-ngeo/js/apps/Controller%.js_tmpl: ngeo/contribs/gmf/apps/%/Controller.js +$(APPS_PACKAGE_PATH_ALT_NONDOCKER)/static-ngeo/js/apps/%.html.ejs_tmpl: \ + ngeo/contribs/gmf/apps/%/index.html.ejs \ + geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_create_template/ + $(PRERULE_CMD) + mkdir --parent $(dir $@) + import-ngeo-apps --html --non-docker $* $< $@ + +$(APPS_PACKAGE_PATH_ALT)/static-ngeo/js/apps/Controller%.js_tmpl: \ + ngeo/contribs/gmf/apps/%/Controller.js \ + geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_create_template/ $(PRERULE_CMD) mkdir --parent $(dir $@) import-ngeo-apps --js $* $< $@ diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockerupdate/+dot+upgrade.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockerupdate/+dot+upgrade.yaml_tmpl index e7cbf7349d..edfe16e88d 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockerupdate/+dot+upgrade.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockerupdate/+dot+upgrade.yaml_tmpl @@ -25,6 +25,20 @@ default_project_file: - geoportal/{{package}}_geoportal/static-ngeo/js/{{package}}module\.js - mapserver/demo\.map\.mako +no_diff: + - .*\.po + - CONST_.+ + - .*/CONST_.+ +extra: + - geoportal/demo_geoportal/static-ngeo/js/apps/desktop_alt\.html\.ejs + - geoportal/demo_geoportal/static-ngeo/js/apps/mobile_alt\.html\.ejs + - geoportal/demo_geoportal/static-ngeo/js/apps/oeview\.html\.ejs + - geoportal/demo_geoportal/static-ngeo/js/apps/oeedit\.html\.ejs + - geoportal/demo_geoportal/static-ngeo/js/apps/Controllerdesktop_alt\.js + - geoportal/demo_geoportal/static-ngeo/js/apps/Controllermobile_alt\.js + - geoportal/demo_geoportal/static-ngeo/js/apps/Controlleroeview\.js + - geoportal/demo_geoportal/static-ngeo/js/apps/Controlleroeedit\.js + files_to_remove: - file: .build version: 2.3 diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl index 05a7ccdae8..9b2dafa1c1 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl @@ -26,6 +26,20 @@ default_project_file: - geoportal/{{package}}_geoportal/static-ngeo/js/{{package}}module\.js - mapserver/demo\.map\.tmpl\.mako +no_diff: + - .*\.po + - CONST_.+ + - .*/CONST_.+ +extra: + - geoportal/demo_geoportal/static-ngeo/js/apps/desktop_alt\.html\.ejs + - geoportal/demo_geoportal/static-ngeo/js/apps/mobile_alt\.html\.ejs + - geoportal/demo_geoportal/static-ngeo/js/apps/oeview\.html\.ejs + - geoportal/demo_geoportal/static-ngeo/js/apps/oeedit\.html\.ejs + - geoportal/demo_geoportal/static-ngeo/js/apps/Controllerdesktop_alt\.js + - geoportal/demo_geoportal/static-ngeo/js/apps/Controllermobile_alt\.js + - geoportal/demo_geoportal/static-ngeo/js/apps/Controlleroeview\.js + - geoportal/demo_geoportal/static-ngeo/js/apps/Controlleroeedit\.js + files_to_remove: - file: .build version: 2.3 diff --git a/geoportal/c2cgeoportal_geoportal/scripts/c2cupgrade.py b/geoportal/c2cgeoportal_geoportal/scripts/c2cupgrade.py index d6f5084dc8..bf2772def2 100644 --- a/geoportal/c2cgeoportal_geoportal/scripts/c2cupgrade.py +++ b/geoportal/c2cgeoportal_geoportal/scripts/c2cupgrade.py @@ -499,6 +499,8 @@ def step7(self, step): def is_managed(self, file_): default_project_file = self.get_upgrade('default_project_file') + + # managed means managed by the application owner, not the c2cupgrade managed = False for pattern in default_project_file['include']: if re.match(pattern + '$', file_): @@ -512,6 +514,13 @@ def is_managed(self, file_): print('managed', file_, pattern) managed = False break + + if not managed and not os.path.exists(file_): + for pattern in self.get_upgrade('extra'): + if re.match(pattern + '$', file_): + print("File '{}' is an extra by migration config pattern '{}'.".format(file_, pattern)) + managed = True + if not managed: for pattern in self.project['managed_files']: if re.match(pattern + '$', file_): @@ -578,17 +587,29 @@ def step8(self, step): "file (listed in the `changelog.diff` file)." ) + def get_modified(self, status_path): + status = check_output(["git", "status", "--short", status_path]).decode("utf-8") + status = [s for s in status.split("\n") if len(s) > 3] + status = [s[3:] for s in status if s[:3].strip() == "M"] + for pattern in self.get_upgrade('no_diff'): + matcher = re.compile('CONST_create_template/{}$'.format(pattern)) + status = [s for s in status if not matcher.match(s)] + status = [s for s in status if os.path.exists(s[len("CONST_create_template/"):])] + status = [s for s in status if not filecmp.cmp(s, s[len("CONST_create_template/"):])] + return status + @Step(9) def step9(self, step): if os.path.isfile("changelog.diff"): os.unlink("changelog.diff") + status = self.get_modified("CONST_create_template/geoportal/{}_geoportal/static-ngeo".format( + self.project["project_package"] + )) + with open("ngeo.diff", "w") as diff_file: - check_call([ - "git", "diff", "--", "--staged", - "CONST_create_template/geoportal/{}_geoportal/static-ngeo".format( - self.project["project_package"]), - ], stdout=diff_file) + if len(status) != 0: + check_call(["git", "diff", "--staged", "--"] + status, stdout=diff_file) if os.path.getsize("ngeo.diff") == 0: self.run_step(step + 1) @@ -604,28 +625,20 @@ def step10(self, step): if os.path.isfile("ngeo.diff"): os.unlink("ngeo.diff") - status = check_output(["git", "status", "--short", "CONST_create_template"]).decode("utf-8") - status = [s for s in status.split("\n") if len(s) > 3] - status = [s[3:] for s in status if s[:3].strip() == "M"] + status = self.get_modified("CONST_create_template") status = [s for s in status if not s.startswith( - "CONST_create_template/{}/static-ngeo/".format(self.project["project_package"]), + "CONST_create_template/geoportal/{}_geoportal/static-ngeo/".format( + self.project["project_package"] + ), )] - matcher = re.compile(r"CONST_create_template.*/CONST_.+") - status = [s for s in status if not matcher.match(s)] - matcher = re.compile(r".*\.po$") - status = [s for s in status if not matcher.match(s)] - status = [ - s for s in status if - not os.path.exists(s[len("CONST_create_template/"):]) or - not filecmp.cmp(s, s[len("CONST_create_template/"):]) - ] self.options.use_makefile = True self.options.makefile = self.options.new_makefile if len(status) > 0: with open("create.diff", "w") as diff_file: - check_call(["git", "diff", "--staged", "--"] + status, stdout=diff_file) + if len(status) != 0: + check_call(["git", "diff", "--staged", "--"] + status, stdout=diff_file) if os.path.getsize("create.diff") == 0: self.run_step(step + 1) From d6eec125a1d980a864896b12f773edfd9c904c79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Thu, 21 Jun 2018 12:49:27 +0200 Subject: [PATCH 53/92] Add functionalities.available_in_templates to update paths --- geoportal/c2cgeoportal_geoportal/scaffolds/create/vars.yaml_tmpl | 1 + .../scaffolds/nondockercreate/vars.yaml_tmpl | 1 + 2 files changed, 2 insertions(+) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/vars.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/create/vars.yaml_tmpl index 1f6245d04a..15552f553a 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/vars.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/vars.yaml_tmpl @@ -284,6 +284,7 @@ update_paths: - docker_services.geoportal.environment - docker_services.tilecloudchain.environment - fulltextsearch + - functionalities.available_in_templates - interfaces_theme - shortener diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/vars.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/vars.yaml_tmpl index e6428f2c94..450670dad8 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/vars.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/vars.yaml_tmpl @@ -288,6 +288,7 @@ update_paths: - docker_services.geoportal.environment - docker_services.tilecloudchain.environment - fulltextsearch + - functionalities.available_in_templates - interfaces_theme - shortener From 94bb142b6b2c60e076357b368bebfbe332703401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Thu, 21 Jun 2018 12:50:28 +0200 Subject: [PATCH 54/92] Add preremove to be stronger --- .../c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl | 1 + 1 file changed, 1 insertion(+) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl index 53f74ca9fd..17b053f407 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl @@ -602,6 +602,7 @@ $(APP_OUTPUT_DIR)/images/: /usr/lib/node_modules/jquery-ui/themes/base/images $(APP_OUTPUT_DIR)/images/ $(PRERULE_CMD) # Workaround to make Webpack working for ol/index.js + rm --force geoportal/node_modules ln --symbolic /usr/lib/node_modules/ geoportal/ (cd geoportal; INTERFACE=$* webpack $(WEBPACK_ARGS)) rm geoportal/node_modules From 492d93f4c21980549f421d0f001d8c3330d422a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Thu, 21 Jun 2018 13:41:08 +0200 Subject: [PATCH 55/92] Fix dev mode for non Docker again --- .../scaffolds/nondockercreate/+package+.mk_tmpl | 2 ++ .../scaffolds/nondockercreate/vars_nondocker.yaml_tmpl | 2 ++ .../scaffolds/update/CONST_Makefile_tmpl | 3 --- .../scaffolds/update/CONST_vars.yaml_tmpl | 2 -- geoportal/c2cgeoportal_geoportal/views/dev.py | 2 +- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/+package+.mk_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/+package+.mk_tmpl index 0bd655cd1d..7fe2c7f15c 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/+package+.mk_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/+package+.mk_tmpl @@ -17,6 +17,8 @@ GIT_REMOTE_URL ?= git@github.com:camptocamp/{{package}}.git DEPLOY_BRANCH_BASE_URL ?= $(VISIBLE_PROTOCOL)://$(VISIBLE_HOST) DEPLOY_BRANCH_MAKEFILE ?= {{package}}.mk +DEV_SERVER_PORT ?= 8081 +export DEV_SERVER_PORT ifeq ($(FINALISE), TRUE) include nondocker-finalise.mk diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/vars_nondocker.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/vars_nondocker.yaml_tmpl index f7867cb33c..f5cfb9e003 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/vars_nondocker.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/vars_nondocker.yaml_tmpl @@ -25,6 +25,7 @@ vars: tinyowsproxy: tinyows_url: http://localhost/{instanceid}/tinyows print_url: http://localhost:8080/print-{instanceid}/print/{package} + devserver_url: http://localhost:{DEV_SERVER_PORT}/{instanceid}/wsgi/ pdfreport: print_url: http://localhost:8080/print-{instanceid}/print/{package} servers: @@ -90,3 +91,4 @@ environment: - PRINT_URL - EXTERNAL_PYTHON_VERSION_MAJOR - EXTERNAL_PYTHON_VERSION_MINOR + - DEV_SERVER_PORT diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl index 17b053f407..cf0cc34529 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl @@ -2,9 +2,6 @@ GEOMAPFISH_VERSION ?= {{geomapfish_version}} export GEOMAPFISH_VERSION export PACKAGE = {{package}} -DEV_SERVER_PORT ?= 8081 -export DEV_SERVER_PORT - INSTANCE ?= main export INSTANCE diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl index e9ef1f93d6..36a82f9aab 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl @@ -14,7 +14,6 @@ vars: host: '{VISIBLE_WEB_HOST}' web_protocol: '{VISIBLE_WEB_PROTOCOL}' entry_point: '{VISIBLE_ENTRY_POINT}' - dev_server_port: '{DEV_SERVER_PORT}' docker_entry_point: '{DOCKER_ENTRY_POINT}' docker_host: '{DOCKER_WEB_HOST}' docker_web_protocol: '{DOCKER_WEB_PROTOCOL}' @@ -540,7 +539,6 @@ environment: - WMTSCAPABILITIES_PATH - BUILD_VOLUME_NAME - PROJECT_DIRECTORY - - DEV_SERVER_PORT - INSTANCE runtime_environment: diff --git a/geoportal/c2cgeoportal_geoportal/views/dev.py b/geoportal/c2cgeoportal_geoportal/views/dev.py index d2640498df..037dcdba21 100644 --- a/geoportal/c2cgeoportal_geoportal/views/dev.py +++ b/geoportal/c2cgeoportal_geoportal/views/dev.py @@ -49,7 +49,7 @@ def __init__(self, request): def dev(self): path = self.THEME_RE.sub('', self.request.path_info) if self.request.path.endswith('/dynamic.js'): - return HTTPFound(location=self.request.route_url('dynamic')) + return HTTPFound(location=self.request.route_url('dynamic'), _query=self.request.params) else: return self._proxy_response('dev', "{}/{}".format( self.dev_url.rstrip('/'), path.lstrip('/') From a02a1cd46aba2bac99da45281717696cb78f1370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Thu, 21 Jun 2018 13:55:23 +0200 Subject: [PATCH 56/92] And for the Docker mode --- geoportal/c2cgeoportal_geoportal/views/dev.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/geoportal/c2cgeoportal_geoportal/views/dev.py b/geoportal/c2cgeoportal_geoportal/views/dev.py index 037dcdba21..3eb8fc4bdb 100644 --- a/geoportal/c2cgeoportal_geoportal/views/dev.py +++ b/geoportal/c2cgeoportal_geoportal/views/dev.py @@ -33,6 +33,7 @@ from pyramid.httpexceptions import HTTPFound from pyramid.view import view_config import re +import os logger = logging.getLogger(__name__) @@ -44,6 +45,8 @@ class Dev(Proxy): def __init__(self, request): super().__init__(request) self.dev_url = self.request.registry.settings['devserver_url'] + if 'VISIBLE_ENTRY_POINT' in os.environ: + self.dev_url = self.dev_url.replace('${VISIBLE_ENTRY_POINT}', os.environ['VISIBLE_ENTRY_POINT']) @view_config(route_name='dev') def dev(self): From 300485488c6c3f3952cb5b2e0bdc26f53628d65a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Thu, 21 Jun 2018 14:32:49 +0200 Subject: [PATCH 57/92] Set gunicorn timeout to 60s --- .../scaffolds/update/CONST_Makefile_tmpl | 13 ++++++++----- .../scaffolds/update/CONST_vars.yaml_tmpl | 7 +++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl index 53f74ca9fd..6440de2b7d 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl @@ -395,11 +395,14 @@ checks: flake8 $(CLIENT_CHECK_RULE) git-attributes yamllint spell git-attributes: git --no-pager diff --check `git log --oneline | tail -1 | cut --fields=1 --delimiter=' '` -YAML_FILES ?= $(filter-out ./tilegeneration/config.yaml ./geoportal/config.yaml ./geoportal/alembic.yaml,$(shell find \ - -name .build -prune -or \ - -name cgxp -prune -or \ - -name node_modules -prune -or \ - \( -name "*.yml" -or -name "*.yaml" \) -print)) +YAML_FILES ?= $(filter-out ./tilegeneration/config.yaml ./geoportal/config.yaml \ + ./geoportal/alembic.yaml ./docker-compose-dev.yaml ./docker-compose.yaml, \ + $(shell find \ + -name .build -prune -or \ + -name cgxp -prune -or \ + -name node_modules -prune -or \ + \( -name "*.yml" -or -name "*.yaml" \) -print \ + )) .PHONY: yamllint yamllint: $(YAML_FILES) yamllint --strict --config-file=yamllint.yaml -s $(YAML_FILES) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl index e9ef1f93d6..ce8e6cdeaf 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl @@ -60,6 +60,13 @@ vars: geoportal: environment: <<: *geo-run-env + GUNICORN_PARAMS: + --bind=:8080 + --worker-class=gthread + --threads=10 + --workers=5 + --timeout=60 + --worker-connections=1000 VISIBLE_WEB_HOST: '{docker_host}' VISIBLE_WEB_PROTOCOL: '{docker_web_protocol}' TINYOWS_URL: http://tinyows/ From 61ce27457065b92d045d5911cc8822583aaaab17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Fri, 22 Jun 2018 15:59:02 +0200 Subject: [PATCH 58/92] Get the qgisserver plugin from geomapfish_qgisserver repo https://github.com/camptocamp/geomapfish_qgisserver --- docker/qgisserver/Dockerfile | 3 + docker/qgisserver/__init__.py | 29 ++++ docker/qgisserver/accesscontrol.py | 231 +++++++++++++++++++++++++++++ docker/qgisserver/metadata.txt | 30 ++++ 4 files changed, 293 insertions(+) create mode 100644 docker/qgisserver/Dockerfile create mode 100644 docker/qgisserver/__init__.py create mode 100644 docker/qgisserver/accesscontrol.py create mode 100644 docker/qgisserver/metadata.txt diff --git a/docker/qgisserver/Dockerfile b/docker/qgisserver/Dockerfile new file mode 100644 index 0000000000..e6e0ee781d --- /dev/null +++ b/docker/qgisserver/Dockerfile @@ -0,0 +1,3 @@ +FROM camptocamp/qgis-server:3 + +COPY * /var/www/plugins/geomapfish_qgisserver/ diff --git a/docker/qgisserver/__init__.py b/docker/qgisserver/__init__.py new file mode 100644 index 0000000000..0feefefecc --- /dev/null +++ b/docker/qgisserver/__init__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +""" +Copyright: (C) 2016 by Camptocamp SA +Contact: info@camptocamp.com + +.. note:: This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by the + Free Software Foundation; either version 2 of the License, or (at your + option) any later version. +""" + + +from qgis.core import QgsMessageLog +from geomapfish_qgisserver.accesscontrol import GMFException + +try: + import pkg_resources +except: + pass + + +def serverClassFactory(serverIface): # noqa + QgsMessageLog.logMessage("Starting GeoMapFish access restriction...") + + try: + from geomapfish_qgisserver.accesscontrol import GeoMapFishAccessControl + return GeoMapFishAccessControl(serverIface) + except GMFException as e: + QgsMessageLog.logMessage(str(e)) diff --git a/docker/qgisserver/accesscontrol.py b/docker/qgisserver/accesscontrol.py new file mode 100644 index 0000000000..90df469de5 --- /dev/null +++ b/docker/qgisserver/accesscontrol.py @@ -0,0 +1,231 @@ +# -*- coding: utf-8 -*- +""" +Copyright: (C) 2016 by Camptocamp SA +Contact: info@camptocamp.com + +.. note:: This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by the + Free Software Foundation; either version 2 of the License, or (at your + option) any later version. +""" + +from qgis.core import QgsMessageLog + +import os +import traceback +from shapely import ops +import geoalchemy2 +import sqlalchemy +import sqlahelper + +from qgis.server import QgsAccessControlFilter +from qgis.core import QgsDataSourceUri +import c2cgeoportal + + +class GMFException(Exception): + def __init__(self, msg): + super(GMFException, self).__init__(msg) + + +class GeoMapFishAccessControl(QgsAccessControlFilter): + """ Implements GeoMapFish access restriction """ + + EXPRESSION_TYPE = ["GPKG", "PostgreSQL database with PostGIS extension"] + + def __init__(self, server_iface): + super(QgsAccessControlFilter, self).__init__(server_iface) + + self.server_iface = server_iface + self.area_cache = {} + + try: + if "GEOMAPFISH_SCHEMA" not in os.environ: + raise GMFException("The environment variable 'GEOMAPFISH_SCHEMA' is not defined.") + if "GEOMAPFISH_SRID" not in os.environ: + raise GMFException("The environment variable 'GEOMAPFISH_SRID' is not defined.") + if "GEOMAPFISH_OGCSERVER" not in os.environ: + raise GMFException("The environment variable 'GEOMAPFISH_OGCSERVER' is not defined.") + if "GEOMAPFISH_SQLALCHEMYURL" not in os.environ: + raise GMFException("The environment variable 'GEOMAPFISH_SQLALCHEMYURL' is not defined.") + + c2cgeoportal.schema = os.environ["GEOMAPFISH_SCHEMA"] + c2cgeoportal.srid = os.environ["GEOMAPFISH_SRID"] + + sqlahelper.add_engine(sqlalchemy.create_engine(os.environ["GEOMAPFISH_SQLALCHEMYURL"])) + + from c2cgeoportal.models import DBSession, LayerWMS, OGCServer + + self.ogcserver = DBSession.query(OGCServer).filter(OGCServer.name == unicode(os.environ["GEOMAPFISH_OGCSERVER"])).one() + + self.layers = {} + # TODO manage groups ... + for layer in DBSession.query(LayerWMS).filter(LayerWMS.ogc_server_id == self.ogcserver.id).all(): + for name in layer.layer.split(','): + if name not in self.layers: + self.layers[name] = [] + self.layers[name].append(layer) + + server_iface.registerAccessControl(self, 100) + except Exception as e: + QgsMessageLog.logMessage(traceback.format_tb(e)) + QgsMessageLog.logMessage(str(e)) + raise + + def get_role(self): + from c2cgeoportal.models import DBSession, Role + + # headers = self.serverInterface().requestHandler() + + # return DBSession.query(User).get(headers.parameterMap()['USER_ID']).role + # return DBSession.query(Role).get(headers.parameterMap()['ROLE']) + return DBSession.query(Role).first() + + def get_restriction_areas(self, gmf_layers, rw=False, role=False): + """ + None => full acces + [] => no acces + shapely.ops.cascaded_union(result) => geom of access + """ + if role is False: + role = self.get_role() + + restriction_areas = [] + for layer in gmf_layers: + for restriction_area in layer.restrictionareas: + if role in restriction_area.roles and rw is False or restriction_area.readwrite is True: + if restriction_area.area is None: + return None + else: + restriction_areas.append(geoalchemy2.shape.to_shape( + restriction_area.area + )) + + return restriction_areas + + def get_area(self, layer, rw=False): + role = self.get_role() + key = (layer.name(), role.name, rw) + + if key in self.area_cache: + return self.area_cache[key] + + gmf_layers = self.layers[layer.name()] + restriction_areas = self.get_restriction_areas(gmf_layers, role=role) + + if restriction_areas is None: + self.area_cache[key] = None + return None + + area = ops.unary_union(restriction_areas).wkt + self.area_cache[key] = area + return area + + def layerFilterSubsetString(self, layer): # NOQA + """ Return an additional subset string (typically SQL) filter """ + QgsMessageLog.logMessage("layerFilterSubsetString {}".format(layer.dataProvider().storageType())) + + try: + if layer.dataProvider().storageType() not in self.EXPRESSION_TYPE: + return None + + area = self.get_area(layer) + if area is None: + return None + area = "ST_GeomFromText('{}', {})".format( + area, c2cgeoportal.srid + ) + if int(c2cgeoportal.srid) != layer.crs().postgisSrid(): + area = "ST_transform({}, {})".format( + area, layer.crs().postgisSrid() + ) + QgsMessageLog.logMessage("ST_intersects({}, {})".format( + QgsDataSourceUri(layer.dataProvider().dataSourceUri()).geometryColumn(), area + )) + return "ST_intersects({}, {})".format( + QgsDataSourceUri(layer.dataProvider().dataSourceUri()).geometryColumn(), area + ) + except Exception: + QgsMessageLog.logMessage(traceback.format_exc()) + raise + + def layerFilterExpression(self, layer): # NOQA + """ Return an additional expression filter """ + QgsMessageLog.logMessage("layerFilterExpression {}".format(layer.dataProvider().storageType())) + + try: + if layer.dataProvider().storageType() in self.EXPRESSION_TYPE: + return None + + area = self.get_area(layer) + + if area is None: + return None + + QgsMessageLog.logMessage("intersects($geometry, geom_from_wkt('{}'))".format(area)) + #return "geometry = '{}'".format(ops.unary_union(restriction_areas).wkt) + #return "fid = 2" + # TODO cache the union + # TODO verify the geometry + return "intersects($geometry, transform(geom_from_wkt('{}'), 'EPSG:{}', 'EPSG:{}')".format( + area, c2cgeoportal.srid, layer.crs().projectionAcronym() + ) + except Exception: + QgsMessageLog.logMessage(traceback.format_exc()) + raise + + def layerPermissions(self, layer): # NOQA + """ Return the layer rights """ + QgsMessageLog.logMessage("layerPermissions {}".format(layer.name())) + + try: + rights = QgsAccessControlFilter.LayerPermissions() + rights.canRead = rights.canInsert = rights.canUpdate = rights.canDelete = False + + if layer.name() not in self.layers: + return rights + + gmf_layers = self.layers[layer.name()] + for l in gmf_layers: + if l.public is True: + rights.canRead is True + + if not rights.canRead: + role = self.get_role() + restriction_areas = self.get_restriction_areas(gmf_layers, role=role) + if restriction_areas is not None and len(restriction_areas) == 0: + return rights + rights.canRead = True + + restriction_areas = self.get_restriction_areas(gmf_layers, rw=True, role=role) + rights.canInsert = rights.canUpdate = rights.canDelete = \ + restriction_areas is None or len(restriction_areas) > 0 + + return rights + except Exception: + QgsMessageLog.logMessage(traceback.format_exc()) + raise + + def authorizedLayerAttributes(self, layer, attributes): # NOQA + """ Return the authorised layer attributes """ + + # TODO + return attributes + + def allowToEdit(self, layer, feature): # NOQA + """ Are we authorise to modify the following geometry """ + QgsMessageLog.logMessage("allowToEdit") + + try: + area = self.get_area(layer, rw=True) + + return area is None or area.intersect(feature.geom) + except Exception: + QgsMessageLog.logMessage(traceback.format_exc()) + raise + + def cacheKey(self): # NOQA + return "{}-{}".format( + self.serverInterface().requestHandler().parameter("Host"), + self.get_role().name, + ) diff --git a/docker/qgisserver/metadata.txt b/docker/qgisserver/metadata.txt new file mode 100644 index 0000000000..ae9208d463 --- /dev/null +++ b/docker/qgisserver/metadata.txt @@ -0,0 +1,30 @@ +# This file contains metadata for your plugin. Beginning +# with version 1.8 this is the preferred way to supply information about a +# plugin. The current method of embedding metadata in __init__.py will +# be supported until version 2.0 + +# This file should be included when you package your plugin. + +# Mandatory items: + + +[general] +name=GeoMapFish +description=GeoMapFish access control for QGIS server +qgisMinimumVersion=2.0 +version=0.1 +author=Camptocamp SA +email=info@camptocamp.com + +tags=server +server=True + +homepage=https://github.com/camptocamp/geomapfish_qgisserver +tracker=https://github.com/camptocamp/geomapfish_qgisserver/issues +repository=https://github.com/camptocamp/geomapfish_qgisserver +icon=resources/img/icons/icon.png +# experimental flag +experimental=True + +# deprecated flag (applies to the whole plugin, not just a single version +deprecated=False From ce1b87af822d51a8a2d459ebba9f5f6ffc74533a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Fri, 22 Jun 2018 16:07:41 +0200 Subject: [PATCH 59/92] Add missing alias in nondocker mode --- .../scaffolds/create/geoportal/webpack.apps.js.mako_tmpl | 2 +- .../nondockercreate/geoportal/webpack.apps.js.mako_tmpl | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/webpack.apps.js.mako_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/webpack.apps.js.mako_tmpl index 4736dfeba7..3a7e601b99 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/webpack.apps.js.mako_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/webpack.apps.js.mako_tmpl @@ -72,5 +72,5 @@ module.exports = { alias: { {{package}}: path.resolve(__dirname, '{{package}}_geoportal/static-ngeo/js'), } - } + }, }; diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/geoportal/webpack.apps.js.mako_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/geoportal/webpack.apps.js.mako_tmpl index fd9794dacd..95d1887e30 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/geoportal/webpack.apps.js.mako_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/geoportal/webpack.apps.js.mako_tmpl @@ -65,4 +65,9 @@ module.exports = { rules }, plugins: plugins, + resolve: { + alias: { + {{package}}: path.resolve(__dirname, '{{package}}_geoportal/static-ngeo/js'), + } + }, }; From fb5cae58b36c18b40e09d328f6c0ca861785153e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Fri, 22 Jun 2018 16:14:10 +0200 Subject: [PATCH 60/92] Fix codacy errors --- docker/qgisserver/Dockerfile | 1 + docker/qgisserver/__init__.py | 5 ----- docker/qgisserver/accesscontrol.py | 12 ++++++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docker/qgisserver/Dockerfile b/docker/qgisserver/Dockerfile index e6e0ee781d..6d93bec455 100644 --- a/docker/qgisserver/Dockerfile +++ b/docker/qgisserver/Dockerfile @@ -1,3 +1,4 @@ FROM camptocamp/qgis-server:3 +LABEL maintainer Camptocamp "info@camptocamp.com" COPY * /var/www/plugins/geomapfish_qgisserver/ diff --git a/docker/qgisserver/__init__.py b/docker/qgisserver/__init__.py index 0feefefecc..73dd52ebce 100644 --- a/docker/qgisserver/__init__.py +++ b/docker/qgisserver/__init__.py @@ -13,11 +13,6 @@ from qgis.core import QgsMessageLog from geomapfish_qgisserver.accesscontrol import GMFException -try: - import pkg_resources -except: - pass - def serverClassFactory(serverIface): # noqa QgsMessageLog.logMessage("Starting GeoMapFish access restriction...") diff --git a/docker/qgisserver/accesscontrol.py b/docker/qgisserver/accesscontrol.py index 90df469de5..3096cee93c 100644 --- a/docker/qgisserver/accesscontrol.py +++ b/docker/qgisserver/accesscontrol.py @@ -34,7 +34,7 @@ class GeoMapFishAccessControl(QgsAccessControlFilter): EXPRESSION_TYPE = ["GPKG", "PostgreSQL database with PostGIS extension"] def __init__(self, server_iface): - super(QgsAccessControlFilter, self).__init__(server_iface) + super().__init__(server_iface) self.server_iface = server_iface self.area_cache = {} @@ -72,7 +72,8 @@ def __init__(self, server_iface): QgsMessageLog.logMessage(str(e)) raise - def get_role(self): + @staticmethod + def get_role(): from c2cgeoportal.models import DBSession, Role # headers = self.serverInterface().requestHandler() @@ -188,7 +189,7 @@ def layerPermissions(self, layer): # NOQA gmf_layers = self.layers[layer.name()] for l in gmf_layers: if l.public is True: - rights.canRead is True + rights.canRead = True if not rights.canRead: role = self.get_role() @@ -206,9 +207,12 @@ def layerPermissions(self, layer): # NOQA QgsMessageLog.logMessage(traceback.format_exc()) raise - def authorizedLayerAttributes(self, layer, attributes): # NOQA + @staticmethod + def authorizedLayerAttributes(layer, attributes): # NOQA """ Return the authorised layer attributes """ + del layer + # TODO return attributes From 05a3eec1f3c4d31165d338e49ccd5fae2e8e8e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Fri, 22 Jun 2018 16:26:38 +0200 Subject: [PATCH 61/92] Fix spell --- docker/qgisserver/accesscontrol.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/qgisserver/accesscontrol.py b/docker/qgisserver/accesscontrol.py index 3096cee93c..93d78d7e78 100644 --- a/docker/qgisserver/accesscontrol.py +++ b/docker/qgisserver/accesscontrol.py @@ -84,8 +84,8 @@ def get_role(): def get_restriction_areas(self, gmf_layers, rw=False, role=False): """ - None => full acces - [] => no acces + None => full access + [] => no access shapely.ops.cascaded_union(result) => geom of access """ if role is False: From 80848d09685758a6fe810ccef732411110cf2ecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Fri, 22 Jun 2018 16:46:28 +0200 Subject: [PATCH 62/92] Don't build mapcache if no tilecloud_chain --- .../scaffolds/nondockercreate/nondocker-override.mk | 2 ++ 1 file changed, 2 insertions(+) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-override.mk b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-override.mk index 6377b8aa26..bd557fa1bc 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-override.mk +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-override.mk @@ -15,7 +15,9 @@ export MAPSERVER_URL PRINT_URL ?= http://print:8080/print/ export PRINT_URL PRINT_CONFIG_FILE ?= print/print-apps/$(PACKAGE)/config.yaml +ifeq ($(TILECLOUD_CHAIN), TRUE) MAPCACHE_FILE ?= apache/mapcache.xml +endif TILEGENERATION_CONFIG_FILE = tilegeneration/config.yaml VISIBLE_WEB_PROTOCOL ?= https From 451ba62ca47c2c5c2dbc34826a4e718f51bd90be Mon Sep 17 00:00:00 2001 From: Wolfgang Kaltz Date: Mon, 25 Jun 2018 08:06:20 +0200 Subject: [PATCH 63/92] typos --- doc/integrator/create_application.rst.mako | 48 +++++++++++----------- doc/integrator/install_application.rst | 43 ++++++++++--------- 2 files changed, 44 insertions(+), 47 deletions(-) diff --git a/doc/integrator/create_application.rst.mako b/doc/integrator/create_application.rst.mako index 4bce4a530b..d371a15bc0 100644 --- a/doc/integrator/create_application.rst.mako +++ b/doc/integrator/create_application.rst.mako @@ -16,7 +16,7 @@ that is already alongside the existing c2cgeoportal application. .. note:: Some c2cgeoportal applications provide their own scaffolds. For example - a *parent* application may provide a scaffold for creating *child* + in a multi-project, a *parent* application may provide a scaffold for creating *child* applications. In that case, the c2cgeoportal scaffolds, as well as the application scaffolds, should be applied. @@ -30,7 +30,7 @@ created by the c2cgeoportal scaffolds (the ``c2cgeoportal_create`` and Install c2cgeoportal -------------------- -This step is required if you cannot, or do not want, to create the c2cgeoportal +This step is required if you cannot, or do not want to, create the c2cgeoportal application from an existing one. For example, if you are creating a child application from an existing parent application, it means you already have ``c2cgeoportal`` installed, so you can just skip this section, and directly go @@ -71,7 +71,7 @@ You should at least see the c2cgeoportal scaffolds: Create the new application -------------------------- -The first step in the project creation is to chose a project name +The first step in the project creation is to choose a project name ````, and a package name ````. Normally the project name should be the same name as the Git repository name. @@ -103,7 +103,7 @@ it later. .. note:: - You can define these information directly in the command line using + You can define this information directly in the command line using parameters: .. prompt:: bash @@ -172,8 +172,8 @@ Commit and push on the main repository: git commit -m "Initial commit" git push origin master -Configuration of different environment in your project ------------------------------------------------------- +Configuration of different environments in your project +------------------------------------------------------- Concepts ........ @@ -186,42 +186,40 @@ Concepts Hierarchy and extending your configuration files ................................................ -The configuration files (Makefile and vars) have a hierarchy between them. -These files extend other files. A Makefile extends another Makefile -and similarly a vars file extends another vars file. This extension is visible -in Makefile files: +The configuration files (Makefile and vars) are organized in a hierarchy. +A Makefile extends another Makefile and similarly a vars file extends another vars file. +This extension mechanism is used in Makefile files as follows: .. code:: make include CONST_Makefile -and vars files: +and in vars files as follows: .. code:: yaml extends: CONST_vars.yaml -CONST files are files that should not be changed because they are replaced +``CONST`` files are files that should not be changed because they are replaced during application updates, so your changes will be systematically lost. You can extend these files as many times as you like, although it is not recommended to exceed 3-4 levels for readability and simplicity. .. image:: ../_static/doc_hierarchie.png -Whenever possible, it is strongly advised not to extend the -``vars_.yaml`` file, and we recommend that you use dynamic variables as -described below. However some use cases may need to do so: +Whenever possible, it is strongly advised not to extend the ``vars_.yaml`` file. +We recommend instead that you use dynamic variables as described below. +However, in some use cases extending ``vars_.yaml`` may be needed: -* Configuring really different environments. -* Configuration of a multi-project (see below for this specific use case). +* Configuring highly specific environments +* Configuration of a multi-project -Use of dynamic variable -....................... +Use of dynamic variables +........................ Variables used in the application configuration files (files ``vars_.yaml``) -can be made dynamic by means of environment variable. In the main file -``vars_.yaml``, added the ``interpreted`` block at the bottom of the -file. +can be made dynamic by means of environment variable. For this, in the main file +``vars_.yaml``, add a block ``interpreted`` at the bottom of the file. In this same file, you can change the value of a parameter by putting it in uppercase (example: ``host: HOST``). This parameter must be listed in the @@ -260,7 +258,7 @@ Configure the application As the integrator you need to edit the ``vars.yaml`` and ``.mk`` files to configure the application. -Do not miss to add your changes to git: +Do not forget to add your changes to git: .. prompt:: bash @@ -270,7 +268,7 @@ Do not miss to add your changes to git: .. note:: - If you use the check collector do not miss to add the new child to + If you are using a multi-project, you should add all new children to the parent site check_collector configuration. .. note:: @@ -341,7 +339,7 @@ Please see the Mako documentation for details: http://docs.makotemplates.org/en/latest/ -The result is also a file without the .mako. +The result is also a file without the ``.mako``. **Syntax** diff --git a/doc/integrator/install_application.rst b/doc/integrator/install_application.rst index b73f0cb3f3..de25a316d3 100644 --- a/doc/integrator/install_application.rst +++ b/doc/integrator/install_application.rst @@ -3,21 +3,17 @@ Install an existing application =============================== -On this page we explain all the procedures to build an application from +On this page we explain the procedure to build an application from only the code. -For example If you want to use an existing database you should ignore +If you want to use an existing database, you should ignore all the commands concerning the database. -This guide considers that: - - We use a server manages by Camptocamp, meaning: - - all dependencies described in the - :ref:`requirements ` are installed, - - Postgres has a GIS template ``template_postgis`` and a user ``www-data``, - - Apache uses the user ``www-data``. - - Use Git as revision control. +This guide assumes that: + - all dependencies described in the :ref:`requirements ` are installed, + - Git is used as revision control. -For the others system there is some notes to give some help. +For specific environments, this page contains some notes to give some help. Set up the database ------------------- @@ -28,7 +24,8 @@ tables are located in a specific schema of the database. .. note:: - Multiple specific schemas are actually used in a parent/child architecture. + In a multi-project application architecture, multiple specific schemas + must be used. If the application has MapServer layers linked to PostGIS tables, these tables and the application-specific tables must be in the same database, preferably in @@ -79,11 +76,11 @@ full-text search: .. _integrator_install_application_create_schema: -Create the schema -~~~~~~~~~~~~~~~~~ +Create the schemas +~~~~~~~~~~~~~~~~~~ -Each parent or child needs two application-specific schemas, -then to create it use: +Each application needs two application-specific schemas. +To create them, do: .. prompt:: bash @@ -92,6 +89,8 @@ then to create it use: with ```` and ```` replaced by the actual database name, and schema name ('main' by default), respectively. +Note that if you are using a multi-project, you need to define both schemas +for the parent and for each child. .. _integrator_install_application_create_user: @@ -108,7 +107,7 @@ We use a specific user for the application, ``www-data`` by default. sudo -u postgres createuser -P -Give the rights to the user: +Give the necessary rights to the user: .. prompt:: bash @@ -119,7 +118,7 @@ Give the rights to the user: .. note:: - If you do not use the www-data user for Apache replace it by the right user. + If you do not use the ``www-data`` user for Apache, replace it by the right user. Install the application @@ -128,7 +127,7 @@ Install the application Get the application source tree ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If Git is used for the application use the following command to get the +If GitHub is used for the application, use the following command to get the application source tree: .. prompt:: bash @@ -156,7 +155,7 @@ Index (PyPI) and they have to be downloaded manually and stored. This is because these packages use DLLs and binaries which would have to be compiled using a C compiler. -Furthermore, some changes in the apache WSGI and MapServer configurations are +Furthermore, some changes in the Apache WSGI and MapServer configurations are required to make c2cgeoportal work on Windows. Also, between all the different command interfaces available on Windows (cmd, @@ -172,7 +171,7 @@ Only use Windows default command interface:: Cygwin and git mingw are not compatible. Powershell is untested. -Complementarily you need to add all the resource paths to your system PATH +In addition, you need to add all the resource paths to your system PATH environment variable, for cygwin, git and node binaries. Cygwin @@ -252,7 +251,7 @@ already be the case; otherwise look at ``scaffolds/create/apache/application.wsg Install the application ~~~~~~~~~~~~~~~~~~~~~~~ -Then you can build and install the application with the command: +You can build and install the application with the command: .. prompt:: bash @@ -278,7 +277,7 @@ This previous command will do many things like: * build the javascript and css resources into compressed files, -Your application should be available at: +Your application should now be available at: ``http:///``. Where the ```` is directly linked to the virtual host. From 22c2e2712c97ade50059a847f90172acdb936ba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Thu, 21 Jun 2018 17:02:32 +0200 Subject: [PATCH 64/92] Be able to run as non-root Fix for kubernetes (RunAsGroup is not yet available) --- .../scaffolds/create/Dockerfile | 38 +++++++++---------- .../scaffolds/create/bin/eval-templates | 2 +- .../scaffolds/create/docker-compose.yaml.mako | 6 +++ .../create/geoportal/Dockerfile.mako | 4 +- .../scaffolds/update/CONST_vars.yaml_tmpl | 6 +++ travis/docker-compose.yaml.mako | 6 +++ 6 files changed, 41 insertions(+), 21 deletions(-) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/Dockerfile b/geoportal/c2cgeoportal_geoportal/scaffolds/create/Dockerfile index 6c26ccd0c8..cc2d13b7da 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/Dockerfile +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/Dockerfile @@ -7,24 +7,24 @@ RUN \ apt-get clean && \ rm --recursive --force /var/lib/apt/lists/* -COPY bin/* /usr/bin/ - -COPY mapserver /etc/mapserver -VOLUME /etc/mapserver - -#COPY qgisserver /project -#VOLUME /project - -COPY mapcache /mapcache -VOLUME /mapcache - -COPY tilegeneration /etc/tilegeneration -VOLUME /etc/tilegeneration - -COPY print/print-apps /usr/local/tomcat/webapps/ROOT/print-apps -VOLUME /usr/local/tomcat/webapps/ROOT/print-apps - -COPY front /etc/haproxy -VOLUME /etc/haproxy +COPY . /tmp/config/ + +RUN mv /tmp/config/bin/* /usr/bin/ && \ + if [ -e /tmp/config/mapserver ]; then mv /tmp/config/mapserver /etc/; fi && \ + if [ -e /tmp/config/tilegeneration ]; then mv /tmp/config/tilegeneration /etc/; fi && \ + if [ -e /tmp/config/qgisserver ]; then mv /tmp/config/qgisserver /project; fi && \ + if [ -e /tmp/config/mapcache ]; then mv /tmp/config/mapcache /etc/; fi && \ + if [ -e /tmp/config/front ]; then mv /tmp/config/front /etc/haproxy; fi && \ + mkdir --parent /usr/local/tomcat/webapps/ROOT/ && \ + if [ -e /tmp/config/print ]; then mv /tmp/config/print/print-apps /usr/local/tomcat/webapps/ROOT/; fi && \ + chmod g+w -R /etc /usr/local/tomcat/webapps && \ + adduser www-data root + +VOLUME /etc/mapserver \ + /project \ + /etc/mapcache \ + /etc/tilegeneration \ + /usr/local/tomcat/webapps/ROOT/print-apps \ + /etc/haproxy ENTRYPOINT [ "/usr/bin/eval-templates" ] diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/bin/eval-templates b/geoportal/c2cgeoportal_geoportal/scaffolds/create/bin/eval-templates index 7a15e27972..6bfec52dfb 100755 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/bin/eval-templates +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/bin/eval-templates @@ -3,7 +3,7 @@ export VISIBLE_WEB_HOST_RE_ESCAPED=`python3 -c "print(__import__('re').escape('${VISIBLE_WEB_HOST}'))"` export VISIBLE_ENTRY_POINT_RE_ESCAPED=`python3 -c "print(__import__('re').escape('${VISIBLE_ENTRY_POINT}'))"` -find /etc /mapcache /usr/local/tomcat/webapps/ -name '*.tmpl' -print | while read file +find /etc /usr/local/tomcat/webapps/ -name '*.tmpl' -print | while read file do echo "Evaluate: ${file}" envsubst < ${file} > ${file%.tmpl} diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako index 6fef88b064..0bc5c0bbaa 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako @@ -8,10 +8,12 @@ version: '2' services: config: image: ${docker_base}-config:${docker_tag} + user: www-data ${service_defaults('config')}\ print: image: camptocamp/mapfish_print:3.14 + user: www-data volumes_from: - config:ro ${service_defaults('print', 8080)}\ @@ -45,6 +47,7 @@ ${service_defaults('mapcache', 80)}\ memcached: image: memcached:1.5 + user: www-data command: - memcached - --memory-limit=512 @@ -52,6 +55,7 @@ ${service_defaults('memcached', 11211)}\ redis: image: redis:3.2 + user: www-data command: - redis-server - --save @@ -82,12 +86,14 @@ ${service_defaults('tilecloudchain')}\ geoportal: image: ${docker_base}-geoportal:${docker_tag} + user: www-data volumes: - /var/sig:/var/sig:ro ${service_defaults('geoportal', 8080)}\ alembic: image: ${docker_base}-geoportal:${docker_tag} + user: www-data command: - alembic - --name=static diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/Dockerfile.mako b/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/Dockerfile.mako index 5aa28ada1d..064d847677 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/Dockerfile.mako +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/Dockerfile.mako @@ -21,7 +21,9 @@ WORKDIR /app COPY . /app RUN mv webpack.apps.js webpack.apps.js.tmpl && \ - ln --symbolic /usr/lib/node_modules/ . + ln --symbolic /usr/lib/node_modules/ . && \ + chmod g+w -R . && \ + adduser www-data root ARG GIT_HASH diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl index 4dced55858..ecff4b45f9 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl @@ -78,6 +78,12 @@ vars: C2C_BROADCAST_PREFIX: broadcast_geoportal_ tilecloudchain: environment: + GUNICORN_PARAMS: + --bind=:8080 + --worker-class=gthread + --threads=10 + --workers=5 + --worker-connections=1000 VISIBLE_ENTRY_POINT: '{docker_entry_point}' TILEGENERATION_CONFIGFILE: /etc/tilegeneration/config.yaml C2C_BASE_PATH: /c2c_tiles diff --git a/travis/docker-compose.yaml.mako b/travis/docker-compose.yaml.mako index 0e4b5f6d17..56367e61cf 100644 --- a/travis/docker-compose.yaml.mako +++ b/travis/docker-compose.yaml.mako @@ -7,6 +7,8 @@ version: '2' services: config: image: ${docker_base}-config:${docker_tag} + user: www-data +${service_defaults('config')}\ db: image: ${docker_base}-testdb:${docker_tag} @@ -18,6 +20,7 @@ ${service_defaults('external-db', 5432)}\ print: image: camptocamp/mapfish_print:3.14 + user: www-data volumes_from: - config:ro ${service_defaults('print', 8080)}\ @@ -26,12 +29,15 @@ ${service_defaults('print', 8080)}\ image: camptocamp/mapserver:7.0 volumes_from: - config:rw + entrypoint: [] ${service_defaults('mapserver', 80)}\ redis: image: redis:3.2 + user: www-data ${service_defaults('mapserver', 6379)}\ geoportal: image: ${docker_base}-geoportal:${docker_tag} + user: www-data ${service_defaults('geoportal', 8080, True)}\ From 003d7a874df23504b8446953baaeab385e64bfcd Mon Sep 17 00:00:00 2001 From: Wolfgang Kaltz Date: Mon, 25 Jun 2018 09:04:00 +0200 Subject: [PATCH 65/92] remove obsolete part --- doc/integrator/create_application.rst.mako | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/doc/integrator/create_application.rst.mako b/doc/integrator/create_application.rst.mako index d371a15bc0..de0b74a2d3 100644 --- a/doc/integrator/create_application.rst.mako +++ b/doc/integrator/create_application.rst.mako @@ -30,18 +30,7 @@ created by the c2cgeoportal scaffolds (the ``c2cgeoportal_create`` and Install c2cgeoportal -------------------- -This step is required if you cannot, or do not want to, create the c2cgeoportal -application from an existing one. For example, if you are creating a child -application from an existing parent application, it means you already have -``c2cgeoportal`` installed, so you can just skip this section, and directly go -to the next. - -Also, installing ``c2cgeoportal``, as described in this section, requires -access to the c2cgeoportal GitHub repository. If you cannot view the -https://github.com/camptocamp/c2cgeoportal page in your browser that means you -do not have the required permissions. Please contact Camptocamp in that case. - -To get ``c2cgeoportal`` you first need to get the related docker image: +To get ``c2cgeoportal``, you need to get the related docker image: .. prompt:: bash From 6a6b8f8ec74d5581da01fabff56bf8609cf473f1 Mon Sep 17 00:00:00 2001 From: Michael Kalbermatten Date: Mon, 25 Jun 2018 09:43:49 +0200 Subject: [PATCH 66/92] Windows cannot run get-pip-dependencies without shell & update pip --- .../scaffolds/nondockercreate/nondocker-finalise.mk_tmpl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-finalise.mk_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-finalise.mk_tmpl index 4e2987df4c..8169c03f2e 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-finalise.mk_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-finalise.mk_tmpl @@ -162,7 +162,8 @@ else $(PYTHON) -m venv --system-site-packages .build/venv endif ifeq ($(OPERATING_SYSTEM), WINDOWS) - $(PYTHON_BIN)/python -m pip install `./get-pip-dependencies c2cgeoportal-commons c2cgeoportal-geoportal c2cgeoportal-admin Shapely Fiona rasterio GDAL flake8-mypy mypy` + $(PYTHON_BIN)/python -m pip install --upgrade pip + $(PYTHON_BIN)/python -m pip install $(shell python ./get-pip-dependencies c2cgeoportal-commons c2cgeoportal-geoportal c2cgeoportal-admin Shapely Fiona rasterio GDAL flake8-mypy mypy) else $(PYTHON_BIN)/python -m pip install `./get-pip-dependencies c2cgeoportal-commons c2cgeoportal-geoportal c2cgeoportal-admin GDAL flake8-mypy mypy` endif From f9ea4b2202d83d838eae76896cd62f846106113f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Mon, 25 Jun 2018 11:12:00 +0200 Subject: [PATCH 67/92] Fix docker push --- .../scaffolds/update/CONST_Makefile_tmpl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl index b7871ecd75..a3610dfe7d 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl @@ -704,9 +704,8 @@ endif .PHONY: push-docker push-docker: - docker push $(DOCKER_BASE)-print:$(DOCKER_TAG) + docker push $(DOCKER_BASE)-config:$(DOCKER_TAG) docker push $(DOCKER_BASE)-geoportal:$(DOCKER_TAG) - docker push $(DOCKER_BASE)-mapserver:$(DOCKER_TAG) geoportal/alembic.yaml: $(ALEMBIC_YAML_FILE) vars.yaml CONST_vars.yaml $(PRERULE_CMD) From f8a9c036ed63740fc75f489e0ccdb9ab51e3ec97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Mon, 25 Jun 2018 11:19:07 +0200 Subject: [PATCH 68/92] Print can be disable again Removes no more used MAPSERVER var --- doc/integrator/legacy.rst | 1 - doc/integrator/legacy_print.rst | 22 ------------------- doc/integrator/make.rst | 9 ++++---- doc/integrator/print.rst | 2 +- doc/integrator/requirements.rst | 4 +--- .../nondocker-finalise.mk_tmpl | 4 +++- .../scaffolds/update/CONST_Makefile_tmpl | 6 ++--- 7 files changed, 12 insertions(+), 36 deletions(-) delete mode 100644 doc/integrator/legacy_print.rst diff --git a/doc/integrator/legacy.rst b/doc/integrator/legacy.rst index d8cff6a1eb..d34362ca97 100644 --- a/doc/integrator/legacy.rst +++ b/doc/integrator/legacy.rst @@ -8,5 +8,4 @@ Legacy components legacy_cgxp legacy_editing - legacy_print legacy_advanced diff --git a/doc/integrator/legacy_print.rst b/doc/integrator/legacy_print.rst deleted file mode 100644 index b8fe211686..0000000000 --- a/doc/integrator/legacy_print.rst +++ /dev/null @@ -1,22 +0,0 @@ -.. _integrator_legacy_print: - -Print version 2.x -================= - -The print version 2.x cannot be used in the ngeo application be it still can be used in the CGXP application. - -The print version 3.x and the version 2.x cannot be used together. - -To use it you should: - - * Keep the print war in your repository. - * Add ``PRINT_VERSION ?= 2`` in your project makefile. - * Add in the ``vars`` of your project vars file: ``print_url: http://localhost:8080/print/pdf/``. - -Later on, if you want to use print version 3.x, follow the above instructions. - - * Remove your war file ``git rm print/print-servlet.war``. - * Remove the ``PRINT_VERSION`` from your project makefile (default is 3). - * In your project vars file remove the following keys: - - * ``vars/print_url`` diff --git a/doc/integrator/make.rst b/doc/integrator/make.rst index 73bf3f1379..cd0555d522 100644 --- a/doc/integrator/make.rst +++ b/doc/integrator/make.rst @@ -78,14 +78,13 @@ The following variables may be set in the makefiles: * ``CONFIG_VARS``: The list of parameters read from the project YAML configuration file. * ``DEVELOPMENT``: If ``TRUE`` the ``CSS`` and ``JS`` files are not minified and the - ``development.ini`` pyramid config file is used, default to ``FALSE``. + ``development.ini`` pyramid config file is used, default is ``FALSE``. * ``DISABLE_BUILD_RULES``: List of rules we want to disable, default is empty. -* ``LANGUAGES``: List of available languages. +* ``LANGUAGES``: List of available languages, default is ``en fr de``. * ``CGXP_INTERFACES``: List of CGXP interfaces, default is empty. * ``NGEO_INTERFACES``: List of ngeo interfaces, default is ``mobile desktop``. -* ``PRINT``: Mapfish print is enable, default to ``TRUE``. -* ``MAPSERVER``: MapServer is enable, default to ``TRUE``. -* ``TILECLOUD_CHAIN``: ``TRUE`` to indicate that we use TileCloud-chain, default to ``TRUE``. +* ``PRINT``: Mapfish print is enabled, default is ``TRUE``. +* ``TILECLOUD_CHAIN``: ``TRUE`` to indicate that we use TileCloud-chain, default is ``TRUE``. Secrets diff --git a/doc/integrator/print.rst b/doc/integrator/print.rst index 9b9d03d6a8..069443caff 100644 --- a/doc/integrator/print.rst +++ b/doc/integrator/print.rst @@ -38,7 +38,7 @@ generated by the templating. Then we should do: .. code:: make - PRINT_VERSION = NONE + PRINT = FALSE * Point to the parent print server by editing the following lines in the ``vars.yaml`` file: diff --git a/doc/integrator/requirements.rst b/doc/integrator/requirements.rst index 478c4098d3..ff9aea7d2a 100644 --- a/doc/integrator/requirements.rst +++ b/doc/integrator/requirements.rst @@ -119,9 +119,7 @@ Cygwin's git for Windows. To do so: Print ^^^^^^ -If using MapFish Print v3 (thus defining ``PRINT_VERSION ?= 3`` in your -makefile), then you should define the service name of your Tomcat server. In -your makefile, define the following variables: +In your makefile, define the following variables: .. prompt:: bash diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-finalise.mk_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-finalise.mk_tmpl index 4e2987df4c..fae25acf08 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-finalise.mk_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-finalise.mk_tmpl @@ -15,7 +15,8 @@ endif # Print - +PRINT ?= TRUE +ifeq ($(PRINT), TRUE) PRINT_WAR ?= print-$(INSTANCE_ID).war PRINT_OUTPUT ?= /srv/tomcat/tomcat1/webapps JASPERREPORTS_VERSION ?= 6.1.1 @@ -31,6 +32,7 @@ PRINT_REQUIREMENT += $(PRINT_EXTRA_LIBS) \ print/WEB-INF/classes/mapfish-spring-application-context-override.xml \ print/print-servlet.war \ $(shell $(FIND) print/print-apps) +endif # Apache diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl index b7871ecd75..8ff2068e9f 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl @@ -17,8 +17,6 @@ export DOCKER_ENTRY_POINT export DOCKER_WEB_HOST export DOCKER_WEB_PROTOCOL -MAPSERVER ?= TRUE - ifeq ($(DEBUG), TRUE) PRERULE_CMD ?= @echo "Build \033[1;34m$@\033[0m due modification on \033[1;34m$?\033[0m" 1>&2; ls -t --full-time --reverse $? $@ 1>&2 || true endif @@ -45,7 +43,7 @@ CGXP_API ?= TRUE CGXP_XAPI ?= FALSE INTERFACES += $(NGEO_INTERFACES) $(CGXP_INTERFACES) export INTERFACES -# Used print version +# Use print PRINT ?= TRUE # Enable Docker target DOCKER_BASE ?= camptocamp/{{package}} @@ -302,9 +300,11 @@ SERVER_LOCALISATION_RULES = $(addprefix /build/lang-server-, $(LANGUAGES)) endif # Print +ifeq ($(PRINT), TRUE) PRINT_BASE_DIR ?= print PRINT_CONFIG_FILE ?= print/print-apps/$(PACKAGE)/config.yaml.tmpl I18N_SOURCE_FILES += $(PRINT_CONFIG_FILE) +endif PY_FILES = $(shell find $(PACKAGE) -type f -name '*.py' -print) From 8b7cfb17bbf13bb459a6f2549ff137e60d8a6088 Mon Sep 17 00:00:00 2001 From: Wolfgang Kaltz Date: Mon, 25 Jun 2018 11:53:14 +0200 Subject: [PATCH 69/92] fix alembic parameters for build; fix build instruction for docu --- doc/integrator/install_application.rst | 2 +- .../scaffolds/update/CONST_Makefile_tmpl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/integrator/install_application.rst b/doc/integrator/install_application.rst index de25a316d3..beecbdbeb1 100644 --- a/doc/integrator/install_application.rst +++ b/doc/integrator/install_application.rst @@ -255,7 +255,7 @@ You can build and install the application with the command: .. prompt:: bash - make --makefile=.mk docker-build + ./docker-run make --makefile=.mk build Create the application tables, and directly set the version (details later): diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl index b7871ecd75..241df7da83 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl @@ -459,8 +459,8 @@ lint-ngeo: /build/eslint.timestamp .PHONY: upgrade-db upgrade-db: geoportal/alembic.ini geoportal/alembic.yaml - alembic --name=main upgrade head - alembic --name=static upgrade head + alembic --config=$< --name=main upgrade head + alembic --config=$< --name=static upgrade head # Templates From 00b69143be820a3627df2c78c8078923f9650e6f Mon Sep 17 00:00:00 2001 From: Wolfgang Kaltz Date: Mon, 25 Jun 2018 13:30:41 +0200 Subject: [PATCH 70/92] fix redirect of output --- .../scaffolds/nondockercreate/nondocker-finalise.mk_tmpl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-finalise.mk_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-finalise.mk_tmpl index 8169c03f2e..d83b9e36c8 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-finalise.mk_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-finalise.mk_tmpl @@ -175,9 +175,9 @@ endif $(DOCKER_RUN) cp /opt/npm-packages . $(PYTHON_BIN)/python -m pip install --editable=c2cgeoportal_commons --editable=c2cgeoportal_geoportal --editable=c2cgeoportal_admin $(PYTHON_BIN)/python -m pip install --editable=geoportal - $(PYTHON_BIN)/python -m compileall -q .build/venv 2&>/dev/null|| true - $(PYTHON_BIN)/python -m compileall -q c2cgeoportal_* 2&>/dev/null || true - $(PYTHON_BIN)/python -m compileall -q geoportal/$(PACKAGE)_geoportal -x geoportal/$(PACKAGE)_geoportal/static.* 2&>/dev/null || true + $(PYTHON_BIN)/python -m compileall -q .build/venv >/dev/null|| true + $(PYTHON_BIN)/python -m compileall -q c2cgeoportal_* >/dev/null || true + $(PYTHON_BIN)/python -m compileall -q geoportal/$(PACKAGE)_geoportal -x geoportal/$(PACKAGE)_geoportal/static.* >/dev/null || true rm --force --recursive admin/node_modules mkdir --parent admin cat npm-packages | xargs npm install --prefix ./admin From 4a35076c6b57c961231a5ad96ab9db601b44ab8e Mon Sep 17 00:00:00 2001 From: Wolfgang Kaltz Date: Mon, 25 Jun 2018 13:50:28 +0200 Subject: [PATCH 71/92] adding space for better readability --- .../scaffolds/nondockercreate/nondocker-finalise.mk_tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-finalise.mk_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-finalise.mk_tmpl index d83b9e36c8..0a24dd1b45 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-finalise.mk_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-finalise.mk_tmpl @@ -175,7 +175,7 @@ endif $(DOCKER_RUN) cp /opt/npm-packages . $(PYTHON_BIN)/python -m pip install --editable=c2cgeoportal_commons --editable=c2cgeoportal_geoportal --editable=c2cgeoportal_admin $(PYTHON_BIN)/python -m pip install --editable=geoportal - $(PYTHON_BIN)/python -m compileall -q .build/venv >/dev/null|| true + $(PYTHON_BIN)/python -m compileall -q .build/venv >/dev/null || true $(PYTHON_BIN)/python -m compileall -q c2cgeoportal_* >/dev/null || true $(PYTHON_BIN)/python -m compileall -q geoportal/$(PACKAGE)_geoportal -x geoportal/$(PACKAGE)_geoportal/static.* >/dev/null || true rm --force --recursive admin/node_modules From f446263b55c6d0ad75b4957a5a245aaab1636d4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Mon, 25 Jun 2018 11:24:51 +0200 Subject: [PATCH 72/92] Split the too big .build/venv.timestamp rule --- .../nondocker-finalise.mk_tmpl | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-finalise.mk_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-finalise.mk_tmpl index 23dfa627c4..e5d062953c 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-finalise.mk_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker-finalise.mk_tmpl @@ -68,7 +68,8 @@ clean-all: .PHONY: build build: $(PRINT_OUTPUT_WAR) \ - .build/apache.timestamp + .build/apache.timestamp \ + .build/npm.timestamp \ # Apache @@ -154,7 +155,21 @@ PYTHON_BIN = .build/venv/bin DOCKER_RUN = ./docker-run endif -.build/venv.timestamp: .config +extract-docker: c2cgeoportal_commons c2cgeoportal_geoportal c2cgeoportal_admin npm-packages + +c2cgeoportal_commons: .config + rm --force --recursive c2cgeoportal_commons + $(DOCKER_RUN) cp -r /opt/c2cgeoportal_commons c2cgeoportal_commons + +c2cgeoportal_geoportal: .config + rm --force --recursive c2cgeoportal_geoportal + $(DOCKER_RUN) cp -r /opt/c2cgeoportal_geoportal c2cgeoportal_geoportal + +c2cgeoportal_admin: .config + rm --force --recursive c2cgeoportal_admin + $(DOCKER_RUN) cp -r /opt/c2cgeoportal_admin c2cgeoportal_admin + +.build/venv.timestamp: .config c2cgeoportal_commons c2cgeoportal_geoportal c2cgeoportal_admin $(PRERULE_CMD) mkdir --parent .build rm -rf .build/venv @@ -170,16 +185,16 @@ else $(PYTHON_BIN)/python -m pip install `./get-pip-dependencies c2cgeoportal-commons c2cgeoportal-geoportal c2cgeoportal-admin GDAL flake8-mypy mypy` endif $(PYTHON_BIN)/python -m pip install -r requirements.txt - rm --force --recursive c2cgeoportal_commons c2cgeoportal_geoportal c2cgeoportal_admin - $(DOCKER_RUN) cp -r /opt/c2cgeoportal_commons c2cgeoportal_commons - $(DOCKER_RUN) cp -r /opt/c2cgeoportal_geoportal c2cgeoportal_geoportal - $(DOCKER_RUN) cp -r /opt/c2cgeoportal_admin c2cgeoportal_admin - $(DOCKER_RUN) cp /opt/npm-packages . $(PYTHON_BIN)/python -m pip install --editable=c2cgeoportal_commons --editable=c2cgeoportal_geoportal --editable=c2cgeoportal_admin $(PYTHON_BIN)/python -m pip install --editable=geoportal $(PYTHON_BIN)/python -m compileall -q .build/venv >/dev/null || true $(PYTHON_BIN)/python -m compileall -q c2cgeoportal_* >/dev/null || true $(PYTHON_BIN)/python -m compileall -q geoportal/$(PACKAGE)_geoportal -x geoportal/$(PACKAGE)_geoportal/static.* >/dev/null || true + +npm-packages: .config + $(DOCKER_RUN) cp /opt/npm-packages . + +.build/npm.timestamp: .config npm-packages rm --force --recursive admin/node_modules mkdir --parent admin cat npm-packages | xargs npm install --prefix ./admin From 2561d5fe0efc6ccd8d40a7d9d1f853af4f941f7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Mon, 25 Jun 2018 16:02:48 +0200 Subject: [PATCH 73/92] Generated file commited by error in the upgrade to 2.3.1 --- .../scaffolds/nondockerupdate/+dot+upgrade.yaml_tmpl | 3 +++ .../scaffolds/update/+dot+upgrade.yaml_tmpl | 3 +++ 2 files changed, 6 insertions(+) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockerupdate/+dot+upgrade.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockerupdate/+dot+upgrade.yaml_tmpl index edfe16e88d..53a7a82f44 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockerupdate/+dot+upgrade.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockerupdate/+dot+upgrade.yaml_tmpl @@ -106,6 +106,9 @@ files_to_remove: - file: apache/README.txt.mako version: 2.3 from: 2.2 + - file: alembic.yaml + version: 2.3.2 + from: 2.3.0 files_to_move: - from: vars_{{package}}.yaml diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl index 9b2dafa1c1..b7880b51a9 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl @@ -146,6 +146,9 @@ files_to_remove: - file: mapserver/Dockerfile version: 2.3 from: 2.2 + - file: alembic.yaml + version: 2.3.2 + from: 2.3.0 files_to_move: - from: vars_{{package}}.yaml From 3ff5d1c4e626465b3f0a9ce421b1f9da30de8953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Tue, 26 Jun 2018 07:58:10 +0200 Subject: [PATCH 74/92] Fix max request argument to gunicorn --- .../scaffolds/update/CONST_vars.yaml_tmpl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl index ecff4b45f9..2c2fd78279 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl @@ -65,7 +65,8 @@ vars: --threads=10 --workers=5 --timeout=60 - --worker-connections=1000 + --max-requests=1000 + --max-requests-jitter=100 VISIBLE_WEB_HOST: '{docker_host}' VISIBLE_WEB_PROTOCOL: '{docker_web_protocol}' TINYOWS_URL: http://tinyows/ @@ -83,7 +84,8 @@ vars: --worker-class=gthread --threads=10 --workers=5 - --worker-connections=1000 + --max-requests=1000 + --max-requests-jitter=100 VISIBLE_ENTRY_POINT: '{docker_entry_point}' TILEGENERATION_CONFIGFILE: /etc/tilegeneration/config.yaml C2C_BASE_PATH: /c2c_tiles From f7a01b781b5411d4dd7d6df01a2f6202de39a10d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Tue, 26 Jun 2018 09:59:28 +0200 Subject: [PATCH 75/92] Don't use devserver on build, add logs --- .../scaffolds/create/geoportal/webpack.apps.js.mako_tmpl | 3 +++ .../nondockercreate/geoportal/webpack.apps.js.mako_tmpl | 3 +++ .../scaffolds/update/CONST_Makefile_tmpl | 2 ++ 3 files changed, 8 insertions(+) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/webpack.apps.js.mako_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/webpack.apps.js.mako_tmpl index 3a7e601b99..5c6b75b492 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/webpack.apps.js.mako_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/geoportal/webpack.apps.js.mako_tmpl @@ -58,6 +58,9 @@ const rules = [ const noDevServer = process.env['NO_DEV_SERVER'] == 'TRUE'; devServer = dev && !noDevServer; +console.log("Use dev mode: " + dev) +console.log("Use dev server mode: " + devServer) + module.exports = { output: { path: path.resolve(__dirname, '{{package}}_geoportal/static-ngeo/build/'), diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/geoportal/webpack.apps.js.mako_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/geoportal/webpack.apps.js.mako_tmpl index 95d1887e30..0dfedc8bbd 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/geoportal/webpack.apps.js.mako_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/geoportal/webpack.apps.js.mako_tmpl @@ -55,6 +55,9 @@ const rules = [ const noDevServer = process.env['NO_DEV_SERVER'] == 'TRUE'; devServer = dev && !noDevServer; +console.log("Use dev mode: " + dev) +console.log("Use dev server mode: " + devServer) + module.exports = { output: { path: path.resolve(__dirname, '{{package}}_geoportal/static-ngeo/build/'), diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl index 30a0b10443..d9b10a1fe2 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl @@ -115,6 +115,8 @@ GIT_HASH ?= $(shell git rev-parse HEAD) # ngeo NODE_ENV ?= production export NODE_ENV +NO_DEV_SERVER ?= TRUE +export NO_DEV_SERVER ANGULAR_VERSION = 1.6 APP_OUTPUT_DIR = geoportal/$(PACKAGE)_geoportal/static-ngeo/build GCC_JS_FILES = $(shell find /usr/lib/node_modules/openlayers/src/ol /usr/lib/node_modules/ngeo/src /usr/lib/node_modules/ol-cesium/src -type f -name '*.js' 2> /dev/null) From 6d780ec2000e73312a2f1279e28500e27ddbd9cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Tue, 26 Jun 2018 10:02:28 +0200 Subject: [PATCH 76/92] Tilecloud-chain as nonroot --- .../scaffolds/create/docker-compose.yaml.mako | 4 +++- .../scaffolds/create/front/haproxy.cfg.tmpl | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako index 0bc5c0bbaa..ac479e6526 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako @@ -70,12 +70,14 @@ ${service_defaults('redis', 6379)}\ tilecloudchain: image: camptocamp/tilecloud-chain:1.6 + user: www-data volumes_from: - config:ro -${service_defaults('tilecloudchain', 80)}\ +${service_defaults('tilecloudchain', 8080)}\ tilegeneration_slave: image: camptocamp/tilecloud-chain:1.6 + user: www-data volumes_from: - config:ro ${service_defaults('tilecloudchain')}\ diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/front/haproxy.cfg.tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/create/front/haproxy.cfg.tmpl index f96a4a25c6..148998925f 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/front/haproxy.cfg.tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/front/haproxy.cfg.tmpl @@ -59,7 +59,7 @@ backend geoportal backend tilecloudchain option httpchk GET ${VISIBLE_ENTRY_POINT}c2c_tiles/health_check HTTP/1.0\r\nUser-Agent:\ healthcheck http-check expect status 200 - server linked tilecloudchain:80 resolvers dns #check + server linked tilecloudchain:8080 resolvers dns #check frontend plain From 0d17b0294a4415ebb6e31074f1ed2ae2b3204a89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Tue, 26 Jun 2018 11:11:40 +0200 Subject: [PATCH 77/92] Tinyows as non root --- .../scaffolds/create/docker-compose.yaml.mako | 3 ++- .../scaffolds/update/CONST_vars.yaml_tmpl | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako index 0bc5c0bbaa..4f05149720 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako @@ -35,9 +35,10 @@ ${service_defaults('mapserver', 80)}\ tinyows: image: camptocamp/tinyows + user: www-data volumes_from: - config:ro -${service_defaults('tinyows', 80)}\ +${service_defaults('tinyows', 8080)}\ mapcache: image: camptocamp/mapcache:1.6 diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl index ecff4b45f9..a6cd4c558d 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl @@ -68,7 +68,7 @@ vars: --worker-connections=1000 VISIBLE_WEB_HOST: '{docker_host}' VISIBLE_WEB_PROTOCOL: '{docker_web_protocol}' - TINYOWS_URL: http://tinyows/ + TINYOWS_URL: http://tinyows:8080/ MAPSERVER_URL: http://mapserver/ PRINT_URL: http://print:8080/print/ DEVSERVER_URL: http://webpack-dev-server:8080${entry_point} From e1c60b4ed58fbdd6c0064cbbf2f252913a1a5fb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Tue, 26 Jun 2018 11:42:41 +0200 Subject: [PATCH 78/92] Add missing docker ignore file --- docker/qgisserver/.dockerignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 docker/qgisserver/.dockerignore diff --git a/docker/qgisserver/.dockerignore b/docker/qgisserver/.dockerignore new file mode 100644 index 0000000000..ea084780bc --- /dev/null +++ b/docker/qgisserver/.dockerignore @@ -0,0 +1,3 @@ +* +!accesscontrol.py +!metadata.txt From 918b2b79205758bb2a8108d7b6a4dd9d007b6e25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Tue, 26 Jun 2018 10:19:14 +0200 Subject: [PATCH 79/92] Mapcache as nonroot --- .../scaffolds/create/docker-compose.yaml.mako | 3 ++- .../scaffolds/update/CONST_vars.yaml_tmpl | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako index 0bc5c0bbaa..99039bfb0b 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako @@ -41,9 +41,10 @@ ${service_defaults('tinyows', 80)}\ mapcache: image: camptocamp/mapcache:1.6 + user: www-data volumes_from: - config:ro -${service_defaults('mapcache', 80)}\ +${service_defaults('mapcache', 8080)}\ memcached: image: memcached:1.5 diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl index ecff4b45f9..f9f7ffc182 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl @@ -580,7 +580,7 @@ runtime_environment: - name: PRINT_URL default: http://print:8080/print/ - name: MAPCACHE_URL - default: http://mapcache/ + default: http://mapcache:8080/ - name: DEVSERVER_URL default: http://webpack-dev-server:8080/ - name: REDIS_HOST From 01ad2d513d50e7608ba0a053b279ab7f4f90ce27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Wed, 27 Jun 2018 08:00:32 +0200 Subject: [PATCH 80/92] Don't use the tinyows entrypoint --- .../scaffolds/create/docker-compose.yaml.mako | 1 + 1 file changed, 1 insertion(+) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako index 88757d09ec..d3a13a3a08 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako @@ -38,6 +38,7 @@ ${service_defaults('mapserver', 80)}\ user: www-data volumes_from: - config:ro + entrypoint: [] ${service_defaults('tinyows', 8080)}\ mapcache: From c80286763381fabcb5e6e1f72ebdeb5556f3733a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Tue, 26 Jun 2018 11:14:33 +0200 Subject: [PATCH 81/92] Mapserver as nonroot --- .../scaffolds/create/docker-compose.yaml.mako | 3 ++- .../scaffolds/update/CONST_vars.yaml_tmpl | 4 ++-- travis/docker-compose.yaml.mako | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako index d3a13a3a08..19350a1d9b 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako @@ -20,12 +20,13 @@ ${service_defaults('print', 8080)}\ mapserver: image: camptocamp/mapserver:7.0 + user: www-data volumes_from: - config:rw volumes: - /var/sig:/var/sig:ro entrypoint: [] -${service_defaults('mapserver', 80)}\ +${service_defaults('mapserver', 8080)}\ ## qgisserver: ## image: camptocamp/qgis-server:latest diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl index cbe3aadb9f..c9d99a3655 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_vars.yaml_tmpl @@ -36,7 +36,7 @@ vars: PGDATABASE: geomapfish GEOPORTAL_INTERNAL_URL: http://geoportal/ MAPCACHE_URL: http://mapcache/mapcache/ - MAPSERVER_URL: http://mapserver/ + MAPSERVER_URL: http://mapserver:8080/ MEMCACHED_HOST: memcached MEMCACHED_PORT: 11211 TILEGENERATION_SQS_QUEUE: '' @@ -70,7 +70,7 @@ vars: VISIBLE_WEB_HOST: '{docker_host}' VISIBLE_WEB_PROTOCOL: '{docker_web_protocol}' TINYOWS_URL: http://tinyows:8080/ - MAPSERVER_URL: http://mapserver/ + MAPSERVER_URL: http://mapserver:8080/ PRINT_URL: http://print:8080/print/ DEVSERVER_URL: http://webpack-dev-server:8080${entry_point} REDIS_HOST: redis diff --git a/travis/docker-compose.yaml.mako b/travis/docker-compose.yaml.mako index 56367e61cf..432abe9ab3 100644 --- a/travis/docker-compose.yaml.mako +++ b/travis/docker-compose.yaml.mako @@ -27,6 +27,7 @@ ${service_defaults('print', 8080)}\ mapserver: image: camptocamp/mapserver:7.0 + user: www-data volumes_from: - config:rw entrypoint: [] From abaea223e3c142582ea4c9b5337610d86055eec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Wed, 27 Jun 2018 13:05:00 +0200 Subject: [PATCH 82/92] Use the c2c.template 2.1.0 final version --- docker/build/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/build/requirements.txt b/docker/build/requirements.txt index ee168152e8..b8943a2c5a 100644 --- a/docker/build/requirements.txt +++ b/docker/build/requirements.txt @@ -4,7 +4,7 @@ Babel==2.5.3 # i18n boto3==1.7.4 # Tile generation bottle==0.12.13 # geoportal c2c.cssmin==0.7.dev6 # CGXP build -c2c.template==2.1.0.dev1 # geoportal +c2c.template==2.1.0 # geoportal c2cgeoform==2.0.dev20180605 # commons codacy-coverage==1.3.11 # Codacy send report codespell==1.12.0 # Lint From fc5d32a0b86a0323d6d3be24eb0a6da17df0a230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Tue, 26 Jun 2018 11:57:41 +0200 Subject: [PATCH 83/92] Integrate GeoMapFish QGIS server image in the build --- .gitignore | 1 + Makefile | 13 +++++++++++++ docker/qgisserver/.dockerignore | 2 ++ docker/qgisserver/Dockerfile | 7 +++++-- travis/publish-docker | 2 +- 5 files changed, 22 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 17de3f79cb..fd0a5b6fa6 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ /admin/npm-packages /admin/c2cgeoportal_admin/locale/ /admin/c2cgeoportal_admin.egg-info/ +/docker/qgisserver/commons/ /docker/admin-build/Dockerfile /docker/admin-build/npm-packages /docker/test-db/12-alembic.sql diff --git a/Makefile b/Makefile index 709dfa1f3e..886d6f2879 100644 --- a/Makefile +++ b/Makefile @@ -107,6 +107,7 @@ docker-build: .PHONY: build build: \ docker-build-build \ + docker-build-qgisserver \ docker-build-testdb \ prepare-tests @@ -185,6 +186,18 @@ docker-build-build: $(shell docker-required --path . --replace-pattern='^test(.* $(APPS_FILES_ALT) docker build --build-arg=VERSION=$(VERSION) --tag=$(DOCKER_BASE)-build:$(MAJOR_VERSION) . +docker/qgisserver/commons: commons + rm --recursive --force $@ + cp --recursive $< $@ + rm --recursive --force $@/c2cgeoportal_commons/alembic + rm $@/tests.yaml.mako + touch $@ + +.PHONY: docker-build-qgisserver +docker-build-qgisserver: $(shell docker-required --path docker/qgisserver) docker/qgisserver/commons + docker build --tag=$(DOCKER_BASE)-qgisserver:$(MAJOR_VERSION) docker/qgisserver + + .PHONY: prepare-tests prepare-tests: \ geoportal/tests/functional/test.ini \ diff --git a/docker/qgisserver/.dockerignore b/docker/qgisserver/.dockerignore index ea084780bc..7ae876f9e6 100644 --- a/docker/qgisserver/.dockerignore +++ b/docker/qgisserver/.dockerignore @@ -1,3 +1,5 @@ * +!__init__.py !accesscontrol.py !metadata.txt +!commons diff --git a/docker/qgisserver/Dockerfile b/docker/qgisserver/Dockerfile index 6d93bec455..9c6fa5f21d 100644 --- a/docker/qgisserver/Dockerfile +++ b/docker/qgisserver/Dockerfile @@ -1,4 +1,7 @@ -FROM camptocamp/qgis-server:3 +FROM camptocamp/qgis-server:latest LABEL maintainer Camptocamp "info@camptocamp.com" -COPY * /var/www/plugins/geomapfish_qgisserver/ +COPY __init__.py accesscontrol.py metadata.txt /var/www/plugins/geomapfish_qgisserver/ +COPY commons /opt/c2cgeoportal_commons + +RUN python3 -m pip install --editable /opt/c2cgeoportal_commons diff --git a/travis/publish-docker b/travis/publish-docker index 5e1abb361c..c8105f05e4 100755 --- a/travis/publish-docker +++ b/travis/publish-docker @@ -2,7 +2,7 @@ docker login --username ${DOCKER_USERNAME} --password ${DOCKER_PASSWORD} -for IMAGE in geomapfish-build-dev geomapfish-commons geomapfish-build +for IMAGE in geomapfish-build-dev geomapfish-commons geomapfish-build geomapfish-qgisserver do if [ "${TRAVIS_TAG}" != "" ] then From c9facfbdfea0b40287a97267203afd2bc0ce56ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Tue, 26 Jun 2018 14:34:06 +0200 Subject: [PATCH 84/92] Add some logs --- travis/short-make | 3 +++ 1 file changed, 3 insertions(+) diff --git a/travis/short-make b/travis/short-make index 3b72e21b2d..18f5a5ef2b 100755 --- a/travis/short-make +++ b/travis/short-make @@ -15,4 +15,7 @@ if p.poll() is None: os.environ["DEBUG"] = "TRUE" subprocess.call(["make"] + sys.argv[1:]) exit(2) +if p.returncode != 0: + print("make call error:") + subprocess.call(["make"] + sys.argv[1:]) exit(p.returncode) From f534950f5d59db37f6f00744839506ed68693d58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Wed, 27 Jun 2018 08:09:41 +0200 Subject: [PATCH 85/92] Creates a folder for the plugin --- docker/qgisserver/.dockerignore | 4 +--- docker/qgisserver/Dockerfile | 2 +- docker/qgisserver/{ => geomapfish_plugin}/__init__.py | 0 docker/qgisserver/{ => geomapfish_plugin}/accesscontrol.py | 0 docker/qgisserver/{ => geomapfish_plugin}/metadata.txt | 0 5 files changed, 2 insertions(+), 4 deletions(-) rename docker/qgisserver/{ => geomapfish_plugin}/__init__.py (100%) rename docker/qgisserver/{ => geomapfish_plugin}/accesscontrol.py (100%) rename docker/qgisserver/{ => geomapfish_plugin}/metadata.txt (100%) diff --git a/docker/qgisserver/.dockerignore b/docker/qgisserver/.dockerignore index 7ae876f9e6..4617214b8d 100644 --- a/docker/qgisserver/.dockerignore +++ b/docker/qgisserver/.dockerignore @@ -1,5 +1,3 @@ * -!__init__.py -!accesscontrol.py -!metadata.txt +!geomapfish_plugin !commons diff --git a/docker/qgisserver/Dockerfile b/docker/qgisserver/Dockerfile index 9c6fa5f21d..3976296535 100644 --- a/docker/qgisserver/Dockerfile +++ b/docker/qgisserver/Dockerfile @@ -1,7 +1,7 @@ FROM camptocamp/qgis-server:latest LABEL maintainer Camptocamp "info@camptocamp.com" -COPY __init__.py accesscontrol.py metadata.txt /var/www/plugins/geomapfish_qgisserver/ +COPY geomapfish_plugin/* /var/www/plugins/geomapfish_qgisserver/ COPY commons /opt/c2cgeoportal_commons RUN python3 -m pip install --editable /opt/c2cgeoportal_commons diff --git a/docker/qgisserver/__init__.py b/docker/qgisserver/geomapfish_plugin/__init__.py similarity index 100% rename from docker/qgisserver/__init__.py rename to docker/qgisserver/geomapfish_plugin/__init__.py diff --git a/docker/qgisserver/accesscontrol.py b/docker/qgisserver/geomapfish_plugin/accesscontrol.py similarity index 100% rename from docker/qgisserver/accesscontrol.py rename to docker/qgisserver/geomapfish_plugin/accesscontrol.py diff --git a/docker/qgisserver/metadata.txt b/docker/qgisserver/geomapfish_plugin/metadata.txt similarity index 100% rename from docker/qgisserver/metadata.txt rename to docker/qgisserver/geomapfish_plugin/metadata.txt From 09652769088b9a22b2882cfa3db1dc8c2d054821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Wed, 27 Jun 2018 08:10:43 +0200 Subject: [PATCH 86/92] Removes the exception handler --- .../geomapfish_plugin/accesscontrol.py | 59 +++++++++---------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/docker/qgisserver/geomapfish_plugin/accesscontrol.py b/docker/qgisserver/geomapfish_plugin/accesscontrol.py index 93d78d7e78..4641c24edc 100644 --- a/docker/qgisserver/geomapfish_plugin/accesscontrol.py +++ b/docker/qgisserver/geomapfish_plugin/accesscontrol.py @@ -39,38 +39,33 @@ def __init__(self, server_iface): self.server_iface = server_iface self.area_cache = {} - try: - if "GEOMAPFISH_SCHEMA" not in os.environ: - raise GMFException("The environment variable 'GEOMAPFISH_SCHEMA' is not defined.") - if "GEOMAPFISH_SRID" not in os.environ: - raise GMFException("The environment variable 'GEOMAPFISH_SRID' is not defined.") - if "GEOMAPFISH_OGCSERVER" not in os.environ: - raise GMFException("The environment variable 'GEOMAPFISH_OGCSERVER' is not defined.") - if "GEOMAPFISH_SQLALCHEMYURL" not in os.environ: - raise GMFException("The environment variable 'GEOMAPFISH_SQLALCHEMYURL' is not defined.") - - c2cgeoportal.schema = os.environ["GEOMAPFISH_SCHEMA"] - c2cgeoportal.srid = os.environ["GEOMAPFISH_SRID"] - - sqlahelper.add_engine(sqlalchemy.create_engine(os.environ["GEOMAPFISH_SQLALCHEMYURL"])) - - from c2cgeoportal.models import DBSession, LayerWMS, OGCServer - - self.ogcserver = DBSession.query(OGCServer).filter(OGCServer.name == unicode(os.environ["GEOMAPFISH_OGCSERVER"])).one() - - self.layers = {} - # TODO manage groups ... - for layer in DBSession.query(LayerWMS).filter(LayerWMS.ogc_server_id == self.ogcserver.id).all(): - for name in layer.layer.split(','): - if name not in self.layers: - self.layers[name] = [] - self.layers[name].append(layer) - - server_iface.registerAccessControl(self, 100) - except Exception as e: - QgsMessageLog.logMessage(traceback.format_tb(e)) - QgsMessageLog.logMessage(str(e)) - raise + if "GEOMAPFISH_SCHEMA" not in os.environ: + raise GMFException("The environment variable 'GEOMAPFISH_SCHEMA' is not defined.") + if "GEOMAPFISH_SRID" not in os.environ: + raise GMFException("The environment variable 'GEOMAPFISH_SRID' is not defined.") + if "GEOMAPFISH_OGCSERVER" not in os.environ: + raise GMFException("The environment variable 'GEOMAPFISH_OGCSERVER' is not defined.") + if "GEOMAPFISH_SQLALCHEMYURL" not in os.environ: + raise GMFException("The environment variable 'GEOMAPFISH_SQLALCHEMYURL' is not defined.") + + c2cgeoportal.schema = os.environ["GEOMAPFISH_SCHEMA"] + c2cgeoportal.srid = os.environ["GEOMAPFISH_SRID"] + + sqlahelper.add_engine(sqlalchemy.create_engine(os.environ["GEOMAPFISH_SQLALCHEMYURL"])) + + from c2cgeoportal.models import DBSession, LayerWMS, OGCServer + + self.ogcserver = DBSession.query(OGCServer).filter(OGCServer.name == unicode(os.environ["GEOMAPFISH_OGCSERVER"])).one() + + self.layers = {} + # TODO manage groups ... + for layer in DBSession.query(LayerWMS).filter(LayerWMS.ogc_server_id == self.ogcserver.id).all(): + for name in layer.layer.split(','): + if name not in self.layers: + self.layers[name] = [] + self.layers[name].append(layer) + + server_iface.registerAccessControl(self, 100) @staticmethod def get_role(): From 7c0f5b9a9f842ecf98c077a009e63149dd8de36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Wed, 27 Jun 2018 08:16:10 +0200 Subject: [PATCH 87/92] Some backport from ADL --- .../geomapfish_plugin/accesscontrol.py | 54 ++++++++++++------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/docker/qgisserver/geomapfish_plugin/accesscontrol.py b/docker/qgisserver/geomapfish_plugin/accesscontrol.py index 4641c24edc..9a6ee3ead6 100644 --- a/docker/qgisserver/geomapfish_plugin/accesscontrol.py +++ b/docker/qgisserver/geomapfish_plugin/accesscontrol.py @@ -11,16 +11,21 @@ from qgis.core import QgsMessageLog +import json import os import traceback from shapely import ops import geoalchemy2 import sqlalchemy -import sqlahelper from qgis.server import QgsAccessControlFilter from qgis.core import QgsDataSourceUri -import c2cgeoportal + +from sqlalchemy.orm import configure_mappers +from sqlalchemy.orm import scoped_session +from sqlalchemy.orm import sessionmaker + +from c2cgeoportal_commons.config import config class GMFException(Exception): @@ -48,34 +53,43 @@ def __init__(self, server_iface): if "GEOMAPFISH_SQLALCHEMYURL" not in os.environ: raise GMFException("The environment variable 'GEOMAPFISH_SQLALCHEMYURL' is not defined.") - c2cgeoportal.schema = os.environ["GEOMAPFISH_SCHEMA"] - c2cgeoportal.srid = os.environ["GEOMAPFISH_SRID"] + self.srid = os.environ["GEOMAPFISH_SRID"] + + # TODO: open the geomapfish config file + config._config = {} + config._config['schema'] = os.environ["GEOMAPFISH_SCHEMA"] + config._config['srid'] = os.environ["GEOMAPFISH_SRID"] + self.config = config - sqlahelper.add_engine(sqlalchemy.create_engine(os.environ["GEOMAPFISH_SQLALCHEMYURL"])) + from c2cgeoportal_commons.models.main import LayerWMS, OGCServer + configure_mappers() - from c2cgeoportal.models import DBSession, LayerWMS, OGCServer + engine = sqlalchemy.create_engine(config.get('sqlalchemy_slave.url')) + session_factory = sessionmaker() + session_factory.configure(bind=engine) + self.DBSession = scoped_session(session_factory) - self.ogcserver = DBSession.query(OGCServer).filter(OGCServer.name == unicode(os.environ["GEOMAPFISH_OGCSERVER"])).one() + self.ogcserver = self.DBSession.query(OGCServer) \ + .filter(OGCServer.name == os.environ["GEOMAPFISH_OGCSERVER"]) \ + .one() self.layers = {} # TODO manage groups ... - for layer in DBSession.query(LayerWMS).filter(LayerWMS.ogc_server_id == self.ogcserver.id).all(): + for layer in self.DBSession.query(LayerWMS).filter(LayerWMS.ogc_server_id == self.ogcserver.id).all(): for name in layer.layer.split(','): if name not in self.layers: self.layers[name] = [] self.layers[name].append(layer) + QgsMessageLog.logMessage('[accesscontrol] layers: {}'.format( + json.dumps(self.layers, sort_keys=True, indent=4) + )) server_iface.registerAccessControl(self, 100) - @staticmethod - def get_role(): - from c2cgeoportal.models import DBSession, Role - - # headers = self.serverInterface().requestHandler() - - # return DBSession.query(User).get(headers.parameterMap()['USER_ID']).role - # return DBSession.query(Role).get(headers.parameterMap()['ROLE']) - return DBSession.query(Role).first() + def get_role(self): + from c2cgeoportal_commons.models.main import Role + parameters = self.serverInterface().requestHandler().parameterMap() + return self.DBSession.query(Role).get(parameters['ROLE_ID']) def get_restriction_areas(self, gmf_layers, rw=False, role=False): """ @@ -129,9 +143,9 @@ def layerFilterSubsetString(self, layer): # NOQA if area is None: return None area = "ST_GeomFromText('{}', {})".format( - area, c2cgeoportal.srid + area, self.srid ) - if int(c2cgeoportal.srid) != layer.crs().postgisSrid(): + if int(self.srid) != layer.crs().postgisSrid(): area = "ST_transform({}, {})".format( area, layer.crs().postgisSrid() ) @@ -164,7 +178,7 @@ def layerFilterExpression(self, layer): # NOQA # TODO cache the union # TODO verify the geometry return "intersects($geometry, transform(geom_from_wkt('{}'), 'EPSG:{}', 'EPSG:{}')".format( - area, c2cgeoportal.srid, layer.crs().projectionAcronym() + area, self.srid, layer.crs().projectionAcronym() ) except Exception: QgsMessageLog.logMessage(traceback.format_exc()) From bdc41fd186297692279a8e69f71eb869c4889bc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Wed, 27 Jun 2018 08:50:09 +0200 Subject: [PATCH 88/92] Integrate the QGIS plugin with the config and in the Docker composition --- .../qgisserver/geomapfish_plugin/accesscontrol.py | 15 +++------------ .../scaffolds/create/+dot+gitignore_tmpl | 1 + .../scaffolds/create/docker-compose.yaml.mako | 2 +- .../scaffolds/create/vars.yaml_tmpl | 5 +++++ .../scaffolds/nondockercreate/+dot+gitignore_tmpl | 1 + .../scaffolds/update/CONST_Makefile_tmpl | 14 ++++++++++++-- 6 files changed, 23 insertions(+), 15 deletions(-) diff --git a/docker/qgisserver/geomapfish_plugin/accesscontrol.py b/docker/qgisserver/geomapfish_plugin/accesscontrol.py index 9a6ee3ead6..96fa83f073 100644 --- a/docker/qgisserver/geomapfish_plugin/accesscontrol.py +++ b/docker/qgisserver/geomapfish_plugin/accesscontrol.py @@ -44,21 +44,12 @@ def __init__(self, server_iface): self.server_iface = server_iface self.area_cache = {} - if "GEOMAPFISH_SCHEMA" not in os.environ: - raise GMFException("The environment variable 'GEOMAPFISH_SCHEMA' is not defined.") - if "GEOMAPFISH_SRID" not in os.environ: - raise GMFException("The environment variable 'GEOMAPFISH_SRID' is not defined.") if "GEOMAPFISH_OGCSERVER" not in os.environ: raise GMFException("The environment variable 'GEOMAPFISH_OGCSERVER' is not defined.") - if "GEOMAPFISH_SQLALCHEMYURL" not in os.environ: - raise GMFException("The environment variable 'GEOMAPFISH_SQLALCHEMYURL' is not defined.") self.srid = os.environ["GEOMAPFISH_SRID"] - # TODO: open the geomapfish config file - config._config = {} - config._config['schema'] = os.environ["GEOMAPFISH_SCHEMA"] - config._config['srid'] = os.environ["GEOMAPFISH_SRID"] + config.init(os.environ.get('GEOMAPFISH_CONFIG', '/etc/qgisserver/geomapfish.yaml')) self.config = config from c2cgeoportal_commons.models.main import LayerWMS, OGCServer @@ -84,7 +75,7 @@ def __init__(self, server_iface): json.dumps(self.layers, sort_keys=True, indent=4) )) - server_iface.registerAccessControl(self, 100) + server_iface.registerAccessControl(self, int(os.environ.get("GEOMAPFISH_POSITION", 100))) def get_role(self): from c2cgeoportal_commons.models.main import Role @@ -145,7 +136,7 @@ def layerFilterSubsetString(self, layer): # NOQA area = "ST_GeomFromText('{}', {})".format( area, self.srid ) - if int(self.srid) != layer.crs().postgisSrid(): + if self.srid != layer.crs().postgisSrid(): area = "ST_transform({}, {})".format( area, layer.crs().postgisSrid() ) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+gitignore_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+gitignore_tmpl index 1ec84d1038..7d20b68285 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+gitignore_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/+dot+gitignore_tmpl @@ -18,6 +18,7 @@ __pycache__/ /mapserver/*.map.tmpl /mapserver/tinyows.xml.mako /mapserver/tinyows.xml.tmpl +/qgisserver/geomapfish.yaml /geoportal/jsbuild/app.cfg /geoportal/Dockerfile /geoportal/development.ini diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako index 19350a1d9b..dee6a2900f 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako @@ -29,7 +29,7 @@ ${service_defaults('print', 8080)}\ ${service_defaults('mapserver', 8080)}\ ## qgisserver: -## image: camptocamp/qgis-server:latest +## image: camptocamp/geomapfish-qgisserver:2.3 ## volumes_from: ## - config:ro ##${service_defaults('qgisserver', 80)} diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/vars.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/create/vars.yaml_tmpl index 15552f553a..d2655a1cef 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/vars.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/vars.yaml_tmpl @@ -237,6 +237,11 @@ vars: Sincerely yours The GeoMapfish team + docker_services: + qgisserver: + environment: + GEOMAPFISH_OGCSERVER: + # Checker configuration checker: fulltextsearch: diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/+dot+gitignore_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/+dot+gitignore_tmpl index e3117f4e64..cf1fc7b707 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/+dot+gitignore_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/+dot+gitignore_tmpl @@ -32,6 +32,7 @@ __pycache__/ /mapserver/*.map.tmpl.mako /mapserver/tinyows.xml /mapserver/tinyows.xml.tmpl.mako +/qgisserver/geomapfish.yaml /tilegeneration/config.yaml /tilegeneration/config.yaml.tmpl.mako /geoportal/jsbuild/app.cfg diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl index d9b10a1fe2..f59d2c15c1 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/CONST_Makefile_tmpl @@ -395,7 +395,8 @@ git-attributes: git --no-pager diff --check `git log --oneline | tail -1 | cut --fields=1 --delimiter=' '` YAML_FILES ?= $(filter-out ./tilegeneration/config.yaml ./geoportal/config.yaml \ - ./geoportal/alembic.yaml ./docker-compose-dev.yaml ./docker-compose.yaml, \ + ./qgisserver/geomapfish.yaml ./geoportal/alembic.yaml \ + ./docker-compose-dev.yaml ./docker-compose.yaml, \ $(shell find \ -name .build -prune -or \ -name cgxp -prune -or \ @@ -422,6 +423,7 @@ clean: template-clean $(APP_OUTPUT_DIR)/ \ geoportal/$(PACKAGE)_geoportal/locale/$(PACKAGE)-*.pot \ geoportal/alembic.yaml \ + qgisserver/geomapfish.yaml \ .UPGRADE* \ mapcache \ $(addprefix geoportal/$(PACKAGE)_geoportal/locale/, $(addsuffix /LC_MESSAGES/$(PACKAGE)_geoportal-$(L10N_CLIENT_POSTFIX).mo, $(LANGUAGES))) \ @@ -498,6 +500,13 @@ geoportal/config.yaml: /build/c2ctemplate-cache.json mv /build/_config.yaml $@ touch $@ +qgisserver/geomapfish.yaml: /build/c2ctemplate-cache.json + $(PRERULE_CMD) + mkdir --parent $(dir $@) + c2c-template --cache /build/c2ctemplate-cache.json --get-config $@ \ + sqlalchemy_slave.url schema schema_static srid + touch $@ + # server localisation .PRECIOUS: geoportal/$(PACKAGE)_geoportal/locale/$(PACKAGE)_geoportal-$(L10N_SERVER_POSTFIX).pot @@ -690,7 +699,8 @@ docker-compose-build.yaml: /build/requirements.timestamp .PHONY: docker-build-config docker-build-config: $(shell docker-required --path .) \ $(PRINT_CONFIG_FILE) \ - $(MAPCACHE_FILE) + $(MAPCACHE_FILE) \ + qgisserver/geomapfish.yaml docker build --tag=$(DOCKER_BASE)-config:$(DOCKER_TAG) . .PHONY: docker-build-geoportal From 01c0cd8bab0fdf7fa671129b5750fff7ff8da7d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Wed, 27 Jun 2018 10:43:27 +0200 Subject: [PATCH 89/92] Use QGIS server as nonroot --- .../scaffolds/create/docker-compose.yaml.mako | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako index dee6a2900f..92216ef228 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose.yaml.mako @@ -30,9 +30,10 @@ ${service_defaults('mapserver', 8080)}\ ## qgisserver: ## image: camptocamp/geomapfish-qgisserver:2.3 +## user: www-data ## volumes_from: ## - config:ro -##${service_defaults('qgisserver', 80)} +##${service_defaults('qgisserver', 8080)} tinyows: image: camptocamp/tinyows From 9adf9bc756f48f240295f344b37b71f67945ac2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Wed, 27 Jun 2018 14:00:05 +0200 Subject: [PATCH 90/92] Use requirements install --- docker/qgisserver/.dockerignore | 1 + docker/qgisserver/Dockerfile | 3 +++ docker/qgisserver/requirements.txt | 12 ++++++++++++ 3 files changed, 16 insertions(+) create mode 100644 docker/qgisserver/requirements.txt diff --git a/docker/qgisserver/.dockerignore b/docker/qgisserver/.dockerignore index 4617214b8d..4a13bbd614 100644 --- a/docker/qgisserver/.dockerignore +++ b/docker/qgisserver/.dockerignore @@ -1,3 +1,4 @@ * !geomapfish_plugin !commons +!requirements.txt diff --git a/docker/qgisserver/Dockerfile b/docker/qgisserver/Dockerfile index 3976296535..84c17cb75b 100644 --- a/docker/qgisserver/Dockerfile +++ b/docker/qgisserver/Dockerfile @@ -1,6 +1,9 @@ FROM camptocamp/qgis-server:latest LABEL maintainer Camptocamp "info@camptocamp.com" +COPY requirements.txt /tmp/ +RUN python3 -m pip install --requirement /tmp/requirements.txt + COPY geomapfish_plugin/* /var/www/plugins/geomapfish_qgisserver/ COPY commons /opt/c2cgeoportal_commons diff --git a/docker/qgisserver/requirements.txt b/docker/qgisserver/requirements.txt new file mode 100644 index 0000000000..bc4da0f603 --- /dev/null +++ b/docker/qgisserver/requirements.txt @@ -0,0 +1,12 @@ +c2cgeoform==2.0.dev20180605 # commons +c2c.template==2.1.0.dev1 # geoportal +ColanderAlchemy==0.3.3 # commons +colander==1.4 # commons, admin +deform==2.0.5 # commons, admin +GeoAlchemy2==0.4.2 # commons, geoportal +geojson==2.3.0 # geoportal +iso8601==0.1.11 # deform, by default the version 0.1.12 is installed and is incompatible with deform, rq.filter: <=0.1.11 +papyrus==2.3 # commons, geoportal +pyramid==1.9.1 +SQLAlchemy==1.2.6 +transaction==2.2.1 # commons, geoportal From e2b11913999e535258741d632d9e4279edd59540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Wed, 27 Jun 2018 14:06:29 +0200 Subject: [PATCH 91/92] Solve some conflict with test upgrades --- Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index fa63a0ec8c..34a8ab4e60 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -38,14 +38,14 @@ dockerBuild { sh 'make docker-build' sh 'docker run --name geomapfish-db --env=POSTGRES_USER=www-data --env=POSTGRES_PASSWORD=www-data --env=POSTGRES_DB=geomapfish --publish=5432:5432 --detach camptocamp/geomapfish-test-db' sh 'travis/test-upgrade-convert.sh init ${HOME}/workspace' + sh './docker-run travis/status.sh' + sh './docker-run travis/short-make build' } stage('Tests') { checkout scm parallel 'Lint and test c2cgeoportal': { - sh './docker-run travis/status.sh' sh './docker-run travis/empty-make help' sh 'bash -c "test \\"`./docker-run id`\\" == \\"uid=0(root) gid=0(root) groups=0(root)\\""' - sh './docker-run travis/short-make build' sh './docker-run make doc' // lint sh './docker-run make checks' From 47335db28d751646f518fee4ea956a3a437548dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Wed, 27 Jun 2018 15:05:35 +0200 Subject: [PATCH 92/92] Fix the srid --- docker/qgisserver/geomapfish_plugin/accesscontrol.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docker/qgisserver/geomapfish_plugin/accesscontrol.py b/docker/qgisserver/geomapfish_plugin/accesscontrol.py index 96fa83f073..297f82fef0 100644 --- a/docker/qgisserver/geomapfish_plugin/accesscontrol.py +++ b/docker/qgisserver/geomapfish_plugin/accesscontrol.py @@ -47,10 +47,8 @@ def __init__(self, server_iface): if "GEOMAPFISH_OGCSERVER" not in os.environ: raise GMFException("The environment variable 'GEOMAPFISH_OGCSERVER' is not defined.") - self.srid = os.environ["GEOMAPFISH_SRID"] - config.init(os.environ.get('GEOMAPFISH_CONFIG', '/etc/qgisserver/geomapfish.yaml')) - self.config = config + self.srid = config.get('srid') from c2cgeoportal_commons.models.main import LayerWMS, OGCServer configure_mappers()