diff --git a/.circleci/config.yml b/.circleci/config.yml index 79ebd63c91b..9babc1a6978 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -23,12 +23,12 @@ jobs: - run: name: Build the stack - command: docker-compose -f docker-compose-test.yml build --no-cache + command: docker-compose --env-file .env_test -f docker-compose-test.yml build --no-cache working_directory: ./ - run: name: Start the stack - command: docker-compose -f docker-compose-test.yml up -d + command: docker-compose --env-file .env_test -f docker-compose-test.yml up -d working_directory: ./ - run: @@ -78,21 +78,21 @@ jobs: - run: name: Run test suite command: | - docker-compose -f docker-compose-test.yml exec db psql -U postgres -c 'SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE pid <> pg_backend_pid();' - docker-compose -f docker-compose-test.yml exec db createdb -U postgres -T postgres test_postgres - docker-compose -f docker-compose-test.yml exec db createdb -U postgres -T postgres test_geonode - docker-compose -f docker-compose-test.yml exec db createdb -U postgres -T postgres test_geonode_data - docker-compose -f docker-compose-test.yml exec db psql -U postgres -d test_geonode -c 'CREATE EXTENSION IF NOT EXISTS postgis;' - docker-compose -f docker-compose-test.yml exec db psql -U postgres -d test_geonode_data -c 'CREATE EXTENSION IF NOT EXISTS postgis;' - docker-compose -f docker-compose-test.yml exec django bash -c '<>' + docker-compose --env-file .env_test -f docker-compose-test.yml exec db psql -U postgres -c 'SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE pid <> pg_backend_pid();' + docker-compose --env-file .env_test -f docker-compose-test.yml exec db createdb -U postgres -T postgres test_postgres + docker-compose --env-file .env_test -f docker-compose-test.yml exec db createdb -U postgres -T postgres test_geonode + docker-compose --env-file .env_test -f docker-compose-test.yml exec db createdb -U postgres -T postgres test_geonode_data + docker-compose --env-file .env_test -f docker-compose-test.yml exec db psql -U postgres -d test_geonode -c 'CREATE EXTENSION IF NOT EXISTS postgis;' + docker-compose --env-file .env_test -f docker-compose-test.yml exec db psql -U postgres -d test_geonode_data -c 'CREATE EXTENSION IF NOT EXISTS postgis;' + docker-compose --env-file .env_test -f docker-compose-test.yml exec django bash -c '<>' working_directory: ./ no_output_timeout: 10m - run: name: Run code quality checks command: | - docker-compose -f docker-compose-test.yml exec django bash -c 'black --check geonode' - docker-compose -f docker-compose-test.yml exec django bash -c 'flake8 geonode' - docker-compose -f docker-compose-test.yml exec django bash -c 'codecov; bash <(curl -s https://codecov.io/bash) -t 2c0e7780-1640-45f0-93a3-e103b057d8c8' + docker-compose --env-file .env_test -f docker-compose-test.yml exec django bash -c 'black --check geonode' + docker-compose --env-file .env_test -f docker-compose-test.yml exec django bash -c 'flake8 geonode' + docker-compose --env-file .env_test -f docker-compose-test.yml exec django bash -c 'codecov; bash <(curl -s https://codecov.io/bash) -t 2c0e7780-1640-45f0-93a3-e103b057d8c8' working_directory: ./ workflows: @@ -108,7 +108,7 @@ workflows: name: geonode_test_suite load_docker_cache: false save_docker_cache: false - test_suite: ./test.sh $(python -c "import sys;from geonode import settings;sys.stdout.write('\'' '\''.join([a+'\''.tests'\'' for a in settings.GEONODE_APPS if '\''security'\'' not in a and '\''geoserver'\'' not in a]))") geonode.thumbs.tests geonode.people.tests + test_suite: ./test.sh $(python -c "import sys;from geonode import settings;sys.stdout.write('\'' '\''.join([a+'\''.tests'\'' for a in settings.GEONODE_APPS if '\''security'\'' not in a and '\''geoserver'\'' not in a]))") geonode.thumbs.tests geonode.people.tests geonode.people.socialaccount.providers.geonode_openid_connect.tests - build: name: geonode_test_security load_docker_cache: false diff --git a/.env b/.env.sample similarity index 73% rename from .env rename to .env.sample index 96b166bf2fb..b74e48d371c 100644 --- a/.env +++ b/.env.sample @@ -1,6 +1,4 @@ COMPOSE_PROJECT_NAME=geonode -DOCKERHOST= -DOCKER_HOST_IP= # See https://github.com/containers/podman/issues/13889 # DOCKER_BUILDKIT=0 DOCKER_ENV=production @@ -13,31 +11,29 @@ C_FORCE_ROOT=1 FORCE_REINIT=false INVOKE_LOG_STDOUT=true -# LANGUAGE_CODE=pt +# LANGUAGE_CODE=it-it # LANGUAGES=(('en-us','English'),('it-it','Italiano')) DJANGO_SETTINGS_MODULE=geonode.settings GEONODE_INSTANCE_NAME=geonode -GEONODE_LB_HOST_IP= -GEONODE_LB_PORT= -PUBLIC_PORT=80 -NGINX_BASE_URL= # ################# # backend # ################# POSTGRES_USER=postgres -POSTGRES_PASSWORD=postgres +POSTGRES_PASSWORD={pgpwd} GEONODE_DATABASE=geonode -GEONODE_DATABASE_PASSWORD=geonode +GEONODE_DATABASE_USER=geonode +GEONODE_DATABASE_PASSWORD={dbpwd} GEONODE_GEODATABASE=geonode_data -GEONODE_GEODATABASE_PASSWORD=geonode_data +GEONODE_GEODATABASE_USER=geonode_data +GEONODE_GEODATABASE_PASSWORD={geodbpwd} GEONODE_DATABASE_SCHEMA=public GEONODE_GEODATABASE_SCHEMA=public DATABASE_HOST=db DATABASE_PORT=5432 -DATABASE_URL=postgis://geonode:geonode@db:5432/geonode -GEODATABASE_URL=postgis://geonode_data:geonode_data@db:5432/geonode_data +DATABASE_URL=postgis://geonode:{dbpwd}@db:5432/geonode +GEODATABASE_URL=postgis://geonode_data:{geodbpwd}@db:5432/geonode_data GEONODE_DB_CONN_MAX_AGE=0 GEONODE_DB_CONN_TOUT=5 DEFAULT_BACKEND_DATASTORE=datastore @@ -45,9 +41,9 @@ BROKER_URL=amqp://guest:guest@rabbitmq:5672/ CELERY_BEAT_SCHEDULER=celery.beat:PersistentScheduler ASYNC_SIGNALS=True -SITEURL=http://localhost/ +SITEURL={siteurl}/ -ALLOWED_HOSTS=['django', '*'] +ALLOWED_HOSTS="['django', '{hostname}']" # Data Uploader DEFAULT_BACKEND_UPLOADER=geonode.importer @@ -62,13 +58,14 @@ HAYSTACK_SEARCH_RESULTS_PER_PAGE=200 # nginx # HTTPD Server # ################# -GEONODE_LB_HOST_IP=localhost -GEONODE_LB_PORT=80 +GEONODE_LB_HOST_IP=django +GEONODE_LB_PORT=8000 +NGINX_BASE_URL={siteurl} # IP or domain name and port where the server can be reached on HTTPS (leave HOST empty if you want to use HTTP only) # port where the server can be reached on HTTPS -HTTP_HOST=localhost -HTTPS_HOST= +HTTP_HOST={http_host} +HTTPS_HOST={https_host} HTTP_PORT=80 HTTPS_PORT=443 @@ -78,7 +75,7 @@ HTTPS_PORT=443 # disabled : we do not get a certificate at all (a placeholder certificate will be used) # staging : we get staging certificates (are invalid, but allow to test the process completely and have much higher limit rates) # production : we get a normal certificate (default) -LETSENCRYPT_MODE=disabled +LETSENCRYPT_MODE={letsencrypt_mode} # LETSENCRYPT_MODE=staging # LETSENCRYPT_MODE=production @@ -87,11 +84,13 @@ RESOLVER=127.0.0.11 # ################# # geoserver # ################# -GEOSERVER_WEB_UI_LOCATION=http://localhost/geoserver/ -GEOSERVER_PUBLIC_LOCATION=http://localhost/geoserver/ -GEOSERVER_LOCATION=http://geoserver:8080/geoserver/ +GEOSERVER_LB_HOST_IP=geoserver +GEOSERVER_LB_PORT=8080 +GEOSERVER_WEB_UI_LOCATION={siteurl}/geoserver/ +GEOSERVER_PUBLIC_LOCATION={siteurl}/geoserver/ +GEOSERVER_LOCATION=http://${GEOSERVER_LB_HOST_IP}:${GEOSERVER_LB_PORT}/geoserver/ GEOSERVER_ADMIN_USER=admin -GEOSERVER_ADMIN_PASSWORD=geoserver +GEOSERVER_ADMIN_PASSWORD={geoserverpwd} OGC_REQUEST_TIMEOUT=30 OGC_REQUEST_MAX_RETRIES=1 @@ -102,7 +101,7 @@ OGC_REQUEST_POOL_CONNECTIONS=10 # Java Options & Memory ENABLE_JSONP=true outFormat=text/javascript -GEOSERVER_JAVA_OPTS="-Djava.awt.headless=true -Xms2G -Xmx4G -Dgwc.context.suffix=gwc -XX:+UnlockDiagnosticVMOptions -XX:+LogVMOutput -XX:LogFile=/var/log/jvm.log -XX:PerfDataSamplingInterval=500 -XX:SoftRefLRUPolicyMSPerMB=36000 -XX:-UseGCOverheadLimit -XX:+UseConcMarkSweepGC -XX:ParallelGCThreads=4 -Dfile.encoding=UTF8 -Djavax.servlet.request.encoding=UTF-8 -Djavax.servlet.response.encoding=UTF-8 -Duser.timezone=GMT -Dorg.geotools.shapefile.datetime=false -DGS-SHAPEFILE-CHARSET=UTF-8 -DGEOSERVER_CSRF_DISABLED=true -DPRINT_BASE_URL=http://geoserver:8080/geoserver/pdf -DALLOW_ENV_PARAMETRIZATION=true -Xbootclasspath/a:/usr/local/tomcat/webapps/geoserver/WEB-INF/lib/marlin-0.9.3-Unsafe.jar -Dsun.java2d.renderer=org.marlin.pisces.MarlinRenderingEngine" +GEOSERVER_JAVA_OPTS='-Djava.awt.headless=true -Xms4G -Xmx4G -Dgwc.context.suffix=gwc -XX:+UnlockDiagnosticVMOptions -XX:+LogVMOutput -XX:LogFile=/var/log/jvm.log -XX:PerfDataSamplingInterval=500 -XX:SoftRefLRUPolicyMSPerMB=36000 -XX:-UseGCOverheadLimit -XX:ParallelGCThreads=4 -Dfile.encoding=UTF8 -Djavax.servlet.request.encoding=UTF-8 -Djavax.servlet.response.encoding=UTF-8 -Duser.timezone=GMT -Dorg.geotools.shapefile.datetime=false -DGS-SHAPEFILE-CHARSET=UTF-8 -DGEOSERVER_CSRF_DISABLED=true -DPRINT_BASE_URL={geoserver_ui}/geoserver/pdf -DALLOW_ENV_PARAMETRIZATION=true -Xbootclasspath/a:/usr/local/tomcat/webapps/geoserver/WEB-INF/lib/marlin-0.9.3-Unsafe.jar -Dsun.java2d.renderer=org.marlin.pisces.MarlinRenderingEngine' # ################# # Security @@ -115,8 +114,8 @@ GEOSERVER_JAVA_OPTS="-Djava.awt.headless=true -Xms2G -Xmx4G -Dgwc.context.suffix # in DB will honored. ADMIN_USERNAME=admin -ADMIN_PASSWORD=admin -ADMIN_EMAIL=admin@localhost +ADMIN_PASSWORD={geonodepwd} +ADMIN_EMAIL={email} # EMAIL Notifications EMAIL_ENABLE=False @@ -127,29 +126,36 @@ DJANGO_EMAIL_HOST_USER= DJANGO_EMAIL_HOST_PASSWORD= DJANGO_EMAIL_USE_TLS=False DJANGO_EMAIL_USE_SSL=False -DEFAULT_FROM_EMAIL='GeoNode ' +DEFAULT_FROM_EMAIL='{email}' # eg Company # Session/Access Control LOCKDOWN_GEONODE=False -CORS_ALLOW_ALL_ORIGINS=True X_FRAME_OPTIONS="SAMEORIGIN" SESSION_EXPIRED_CONTROL_ENABLED=True DEFAULT_ANONYMOUS_VIEW_PERMISSION=True DEFAULT_ANONYMOUS_DOWNLOAD_PERMISSION=True +CORS_ALLOW_ALL_ORIGINS=True +GEOSERVER_CORS_ENABLED=True +GEOSERVER_CORS_ALLOWED_ORIGINS=* +GEOSERVER_CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,HEAD,OPTIONS +GEOSERVER_CORS_ALLOWED_HEADERS=* + # Users Registration ACCOUNT_OPEN_SIGNUP=True ACCOUNT_EMAIL_REQUIRED=True ACCOUNT_APPROVAL_REQUIRED=False ACCOUNT_CONFIRM_EMAIL_ON_GET=False ACCOUNT_EMAIL_VERIFICATION=none +ACCOUNT_EMAIL_CONFIRMATION_EMAIL=False +ACCOUNT_EMAIL_CONFIRMATION_REQUIRED=False ACCOUNT_AUTHENTICATION_METHOD=username_email AUTO_ASSIGN_REGISTERED_MEMBERS_TO_REGISTERED_MEMBERS_GROUP_NAME=True # OAuth2 OAUTH2_API_KEY= -OAUTH2_CLIENT_ID=Jrchz2oPY3akmzndmgUTYrs9gczlgoV20YPSvqaV -OAUTH2_CLIENT_SECRET=rCnp5txobUo83EpQEblM8fVj3QT5zb5qRfxNsuPzCqZaiRyIoxM4jdgMiZKFfePBHYXCLd7B8NlkfDBY9HKeIQPcy5Cp08KQNpRHQbjpLItDHv12GvkSeXp6OxaUETv3 +OAUTH2_CLIENT_ID={clientid} +OAUTH2_CLIENT_SECRET={clientsecret} # GeoNode APIs API_LOCKDOWN=False @@ -159,9 +165,9 @@ TASTYPIE_APIKEY= # Production and # Monitoring # ################# -DEBUG=False +DEBUG={debug} -SECRET_KEY='myv-y4#7j-d*p-__@j#*3z@!y24fz8%^z2v6atuy4bo9vqr1_a' +SECRET_KEY='{secret_key}' STATIC_ROOT=/mnt/volumes/statics/static/ MEDIA_ROOT=/mnt/volumes/statics/uploaded/ @@ -175,7 +181,7 @@ MEMCACHED_LOCATION=127.0.0.1:11211 MEMCACHED_LOCK_EXPIRE=3600 MEMCACHED_LOCK_TIMEOUT=10 -MAX_DOCUMENT_SIZE=2 +MAX_DOCUMENT_SIZE=200 CLIENT_RESULTS_LIMIT=5 API_LIMIT_PER_PAGE=1000 @@ -186,7 +192,7 @@ BING_API_KEY= GOOGLE_API_KEY= # Monitoring -MONITORING_ENABLED=True +MONITORING_ENABLED=False MONITORING_DATA_TTL=365 USER_ANALYTICS_ENABLED=True USER_ANALYTICS_GZIP=True @@ -205,13 +211,6 @@ FAVORITE_ENABLED=True RESOURCE_PUBLISHING=False ADMIN_MODERATE_UPLOADS=False -# PostgreSQL -POSTGRESQL_MAX_CONNECTIONS=200 - -# Upload Size Limiting -DEFAULT_MAX_UPLOAD_SIZE=5368709120 -DEFAULT_MAX_PARALLEL_UPLOADS_PER_USER=100 - # LDAP LDAP_ENABLED=False LDAP_SERVER_URL=ldap:// @@ -230,10 +229,22 @@ LDAP_GROUP_PROFILE_MEMBER_ATTR=uniqueMember # ## # Note right autoscale value must coincide with worker concurrency value # CELERY__AUTOSCALE_VALUES="15,10" -# CELERY__WORKER_CONCURRENCY="4" +# CELERY__WORKER_CONCURRENCY="10" # ## # CELERY__OPTS="--without-gossip --without-mingle -Ofair -B -E" # CELERY__BEAT_SCHEDULE="/mnt/volumes/statics/celerybeat-schedule" # CELERY__LOG_LEVEL="INFO" # CELERY__LOG_FILE="/var/log/celery.log" # CELERY__WORKER_NAME="worker1@%h" + +# PostgreSQL +POSTGRESQL_MAX_CONNECTIONS=200 + +# Common containers restart policy +RESTART_POLICY_CONDITION="on-failure" +RESTART_POLICY_DELAY="5s" +RESTART_POLICY_MAX_ATTEMPTS="3" +RESTART_POLICY_WINDOW=120s + +DEFAULT_MAX_UPLOAD_SIZE=5368709120 +DEFAULT_MAX_PARALLEL_UPLOADS_PER_USER=5 diff --git a/.env_dev b/.env_dev index 937d630ad4d..6119f68ebd6 100644 --- a/.env_dev +++ b/.env_dev @@ -1,6 +1,4 @@ COMPOSE_PROJECT_NAME=geonode -DOCKERHOST= -DOCKER_HOST_IP= # See https://github.com/containers/podman/issues/13889 # DOCKER_BUILDKIT=0 DOCKER_ENV=production @@ -13,15 +11,11 @@ C_FORCE_ROOT=1 FORCE_REINIT=false INVOKE_LOG_STDOUT=true -# LANGUAGE_CODE=pt -# LANGUAGES=(('en','English'),('pt','Portuguese')) +# LANGUAGE_CODE=it-it +# LANGUAGES=(('en-us','English'),('it-it','Italiano')) DJANGO_SETTINGS_MODULE=geonode.settings GEONODE_INSTANCE_NAME=geonode -GEONODE_LB_HOST_IP= -GEONODE_LB_PORT= -PUBLIC_PORT=80 -NGINX_BASE_URL= # ################# # backend @@ -29,8 +23,10 @@ NGINX_BASE_URL= POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres GEONODE_DATABASE=geonode +GEONODE_DATABASE_USER=geonode GEONODE_DATABASE_PASSWORD=geonode GEONODE_GEODATABASE=geonode_data +GEONODE_GEODATABASE_USER=geonode GEONODE_GEODATABASE_PASSWORD=geonode GEONODE_DATABASE_SCHEMA=public GEONODE_GEODATABASE_SCHEMA=public @@ -62,8 +58,11 @@ HAYSTACK_SEARCH_RESULTS_PER_PAGE=200 # nginx # HTTPD Server # ################# -GEONODE_LB_HOST_IP=localhost -GEONODE_LB_PORT=80 +GEONODE_LB_HOST_IP=django +GEONODE_LB_PORT=8000 +GEOSERVER_LB_HOST_IP=geoserver +GEOSERVER_LB_PORT=8080 +NGINX_BASE_URL=http://localhost # IP or domain name and port where the server can be reached on HTTPS (leave HOST empty if you want to use HTTP only) # port where the server can be reached on HTTPS @@ -93,8 +92,8 @@ GEOSERVER_LOCATION=http://localhost:8080/geoserver/ GEOSERVER_ADMIN_USER=admin GEOSERVER_ADMIN_PASSWORD=geoserver -OGC_REQUEST_TIMEOUT=60 -OGC_REQUEST_MAX_RETRIES=0 +OGC_REQUEST_TIMEOUT=30 +OGC_REQUEST_MAX_RETRIES=1 OGC_REQUEST_BACKOFF_FACTOR=0.3 OGC_REQUEST_POOL_MAXSIZE=10 OGC_REQUEST_POOL_CONNECTIONS=10 @@ -102,12 +101,18 @@ OGC_REQUEST_POOL_CONNECTIONS=10 # Java Options & Memory ENABLE_JSONP=true outFormat=text/javascript -GEOSERVER_JAVA_OPTS="-Djava.awt.headless=true -Xms2G -Xmx4G -Dgwc.context.suffix=gwc -XX:+UnlockDiagnosticVMOptions -XX:+LogVMOutput -XX:LogFile=/var/log/jvm.log -XX:PerfDataSamplingInterval=500 -XX:SoftRefLRUPolicyMSPerMB=36000 -XX:-UseGCOverheadLimit -XX:+UseConcMarkSweepGC -XX:ParallelGCThreads=4 -Dfile.encoding=UTF8 -Djavax.servlet.request.encoding=UTF-8 -Djavax.servlet.response.encoding=UTF-8 -Duser.timezone=GMT -Dorg.geotools.shapefile.datetime=false -DGS-SHAPEFILE-CHARSET=UTF-8 -DGEOSERVER_CSRF_DISABLED=true -DPRINT_BASE_URL=http://localhost:8080/geoserver/pdf -DALLOW_ENV_PARAMETRIZATION=true -Xbootclasspath/a:/usr/local/tomcat/webapps/geoserver/WEB-INF/lib/marlin-0.9.3-Unsafe.jar -Dsun.java2d.renderer=org.marlin.pisces.MarlinRenderingEngine" +GEOSERVER_JAVA_OPTS='-Djava.awt.headless=true -Xms4G -Xmx4G -Dgwc.context.suffix=gwc -XX:+UnlockDiagnosticVMOptions -XX:+LogVMOutput -XX:LogFile=/var/log/jvm.log -XX:PerfDataSamplingInterval=500 -XX:SoftRefLRUPolicyMSPerMB=36000 -XX:-UseGCOverheadLimit -XX:ParallelGCThreads=4 -Dfile.encoding=UTF8 -Djavax.servlet.request.encoding=UTF-8 -Djavax.servlet.response.encoding=UTF-8 -Duser.timezone=GMT -Dorg.geotools.shapefile.datetime=false -DGS-SHAPEFILE-CHARSET=UTF-8 -DGEOSERVER_CSRF_DISABLED=true -DPRINT_BASE_URL=http://localhost:8080/geoserver/pdf -DALLOW_ENV_PARAMETRIZATION=true -Xbootclasspath/a:/usr/local/tomcat/webapps/geoserver/WEB-INF/lib/marlin-0.9.3-Unsafe.jar -Dsun.java2d.renderer=org.marlin.pisces.MarlinRenderingEngine' # ################# # Security # ################# # Admin Settings +# +# ADMIN_PASSWORD is used to overwrite the GeoNode admin password **ONLY** the first time +# GeoNode is run. If you need to overwrite it again, you need to set the env var FORCE_REINIT, +# otherwise the invoke updateadmin task will be skipped and the current password already stored +# in DB will honored. + ADMIN_USERNAME=admin ADMIN_PASSWORD=admin ADMIN_EMAIL=admin@localhost @@ -125,18 +130,25 @@ DEFAULT_FROM_EMAIL='GeoNode ' # Session/Access Control LOCKDOWN_GEONODE=False -CORS_ALLOW_ALL_ORIGINS=True X_FRAME_OPTIONS="SAMEORIGIN" SESSION_EXPIRED_CONTROL_ENABLED=True DEFAULT_ANONYMOUS_VIEW_PERMISSION=True DEFAULT_ANONYMOUS_DOWNLOAD_PERMISSION=True +CORS_ALLOW_ALL_ORIGINS=True +GEOSERVER_CORS_ENABLED=True +GEOSERVER_CORS_ALLOWED_ORIGINS=* +GEOSERVER_CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,HEAD,OPTIONS +GEOSERVER_CORS_ALLOWED_HEADERS=* + # Users Registration ACCOUNT_OPEN_SIGNUP=True ACCOUNT_EMAIL_REQUIRED=True ACCOUNT_APPROVAL_REQUIRED=False ACCOUNT_CONFIRM_EMAIL_ON_GET=False ACCOUNT_EMAIL_VERIFICATION=none +ACCOUNT_EMAIL_CONFIRMATION_EMAIL=False +ACCOUNT_EMAIL_CONFIRMATION_REQUIRED=False ACCOUNT_AUTHENTICATION_METHOD=username_email AUTO_ASSIGN_REGISTERED_MEMBERS_TO_REGISTERED_MEMBERS_GROUP_NAME=True @@ -169,7 +181,7 @@ MEMCACHED_LOCATION=127.0.0.1:11211 MEMCACHED_LOCK_EXPIRE=3600 MEMCACHED_LOCK_TIMEOUT=10 -MAX_DOCUMENT_SIZE=2 +MAX_DOCUMENT_SIZE=200 CLIENT_RESULTS_LIMIT=5 API_LIMIT_PER_PAGE=1000 @@ -202,6 +214,11 @@ ADMIN_MODERATE_UPLOADS=False # PostgreSQL POSTGRESQL_MAX_CONNECTIONS=200 -# Upload Size Limiting +# Common containers restart policy +RESTART_POLICY_CONDITION="on-failure" +RESTART_POLICY_DELAY="5s" +RESTART_POLICY_MAX_ATTEMPTS="3" +RESTART_POLICY_WINDOW=120s + DEFAULT_MAX_UPLOAD_SIZE=5368709120 -DEFAULT_MAX_PARALLEL_UPLOADS_PER_USER=100 \ No newline at end of file +DEFAULT_MAX_PARALLEL_UPLOADS_PER_USER=5 diff --git a/.env_local b/.env_local index 4bec5dc60bc..48cba183d81 100644 --- a/.env_local +++ b/.env_local @@ -1,6 +1,4 @@ COMPOSE_PROJECT_NAME=geonode -DOCKERHOST= -DOCKER_HOST_IP= # See https://github.com/containers/podman/issues/13889 # DOCKER_BUILDKIT=0 DOCKER_ENV=production @@ -13,15 +11,11 @@ C_FORCE_ROOT=1 FORCE_REINIT=false INVOKE_LOG_STDOUT=true -# LANGUAGE_CODE=pt -# LANGUAGES=(('en','English'),('pt','Portuguese')) +# LANGUAGE_CODE=it-it +# LANGUAGES=(('en-us','English'),('it-it','Italiano')) DJANGO_SETTINGS_MODULE=geonode.settings GEONODE_INSTANCE_NAME=geonode -GEONODE_LB_HOST_IP= -GEONODE_LB_PORT= -PUBLIC_PORT=80 -NGINX_BASE_URL= # ################# # backend @@ -29,8 +23,10 @@ NGINX_BASE_URL= POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres GEONODE_DATABASE=geonode +GEONODE_DATABASE_USER=geonode GEONODE_DATABASE_PASSWORD=geonode GEONODE_GEODATABASE=geonode_data +GEONODE_GEODATABASE_USER=geonode GEONODE_GEODATABASE_PASSWORD=geonode GEONODE_DATABASE_SCHEMA=public GEONODE_GEODATABASE_SCHEMA=public @@ -45,7 +41,7 @@ BROKER_URL=amqp://admin:admin@localhost:5672// CELERY_BEAT_SCHEDULER=celery.beat:PersistentScheduler ASYNC_SIGNALS=False -SITEURL=http://localhost/ +SITEURL=http://localhost:8000/ ALLOWED_HOSTS="['django', '*']" @@ -62,8 +58,11 @@ HAYSTACK_SEARCH_RESULTS_PER_PAGE=200 # nginx # HTTPD Server # ################# -GEONODE_LB_HOST_IP=localhost -GEONODE_LB_PORT=80 +GEONODE_LB_HOST_IP=django +GEONODE_LB_PORT=8000 +GEOSERVER_LB_HOST_IP=geoserver +GEOSERVER_LB_PORT=8080 +NGINX_BASE_URL=https://localhost # IP or domain name and port where the server can be reached on HTTPS (leave HOST empty if you want to use HTTP only) # port where the server can be reached on HTTPS @@ -102,12 +101,18 @@ OGC_REQUEST_POOL_CONNECTIONS=10 # Java Options & Memory ENABLE_JSONP=true outFormat=text/javascript -GEOSERVER_JAVA_OPTS="-Djava.awt.headless=true -Xms2G -Xmx4G -Dgwc.context.suffix=gwc -XX:+UnlockDiagnosticVMOptions -XX:+LogVMOutput -XX:LogFile=/var/log/jvm.log -XX:PerfDataSamplingInterval=500 -XX:SoftRefLRUPolicyMSPerMB=36000 -XX:-UseGCOverheadLimit -XX:+UseConcMarkSweepGC -XX:ParallelGCThreads=4 -Dfile.encoding=UTF8 -Djavax.servlet.request.encoding=UTF-8 -Djavax.servlet.response.encoding=UTF-8 -Duser.timezone=GMT -Dorg.geotools.shapefile.datetime=false -DGS-SHAPEFILE-CHARSET=UTF-8 -DGEOSERVER_CSRF_DISABLED=true -DPRINT_BASE_URL=http://localhost:8080/geoserver/pdf -DALLOW_ENV_PARAMETRIZATION=true -Xbootclasspath/a:/usr/local/tomcat/webapps/geoserver/WEB-INF/lib/marlin-0.9.3-Unsafe.jar -Dsun.java2d.renderer=org.marlin.pisces.MarlinRenderingEngine" +GEOSERVER_JAVA_OPTS='-Djava.awt.headless=true -Xms4G -Xmx4G -Dgwc.context.suffix=gwc -XX:+UnlockDiagnosticVMOptions -XX:+LogVMOutput -XX:LogFile=/var/log/jvm.log -XX:PerfDataSamplingInterval=500 -XX:SoftRefLRUPolicyMSPerMB=36000 -XX:-UseGCOverheadLimit -XX:ParallelGCThreads=4 -Dfile.encoding=UTF8 -Djavax.servlet.request.encoding=UTF-8 -Djavax.servlet.response.encoding=UTF-8 -Duser.timezone=GMT -Dorg.geotools.shapefile.datetime=false -DGS-SHAPEFILE-CHARSET=UTF-8 -DGEOSERVER_CSRF_DISABLED=true -DPRINT_BASE_URL=http://localhost:8080/geoserver/pdf -DALLOW_ENV_PARAMETRIZATION=true -Xbootclasspath/a:/usr/local/tomcat/webapps/geoserver/WEB-INF/lib/marlin-0.9.3-Unsafe.jar -Dsun.java2d.renderer=org.marlin.pisces.MarlinRenderingEngine' # ################# # Security # ################# # Admin Settings +# +# ADMIN_PASSWORD is used to overwrite the GeoNode admin password **ONLY** the first time +# GeoNode is run. If you need to overwrite it again, you need to set the env var FORCE_REINIT, +# otherwise the invoke updateadmin task will be skipped and the current password already stored +# in DB will honored. + ADMIN_USERNAME=admin ADMIN_PASSWORD=admin ADMIN_EMAIL=admin@localhost @@ -125,18 +130,25 @@ DEFAULT_FROM_EMAIL='GeoNode ' # Session/Access Control LOCKDOWN_GEONODE=False -CORS_ALLOW_ALL_ORIGINS=True X_FRAME_OPTIONS="SAMEORIGIN" SESSION_EXPIRED_CONTROL_ENABLED=True DEFAULT_ANONYMOUS_VIEW_PERMISSION=True DEFAULT_ANONYMOUS_DOWNLOAD_PERMISSION=True +CORS_ALLOW_ALL_ORIGINS=True +GEOSERVER_CORS_ENABLED=True +GEOSERVER_CORS_ALLOWED_ORIGINS=* +GEOSERVER_CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,HEAD,OPTIONS +GEOSERVER_CORS_ALLOWED_HEADERS=* + # Users Registration ACCOUNT_OPEN_SIGNUP=True ACCOUNT_EMAIL_REQUIRED=True ACCOUNT_APPROVAL_REQUIRED=False ACCOUNT_CONFIRM_EMAIL_ON_GET=False ACCOUNT_EMAIL_VERIFICATION=none +ACCOUNT_EMAIL_CONFIRMATION_EMAIL=False +ACCOUNT_EMAIL_CONFIRMATION_REQUIRED=False ACCOUNT_AUTHENTICATION_METHOD=username_email AUTO_ASSIGN_REGISTERED_MEMBERS_TO_REGISTERED_MEMBERS_GROUP_NAME=True @@ -169,7 +181,7 @@ MEMCACHED_LOCATION=127.0.0.1:11211 MEMCACHED_LOCK_EXPIRE=3600 MEMCACHED_LOCK_TIMEOUT=10 -MAX_DOCUMENT_SIZE=2 +MAX_DOCUMENT_SIZE=200 CLIENT_RESULTS_LIMIT=5 API_LIMIT_PER_PAGE=1000 @@ -180,7 +192,7 @@ BING_API_KEY= GOOGLE_API_KEY= # Monitoring -MONITORING_ENABLED=True +MONITORING_ENABLED=False MONITORING_DATA_TTL=365 USER_ANALYTICS_ENABLED=True USER_ANALYTICS_GZIP=True @@ -202,6 +214,11 @@ ADMIN_MODERATE_UPLOADS=False # PostgreSQL POSTGRESQL_MAX_CONNECTIONS=200 -# Upload Size Limiting +# Common containers restart policy +RESTART_POLICY_CONDITION="on-failure" +RESTART_POLICY_DELAY="5s" +RESTART_POLICY_MAX_ATTEMPTS="3" +RESTART_POLICY_WINDOW=120s + DEFAULT_MAX_UPLOAD_SIZE=5368709120 -DEFAULT_MAX_PARALLEL_UPLOADS_PER_USER=100 \ No newline at end of file +DEFAULT_MAX_PARALLEL_UPLOADS_PER_USER=5 diff --git a/.env_test b/.env_test index 78eebee94cb..fb1910d57b1 100644 --- a/.env_test +++ b/.env_test @@ -1,6 +1,4 @@ COMPOSE_PROJECT_NAME=geonode -DOCKERHOST= -DOCKER_HOST_IP= # See https://github.com/containers/podman/issues/13889 # DOCKER_BUILDKIT=0 DOCKER_ENV=production @@ -13,15 +11,11 @@ C_FORCE_ROOT=1 FORCE_REINIT=false INVOKE_LOG_STDOUT=true -# LANGUAGE_CODE=pt -# LANGUAGES=(('en','English'),('pt','Portuguese')) +# LANGUAGE_CODE=it-it +# LANGUAGES=(('en-us','English'),('it-it','Italiano')) DJANGO_SETTINGS_MODULE=geonode.settings GEONODE_INSTANCE_NAME=geonode -GEONODE_LB_HOST_IP= -GEONODE_LB_PORT= -PUBLIC_PORT=80 -NGINX_BASE_URL= # ################# # backend @@ -29,8 +23,10 @@ NGINX_BASE_URL= POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres GEONODE_DATABASE=geonode +GEONODE_DATABASE_USER=geonode GEONODE_DATABASE_PASSWORD=geonode GEONODE_GEODATABASE=geonode_data +GEONODE_GEODATABASE_USER=geonode_data GEONODE_GEODATABASE_PASSWORD=geonode_data GEONODE_DATABASE_SCHEMA=public GEONODE_GEODATABASE_SCHEMA=public @@ -45,7 +41,7 @@ BROKER_URL=amqp://guest:guest@rabbitmq:5672/ CELERY_BEAT_SCHEDULER=celery.beat:PersistentScheduler ASYNC_SIGNALS=True -SITEURL=http://localhost:8001/ +SITEURL=http://localhost:8000/ ALLOWED_HOSTS="['django', '*']" @@ -62,15 +58,18 @@ HAYSTACK_SEARCH_RESULTS_PER_PAGE=200 # nginx # HTTPD Server # ################# -GEONODE_LB_HOST_IP=localhost -GEONODE_LB_PORT=80 +GEONODE_LB_HOST_IP=django +GEONODE_LB_PORT=8000 +GEOSERVER_LB_HOST_IP=geoserver +GEOSERVER_LB_PORT=8080 +NGINX_BASE_URL=http://localhost # IP or domain name and port where the server can be reached on HTTPS (leave HOST empty if you want to use HTTP only) # port where the server can be reached on HTTPS HTTP_HOST=localhost HTTPS_HOST= -HTTP_PORT=8001 +HTTP_PORT=8000 HTTPS_PORT=443 # Let's Encrypt certificates for https encryption. You must have a domain name as HTTPS_HOST (doesn't work @@ -87,7 +86,7 @@ RESOLVER=127.0.0.11 # ################# # geoserver # ################# -GEOSERVER_WEB_UI_LOCATION=http://localhost:8001/geoserver/ +GEOSERVER_WEB_UI_LOCATION=http://localhost:8000/geoserver/ GEOSERVER_PUBLIC_LOCATION=http://localhost/geoserver/ GEOSERVER_LOCATION=http://geoserver:8080/geoserver/ GEOSERVER_ADMIN_USER=admin @@ -103,12 +102,12 @@ OGC_REQUEST_POOL_CONNECTIONS=10 # catalogue # ################# CATALOGUE_ENGINE=geonode.catalogue.backends.pycsw_local -CATALOGUE_URL=http://localhost:8001/catalogue/csw +CATALOGUE_URL=http://localhost:8000/catalogue/csw # Java Options & Memory ENABLE_JSONP=true outFormat=text/javascript -GEOSERVER_JAVA_OPTS="-Djava.awt.headless=true -Xms2G -Xmx4G -Dgwc.context.suffix=gwc -XX:+UnlockDiagnosticVMOptions -XX:+LogVMOutput -XX:LogFile=/var/log/jvm.log -XX:PerfDataSamplingInterval=500 -XX:SoftRefLRUPolicyMSPerMB=36000 -XX:-UseGCOverheadLimit -XX:+UseConcMarkSweepGC -XX:ParallelGCThreads=4 -Dfile.encoding=UTF8 -Djavax.servlet.request.encoding=UTF-8 -Djavax.servlet.response.encoding=UTF-8 -Duser.timezone=GMT -Dorg.geotools.shapefile.datetime=false -DGS-SHAPEFILE-CHARSET=UTF-8 -DGEOSERVER_CSRF_DISABLED=true -DPRINT_BASE_URL=http://localhost:8001/geoserver/pdf -DALLOW_ENV_PARAMETRIZATION=true -Xbootclasspath/a:/usr/local/tomcat/webapps/geoserver/WEB-INF/lib/marlin-0.9.3-Unsafe.jar -Dsun.java2d.renderer=org.marlin.pisces.MarlinRenderingEngine" +GEOSERVER_JAVA_OPTS='-Djava.awt.headless=true -Xms4G -Xmx4G -Dgwc.context.suffix=gwc -XX:+UnlockDiagnosticVMOptions -XX:+LogVMOutput -XX:LogFile=/var/log/jvm.log -XX:PerfDataSamplingInterval=500 -XX:SoftRefLRUPolicyMSPerMB=36000 -XX:-UseGCOverheadLimit -XX:ParallelGCThreads=4 -Dfile.encoding=UTF8 -Djavax.servlet.request.encoding=UTF-8 -Djavax.servlet.response.encoding=UTF-8 -Duser.timezone=GMT -Dorg.geotools.shapefile.datetime=false -DGS-SHAPEFILE-CHARSET=UTF-8 -DGEOSERVER_CSRF_DISABLED=true -DPRINT_BASE_URL=http://localhost/geoserver/pdf -DALLOW_ENV_PARAMETRIZATION=true -Xbootclasspath/a:/usr/local/tomcat/webapps/geoserver/WEB-INF/lib/marlin-0.9.3-Unsafe.jar -Dsun.java2d.renderer=org.marlin.pisces.MarlinRenderingEngine' # ################# # Security @@ -137,18 +136,25 @@ DEFAULT_FROM_EMAIL='GeoNode ' # Session/Access Control LOCKDOWN_GEONODE=False -CORS_ALLOW_ALL_ORIGINS=True X_FRAME_OPTIONS="SAMEORIGIN" SESSION_EXPIRED_CONTROL_ENABLED=True DEFAULT_ANONYMOUS_VIEW_PERMISSION=True DEFAULT_ANONYMOUS_DOWNLOAD_PERMISSION=True +CORS_ALLOW_ALL_ORIGINS=True +GEOSERVER_CORS_ENABLED=True +GEOSERVER_CORS_ALLOWED_ORIGINS=* +GEOSERVER_CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,HEAD,OPTIONS +GEOSERVER_CORS_ALLOWED_HEADERS=* + # Users Registration ACCOUNT_OPEN_SIGNUP=True ACCOUNT_EMAIL_REQUIRED=True ACCOUNT_APPROVAL_REQUIRED=False ACCOUNT_CONFIRM_EMAIL_ON_GET=False ACCOUNT_EMAIL_VERIFICATION=none +ACCOUNT_EMAIL_CONFIRMATION_EMAIL=False +ACCOUNT_EMAIL_CONFIRMATION_REQUIRED=False ACCOUNT_AUTHENTICATION_METHOD=username_email AUTO_ASSIGN_REGISTERED_MEMBERS_TO_REGISTERED_MEMBERS_GROUP_NAME=True @@ -157,6 +163,9 @@ OAUTH2_API_KEY= OAUTH2_CLIENT_ID=Jrchz2oPY3akmzndmgUTYrs9gczlgoV20YPSvqaV OAUTH2_CLIENT_SECRET=rCnp5txobUo83EpQEblM8fVj3QT5zb5qRfxNsuPzCqZaiRyIoxM4jdgMiZKFfePBHYXCLd7B8NlkfDBY9HKeIQPcy5Cp08KQNpRHQbjpLItDHv12GvkSeXp6OxaUETv3 +SOCIALACCOUNT_OIDC_PROVIDER_ENABLED=True +SOCIALACCOUNT_PROVIDER=google + # GeoNode APIs API_LOCKDOWN=False TASTYPIE_APIKEY= @@ -181,7 +190,7 @@ MEMCACHED_LOCATION=127.0.0.1:11211 MEMCACHED_LOCK_EXPIRE=3600 MEMCACHED_LOCK_TIMEOUT=10 -MAX_DOCUMENT_SIZE=2 +MAX_DOCUMENT_SIZE=200 CLIENT_RESULTS_LIMIT=5 API_LIMIT_PER_PAGE=1000 @@ -192,7 +201,7 @@ BING_API_KEY= GOOGLE_API_KEY= # Monitoring -MONITORING_ENABLED=True +MONITORING_ENABLED=False MONITORING_DATA_TTL=365 USER_ANALYTICS_ENABLED=True USER_ANALYTICS_GZIP=True @@ -214,6 +223,17 @@ ADMIN_MODERATE_UPLOADS=False # PostgreSQL POSTGRESQL_MAX_CONNECTIONS=200 -# Upload Size Limiting +# Common containers restart policy +RESTART_POLICY_CONDITION="on-failure" +RESTART_POLICY_DELAY="5s" +RESTART_POLICY_MAX_ATTEMPTS="3" +RESTART_POLICY_WINDOW=120s + DEFAULT_MAX_UPLOAD_SIZE=5368709120 -DEFAULT_MAX_PARALLEL_UPLOADS_PER_USER=100 \ No newline at end of file +DEFAULT_MAX_PARALLEL_UPLOADS_PER_USER=100 + +# Azure AD +MICROSOFT_TENANT_ID= +AZURE_CLIENT_ID= +AZURE_SECRET_KEY= +AZURE_KEY= diff --git a/.gitignore b/.gitignore index bfe33426c44..7a455624878 100644 --- a/.gitignore +++ b/.gitignore @@ -95,3 +95,5 @@ scripts/spcgeonode/_volume_* # Docker Hub hooks !hooks/* +.env + diff --git a/Dockerfile b/Dockerfile index 6c62736737b..0805c6494d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,51 +1,15 @@ -FROM ubuntu:22.04 +FROM geonode/geonode-base:latest-ubuntu-22.10 LABEL GeoNode development team -RUN mkdir -p /usr/src/geonode - -## Enable postgresql-client-13 -RUN apt-get update -y && apt-get install curl wget unzip gnupg2 -y -RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - -# will install python3.10 -RUN apt-get install lsb-core -y -RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" |tee /etc/apt/sources.list.d/pgdg.list - -# Prepraing dependencies -RUN apt-get install -y \ - libgdal-dev libpq-dev libxml2-dev \ - libxml2 libxslt1-dev zlib1g-dev libjpeg-dev \ - libmemcached-dev libldap2-dev libsasl2-dev libffi-dev - -RUN apt-get update -y && apt-get install -y --no-install-recommends \ - gcc zip gettext geoip-bin cron \ - postgresql-client-13 \ - python3-all-dev python3-dev \ - python3-gdal python3-psycopg2 python3-ldap \ - python3-pip python3-pil python3-lxml \ - uwsgi uwsgi-plugin-python3 python3-gdbm python-is-python3 gdal-bin - -RUN apt-get install -y devscripts build-essential debhelper pkg-kde-tools sharutils -# RUN git clone https://salsa.debian.org/debian-gis-team/proj.git /tmp/proj -# RUN cd /tmp/proj && debuild -i -us -uc -b && dpkg -i ../*.deb - -# Install pip packages -RUN pip3 install uwsgi \ - && pip install pip --upgrade \ - && pip install pygdal==$(gdal-config --version).* flower==0.9.4 - -# Activate "memcached" -RUN apt-get install -y memcached -RUN pip install sherlock - # add bower and grunt command COPY . /usr/src/geonode/ WORKDIR /usr/src/geonode -COPY monitoring-cron /etc/cron.d/monitoring-cron -RUN chmod 0644 /etc/cron.d/monitoring-cron -RUN crontab /etc/cron.d/monitoring-cron -RUN touch /var/log/cron.log -RUN service cron start +#COPY monitoring-cron /etc/cron.d/monitoring-cron +#RUN chmod 0644 /etc/cron.d/monitoring-cron +#RUN crontab /etc/cron.d/monitoring-cron +#RUN touch /var/log/cron.log +#RUN service cron start COPY wait-for-databases.sh /usr/bin/wait-for-databases RUN chmod +x /usr/bin/wait-for-databases @@ -64,10 +28,12 @@ RUN chmod +x /usr/bin/celery-cmd # RUN cd /usr/src/geonode-contribs/geonode-logstash; pip install --upgrade -e . \ # cd /usr/src/geonode-contribs/ldap; pip install --upgrade -e . -RUN pip install --upgrade --no-cache-dir --src /usr/src -r requirements.txt -RUN pip install --upgrade -e . +RUN yes w | pip install --src /usr/src -Ur requirements.txt +RUN yes w | pip install --upgrade -e . # Cleanup apt update lists +RUN apt-get autoremove --purge +RUN apt-get clean RUN rm -rf /var/lib/apt/lists/* # Export ports diff --git a/README.md b/README.md index db5ca65a4b3..8ec744e587e 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Table of Contents - [Table of Contents](#table-of-contents) - [What is GeoNode?](#what-is-geonode) - [Try out GeoNode](#try-out-geonode) + - [Quick Docker Start](#quick-docker-start) - [Install](#install) - [Learn GeoNode](#learn-geonode) - [Development](#development) @@ -47,17 +48,51 @@ maps, editing metadata, styles and much more. To get an overview what GeoNode can do we recommend to have a look at the [Users Workshop](https://docs.geonode.org/en/master/usage/index.html). +Quick Docker Start +------------------ + + ```bash + python3.10 -m venv ~/.venvs/geonode + source ~/.venvs/geonode/bin/activate + + pip install Django==3.2.* + ``` + ```bash + python create-envfile.py + ``` +`create-envfile.py` accepts the following arguments: + +- `--https`: Enable SSL. It's disabled by default +- `--env_type`: + - When set to `prod` `DEBUG` is disabled and the creation of a valid `SSL` is requested to Letsencrypt's ACME server + - When set to `test` `DEBUG` is disabled and a test `SSL` certificate is generated for local testing + - When set to `dev` `DEBUG` is enabled and no `SSL` certificate is generated +- `--hostname`: The URL that whill serve GeoNode (`localhost` by default) +- `--email`: The administrator's email. Notice that a real email and a valid SMPT configurations are required if `--env_type` is seto to `prod`. Letsencrypt uses to email for issuing the SSL certificate +- `--geonodepwd`: GeoNode's administrator password. A random value is set if left empty +- `--geoserverpwd`: GeoNode's administrator password. A random value is set if left empty +- `--pgpwd`: PostgreSQL's administrator password. A random value is set if left empty +- `--dbpwd`: GeoNode DB user role's password. A random value is set if left empty +- `--geodbpwd`: GeoNode data DB user role's password. A random value is set if left empty +- `--clientid`: Client id of Geoserver's GeoNode Oauth2 client. A random value is set if left empty +- `--clientsecret`: Client secret of Geoserver's GeoNode Oauth2 client. A random value is set if left empty + +```bash + docker compose build + docker compose up -d +``` + Install ------- - The latest official release is 4.0.2! + The latest official release is 4.1.0! GeoNode can be setup in different ways, flavors and plattforms. If you´re planning to do development or install for production please visit the offical GeoNode installation documentation: - [Docker](https://docs.geonode.org/en/master/install/advanced/core/index.html#docker) -- [Ubuntu 20.04 LTS](https://docs.geonode.org/en/master/install/advanced/core/index.html#ubuntu-20-04lts) +- [Ubuntu 22.04](https://docs.geonode.org/en/master/install/advanced/core/index.html#ubuntu-22-04) Learn GeoNode ------------- diff --git a/create-envfile.py b/create-envfile.py new file mode 100644 index 00000000000..c910e912618 --- /dev/null +++ b/create-envfile.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2022 OSGeo +# +# 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 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### +import argparse +import json +import logging +import os +import random +import re +import string +import sys +import ast + +dir_path = os.path.dirname(os.path.realpath(__file__)) + +logger = logging.getLogger() +handler = logging.StreamHandler(sys.stdout) +logger.setLevel(logging.INFO) +formatter = logging.Formatter("%(levelname)s - %(message)s") +handler.setFormatter(formatter) +logger.addHandler(handler) + + +def shuffle(chars): + chars_as_list = list(chars) + random.shuffle(chars_as_list) + return "".join(chars_as_list) + + +_simple_chars = shuffle(string.ascii_letters + string.digits) +_strong_chars = shuffle( + string.ascii_letters + string.digits + string.punctuation.replace('"', "").replace("'", "").replace("`", "") +) + + +def generate_env_file(args): + # validity checks + if not os.path.exists(args.sample_file): + logger.error(f"File does not exists {args.sample_file}") + raise FileNotFoundError + + if args.file and not os.path.isfile(args.file): + logger.error(f"File does not exists: {args.file}") + raise FileNotFoundError + + if args.https and not args.email: + raise Exception("With HTTPS enabled, the email parameter is required") + + _sample_file = None + with open(args.sample_file, "r+") as sample_file: + _sample_file = sample_file.read() + + if not _sample_file: + raise Exception("Sample file is empty!") + + def _get_vals_to_replace(args): + _config = ["sample_file", "file", "env_type", "https", "email"] + _jsfile = {} + if args.file: + with open(args.file) as _json_file: + _jsfile = json.load(_json_file) + + _vals_to_replace = {key: _jsfile.get(key, val) for key, val in vars(args).items() if key not in _config} + tcp = "https" if ast.literal_eval(f"{_jsfile.get('https', args.https)}".capitalize()) else "http" + + _vals_to_replace["public_port"] = ( + "443" if ast.literal_eval(f"{_jsfile.get('https', args.https)}".capitalize()) else "80" + ) + _vals_to_replace["http_host"] = _jsfile.get("hostname", args.hostname) if tcp == "http" else "" + _vals_to_replace["https_host"] = _jsfile.get("hostname", args.hostname) if tcp == "https" else "" + + _vals_to_replace["siteurl"] = f"{tcp}://{_jsfile.get('hostname', args.hostname)}" + _vals_to_replace["secret_key"] = _jsfile.get("secret_key", args.secret_key) or "".join( + random.choice(_strong_chars) for _ in range(50) + ) + _vals_to_replace["letsencrypt_mode"] = ( + "disabled" + if not _vals_to_replace.get("https_host") + else "staging" + if _jsfile.get("env_type", args.env_type) in ["test"] + else "production" + ) + _vals_to_replace["debug"] = False if _jsfile.get("env_type", args.env_type) in ["prod", "test"] else True + _vals_to_replace["email"] = _jsfile.get("email", args.email) + + if tcp == "https" and not _vals_to_replace["email"]: + raise Exception("With HTTPS enabled, the email parameter is required") + + return {**_jsfile, **_vals_to_replace} + + for key, val in _get_vals_to_replace(args).items(): + _val = val or "".join(random.choice(_simple_chars) for _ in range(15)) + if isinstance(val, bool) or key in ["email", "http_host", "https_host"]: + _val = str(val) + _sample_file = re.sub( + "{" + key + "}", + lambda _: _val, + _sample_file, + ) + + with open(f"{dir_path}/.env", "w+") as output_env: + output_env.write(_sample_file) + logger.info(f".env file created: {dir_path}/.env") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + prog="ENV file builder", + description="Tool for generate environment file automatically. The information can be passed or via CLI or via JSON file ( --file /path/env.json)", + usage="python create-envfile.py localhost -f /path/to/json/file.json", + allow_abbrev=False + ) + parser.add_argument( + "--noinput", + "--no-input", + action="store_false", + dest="confirmation", + help=("skips prompting for confirmation."), + ) + parser.add_argument( + "-hn", + "--hostname", + help=f"Host name, default localhost", + default="localhost", + ) + + # expected path as a value + parser.add_argument( + "-sf", + "--sample_file", + help=f"Path of the sample file to use as a template. Default is: {dir_path}/.env.sample", + default=f"{dir_path}/.env.sample", + ) + parser.add_argument( + "-f", + "--file", + help="absolute path of the file with the configuration. Note: we expect that the keys of the dictionary have the same name as the CLI params", + ) + # booleans + parser.add_argument("--https", action="store_true", default=False, help="If provided, https is used") + # strings + parser.add_argument("--email", help="Admin email, this field is required if https is enabled") + + parser.add_argument("--geonodepwd", help="GeoNode admin password") + parser.add_argument("--geoserverpwd", help="Geoserver admin password") + parser.add_argument("--pgpwd", help="PostgreSQL password") + parser.add_argument("--dbpwd", help="GeoNode DB user password") + parser.add_argument("--geodbpwd", help="Geodatabase user password") + parser.add_argument("--clientid", help="Oauth2 client id") + parser.add_argument("--clientsecret", help="Oauth2 client secret") + parser.add_argument("--secret_key", help="Django Secret Key") + + parser.add_argument( + "--env_type", + help="Development/production or test", + choices=["prod", "test", "dev"], + default="prod", + ) + + args = parser.parse_args() + + if not args.confirmation: + generate_env_file(args) + else: + overwrite_env = input("This action will overwrite any existing .env file. Do you wish to continue? (y/n)") + if overwrite_env not in ["y", "n"]: + logger.error("Please enter a valid response") + if overwrite_env == "y": + generate_env_file(args) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml new file mode 100644 index 00000000000..0ac801a9621 --- /dev/null +++ b/docker-compose-dev.yml @@ -0,0 +1,165 @@ +version: '3.9' + +# Common Django template for GeoNode and Celery services below +x-common-django: + &default-common-django + image: geonode/geonode:local + restart: unless-stopped + env_file: + - .env + volumes: + - '.:/usr/src/geonode' + - statics:/mnt/volumes/statics + - geoserver-data-dir:/geoserver_data/data + - backup-restore:/backup_restore + - data:/data + - tmp:/tmp + depends_on: + db: + condition: service_healthy + +services: + + # Our custom django application. It includes Geonode. + django: + << : *default-common-django + build: + context: ./ + dockerfile: Dockerfile + container_name: django4${COMPOSE_PROJECT_NAME} + healthcheck: + test: "curl -m 10 --fail --silent --write-out 'HTTP CODE : %{http_code}\n' --output /dev/null http://django:8000/" + start_period: 60s + interval: 60s + timeout: 10s + retries: 2 + environment: + - IS_CELERY=False + entrypoint: ["/usr/src/geonode/entrypoint.sh"] + command: "uwsgi --ini /usr/src/geonode/uwsgi.ini" + + # Celery worker that executes celery tasks created by Django. + celery: + << : *default-common-django + container_name: celery4${COMPOSE_PROJECT_NAME} + depends_on: + django: + condition: service_healthy + environment: + - IS_CELERY=True + entrypoint: ["/usr/src/geonode/entrypoint.sh"] + command: "celery-cmd" + + # Nginx is serving django static and media files and proxies to django and geonode + geonode: + image: geonode/nginx:1.25.1 + build: ./scripts/docker/nginx/ + container_name: nginx4${COMPOSE_PROJECT_NAME} + env_file: + - .env + environment: + - RESOLVER=127.0.0.11 + ports: + - "${HTTP_PORT}:80" + - "${HTTPS_PORT}:443" + volumes: + - nginx-confd:/etc/nginx + - nginx-certificates:/geonode-certificates + - statics:/mnt/volumes/statics + restart: unless-stopped + + # Gets and installs letsencrypt certificates + letsencrypt: + image: geonode/letsencrypt:latest + build: ./scripts/docker/letsencrypt/ + container_name: letsencrypt4${COMPOSE_PROJECT_NAME} + env_file: + - .env + volumes: + - nginx-certificates:/geonode-certificates + restart: unless-stopped + + # Geoserver backend + geoserver: + image: geonode/geoserver:2.23.0 + container_name: geoserver4${COMPOSE_PROJECT_NAME} + healthcheck: + test: "curl -m 10 --fail --silent --write-out 'HTTP CODE : %{http_code}\n' --output /dev/null http://geoserver:8080/geoserver/ows" + start_period: 60s + interval: 60s + timeout: 10s + retries: 2 + env_file: + - .env + ports: + - "8080:8080" + volumes: + - statics:/mnt/volumes/statics + - geoserver-data-dir:/geoserver_data/data + - backup-restore:/backup_restore + - data:/data + - tmp:/tmp + restart: unless-stopped + depends_on: + data-dir-conf: + condition: service_healthy + django: + condition: service_healthy + + data-dir-conf: + image: geonode/geoserver_data:2.23.0 + container_name: gsconf4${COMPOSE_PROJECT_NAME} + entrypoint: sleep infinity + volumes: + - geoserver-data-dir:/geoserver_data/data + restart: unless-stopped + healthcheck: + test: "ls -A '/geoserver_data/data' | wc -l" + + # PostGIS database. + db: + # use geonode official postgis 15 image + image: geonode/postgis:15 + command: postgres -c "max_connections=${POSTGRESQL_MAX_CONNECTIONS}" + container_name: db4${COMPOSE_PROJECT_NAME} + env_file: + - .env + volumes: + - dbdata:/var/lib/postgresql/data + - dbbackups:/pg_backups + restart: unless-stopped + healthcheck: + test: "pg_isready -d postgres -U postgres" + # uncomment to enable remote connections to postgres + #ports: + # - "5432:5432" + + # Vanilla RabbitMQ service. This is needed by celery + rabbitmq: + image: rabbitmq:3-alpine + container_name: rabbitmq4${COMPOSE_PROJECT_NAME} + volumes: + - rabbitmq:/var/lib/rabbitmq + restart: unless-stopped + +volumes: + statics: + name: ${COMPOSE_PROJECT_NAME}-statics + nginx-confd: + name: ${COMPOSE_PROJECT_NAME}-nginxconfd + nginx-certificates: + name: ${COMPOSE_PROJECT_NAME}-nginxcerts + geoserver-data-dir: + name: ${COMPOSE_PROJECT_NAME}-gsdatadir + dbdata: + name: ${COMPOSE_PROJECT_NAME}-dbdata + dbbackups: + name: ${COMPOSE_PROJECT_NAME}-dbbackups + backup-restore: + name: ${COMPOSE_PROJECT_NAME}-backup-restore + data: + name: ${COMPOSE_PROJECT_NAME}-data + tmp: + name: ${COMPOSE_PROJECT_NAME}-tmp + rabbitmq: + name: ${COMPOSE_PROJECT_NAME}-rabbitmq diff --git a/docker-compose-test.yml b/docker-compose-test.yml index de398f099a7..762183a5eac 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -3,8 +3,8 @@ version: '3.9' # Common Django template for GeoNode and Celery services below x-common-django: &default-common-django - image: geonode/geonode:4.0 - restart: on-failure + image: geonode/geonode:latest-ubuntu-22.10 + restart: unless-stopped env_file: - .env_test volumes: @@ -17,8 +17,6 @@ x-common-django: depends_on: db: condition: service_healthy - geoserver: - condition: service_healthy services: @@ -30,11 +28,11 @@ services: dockerfile: Dockerfile container_name: django4${COMPOSE_PROJECT_NAME} healthcheck: - test: "curl --fail --silent --write-out 'HTTP CODE : %{http_code}\n' --output /dev/null http://127.0.0.1:8001/" + test: "curl -m 10 --fail --silent --write-out 'HTTP CODE : %{http_code}\n' --output /dev/null http://django:8000/" start_period: 60s interval: 60s timeout: 10s - retries: 10 + retries: 2 environment: - IS_CELERY=False entrypoint: ["/usr/src/geonode/entrypoint.sh"] @@ -43,10 +41,10 @@ services: # Celery worker that executes celery tasks created by Django. celery: << : *default-common-django - image: geonode/geonode:4.0 container_name: celery4${COMPOSE_PROJECT_NAME} depends_on: - - django + django: + condition: service_healthy environment: - IS_CELERY=True entrypoint: ["/usr/src/geonode/entrypoint.sh"] @@ -54,15 +52,12 @@ services: # Nginx is serving django static and media files and proxies to django and geonode geonode: - image: geonode/nginx:4.0 + image: geonode/nginx:1.25.1 build: ./scripts/docker/nginx/ container_name: nginx4${COMPOSE_PROJECT_NAME} + env_file: + - .env_test environment: - - HTTPS_HOST=${HTTPS_HOST} - - HTTP_HOST=${HTTP_HOST} - - HTTPS_PORT=${HTTPS_PORT} - - HTTP_PORT=${HTTP_PORT} - - LETSENCRYPT_MODE=${LETSENCRYPT_MODE} - RESOLVER=127.0.0.11 ports: - "${HTTP_PORT}:80" @@ -71,46 +66,45 @@ services: - nginx-confd:/etc/nginx - nginx-certificates:/geonode-certificates - statics:/mnt/volumes/statics - restart: on-failure + restart: unless-stopped # Gets and installs letsencrypt certificates letsencrypt: - image: geonode/letsencrypt:4.0 + image: geonode/letsencrypt:latest build: ./scripts/docker/letsencrypt/ container_name: letsencrypt4${COMPOSE_PROJECT_NAME} - environment: - - HTTPS_HOST=${HTTPS_HOST} - - HTTP_HOST=${HTTP_HOST} - - ADMIN_EMAIL=${ADMIN_EMAIL} - - LETSENCRYPT_MODE=${LETSENCRYPT_MODE} + env_file: + - .env_test volumes: - nginx-certificates:/geonode-certificates - restart: on-failure + restart: unless-stopped # Geoserver backend geoserver: image: geonode/geoserver:2.23.0 container_name: geoserver4${COMPOSE_PROJECT_NAME} healthcheck: - test: "curl --fail --silent --write-out 'HTTP CODE : %{http_code}\n' --output /dev/null http://127.0.0.1:8080/geoserver/ows" + test: "curl -m 10 --fail --silent --write-out 'HTTP CODE : %{http_code}\n' --output /dev/null http://geoserver:8080/geoserver/ows" start_period: 60s interval: 60s timeout: 10s - retries: 10 + retries: 2 env_file: - .env_test + ports: + - "8080:8080" volumes: - statics:/mnt/volumes/statics - geoserver-data-dir:/geoserver_data/data - backup-restore:/backup_restore - data:/data - tmp:/tmp - restart: on-failure + restart: unless-stopped depends_on: - db: - condition: service_healthy data-dir-conf: condition: service_healthy + django: + condition: service_healthy data-dir-conf: image: geonode/geoserver_data:2.23.0 @@ -118,14 +112,14 @@ services: entrypoint: sleep infinity volumes: - geoserver-data-dir:/geoserver_data/data - restart: on-failure + restart: unless-stopped healthcheck: test: "ls -A '/geoserver_data/data' | wc -l" # PostGIS database. db: - # use geonode official postgis 13 image - image: geonode/postgis:13 + # use geonode official postgis 15 image + image: geonode/postgis:15 command: postgres -c "max_connections=${POSTGRESQL_MAX_CONNECTIONS}" container_name: db4${COMPOSE_PROJECT_NAME} env_file: @@ -133,7 +127,7 @@ services: volumes: - dbdata:/var/lib/postgresql/data - dbbackups:/pg_backups - restart: on-failure + restart: unless-stopped healthcheck: test: "pg_isready -d postgres -U postgres" # uncomment to enable remote connections to postgres @@ -142,11 +136,11 @@ services: # Vanilla RabbitMQ service. This is needed by celery rabbitmq: - image: rabbitmq:3.7-alpine + image: rabbitmq:3-alpine container_name: rabbitmq4${COMPOSE_PROJECT_NAME} volumes: - rabbitmq:/var/lib/rabbitmq - restart: on-failure + restart: unless-stopped volumes: statics: diff --git a/docker-compose.yml b/docker-compose.yml index a4fc63d9b88..853231d824a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,12 +3,11 @@ version: '3.9' # Common Django template for GeoNode and Celery services below x-common-django: &default-common-django - image: geonode/geonode:4.0 - restart: on-failure + image: geonode/geonode:latest-ubuntu-22.10 + restart: unless-stopped env_file: - .env volumes: - # - '.:/usr/src/geonode' - statics:/mnt/volumes/statics - geoserver-data-dir:/geoserver_data/data - backup-restore:/backup_restore @@ -17,24 +16,19 @@ x-common-django: depends_on: db: condition: service_healthy - geoserver: - condition: service_healthy services: # Our custom django application. It includes Geonode. django: << : *default-common-django - build: - context: ./ - dockerfile: Dockerfile container_name: django4${COMPOSE_PROJECT_NAME} healthcheck: - test: "curl --fail --silent --write-out 'HTTP CODE : %{http_code}\n' --output /dev/null http://127.0.0.1:8001/" + test: "curl -m 10 --fail --silent --write-out 'HTTP CODE : %{http_code}\n' --output /dev/null http://django:8000/" start_period: 60s interval: 60s timeout: 10s - retries: 10 + retries: 2 environment: - IS_CELERY=False entrypoint: ["/usr/src/geonode/entrypoint.sh"] @@ -43,10 +37,10 @@ services: # Celery worker that executes celery tasks created by Django. celery: << : *default-common-django - image: geonode/geonode:4.0 container_name: celery4${COMPOSE_PROJECT_NAME} depends_on: - - django + django: + condition: service_healthy environment: - IS_CELERY=True entrypoint: ["/usr/src/geonode/entrypoint.sh"] @@ -54,15 +48,12 @@ services: # Nginx is serving django static and media files and proxies to django and geonode geonode: - image: geonode/nginx:4.0 + image: geonode/nginx:1.25.1 build: ./scripts/docker/nginx/ container_name: nginx4${COMPOSE_PROJECT_NAME} + env_file: + - .env environment: - - HTTPS_HOST=${HTTPS_HOST} - - HTTP_HOST=${HTTP_HOST} - - HTTPS_PORT=${HTTPS_PORT} - - HTTP_PORT=${HTTP_PORT} - - LETSENCRYPT_MODE=${LETSENCRYPT_MODE} - RESOLVER=127.0.0.11 ports: - "${HTTP_PORT}:80" @@ -71,46 +62,45 @@ services: - nginx-confd:/etc/nginx - nginx-certificates:/geonode-certificates - statics:/mnt/volumes/statics - restart: on-failure + restart: unless-stopped # Gets and installs letsencrypt certificates letsencrypt: - image: geonode/letsencrypt:4.0 + image: geonode/letsencrypt:latest build: ./scripts/docker/letsencrypt/ container_name: letsencrypt4${COMPOSE_PROJECT_NAME} - environment: - - HTTPS_HOST=${HTTPS_HOST} - - HTTP_HOST=${HTTP_HOST} - - ADMIN_EMAIL=${ADMIN_EMAIL} - - LETSENCRYPT_MODE=${LETSENCRYPT_MODE} + env_file: + - .env volumes: - nginx-certificates:/geonode-certificates - restart: on-failure + restart: unless-stopped # Geoserver backend geoserver: image: geonode/geoserver:2.23.0 container_name: geoserver4${COMPOSE_PROJECT_NAME} healthcheck: - test: "curl --fail --silent --write-out 'HTTP CODE : %{http_code}\n' --output /dev/null http://127.0.0.1:8080/geoserver/ows" + test: "curl -m 10 --fail --silent --write-out 'HTTP CODE : %{http_code}\n' --output /dev/null http://geoserver:8080/geoserver/ows" start_period: 60s interval: 60s timeout: 10s - retries: 10 + retries: 2 env_file: - .env + ports: + - "8080:8080" volumes: - statics:/mnt/volumes/statics - geoserver-data-dir:/geoserver_data/data - backup-restore:/backup_restore - data:/data - tmp:/tmp - restart: on-failure + restart: unless-stopped depends_on: - db: - condition: service_healthy data-dir-conf: condition: service_healthy + django: + condition: service_healthy data-dir-conf: image: geonode/geoserver_data:2.23.0 @@ -118,14 +108,14 @@ services: entrypoint: sleep infinity volumes: - geoserver-data-dir:/geoserver_data/data - restart: on-failure + restart: unless-stopped healthcheck: test: "ls -A '/geoserver_data/data' | wc -l" # PostGIS database. db: - # use geonode official postgis 13 image - image: geonode/postgis:13 + # use geonode official postgis 15 image + image: geonode/postgis:15 command: postgres -c "max_connections=${POSTGRESQL_MAX_CONNECTIONS}" container_name: db4${COMPOSE_PROJECT_NAME} env_file: @@ -133,7 +123,7 @@ services: volumes: - dbdata:/var/lib/postgresql/data - dbbackups:/pg_backups - restart: on-failure + restart: unless-stopped healthcheck: test: "pg_isready -d postgres -U postgres" # uncomment to enable remote connections to postgres @@ -142,11 +132,11 @@ services: # Vanilla RabbitMQ service. This is needed by celery rabbitmq: - image: rabbitmq:3.7-alpine + image: rabbitmq:3-alpine container_name: rabbitmq4${COMPOSE_PROJECT_NAME} volumes: - rabbitmq:/var/lib/rabbitmq - restart: on-failure + restart: unless-stopped volumes: statics: diff --git a/entrypoint.sh b/entrypoint.sh index 4e394cdc703..6bb062b910e 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -41,7 +41,7 @@ echo MONITORING_HOST_NAME=$MONITORING_HOST_NAME echo MONITORING_SERVICE_NAME=$MONITORING_SERVICE_NAME echo MONITORING_DATA_TTL=$MONITORING_DATA_TTL -invoke waitfordbs +# invoke waitfordbs cmd="$@" @@ -54,7 +54,6 @@ else invoke prepare if [ ${FORCE_REINIT} = "true" ] || [ ${FORCE_REINIT} = "True" ] || [ ! -e "/mnt/volumes/statics/geonode_init.lock" ]; then - invoke updategeoip invoke fixtures invoke monitoringfixture invoke initialized @@ -62,8 +61,6 @@ else fi invoke statics - invoke waitforgeoserver - invoke geoserverfixture echo "Executing UWSGI server $cmd for Production" fi diff --git a/geonode/__init__.py b/geonode/__init__.py index e221b120dab..9b1e32fbb4d 100644 --- a/geonode/__init__.py +++ b/geonode/__init__.py @@ -19,7 +19,7 @@ import os -__version__ = (4, 1, 0, "dev", 0) +__version__ = (4, 2, 0, "dev", 0) default_app_config = "geonode.apps.AppConfig" diff --git a/geonode/api/__init__.py b/geonode/api/__init__.py index 79177e00bdd..55b0b0cb9fd 100644 --- a/geonode/api/__init__.py +++ b/geonode/api/__init__.py @@ -16,3 +16,4 @@ # along with this program. If not, see . # ######################################################################### +default_app_config = "geonode.api.apps.GeoNodeApiAppConfig" diff --git a/geonode/api/api.py b/geonode/api/api.py index 0663176dec9..4303c0caa4b 100644 --- a/geonode/api/api.py +++ b/geonode/api/api.py @@ -584,6 +584,16 @@ class OwnersResource(TypeFilteredResource): full_name = fields.CharField(null=True) + def apply_filters(self, request, applicable_filters): + """filter by group if applicable by group functionality""" + + semi_filtered = super().apply_filters(request, applicable_filters) + + if request.user and not request.user.is_superuser: + semi_filtered = get_available_users(request.user) + + return semi_filtered + def dehydrate_full_name(self, bundle): return bundle.obj.get_full_name() or bundle.obj.username diff --git a/geonode/api/apps.py b/geonode/api/apps.py new file mode 100644 index 00000000000..bf3d85231a9 --- /dev/null +++ b/geonode/api/apps.py @@ -0,0 +1,24 @@ +######################################################################### +# +# Copyright (C) 2023 OSGeo +# +# 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 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### +from django.apps import AppConfig + + +class GeoNodeApiAppConfig(AppConfig): + name = "geonode.api" + verbose_name = "GeoNode API v1" diff --git a/geonode/api/tests.py b/geonode/api/tests.py index e8fb96626bb..39a428c21fb 100644 --- a/geonode/api/tests.py +++ b/geonode/api/tests.py @@ -321,7 +321,7 @@ def test_owners_lockdown(self): self.api_client.client.login(username="bobby", password="bob") resp = self.api_client.get(filter_url) self.assertValidJSONResponse(resp) - self.assertEqual(len(self.deserialize(resp)["objects"]), 9) + self.assertEqual(len(self.deserialize(resp)["objects"]), 6) # Returns limitted info about other users bobby = get_user_model().objects.get(username="bobby") owners = self.deserialize(resp)["objects"] @@ -332,6 +332,12 @@ def test_owners_lockdown(self): self.assertIsNone(owner.get("email")) self.assertIsNone(owner.get("first_name")) + # now test with logged in admin + self.api_client.client.login(username="admin", password="admin") + resp = self.api_client.get(filter_url) + self.assertValidJSONResponse(resp) + self.assertEqual(len(self.deserialize(resp)["objects"]), 9) + @override_settings(API_LOCKDOWN=True) def test_groups_lockdown(self): groups_list_url = reverse("api_dispatch_list", kwargs={"api_name": "api", "resource_name": "groups"}) diff --git a/geonode/base/api/filters.py b/geonode/base/api/filters.py index f8691e0178d..a2428bfdc65 100644 --- a/geonode/base/api/filters.py +++ b/geonode/base/api/filters.py @@ -56,11 +56,16 @@ class TKeywordsFilter(BaseFilterBackend): """ def filter_queryset(self, request, queryset, view): - return ( - self.filter_queryset_GROUP(request, queryset, view) - if "force_and" not in request.GET - else self.filter_queryset_AND(request, queryset, view) - ) + # we must make the GET mutable since in the filters, some queryparams are popped + request.GET._mutable = True + try: + return ( + self.filter_queryset_GROUP(request, queryset, view) + if "force_and" not in request.GET + else self.filter_queryset_AND(request, queryset, view) + ) + finally: + request.GET._mutable = False def filter_queryset_AND(self, request, queryset, view): """ diff --git a/geonode/base/api/tests.py b/geonode/base/api/tests.py index b67f6d389d3..d9d8e8d7cea 100644 --- a/geonode/base/api/tests.py +++ b/geonode/base/api/tests.py @@ -629,15 +629,15 @@ def test_base_resources(self): }, { "name": "", - "slug": "http-localhost-8001-thesaurus-no-about-thesauro-38", - "uri": "http://localhost:8001//thesaurus/no-about-thesauro#38", + "slug": "http-localhost-8000-thesaurus-no-about-thesauro-38", + "uri": "http://localhost:8000//thesaurus/no-about-thesauro#38", "thesaurus": {"name": "Thesauro without the about", "slug": "no-about-thesauro", "uri": ""}, "i18n": {}, }, { "name": "bar_keyword", - "slug": "http-localhost-8001-thesaurus-no-about-thesauro-bar-keyword", - "uri": "http://localhost:8001//thesaurus/no-about-thesauro#bar_keyword", + "slug": "http-localhost-8000-thesaurus-no-about-thesauro-bar-keyword", + "uri": "http://localhost:8000//thesaurus/no-about-thesauro#bar_keyword", "thesaurus": {"name": "Thesauro without the about", "slug": "no-about-thesauro", "uri": ""}, "i18n": {}, }, diff --git a/geonode/base/fixtures/test_xml.xml b/geonode/base/fixtures/test_xml.xml index 34a2af08443..1b990340631 100755 --- a/geonode/base/fixtures/test_xml.xml +++ b/geonode/base/fixtures/test_xml.xml @@ -406,7 +406,7 @@ - + vector eng diff --git a/geonode/base/models.py b/geonode/base/models.py index 6da70b6994c..1efe7288b0d 100644 --- a/geonode/base/models.py +++ b/geonode/base/models.py @@ -41,7 +41,7 @@ from django.contrib.auth import get_user_model from django.db.models.fields.json import JSONField from django.utils.functional import cached_property, classproperty -from django.contrib.gis.geos import Polygon, Point +from django.contrib.gis.geos import GEOSGeometry, Polygon, Point from django.contrib.gis.db.models import PolygonField from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ @@ -204,6 +204,21 @@ def geographic_bounding_box(self): """BBOX is in the format: [x0,x1,y0,y1].""" return bbox_to_wkt(self.bbox_x0, self.bbox_x1, self.bbox_y0, self.bbox_y1, srid=self.srid) + @property + def geom(self): + srid, wkt = self.geographic_bounding_box.split(";") + srid = re.findall(r"\d+", srid) + geom = GEOSGeometry(wkt, srid=int(srid[0])) + geom.transform(4326) + return geom + + def is_assignable_to_geom(self, extent_geom: GEOSGeometry): + region_geom = self.geom + + if region_geom.contains(extent_geom) or region_geom.overlaps(extent_geom): + return True + return False + class Meta: ordering = ("name",) verbose_name_plural = "Metadata Regions" diff --git a/geonode/base/tests.py b/geonode/base/tests.py index 7ee9a49b557..3c6cfb9b4f2 100644 --- a/geonode/base/tests.py +++ b/geonode/base/tests.py @@ -48,13 +48,14 @@ Menu, MenuItem, Configuration, + Region, TopicCategory, Thesaurus, ThesaurusKeyword, generate_thesaurus_reference, ) from django.conf import settings -from django.contrib.gis.geos import Polygon +from django.contrib.gis.geos import Polygon, GEOSGeometry from django.template import Template, Context from django.contrib.auth import get_user_model from geonode.storage.manager import storage_manager @@ -1124,3 +1125,41 @@ def test_keyword_raise_db_error(self, add_root_mocked): "Error during the keyword creation for keyword: keyword2", [x.message for x in _log.records], ) + + +class TestRegions(GeoNodeBaseTestSupport): + def setUp(self): + self.dataset_inside_region = GEOSGeometry( + "POLYGON ((-4.01799226543944599 57.18451093931114571, 8.89409253052255622 56.91828238681708285, \ + 9.29343535926363984 47.73339732577194638, -3.75176371294537603 48.13274015451304422, \ + -4.01799226543944599 57.18451093931114571))", + srid=4326, + ) + + self.dataset_overlapping_region = GEOSGeometry( + "POLYGON ((15.28357779038003628 33.6232840435866791, 28.19566258634203848 33.35705549109261625, \ + 28.5950054150831221 24.17217043004747978, 15.54980634287410624 24.57151325878857762, \ + 15.28357779038003628 33.6232840435866791))", + srid=4326, + ) + + self.dataset_outside_region = GEOSGeometry( + "POLYGON ((-3.75176371294537603 23.10725622007123548, 9.16032108301662618 22.84102766757717262, \ + 9.5596639117577098 13.65614260653203615, -3.48553516045130607 14.05548543527313399, \ + -3.75176371294537603 23.10725622007123548))", + srid=4326, + ) + + def test_region_assignment_for_extent(self): + region = Region.objects.get(code="EUR") + + self.assertTrue( + region.is_assignable_to_geom(self.dataset_inside_region), "Extent inside a region shouldn't be assigned" + ) + self.assertTrue( + region.is_assignable_to_geom(self.dataset_overlapping_region), + "Extent overlapping a region should be assigned", + ) + self.assertFalse( + region.is_assignable_to_geom(self.dataset_outside_region), "Extent outside a region should be assigned" + ) diff --git a/geonode/catalogue/__init__.py b/geonode/catalogue/__init__.py index 3b577289766..d1b9c72dbff 100644 --- a/geonode/catalogue/__init__.py +++ b/geonode/catalogue/__init__.py @@ -25,6 +25,7 @@ from django.core.exceptions import ImproperlyConfigured from importlib import import_module +default_app_config = "geonode.catalogue.apps.GeoNodeCatalogueAppConfig" DEFAULT_CATALOGUE_ALIAS = "default" # GeoNode uses this if the CATALOGUE setting is empty (None). diff --git a/geonode/catalogue/apps.py b/geonode/catalogue/apps.py new file mode 100644 index 00000000000..961648a7945 --- /dev/null +++ b/geonode/catalogue/apps.py @@ -0,0 +1,24 @@ +######################################################################### +# +# Copyright (C) 2023 OSGeo +# +# 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 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### +from django.apps import AppConfig + + +class GeoNodeCatalogueAppConfig(AppConfig): + name = "geonode.catalogue" + verbose_name = "GeoNode CSW Catalogue" diff --git a/geonode/catalogue/metadataxsl/__init__.py b/geonode/catalogue/metadataxsl/__init__.py index 79177e00bdd..0143f08e23a 100644 --- a/geonode/catalogue/metadataxsl/__init__.py +++ b/geonode/catalogue/metadataxsl/__init__.py @@ -16,3 +16,4 @@ # along with this program. If not, see . # ######################################################################### +default_app_config = "geonode.catalogue.metadataxsl.apps.GeoNodeCatalogueMetadataxslAppConfig" diff --git a/geonode/catalogue/metadataxsl/apps.py b/geonode/catalogue/metadataxsl/apps.py new file mode 100644 index 00000000000..af8e4d6001f --- /dev/null +++ b/geonode/catalogue/metadataxsl/apps.py @@ -0,0 +1,24 @@ +######################################################################### +# +# Copyright (C) 2023 OSGeo +# +# 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 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### +from django.apps import AppConfig + + +class GeoNodeCatalogueMetadataxslAppConfig(AppConfig): + name = "geonode.catalogue.metadataxsl" + verbose_name = "GeoNode Catalogue metadataxsl" diff --git a/geonode/catalogue/templates/catalogue/full_metadata.xml b/geonode/catalogue/templates/catalogue/full_metadata.xml index 39bffae1d3c..955f81c3e55 100644 --- a/geonode/catalogue/templates/catalogue/full_metadata.xml +++ b/geonode/catalogue/templates/catalogue/full_metadata.xml @@ -436,9 +436,11 @@ + {% if layer.spatial_representation_type %} {{layer.spatial_representation_type.identifier}} + {% endif %} {{layer.language}} diff --git a/geonode/celery_app.py b/geonode/celery_app.py index 0e3c411c2ad..5f9b5e95624 100644 --- a/geonode/celery_app.py +++ b/geonode/celery_app.py @@ -49,14 +49,14 @@ def setup_periodic_tasks(sender, **kwargs): @app.task( bind=True, - name='{{project_name}}.test', + name='geonode.test', queue='default') def test(arg): _log(arg) @app.task( bind=True, - name='{{project_name}}.debug_task', + name='geonode.debug_task', queue='default') def debug_task(self): _log(f"Request: {self.request}") diff --git a/geonode/documents/admin.py b/geonode/documents/admin.py index 36511c7d331..9af881f0e6d 100644 --- a/geonode/documents/admin.py +++ b/geonode/documents/admin.py @@ -36,6 +36,7 @@ class Meta(ResourceBaseAdminForm.Meta): class DocumentAdmin(TabbedTranslationAdmin): + exclude = ("ll_bbox_polygon", "bbox_polygon", "srid") list_display = ("id", "title", "date", "category", "group", "is_approved", "is_published", "metadata_completeness") list_display_links = ("id",) list_editable = ("title", "category", "group", "is_approved", "is_published") @@ -55,6 +56,7 @@ class DocumentAdmin(TabbedTranslationAdmin): "is_approved", "is_published", ) + readonly_fields = ("geographic_bounding_box",) date_hierarchy = "date" form = DocumentAdminForm actions = [metadata_batch_edit] diff --git a/geonode/documents/api/tests.py b/geonode/documents/api/tests.py index dbb3b13588b..1951da4e2e1 100644 --- a/geonode/documents/api/tests.py +++ b/geonode/documents/api/tests.py @@ -28,6 +28,7 @@ from geonode import settings from geonode.base.populate_test_data import create_models +from geonode.base.enumerations import SOURCE_TYPE_REMOTE from geonode.documents.models import Document logger = logging.getLogger(__name__) @@ -172,6 +173,22 @@ def test_creation_from_url_should_create_the_doc(self): created_doc_url = actual.json().get("document", {}).get("doc_url", "") self.assertEqual(created_doc_url, doc_url) + def test_remote_document_is_marked_remote(self): + """Tests creating an external document set its sourcetype to REMOTE.""" + self.client.force_login(self.admin) + doc_url = "https://example.com/image" + payload = { + "document": { + "title": "A remote document is remote", + "doc_url": doc_url, + "extension": "jpeg", + } + } + actual = self.client.post(self.url, data=payload, format="json") + self.assertEqual(201, actual.status_code) + created_sourcetype = actual.json().get("document", {}).get("sourcetype", "") + self.assertEqual(created_sourcetype, SOURCE_TYPE_REMOTE) + def test_either_path_or_url_doc(self): """ If file_path is not available, should raise error diff --git a/geonode/documents/api/views.py b/geonode/documents/api/views.py index 05159272363..720cb7639ba 100644 --- a/geonode/documents/api/views.py +++ b/geonode/documents/api/views.py @@ -31,6 +31,7 @@ from geonode.base.api.filters import DynamicSearchFilter, ExtentFilter from geonode.base.api.pagination import GeoNodeApiPagination from geonode.base.api.permissions import UserHasPerms +from geonode.base import enumerations from geonode.documents.api.exceptions import DocumentException from geonode.documents.models import Document @@ -123,6 +124,7 @@ def perform_create(self, serializer): payload["files"] = [manager.get_retrieved_paths().get("base_file")] if doc_url: payload["doc_url"] = doc_url + payload["sourcetype"] = enumerations.SOURCE_TYPE_REMOTE resource = serializer.save(**payload) diff --git a/geonode/documents/exif/__init__.py b/geonode/documents/exif/__init__.py index 79177e00bdd..bc045338385 100644 --- a/geonode/documents/exif/__init__.py +++ b/geonode/documents/exif/__init__.py @@ -16,3 +16,4 @@ # along with this program. If not, see . # ######################################################################### +default_app_config = "geonode.documents.exif.apps.GeoNodeDocumentsExifAppConfig" diff --git a/geonode/documents/exif/apps.py b/geonode/documents/exif/apps.py new file mode 100644 index 00000000000..eaeb3e2a031 --- /dev/null +++ b/geonode/documents/exif/apps.py @@ -0,0 +1,24 @@ +######################################################################### +# +# Copyright (C) 2023 OSGeo +# +# 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 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### +from django.apps import AppConfig + + +class GeoNodeDocumentsExifAppConfig(AppConfig): + name = "geonode.documents.exif" + verbose_name = "GeoNode Documents Exif" diff --git a/geonode/documents/models.py b/geonode/documents/models.py index e9a81dac1dc..402194f9908 100644 --- a/geonode/documents/models.py +++ b/geonode/documents/models.py @@ -116,17 +116,17 @@ def mime_type(self): @property def is_audio(self): AUDIOTYPES = [_e for _e, _t in DOCUMENT_TYPE_MAP.items() if _t == "audio"] - return self.is_file and self.extension.lower() in AUDIOTYPES + return self.extension and self.extension.lower() in AUDIOTYPES @property def is_image(self): IMGTYPES = [_e for _e, _t in DOCUMENT_TYPE_MAP.items() if _t == "image"] - return self.is_file and self.extension.lower() in IMGTYPES + return self.extension and self.extension.lower() in IMGTYPES @property def is_video(self): VIDEOTYPES = [_e for _e, _t in DOCUMENT_TYPE_MAP.items() if _t == "video"] - return self.is_file and self.extension.lower() in VIDEOTYPES + return self.extension and self.extension.lower() in VIDEOTYPES @property def class_name(self): diff --git a/geonode/documents/tasks.py b/geonode/documents/tasks.py index a5ddb247a19..e0ed9617354 100644 --- a/geonode/documents/tasks.py +++ b/geonode/documents/tasks.py @@ -25,7 +25,7 @@ from celery.utils.log import get_task_logger from geonode.celery_app import app -from geonode.storage.manager import storage_manager +from geonode.storage.manager import StorageManager from ..base.models import ResourceBase from .models import Document @@ -90,6 +90,8 @@ def create_document_thumbnail(self, object_id): """ logger.debug(f"Generating thumbnail for document #{object_id}.") + storage_manager = StorageManager() + try: document = Document.objects.get(id=object_id) except Document.DoesNotExist: @@ -98,33 +100,52 @@ def create_document_thumbnail(self, object_id): image_file = None thumbnail_content = None + remove_tmp_file = False centering = (0.5, 0.5) - if document.is_image: - dname = storage_manager.path(document.files[0]) - if storage_manager.exists(dname): - image_file = storage_manager.open(dname, "rb") + doc_path = None + if document.files: + doc_path = storage_manager.path(document.files[0]) + elif document.doc_url: + doc_path = document.doc_url + remove_tmp_file = True + if document.is_image: try: - image = Image.open(image_file) - with io.BytesIO() as output: - image.save(output, format="PNG") - thumbnail_content = output.getvalue() - output.close() + image_file = storage_manager.open(doc_path) except Exception as e: - logger.debug(f"Could not generate thumbnail: {e}") - finally: - if image_file is not None: - image_file.close() - - elif doc_renderer.supports(document.files[0]): + logger.debug(f"Could not generate thumbnail from remote document {document.doc_url}: {e}") + + if image_file: + try: + image = Image.open(image_file) + with io.BytesIO() as output: + image.save(output, format="PNG") + thumbnail_content = output.getvalue() + output.close() + except Exception as e: + logger.debug(f"Could not generate thumbnail: {e}") + finally: + if image_file is not None: + image_file.close() + if remove_tmp_file: + storage_manager.delete(doc_path) + + elif doc_renderer.supports(doc_path): + # in case it's a remote document we want to retrieve it first + if document.doc_url: + doc_path = storage_manager.open(doc_path).name + remove_tmp_file = True try: - thumbnail_content = doc_renderer.render(document.files[0]) - preferred_centering = doc_renderer.preferred_crop_centering(document.files[0]) + thumbnail_content = doc_renderer.render(doc_path) + preferred_centering = doc_renderer.preferred_crop_centering(doc_path) if preferred_centering is not None: centering = preferred_centering except Exception as e: print(e) + finally: + if remove_tmp_file: + storage_manager.delete(doc_path) if not thumbnail_content: logger.warning(f"Thumbnail for document #{object_id} empty.") ResourceBase.objects.filter(id=document.id).update(thumbnail_url=None) diff --git a/geonode/documents/tests.py b/geonode/documents/tests.py index 369256492fa..8bf91a88529 100644 --- a/geonode/documents/tests.py +++ b/geonode/documents/tests.py @@ -47,6 +47,7 @@ from geonode.layers.models import Dataset from geonode.compat import ensure_string from geonode.base.models import License, Region +from geonode.base.enumerations import SOURCE_TYPE_REMOTE from geonode.documents import DocumentsAppConfig from geonode.resource.manager import resource_manager from geonode.documents.forms import DocumentFormMixin @@ -137,6 +138,21 @@ def test_create_document_with_rel(self, thumb): self.assertEqual(Document.objects.get(pk=c.id).title, "theimg") self.assertEqual(DocumentResourceLink.objects.get(pk=_d.id).object_id, m.id) + def test_remote_document_is_marked_remote(self): + """Tests creating an external document set its sourcetype to REMOTE.""" + self.client.login(username="admin", password="admin") + form_data = { + "title": "A remote document through form is remote", + "doc_url": "http://www.geonode.org/map.pdf", + } + + response = self.client.post(reverse("document_upload"), data=form_data) + + self.assertEqual(response.status_code, 302) + + d = Document.objects.get(title="A remote document through form is remote") + self.assertEqual(d.sourcetype, SOURCE_TYPE_REMOTE) + def test_create_document_url(self): """Tests creating an external document instead of a file.""" @@ -253,7 +269,7 @@ def test_non_image_documents_thumbnail(self): finally: Document.objects.filter(title="Non img File Doc").delete() - def test_image_documents_thumbnail(self): + def test_documents_thumbnail(self): self.client.login(username="admin", password="admin") try: # test image doc @@ -274,6 +290,22 @@ def test_image_documents_thumbnail(self): self.assertEqual(file.size, (400, 200)) # check thumbnail qualty and extention self.assertEqual(file.format, "JPEG") + data = { + "title": "Remote img File Doc", + "doc_url": "https://raw.githubusercontent.com/GeoNode/geonode/master/geonode/documents/tests/data/img.gif", + "extension": "gif", + } + with self.settings(THUMBNAIL_SIZE={"width": 400, "height": 200}): + self.client.post(reverse("document_upload"), data=data) + d = Document.objects.get(title="Remote img File Doc") + self.assertIsNotNone(d.thumbnail_url) + thumb_file = os.path.join( + settings.MEDIA_ROOT, f"thumbs/{os.path.basename(urlparse(d.thumbnail_url).path)}" + ) + file = Image.open(thumb_file) + self.assertEqual(file.size, (400, 200)) + # check thumbnail qualty and extention + self.assertEqual(file.format, "JPEG") # test pdf doc with open(os.path.join(f"{self.project_root}", "tests/data/pdf_doc.pdf"), "rb") as f: data = { diff --git a/geonode/documents/views.py b/geonode/documents/views.py index 3ba50d2f6bc..ed4355fcee2 100644 --- a/geonode/documents/views.py +++ b/geonode/documents/views.py @@ -51,6 +51,7 @@ from geonode.security.utils import get_user_visible_groups, AdvancedSecurityWorkflowManager from geonode.base.forms import CategoryForm, TKeywordForm, ThesaurusAvailableForm from geonode.base.models import Thesaurus, TopicCategory +from geonode.base import enumerations from .utils import get_download_response @@ -184,7 +185,10 @@ def form_valid(self, form): None, resource_type=Document, defaults=dict( - owner=self.request.user, doc_url=doc_form.pop("doc_url", None), title=doc_form.pop("title", None) + owner=self.request.user, + doc_url=doc_form.pop("doc_url", None), + title=doc_form.pop("title", None), + sourcetype=enumerations.SOURCE_TYPE_REMOTE, ), ) diff --git a/geonode/facets/models.py b/geonode/facets/models.py index d271c188c9d..0270720e346 100644 --- a/geonode/facets/models.py +++ b/geonode/facets/models.py @@ -28,6 +28,8 @@ FACET_TYPE_USER = "user" FACET_TYPE_THESAURUS = "thesaurus" FACET_TYPE_CATEGORY = "category" +FACET_TYPE_BASE = "base" +FACET_TYPE_KEYWORD = "keyword" logger = logging.getLogger(__name__) @@ -37,6 +39,9 @@ class FacetProvider: Provides access to the facet information and the related topics """ + def __init__(self, **kwargs): + self.config = kwargs.get("config", {}).copy() + def __str__(self): return f"{self.__class__.__name__}[{self.name}]" @@ -49,7 +54,7 @@ def name(self) -> str: """ self.get_info()["name"] - def get_info(self, lang="en") -> dict: + def get_info(self, lang="en", **kwargs) -> dict: """ Get the basic info for this provider, as a dict with these keys: - 'name': the name of the provider (the one returned by name()) @@ -66,7 +71,14 @@ def get_info(self, lang="en") -> dict: pass def get_facet_items( - self, queryset, start: int = 0, end: int = DEFAULT_FACET_PAGE_SIZE, lang="en", topic_contains: str = None + self, + queryset, + start: int = 0, + end: int = DEFAULT_FACET_PAGE_SIZE, + lang="en", + topic_contains: str = None, + keys: set = {}, + **kwargs, ) -> (int, list): """ Return the items of the facets, in a tuple: @@ -82,10 +94,25 @@ def get_facet_items( :param end: int: pagination, the index of the last returned item :param lang: the preferred language for the labels :param topic_contains: only returns matching topics + :param keys: only returns topics with given keys, even if their count is 0 :return: a tuple int:total count of record, list of items """ pass + def get_topics(self, keys: list, lang="en", **kwargs) -> list: + """ + Return the topics with the requested ids as a list + - list, topic records. A topic record is a dict having these keys: + - key: the key of the items that should be used for filtering + - label: a generic label for the item; the client should try and localize it whenever possible + - localized_label: a localized label for the item + - other facet specific keys + :param keys: the list of the keys of the topics, as returned by the get_facet_items() method + :param lang: the preferred language for the labels + :return: list of items + """ + pass + @classmethod def register(cls, registry, **kwargs) -> None: """ @@ -111,10 +138,10 @@ def _load_facets_configuration(self) -> None: logger.info("Initializing Facets") - if providers := getattr(settings, "FACET_PROVIDERS", []): - _providers = [import_string(module_path) for module_path in providers] - for provider in _providers: - provider.register(self) + for providerconf in getattr(settings, "FACET_PROVIDERS", []): + clz = providerconf["class"] + provider = import_string(clz) + provider.register(self, config=providerconf.get("config", {})) def register_facet_provider(self, provider: FacetProvider): logger.info(f"Registering {provider}") diff --git a/geonode/facets/providers/baseinfo.py b/geonode/facets/providers/baseinfo.py new file mode 100644 index 00000000000..f81e7583c66 --- /dev/null +++ b/geonode/facets/providers/baseinfo.py @@ -0,0 +1,151 @@ +######################################################################### +# +# Copyright (C) 2023 Open Source Geospatial Foundation - all rights reserved +# +# 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 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### + +import logging + +from django.db.models import Count + +from geonode.facets.models import FacetProvider, DEFAULT_FACET_PAGE_SIZE, FACET_TYPE_BASE + +logger = logging.getLogger(__name__) + + +class ResourceTypeFacetProvider(FacetProvider): + """ + Implements faceting for resources' type and subtype + """ + + @property + def name(self) -> str: + return "resourcetype" + + def get_info(self, lang="en", **kwargs) -> dict: + return { + "name": self.name, + "key": "filter{resource_type.in}", # deprecated + "filter": "filter{resource_type.in}", + "label": "Resource type", + "type": FACET_TYPE_BASE, + "hierarchical": True, + } + + def get_facet_items( + self, + queryset=None, + start: int = 0, + end: int = DEFAULT_FACET_PAGE_SIZE, + lang="en", + topic_contains: str = None, + keys: set = {}, + **kwargs, + ) -> (int, list): + logger.debug("Retrieving facets for %s", self.name) + + if topic_contains: + logger.warning(f"Facet {self.name} does not support topic_contains filtering") + + q = queryset.values("resource_type", "subtype") + q = q.annotate(ctype=Count("resource_type"), csub=Count("subtype")) + q = q.order_by() + + # aggregate subtypes into rtypes + tree = {} + for r in q.all(): + res_type = r["resource_type"] + t = tree.get(res_type, {"cnt": 0, "sub": {}}) + t["cnt"] += r["ctype"] + if sub := r["subtype"]: + t["sub"][sub] = {"cnt": r["ctype"]} + tree[res_type] = t + + logger.info("Found %d main facets for %s", len(tree), self.name) + logger.debug(" ---> %s\n\n", q.query) + logger.debug(" ---> %r\n\n", q.all()) + + topics = [] + for rtype, info in tree.items(): + t = {"key": rtype, "label": rtype, "count": info["cnt"]} + if sub := info["sub"]: + children = [] + for stype, sinfo in sub.items(): + children.append({"key": stype, "label": stype, "count": sinfo["cnt"]}) + t["filter"] = "filter{subtype.in}" + t["items"] = sorted(children, reverse=True, key=lambda x: x["count"]) + topics.append(t) + + return len(topics), sorted(topics, reverse=True, key=lambda x: x["count"]) + + @classmethod + def register(cls, registry, **kwargs) -> None: + registry.register_facet_provider(ResourceTypeFacetProvider(**kwargs)) + + +class FeaturedFacetProvider(FacetProvider): + """ + Implements faceting for resources flagged as featured + """ + + @property + def name(self) -> str: + return "featured" + + def get_info(self, lang="en", **kwargs) -> dict: + return { + "name": self.name, + "key": "filter{featured}", + "label": "Featured", + "type": FACET_TYPE_BASE, + "hierarchical": False, + "order": 0, + } + + def get_facet_items( + self, + queryset=None, + start: int = 0, + end: int = DEFAULT_FACET_PAGE_SIZE, + lang="en", + topic_contains: str = None, + keys: set = {}, + **kwargs, + ) -> (int, list): + logger.debug("Retrieving facets for %s", self.name) + + if topic_contains: + logger.warning(f"Facet {self.name} does not support topic_contains filtering") + + q = queryset.values("featured").annotate(cnt=Count("featured")).order_by() + + logger.debug(" ---> %s\n\n", q.query) + logger.debug(" ---> %r\n\n", q.all()) + + topics = [ + { + "key": r["featured"], + "label": str(r["featured"]), + "count": r["cnt"], + } + for r in q[start:end] + ] + + return 2, topics + + @classmethod + def register(cls, registry, **kwargs) -> None: + registry.register_facet_provider(FeaturedFacetProvider(**kwargs)) diff --git a/geonode/facets/providers/category.py b/geonode/facets/providers/category.py index 3db78eb4d03..28b27749bbf 100644 --- a/geonode/facets/providers/category.py +++ b/geonode/facets/providers/category.py @@ -21,6 +21,7 @@ from django.db.models import Count +from geonode.base.models import TopicCategory from geonode.facets.models import FacetProvider, DEFAULT_FACET_PAGE_SIZE, FACET_TYPE_CATEGORY logger = logging.getLogger(__name__) @@ -35,14 +36,13 @@ class CategoryFacetProvider(FacetProvider): def name(self) -> str: return "category" - def get_info(self, lang="en") -> dict: + def get_info(self, lang="en", **kwargs) -> dict: return { "name": self.name, - "key": "filter{category__identifier}", + "key": "filter{category.identifier}", # deprecated + "filter": "filter{category.identifier}", "label": "Category", "type": FACET_TYPE_CATEGORY, - "hierarchical": False, - "order": 2, } def get_facet_items( @@ -52,13 +52,26 @@ def get_facet_items( end: int = DEFAULT_FACET_PAGE_SIZE, lang="en", topic_contains: str = None, + keys: set = {}, + **kwargs, ) -> (int, list): logger.debug("Retrieving facets for %s", self.name) - q = queryset.values("category__identifier", "category__gn_description", "category__fa_class") + filters = {"category__isnull": False} + if topic_contains: - q = q.filter(category__gn_description=topic_contains) - q = q.annotate(count=Count("owner")).order_by("-count") + filters["category__gn_description"] = topic_contains + + if keys: + logger.debug("Filtering by keys %r", keys) + filters["category__identifier__in"] = keys + + q = ( + queryset.values("category__identifier", "category__gn_description", "category__fa_class") + .filter(**filters) + .annotate(count=Count("owner")) + .order_by("-count") + ) cnt = q.count() @@ -78,6 +91,21 @@ def get_facet_items( return cnt, topics + def get_topics(self, keys: list, lang="en", **kwargs) -> list: + q = TopicCategory.objects.filter(identifier__in=keys) + + logger.debug(" ---> %s\n\n", q.query) + logger.debug(" ---> %r\n\n", q.all()) + + return [ + { + "key": r.identifier, + "label": r.gn_description, + "fa_class": r.fa_class, + } + for r in q.all() + ] + @classmethod def register(cls, registry, **kwargs) -> None: - registry.register_facet_provider(CategoryFacetProvider()) + registry.register_facet_provider(CategoryFacetProvider(**kwargs)) diff --git a/geonode/facets/providers/keyword.py b/geonode/facets/providers/keyword.py new file mode 100644 index 00000000000..ce9e5f6f8f9 --- /dev/null +++ b/geonode/facets/providers/keyword.py @@ -0,0 +1,108 @@ +######################################################################### +# +# Copyright (C) 2023 Open Source Geospatial Foundation - all rights reserved +# +# 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 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### + +import logging + +from django.db.models import Count + +from geonode.base.models import HierarchicalKeyword +from geonode.facets.models import FacetProvider, DEFAULT_FACET_PAGE_SIZE, FACET_TYPE_KEYWORD + +logger = logging.getLogger(__name__) + + +class KeywordFacetProvider(FacetProvider): + """ + Implements faceting for resource's keywords + """ + + @property + def name(self) -> str: + return "keyword" + + def get_info(self, lang="en", **kwargs) -> dict: + return { + "name": self.name, + "key": "filter{keywords.slug.in}", # deprecated + "filter": "filter{keywords.slug.in}", + "label": "Keyword", + "type": FACET_TYPE_KEYWORD, + } + + def get_facet_items( + self, + queryset=None, + start: int = 0, + end: int = DEFAULT_FACET_PAGE_SIZE, + lang="en", + topic_contains: str = None, + keys: set = {}, + **kwargs, + ) -> (int, list): + logger.debug("Retrieving facets for %s", self.name) + + filters = {"keywords__isnull": False} + if topic_contains: + filters["keywords__name__icontains"] = topic_contains + + if keys: + logger.debug("Filtering by keys %r", keys) + filters["keywords__slug__in"] = keys + + q = ( + queryset.filter(**filters) + .values("keywords__slug", "keywords__name") + .annotate(count=Count("keywords__slug")) + .order_by("-count") + ) + + cnt = q.count() + + logger.info("Found %d facets for %s", cnt, self.name) + logger.debug(" ---> %s\n\n", q.query) + logger.debug(" ---> %r\n\n", q.all()) + + topics = [ + { + "key": r["keywords__slug"], + "label": r["keywords__name"], + "count": r["count"], + } + for r in q[start:end].all() + ] + + return cnt, topics + + def get_topics(self, keys: list, lang="en", **kwargs) -> list: + q = HierarchicalKeyword.objects.filter(slug__in=keys).values("slug", "name") + + logger.debug(" ---> %s\n\n", q.query) + logger.debug(" ---> %r\n\n", q.all()) + + return [ + { + "key": r["slug"], + "label": r["name"], + } + for r in q.all() + ] + + @classmethod + def register(cls, registry, **kwargs) -> None: + registry.register_facet_provider(KeywordFacetProvider(**kwargs)) diff --git a/geonode/facets/providers/region.py b/geonode/facets/providers/region.py index b0cd5d45ce8..c40138e58bb 100644 --- a/geonode/facets/providers/region.py +++ b/geonode/facets/providers/region.py @@ -21,6 +21,7 @@ from django.db.models import Count +from geonode.base.models import Region from geonode.facets.models import FacetProvider, DEFAULT_FACET_PAGE_SIZE, FACET_TYPE_PLACE logger = logging.getLogger(__name__) @@ -35,14 +36,13 @@ class RegionFacetProvider(FacetProvider): def name(self) -> str: return "region" - def get_info(self, lang="en") -> dict: + def get_info(self, lang="en", **kwargs) -> dict: return { "name": self.name, - "key": "filter{regions.code.in}", + "key": "filter{regions.code.in}", # deprecated + "filter": "filter{regions.code.in}", "label": "Region", "type": FACET_TYPE_PLACE, - "hierarchical": False, # source data is hierarchical, but this implementation is flat - "order": 2, } def get_facet_items( @@ -52,13 +52,26 @@ def get_facet_items( end: int = DEFAULT_FACET_PAGE_SIZE, lang="en", topic_contains: str = None, + keys: set = {}, + **kwargs, ) -> (int, list): logger.debug("Retrieving facets for %s", self.name) - q = queryset.filter(regions__isnull=False).values("regions__code", "regions__name") + filters = {"regions__isnull": False} + if topic_contains: - q = q.filter(regions__name=topic_contains) - q = q.annotate(count=Count("regions__code")).order_by("-count") + filters["regions__name"] = topic_contains + + if keys: + logger.debug("Filtering by keys %r", keys) + filters["regions__code__in"] = keys + + q = ( + queryset.filter(**filters) + .values("regions__code", "regions__name") + .annotate(count=Count("regions__code")) + .order_by("-count") + ) cnt = q.count() @@ -77,6 +90,20 @@ def get_facet_items( return cnt, topics + def get_topics(self, keys: list, lang="en", **kwargs) -> list: + q = Region.objects.filter(code__in=keys).values("code", "name") + + logger.debug(" ---> %s\n\n", q.query) + logger.debug(" ---> %r\n\n", q.all()) + + return [ + { + "key": r["code"], + "label": r["name"], + } + for r in q.all() + ] + @classmethod def register(cls, registry, **kwargs) -> None: - registry.register_facet_provider(RegionFacetProvider()) + registry.register_facet_provider(RegionFacetProvider(**kwargs)) diff --git a/geonode/facets/providers/thesaurus.py b/geonode/facets/providers/thesaurus.py index 5674635b6bd..bb40f455924 100644 --- a/geonode/facets/providers/thesaurus.py +++ b/geonode/facets/providers/thesaurus.py @@ -19,8 +19,9 @@ import logging -from django.db.models import Count +from django.db.models import Count, OuterRef, Subquery +from geonode.base.models import ThesaurusKeyword, ThesaurusKeywordLabel from geonode.facets.models import FacetProvider, DEFAULT_FACET_PAGE_SIZE, FACET_TYPE_THESAURUS logger = logging.getLogger(__name__) @@ -31,25 +32,27 @@ class ThesaurusFacetProvider(FacetProvider): Implements faceting for a given Thesaurus """ - def __init__(self, identifier, title, order, labels: dict): + def __init__(self, identifier, title, order, labels: dict, **kwargs): + super().__init__(**kwargs) + self._name = identifier self.label = title - self.order = order self.labels = labels + self.config["order"] = order + @property def name(self) -> str: return self._name - def get_info(self, lang="en") -> dict: + def get_info(self, lang="en", **kwargs) -> dict: return { "name": self._name, - "key": "filter{tkeywords}", + "key": "filter{tkeywords}", # deprecated + "filter": "filter{tkeywords}", "label": self.labels.get(lang, self.label), "is_localized": self.labels.get(lang, None) is not None, "type": FACET_TYPE_THESAURUS, - "hierarchical": False, - "order": self.order, } def get_facet_items( @@ -59,36 +62,46 @@ def get_facet_items( end: int = DEFAULT_FACET_PAGE_SIZE, lang="en", topic_contains: str = None, + keys: set = {}, **kwargs, ) -> (int, list): logger.debug("Retrieving facets for %s", self._name) filter = { "tkeywords__thesaurus__identifier": self._name, - "tkeywords__keyword__lang": lang, } if topic_contains: filter["tkeywords__keyword__label__icontains"] = topic_contains + if keys: + logger.debug("Filtering by keys %r\n", keys) + filter["tkeywords__in"] = keys + q = ( queryset.filter(**filter) - .values("tkeywords", "tkeywords__keyword__label", "tkeywords__alt_label") + .values("tkeywords", "tkeywords__alt_label") .annotate(count=Count("tkeywords")) + .annotate( + localized_label=Subquery( + ThesaurusKeywordLabel.objects.filter(keyword=OuterRef("tkeywords"), lang=lang).values("label") + ) + ) .order_by("-count") ) + logger.debug(" ---> %s\n\n", q.query) + cnt = q.count() logger.info("Found %d facets for %s", cnt, self._name) - logger.debug(" ---> %s\n\n", q.query) logger.debug(" ---> %r\n\n", q.all()) topics = [ { "key": r["tkeywords"], - "label": r["tkeywords__keyword__label"] or r["tkeywords__alt_label"], - "is_localized": r["tkeywords__keyword__label"] is not None, + "label": r["localized_label"] or r["tkeywords__alt_label"], + "is_localized": r["localized_label"] is not None, "count": r["count"], } for r in q[start:end].all() @@ -96,6 +109,29 @@ def get_facet_items( return cnt, topics + def get_topics(self, keys: list, lang="en", **kwargs) -> list: + q = ( + ThesaurusKeyword.objects.filter(id__in=keys) + .values("id", "alt_label") + .annotate( + localized_label=Subquery( + ThesaurusKeywordLabel.objects.filter(keyword=OuterRef("id"), lang=lang).values("label") + ) + ) + ) + + logger.debug(" ---> %s\n\n", q.query) + logger.debug(" ---> %r\n\n", q.all()) + + return [ + { + "key": r["id"], + "label": r["localized_label"] or r["alt_label"], + "is_localized": r["localized_label"] is not None, + } + for r in q.all() + ] + @classmethod def register(cls, registry, **kwargs) -> None: # registry.register_facet_provider(CategoryFacetProvider()) @@ -123,5 +159,5 @@ def register(cls, registry, **kwargs) -> None: logger.info("Creating providers for %r", ret) for t in ret.values(): registry.register_facet_provider( - ThesaurusFacetProvider(t["identifier"], t["title"], t["order"], t["labels"]) + ThesaurusFacetProvider(t["identifier"], t["title"], t["order"], t["labels"], **kwargs) ) diff --git a/geonode/facets/providers/users.py b/geonode/facets/providers/users.py index 8a1e5c3effe..cf2a52cd9ba 100644 --- a/geonode/facets/providers/users.py +++ b/geonode/facets/providers/users.py @@ -19,6 +19,7 @@ import logging +from django.contrib.auth import get_user_model from django.db.models import Count from geonode.facets.models import FacetProvider, DEFAULT_FACET_PAGE_SIZE, FACET_TYPE_USER @@ -35,14 +36,13 @@ class OwnerFacetProvider(FacetProvider): def name(self) -> str: return "owner" - def get_info(self, lang="en") -> dict: + def get_info(self, lang="en", **kwargs) -> dict: return { "name": "owner", - "key": "owner", + "key": "filter{owner.pk.in}", # deprecated + "filter": "filter{owner.pk.in}", "label": "Owner", "type": FACET_TYPE_USER, - "hierarchical": False, - "order": 5, } def get_facet_items( @@ -52,13 +52,26 @@ def get_facet_items( end: int = DEFAULT_FACET_PAGE_SIZE, lang="en", topic_contains: str = None, + keys: set = {}, + **kwargs, ) -> (int, list): logger.debug("Retrieving facets for OWNER") - q = queryset.values("owner", "owner__username") + filters = dict() + if topic_contains: - q = q.filter(owner__username__icontains=topic_contains) - q = q.annotate(count=Count("owner")).order_by("-count") + filters["owner__username__icontains"] = topic_contains + + if keys: + logger.debug("Filtering by keys %r", keys) + filters["owner__in"] = keys + + q = ( + queryset.values("owner", "owner__username") + .filter(**filters) + .annotate(count=Count("owner")) + .order_by("-count") + ) cnt = q.count() @@ -70,7 +83,6 @@ def get_facet_items( { "key": r["owner"], "label": r["owner__username"], - "localized_label": r["owner__username"], "count": r["count"], } for r in q[start:end] @@ -78,6 +90,20 @@ def get_facet_items( return cnt, topics + def get_topics(self, keys: list, lang="en", **kwargs) -> list: + q = get_user_model().objects.filter(id__in=keys).values("id", "username") + + logger.debug(" ---> %s\n\n", q.query) + logger.debug(" ---> %r\n\n", q.all()) + + return [ + { + "key": r["id"], + "label": r["username"], + } + for r in q.all() + ] + @classmethod def register(cls, registry, **kwargs) -> None: - registry.register_facet_provider(OwnerFacetProvider()) + registry.register_facet_provider(OwnerFacetProvider(**kwargs)) diff --git a/geonode/facets/tests.py b/geonode/facets/tests.py index 67fcf27e11e..cf05e1e3734 100644 --- a/geonode/facets/tests.py +++ b/geonode/facets/tests.py @@ -27,7 +27,21 @@ from django.test import RequestFactory from django.urls import reverse -from geonode.base.models import Thesaurus, ThesaurusLabel, ThesaurusKeyword, ThesaurusKeywordLabel, ResourceBase, Region +from geonode.base.models import ( + Thesaurus, + ThesaurusLabel, + ThesaurusKeyword, + ThesaurusKeywordLabel, + ResourceBase, + Region, + TopicCategory, + HierarchicalKeyword, +) +from geonode.facets.models import facet_registry +from geonode.facets.providers.baseinfo import FeaturedFacetProvider +from geonode.facets.providers.category import CategoryFacetProvider +from geonode.facets.providers.keyword import KeywordFacetProvider +from geonode.facets.providers.region import RegionFacetProvider from geonode.tests.base import GeoNodeBaseTestSupport import geonode.facets.views as views @@ -45,6 +59,8 @@ def setUpClass(cls): cls._create_thesauri() cls._create_regions() + cls._create_categories() + cls._create_keywords() cls._create_resources() cls.rf = RequestFactory() @@ -67,7 +83,7 @@ def _create_thesauri(cls): cls.thesauri_k = {} for tn in range(2): - t = Thesaurus.objects.create(identifier=f"t_{tn}", title=f"Thesaurus {tn}") + t = Thesaurus.objects.create(identifier=f"t_{tn}", title=f"Thesaurus {tn}", order=100 + tn * 10) cls.thesauri[tn] = t for tl in ( "en", @@ -76,7 +92,7 @@ def _create_thesauri(cls): ThesaurusLabel.objects.create(thesaurus=t, lang=tl, label=f"TLabel {tn} {tl}") for tkn in range(10): - tk = ThesaurusKeyword.objects.create(thesaurus=t, alt_label=f"alt_tkn{tkn}_t{tn}") + tk = ThesaurusKeyword.objects.create(thesaurus=t, alt_label=f"T{tn}_K{tkn}_ALT") cls.thesauri_k[f"{tn}_{tkn}"] = tk for tkl in ( "en", @@ -95,6 +111,28 @@ def _create_regions(cls): ): cls.regions[code] = Region.objects.create(code=code, name=name) + @classmethod + def _create_categories(cls): + cls.cats = {} + + for code, name in ( + ("C0", "Cat0"), + ("C1", "Cat1"), + ("C2", "Cat2"), + ): + cls.cats[code] = TopicCategory.objects.create(identifier=code, description=name, gn_description=name) + + @classmethod + def _create_keywords(cls): + cls.kw = {} + + for code, name in ( + ("K0", "Keyword0"), + ("K1", "Keyword1"), + ("K2", "Keyword2"), + ): + cls.kw[code] = HierarchicalKeyword.objects.create(slug=code, name=name) + @classmethod def _create_resources(self): public_perm_spec = {"users": {"AnonymousUser": ["view_resourcebase"]}, "groups": []} @@ -112,49 +150,66 @@ def _create_resources(self): # These are the assigned keywords to the Resources - # RB00 -> T1K0 R0,R1 - # RB01 -> T0K0 T1K0 R0 - # RB02 -> T1K0 R1 - # RB03 -> T0K0 T1K0 - # RB04 -> T1K0 - # RB05 -> T0K0 T1K0 - # RB06 -> T1K0 - # RB07 -> T0K0 T1K0 - # RB08 -> T1K0 T1K1 R1 + # RB00 -> T1K0 R0,R1 FEAT K0 C0 + # RB01 -> T0K0 T1K0 R0 FEAT K1 + # RB02 -> T1K0 R1 FEAT K2 C0 + # RB03 -> T0K0 T1K0 K0 + # RB04 -> T1K0 K0,K1 C0 + # RB05 -> T0K0 T1K0 K0,K2 C1 + # RB06 -> T1K0 FEAT + # RB07 -> T0K0 T1K0 FEAT + # RB08 -> T1K0 T1K1 R1 FEAT # RB09 -> T0K0 T1K0 T1K1 # RB10 -> T1K1 # RB11 -> T0K0 T0K1 T1K1 - # RB12 -> T1K1 - # RB13 -> T0K0 T0K1 R1 - # RB14 -> - # RB15 -> T0K0 T0K1 - # RB16 -> + # RB12 -> T1K1 FEAT + # RB13 -> T0K0 T0K1 R1 FEAT + # RB14 -> FEAT + # RB15 -> T0K0 T0K1 C1 + # RB16 -> C1 # RB17 -> T0K0 T0K1 - # RB18 -> - # RB19 -> T0K0 T0K1 + # RB18 -> FEAT C2 + # RB19 -> T0K0 T0K1 FEAT C2 if x % 2 == 1: - print(f"ADDING KEYWORDS {self.thesauri_k['0_0']} to RB {d}") + logger.debug(f"ADDING KEYWORDS {self.thesauri_k['0_0']} to RB {d}") d.tkeywords.add(self.thesauri_k["0_0"]) - d.save() if x % 2 == 1 and x > 10: - print(f"ADDING KEYWORDS {self.thesauri_k['0_1']} to RB {d}") + logger.debug(f"ADDING KEYWORDS {self.thesauri_k['0_1']} to RB {d}") d.tkeywords.add(self.thesauri_k["0_1"]) - d.save() if x < 10: - print(f"ADDING KEYWORDS {self.thesauri_k['1_0']} to RB {d}") + logger.debug(f"ADDING KEYWORDS {self.thesauri_k['1_0']} to RB {d}") d.tkeywords.add(self.thesauri_k["1_0"]) - d.save() if 7 < x < 13: d.tkeywords.add(self.thesauri_k["1_1"]) - d.save() - if x in (0, 1): - d.regions.add(self.regions["R0"]) - d.save() - if x in (0, 2, 8, 13): - d.regions.add(self.regions["R1"]) - d.save() + if (x % 6) in (0, 1, 2): + d.featured = True + + for reg, idx in ( + ("R0", (0, 1)), + ("R1", (0, 2, 8, 13)), + ): + if x in idx: + d.regions.add(self.regions[reg]) + + for kw, idx in ( + ("K0", (0, 3, 4, 5)), + ("K1", [1, 4]), + ("K2", [2, 5]), + ): + if x in idx: + d.keywords.add(self.kw[kw]) + + for cat, idx in ( + ("C0", [0, 2, 4]), + ("C1", [5, 15, 16]), + ("C2", [18, 19]), + ): + if x in idx: + d.category = self.cats[cat] + + d.save() d.set_permissions(public_perm_spec) @staticmethod @@ -167,9 +222,9 @@ def test_facets_base(self): obj = json.loads(res.content) self.assertIn("facets", obj) facets_list = obj["facets"] - self.assertEqual(5, len(facets_list)) + self.assertEqual(8, len(facets_list)) fmap = self._facets_to_map(facets_list) - for name in ("category", "owner", "t_0", "t_1"): + for name in ("category", "owner", "t_0", "t_1", "featured", "resourcetype", "keyword"): self.assertIn(name, fmap) def test_facets_rich(self): @@ -187,13 +242,29 @@ def test_facets_rich(self): obj = json.loads(res.content) facets_list = obj["facets"] - self.assertEqual(5, len(facets_list)) + self.assertEqual(8, len(facets_list)) fmap = self._facets_to_map(facets_list) for expected in ( { "name": "category", "topics": { - "total": 1, + "total": 3, + "items": [ + {"label": "Cat0", "count": 3}, + {"label": "Cat1", "count": 3}, + {"label": "Cat2", "count": 2}, + ], + }, + }, + { + "name": "keyword", + "topics": { + "total": 3, + "items": [ + {"label": "Keyword0", "count": 4}, + {"label": "Keyword1", "count": 2}, + {"label": "Keyword2", "count": 2}, + ], }, }, { @@ -231,6 +302,25 @@ def test_facets_rich(self): ], }, }, + { + "name": "featured", + "topics": { + "total": 2, + "items": [ + {"label": "True", "key": True, "count": 11}, + {"label": "False", "key": False, "count": 9}, + ], + }, + }, + { + "name": "resourcetype", + "topics": { + "total": 1, + "items": [ + {"label": "resourcebase", "key": "resourcebase", "count": 20}, + ], + }, + }, ): name = expected["name"] self.assertIn(name, fmap) @@ -252,7 +342,9 @@ def test_facets_rich(self): found = item break - self.assertIsNotNone(item, f"topic not found '{exp_label}'") + self.assertIsNotNone( + found, f"topic not found '{exp_label}' for facet '{name}' -- found items {items}" + ) for exp_field in exp_item: self.assertEqual( exp_item[exp_field], found[exp_field], f"Mismatch item key:{exp_field} facet:{name}" @@ -261,8 +353,191 @@ def test_facets_rich(self): def test_bad_lang(self): # for thesauri, make sure that by requesting a non-existent language the faceting is still working, # using the default labels - # TODO impl+test - pass + + # run the request with a valid language + req = self.rf.get(reverse("get_facet", args=["t_0"]), data={"lang": "en"}) + res: JsonResponse = views.get_facet(req, "t_0") + obj = json.loads(res.content) + + self.assertEqual(2, obj["topics"]["total"]) + self.assertEqual(10, obj["topics"]["items"][0]["count"]) + self.assertEqual("T0_K0_en", obj["topics"]["items"][0]["label"]) + self.assertTrue(obj["topics"]["items"][0]["is_localized"]) + + # run the request with an INVALID language + req = self.rf.get(reverse("get_facet", args=["t_0"]), data={"lang": "ZZ"}) + res: JsonResponse = views.get_facet(req, "t_0") + obj = json.loads(res.content) + + self.assertEqual(2, obj["topics"]["total"]) + self.assertEqual(10, obj["topics"]["items"][0]["count"]) # make sure the count is still there + self.assertEqual("T0_K0_ALT", obj["topics"]["items"][0]["label"]) # check for the alternate label + self.assertFalse(obj["topics"]["items"][0]["is_localized"]) # check for the localization flag + + def test_topics(self): + for facet, keys, exp in ( + ("t_0", [self.thesauri_k["0_0"].id, self.thesauri_k["0_1"].id, -999], 2), + ("category", ["C1", "C2", "nomatch"], 2), + ("owner", [self.user.id, -100], 1), + ("region", ["R0", "R1", "nomatch"], 2), + ): + req = self.rf.get(reverse("get_facet_topics", args=[facet]), data={"lang": "en", "key": keys}) + res: JsonResponse = views.get_facet_topics(req, facet) + obj = json.loads(res.content) + self.assertEqual(exp, len(obj["topics"]["items"]), f"Unexpected topic count {exp} for facet {facet}") + + def test_prefiltering(self): + reginfo = RegionFacetProvider().get_info() + t0info = facet_registry.get_provider("t_0").get_info() + t1info = facet_registry.get_provider("t_1").get_info() + + for facet, filters, totals, count0 in ( + ("t_0", {}, 2, 10), + ("t_0", {reginfo["key"]: "R0"}, 1, 1), + ("t_1", {}, 2, 10), + ("t_1", {reginfo["key"]: "R0"}, 1, 2), + ("t_1", {reginfo["key"]: "R1"}, 2, 3), + (reginfo["name"], {}, 2, 4), + (reginfo["name"], {t0info["key"]: self.thesauri_k["0_0"].id}, 2, 1), + (reginfo["name"], {t1info["key"]: self.thesauri_k["1_0"].id}, 2, 3), + ): + req = self.rf.get(reverse("get_facet", args=[facet]), data=filters) + res: JsonResponse = views.get_facet(req, facet) + obj = json.loads(res.content) + self.assertEqual(totals, obj["topics"]["total"], f"Bad totals for facet '{facet} and filter {filters}") + self.assertEqual(count0, obj["topics"]["items"][0]["count"], f"Bad count0 for facet '{facet}") + + def test_prefiltering_tkeywords(self): + regname = RegionFacetProvider().name + featname = FeaturedFacetProvider().name + t1filter = facet_registry.get_provider("t_1").get_info()["filter"] + tkey_1_1 = self.thesauri_k["1_1"].id + + expected_region = {"R1": 1} + expected_feat = {True: 2, False: 3} + + # Run the single requests + for facet, params, items in ( + (regname, {t1filter: tkey_1_1}, expected_region), + (featname, {t1filter: tkey_1_1}, expected_feat), + ): + req = self.rf.get(reverse("get_facet", args=[facet]), data=params) + res: JsonResponse = views.get_facet(req, facet) + obj = json.loads(res.content) + + self.assertEqual( + len(items), + len(obj["topics"]["items"]), + f"Bad count for items '{facet} \n PARAMS: {params} \n RESULT: {obj} \n EXPECTED: {items}", + ) + # search item + for item in items.keys(): + found = next((i for i in obj["topics"]["items"] if i["key"] == item), None) + self.assertIsNotNone(found, f"Topic '{item}' not found in facet {facet} -- {obj}") + self.assertEqual(items[item], found.get("count", None), f"Bad count for facet '{facet}:{item}") + + # Run the single request + req = self.rf.get(reverse("list_facets"), data={"include_topics": 1, t1filter: tkey_1_1}) + res: JsonResponse = views.list_facets(req) + obj = json.loads(res.content) + + facets_list = obj["facets"] + fmap = self._facets_to_map(facets_list) + + for name, items in ( + (regname, expected_region), + (featname, expected_feat), + ): + self.assertIn(name, fmap) + facet = fmap[name] + + for item in items.keys(): + found = next((i for i in facet["topics"]["items"] if i["key"] == item), None) + self.assertIsNotNone(found, f"Topic '{item}' not found in facet {facet} -- {facet}") + self.assertEqual(items[item], found.get("count", None), f"Bad count for facet '{facet}:{item}") + + def test_config(self): + for facet, type, order in ( + ("resourcetype", None, None), + ("t_0", "select", 100), + ("category", "select", 5), + ("region", "select", 7), + ("owner", "select", 8), + ): + req = self.rf.get(reverse("get_facet", args=[facet]), data={"include_config": True}) + res: JsonResponse = views.get_facet(req, facet) + obj = json.loads(res.content) + self.assertIn("config", obj, "Config info not found in payload") + conf = obj["config"] + + if type is None: + self.assertNotIn("type", conf) + else: + self.assertEqual(type, conf["type"], "Unexpected type") + + if order is None: + self.assertNotIn("order", conf) + else: + self.assertEqual(order, conf["order"], "Unexpected order") + + def test_count0(self): + reginfo = RegionFacetProvider().get_info() + regfilter = reginfo["filter"] + regname = reginfo["name"] + + catinfo = CategoryFacetProvider().get_info() + catname = catinfo["name"] + + kwinfo = KeywordFacetProvider().get_info() + kwfilter = kwinfo["filter"] + kwname = kwinfo["name"] + + t0filter = facet_registry.get_provider("t_0").get_info()["filter"] + t1filter = facet_registry.get_provider("t_1").get_info()["filter"] + + def t(tk): + return self.thesauri_k[tk].id + + for facet, params, items in ( + # thesauri + ("t_1", {regfilter: "R0"}, {t("1_0"): 2}), + ("t_1", {regfilter: "R0", "key": [t("1_0")]}, {t("1_0"): 2}), + ("t_1", {regfilter: "R0", t0filter: t("0_1")}, {}), + ("t_1", {regfilter: "R0", t0filter: t("0_1"), "key": [t("1_0")]}, {t("1_0"): None}), + ( + "t_1", + {regfilter: "R0", t0filter: t("0_1"), "key": [t("1_1"), t("1_0")]}, + {t("1_0"): None, t("1_1"): None}, + ), + # regions + (regname, {t1filter: t("1_1")}, {"R1": 1}), + (regname, {t1filter: t("1_1"), "key": ["R0", "R1"]}, {"R1": 1, "R0": None}), + (regname, {t1filter: t("1_1"), "key": ["R0"]}, {"R0": None}), + # category + (catname, {t1filter: t("1_0")}, {"C0": 3, "C1": 1}), + (catname, {t1filter: t("1_0"), "key": ["C0", "C2"]}, {"C0": 3, "C2": None}), + (catname, {kwfilter: "K1"}, {"C0": 1}), + (catname, {kwfilter: "K1", "key": ["C0", "C2"]}, {"C0": 1, "C2": None}), + # keyword + (kwname, {t0filter: t("0_0")}, {"K0": 2, "K1": 1, "K2": 1}), + (kwname, {t0filter: t("0_0"), regfilter: "R0"}, {"K1": 1}), + (kwname, {t0filter: t("0_0"), regfilter: "R0", "key": ["K0"]}, {"K0": None}), + ): + req = self.rf.get(reverse("get_facet", args=[facet]), data=params) + res: JsonResponse = views.get_facet(req, facet) + obj = json.loads(res.content) + # self.assertEqual(totals, obj["topics"]["total"], f"Bad totals for facet '{facet} and params {params}") + + self.assertEqual( + len(items), + len(obj["topics"]["items"]), + f"Bad count for items '{facet} \n PARAMS: {params} \n RESULT: {obj} \n EXPECTED: {items}", + ) + # search item + for item in items.keys(): + found = next((i for i in obj["topics"]["items"] if i["key"] == item), None) + self.assertIsNotNone(found, f"Topic '{item}' not found in facet {facet} -- {obj}") + self.assertEqual(items[item], found.get("count", None), f"Bad count for facet '{facet}:{item}") def test_user_auth(self): # make sure the user authorization pre-filters the visible resources diff --git a/geonode/facets/urls.py b/geonode/facets/urls.py index 82aef71e751..42bf08de85b 100644 --- a/geonode/facets/urls.py +++ b/geonode/facets/urls.py @@ -23,4 +23,5 @@ urlpatterns = [ path("facets", views.list_facets, name="list_facets"), path("facets/", views.get_facet, name="get_facet"), + path("facets//topics", views.get_facet_topics, name="get_facet_topics"), ] diff --git a/geonode/facets/views.py b/geonode/facets/views.py index 31961bb3b6d..a02c8aa961f 100644 --- a/geonode/facets/views.py +++ b/geonode/facets/views.py @@ -23,10 +23,12 @@ from rest_framework.authentication import SessionAuthentication, BasicAuthentication from rest_framework.decorators import api_view, authentication_classes -from django.http import HttpResponseNotFound, JsonResponse +from django.http import HttpResponseNotFound, JsonResponse, HttpResponseBadRequest from django.urls import reverse from django.conf import settings + +from geonode.base.api.views import ResourceBaseViewSet from geonode.base.models import ResourceBase from geonode.facets.models import FacetProvider, DEFAULT_FACET_PAGE_SIZE, facet_registry from geonode.security.utils import get_visible_resources @@ -36,6 +38,7 @@ PARAM_LANG = "lang" PARAM_ADD_LINKS = "add_links" PARAM_INCLUDE_TOPICS = "include_topics" +PARAM_INCLUDE_CONFIG = "include_config" PARAM_TOPIC_CONTAINS = "topic_contains" logger = logging.getLogger(__name__) @@ -47,12 +50,18 @@ def list_facets(request, **kwargs): lang, lang_requested = _resolve_language(request) add_links = _resolve_boolean(request, PARAM_ADD_LINKS, False) include_topics = _resolve_boolean(request, PARAM_INCLUDE_TOPICS, False) + include_config = _resolve_boolean(request, PARAM_INCLUDE_CONFIG, False) facets = [] + prefiltered = None for provider in facet_registry.get_providers(): logger.debug("Fetching data from provider %r", provider) info = provider.get_info(lang=lang) + + if include_config: + info["config"] = provider.config + if add_links: link_args = {PARAM_ADD_LINKS: True} if lang_requested: # only add lang param if specified in current call @@ -60,7 +69,8 @@ def list_facets(request, **kwargs): info["link"] = f"{reverse('get_facet', args=[info['name']])}?{urlencode(link_args)}" if include_topics: - info["topics"] = _get_topics(provider, queryset=_prefilter_topics(request), lang=lang) + prefiltered = prefiltered or _prefilter_topics(request) + info["topics"] = _get_topics(provider, queryset=prefiltered, lang=lang) facets.append(info) @@ -81,15 +91,21 @@ def get_facet(request, facet): # parse some query params lang, lang_requested = _resolve_language(request) add_link = _resolve_boolean(request, PARAM_ADD_LINKS, False) + include_config = _resolve_boolean(request, PARAM_INCLUDE_CONFIG, False) + topic_contains = request.GET.get(PARAM_TOPIC_CONTAINS, None) + keys = set(request.query_params.getlist("key")) + page = int(request.GET.get(PARAM_PAGE, 0)) page_size = int(request.GET.get(PARAM_PAGE_SIZE, DEFAULT_FACET_PAGE_SIZE)) info = provider.get_info(lang) + if include_config: + info["config"] = provider.config qs = _prefilter_topics(request) topics = _get_topics( - provider, queryset=qs, page=page, page_size=page_size, lang=lang, topic_contains=topic_contains + provider, queryset=qs, page=page, page_size=page_size, lang=lang, topic_contains=topic_contains, keys=keys ) if add_link: @@ -116,6 +132,25 @@ def get_facet(request, facet): return JsonResponse(info) +@api_view(["GET"]) +def get_facet_topics(request, facet): + logger.debug("get_facet_topics -> %r", facet) + + # retrieve provider for the requested facet + provider: FacetProvider = facet_registry.get_provider(facet) + if not provider: + return HttpResponseNotFound("Facet not found") + + # parse some query params + lang, lang_requested = _resolve_language(request) + keys = request.query_params.getlist("key") + if not keys: + return HttpResponseBadRequest("Missing key parameter") + + ret = {"topics": {"items": provider.get_topics(keys, lang=lang)}} + return JsonResponse(ret) + + def _get_topics( provider, queryset, @@ -123,11 +158,23 @@ def _get_topics( page_size: int = DEFAULT_FACET_PAGE_SIZE, lang: str = "en", topic_contains: str = None, + keys: set = {}, + **kwargs, ): start = page * page_size end = start + page_size - cnt, items = provider.get_facet_items(queryset, start=start, end=end, lang=lang, topic_contains=topic_contains) + cnt, items = provider.get_facet_items( + queryset, start=start, end=end, lang=lang, topic_contains=topic_contains, keys=keys + ) + + if keys: + keys.difference_update({str(t["key"]) for t in items}) + if keys: + ext = provider.get_topics(keys, lang) + items.extend(ext) + cnt += len(ext) + logger.debug("Extending facets to %d for %s", cnt, provider.name) return {"page": page, "page_size": page_size, "start": start, "total": cnt, "items": items} @@ -141,8 +188,16 @@ def _prefilter_topics(request): :return: a QuerySet on ResourceBase """ logger.debug("Filtering by user '%s'", request.user) - # return ResourceBase.objects - return get_visible_resources(ResourceBase.objects, request.user) + filters = {k: vlist for k, vlist in request.query_params.lists() if k.startswith("filter{")} + logger.warning(f"FILTERING BY {filters}") + + if filters: + viewset = ResourceBaseViewSet(request=request, format_kwarg={}, kwargs=filters) + viewset.initial(request) + return get_visible_resources(queryset=viewset.filter_queryset(viewset.get_queryset()), user=request.user) + else: + # return ResourceBase.objects + return get_visible_resources(ResourceBase.objects, request.user) def _resolve_language(request) -> (str, bool): diff --git a/geonode/favorite/__init__.py b/geonode/favorite/__init__.py index 79177e00bdd..83005e67c7f 100644 --- a/geonode/favorite/__init__.py +++ b/geonode/favorite/__init__.py @@ -16,3 +16,4 @@ # along with this program. If not, see . # ######################################################################### +default_app_config = "geonode.favorite.apps.GeoNodeFavoriteAppConfig" diff --git a/geonode/favorite/apps.py b/geonode/favorite/apps.py new file mode 100644 index 00000000000..0b27c6eb7e3 --- /dev/null +++ b/geonode/favorite/apps.py @@ -0,0 +1,24 @@ +######################################################################### +# +# Copyright (C) 2023 OSGeo +# +# 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 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### +from django.apps import AppConfig + + +class GeoNodeFavoriteAppConfig(AppConfig): + name = "geonode.favorite" + verbose_name = "GeoNode Favorite" diff --git a/geonode/geoapps/admin.py b/geonode/geoapps/admin.py index f0217db4cb2..33c8ff2f7c6 100644 --- a/geonode/geoapps/admin.py +++ b/geonode/geoapps/admin.py @@ -32,6 +32,7 @@ class Meta(ResourceBaseAdminForm.Meta): class GeoAppAdmin(TabbedTranslationAdmin): + exclude = ("ll_bbox_polygon", "bbox_polygon", "srid") list_display_links = ("title",) list_display = ( "id", @@ -65,6 +66,7 @@ class GeoAppAdmin(TabbedTranslationAdmin): "is_approved", "is_published", ) + readonly_fields = ("geographic_bounding_box",) form = GeoAppAdminForm def delete_queryset(self, request, queryset): diff --git a/geonode/geoserver/createlayer/__init__.py b/geonode/geoserver/createlayer/__init__.py index 3d54b02dfef..a304ef09a5b 100644 --- a/geonode/geoserver/createlayer/__init__.py +++ b/geonode/geoserver/createlayer/__init__.py @@ -16,3 +16,4 @@ # along with this program. If not, see . # ######################################################################### +default_app_config = "geonode.geoserver.createlayer.apps.GeoNodeGeoserverCreatelayerAppConfig" diff --git a/geonode/geoserver/createlayer/apps.py b/geonode/geoserver/createlayer/apps.py new file mode 100644 index 00000000000..a3b9434b8a4 --- /dev/null +++ b/geonode/geoserver/createlayer/apps.py @@ -0,0 +1,24 @@ +######################################################################### +# +# Copyright (C) 2023 OSGeo +# +# 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 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### +from django.apps import AppConfig + + +class GeoNodeGeoserverCreatelayerAppConfig(AppConfig): + name = "geonode.geoserver.createlayer" + verbose_name = "GeoNode Geoserver Createlayer" diff --git a/geonode/geoserver/helpers.py b/geonode/geoserver/helpers.py index d04fb3e12ff..cc9135d243f 100755 --- a/geonode/geoserver/helpers.py +++ b/geonode/geoserver/helpers.py @@ -2287,7 +2287,7 @@ def get_dataset_capabilities_url(layer, version="1.3.0", access_token=None): workspace_layername = layer.alternate.split(":") if ":" in layer.alternate else ("", layer.alternate) wms_url = settings.GEOSERVER_PUBLIC_LOCATION if not layer.remote_service: - wms_url = f"{wms_url}{'/'.join(workspace_layername)}/wms?service=wms&version={version}&request=GetCapabilities" # noqa + wms_url = f"{wms_url}{'/'.join(workspace_layername)}/ows?service=wms&version={version}&request=GetCapabilities" # noqa if access_token: wms_url += f"&access_token={access_token}" else: @@ -2296,6 +2296,22 @@ def get_dataset_capabilities_url(layer, version="1.3.0", access_token=None): return wms_url +def get_layer_ows_url(layer, access_token=None): + """ + Generate the layer-specific GetCapabilities URL + """ + workspace_layername = layer.alternate.split(":") if ":" in layer.alternate else ("", layer.alternate) + ows_url = settings.GEOSERVER_PUBLIC_LOCATION + if not layer.remote_service: + ows_url = f"{ows_url}{'/'.join(workspace_layername)}/ows" # noqa + if access_token: + ows_url += f"?access_token={access_token}" + else: + ows_url = f"{layer.remote_service.service_url}" + + return ows_url + + def wps_format_is_supported(_format, dataset_type): return (_format, dataset_type) in WPS_ACCEPTABLE_FORMATS diff --git a/geonode/geoserver/processing/__init__.py b/geonode/geoserver/processing/__init__.py index 6b4db6084e8..333409c58a5 100644 --- a/geonode/geoserver/processing/__init__.py +++ b/geonode/geoserver/processing/__init__.py @@ -16,3 +16,4 @@ # along with this program. If not, see . # ######################################################################### +default_app_config = "geonode.geoserver.processing.apps.GeoNodeGeoserverProcessingAppConfig" diff --git a/geonode/geoserver/processing/apps.py b/geonode/geoserver/processing/apps.py new file mode 100644 index 00000000000..8b5d10c06cb --- /dev/null +++ b/geonode/geoserver/processing/apps.py @@ -0,0 +1,24 @@ +######################################################################### +# +# Copyright (C) 2023 OSGeo +# +# 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 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### +from django.apps import AppConfig + + +class GeoNodeGeoserverProcessingAppConfig(AppConfig): + name = "geonode.geoserver.processing" + verbose_name = "GeoNode Geoserver Processing" diff --git a/geonode/geoserver/tests/test_helpers.py b/geonode/geoserver/tests/test_helpers.py index 80a0823bf32..cf841bdf457 100644 --- a/geonode/geoserver/tests/test_helpers.py +++ b/geonode/geoserver/tests/test_helpers.py @@ -40,6 +40,7 @@ get_dataset_storetype, extract_name_from_sld, get_dataset_capabilities_url, + get_layer_ows_url, ) from geonode.geoserver.ows import _wcs_link, _wfs_link, _wms_link @@ -292,6 +293,17 @@ def test_dataset_capabilties_url(self): ows_url = settings.GEOSERVER_PUBLIC_LOCATION identifier = "geonode:CA" dataset = Dataset.objects.get(alternate=identifier) - expected_url = f"{ows_url}geonode/CA/wms?service=wms&version=1.3.0&request=GetCapabilities" + expected_url = f"{ows_url}geonode/CA/ows?service=wms&version=1.3.0&request=GetCapabilities" capabilities_url = get_dataset_capabilities_url(dataset) self.assertEqual(capabilities_url, expected_url, capabilities_url) + + @on_ogc_backend(geoserver.BACKEND_PACKAGE) + def test_layer_ows_url(self): + from geonode.layers.models import Dataset + + ows_url = settings.GEOSERVER_PUBLIC_LOCATION + identifier = "geonode:CA" + dataset = Dataset.objects.get(alternate=identifier) + expected_url = f"{ows_url}geonode/CA/ows" + capabilities_url = get_layer_ows_url(dataset) + self.assertEqual(capabilities_url, expected_url, capabilities_url) diff --git a/geonode/groups/models.py b/geonode/groups/models.py index 373d683d40c..59ae6f61392 100644 --- a/geonode/groups/models.py +++ b/geonode/groups/models.py @@ -32,6 +32,7 @@ from django.contrib.auth.models import Group from django.templatetags.static import static from django.contrib.auth import get_user_model +from django.core.exceptions import ObjectDoesNotExist from django.utils.translation import ugettext_lazy as _ from geonode.utils import build_absolute_uri @@ -190,26 +191,43 @@ def can_view(self, user): else: return True - def join(self, user, **kwargs): + def validate_user(self, user): if not user or user.is_anonymous or user == user.get_anonymous(): raise ValueError("The invited user cannot be anonymous") - _members = GroupMember.objects.filter(group=self, user=user) - if not _members.count(): - GroupMember.objects.get_or_create(group=self, user=user, defaults=kwargs) - else: + + def join(self, user, **kwargs): + self.validate_user(user) + try: + GroupMember.objects.get(group=self, user=user) logger.warning(f'The invited user "{user.username}" is already a member') + except ObjectDoesNotExist: + GroupMember.objects.create(group=self, user=user, **kwargs) def leave(self, user, **kwargs): - if not user or user.is_anonymous or user == user.get_anonymous(): - raise ValueError("The invited user cannot be anonymous") + self.validate_user(user) _members = GroupMember.objects.filter(group=self, user=user) - if _members.count(): - for _member in _members: - _member.delete() - user.groups.remove(self.group) + if _members.exists(): + _members.delete() + user.groups.remove(self.group) else: logger.warning(f'The invited user "{user.username}" is not a member') + def promote(self, user, **kwargs): + self.validate_user(user) + try: + _member = GroupMember.objects.get(group=self, user=user) + _member.promote() + except ObjectDoesNotExist: + logger.warning(f'The invited user "{user.username}" is not a member') + + def demote(self, user, **kwargs): + self.validate_user(user) + try: + _member = GroupMember.objects.get(group=self, user=user) + _member.demote() + except ObjectDoesNotExist: + logger.warning(f'The invited user "{user.username}" is not a member') + def get_absolute_url(self): return reverse( "group_detail", diff --git a/geonode/harvesting/harvesters/arcgis.py b/geonode/harvesting/harvesters/arcgis.py index c57202cc404..f64934f5b78 100644 --- a/geonode/harvesting/harvesters/arcgis.py +++ b/geonode/harvesting/harvesters/arcgis.py @@ -205,8 +205,8 @@ def _get_resource_descriptor( epsg_code, spatial_extent = _parse_spatial_extent(layer_representation["extent"]) ows_url = harvestable_resource.unique_identifier.rpartition("/")[0] store = slugify(ows_url) - name = layer_representation["id"] - title = layer_representation["name"] + name = layer_representation.get("id", layer_representation.get("name", "Undefined")) + title = layer_representation.get("name", layer_representation.get("title", "Undefined")) workspace = "remoteWorkspace" alternate = f"{workspace}:{name}" return resourcedescriptor.RecordDescription( @@ -307,8 +307,8 @@ def _get_resource_descriptor( epsg_code, spatial_extent = _parse_spatial_extent(layer_representation["extent"]) ows_url = harvestable_resource.unique_identifier.rpartition("/")[0] store = slugify(ows_url) - name = layer_representation["id"] - title = layer_representation["name"] + name = layer_representation.get("id", layer_representation.get("name", "Undefined")) + title = layer_representation.get("name", layer_representation.get("title", "Undefined")) workspace = "remoteWorkspace" alternate = f"{workspace}:{name}" return resourcedescriptor.RecordDescription( diff --git a/geonode/layers/admin.py b/geonode/layers/admin.py index 379d361943b..7cb712206d1 100644 --- a/geonode/layers/admin.py +++ b/geonode/layers/admin.py @@ -55,6 +55,7 @@ class Meta(ResourceBaseAdminForm.Meta): class DatasetAdmin(TabbedTranslationAdmin): + exclude = ("ll_bbox_polygon", "bbox_polygon", "srid") list_display = ( "id", "alternate", @@ -86,7 +87,7 @@ class DatasetAdmin(TabbedTranslationAdmin): search_fields = ("alternate", "title", "abstract", "purpose", "is_approved", "is_published", "state") filter_horizontal = ("contacts",) date_hierarchy = "date" - readonly_fields = ("uuid", "alternate", "workspace") + readonly_fields = ("uuid", "alternate", "workspace", "geographic_bounding_box") inlines = [AttributeInline] form = DatasetAdminForm actions = [metadata_batch_edit] diff --git a/geonode/layers/api/serializers.py b/geonode/layers/api/serializers.py index fd8736fc4eb..3d39fdefe20 100644 --- a/geonode/layers/api/serializers.py +++ b/geonode/layers/api/serializers.py @@ -88,20 +88,20 @@ def get_attribute(self, instance): else: _attributes = instance.attributes.filter(visible=True).order_by("display_order") if _attributes.exists(): - _template = "
" + _template = '
' for _field in _attributes: _label = _field.attribute_label or _field.attribute _template += '
' if _field.featureinfo_type == Attribute.TYPE_HREF: _template += ( '
%s:
\ - ' + ' % (_label, _field, _field) ) elif _field.featureinfo_type == Attribute.TYPE_IMAGE: _template += ( '
\ - %s
' + %s
' % (_field.attribute, _field.attribute, _label, _label) ) elif _field.featureinfo_type in ( @@ -115,32 +115,32 @@ def get_attribute(self, instance): if "youtube" in _field.featureinfo_type: _template += ( '
\ -
' +
' % (_field.attribute) ) else: _type = f"video/{_field.featureinfo_type[11:]}" _template += ( '
\ -
' +
' % (_field.attribute, _type) ) elif _field.featureinfo_type == Attribute.TYPE_AUDIO: _template += ( '
\ -
' + ' % (_field.attribute) ) elif _field.featureinfo_type == Attribute.TYPE_IFRAME: _template += ( '
\ -
' + ' % (_field.attribute) ) elif _field.featureinfo_type == Attribute.TYPE_PROPERTY: _template += ( '
%s:
\ -
${properties.%s}
' +
${properties[\'%s\']}
' % (_label, _field.attribute) ) _template += "" @@ -173,6 +173,7 @@ class Meta: "featureinfo_custom_template", "ows_url", "capabilities_url", + "dataset_ows_url", "workspace", "default_style", "styles", diff --git a/geonode/layers/api/tests.py b/geonode/layers/api/tests.py index dc6475bfdcc..7361e5c744a 100644 --- a/geonode/layers/api/tests.py +++ b/geonode/layers/api/tests.py @@ -93,8 +93,8 @@ def test_datasets(self): self.assertIsNotNone(response.data["dataset"].get("featureinfo_custom_template")) self.assertEqual( response.data["dataset"].get("featureinfo_custom_template"), - '
Name:
\ -
${properties.name}
', + '
Name:
\ +
${properties[\'name\']}
', ) _dataset.featureinfo_custom_template = "
Foo Bar
" @@ -104,8 +104,8 @@ def test_datasets(self): self.assertIsNotNone(response.data["dataset"].get("featureinfo_custom_template")) self.assertEqual( response.data["dataset"].get("featureinfo_custom_template"), - '
Name:
\ -
${properties.name}
', + '
Name:
\ +
${properties[\'name\']}
', ) _dataset.use_featureinfo_custom_template = True diff --git a/geonode/layers/metadata.py b/geonode/layers/metadata.py index e61577ff5b5..74aab2ce8a9 100644 --- a/geonode/layers/metadata.py +++ b/geonode/layers/metadata.py @@ -79,7 +79,9 @@ def iso2dict(exml): mdata = MD_Metadata(exml) identifier = mdata.identifier vals["language"] = mdata.language or mdata.languagecode or "eng" - vals["spatial_representation_type"] = mdata.hierarchy + if mdata.identification[0].spatialrepresentationtype: + vals["spatial_representation_type"] = mdata.identification[0].spatialrepresentationtype[0] + vals["date"] = sniff_date(mdata.datestamp) if hasattr(mdata, "identification"): diff --git a/geonode/layers/models.py b/geonode/layers/models.py index 11612ed8f09..0d6c425e8e0 100644 --- a/geonode/layers/models.py +++ b/geonode/layers/models.py @@ -261,6 +261,12 @@ def capabilities_url(self): return get_dataset_capabilities_url(self) + @property + def dataset_ows_url(self): + from geonode.geoserver.helpers import get_layer_ows_url + + return get_layer_ows_url(self) + @property def embed_url(self): try: diff --git a/geonode/layers/tests.py b/geonode/layers/tests.py index a24fb52ee01..2deb3ca0a21 100644 --- a/geonode/layers/tests.py +++ b/geonode/layers/tests.py @@ -373,7 +373,6 @@ def test_dataset_links(self): links = Link.objects.filter(resource=lyr.resourcebase_ptr, link_type="image") self.assertIsNotNone(links) - # get and update original link to external Link.objects.filter(resource=lyr.resourcebase_ptr, link_type="original").update( url="http://google.com/test" ) @@ -1211,7 +1210,7 @@ def test_dataset_download_invalid_wps_format(self): self.assertEqual(500, response.status_code) self.assertDictEqual({"error": "The format provided is not valid for the selected resource"}, response.json()) - @patch("geonode.layers.views.HttpClient.request") + @patch("geonode.resource.download_handler.HttpClient.request") def test_dataset_download_call_the_catalog_raise_error_for_no_200(self, mocked_catalog): _response = MagicMock(status_code=500, content="foo-bar") mocked_catalog.return_value = _response, "foo-bar" @@ -1221,12 +1220,9 @@ def test_dataset_download_call_the_catalog_raise_error_for_no_200(self, mocked_c url = reverse("dataset_download", args=[dataset.alternate]) response = self.client.get(url) self.assertEqual(500, response.status_code) - self.assertDictEqual( - {"error": "Download dataset exception: error during call with GeoServer: foo-bar"}, response.json() - ) + self.assertDictEqual({"error": "Download dataset exception: error during call with GeoServer"}, response.json()) - @patch("geonode.layers.views.HttpClient.request") - def test_dataset_download_call_the_catalog_raise_error_for_error_content(self, mocked_catalog): + def test_dataset_download_call_the_catalog_raise_error_for_error_content(self): content = """ @@ -1235,14 +1231,15 @@ def test_dataset_download_call_the_catalog_raise_error_for_error_content(self, m """ # noqa _response = MagicMock(status_code=200, text=content, headers={"Content-Type": "text/xml"}) - mocked_catalog.return_value = _response, content # if settings.USE_GEOSERVER is false, the URL must be redirected self.client.login(username="admin", password="admin") dataset = Dataset.objects.first() - url = reverse("dataset_download", args=[dataset.alternate]) - response = self.client.get(url) - self.assertEqual(500, response.status_code) - self.assertDictEqual({"error": "InvalidParameterValue: Foo Bar Exception"}, response.json()) + with patch("geonode.resource.download_handler.HttpClient.request") as mocked_catalog: + mocked_catalog.return_value = _response, content + url = reverse("dataset_download", args=[dataset.alternate]) + response = self.client.get(url) + self.assertEqual(500, response.status_code) + self.assertDictEqual({"error": "InvalidParameterValue: Foo Bar Exception"}, response.json()) def test_dataset_download_call_the_catalog_works(self): # if settings.USE_GEOSERVER is false, the URL must be redirected @@ -1250,7 +1247,7 @@ def test_dataset_download_call_the_catalog_works(self): self.client.login(username="admin", password="admin") dataset = Dataset.objects.first() layer = create_dataset(dataset.title, dataset.title, dataset.owner, "Point") - with patch("geonode.layers.views.HttpClient.request") as mocked_catalog: + with patch("geonode.resource.download_handler.HttpClient.request") as mocked_catalog: mocked_catalog.return_value = _response, "" url = reverse("dataset_download", args=[layer.alternate]) response = self.client.get(url) @@ -1269,21 +1266,21 @@ def test_dataset_download_call_the_catalog_work_anonymous(self): _response = MagicMock(status_code=200, text="", headers={"Content-Type": ""}) # noqa dataset = Dataset.objects.first() layer = create_dataset(dataset.title, dataset.title, dataset.owner, "Point") - with patch("geonode.layers.views.HttpClient.request") as mocked_catalog: + with patch("geonode.resource.download_handler.HttpClient.request") as mocked_catalog: mocked_catalog.return_value = _response, "" url = reverse("dataset_download", args=[layer.alternate]) response = self.client.get(url) self.assertTrue(response.status_code == 200) @override_settings(USE_GEOSERVER=True) - @patch("geonode.layers.views.get_template") + @patch("geonode.resource.download_handler.get_template") def test_dataset_download_call_the_catalog_work_for_raster(self, pathed_template): # if settings.USE_GEOSERVER is false, the URL must be redirected _response = MagicMock(status_code=200, text="", headers={"Content-Type": ""}) # noqa dataset = Dataset.objects.filter(subtype="raster").first() layer = create_dataset(dataset.title, dataset.title, dataset.owner, "Point") Dataset.objects.filter(alternate=layer.alternate).update(subtype="raster") - with patch("geonode.layers.views.HttpClient.request") as mocked_catalog: + with patch("geonode.resource.download_handler.HttpClient.request") as mocked_catalog: mocked_catalog.return_value = _response, "" url = reverse("dataset_download", args=[layer.alternate]) response = self.client.get(url) @@ -1296,13 +1293,13 @@ def test_dataset_download_call_the_catalog_work_for_raster(self, pathed_template ) @override_settings(USE_GEOSERVER=True) - @patch("geonode.layers.views.get_template") + @patch("geonode.resource.download_handler.get_template") def test_dataset_download_call_the_catalog_work_for_vector(self, pathed_template): # if settings.USE_GEOSERVER is false, the URL must be redirected _response = MagicMock(status_code=200, text="", headers={"Content-Type": ""}) # noqa dataset = Dataset.objects.filter(subtype="vector").first() layer = create_dataset(dataset.title, dataset.title, dataset.owner, "Point") - with patch("geonode.layers.views.HttpClient.request") as mocked_catalog: + with patch("geonode.resource.download_handler.HttpClient.request") as mocked_catalog: mocked_catalog.return_value = _response, "" url = reverse("dataset_download", args=[layer.alternate]) response = self.client.get(url) @@ -1632,7 +1629,7 @@ def test_set_metadata_return_expected_values_from_xml(self): "date": datetime.datetime(2021, 4, 9, 9, 0, 46), "language": "eng", "purpose": None, - "spatial_representation_type": "dataset", + "spatial_representation_type": "vector", "supplemental_information": "No information provided", "temporal_extent_end": None, "temporal_extent_start": None, @@ -1692,7 +1689,7 @@ def setUp(self): "date": datetime.datetime(2021, 4, 9, 9, 0, 46), "language": "eng", "purpose": None, - "spatial_representation_type": "dataset", + "spatial_representation_type": "vector", "supplemental_information": "No information provided", "temporal_extent_end": None, "temporal_extent_start": None, diff --git a/geonode/layers/views.py b/geonode/layers/views.py index 609a7f0c839..d9b08da7f97 100644 --- a/geonode/layers/views.py +++ b/geonode/layers/views.py @@ -23,21 +23,18 @@ import logging import warnings import traceback -from django.urls import reverse from owslib.wfs import WebFeatureService -import xml.etree.ElementTree as ET from django.conf import settings from django.db.models import F -from django.http import Http404, JsonResponse +from django.http import Http404 from django.contrib import messages from django.shortcuts import render from django.utils.html import escape from django.forms.utils import ErrorList from django.contrib.auth import get_user_model -from django.template.loader import get_template from django.utils.translation import ugettext as _ from django.core.exceptions import PermissionDenied from django.forms.models import inlineformset_factory @@ -50,9 +47,8 @@ from geonode import geoserver from geonode.layers.metadata import parse_metadata -from geonode.proxy.views import fetch_response_headers from geonode.resource.manager import resource_manager -from geonode.geoserver.helpers import set_dataset_style, wps_format_is_supported +from geonode.geoserver.helpers import set_dataset_style from geonode.resource.utils import update_resource from geonode.base.auth import get_or_create_token @@ -70,9 +66,10 @@ from geonode.groups.models import GroupProfile from geonode.security.utils import get_user_visible_groups, AdvancedSecurityWorkflowManager from geonode.people.forms import ProfileForm -from geonode.utils import HttpClient, check_ogc_backend, llbbox_to_mercator, resolve_object, mkdtemp +from geonode.utils import check_ogc_backend, llbbox_to_mercator, resolve_object, mkdtemp from geonode.geoserver.helpers import ogc_server_settings, select_relevant_files, write_uploaded_files_to_disk from geonode.geoserver.security import set_geowebcache_invalidate_cache +from django.utils.module_loading import import_string if check_ogc_backend(geoserver.BACKEND_PACKAGE): from geonode.geoserver.helpers import gs_catalog @@ -736,69 +733,8 @@ def dataset_metadata_advanced(request, layername): @csrf_exempt def dataset_download(request, layername): - try: - dataset = _resolve_dataset(request, layername, "base.download_resourcebase", _PERMISSION_MSG_GENERIC) - except Exception as e: - raise Http404(Exception(_("Not found"), e)) - - if not settings.USE_GEOSERVER: - # if GeoServer is not used, we redirect to the proxy download - return HttpResponseRedirect(reverse("download", args=[dataset.id])) - - download_format = request.GET.get("export_format") - - if download_format and not wps_format_is_supported(download_format, dataset.subtype): - logger.error("The format provided is not valid for the selected resource") - return JsonResponse({"error": "The format provided is not valid for the selected resource"}, status=500) - - _format = "application/zip" if dataset.is_vector() else "image/tiff" - # getting default payload - tpl = get_template("geoserver/dataset_download.xml") - ctx = {"alternate": dataset.alternate, "download_format": download_format or _format} - # applying context for the payload - payload = tpl.render(ctx) - - # init of Client - client = HttpClient() - - headers = {"Content-type": "application/xml", "Accept": "application/xml"} - - # defining the URL needed fr the download - url = f"{settings.OGC_SERVER['default']['LOCATION']}ows?service=WPS&version=1.0.0&REQUEST=Execute" - if not request.user.is_anonymous: - # define access token for the user - access_token = get_or_create_token(request.user) - url += f"&access_token={access_token}" - - # request to geoserver - response, content = client.request(url=url, data=payload, method="post", headers=headers) - - if response.status_code != 200: - logger.error(f"Download dataset exception: error during call with GeoServer: {response.content}") - return JsonResponse( - {"error": f"Download dataset exception: error during call with GeoServer: {response.content}"}, status=500 - ) - - # error handling - namespaces = {"ows": "http://www.opengis.net/ows/1.1", "wps": "http://www.opengis.net/wps/1.0.0"} - response_type = response.headers.get("Content-Type") - if response_type == "text/xml": - # parsing XML for get exception - content = ET.fromstring(response.text) - exc = content.find("*//ows:Exception", namespaces=namespaces) or content.find( - "ows:Exception", namespaces=namespaces - ) - if exc: - exc_text = exc.find("ows:ExceptionText", namespaces=namespaces) - logger.error(f"{exc.attrib.get('exceptionCode')} {exc_text.text}") - return JsonResponse({"error": f"{exc.attrib.get('exceptionCode')}: {exc_text.text}"}, status=500) - - return_response = fetch_response_headers( - HttpResponse(content=response.content, status=response.status_code, content_type=download_format), - response.headers, - ) - return_response.headers["Content-Type"] = download_format or _format - return return_response + DownloadHandler = import_string(settings.DATASET_DOWNLOAD_HANDLER) + return DownloadHandler(request, layername).get_download_response() @login_required diff --git a/geonode/local_settings.py.geoserver.sample b/geonode/local_settings.py.geoserver.sample index 733589d6d36..59ac574511b 100644 --- a/geonode/local_settings.py.geoserver.sample +++ b/geonode/local_settings.py.geoserver.sample @@ -26,6 +26,7 @@ import ast import os + try: # python2 from urlparse import urlparse, urlunparse, urlsplit, urljoin except ImportError: @@ -35,159 +36,129 @@ from geonode.settings import * PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__)) -MEDIA_ROOT = os.getenv('MEDIA_ROOT', os.path.join(PROJECT_ROOT, "uploaded")) +MEDIA_ROOT = os.getenv("MEDIA_ROOT", os.path.join(PROJECT_ROOT, "uploaded")) -STATIC_ROOT = os.getenv('STATIC_ROOT', - os.path.join(PROJECT_ROOT, "static_root") - ) +STATIC_ROOT = os.getenv("STATIC_ROOT", os.path.join(PROJECT_ROOT, "static_root")) -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" # Login and logout urls override -LOGIN_URL = os.getenv('LOGIN_URL', '{}account/login/'.format(SITEURL)) -LOGOUT_URL = os.getenv('LOGOUT_URL', '{}account/logout/'.format(SITEURL)) +LOGIN_URL = os.getenv("LOGIN_URL", "{}account/login/".format(SITEURL)) +LOGOUT_URL = os.getenv("LOGOUT_URL", "{}account/logout/".format(SITEURL)) -ACCOUNT_LOGIN_REDIRECT_URL = os.getenv('LOGIN_REDIRECT_URL', SITEURL) -ACCOUNT_LOGOUT_REDIRECT_URL = os.getenv('LOGOUT_REDIRECT_URL', SITEURL) +ACCOUNT_LOGIN_REDIRECT_URL = os.getenv("LOGIN_REDIRECT_URL", SITEURL) +ACCOUNT_LOGOUT_REDIRECT_URL = os.getenv("LOGOUT_REDIRECT_URL", SITEURL) -AVATAR_GRAVATAR_SSL = ast.literal_eval(os.getenv('AVATAR_GRAVATAR_SSL', 'True')) +AVATAR_GRAVATAR_SSL = ast.literal_eval(os.getenv("AVATAR_GRAVATAR_SSL", "True")) # Backend DATABASES = { - 'default': { - 'ENGINE': 'django.contrib.gis.db.backends.postgis', - 'NAME': 'geonode', - 'USER': 'geonode', - 'PASSWORD': 'geonode', - 'HOST': 'localhost', - 'PORT': '5432', - 'CONN_MAX_AGE': 0, - 'CONN_TOUT': 5, - 'OPTIONS': { - 'connect_timeout': 5, - } + "default": { + "ENGINE": "django.contrib.gis.db.backends.postgis", + "NAME": "geonode", + "USER": "geonode", + "PASSWORD": "geonode", + "HOST": "localhost", + "PORT": "5432", + "CONN_MAX_AGE": 0, + "CONN_TOUT": 5, + "OPTIONS": { + "connect_timeout": 5, + }, }, # vector datastore for uploads - 'datastore': { - 'ENGINE': 'django.contrib.gis.db.backends.postgis', + "datastore": { + "ENGINE": "django.contrib.gis.db.backends.postgis", # 'ENGINE': '', # Empty ENGINE name disables - 'NAME': 'geonode_data', - 'USER': 'geonode', - 'PASSWORD': 'geonode', - 'HOST': 'localhost', - 'PORT': '5432', - 'CONN_MAX_AGE': 0, - 'CONN_TOUT': 5, - 'OPTIONS': { - 'connect_timeout': 5, - } - } + "NAME": "geonode_data", + "USER": "geonode", + "PASSWORD": "geonode", + "HOST": "localhost", + "PORT": "5432", + "CONN_MAX_AGE": 0, + "CONN_TOUT": 5, + "OPTIONS": { + "connect_timeout": 5, + }, + }, } -GEOSERVER_LOCATION = os.getenv( - 'GEOSERVER_LOCATION', 'http://localhost:8080/geoserver/' -) +GEOSERVER_LOCATION = os.getenv("GEOSERVER_LOCATION", "http://localhost:8080/geoserver/") -GEOSERVER_PUBLIC_HOST = os.getenv( - 'GEOSERVER_PUBLIC_HOST', SITE_HOST_NAME -) +GEOSERVER_PUBLIC_HOST = os.getenv("GEOSERVER_PUBLIC_HOST", SITE_HOST_NAME) -GEOSERVER_PUBLIC_PORT = os.getenv( - 'GEOSERVER_PUBLIC_PORT', 8080 -) +GEOSERVER_PUBLIC_PORT = os.getenv("GEOSERVER_PUBLIC_PORT", 8080) -_default_public_location = 'http://{}:{}/geoserver/'.format(GEOSERVER_PUBLIC_HOST, GEOSERVER_PUBLIC_PORT) if GEOSERVER_PUBLIC_PORT else 'http://{}/geoserver/'.format(GEOSERVER_PUBLIC_HOST) - -GEOSERVER_WEB_UI_LOCATION = os.getenv( - 'GEOSERVER_WEB_UI_LOCATION', GEOSERVER_LOCATION +_default_public_location = ( + "http://{}:{}/geoserver/".format(GEOSERVER_PUBLIC_HOST, GEOSERVER_PUBLIC_PORT) + if GEOSERVER_PUBLIC_PORT + else "http://{}/geoserver/".format(GEOSERVER_PUBLIC_HOST) ) -GEOSERVER_PUBLIC_LOCATION = os.getenv( - 'GEOSERVER_PUBLIC_LOCATION', _default_public_location -) +GEOSERVER_WEB_UI_LOCATION = os.getenv("GEOSERVER_WEB_UI_LOCATION", GEOSERVER_LOCATION) -OGC_SERVER_DEFAULT_USER = os.getenv( - 'GEOSERVER_ADMIN_USER', 'admin' -) +GEOSERVER_PUBLIC_LOCATION = os.getenv("GEOSERVER_PUBLIC_LOCATION", _default_public_location) -OGC_SERVER_DEFAULT_PASSWORD = os.getenv( - 'GEOSERVER_ADMIN_PASSWORD', 'geoserver' -) +GEOSERVER_ADMIN_USER = os.getenv("GEOSERVER_ADMIN_USER", "admin") + +GEOSERVER_ADMIN_PASSWORD = os.getenv("GEOSERVER_ADMIN_PASSWORD", "geoserver") # OGC (WMS/WFS/WCS) Server Settings OGC_SERVER = { - 'default': { - 'BACKEND': 'geonode.geoserver', - 'LOCATION': GEOSERVER_LOCATION, - 'WEB_UI_LOCATION': GEOSERVER_WEB_UI_LOCATION, - 'LOGIN_ENDPOINT': 'j_spring_oauth2_geonode_login', - 'LOGOUT_ENDPOINT': 'j_spring_oauth2_geonode_logout', + "default": { + "BACKEND": "geonode.geoserver", + "LOCATION": GEOSERVER_LOCATION, + "WEB_UI_LOCATION": GEOSERVER_WEB_UI_LOCATION, + "LOGIN_ENDPOINT": "j_spring_oauth2_geonode_login", + "LOGOUT_ENDPOINT": "j_spring_oauth2_geonode_logout", # PUBLIC_LOCATION needs to be kept like this because in dev mode # the proxy won't work and the integration tests will fail # the entire block has to be overridden in the local_settings - 'PUBLIC_LOCATION': GEOSERVER_PUBLIC_LOCATION, - 'USER': OGC_SERVER_DEFAULT_USER, - 'PASSWORD': OGC_SERVER_DEFAULT_PASSWORD, - 'MAPFISH_PRINT_ENABLED': True, - 'PRINT_NG_ENABLED': True, - 'GEONODE_SECURITY_ENABLED': True, - 'GEOFENCE_SECURITY_ENABLED': True, - 'GEOFENCE_TIMEOUT': int(os.getenv('GEOFENCE_TIMEOUT', os.getenv('OGC_REQUEST_TIMEOUT', '60'))), - 'WMST_ENABLED': False, - 'BACKEND_WRITE_ENABLED': True, - 'WPS_ENABLED': False, - 'LOG_FILE': '%s/geoserver/data/logs/geoserver.log' % os.path.abspath(os.path.join(PROJECT_ROOT, os.pardir)), + "PUBLIC_LOCATION": GEOSERVER_PUBLIC_LOCATION, + "USER": GEOSERVER_ADMIN_USER, + "PASSWORD": GEOSERVER_ADMIN_PASSWORD, + "MAPFISH_PRINT_ENABLED": True, + "PRINT_NG_ENABLED": True, + "GEONODE_SECURITY_ENABLED": True, + "GEOFENCE_SECURITY_ENABLED": True, + "GEOFENCE_TIMEOUT": int(os.getenv("GEOFENCE_TIMEOUT", os.getenv("OGC_REQUEST_TIMEOUT", "60"))), + "WMST_ENABLED": False, + "BACKEND_WRITE_ENABLED": True, + "WPS_ENABLED": False, + "LOG_FILE": "%s/geoserver/data/logs/geoserver.log" % os.path.abspath(os.path.join(PROJECT_ROOT, os.pardir)), # Set to dictionary identifier of database containing spatial data in DATABASES dictionary to enable - 'DATASTORE': 'datastore', - 'TIMEOUT': int(os.getenv('OGC_REQUEST_TIMEOUT', '60')), - 'MAX_RETRIES': int(os.getenv('OGC_REQUEST_MAX_RETRIES', '5')), - 'BACKOFF_FACTOR': float(os.getenv('OGC_REQUEST_BACKOFF_FACTOR', '0.3')), - 'POOL_MAXSIZE': int(os.getenv('OGC_REQUEST_POOL_MAXSIZE', '10')), - 'POOL_CONNECTIONS': int(os.getenv('OGC_REQUEST_POOL_CONNECTIONS', '10')), + "DATASTORE": "datastore", + "TIMEOUT": int(os.getenv("OGC_REQUEST_TIMEOUT", "60")), + "MAX_RETRIES": int(os.getenv("OGC_REQUEST_MAX_RETRIES", "5")), + "BACKOFF_FACTOR": float(os.getenv("OGC_REQUEST_BACKOFF_FACTOR", "0.3")), + "POOL_MAXSIZE": int(os.getenv("OGC_REQUEST_POOL_MAXSIZE", "10")), + "POOL_CONNECTIONS": int(os.getenv("OGC_REQUEST_POOL_CONNECTIONS", "10")), } } # If you want to enable Mosaics use the following configuration UPLOADER = { - 'BACKEND': 'geonode.importer', - 'OPTIONS': { - 'TIME_ENABLED': True, - 'MOSAIC_ENABLED': False, + "BACKEND": "geonode.importer", + "OPTIONS": { + "TIME_ENABLED": True, + "MOSAIC_ENABLED": False, }, - 'SUPPORTED_CRS': [ - 'EPSG:4326', - 'EPSG:3785', - 'EPSG:3857', - 'EPSG:32647', - 'EPSG:32736' - ], - 'SUPPORTED_EXT': [ - '.shp', - '.csv', - '.kml', - '.kmz', - '.json', - '.geojson', - '.tif', - '.tiff', - '.geotiff', - '.gml', - '.xml' - ] + "SUPPORTED_CRS": ["EPSG:4326", "EPSG:3785", "EPSG:3857", "EPSG:32647", "EPSG:32736"], + "SUPPORTED_EXT": [".shp", ".csv", ".kml", ".kmz", ".json", ".geojson", ".tif", ".tiff", ".geotiff", ".gml", ".xml"], } # CSW settings CATALOGUE = { - 'default': { + "default": { # The underlying CSW implementation # default is pycsw in local mode (tied directly to GeoNode Django DB) - 'ENGINE': 'geonode.catalogue.backends.pycsw_local', + "ENGINE": "geonode.catalogue.backends.pycsw_local", # pycsw in non-local mode # 'ENGINE': 'geonode.catalogue.backends.pycsw_http', # deegree and others # 'ENGINE': 'geonode.catalogue.backends.generic', # The FULLY QUALIFIED base url to the CSW instance for this GeoNode - 'URL': urljoin(SITEURL, '/catalogue/csw'), + "URL": urljoin(SITEURL, "/catalogue/csw"), # 'URL': 'http://localhost:8080/deegree-csw-demo-3.0.4/services', # 'ALTERNATES_ONLY': True, } @@ -196,69 +167,67 @@ CATALOGUE = { # pycsw settings PYCSW = { # pycsw configuration - 'CONFIGURATION': { + "CONFIGURATION": { # uncomment / adjust to override server config system defaults # 'server': { # 'maxrecords': '10', # 'pretty_print': 'true', # 'federatedcatalogues': 'http://catalog.data.gov/csw' # }, - 'server': { - 'home': '.', - 'url': CATALOGUE['default']['URL'], - 'encoding': 'UTF-8', - 'language': LANGUAGE_CODE, - 'maxrecords': '20', - 'pretty_print': 'true', + "server": { + "home": ".", + "url": CATALOGUE["default"]["URL"], + "encoding": "UTF-8", + "language": LANGUAGE_CODE, + "maxrecords": "20", + "pretty_print": "true", # 'domainquerytype': 'range', - 'domaincounts': 'true', - 'profiles': 'apiso,ebrim', + "domaincounts": "true", + "profiles": "apiso,ebrim", }, - 'manager': { + "manager": { # authentication/authorization is handled by Django - 'transactions': 'false', - 'allowed_ips': '*', + "transactions": "false", + "allowed_ips": "*", # 'csw_harvest_pagesize': '10', }, - 'metadata:main': { - 'identification_title': 'GeoNode Catalogue', - 'identification_abstract': 'GeoNode is an open source platform' \ - ' that facilitates the creation, sharing, and collaborative use' \ - ' of geospatial data', - 'identification_keywords': 'sdi, catalogue, discovery, metadata,' \ - ' GeoNode', - 'identification_keywords_type': 'theme', - 'identification_fees': 'None', - 'identification_accessconstraints': 'None', - 'provider_name': 'Organization Name', - 'provider_url': SITEURL, - 'contact_name': 'Lastname, Firstname', - 'contact_position': 'Position Title', - 'contact_address': 'Mailing Address', - 'contact_city': 'City', - 'contact_stateorprovince': 'Administrative Area', - 'contact_postalcode': 'Zip or Postal Code', - 'contact_country': 'Country', - 'contact_phone': '+xx-xxx-xxx-xxxx', - 'contact_fax': '+xx-xxx-xxx-xxxx', - 'contact_email': 'Email Address', - 'contact_url': 'Contact URL', - 'contact_hours': 'Hours of Service', - 'contact_instructions': 'During hours of service. Off on ' \ - 'weekends.', - 'contact_role': 'pointOfContact', + "metadata:main": { + "identification_title": "GeoNode Catalogue", + "identification_abstract": "GeoNode is an open source platform" + " that facilitates the creation, sharing, and collaborative use" + " of geospatial data", + "identification_keywords": "sdi, catalogue, discovery, metadata," " GeoNode", + "identification_keywords_type": "theme", + "identification_fees": "None", + "identification_accessconstraints": "None", + "provider_name": "Organization Name", + "provider_url": SITEURL, + "contact_name": "Lastname, Firstname", + "contact_position": "Position Title", + "contact_address": "Mailing Address", + "contact_city": "City", + "contact_stateorprovince": "Administrative Area", + "contact_postalcode": "Zip or Postal Code", + "contact_country": "Country", + "contact_phone": "+xx-xxx-xxx-xxxx", + "contact_fax": "+xx-xxx-xxx-xxxx", + "contact_email": "Email Address", + "contact_url": "Contact URL", + "contact_hours": "Hours of Service", + "contact_instructions": "During hours of service. Off on " "weekends.", + "contact_role": "pointOfContact", + }, + "metadata:inspire": { + "enabled": "true", + "languages_supported": "eng,gre", + "default_language": "eng", + "date": "YYYY-MM-DD", + "gemet_keywords": "Utility and governmental services", + "conformity_service": "notEvaluated", + "contact_name": "Organization Name", + "contact_email": "Email Address", + "temp_extent": "YYYY-MM-DD/YYYY-MM-DD", }, - 'metadata:inspire': { - 'enabled': 'true', - 'languages_supported': 'eng,gre', - 'default_language': 'eng', - 'date': 'YYYY-MM-DD', - 'gemet_keywords': 'Utility and governmental services', - 'conformity_service': 'notEvaluated', - 'contact_name': 'Organization Name', - 'contact_email': 'Email Address', - 'temp_extent': 'YYYY-MM-DD/YYYY-MM-DD', - } } } @@ -268,50 +237,51 @@ PYCSW = { # default map projection # Note: If set to EPSG:4326, then only EPSG:4326 basemaps will work. -DEFAULT_MAP_CRS = os.environ.get('DEFAULT_MAP_CRS', "EPSG:3857") +DEFAULT_MAP_CRS = os.environ.get("DEFAULT_MAP_CRS", "EPSG:3857") -DEFAULT_LAYER_FORMAT = os.environ.get('DEFAULT_LAYER_FORMAT', "image/png") +DEFAULT_LAYER_FORMAT = os.environ.get("DEFAULT_LAYER_FORMAT", "image/png") # Where should newly created maps be focused? -DEFAULT_MAP_CENTER = (os.environ.get('DEFAULT_MAP_CENTER_X', 0), os.environ.get('DEFAULT_MAP_CENTER_Y', 0)) +DEFAULT_MAP_CENTER = (os.environ.get("DEFAULT_MAP_CENTER_X", 0), os.environ.get("DEFAULT_MAP_CENTER_Y", 0)) # How tightly zoomed should newly created maps be? # 0 = entire world; # maximum zoom is between 12 and 15 (for Google Maps, coverage varies by area) -DEFAULT_MAP_ZOOM = int(os.environ.get('DEFAULT_MAP_ZOOM', 3)) +DEFAULT_MAP_ZOOM = int(os.environ.get("DEFAULT_MAP_ZOOM", 3)) -MAPBOX_ACCESS_TOKEN = os.environ.get('MAPBOX_ACCESS_TOKEN', None) -BING_API_KEY = os.environ.get('BING_API_KEY', None) -GOOGLE_API_KEY = os.environ.get('GOOGLE_API_KEY', None) +MAPBOX_ACCESS_TOKEN = os.environ.get("MAPBOX_ACCESS_TOKEN", None) +BING_API_KEY = os.environ.get("BING_API_KEY", None) +GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY", None) -GEONODE_CLIENT_LAYER_PREVIEW_LIBRARY = os.getenv('GEONODE_CLIENT_LAYER_PREVIEW_LIBRARY', 'mapstore') +GEONODE_CLIENT_LAYER_PREVIEW_LIBRARY = os.getenv("GEONODE_CLIENT_LAYER_PREVIEW_LIBRARY", "mapstore") MAP_BASELAYERS = [{}] """ MapStore2 REACT based Client parameters """ -if GEONODE_CLIENT_LAYER_PREVIEW_LIBRARY == 'mapstore': - GEONODE_CLIENT_HOOKSET = os.getenv('GEONODE_CLIENT_HOOKSET', 'geonode_mapstore_client.hooksets.MapStoreHookSet') +if GEONODE_CLIENT_LAYER_PREVIEW_LIBRARY == "mapstore": + GEONODE_CLIENT_HOOKSET = os.getenv("GEONODE_CLIENT_HOOKSET", "geonode_mapstore_client.hooksets.MapStoreHookSet") - if 'geonode_mapstore_client' not in INSTALLED_APPS: + if "geonode_mapstore_client" not in INSTALLED_APPS: INSTALLED_APPS += ( - 'mapstore2_adapter', - 'geonode_mapstore_client',) + "mapstore2_adapter", + "geonode_mapstore_client", + ) def get_geonode_catalogue_service(): if PYCSW: pycsw_config = PYCSW["CONFIGURATION"] if pycsw_config: - pycsw_catalogue = { - ("%s" % pycsw_config['metadata:main']['identification_title']): { - "url": CATALOGUE['default']['URL'], - "type": "csw", - "title": pycsw_config['metadata:main']['identification_title'], - "autoload": True - } + pycsw_catalogue = { + ("%s" % pycsw_config["metadata:main"]["identification_title"]): { + "url": CATALOGUE["default"]["URL"], + "type": "csw", + "title": pycsw_config["metadata:main"]["identification_title"], + "autoload": True, } - return pycsw_catalogue + } + return pycsw_catalogue return None GEONODE_CATALOGUE_SERVICE = get_geonode_catalogue_service() @@ -321,7 +291,9 @@ if GEONODE_CLIENT_LAYER_PREVIEW_LIBRARY == 'mapstore': MAPSTORE_CATALOGUE_SELECTED_SERVICE = "" if GEONODE_CATALOGUE_SERVICE: - MAPSTORE_CATALOGUE_SERVICES[list(GEONODE_CATALOGUE_SERVICE.keys())[0]] = GEONODE_CATALOGUE_SERVICE[list(GEONODE_CATALOGUE_SERVICE.keys())[0]] + MAPSTORE_CATALOGUE_SERVICES[list(GEONODE_CATALOGUE_SERVICE.keys())[0]] = GEONODE_CATALOGUE_SERVICE[ + list(GEONODE_CATALOGUE_SERVICE.keys())[0] + ] MAPSTORE_CATALOGUE_SELECTED_SERVICE = list(GEONODE_CATALOGUE_SERVICE.keys())[0] DEFAULT_MS2_BACKGROUNDS = [ @@ -333,7 +305,7 @@ if GEONODE_CLIENT_LAYER_PREVIEW_LIBRARY == 'mapstore': "source": "Stamen", "group": "background", "thumbURL": "https://stamen-tiles-c.a.ssl.fastly.net/watercolor/0/0/0.jpg", - "visibility": False + "visibility": False, }, { "type": "tileprovider", @@ -343,7 +315,7 @@ if GEONODE_CLIENT_LAYER_PREVIEW_LIBRARY == 'mapstore': "source": "Stamen", "group": "background", "thumbURL": "https://stamen-tiles-d.a.ssl.fastly.net/terrain/0/0/0.png", - "visibility": False + "visibility": False, }, { "type": "tileprovider", @@ -353,7 +325,7 @@ if GEONODE_CLIENT_LAYER_PREVIEW_LIBRARY == 'mapstore': "source": "Stamen", "group": "background", "thumbURL": "https://stamen-tiles-d.a.ssl.fastly.net/toner/0/0/0.png", - "visibility": False + "visibility": False, }, { "type": "osm", @@ -361,7 +333,7 @@ if GEONODE_CLIENT_LAYER_PREVIEW_LIBRARY == 'mapstore': "name": "mapnik", "source": "osm", "group": "background", - "visibility": True + "visibility": True, }, { "type": "tileprovider", @@ -370,7 +342,7 @@ if GEONODE_CLIENT_LAYER_PREVIEW_LIBRARY == 'mapstore': "name": "OpenTopoMap", "source": "OpenTopoMap", "group": "background", - "visibility": False + "visibility": False, }, { "type": "wms", @@ -384,14 +356,14 @@ if GEONODE_CLIENT_LAYER_PREVIEW_LIBRARY == 'mapstore': "https://maps3.geosolutionsgroup.com/geoserver/wms", "https://maps4.geosolutionsgroup.com/geoserver/wms", "https://maps5.geosolutionsgroup.com/geoserver/wms", - "https://maps6.geosolutionsgroup.com/geoserver/wms" + "https://maps6.geosolutionsgroup.com/geoserver/wms", ], "group": "background", "thumbURL": f"{SITEURL}static/mapstorestyle/img/s2cloudless-s2cloudless.png", "visibility": False, "credits": { - "title": "Sentinel-2 cloudless 2016 by EOX IT Services GmbH" - } + "title": 'Sentinel-2 cloudless 2016 by EOX IT Services GmbH' + }, }, { "source": "ol", @@ -401,8 +373,8 @@ if GEONODE_CLIENT_LAYER_PREVIEW_LIBRARY == 'mapstore': "title": "Empty Background", "type": "empty", "visibility": False, - "args": ["Empty Background", {"visibility": False}] - } + "args": ["Empty Background", {"visibility": False}], + }, ] if MAPBOX_ACCESS_TOKEN: @@ -413,11 +385,14 @@ if GEONODE_CLIENT_LAYER_PREVIEW_LIBRARY == 'mapstore': "name": "MapBox streets-v11", "accessToken": "%s" % MAPBOX_ACCESS_TOKEN, "source": "streets-v11", - "thumbURL": "https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/256/6/33/23?access_token=%s" % MAPBOX_ACCESS_TOKEN, + "thumbURL": "https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/256/6/33/23?access_token=%s" + % MAPBOX_ACCESS_TOKEN, "group": "background", - "visibility": True + "visibility": True, } - DEFAULT_MS2_BACKGROUNDS = [MAPBOX_BASEMAPS,] + DEFAULT_MS2_BACKGROUNDS + DEFAULT_MS2_BACKGROUNDS = [ + MAPBOX_BASEMAPS, + ] + DEFAULT_MS2_BACKGROUNDS if BING_API_KEY: BING_BASEMAPS = [ @@ -428,7 +403,7 @@ if GEONODE_CLIENT_LAYER_PREVIEW_LIBRARY == 'mapstore': "source": "bing", "group": "background", "apiKey": "{{apiKey}}", - "visibility": True + "visibility": True, }, { "type": "bing", @@ -438,7 +413,7 @@ if GEONODE_CLIENT_LAYER_PREVIEW_LIBRARY == 'mapstore': "group": "background", "apiKey": "{{apiKey}}", "thumbURL": "%sstatic/mapstorestyle/img/bing_road_on_demand.png" % SITEURL, - "visibility": False + "visibility": False, }, { "type": "bing", @@ -448,7 +423,7 @@ if GEONODE_CLIENT_LAYER_PREVIEW_LIBRARY == 'mapstore': "group": "background", "apiKey": "{{apiKey}}", "thumbURL": "%sstatic/mapstorestyle/img/bing_aerial_w_labels.png" % SITEURL, - "visibility": False + "visibility": False, }, { "type": "bing", @@ -458,64 +433,67 @@ if GEONODE_CLIENT_LAYER_PREVIEW_LIBRARY == 'mapstore': "group": "background", "apiKey": "{{apiKey}}", "thumbURL": "%sstatic/mapstorestyle/img/bing_canvas_dark.png" % SITEURL, - "visibility": False - } + "visibility": False, + }, ] - DEFAULT_MS2_BACKGROUNDS = [BING_BASEMAPS, ] + DEFAULT_MS2_BACKGROUNDS + DEFAULT_MS2_BACKGROUNDS = [ + BING_BASEMAPS, + ] + DEFAULT_MS2_BACKGROUNDS MAPSTORE_BASELAYERS = DEFAULT_MS2_BACKGROUNDS # MAPSTORE_BASELAYERS_SOURCES allow to configure tilematrix sets for wmts layers - MAPSTORE_BASELAYERS_SOURCES = os.environ.get('MAPSTORE_BASELAYERS_SOURCES', {}) + MAPSTORE_BASELAYERS_SOURCES = os.environ.get("MAPSTORE_BASELAYERS_SOURCES", {}) # -- END Client Hooksets Setup LOGGING = { - 'version': 1, - 'disable_existing_loggers': True, - 'formatters': { - 'verbose': { - 'format': '%(levelname)s %(asctime)s %(module)s %(process)d ' - '%(thread)d %(message)s' - }, - 'simple': { - 'format': '%(message)s', + "version": 1, + "disable_existing_loggers": True, + "formatters": { + "verbose": {"format": "%(levelname)s %(asctime)s %(module)s %(process)d " "%(thread)d %(message)s"}, + "simple": { + "format": "%(message)s", }, }, - 'filters': { - 'require_debug_false': { - '()': 'django.utils.log.RequireDebugFalse' - } - }, - 'handlers': { - 'console': { - 'level': 'INFO', - 'class': 'logging.StreamHandler', - 'formatter': 'simple' + "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}}, + "handlers": { + "console": {"level": "INFO", "class": "logging.StreamHandler", "formatter": "simple"}, + "mail_admins": { + "level": "ERROR", + "filters": ["require_debug_false"], + "class": "django.utils.log.AdminEmailHandler", }, - 'mail_admins': { - 'level': 'ERROR', - 'filters': ['require_debug_false'], - 'class': 'django.utils.log.AdminEmailHandler', - } }, "loggers": { "django": { - "handlers": ["console"], "level": "ERROR", }, + "handlers": ["console"], + "level": "ERROR", + }, "geonode": { - "handlers": ["console"], "level": "INFO", }, + "handlers": ["console"], + "level": "INFO", + }, "geoserver-restconfig.catalog": { - "handlers": ["console"], "level": "ERROR", }, + "handlers": ["console"], + "level": "ERROR", + }, "owslib": { - "handlers": ["console"], "level": "ERROR", }, + "handlers": ["console"], + "level": "ERROR", + }, "pycsw": { - "handlers": ["console"], "level": "INFO", }, + "handlers": ["console"], + "level": "INFO", + }, "celery": { - 'handlers': ["console"], 'level': 'ERROR', }, + "handlers": ["console"], + "level": "ERROR", + }, }, } # Additional settings -X_FRAME_OPTIONS = 'ALLOW-FROM %s' % SITEURL +X_FRAME_OPTIONS = "ALLOW-FROM %s" % SITEURL CORS_ALLOW_ALL_ORIGINS = True GEOIP_PATH = "/usr/local/share/GeoIP" diff --git a/geonode/locale/en/LC_MESSAGES/django.mo b/geonode/locale/en/LC_MESSAGES/django.mo index f8663d06708..b7be4680ab4 100644 Binary files a/geonode/locale/en/LC_MESSAGES/django.mo and b/geonode/locale/en/LC_MESSAGES/django.mo differ diff --git a/geonode/locale/en/LC_MESSAGES/django.po b/geonode/locale/en/LC_MESSAGES/django.po index ae445ae92c7..d6ce030a65a 100644 --- a/geonode/locale/en/LC_MESSAGES/django.po +++ b/geonode/locale/en/LC_MESSAGES/django.po @@ -3982,7 +3982,7 @@ msgid "introduce yourself" msgstr "introduce yourself" msgid "Position Name" -msgstr "Position Name" +msgstr "Position" msgid "role or position of the responsible person" msgstr "role or position of the responsible person" diff --git a/geonode/locale/it/LC_MESSAGES/django.mo b/geonode/locale/it/LC_MESSAGES/django.mo index a093d7f2986..ca8e5e19874 100644 Binary files a/geonode/locale/it/LC_MESSAGES/django.mo and b/geonode/locale/it/LC_MESSAGES/django.mo differ diff --git a/geonode/locale/it/LC_MESSAGES/django.po b/geonode/locale/it/LC_MESSAGES/django.po index 154a9219bfc..8791b1cfec4 100644 --- a/geonode/locale/it/LC_MESSAGES/django.po +++ b/geonode/locale/it/LC_MESSAGES/django.po @@ -39,16 +39,15 @@ msgstr "" "Project-Id-Version: GeoNode\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-07-08 09:02+0300\n" -"PO-Revision-Date: 2021-04-29 13:43+0200\n" +"PO-Revision-Date: 2023-07-20 11:35+0200\n" "Last-Translator: Julien Collaer \n" -"Language-Team: Italian (http://www.transifex.com/geonode/geonode/language/" -"it/)\n" +"Language-Team: Italian (http://www.transifex.com/geonode/geonode/language/it/)\n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Poedit 2.4.2\n" +"X-Generator: Poedit 3.0.1\n" msgid "Request to download a resource" msgstr "Richiedi di scaricare la risorsa" @@ -60,8 +59,7 @@ msgid "Request resource change" msgstr "Richiedi di modificare la risorsa" msgid "Owner has requested permissions to modify a resource" -msgstr "" -"Il proprietario ha richiesto le autorizzazioni per modificare la risorsa" +msgstr "Il proprietario ha richiesto le autorizzazioni per modificare la risorsa" msgid "series" msgstr "serie" @@ -199,11 +197,10 @@ msgid "Free-text Keywords" msgstr "Free-text Keywords" msgid "" -"A space or comma-separated list of keywords. Use the widget to select from " -"Hierarchical tree." +"A space or comma-separated list of keywords. Use the widget to select from Hierarchical tree." msgstr "" -"Uno spazio o un elenco delimitato da virgole di parole chiave. Utilizzare il " -"widget per selezionare dalla struttura ad albero gerarchica." +"Uno spazio o un elenco delimitato da virgole di parole chiave. Utilizzare il widget per " +"selezionare dalla struttura ad albero gerarchica." msgid "Regions" msgstr "Regioni" @@ -220,11 +217,9 @@ msgstr "Licenza" msgid "Language" msgstr "Lingua" -#, fuzzy -#| msgid "Users" msgid "User" msgid_plural "Users" -msgstr[0] "Utenti" +msgstr[0] "Utente" msgstr[1] "Utenti" msgid "Permission Type" @@ -258,41 +253,37 @@ msgid "version of the cited resource" msgstr "versione della risorsa citata" msgid "" -"authority or function assigned, as to a ruler, legislative assembly, " -"delegate, or the like." +"authority or function assigned, as to a ruler, legislative assembly, delegate, or the like." msgstr "" -"autorità o funzione assegnata, ad esempio un regolatore, un'assemblea " -"legislativa, un delegato o simili." +"autorità o funzione assegnata, ad esempio un regolatore, un'assemblea legislativa, un " +"delegato o simili." msgid "a DOI will be added by Admin before publication." msgstr "un DOI verrà aggiunto dall'amministratore prima della pubblicazione." msgid "summary of the intentions with which the resource(s) was developed" -msgstr "" -"sintesi delle intenzioni con cui la risorsa (o le risorse) sono state " -"sviluppate" +msgstr "sintesi delle intenzioni con cui la risorsa (o le risorse) sono state sviluppate" msgid "" -"frequency with which modifications and deletions are made to the data after " -"it is first produced" +"frequency with which modifications and deletions are made to the data after it is first " +"produced" msgstr "" -"frequenza con cui le modifiche e le cancellazioni sui dati vengono eseguite " -"dopo essere stati prodotti la prima volta" +"frequenza con cui le modifiche e le cancellazioni sui dati vengono eseguite dopo essere " +"stati prodotti la prima volta" msgid "" -"commonly used word(s) or formalised word(s) or phrase(s) used to describe " -"the subject (space or comma-separated)" +"commonly used word(s) or formalised word(s) or phrase(s) used to describe the subject (space " +"or comma-separated)" msgstr "" -"parola (o parole) comunemente usate o parola (o parole) e frase (o frasi) " -"formalizzate usate per descrivere il soggetto (separando attraverso spazio o " -"virgola" +"parola (o parole) comunemente usate o parola (o parole) e frase (o frasi) formalizzate usate " +"per descrivere il soggetto (separando attraverso spazio o virgola" msgid "" -"formalised word(s) or phrase(s) from a fixed thesaurus used to describe the " -"subject (space or comma-separated)" +"formalised word(s) or phrase(s) from a fixed thesaurus used to describe the subject (space " +"or comma-separated)" msgstr "" -"parola(e) o frase(i) formalizzate da un thesaurus fisso utilizzato per " -"descrivere l'oggetto (spazio o delimitato da virgole)" +"parola(e) o frase(i) formalizzate da un thesaurus fisso utilizzato per descrivere l'oggetto " +"(spazio o delimitato da virgole)" msgid "keyword identifies a location" msgstr "parola chiave che identifica una posizione" @@ -301,11 +292,9 @@ msgid "limitation(s) placed upon the access or use of the data." msgstr "limitazione(i) sull'accesso o l'uso dei dati." msgid "" -"other restrictions and legal prerequisites for accessing and using the " -"resource or metadata" +"other restrictions and legal prerequisites for accessing and using the resource or metadata" msgstr "" -"altre restrizioni e requisiti legali per l'accesso e l'uso della risorsa o " -"dei metadati" +"altre restrizioni e requisiti legali per l'accesso e l'uso della risorsa o dei metadati" msgid "license of the dataset" msgstr "licenza del dataset" @@ -314,11 +303,11 @@ msgid "language used within the dataset" msgstr "lingua usata all'interno del dataset" msgid "" -"high-level geographic data thematic classification to assist in the grouping " -"and search of available geographic data sets." +"high-level geographic data thematic classification to assist in the grouping and search of " +"available geographic data sets." msgstr "" -"classificazione tematica di dati geografici di alto livello per facilitare " -"il raggruppamento e la ricerca di set di dati geografici disponibili." +"classificazione tematica di dati geografici di alto livello per facilitare il raggruppamento " +"e la ricerca di set di dati geografici disponibili." msgid "method used to represent geographic information in the dataset." msgstr "metodo di rappresentazione spaziale del dataset." @@ -329,12 +318,10 @@ msgstr "periodo di tempo coperto dal contenuto del dataset (inizio)" msgid "time period covered by the content of the dataset (end)" msgstr "periodo di tempo coperto dal contenuto del dataset (fine)" -msgid "" -"general explanation of the data producer's knowledge about the lineage of a " -"dataset" +msgid "general explanation of the data producer's knowledge about the lineage of a dataset" msgstr "" -"spiegazione generale della conoscenza del produttore dei dati riguardo il " -"lignaggio di un dataset" +"spiegazione generale della conoscenza del produttore dei dati riguardo il lignaggio di un " +"dataset" msgid "title" msgstr "titolo" @@ -438,10 +425,8 @@ msgstr "Questa risorsa è stata convalidata da un editore o un curatore?" msgid "Thumbnail url" msgstr "Url della miniatura" -#, fuzzy -#| msgid "Dirty State" msgid "State" -msgstr "Non ancora aggiornato" +msgstr "Stato" msgid "Hold the resource processing state." msgstr "" @@ -621,25 +606,17 @@ msgid "Reason for the request" msgstr "Motivo della richiesta" msgid "" -"To allow the change, set the resource to not \"Approved\" under the metadata " -"settingsand write message to the owner to notify him" +"To allow the change, set the resource to not \"Approved\" under the metadata settingsand " +"write message to the owner to notify him" msgstr "" -"Per consentire la modifica, impostare la risorsa su non \"Approvato\" nelle " -"impostazioni dei metadati e scrivere un messaggio al proprietario per " -"notificarlo" +"Per consentire la modifica, impostare la risorsa su non \"Approvato\" nelle impostazioni dei " +"metadati e scrivere un messaggio al proprietario per notificarlo" -#, fuzzy -#| msgid "Service rescanned successfully" msgid "Resource Cloned Successfully!" -msgstr "Il servizio è stato analizzato di nuovo correttamente" +msgstr "La risorsa è stata clonata con successo!" -#, fuzzy, python-brace-format -#| msgid "" -#| "Some error occurred while trying to access the uploaded schema: {str(e)}" msgid "Error Occurred while Cloning the Resource: {e}" -msgstr "" -"Si è verificato un errore durante il tentativo di accesso allo schema " -"caricato: {str(e)}" +msgstr "Si è verificato un errore durante la clonazione della risorsa: {e}" msgid "Resource Metadata" msgstr "Metadati delle risorse" @@ -675,237 +652,219 @@ msgid "GeoNode Client Library" msgstr "Libreria client di GeoNode" msgid "" -"Flora and/or fauna in natural environment. Examples: wildlife, vegetation, " -"biological sciences, ecology, wilderness, sealife, wetlands, habitat." +"Flora and/or fauna in natural environment. Examples: wildlife, vegetation, biological " +"sciences, ecology, wilderness, sealife, wetlands, habitat." msgstr "" -"Flora e/o fauna in ambiente naturale. Esempi: fauna selvatica, vegetazione, " -"scienze biologiche, ecologia, natura selvaggia, vita marina, zone umide, " -"habitat." +"Flora e/o fauna in ambiente naturale. Esempi: fauna selvatica, vegetazione, scienze " +"biologiche, ecologia, natura selvaggia, vita marina, zone umide, habitat." msgid "Biota" msgstr "Biota" -msgid "" -"Legal land descriptions. Examples: political and administrative boundaries." -msgstr "" -"Descrizioni legali dei terreni. Esempi: confini politici e amministrativi." +msgid "Legal land descriptions. Examples: political and administrative boundaries." +msgstr "Descrizioni legali dei terreni. Esempi: confini politici e amministrativi." msgid "Boundaries" msgstr "Confini" msgid "" -"Processes and phenomena of the atmosphere. Examples: cloud cover, weather, " -"climate, atmospheric conditions, climate change, precipitation." +"Processes and phenomena of the atmosphere. Examples: cloud cover, weather, climate, " +"atmospheric conditions, climate change, precipitation." msgstr "" -"Processi e fenomeni dell'atmosfera. Esempi: copertura nuvolosa, meteo, " -"clima, condizioni atmosferiche, cambiamenti climatici, precipitazioni." +"Processi e fenomeni dell'atmosfera. Esempi: copertura nuvolosa, meteo, clima, condizioni " +"atmosferiche, cambiamenti climatici, precipitazioni." msgid "Climatology Meteorology Atmosphere" msgstr "Climatologia Meteorologia Atmosfera" msgid "" -"Economic activities, conditions and employment. Examples: production, " -"labour, revenue, commerce, industry, tourism and ecotourism, forestry, " -"fisheries, commercial or subsistence hunting, exploration and exploitation " -"of resources such as minerals, oil and gas." +"Economic activities, conditions and employment. Examples: production, labour, revenue, " +"commerce, industry, tourism and ecotourism, forestry, fisheries, commercial or subsistence " +"hunting, exploration and exploitation of resources such as minerals, oil and gas." msgstr "" -"Attività economiche, condizioni e occupazione. Esempi: produzione, lavoro, " -"entrate, commercio, industria, turismo ed ecoturismo, silvicoltura, pesca, " -"caccia commerciale o di sussistenza, esplorazione e sfruttamento di risorse " -"quali minerali, petrolio e gas." +"Attività economiche, condizioni e occupazione. Esempi: produzione, lavoro, entrate, " +"commercio, industria, turismo ed ecoturismo, silvicoltura, pesca, caccia commerciale o di " +"sussistenza, esplorazione e sfruttamento di risorse quali minerali, petrolio e gas." msgid "Economy" msgstr "Economia" msgid "" -"Height above or below sea level. Examples: altitude, bathymetry, digital " -"elevation models, slope, derived products." +"Height above or below sea level. Examples: altitude, bathymetry, digital elevation models, " +"slope, derived products." msgstr "" -"Altezza sopra o sotto il livello del mare. Esempi: altitudine, batimetria, " -"modelli digitali di elevazione, pendenza, prodotti derivati." +"Altezza sopra o sotto il livello del mare. Esempi: altitudine, batimetria, modelli digitali " +"di elevazione, pendenza, prodotti derivati." msgid "Elevation" msgstr "Elevazione" msgid "" -"Environmental resources, protection and conservation. Examples: " -"environmental pollution, waste storage and treatment, environmental impact " -"assessment, monitoring environmental risk, nature reserves, landscape." +"Environmental resources, protection and conservation. Examples: environmental pollution, " +"waste storage and treatment, environmental impact assessment, monitoring environmental risk, " +"nature reserves, landscape." msgstr "" -"Risorse ambientali, protezione e conservazione. Esempi: inquinamento " -"ambientale, stoccaggio e trattamento dei rifiuti, valutazione dell'impatto " -"ambientale, monitoraggio del rischio ambientale, riserve naturali, paesaggio." +"Risorse ambientali, protezione e conservazione. Esempi: inquinamento ambientale, stoccaggio " +"e trattamento dei rifiuti, valutazione dell'impatto ambientale, monitoraggio del rischio " +"ambientale, riserve naturali, paesaggio." msgid "Environment" msgstr "Ambiente" msgid "" -"Rearing of animals and/or cultivation of plants. Examples: agriculture, " -"irrigation, aquaculture, plantations, herding, pests and diseases affecting " -"crops and livestock." +"Rearing of animals and/or cultivation of plants. Examples: agriculture, irrigation, " +"aquaculture, plantations, herding, pests and diseases affecting crops and livestock." msgstr "" -"Allevamento di animali e/o coltivazione di piante. Esempi: agricoltura, " -"irrigazione, acquacoltura, piantagioni, mandrie, parassiti e malattie che " -"colpiscono le colture e il bestiame." +"Allevamento di animali e/o coltivazione di piante. Esempi: agricoltura, irrigazione, " +"acquacoltura, piantagioni, mandrie, parassiti e malattie che colpiscono le colture e il " +"bestiame." msgid "Farming" msgstr "Agricoltura" msgid "" -"Information pertaining to earth sciences. Examples: geophysical features and " -"processes, geology, minerals, sciences dealing with the composition, " -"structure and origin of the earth s rocks, risks of earthquakes, volcanic " -"activity, landslides, gravity information, soils, permafrost, hydrogeology, " -"erosion." +"Information pertaining to earth sciences. Examples: geophysical features and processes, " +"geology, minerals, sciences dealing with the composition, structure and origin of the earth " +"s rocks, risks of earthquakes, volcanic activity, landslides, gravity information, soils, " +"permafrost, hydrogeology, erosion." msgstr "" -"Informazioni relative alle scienze della terra. Esempi: caratteristiche e " -"processi geofisici, geologia, minerali, scienze che si occupano della " -"composizione, struttura e origine delle rocce terrestri, rischi di " -"terremoti, attività vulcanica, frane, informazioni sulla gravità, suoli, " -"permafrost, idrogeologia, erosione." +"Informazioni relative alle scienze della terra. Esempi: caratteristiche e processi " +"geofisici, geologia, minerali, scienze che si occupano della composizione, struttura e " +"origine delle rocce terrestri, rischi di terremoti, attività vulcanica, frane, informazioni " +"sulla gravità, suoli, permafrost, idrogeologia, erosione." msgid "Geoscientific Information" msgstr "Informazioni geoscientifiche" msgid "" -"Health, health services, human ecology, and safety. Examples: disease and " -"illness, factors affecting health, hygiene, substance abuse, mental and " -"physical health, health services." +"Health, health services, human ecology, and safety. Examples: disease and illness, factors " +"affecting health, hygiene, substance abuse, mental and physical health, health services." msgstr "" -"Salute, servizi sanitari, ecologia umana e sicurezza. Esempi: malattia e " -"malattia, fattori che influenzano la salute, l'igiene, l'abuso di sostanze, " -"la salute mentale e fisica, i servizi sanitari." +"Salute, servizi sanitari, ecologia umana e sicurezza. Esempi: malattia e malattia, fattori " +"che influenzano la salute, l'igiene, l'abuso di sostanze, la salute mentale e fisica, i " +"servizi sanitari." msgid "Health" msgstr "Salute" msgid "" -"Base maps. Examples: land cover, topographic maps, imagery, unclassified " -"images, annotations." +"Base maps. Examples: land cover, topographic maps, imagery, unclassified images, annotations." msgstr "" -"Mappe di base. Esempi: copertura del terreno, mappe topografiche, immagini, " -"immagini non classificate, annotazioni." +"Mappe di base. Esempi: copertura del terreno, mappe topografiche, immagini, immagini non " +"classificate, annotazioni." msgid "Imagery Base Maps Earth Cover" msgstr "Mappe Immagini di Base Copertina della Terra" msgid "" -"Inland water features, drainage systems and their characteristics. Examples: " -"rivers and glaciers, salt lakes, water utilization plans, dams, currents, " -"floods, water quality, hydrographic charts." +"Inland water features, drainage systems and their characteristics. Examples: rivers and " +"glaciers, salt lakes, water utilization plans, dams, currents, floods, water quality, " +"hydrographic charts." msgstr "" -"Caratteristiche dell'acqua dell'entroterra, sistemi di drenaggio e loro " -"caratteristiche. Esempi: fiumi e ghiacciai, laghi salati, piani di utilizzo " -"dell'acqua, dighe, correnti, inondazioni, qualità dell'acqua, grafici " -"idrografici." +"Caratteristiche dell'acqua dell'entroterra, sistemi di drenaggio e loro caratteristiche. " +"Esempi: fiumi e ghiacciai, laghi salati, piani di utilizzo dell'acqua, dighe, correnti, " +"inondazioni, qualità dell'acqua, grafici idrografici." msgid "Inland Waters" msgstr "Acque interne" msgid "" -"Military bases, structures, activities. Examples: barracks, training " -"grounds, military transportation, information collection." +"Military bases, structures, activities. Examples: barracks, training grounds, military " +"transportation, information collection." msgstr "" -"Basi militari, strutture, attività. Esempi: caserme, campi di addestramento, " -"trasporto militare, raccolta di informazioni." +"Basi militari, strutture, attività. Esempi: caserme, campi di addestramento, trasporto " +"militare, raccolta di informazioni." msgid "Intelligence Military" msgstr "Intelligence militare" msgid "" -"Positional information and services. Examples: addresses, geodetic networks, " -"control points, postal zones and services, place names." +"Positional information and services. Examples: addresses, geodetic networks, control points, " +"postal zones and services, place names." msgstr "" -"Informazioni e servizi di localizzazione. Esempi: indirizzi, reti " -"geodetiche, punti di controllo, zone postali e servizi, nomi di luoghi." +"Informazioni e servizi di localizzazione. Esempi: indirizzi, reti geodetiche, punti di " +"controllo, zone postali e servizi, nomi di luoghi." msgid "Location" -msgstr "Posizione" +msgstr "Recapito" msgid "" -"Features and characteristics of salt water bodies (excluding inland waters). " -"Examples: tides, tidal waves, coastal information, reefs." +"Features and characteristics of salt water bodies (excluding inland waters). Examples: " +"tides, tidal waves, coastal information, reefs." msgstr "" -"Caratteristiche e caratteristiche dei corpi idrici dell'acqua salata " -"(escluse le acque interne). Esempi: maree, onde di marea, informazioni " -"costiere, barriere coralline." +"Caratteristiche e caratteristiche dei corpi idrici dell'acqua salata (escluse le acque " +"interne). Esempi: maree, onde di marea, informazioni costiere, barriere coralline." msgid "Oceans" msgstr "Oceani" msgid "" -"Information used for appropriate actions for future use of the land. " -"Examples: land use maps, zoning maps, cadastral surveys, land ownership." +"Information used for appropriate actions for future use of the land. Examples: land use " +"maps, zoning maps, cadastral surveys, land ownership." msgstr "" -"Informazioni utilizzate per azioni appropriate per un uso futuro del " -"terreno. Esempi: mappe dell'uso del suolo, mappe di zonzing, indagini " -"cadastrali, proprietà del terreno." +"Informazioni utilizzate per azioni appropriate per un uso futuro del terreno. Esempi: mappe " +"dell'uso del suolo, mappe di zonzing, indagini cadastrali, proprietà del terreno." msgid "Planning Cadastre" msgstr "Pianificazione catasto" msgid "" -"Settlements, anthropology, archaeology, education, traditional beliefs, " -"manners and customs, demographic data, recreational areas and activities, " -"social impact assessments, crime and justice, census information. Economic " -"activities, conditions and employment." +"Settlements, anthropology, archaeology, education, traditional beliefs, manners and customs, " +"demographic data, recreational areas and activities, social impact assessments, crime and " +"justice, census information. Economic activities, conditions and employment." msgstr "" -"Insediamenti, antropologia, archeologia, educazione, credenze tradizionali, " -"modi e costumi, dati demografici, aree e attività ricreative, valutazioni di " -"impatto sociale, criminalità e giustizia, informazioni sul censimento. " -"Attività economiche, condizioni e occupazione." +"Insediamenti, antropologia, archeologia, educazione, credenze tradizionali, modi e costumi, " +"dati demografici, aree e attività ricreative, valutazioni di impatto sociale, criminalità e " +"giustizia, informazioni sul censimento. Attività economiche, condizioni e occupazione." msgid "Population" msgstr "Popolazione" msgid "" -"Characteristics of society and cultures. Examples: settlements, " -"anthropology, archaeology, education, traditional beliefs, manners and " -"customs, demographic data, recreational areas and activities, social impact " -"assessments, crime and justice, census information." +"Characteristics of society and cultures. Examples: settlements, anthropology, archaeology, " +"education, traditional beliefs, manners and customs, demographic data, recreational areas " +"and activities, social impact assessments, crime and justice, census information." msgstr "" -"Caratteristiche della società e delle culture. Esempi: insediamenti, " -"antropologia, archeologia, istruzione, credenze tradizionali, modi e " -"costumi, dati demografici, aree ricreative e attività, valutazioni " -"dell'impatto sociale, criminalità e giustizia, informazioni sul censimento." +"Caratteristiche della società e delle culture. Esempi: insediamenti, antropologia, " +"archeologia, istruzione, credenze tradizionali, modi e costumi, dati demografici, aree " +"ricreative e attività, valutazioni dell'impatto sociale, criminalità e giustizia, " +"informazioni sul censimento." msgid "Society" msgstr "Società" msgid "" -"Man-made construction. Examples: buildings, museums, churches, factories, " -"housing, monuments, shops, towers." +"Man-made construction. Examples: buildings, museums, churches, factories, housing, " +"monuments, shops, towers." msgstr "" -"Costruzione artificiale. Esempi: edifici, musei, chiese, fabbriche, " -"abitazioni, monumenti, negozi, torri." +"Costruzione artificiale. Esempi: edifici, musei, chiese, fabbriche, abitazioni, monumenti, " +"negozi, torri." msgid "Structure" msgstr "Strutture" msgid "" -"Means and aids for conveying persons and/or goods. Examples: roads, airports/" -"airstrips, shipping routes, tunnels, nautical charts, vehicle or vessel " -"location, aeronautical charts, railways." +"Means and aids for conveying persons and/or goods. Examples: roads, airports/airstrips, " +"shipping routes, tunnels, nautical charts, vehicle or vessel location, aeronautical charts, " +"railways." msgstr "" -"Mezzi e aiuti per il trasporto di persone e/o merci. Esempi: strade, " -"aeroporti/sciviole, rotte marittime, gallerie, carte nautiche, posizione di " -"veicoli o navi, carte aeronautiche, ferrovie." +"Mezzi e aiuti per il trasporto di persone e/o merci. Esempi: strade, aeroporti/sciviole, " +"rotte marittime, gallerie, carte nautiche, posizione di veicoli o navi, carte aeronautiche, " +"ferrovie." msgid "Transportation" msgstr "Trasporti" msgid "" -"Energy, water and waste systems and communications infrastructure and " -"services. Examples: hydroelectricity, geothermal, solar and nuclear sources " -"of energy, water purification and distribution, sewage collection and " -"disposal, electricity and gas distribution, data communication, " -"telecommunication, radio, communication networks." +"Energy, water and waste systems and communications infrastructure and services. Examples: " +"hydroelectricity, geothermal, solar and nuclear sources of energy, water purification and " +"distribution, sewage collection and disposal, electricity and gas distribution, data " +"communication, telecommunication, radio, communication networks." msgstr "" -"Sistemi energetici, idrici e dei rifiuti, infrastrutture e servizi di " -"comunicazione. Esempi: fonti idroelettriche, geotermiche, solari e nucleari " -"di energia, depurazione e distribuzione dell'acqua, raccolta e smaltimento " -"delle acque reflue, distribuzione di elettricità e gas, comunicazione dei " -"dati, telecomunicazioni, radio, reti di comunicazione." +"Sistemi energetici, idrici e dei rifiuti, infrastrutture e servizi di comunicazione. Esempi: " +"fonti idroelettriche, geotermiche, solari e nucleari di energia, depurazione e distribuzione " +"dell'acqua, raccolta e smaltimento delle acque reflue, distribuzione di elettricità e gas, " +"comunicazione dei dati, telecomunicazioni, radio, reti di comunicazione." msgid "Utilities Communication" msgstr "Servizi Comunicazioni" @@ -1051,15 +1010,13 @@ msgstr "Procedura guidata" msgid "Advanced Edit" msgstr "Modifica avanzata" -#, fuzzy -#| msgid "Document" msgid "Document" msgid_plural "Documents" msgstr[0] "Documento" -msgstr[1] "Documento" +msgstr[1] "Documenti" msgid "Clone" -msgstr "" +msgstr "Clona" msgid "Replace" msgstr "Sostituisci" @@ -1098,8 +1055,7 @@ msgid "Permissions" msgstr "Permessi" msgid "Click the button below to change the permissions of this document." -msgstr "" -"Clicca il pulsante qui sotto per modificare i permessi di questo documento." +msgstr "Clicca il pulsante qui sotto per modificare i permessi di questo documento." msgid "Change Document Permissions" msgstr "Cambia i permessi del Documento" @@ -1124,9 +1080,7 @@ msgid "Check Schema mandatory fields" msgstr "Controllare i campi obbligatori dello schema" msgid "Error updating metadata. Please check the following fields: " -msgstr "" -"Errore nell'aggiornamento dei metadati. Per favore controlla i seguenti " -"campi: " +msgstr "Errore nell'aggiornamento dei metadati. Per favore controlla i seguenti campi: " msgid "Done" msgstr "Fatto" @@ -1156,7 +1110,7 @@ msgid "Return to Document" msgstr "Ritorna al documento" msgid "Deleting" -msgstr "Sto cancellando" +msgstr "Sto eliminando" msgid "Remove Document" msgstr "Rimuovi documento" @@ -1164,13 +1118,13 @@ msgstr "Rimuovi documento" #, python-format msgid "" "\n" -" Are you sure you want to remove %(document_title)s?\n" +" Are you sure you want to remove %(document_title)s?\n" " " msgstr "" "\n" -" Sei sicuro di voler rimuovere %(document_title)s?\n" +" Sei sicuro di voler rimuovere %(document_title)s?\n" " " msgid "Yes, I am sure" @@ -1300,7 +1254,7 @@ msgid "Log in to add/delete Favorites." msgstr "Accedi per aggiungere/eliminare preferiti." msgid "Delete from Favorites" -msgstr "Cancella dai preferiti" +msgstr "Elimina dai preferiti" msgid "Favorites" msgstr "Preferiti" @@ -1400,12 +1354,11 @@ msgid "Embed Iframe" msgstr "Incorpora Iframe" msgid "" -"To embed this map, add the following code snippet and customize its " -"properties (scrolling, width, height) based on your needs to your site" +"To embed this map, add the following code snippet and customize its properties (scrolling, " +"width, height) based on your needs to your site" msgstr "" -"Per incorporare questa mappa, aggiungere il frammento di codice seguente e " -"personalizzarne le proprietà (scorrimento, larghezza, altezza) in base alle " -"proprie esigenze nel sito" +"Per incorporare questa mappa, aggiungere il frammento di codice seguente e personalizzarne " +"le proprietà (scorrimento, larghezza, altezza) in base alle proprie esigenze nel sito" msgid "Download" msgstr "Scarica" @@ -1425,22 +1378,19 @@ msgid "for %(map_title)s" msgstr "per %(map_title)s" msgid "" -"Note: this geoapp's orginal metadata was populated by importing a metadata " -"XML file.\n" -" GeoNode's metadata import supports a subset of ISO, FGDC, and Dublin " -"Core metadata elements.\n" +"Note: this geoapp's orginal metadata was populated by importing a metadata XML file.\n" +" GeoNode's metadata import supports a subset of ISO, FGDC, and Dublin Core metadata " +"elements.\n" " Some of your original metadata may have been lost." msgstr "" -"Nota: i metadati orginali di questa geoapp sono stati popolati importando un " -"file XML di metadati.\n" -" L'importazione dei metadati di GeoNode supporta un sottoinsieme di " -"elementi di metadati ISO, FGDC e Dublin Core.\n" +"Nota: i metadati orginali di questa geoapp sono stati popolati importando un file XML di " +"metadati.\n" +" L'importazione dei metadati di GeoNode supporta un sottoinsieme di elementi di " +"metadati ISO, FGDC e Dublin Core.\n" " Alcuni dei metadati originali potrebbero essere stati persi." -#, fuzzy -#| msgid "Explore Maps" msgid "Explore Apps" -msgstr "Esplora mappe" +msgstr "Esplora apps" #, python-format msgid "" @@ -1452,10 +1402,8 @@ msgstr "" " Modifica dei dettagli per il %(map_title)s\n" " " -#, fuzzy -#| msgid "Return to Map" msgid "Return to GeoApp" -msgstr "Ritorna alla mappa" +msgstr "Ritorna alla GeoApp" msgid "New Map" msgstr "Nuova mappa" @@ -1463,13 +1411,13 @@ msgstr "Nuova mappa" #, python-format msgid "" "\n" -" Are you sure you want to remove %(resource_title)s?\n" +" Are you sure you want to remove %(resource_title)s?\n" " " msgstr "" "\n" -" Siete sicuri di voler rimuovere %(resource_title)s?\n" +" Siete sicuri di voler rimuovere %(resource_title)s?\n" " " msgid "Other Settings" @@ -1523,39 +1471,29 @@ msgstr "Linee" msgid "Polygons" msgstr "Poligoni" -#, fuzzy -#| msgid "dataset" msgid "Dataset name" -msgstr "dataset" +msgstr "Nome del dataset" -#, fuzzy -#| msgid "Dataset Attributes" msgid "Dataset title" -msgstr "Attributi del dataset" +msgstr "Titolo del dataset" msgid "Geometry type" msgstr "Tipo di geometria" -#, fuzzy -#| msgid "Featured Datasets" msgid "Create Dataset" -msgstr "Dataset in primo piano" +msgstr "Crea Dataset" -#, fuzzy -#| msgid "Explore Layers" msgid "Explore Datasets" -msgstr "Esplora livelli" +msgstr "Esplora dataset" -#, fuzzy -#| msgid "Create an empty layer" msgid "Create an empty dataset" -msgstr "Creare un livello vuoto" +msgstr "Crea un dataset vuoto" msgid "Add Attribute" -msgstr "Aggiungere attributo" +msgstr "Aggiungi attributo" msgid "Create" -msgstr "Creare" +msgstr "Crea" msgid "String" msgstr "Stringa" @@ -1616,18 +1554,16 @@ msgstr "PNG" msgid "You don't have permissions to change style for this layer" msgstr "" -"Non si dispone delle autorizzazioni necessarie per modificare lo stile di " -"questo livello" +"Non si dispone delle autorizzazioni necessarie per modificare lo stile di questo livello" msgid "Bad HTTP Authorization Credentials." msgstr "Credenziali di autorizzazione HTTP errate." msgid "" -"a short version of the name consisting only of letters, numbers, underscores " -"and hyphens." +"a short version of the name consisting only of letters, numbers, underscores and hyphens." msgstr "" -"una versione abbreviata del nome consiste solo di lettere, numeri, caratteri " -"di sottolineatura e trattini." +"una versione abbreviata del nome consiste solo di lettere, numeri, caratteri di " +"sottolineatura e trattini." msgid "A group already exists with that slug." msgstr "Un gruppo esiste già con questo slug." @@ -1642,10 +1578,8 @@ msgid "Assign manager role" msgstr "Assegnare il ruolo di manager" #, python-format -msgid "" -"The following are not valid usernames: %(errors)s; not added to the group" -msgstr "" -"I seguenti non sono nomi utente validi: %(errors)s; non aggiunto al gruppo" +msgid "The following are not valid usernames: %(errors)s; not added to the group" +msgstr "I seguenti non sono nomi utente validi: %(errors)s; non aggiunto al gruppo" msgid "Group Categories" msgstr "Categorie gruppo" @@ -1660,24 +1594,23 @@ msgid "Private" msgstr "Privato" msgid "" -"Public: Any registered user can view and join a public group.
Public " -"(invite-only):Any registered user can view the group. Only invited users " -"can join.
Private: Registered users cannot see any details about the " -"group, including membership. Only invited users can join." +"Public: Any registered user can view and join a public group.
Public (invite-only):Any " +"registered user can view the group. Only invited users can join.
Private: Registered " +"users cannot see any details about the group, including membership. Only invited users can " +"join." msgstr "" -"Pubblico: Qualsiasi utente registrato può visualizzare e unirsi a un gruppo " -"pubblico.
(Solo su invito) Pubblico: Qualsiasi utente registrato può " -"visualizzare il gruppo. Solo gli utenti invitati possono partecipare.
" -"Per uso privato: Gli utenti registrati non possono vedere tutti i dettagli " -"sul gruppo, compresa l'appartenenza. Solo gli utenti invitati possono " -"partecipare." +"Pubblico: Qualsiasi utente registrato può visualizzare e unirsi a un gruppo pubblico.
" +"(Solo su invito) Pubblico: Qualsiasi utente registrato può visualizzare il gruppo. Solo gli " +"utenti invitati possono partecipare.
Per uso privato: Gli utenti registrati non possono " +"vedere tutti i dettagli sul gruppo, compresa l'appartenenza. Solo gli utenti invitati " +"possono partecipare." msgid "" -"Email used to contact one or all group members, such as a mailing list, " -"shared email, or exchange group." +"Email used to contact one or all group members, such as a mailing list, shared email, or " +"exchange group." msgstr "" -"E-mail utilizzato per contattare gli utenti di uno o di tutti i gruppi, come " -"ad esempio una mailing list, e-mail condivisa, o un gruppo di scambio." +"E-mail utilizzato per contattare gli utenti di uno o di tutti i gruppi, come ad esempio una " +"mailing list, e-mail condivisa, o un gruppo di scambio." msgid "Logo" msgstr "Logo" @@ -1777,9 +1710,7 @@ msgid "Join Group" msgstr "Iscriviti al gruppo" msgid "Anyone may view this group but membership is by invitation only." -msgstr "" -"Chiunque può visualizzare questo gruppo, ma l'iscrizione è possibile solo su " -"invito." +msgstr "Chiunque può visualizzare questo gruppo, ma l'iscrizione è possibile solo su invito." msgid "Membership is by invitation only." msgstr "L'iscrizione è possibile solo su invito." @@ -1862,30 +1793,25 @@ msgstr "Inserimento risorse..." msgid "checking-availability" msgstr "" -#, fuzzy -#| msgid "Harvested" msgid "Harvester name" -msgstr "Raccolte" +msgstr "Nome dell'harvester" msgid "Base URL of the remote service that is to be harvested" msgstr "" msgid "" -"Whether to periodically schedule this harvester to look for resources on the " -"remote service" +"Whether to periodically schedule this harvester to look for resources on the remote service" msgstr "" msgid "" -"How often (in minutes) should new harvesting sessions be automatically " -"scheduled? Setting this value to zero has the same effect as setting " -"`scheduling_enabled` to False " +"How often (in minutes) should new harvesting sessions be automatically scheduled? Setting " +"this value to zero has the same effect as setting `scheduling_enabled` to False " msgstr "" msgid "Whether the remote service is known to be available or not" msgstr "" -msgid "" -"How often (in minutes) should the remote service be checked for availability?" +msgid "How often (in minutes) should the remote service be checked for availability?" msgstr "" msgid "Last time the remote server was checked for availability" @@ -1894,26 +1820,20 @@ msgstr "" msgid "Last time the remote server was checked for harvestable resources" msgstr "" -#, fuzzy -#| msgid "version of the cited resource" msgid "Default owner of harvested resources" -msgstr "versione della risorsa citata" +msgstr "Proprietario di default della risorsa harvestata" -#, fuzzy -#| msgid "Set permissions on selected resources" msgid "Default access permissions of harvested resources" -msgstr "Impostare le autorizzazioni per le risorse selezionate" +msgstr "Autorizzazioni di default per le risorse harvestate" -msgid "" -"Should new resources be harvested automatically without explicit selection?" +msgid "Should new resources be harvested automatically without explicit selection?" msgstr "" msgid "" -"Orphan resources are those that have previously been created by means of a " -"harvesting operation but that GeoNode can no longer find on the remote " -"service being harvested. Should these resources be deleted from GeoNode " -"automatically? This also applies to when a harvester configuration is " -"deleted, in which case all of the resources that originated from that " +"Orphan resources are those that have previously been created by means of a harvesting " +"operation but that GeoNode can no longer find on the remote service being harvested. Should " +"these resources be deleted from GeoNode automatically? This also applies to when a harvester " +"configuration is deleted, in which case all of the resources that originated from that " "harvester are now considered to be orphan." msgstr "" @@ -1921,14 +1841,14 @@ msgid "Date of last update to the harvester configuration." msgstr "" msgid "" -"Harvester class used to perform harvesting sessions. New harvester types can " -"be added by an admin by changing the main GeoNode `settings.py` file" +"Harvester class used to perform harvesting sessions. New harvester types can be added by an " +"admin by changing the main GeoNode `settings.py` file" msgstr "" msgid "" -"Configuration specific to each harvester type. Please consult GeoNode " -"documentation on harvesting for more info. This field is mandatory, so at " -"the very least an empty object (i.e. {}) must be supplied." +"Configuration specific to each harvester type. Please consult GeoNode documentation on " +"harvesting for more info. This field is mandatory, so at the very least an empty object (i." +"e. {}) must be supplied." msgstr "" msgid "Periodic task used to configure harvest scheduling" @@ -1950,17 +1870,15 @@ msgid "harvesting-resource" msgstr "Inserimento risorse..." msgid "" -"Identifier that allows referencing the resource on its remote service in a " -"unique fashion. This is usually automatically filled by the harvester " -"worker. The harvester worker needs to know how to either read or generate " -"this from each remote resource in order to be able to compare the " -"availability of resources between consecutive harvesting sessions." +"Identifier that allows referencing the resource on its remote service in a unique fashion. " +"This is usually automatically filled by the harvester worker. The harvester worker needs to " +"know how to either read or generate this from each remote resource in order to be able to " +"compare the availability of resources between consecutive harvesting sessions." msgstr "" msgid "" -"Type of the resource in the remote service. Each harvester worker knows how " -"to fill this field, in accordance with the resources for which harvesting is " -"supported" +"Type of the resource in the remote service. Each harvester worker knows how to fill this " +"field, in accordance with the resources for which harvesting is supported" msgstr "" #, python-brace-format @@ -1973,9 +1891,7 @@ msgstr "L'indirizzo e-mail '{email}' ha già accettato un invito." #, python-brace-format msgid "An active user is already using the e-mail address '{email}'" -msgstr "" -"Un utente attivo sta già utilizzando l'indirizzo di posta elettronica " -"'{email}'" +msgstr "Un utente attivo sta già utilizzando l'indirizzo di posta elettronica '{email}'" msgid "E-mail" msgstr "E-mail" @@ -1986,11 +1902,11 @@ msgstr "Inviti mandati con successo a '%(email)s'" #, python-format msgid "" -"Sorry, it was not possible to invite '%(email)s' due to the following isse: " -"%(error)s (%(type)s)" +"Sorry, it was not possible to invite '%(email)s' due to the following isse: %(error)s " +"(%(type)s)" msgstr "" -"Spiacenti, non è stato possibile invitare '%(email)s' a causa di quanto " -"segue: %(error)s (%(type)s)" +"Spiacenti, non è stato possibile invitare '%(email)s' a causa di quanto segue: %(error)s " +"(%(type)s)" msgid "Layer Created" msgstr "Livello creato" @@ -2074,8 +1990,7 @@ msgid "use featureinfo custom template?" msgstr "utilizzare il modello personalizzato featureinfo?" msgid "specifies wether or not use a custom GetFeatureInfo template." -msgstr "" -"specifica se si utilizza o meno un modello GetFeatureInfo personalizzato." +msgstr "specifica se si utilizza o meno un modello GetFeatureInfo personalizzato." msgid "featureinfo custom template" msgstr "featureinfo modello personalizzato" @@ -2119,11 +2034,8 @@ msgstr "specifica se l'attributo deve essere visualizzato nei risultati" msgid "display order" msgstr "mostra ordine" -msgid "" -"specifies the order in which attribute should be displayed in identify " -"results" -msgstr "" -"specifica l'ordine con cui l'attributo deve essere visualizzato nei risultati" +msgid "specifies the order in which attribute should be displayed in identify results" +msgstr "specifica l'ordine con cui l'attributo deve essere visualizzato nei risultati" msgid "Label" msgstr "Etichetta" @@ -2159,11 +2071,10 @@ msgid "featureinfo type" msgstr "tipo featureinfo" msgid "" -"specifies if the attribute should be rendered with an HTML widget on " -"GetFeatureInfo template." +"specifies if the attribute should be rendered with an HTML widget on GetFeatureInfo template." msgstr "" -"specifica se il rendering dell'attributo deve essere eseguito con un widget " -"HTML nel modello GetFeatureInfo." +"specifica se il rendering dell'attributo deve essere eseguito con un widget HTML nel modello " +"GetFeatureInfo." msgid "count" msgstr "conta" @@ -2216,15 +2127,11 @@ msgstr "ultimo modificato" msgid "date when attribute statistics were last updated" msgstr "data in cui le statistiche degli attributi sono stati aggiornati" -#, fuzzy -#| msgid "Append Data" msgid "Append to Dataset" -msgstr "Accoda dati" +msgstr "Aggiungi al dataset" -#, fuzzy -#| msgid "Return to Layer" msgid "Return to Dataset" -msgstr "Torna al livello" +msgstr "Torna al dataset" msgid "Preserve Metadata XML" msgstr "Preserva metadati XML" @@ -2241,10 +2148,8 @@ msgstr "Modifica contatto" msgid "Assign a new point of contact to the layers below:" msgstr "Assegna un nuovo contatto ai layer sottostanti:" -#, fuzzy -#| msgid "Layer WMS GetCapabilities document" msgid "Dataset WMS GetCapabilities document" -msgstr "Documento WMS GetCapabilities del livello" +msgstr "Documento WMS GetCapabilities del dataset" msgid "Filter Granules" msgstr "Filtra granuli" @@ -2300,18 +2205,14 @@ msgstr "Mediana" msgid "Standard Deviation" msgstr "Deviazione standard" -#, fuzzy -#| msgid "Rate this layer" msgid "Rate this dataset" -msgstr "Giudica questo livello" +msgstr "Valuta questo dataset" msgid "Analyze with" msgstr "Analizza con" -#, fuzzy -#| msgid "Download the" msgid "Download Dataset" -msgstr "Scarica il" +msgstr "Scarica dataset" msgid "Images" msgstr "Immagini" @@ -2356,11 +2257,10 @@ msgid "Pick your download format:" msgstr "Seleziona formato da scaricare:" msgid "" -"No data available for this resource. Please contact a system administrator " -"or a manager." +"No data available for this resource. Please contact a system administrator or a manager." msgstr "" -"Nessun dato disponibile per questa risorsa. Contattare un amministratore di " -"sistema o un responsabile." +"Nessun dato disponibile per questa risorsa. Contattare un amministratore di sistema o un " +"responsabile." msgid "Warning" msgstr "Attenzione" @@ -2377,11 +2277,9 @@ msgstr "Stili" msgid "Manage" msgstr "Gestisci" -#, fuzzy -#| msgid "Layers" msgid "Layer" msgid_plural "Layers" -msgstr[0] "Livelli" +msgstr[0] "Livello" msgstr[1] "Livelli" msgid "Append Data" @@ -2390,10 +2288,8 @@ msgstr "Accoda dati" msgid "Edit data" msgstr "Modifica dati" -#, fuzzy -#| msgid "View Metadata" msgid "View Dataset" -msgstr "Metadati" +msgstr "Vedi Dataset" msgid "Attribute Information" msgstr "Informazioni sugli attributi" @@ -2404,44 +2300,26 @@ msgstr "ISO Feature Catalogue" msgid "Legend" msgstr "Legenda" -#, fuzzy -#| msgid "Maps using this layer" msgid "Maps using this dataset" -msgstr "Mappe che usano questo livello" +msgstr "Mappe che usano questo dataset" -#, fuzzy -#| msgid "List of maps using this layer:" msgid "List of maps using this dataset:" -msgstr "Elenco di mappe che usano questo livello:" +msgstr "Elenco delle mappe che usano questo dataset:" -#, fuzzy -#| msgid "This layer is not currently used in any maps." msgid "This dataset is not currently used in any maps." -msgstr "Questo livello non è al momento utilizzato da alcuna mappa." +msgstr "Questo dataset non è al momento utilizzato da alcuna mappa." -#, fuzzy -#| msgid "Create a map using this layer" msgid "Create a map using this dataset" -msgstr "Crea una mappa usando questo livello" +msgstr "Crea una mappa usando questo dataset" -#, fuzzy -#| msgid "Click the button below to generate a new map based on this layer." msgid "Click the button below to generate a new map based on this dataset." -msgstr "" -"Clicca il pulsante qui sotto per generare una nuova mappa basata su questo " -"livello." +msgstr "Clicca il pulsante qui sotto per generare una nuova mappa basata su questo dataset." -#, fuzzy -#| msgid "Add the layer to an existing map" msgid "Add the dataset to an existing map" -msgstr "Aggiungi il livello ad una mappa esistente" +msgstr "Aggiungi il dataset ad una mappa esistente" -#, fuzzy -#| msgid "Click the button below to add the layer to the selected map." msgid "Click the button below to add the dataset to the selected map." -msgstr "" -"Fare clic sul pulsante sottostante per aggiungere il livello alla mappa " -"selezionata." +msgstr "Fare clic sul pulsante sottostante per aggiungere il dataset alla mappa selezionata." msgid "Add to Map" msgstr "Aggiungi alla mappa" @@ -2453,11 +2331,11 @@ msgid "List of documents related to this layer:" msgstr "Elenco di documenti correlati a questo livello:" msgid "" -"The following styles are associated with this layer. Choose a style to view " -"it in the preview map." +"The following styles are associated with this layer. Choose a style to view it in the " +"preview map." msgstr "" -"I seguenti stili sono associati a questo livello. Scegli uno stile da " -"visualizzare nell'anteprima della mappa." +"I seguenti stili sono associati a questo livello. Scegli uno stile da visualizzare " +"nell'anteprima della mappa." msgid "(default style)" msgstr "(stile predefinito)" @@ -2476,17 +2354,17 @@ msgstr "Aggiorna attributi e statistiche di questo livello" #, fuzzy #| msgid "" -#| "Click the button below to allow GeoNode refreshing the list of available " -#| "Layer Attributes. If the option 'WPS_ENABLED' has been also set on the " -#| "backend, it will recalculate their statistics too." +#| "Click the button below to allow GeoNode refreshing the list of available Layer " +#| "Attributes. If the option 'WPS_ENABLED' has been also set on the backend, it will " +#| "recalculate their statistics too." msgid "" -"Click the button below to allow GeoNode refreshing the list of available " -"Dataset Attributes. If the option 'WPS_ENABLED' has been also set on the " -"backend, it will recalculate their statistics too." +"Click the button below to allow GeoNode refreshing the list of available Dataset Attributes. " +"If the option 'WPS_ENABLED' has been also set on the backend, it will recalculate their " +"statistics too." msgstr "" -"Clicca il pulsante sotto per permettere a GeoNode di aggiornare la lista " -"degli attributi del livello. Se l'opzione 'WPS_ENABLED' è stata settata sul " -"backend, verranno anche ricalcolate le statistiche del livello." +"Clicca il pulsante sotto per permettere a GeoNode di aggiornare la lista degli attributi del " +"livello. Se l'opzione 'WPS_ENABLED' è stata settata sul backend, verranno anche ricalcolate " +"le statistiche del livello." msgid "Refresh Attributes and Statistics" msgstr "Aggiorna attributi e statistiche" @@ -2495,8 +2373,7 @@ msgid "Clear the Server Cache of this layer" msgstr "Pulisci la Cache Server di questo livello" msgid "Click the button below to wipe the tile-cache of this layer." -msgstr "" -"Clicca il pulsante qui sotto per eliminare la tile-cache di questo livello." +msgstr "Clicca il pulsante qui sotto per eliminare la tile-cache di questo livello." #, fuzzy #| msgid "Empty Tiled-Layer Cache" @@ -2504,13 +2381,10 @@ msgid "Empty Tiled-Dataset Cache" msgstr "Svuota la tile-cache del livello" msgid "Click the button below to change the permissions of this layer." -msgstr "" -"Clicca il pulsante qui sotto per modificare i permessi di questo livello." +msgstr "Clicca il pulsante qui sotto per modificare i permessi di questo livello." -#, fuzzy -#| msgid "Change Layer Permissions" msgid "Change Dataset Permissions" -msgstr "Cambia i permessi del livello" +msgstr "Cambia i permessi del dataset" msgid "Remove Mosaic Granules" msgstr "Rimuovere i granuli del mosaico" @@ -2518,13 +2392,13 @@ msgstr "Rimuovere i granuli del mosaico" #, python-format msgid "" "\n" -" Are you sure you want to remove Granule %(granule_id)s of the " -"Mosaic %(layer_title)s?\n" +" Are you sure you want to remove Granule %(granule_id)s of the Mosaic %(layer_title)s?\n" " " msgstr "" "\n" -" Si è sicuri di voler rimuovere il granulo %(granule_id)s del " -"mosaico %(layer_title)s?\n" +" Si è sicuri di voler rimuovere il granulo %(granule_id)s del mosaico %(layer_title)s?\n" " " msgid "This action affects the following maps:" @@ -2533,35 +2407,32 @@ msgstr "Questa azione avrà effetto sulle seguenti mappe:" msgid "No maps are using this layer" msgstr "Nessuna mappa utilizza questo livello" -#, fuzzy -#| msgid "Upload status" msgid "Upload Datasets" -msgstr "Stato caricamento" +msgstr "Carica dataset" #, python-format msgid "for %(layer_title)s" msgstr "per %(layer_title)s" msgid "" -"Note: this layer's orginal metadata was populated and preserved by importing " -"a metadata XML file.\n" +"Note: this layer's orginal metadata was populated and preserved by importing a metadata XML " +"file.\n" " This metadata cannot be edited." msgstr "" -"Nota: i metadati originali di questo livello sono stati popolati e " -"conservati importando un file XML di metadati.\n" +"Nota: i metadati originali di questo livello sono stati popolati e conservati importando un " +"file XML di metadati.\n" " Questi metadati non possono essere modificati." msgid "" -"Note: this layer's orginal metadata was populated by importing a metadata " -"XML file.\n" -" GeoNode's metadata import supports a subset of ISO, FGDC, and " -"Dublin Core metadata elements.\n" +"Note: this layer's orginal metadata was populated by importing a metadata XML file.\n" +" GeoNode's metadata import supports a subset of ISO, FGDC, and Dublin Core metadata " +"elements.\n" " Some of your original metadata may have been lost." msgstr "" -"Note: I metadati originali di questo livello sono stati popolati importando " -"un file di metadati XML.\n" -" L'import dei metadati di GeoNode supporta un sottinsieme di elementi " -"di metadata Dublin Core, ISO e FGDC.\n" +"Note: I metadati originali di questo livello sono stati popolati importando un file di " +"metadati XML.\n" +" L'import dei metadati di GeoNode supporta un sottinsieme di elementi di metadata " +"Dublin Core, ISO e FGDC.\n" " Alcuni dei tuoi metadati originali potrebbero essere andati persi." #, python-format @@ -2608,13 +2479,13 @@ msgstr "Rimuovi i livelli" #, python-format msgid "" "\n" -" Are you sure you want to remove %(layer_title)s?\n" +" Are you sure you want to remove %(layer_title)s?\n" " " msgstr "" "\n" -" Sei sicuro di voler eliminare %(layer_title)s?\n" +" Sei sicuro di voler eliminare %(layer_title)s?\n" " " #, fuzzy @@ -2628,29 +2499,25 @@ msgstr "Gestisci stili" #, python-format msgid "" "\n" -" Manage Available Styles for " -"%(layer_title)s\n" +" Manage Available Styles for %(layer_title)s\n" " " msgstr "" "\n" -" Gestisci gli stili disponibili per " -"%(layer_title)s\n" +" Gestisci gli stili disponibili per %(layer_title)s\n" " " -#, fuzzy -#| msgid "Layer Default Style" msgid "Dataset Default Style" -msgstr "Stile predefinito del livello" +msgstr "Stile predefinito del dataset" msgid "Available styles" msgstr "Stili disponibili" msgid "" -"Click on an available style in the upper box to assign it to this layer. " -"Selected styles appear in the lower box." +"Click on an available style in the upper box to assign it to this layer. Selected styles " +"appear in the lower box." msgstr "" -"Fare clic su uno stile disponibile nella casella in alto per assegnare a " -"questo livello. Stili selezionati vengono visualizzati nel riquadro in basso." +"Fare clic su uno stile disponibile nella casella in alto per assegnare a questo livello. " +"Stili selezionati vengono visualizzati nel riquadro in basso." msgid "Update Available Styles" msgstr "Aggiorna gli stili disponibili" @@ -2658,10 +2525,8 @@ msgstr "Aggiorna gli stili disponibili" msgid "Return to Layer" msgstr "Torna al livello" -#, fuzzy -#| msgid "Upload Layer Style" msgid "Upload Dataset Style" -msgstr "Carica stile del livello" +msgstr "Carica stile del dataset" msgid "(SLD - Style Layer Descriptor 1.0, 1.1)" msgstr "(SLD - Descrittore di stile del livello 1.0, 1.1)" @@ -2670,8 +2535,7 @@ msgid "WARNING" msgstr "ATTENZIONE" msgid "This will most probably overwrite the current default style!" -msgstr "" -"Questo molto probabilmente sovrascriverà lo stile predefinito corrente!" +msgstr "Questo molto probabilmente sovrascriverà lo stile predefinito corrente!" msgid "Preview" msgstr "Anteprima" @@ -2679,34 +2543,29 @@ msgstr "Anteprima" msgid "Dataset Attributes" msgstr "Attributi del dataset" -#, fuzzy -#| msgid "Upload status" msgid "Upload Dataset" -msgstr "Stato caricamento" +msgstr "Carica dataset" msgid "Upload status" msgstr "Stato caricamento" msgid "" -"This file needs additional configuration to complete the upload process. " -"Please click on the button to fill the required configuration" +"This file needs additional configuration to complete the upload process. Please click on the " +"button to fill the required configuration" msgstr "" -"Questo file necessita di una configurazione aggiuntiva per completare il " -"processo di caricamento. Clicca sul pulsante per riempire la configurazione " -"richiesta" +"Questo file necessita di una configurazione aggiuntiva per completare il processo di " +"caricamento. Clicca sul pulsante per riempire la configurazione richiesta" msgid "Delete" -msgstr "Cancella" +msgstr "Elimina" msgid "Upload process completed" msgstr "Processo di caricamento completato" -msgid "" -"The upload process cannot be completed because the original file is no more " -"available" +msgid "The upload process cannot be completed because the original file is no more available" msgstr "" -"Impossibile completare il processo di caricamento perché il file originale " -"non è più disponibile" +"Impossibile completare il processo di caricamento perché il file originale non è più " +"disponibile" msgid "Created" msgstr "Creato" @@ -2726,10 +2585,8 @@ msgstr "Eliminare caricamenti incompleti" msgid "Are you sure you want to remove this incomplete upload?" msgstr "Rimuovere questo caricamento incompleto?" -#, fuzzy -#| msgid "Upload Layer Step: Set SRS" msgid "Upload Dataset Step: Set SRS" -msgstr "Carica livello, passo: Imposta SRS" +msgstr "Carica dataset, passo: Imposta SRS" msgid "Provide CRS for " msgstr "Fornire CRS per " @@ -2740,34 +2597,28 @@ msgstr "Sistema di riferimento delle coordinate" #, fuzzy #| msgid "" #| "\n" -#| " A coordinate reference system for this layer could not be " -#| "determined.\n" -#| " Locate or enter the appropriate ESPG code for this layer " -#| "below.\n" +#| " A coordinate reference system for this layer could not be determined.\n" +#| " Locate or enter the appropriate ESPG code for this layer below.\n" #| " One way to do this is do visit:\n" -#| " prj2epsg\n" +#| " prj2epsg\n" #| " and enter the following:\n" #| " " msgid "" "\n" -" A coordinate reference system for this dataset could not be " -"determined.\n" -" Locate or enter the appropriate ESPG code for this dataset " -"below.\n" +" A coordinate reference system for this dataset could not be determined.\n" +" Locate or enter the appropriate ESPG code for this dataset below.\n" " One way to do this is do visit:\n" -" prj2epsg\n" +" prj2epsg\n" " and enter the following:\n" " " msgstr "" "\n" -" Un sistema di riferimento di coordinate per questo strato " -"non poteva essere determinata.\n" -" Individuare o immettere il codice ESPG appropriato per " -"questo livello sottostante.\n" -" Un modo per farlo è fare visitare: prj2epsg\n" +" Un sistema di riferimento di coordinate per questo strato non poteva essere " +"determinata.\n" +" Individuare o immettere il codice ESPG appropriato per questo livello " +"sottostante.\n" +" Un modo per farlo è fare visitare: prj2epsg\n" " e immettere il seguente:\n" " " @@ -2784,27 +2635,24 @@ msgid "Select a Source SRS" msgstr "Seleziona un SRS di origine" msgid "" -"Source SRS EPSG Code is mandatory and represents the native data Spatial " -"Reference System.\n" +"Source SRS EPSG Code is mandatory and represents the native data Spatial Reference System.\n" "

\n" -" This must be coherent with the Geometry values (lon/lat " -"coordinates as an instance) stored on the geospatial dataset.\n" +" This must be coherent with the Geometry values (lon/lat coordinates as " +"an instance) stored on the geospatial dataset.\n" "

\n" -" If not specified on the geospatial data itself, it must " -"be manually declared by the operator.\n" +" If not specified on the geospatial data itself, it must be manually " +"declared by the operator.\n" "

\n" -" More information is provided at the bottom of the page " -"in the \"Additional Help\" sections.\n" +" More information is provided at the bottom of the page in the " +"\"Additional Help\" sections.\n" " " msgstr "" -"Source SRS EPSG Code è obbligatorio e rappresenta il sistema di riferimento " -"spaziale dei dati nativi.

Deve essere coerente " -"con i valori Geometry (coordinate lon/lat come istanza) archiviati nel set " -"di dati geospaziali.

Se non specificato nei dati " -"geospaziali stessi, deve essere dichiarato manualmente " -"dall'operatore.

Ulteriori informazioni sono " -"disponibili nella parte inferiore della pagina nelle sezioni \"Guida " -"aggiuntiva\". " +"Source SRS EPSG Code è obbligatorio e rappresenta il sistema di riferimento spaziale dei " +"dati nativi.

Deve essere coerente con i valori Geometry " +"(coordinate lon/lat come istanza) archiviati nel set di dati geospaziali. " +"

Se non specificato nei dati geospaziali stessi, deve essere dichiarato manualmente " +"dall'operatore.

Ulteriori informazioni sono disponibili nella " +"parte inferiore della pagina nelle sezioni \"Guida aggiuntiva\". " msgid "Next" msgstr "Prossimo" @@ -2831,140 +2679,130 @@ msgid "Spatial Reference System" msgstr "Sistema di riferimento spaziale" msgid "" -"A spatial reference system (SRS) or coordinate reference system (CRS) is a " -"coordinate-based local,\n" -" regional or global system used to locate " -"geographical entities. A spatial reference system defines a specific map\n" -" projection, as well as transformations between " -"different spatial reference systems. Spatial reference systems are\n" -" defined by the OGC's Simple feature access using " -"well-known text, and support has been implemented by several\n" -" standards-based geographic information systems. " -"Spatial reference systems can be referred to using a SRID integer,\n" -" including EPSG codes defined by the " -"International Association of Oil and Gas Producers.\n" -" It is specified in ISO 19111:2007 Geographic " -"information—Spatial referencing by coordinates, also published as\n" -" OGC Abstract Specification, Topic 2: Spatial " -"referencing by coordinate." -msgstr "" -"Un sistema di riferimento spaziale (SRS) o un sistema di riferimento delle " -"coordinate (CRS) è un sistema di riferimento\n" -" sistema regionale o globale utilizzato per " -"individuare le entità geografiche. Un sistema di riferimento spaziale " -"definisce una mappa specifica\n" -" proiezioni, nonché trasformazioni tra diversi " -"sistemi di riferimento spaziale. I sistemi di riferimento spaziali sono\n" -" definito dall'accesso alle funzionalità semplice " -"dell'OGC utilizzando testo noto, e il supporto è stato implementato da " -"diversi\n" -" sistemi di informazione geografica basati su " -"standard. I sistemi di riferimento spaziale possono essere indicati " -"utilizzando un numero intero SRID,\n" -" compresi i codici EPSG definiti " -"dall'Associazione internazionale dei produttori di petrolio e gas.\n" -" È specificato in ISO 19111:2007 Informazioni " -"geografiche: riferimento spaziale in base alle coordinate, pubblicato anche " -"come\n" -" Specifica astratta OGC, Argomento 2: Riferimento " -"spaziale per coordinata." +"A spatial reference system (SRS) or coordinate reference system (CRS) is a coordinate-based " +"local,\n" +" regional or global system used to locate geographical entities. " +"A spatial reference system defines a specific map\n" +" projection, as well as transformations between different spatial " +"reference systems. Spatial reference systems are\n" +" defined by the OGC's Simple feature access using well-known " +"text, and support has been implemented by several\n" +" standards-based geographic information systems. Spatial " +"reference systems can be referred to using a SRID integer,\n" +" including EPSG codes defined by the International Association of " +"Oil and Gas Producers.\n" +" It is specified in ISO 19111:2007 Geographic information—Spatial " +"referencing by coordinates, also published as\n" +" OGC Abstract Specification, Topic 2: Spatial referencing by " +"coordinate." +msgstr "" +"Un sistema di riferimento spaziale (SRS) o un sistema di riferimento delle coordinate (CRS) " +"è un sistema di riferimento\n" +" sistema regionale o globale utilizzato per individuare le entità " +"geografiche. Un sistema di riferimento spaziale definisce una mappa specifica\n" +" proiezioni, nonché trasformazioni tra diversi sistemi di " +"riferimento spaziale. I sistemi di riferimento spaziali sono\n" +" definito dall'accesso alle funzionalità semplice dell'OGC " +"utilizzando testo noto, e il supporto è stato implementato da diversi\n" +" sistemi di informazione geografica basati su standard. I sistemi " +"di riferimento spaziale possono essere indicati utilizzando un numero intero SRID,\n" +" compresi i codici EPSG definiti dall'Associazione internazionale " +"dei produttori di petrolio e gas.\n" +" È specificato in ISO 19111:2007 Informazioni geografiche: " +"riferimento spaziale in base alle coordinate, pubblicato anche come\n" +" Specifica astratta OGC, Argomento 2: Riferimento spaziale per " +"coordinata." msgid "Identifiers" msgstr "Identificatori" msgid "" "\n" -" A Spatial Reference System Identifier (SRID) is a " -"unique value used to unambiguously identify projected, unprojected,\n" -" and local spatial coordinate system definitions. " -"These coordinate systems form the heart of all GIS applications.\n" +" A Spatial Reference System Identifier (SRID) is a unique value used " +"to unambiguously identify projected, unprojected,\n" +" and local spatial coordinate system definitions. These coordinate " +"systems form the heart of all GIS applications.\n" "\n" -" Virtually all major spatial vendors have created " -"their own SRID implementation or refer to those of an authority,\n" +" Virtually all major spatial vendors have created their own SRID " +"implementation or refer to those of an authority,\n" " such as the European Petroleum Survey Group (EPSG).\n" " " msgstr "" "\n" -"Un identificatore SRID (Spatial Reference System Identifier) è un valore " -"univoco utilizzato per identificare in modo inequivocabile\n" -" e le definizioni del sistema di coordinate spaziali " -"locali. Questi sistemi di coordinate costituiscono il cuore di tutte le " -"applicazioni GIS.\n" +"Un identificatore SRID (Spatial Reference System Identifier) è un valore univoco utilizzato " +"per identificare in modo inequivocabile\n" +" e le definizioni del sistema di coordinate spaziali locali. Questi " +"sistemi di coordinate costituiscono il cuore di tutte le applicazioni GIS.\n" "\n" -"Praticamente tutti i principali fornitori spaziali hanno creato la propria " -"implementazione SRID o si riferiscono a quelli di un'autorità,\n" +"Praticamente tutti i principali fornitori spaziali hanno creato la propria implementazione " +"SRID o si riferiscono a quelli di un'autorità,\n" " come l'European Petroleum Survey Group (EPSG).\n" " " msgid "" -"NOTE: As of 2005 the EPSG SRID values are now maintained by the " -"International\n" -" Association of Oil & Gas Producers (OGP) " -"Surveying & Positioning Committee" +"NOTE: As of 2005 the EPSG SRID values are now maintained by the International\n" +" Association of Oil & Gas Producers (OGP) Surveying & Positioning " +"Committee" msgstr "" -"NOTA: a partire dal 2005 i valori EPSG SRID sono ora mantenuti " -"dall'International\n" -" Comitato di rilevamento e posizionamento " -"dell'Associazione dei produttori di petrolio e gas (OGP)" +"NOTA: a partire dal 2005 i valori EPSG SRID sono ora mantenuti dall'International\n" +" Comitato di rilevamento e posizionamento dell'Associazione dei " +"produttori di petrolio e gas (OGP)" msgid "" "\n" -" SRIDs are the primary key for the Open Geospatial " -"Consortium (OGC) spatial_ref_sys metadata table for the Simple\n" -" Features for SQL Specification, Versions 1.1 and " -"1.2, which is defined as follows:\n" +" SRIDs are the primary key for the Open Geospatial Consortium (OGC) " +"spatial_ref_sys metadata table for the Simple\n" +" Features for SQL Specification, Versions 1.1 and 1.2, which is " +"defined as follows:\n" " " msgstr "" "\n" -"Gli S SRID sono la chiave primaria per l'Open Geospatial Consortium (OGC) " -"spatial_ref_sys di metadati per il\n" -" Funzionalità per la specifica SQL, le versioni 1.1 e " -"1.2, definite come segue:\n" +"Gli S SRID sono la chiave primaria per l'Open Geospatial Consortium (OGC) spatial_ref_sys di " +"metadati per il\n" +" Funzionalità per la specifica SQL, le versioni 1.1 e 1.2, definite " +"come segue:\n" " " msgid "" "\n" -" In spatially enabled databases (such as IBM DB2, IBM " -"Informix, Microsoft SQL Server, MySQL, Oracle RDBMS, Teradata, PostGIS and\n" -" SQL Anywhere), SRIDs are used to uniquely identify " -"the coordinate systems used to define columns of spatial data or individual\n" -" spatial objects in a spatial column (depending on " -"the spatial implementation). SRIDs are typically associated with a well " -"known\n" -" text (WKT) string definition of the coordinate " -"system (SRTEXT, above). From the Well Known Text Wikipedia page\n" +" In spatially enabled databases (such as IBM DB2, IBM Informix, " +"Microsoft SQL Server, MySQL, Oracle RDBMS, Teradata, PostGIS and\n" +" SQL Anywhere), SRIDs are used to uniquely identify the coordinate " +"systems used to define columns of spatial data or individual\n" +" spatial objects in a spatial column (depending on the spatial " +"implementation). SRIDs are typically associated with a well known\n" +" text (WKT) string definition of the coordinate system (SRTEXT, " +"above). From the Well Known Text Wikipedia page\n" " " msgstr "" "\n" -"Nei database abilitati per l'ambiente (come IBM DB2, IBM Informix, Microsoft " -"SQL Server, MySQL, Oracle RDBMS, Teradata, PostGIS e\n" -" SQL Anywhere), gli SED vengono utilizzati per " -"identificare in modo univoco i sistemi di coordinate utilizzati per definire " -"colonne di dati spaziali o singoli\n" +"Nei database abilitati per l'ambiente (come IBM DB2, IBM Informix, Microsoft SQL Server, " +"MySQL, Oracle RDBMS, Teradata, PostGIS e\n" +" SQL Anywhere), gli SED vengono utilizzati per identificare in modo " +"univoco i sistemi di coordinate utilizzati per definire colonne di dati spaziali o singoli\n" " oggetti spaziali in una colonna spaziale (a seconda " "dell'implementazione spaziale). I SED sono in genere associati a un\n" -" text (WKT) definizione di stringa del sistema di " -"coordinate (SRTEXT, sopra). Dalla pagina Wikipedia di Ben Known Text\n" +" text (WKT) definizione di stringa del sistema di coordinate (SRTEXT, " +"sopra). Dalla pagina Wikipedia di Ben Known Text\n" " " msgid "" -"“A WKT string for a spatial reference system describes the datum, geoid, " -"coordinate system,\n" +"“A WKT string for a spatial reference system describes the datum, geoid, coordinate system,\n" " and map projection of the spatial objects”." msgstr "" -"\"Una stringa WKT per un sistema di riferimento spaziale descrive il sistema " -"di coordinate di riferimento, geoide,\n" +"\"Una stringa WKT per un sistema di riferimento spaziale descrive il sistema di coordinate " +"di riferimento, geoide,\n" " e la proiezione mappa degli oggetti spaziali\"." msgid "" "\n" -" Here are two common coordinate systems with their " -"EPSG SRID value followed by their well known text:\n" +" Here are two common coordinate systems with their EPSG SRID value " +"followed by their well known text:\n" " " msgstr "" "\n" -" Qui ci sono due sistemi di coordinate comuni con il " -"loro valore di EPSG SRID seguita dal loro WKT:\n" +" Qui ci sono due sistemi di coordinate comuni con il loro valore di " +"EPSG SRID seguita dal loro WKT:\n" " " msgid "" @@ -2987,87 +2825,76 @@ msgstr "" msgid "" "\n" -" SRID values associated with spatial data can be used " -"to constrain spatial operations — for instance, spatial operations cannot be " -"performed\n" -" between spatial objects with differing SRIDs in some " -"systems, or trigger coordinate system transformations between spatial " -"objects in others.\n" +" SRID values associated with spatial data can be used to constrain " +"spatial operations — for instance, spatial operations cannot be performed\n" +" between spatial objects with differing SRIDs in some systems, or " +"trigger coordinate system transformations between spatial objects in others.\n" " " msgstr "" "\n" -"I valori SRID associati ai dati spaziali possono essere utilizzati per " -"vincolare le operazioni spaziali, ad esempio le operazioni spaziali non " -"possono essere eseguite\n" -" tra oggetti spaziali con SRID diversi in alcuni " -"sistemi o attivare trasformazioni del sistema di coordinate tra oggetti " -"spaziali in altri.\n" +"I valori SRID associati ai dati spaziali possono essere utilizzati per vincolare le " +"operazioni spaziali, ad esempio le operazioni spaziali non possono essere eseguite\n" +" tra oggetti spaziali con SRID diversi in alcuni sistemi o attivare " +"trasformazioni del sistema di coordinate tra oggetti spaziali in altri.\n" " " msgid "" -"Source SRS EPSG Code is mandatory and represents the native data Spatial " -"Reference System. This must be coherent with the\n" -" Geometry values (lon/lat coordinates as an " -"instance) stored on the geospatial dataset. If not specified on the " -"geospatial data itself, it\n" +"Source SRS EPSG Code is mandatory and represents the native data Spatial Reference System. " +"This must be coherent with the\n" +" Geometry values (lon/lat coordinates as an instance) stored on " +"the geospatial dataset. If not specified on the geospatial data itself, it\n" " must be manually declared by the operator." msgstr "" -"Source SRS EPSG Code è obbligatorio e rappresenta il sistema di riferimento " -"spaziale dei dati nativi. Questo deve essere coerente con il\n" -" Valori geometrici (coordinate lon/lat come " -"istanza) archiviati nel set di dati geospaziali. Se non viene specificato " -"sui dati geospaziali stessi,\n" -" deve essere dichiarato manualmente " -"dall'operatore." +"Source SRS EPSG Code è obbligatorio e rappresenta il sistema di riferimento spaziale dei " +"dati nativi. Questo deve essere coerente con il\n" +" Valori geometrici (coordinate lon/lat come istanza) archiviati " +"nel set di dati geospaziali. Se non viene specificato sui dati geospaziali stessi,\n" +" deve essere dichiarato manualmente dall'operatore." msgid "" -"Target SRS EPSG Code is optional. This must be used only if we need to re-" -"project the coordinates from Source SRS to another one.\n" +"Target SRS EPSG Code is optional. This must be used only if we need to re-project the " +"coordinates from Source SRS to another one.\n" " " msgstr "" -"Target SRS EPSG Code è facoltativo. Questo deve essere utilizzato solo se è " -"necessario ri-proiettare le coordinate da Source SRS ad un altro.\n" +"Target SRS EPSG Code è facoltativo. Questo deve essere utilizzato solo se è necessario ri-" +"proiettare le coordinate da Source SRS ad un altro.\n" " " #, fuzzy #| msgid "Upload Layer Step: CSV Field Mapping" msgid "Upload Dataset Step: CSV Field Mapping" -msgstr "Upload Layer Step: CSV Field Mapping" +msgstr "Carica dataset, passo: campi CSV" msgid "Geospatial Data" msgstr "Dati geospaziali" msgid "" -"Please indicate which attributes contain the latitude and longitude " -"coordinates in the CSV data." +"Please indicate which attributes contain the latitude and longitude coordinates in the CSV " +"data." msgstr "" -"Si prega di indicare quali attributi contengono le coordinate di latitudine " -"e longitudine nei dati CSV." +"Si prega di indicare quali attributi contengono le coordinate di latitudine e longitudine " +"nei dati CSV." msgid "" "With this data, GeoNode was able to guess which attributes contain the\n" -" latitude and longitude coordinates, but please confirm that " -"the correct\n" +" latitude and longitude coordinates, but please confirm that the correct\n" " attributes are selected below." msgstr "" -"Con questi dati, GeoNode è stato in grado di capire quali attributi " -"contengono\n" -" latitudine e longitudine, ma si prega di verificare che le " -"coordinate siano corrette\n" +"Con questi dati, GeoNode è stato in grado di capire quali attributi contengono\n" +" latitudine e longitudine, ma si prega di verificare che le coordinate siano " +"corrette\n" " per gli attributi sono selezionati di seguito." msgid "Select an attribute" msgstr "Seleziona un attributo" msgid "" -"We did not detect columns that could be used for the latitude and " -"longitude.\n" -" Please verify that you have two columns in your csv file that can be " -"used for\n" +"We did not detect columns that could be used for the latitude and longitude.\n" +" Please verify that you have two columns in your csv file that can be used for\n" " the latitude and longitude." msgstr "" -"Non abbiamo rilevato colonne che potessero essere utilizzate per latitudine " -"e la longitudine.\n" +"Non abbiamo rilevato colonne che potessero essere utilizzate per latitudine e la " +"longitudine.\n" " Verificare di avere due colonne nel file csv che possono essere\n" " latitudine e la longitudine." @@ -3080,10 +2907,8 @@ msgstr "Torna a" msgid "Upload Form" msgstr "Carica Form" -#, fuzzy -#| msgid "Upload Layer Step: Time" msgid "Upload Dataset Step: Time" -msgstr "Caricamento livello: Dimensione temporale" +msgstr "Caricamento dataset: Dimensione temporale" msgid "Inspect data for " msgstr "Esamina dati per " @@ -3092,26 +2917,25 @@ msgid "Configure as Time-Series" msgstr "Configura come serie temporale" msgid "" -"Toggling this selector allows you to configure (or not) this data as a time " -"series; in this case you will also have to select an attribute\n" +"Toggling this selector allows you to configure (or not) this data as a time series; in this " +"case you will also have to select an attribute\n" " to drive the time dimension.\n" "

\n" -" If GeoNode is not able to parse any of the values for " -"the selected attribute red markers will appear to highlight the problems.\n" +" If GeoNode is not able to parse any of the values for the selected " +"attribute red markers will appear to highlight the problems.\n" "

\n" -" More information is provided at the bottom of the page " -"in the \"Additional Help\" sections.\n" +" More information is provided at the bottom of the page in the " +"\"Additional Help\" sections.\n" " " msgstr "" -"L'attivazione/attivazione/attivazione/configurazione di questo selettore " -"consente di configurare (o meno) questi dati come serie di tempo; in questo " -"caso si dovrà anche selezionare un attributo\n" -" per guidare la dimensione " -"temporale.

Se GeoNode non è in grado di " -"analizzare nessuno dei valori per l'attributo selezionato, gli indicatori " -"rossi verranno visualizzati per evidenziare i problemi. " -"

Ulteriori informazioni sono disponibili nella parte inferiore della " -"pagina nelle sezioni \"Guida aggiuntiva\". " +"L'attivazione/attivazione/attivazione/configurazione di questo selettore consente di " +"configurare (o meno) questi dati come serie di tempo; in questo caso si dovrà anche " +"selezionare un attributo\n" +" per guidare la dimensione temporale.

Se " +"GeoNode non è in grado di analizzare nessuno dei valori per l'attributo selezionato, gli " +"indicatori rossi verranno visualizzati per evidenziare i problemi. " +"

Ulteriori informazioni sono disponibili nella parte inferiore della pagina nelle " +"sezioni \"Guida aggiuntiva\". " msgid "No" msgstr "No" @@ -3122,12 +2946,9 @@ msgstr "Utilizzare un attributo timestamp esistente nei dati" msgid "Yes: with an existing Time-Attribute" msgstr "Sì: con un attributo Time esistente" -msgid "" -"Yes: by converting data to a timestamp using standard date/time " -"representation" +msgid "Yes: by converting data to a timestamp using standard date/time representation" msgstr "" -"Sì: convertendo i dati in un timestamp utilizzando la rappresentazione " -"standard di data/ora" +"Sì: convertendo i dati in un timestamp utilizzando la rappresentazione standard di data/ora" msgid "Convert a number field into a year" msgstr "Convertire un campo numerico in un anno" @@ -3135,12 +2956,10 @@ msgstr "Convertire un campo numerico in un anno" msgid "Yes: by converting a number as Year" msgstr "Sì: convertendo un numero come Anno" -msgid "" -"Convert data to a timestamp using standard date/time representation or a " -"custom format" +msgid "Convert data to a timestamp using standard date/time representation or a custom format" msgstr "" -"Convertire i dati in un timestamp utilizzando la rappresentazione di data/" -"ora standard o un formato personalizzato" +"Convertire i dati in un timestamp utilizzando la rappresentazione di data/ora standard o un " +"formato personalizzato" msgid "Start Importer" msgstr "Avvia importazione" @@ -3187,12 +3006,10 @@ msgstr "definito dalla risoluzione" msgid "Continuous Intervals" msgstr "Intervalli continui" -msgid "" -"for data that is frequently updated, resolution describes the frequency of " -"updates" +msgid "for data that is frequently updated, resolution describes the frequency of updates" msgstr "" -"per i dati che viene aggiornato di frequente, risoluzione descrive la " -"frequenza degli aggiornamenti" +"per i dati che viene aggiornato di frequente, risoluzione descrive la frequenza degli " +"aggiornamenti" msgid "Resolution of time attribute" msgstr "Risoluzione di un attributo tempo" @@ -3200,31 +3017,20 @@ msgstr "Risoluzione di un attributo tempo" msgid "Enabling Time" msgstr "Tempo di attivazione" -#, fuzzy -#| msgid "" -#| "A layer can support one or two time attributes. If a single\n" -#| " attribute is used, the layer is considered to " -#| "contain data that is valid at single points in time. If two\n" -#| " attributes are used, the second attribute " -#| "represents the end of a valid period hence the layer is considered\n" -#| " to contain data that is valid at certain periods " -#| "in time." msgid "" "A dataset can support one or two time attributes. If a single\n" -" attribute is used, the dataset is considered to " -"contain data that is valid at single points in time. If two\n" -" attributes are used, the second attribute represents " -"the end of a valid period hence the dataset is considered\n" -" to contain data that is valid at certain periods in " -"time." -msgstr "" -"Un layer può supportare uno o due attributi temporali. Se un singolo\n" -" attributo viene utilizzato, il layer è considerato " -"contenere dati validi in singoli punti nel tempo. Se due\n" -" attributi, il secondo attributo rappresenta la fine " -"di un periodo valido, quindi il layer è considerato\n" -" per contenere dati validi in determinati periodi di " -"tempo." +" attribute is used, the dataset is considered to contain data that is " +"valid at single points in time. If two\n" +" attributes are used, the second attribute represents the end of a " +"valid period hence the dataset is considered\n" +" to contain data that is valid at certain periods in time." +msgstr "" +"Un dataset può supportare uno o due attributi temporali. Se un singolo\n" +" attributo viene utilizzato, il layer è considerato contenere dati " +"validi in singoli punti nel tempo. Se due\n" +" attributi, il secondo attributo rappresenta la fine di un periodo " +"valido, quindi il layer è considerato\n" +" per contenere dati validi in determinati periodi di tempo." msgid "Selecting an Attribute" msgstr "Seleziona un attributo" @@ -3243,30 +3049,24 @@ msgstr "Un numero che rappresenta l'anno" msgid "" "\n" -" For text attributes, one can specify a custom format " -"(as part of the \"Advanced Options\") or use the 'best guess' approach which " -"will try to\n" -" automatically translate well-known recognized " -"patterns into valid times.\n" +" For text attributes, one can specify a custom format (as part of the " +"\"Advanced Options\") or use the 'best guess' approach which will try to\n" +" automatically translate well-known recognized patterns into valid " +"times.\n" " " msgstr "" "\n" -" Per gli attributi di testo, è possibile specificare " -"un formato personalizzato (come parte delle \"Opzioni avanzate\") o " -"utilizzare l'approccio \"best guess\" che cercherà di\n" -" convertire automaticamente i patterns riconosciuti " -"in date valide.\n" +" Per gli attributi di testo, è possibile specificare un formato " +"personalizzato (come parte delle \"Opzioni avanzate\") o utilizzare l'approccio \"best " +"guess\" che cercherà di\n" +" convertire automaticamente i patterns riconosciuti in date valide.\n" " " msgid "The 'best guess' will handle date and optional time variants of" -msgstr "" -"L''ipotesi migliore' in grado di gestire le varianti di data e ora opzionale " -"di" +msgstr "L''ipotesi migliore' in grado di gestire le varianti di data e ora opzionale di" msgid "In terms of the formatting flags noted above, these are" -msgstr "" -"Per quanto riguarda le bandiere di formattazione osservato in precedenza, si " -"tratta di" +msgstr "Per quanto riguarda le bandiere di formattazione osservato in precedenza, si tratta di" msgid "Modal Header" msgstr "Intestazione modale" @@ -3295,11 +3095,9 @@ msgstr "Scelta sbagliata" msgid "Please, select one Time Attribute to test!" msgstr "Si prega di selezionare un Attributo Temporale!" -msgid "" -"Returning to the upload starting page in 5seconds " +msgid "Returning to the upload starting page in 5seconds " msgstr "" -"Sarai rediretto alla pagina iniziale di caricamento entro 5 secondi " +"Sarai rediretto alla pagina iniziale di caricamento entro 5 secondi " msgid " Or just go " msgstr " O semplicemente andare " @@ -3318,21 +3116,13 @@ msgid "You are attempting to {action_type} a raster dataset with a vector." msgstr "Si sta tentando di {action_type} un livello raster con un vettore." #, fuzzy, python-brace-format -#| msgid "" -#| "You are attempting to {action_type} a vector layer with an unknown format." -msgid "" -"You are attempting to {action_type} a vector dataset with an unknown format." -msgstr "" -"Si sta tentando di {action_type} un livello vettoriale con un formato " -"sconosciuto." +#| msgid "You are attempting to {action_type} a vector layer with an unknown format." +msgid "You are attempting to {action_type} a vector dataset with an unknown format." +msgstr "Si sta tentando di {action_type} un livello vettoriale con un formato sconosciuto." #, python-brace-format -msgid "" -"Please ensure the name is consistent with the file you are trying to " -"{action_type}." -msgstr "" -"Assicurarsi che il nome sia coerente con il file che si sta tentando di " -"{action_type}." +msgid "Please ensure the name is consistent with the file you are trying to {action_type}." +msgstr "Assicurarsi che il nome sia coerente con il file che si sta tentando di {action_type}." #, fuzzy #| msgid "Local GeoNode layer has no geometry type." @@ -3341,83 +3131,55 @@ msgstr "Il livello GeoNode locale non ha alcun tipo di geometria." #, python-brace-format msgid "" -"Please ensure there is at least one geometry " -"type that is consistent with the file you are " -"trying to {action_type}." +"Please ensure there is at least one geometry type that is " +"consistent with the file you are trying to {action_type}." msgstr "" -"Assicurarsi che sia disponibile almeno un tipo di geometria coerente con il " -"file che si sta tentando di {action_type}." +"Assicurarsi che sia disponibile almeno un tipo di geometria coerente con il file che si sta " +"tentando di {action_type}." -#, fuzzy -#| msgid "The selected Layer does not exists in the catalog." msgid "The selected Dataset does not exists in the catalog." -msgstr "Il livello selezionato non esiste nel catalogo." +msgstr "Il dataset selezionato non esiste nel catalogo." -#, fuzzy -#| msgid "Please ensure that the layer structure is consistent " msgid "Please ensure that the dataset structure is consistent " -msgstr "Assicurarsi che la struttura del livello sia coerente " +msgstr "Assicurarsi che la struttura del dataset sia coerente " -msgid "" -"Some error occurred while trying to access the uploaded schema: {str(e)}" +msgid "Some error occurred while trying to access the uploaded schema: {str(e)}" msgstr "" -"Si è verificato un errore durante il tentativo di accesso allo schema " -"caricato: {str(e)}" +"Si è verificato un errore durante il tentativo di accesso allo schema caricato: {str(e)}" msgid "" -"There was an error while attempting to upload your data. Please try again, " -"or contact and administrator if the problem continues." +"There was an error while attempting to upload your data. Please try again, or contact and " +"administrator if the problem continues." msgstr "" -"E' occorso un errore durante l'invio dei tuoi dati. Riprova o contatta un " -"amministratore se il problema persiste." +"E' occorso un errore durante l'invio dei tuoi dati. Riprova o contatta un amministratore se " +"il problema persiste." -#, fuzzy -#| msgid "" -#| "Note: this layer's orginal metadata was populated and preserved by " -#| "importing a metadata XML file. This metadata cannot be edited." msgid "" -"Note: this dataset's orginal metadata was populated and preserved by " -"importing a metadata XML file. This metadata cannot be edited." +"Note: this dataset's orginal metadata was populated and preserved by importing a metadata " +"XML file. This metadata cannot be edited." msgstr "" -"Nota: i metadati originali di questo livello sono stati popolati e " -"conservati importando un file XML di metadati. Questi metadati non possono " -"essere modificati." +"Nota: i metadati originali di questo dataset sono stati popolati e conservati importando un " +"file XML di metadati. Questi metadati non possono essere modificati." -#, fuzzy -#| msgid "You are not permitted to delete this layer" msgid "You are not permitted to delete this dataset" -msgstr "Non hai il permesso di eliminare questo livello" +msgstr "Non hai il permesso di eliminare questo dataset" -#, fuzzy -#| msgid "You do not have permissions for this layer." msgid "You do not have permissions for this dataset." -msgstr "Non hai i permessi per questo livello." +msgstr "Non hai i permessi per questo dataset." -#, fuzzy -#| msgid "You are not permitted to modify this layer" msgid "You are not permitted to modify this dataset" -msgstr "Non hai i permessi per modificare questo livello" +msgstr "Non hai i permessi per modificare questo dataset" -#, fuzzy -#| msgid "You are not permitted to modify this layer's metadata" msgid "You are not permitted to modify this dataset's metadata" -msgstr "Non hai il permesso di modificare i metadati di questo livello" +msgstr "Non hai il permesso di modificare i metadati di questo dataset" -#, fuzzy -#| msgid "You are not permitted to view this layer" msgid "You are not permitted to view this dataset" -msgstr "Non hai il permesso di visualizzare questo livello" +msgstr "Non hai il permesso di visualizzare questo dataset" -#, fuzzy -#| msgid "" -#| "This layer is a member of a layer group, you must remove the layer from " -#| "the group before deleting." msgid "" -"This dataset is a member of a layer group, you must remove the dataset from " -"the group before deleting." -msgstr "" -"Questo livello è membro di un gruppo, devi rimuoverlo dal gruppo prima di " -"eliminarlo." +"This dataset is a member of a layer group, you must remove the dataset from the group before " +"deleting." +msgstr "Questo dataset è membro di un gruppo, devi rimuoverlo dal gruppo prima di eliminarlo." #, python-format msgid "couldn't generate thumbnail: %s" @@ -3516,11 +3278,9 @@ msgstr "Valuta questa mappa" msgid "Download Map" msgstr "Scarica la mappa" -#, fuzzy -#| msgid "Maps" msgid "Map" msgid_plural "Maps" -msgstr[0] "Mappe" +msgstr[0] "Mappa" msgstr[1] "Mappe" msgid "Map Layers" @@ -3560,25 +3320,23 @@ msgstr "" msgid "" "\n" -"
Could not find downloadable layers " -"for this map. You can go back to \n" +"
Could not find downloadable layers for this map. " +"You can go back to \n" " " msgstr "" "\n" -"
Impossibile trovare livelli " -"scaricabili per questa mappa. Si può tornare a \n" +"
Impossibile trovare livelli scaricabili per questa " +"mappa. Si può tornare a \n" " " msgid "" "\n" -" Additionally, the map contains these layers which will not be " -"downloaded\n" +" Additionally, the map contains these layers which will not be downloaded\n" " due to security restrictions:\n" " " msgstr "" "\n" -" Inoltre, la mappa contiene questi livelli che non possono essere " -"scaricati\n" +" Inoltre, la mappa contiene questi livelli che non possono essere scaricati\n" " per vincoli di sicurezza:\n" " " @@ -3589,8 +3347,7 @@ msgid "" " " msgstr "" "\n" -" Infine, la mappa contiene questi livelli che non possono essere " -"scaricati\n" +" Infine, la mappa contiene questi livelli che non possono essere scaricati\n" " perchè non sono disponibili direttamente da questo GeoNode:\n" " " @@ -3616,16 +3373,14 @@ msgid "Explore Maps" msgstr "Esplora mappe" msgid "" -"Note: this map's orginal metadata was populated by importing a metadata XML " -"file.\n" -" GeoNode's metadata import supports a subset of ISO, FGDC, and Dublin " -"Core metadata elements.\n" +"Note: this map's orginal metadata was populated by importing a metadata XML file.\n" +" GeoNode's metadata import supports a subset of ISO, FGDC, and Dublin Core metadata " +"elements.\n" " Some of your original metadata may have been lost." msgstr "" -"Nota: i metadati originale di questa mappa era popolato importando un file " -"XML di metadati. Metadati importazione di GeoNode supporta un sottoinsieme " -"di elementi di metadati ISO, FGDC, e Dublin Core. Alcuni dei i metadati " -"originale Potrebbero essere state compromesse." +"Nota: i metadati originale di questa mappa era popolato importando un file XML di metadati. " +"Metadati importazione di GeoNode supporta un sottoinsieme di elementi di metadati ISO, FGDC, " +"e Dublin Core. Alcuni dei i metadati originale Potrebbero essere state compromesse." msgid "Return to Map" msgstr "Ritorna alla mappa" @@ -3636,13 +3391,13 @@ msgstr "Rimuovi mappa" #, python-format msgid "" "\n" -" Are you sure you want to remove %(map_title)s?\n" +" Are you sure you want to remove %(map_title)s?\n" " " msgstr "" "\n" -" Sei sicuro di voler rimuovere " -"%(map_title)s?\n" +" Sei sicuro di voler rimuovere %(map_title)s?\n" " " msgid "You are not permitted to delete this map." @@ -3688,18 +3443,18 @@ msgid "Show list of services" msgstr "Elenco dei servizi" msgid "" -"Process data since specific timestamp (YYYY-MM-DD HH:MM:SS format). If not " -"provided, last sync will be used." +"Process data since specific timestamp (YYYY-MM-DD HH:MM:SS format). If not provided, last " +"sync will be used." msgstr "" -"Elaborare i dati dal timestamp specifico (formato AAAA-MM-GG HH:MM:SS). Se " -"non viene specificato, verrà utilizzata l'ultima sincronizzazione." +"Elaborare i dati dal timestamp specifico (formato AAAA-MM-GG HH:MM:SS). Se non viene " +"specificato, verrà utilizzata l'ultima sincronizzazione." msgid "" -"Process data until specific timestamp (YYYY-MM-DD HH:MM:SS format). If not " -"provided, now will be used." +"Process data until specific timestamp (YYYY-MM-DD HH:MM:SS format). If not provided, now " +"will be used." msgstr "" -"Elaborare i dati fino al timestamp specifico (formato AAAA-MM-GG HH:MM:SS). " -"Se non specificato, verrà utilizzata l'ora attuale." +"Elaborare i dati fino al timestamp specifico (formato AAAA-MM-GG HH:MM:SS). Se non " +"specificato, verrà utilizzata l'ora attuale." msgid "Force check" msgstr "Controllo forzato" @@ -3708,11 +3463,11 @@ msgid "Format of audit log (xml, json)" msgstr "Formato del log di audit (xml, json)" msgid "" -"Should old data be preserved (default: no, data older than settings." -"MONITORING_DATA_TTL will be removed)" +"Should old data be preserved (default: no, data older than settings.MONITORING_DATA_TTL will " +"be removed)" msgstr "" -"Conservare i dati precedenti (impostazione predefinita: no, i dati più " -"vecchi di settings.MONITORING_DATA_TTL verranno rimossi)" +"Conservare i dati precedenti (impostazione predefinita: no, i dati più vecchi di settings." +"MONITORING_DATA_TTL verranno rimossi)" msgid "Should stop on first error occured (default: no)" msgstr "Interruzione al primo errore (impostazione predefinita: no)" @@ -3739,8 +3494,7 @@ msgid "Metric name" msgstr "Nome metrica" msgid "Show data for specific resource in resource_type=resource_name format" -msgstr "" -"Mostra i dati per una risorsa specifica nel formato tipo_risorsa-nome_risorsa" +msgstr "Mostra i dati per una risorsa specifica nel formato tipo_risorsa-nome_risorsa" msgid "Show data for specific resource" msgstr "Mostra dati per risorsa specifica" @@ -3752,12 +3506,10 @@ msgstr "Mostra dati per un'etichetta specifica" msgid "Write result to file, default GEOIP_PATH: {settings.GEOIP_PATH}" msgstr "Scritto risultato su file, default GEOIP_PATH: {settings.GEOIP_PATH}" -msgid "" -"Fetch database from specific url. If nothing provided, default {} will be " -"used" +msgid "Fetch database from specific url. If nothing provided, default {} will be used" msgstr "" -"Recuperare il database da un URL specifico. Se non viene fornito nulla, " -"verrà utilizzato il valore predefinito {}" +"Recuperare il database da un URL specifico. Se non viene fornito nulla, verrà utilizzato il " +"valore predefinito {}" msgid "Overwrite file if exists" msgstr "Sovrascrivi file se esiste già" @@ -3908,8 +3660,7 @@ msgid "Last update must not be older than" msgstr "L'ultimo aggiornamento non deve essere più vecchio di" msgid "Max timeout for given metric before error should be raised" -msgstr "" -"Timeout massimo per la metrica specificata prima che venga generato l'errore" +msgstr "Timeout massimo per la metrica specificata prima che venga generato l'errore" msgid "Monitoring & Analytics" msgstr "Monitoraggio & Analytics" @@ -3972,29 +3723,22 @@ msgstr "Cambia password: %s" msgid "party who authored the resource" msgstr "gruppo che ha creato la risorsa" -msgid "" -"party who has processed the data in a manner such that the resource has been " -"modified" -msgstr "" -"gruppo che ha elaborato i dati in modo tale che la risorsa sia stata " -"modificata" +msgid "party who has processed the data in a manner such that the resource has been modified" +msgstr "gruppo che ha elaborato i dati in modo tale che la risorsa sia stata modificata" msgid "party who published the resource" msgstr "gruppo che ha pubblicato la risorsa" msgid "" -"party that accepts accountability and responsibility for the data and " -"ensures appropriate care and maintenance of the resource" +"party that accepts accountability and responsibility for the data and ensures " +"appropriate care and maintenance of the resource" msgstr "" -"partito che accetta la responsabilità e la responsabilità per i dati e " -"garantisce la cura e la manutenzione appropriata della risorsa" +"partito che accetta la responsabilità e la responsabilità per i dati e garantisce la cura e " +"la manutenzione appropriata della risorsa" -msgid "" -"party who can be contacted for acquiring knowledge about or acquisition of " -"the resource" +msgid "party who can be contacted for acquiring knowledge about or acquisition of the resource" msgstr "" -"parte che può essere contattata per l'acquisizione di conoscenza o " -"acquisizione della risorsa" +"parte che può essere contattata per l'acquisizione di conoscenza o acquisizione della risorsa" msgid "party who distributes the resource" msgstr "parte che distribuisce la risorsa" @@ -4013,8 +3757,7 @@ msgstr "parte che possiede la risorsa" msgid "key party responsible for gathering information and conducting research" msgstr "" -"parte chiave responsabile della raccolta di informazioni e della conduzione " -"della ricerca" +"parte chiave responsabile della raccolta di informazioni e della conduzione della ricerca" msgid "Email Address" msgstr "Indirizzo email" @@ -4032,7 +3775,7 @@ msgid "introduce yourself" msgstr "presentarsi" msgid "Position Name" -msgstr "Ruolo" +msgstr "Incarico" msgid "role or position of the responsible person" msgstr "ruolo o posizione della persona responsabile" @@ -4041,30 +3784,23 @@ msgid "Voice" msgstr "Voce" msgid "" -"telephone number by which individuals can speak to the responsible " -"organization or individual" +"telephone number by which individuals can speak to the responsible organization or individual" msgstr "" -"numero di telefono con cui le persone possono parlare con l'organizzazione o " -"l'individuo responsabile" +"numero di telefono con cui le persone possono parlare con l'organizzazione o l'individuo " +"responsabile" msgid "Facsimile" msgstr "Fax" -msgid "" -"telephone number of a facsimile machine for the responsible organization or " -"individual" -msgstr "" -"numero di telefono o fax per l'organizzazione o l'individuo responsabile" +msgid "telephone number of a facsimile machine for the responsible organization or individual" +msgstr "numero di telefono o fax per l'organizzazione o l'individuo responsabile" msgid "Delivery Point" msgstr "Informazioni di contatto" -msgid "" -"physical and email address at which the organization or individual may be " -"contacted" +msgid "physical and email address at which the organization or individual may be contacted" msgstr "" -"indirizzo fisico e email ai quali l'organizzazione o l'individuo possono " -"essere contattati" +"indirizzo fisico e email ai quali l'organizzazione o l'individuo possono essere contattati" msgid "City" msgstr "Città" @@ -4091,11 +3827,11 @@ msgid "country of the physical address" msgstr "paese dell'indirizzo fisico" msgid "" -"commonly used word(s) or formalised word(s) or phrase(s) used to describe " -"the subject (space or comma-separated" +"commonly used word(s) or formalised word(s) or phrase(s) used to describe the " +"subject (space or comma-separated" msgstr "" -"parola di uso comune (s) o una parola formalizzata (s) o la frase (s) " -"utilizzati per descrivere il soggetto (spazio o separato da virgole" +"parola di uso comune (s) o una parola formalizzata (s) o la frase (s) utilizzati per " +"descrivere il soggetto (spazio o separato da virgole" msgid "Timezone" msgstr "Fuso orario" @@ -4113,11 +3849,11 @@ msgid "Forgot Username" msgstr "Username dimenticato" msgid "" -"Enter your email address and click the submit button.
Your username " -"will be sent to you." +"Enter your email address and click the submit button.
Your username will be sent to " +"you." msgstr "" -"Inserisci il tuo indirizzo email e clicca il bottone submit.
Il tuo " -"username sarà spedito all'indirizzo specificato." +"Inserisci il tuo indirizzo email e clicca il bottone submit.
Il tuo username sarà " +"spedito all'indirizzo specificato." msgid "Create Profile" msgstr "Crea profilo" @@ -4129,7 +3865,7 @@ msgid "Not provided." msgstr "Non fornito." msgid "Position" -msgstr "Posizione" +msgstr "Incarico" msgid "Organization" msgstr "Organizzazione" @@ -4219,12 +3955,8 @@ msgstr "Non è consentito salvare o modificare questa risorsa." msgid "You are not authorized to download this resource." msgstr "Non è consentito scaricare questa risorsa." -msgid "" -"No files have been found for this resource. Please, contact a system " -"administrator." -msgstr "" -"Nessun file trovato per questa risorsa. Contattare un amministratore di " -"sistema." +msgid "No files have been found for this resource. Please, contact a system administrator." +msgstr "Nessun file trovato per questa risorsa. Contattare un amministratore di sistema." msgid "No files found." msgstr "Nessun file trovato." @@ -4232,10 +3964,8 @@ msgstr "Nessun file trovato." msgid "Not Authorized" msgstr "Non autorizzato" -#, fuzzy -#| msgid "GeoNode Themes Library" msgid "GeoNode Resource Processing Library" -msgstr "Libreria dei temi GeoNode" +msgstr "Libreria dei processamenti GeoNode" msgid "Disabling this Task will make the Processing Workflow to skip it." msgstr "" @@ -4253,11 +3983,11 @@ msgid "Error updating permissions :(" msgstr "Errore durante l'aggiornamento delle autorizzazioni :(" msgid "" -"User {username} has download permissions but cannot access the resource. " -"Please update permission consistently!" +"User {username} has download permissions but cannot access the resource. Please update " +"permission consistently!" msgstr "" -"L'utente {username} ha le autorizzazioni di scaricamento ma non può " -"visualizzare la risorsa. Controllare che le autorizzazioni siano consistenti." +"L'utente {username} ha le autorizzazioni di scaricamento ma non può visualizzare la risorsa. " +"Controllare che le autorizzazioni siano consistenti." msgid "You are not allowed to change permissions for this resource" msgstr "Non ti è consentito modificare le autorizzazioni per questa risorsa" @@ -4480,10 +4210,8 @@ msgstr "La Risorsa {resource_id} è in fase di elaborazione" msgid "Service rescanned successfully" msgstr "Il servizio è stato analizzato di nuovo correttamente" -#, fuzzy -#| msgid "You are not permitted to save or edit this resource." msgid "You dont have enougth rigths to see the resource detail" -msgstr "Non è consentito salvare o modificare questa risorsa." +msgstr "Non hai i privilegi necessari per vedere i dettagli di questa risorsa" msgid "You are not permitted to change this service." msgstr "Non è consentito modificare questo servizio." @@ -4545,13 +4273,12 @@ msgstr "Pagina non trovata" msgid "" "\n" -" The page you requested does not exist. Perhaps you are using an " -"outdated bookmark?\n" +" The page you requested does not exist. Perhaps you are using an outdated " +"bookmark?\n" " " msgstr "" "\n" -" La pagina richiesta non esiste. Forse si utilizza un segnalibro " -"obsoleto?\n" +" La pagina richiesta non esiste. Forse si utilizza un segnalibro obsoleto?\n" " " msgid "Toggle navigation" @@ -4578,31 +4305,28 @@ msgid "You are using an outdated browser that is not supported by GeoNode." msgstr "Stai usando un browser obsoleto che non è supportato da GeoNode." msgid "" -"Please use a modern browser like Mozilla Firefox, Google " -"Chrome or Safari." +"Please use a modern browser like Mozilla Firefox, Google Chrome or Safari." msgstr "" -"Utilizza un browser moderno come Mozilla Firefox, Google " -"Chrome o Safari." +"Utilizza un browser moderno come Mozilla Firefox, Google Chrome o Safari." msgid "There was a problem loading this page" msgstr "C'è stato un problema nel caricare la pagina" msgid "" "\n" -" Please contact your GeoNode administrator (they may have " -"received an email automatically if they configured it properly).\n" -" If you are the site administrator, enable debug mode to see the " -"actual error and fix it or file an issue in
GeoNode's issue tracker\n" +" Please contact your GeoNode administrator (they may have received an email " +"automatically if they configured it properly).\n" +" If you are the site administrator, enable debug mode to see the actual error and " +"fix it or file an issue in GeoNode's " +"issue tracker\n" " " msgstr "" "\n" -" Si prega di contattare l'amministratore di GeoNode (può aver " -"ricevuto un'e-mail automaticamente se ha configurato correttamente).\n" -" Se sei l'amministratore del sito, attivare la modalità di debug " -"visualizzare l'errore effettivo e risolvere il problema o un problema del " -"file nel tracciatore di " -"problemi di GeoNode\n" +" Si prega di contattare l'amministratore di GeoNode (può aver ricevuto un'e-mail " +"automaticamente se ha configurato correttamente).\n" +" Se sei l'amministratore del sito, attivare la modalità di debug visualizzare " +"l'errore effettivo e risolvere il problema o un problema del file nel tracciatore di problemi di GeoNode\n" " " #, fuzzy @@ -4703,21 +4427,16 @@ msgstr "Chi può scaricarlo?" msgid "Who can change metadata for it?" msgstr "Chi può modificare i metadati per questo?" -#, fuzzy -#| msgid "Who can edit data for this layer?" msgid "Who can edit data for this dataset?" -msgstr "Chi può modificare i dati di questo livello?" +msgstr "Chi può modificare i dati di questo dataset?" -#, fuzzy -#| msgid "Who can edit styles for this layer?" msgid "Who can edit styles for this dataset?" -msgstr "Chi può modificare gli stili di questo livello?" +msgstr "Chi può modificare gli stili di questo dataset?" -msgid "" -"Who can manage it? (update, delete, change permissions, publish/unpublish it)" +msgid "Who can manage it? (update, delete, change permissions, publish/unpublish it)" msgstr "" -"Chi può gestirlo? (Aggiornare, cancellare, modificare le autorizzazioni, " -"pubblicare / annullare la pubblicazione di esso)" +"Chi può gestirlo? (Aggiornare, eliminare, modificare le autorizzazioni, pubblicare / " +"annullare la pubblicazione di esso)" msgid "Set permissions for this resource" msgstr "Impostare le autorizzazioni per questa risorsa" @@ -4753,12 +4472,11 @@ msgid "Mandatory files : SHP , DBF" msgstr "File obbligatori: .shp, .dbf" msgid "" -"Upload a ZIP file containing an ESRI Shapefile. If the ZIP provides also a ." -"prj file, you don't have to specify the EPSG SRID" +"Upload a ZIP file containing an ESRI Shapefile. If the ZIP provides also a .prj file, you " +"don't have to specify the EPSG SRID" msgstr "" -"Carica uo file ZIP contenente uno Shapegile ESRI. Se lo ZIP fornisceanche un " -"file .prj non è necessario specificare il Sistema di Riferimento (Codice " -"EPSG)" +"Carica uo file ZIP contenente uno Shapegile ESRI. Se lo ZIP fornisceanche un file .prj non è " +"necessario specificare il Sistema di Riferimento (Codice EPSG)" msgid "Choose" msgstr "Seleziona" @@ -4782,32 +4500,31 @@ msgid "Load SHP-ZIP" msgstr "Carica uno Shapefile .zip" msgid "" -"

This will remove the Geo Limits currently drawn on the map.

In " -"order to store the Geo Limits you will need to save them " -"anyway.

Do you want to proceed?

" +"

This will remove the Geo Limits currently drawn on the map.

In order to store the " +"Geo Limits you will need to save them anyway.

Do you want to proceed?" +"

" msgstr "" -"

Questa azione rimuoverà i limiti spaziali attualmente disegnati " -"sullamappa.

Vuoi procedere?

" +"

Questa azione rimuoverà i limiti spaziali attualmente disegnati sullamappa.

Vuoi " +"procedere?

" msgid "" -"

This will override the current stored Geo Limits on the DB.

To " -"apply them you will need to click on Apply Changes button " -"anyway.

WARNING: This operation cannot be reverted!

Do you want to proceed?

" +"

This will override the current stored Geo Limits on the DB.

To apply them you will " +"need to click on Apply Changes button anyway.

WARNING: This operation cannot be reverted!

Do you want to proceed?

" msgstr "" -"

Questa azione sovrascriverà i limiti spaziali attualmente salvantinel DB." -"

Per procedere clicca su Esegui.

ATTENZIONE: Questa azione è irreversibile!

" +"

Questa azione sovrascriverà i limiti spaziali attualmente salvantinel DB.

Per " +"procedere clicca su Esegui.

ATTENZIONE: Questa " +"azione è irreversibile!

" msgid "Save Geo Limit" msgstr "Salve limiti spaziali" msgid "" -"

Geometry successfully saved!

To apply them you " -"will need to click on Apply Changes button.

" +"

Geometry successfully saved!

To apply them you will need to click " +"on Apply Changes button.

" msgstr "" -"

Geometria salvata con successo!

Per applicarli è " -"necessario fare clic sul pulsante Applica modifiche.

" +"

Geometria salvata con successo!

Per applicarli è necessario fare " +"clic sul pulsante Applica modifiche.

" msgid "

Error while trying to save the Geometry!

" msgstr "

Errore nel salvataggio della geometria!

" @@ -4822,13 +4539,13 @@ msgid "

No Geometry found!

" msgstr "

Nessuna geometria trovata!

" msgid "" -"

This will permanently remove the Geo Limits from the DB." -"

To apply them you will need to click on Apply Changes button.

Do you want to proceed?

" +"

This will permanently remove the Geo Limits from the DB.

To apply " +"them you will need to click on Apply Changes button.

Do you want to " +"proceed?

" msgstr "" -"

In questo modo i limiti geografici verranno rimossi " -"definitivamente dal database.

Per applicarli è necessario fare clic " -"sul pulsante Applica modifiche.

Vuoi procedere?

" +"

In questo modo i limiti geografici verranno rimossi definitivamente dal " +"database.

Per applicarli è necessario fare clic sul pulsante Applica modifiche." +"

Vuoi procedere?

" msgid "Delete Geo Limit" msgstr "Elimina limite geografico" @@ -4836,11 +4553,9 @@ msgstr "Elimina limite geografico" msgid "

Geo Limitsy successfully deleted!

" msgstr "

Geo Limite eliminato con successo!

" -msgid "" -"

Error occurred while trying to delete Geo Limits:

" +msgid "

Error occurred while trying to delete Geo Limits:

" msgstr "" -"

Errore durante il tentativo di eliminazione dei limiti " -"geografici:

" +"

Errore durante il tentativo di eliminazione dei limiti geografici:

" msgid "Choose users..." msgstr "Scegli gli utenti ..." @@ -4858,43 +4573,37 @@ msgid "About GeoNode" msgstr "A proposito di GeoNode" msgid "" -"GeoNode is a geospatial content management system, a platform for the " -"management and publication of geospatial data. It brings together mature and " -"stable open-source software projects under a consistent and easy-to-use " -"interface allowing non-specialized users to share data and create " -"interactive maps." +"GeoNode is a geospatial content management system, a platform for the management and " +"publication of geospatial data. It brings together mature and stable open-source software " +"projects under a consistent and easy-to-use interface allowing non-specialized users to " +"share data and create interactive maps." msgstr "" -"GeoNode è un sistema di gestione dei contenuti geospaziali, una piattaforma " -"per la gestione e la pubblicazione di dati geospaziali. Riunisce progetti " -"software open source maturi e stabili sotto un'interfaccia coerente e facile " -"da usare che consente agli utenti non specializzati di condividere dati e " -"creare mappe interattive." +"GeoNode è un sistema di gestione dei contenuti geospaziali, una piattaforma per la gestione " +"e la pubblicazione di dati geospaziali. Riunisce progetti software open source maturi e " +"stabili sotto un'interfaccia coerente e facile da usare che consente agli utenti non " +"specializzati di condividere dati e creare mappe interattive." msgid "" -"Data management tools built into GeoNode allow for integrated creation of " -"data, metadata, and map visualizations. Each dataset in the system can be " -"shared publicly or restricted to allow access to only specific users. Social " -"features like user profiles and commenting and rating systems allow for the " -"development of communities around each platform to facilitate the use, " -"management, and quality control of the data the GeoNode instance contains." +"Data management tools built into GeoNode allow for integrated creation of data, metadata, " +"and map visualizations. Each dataset in the system can be shared publicly or restricted to " +"allow access to only specific users. Social features like user profiles and commenting and " +"rating systems allow for the development of communities around each platform to facilitate " +"the use, management, and quality control of the data the GeoNode instance contains." msgstr "" -"Gli strumenti di gestione dei dati integrati in GeoNode consentono la " -"creazione integrata di dati, metadati e visualizzazioni mappa. Ogni set di " -"dati nel sistema può essere condiviso pubblicamente o limitato per " -"consentire l'accesso solo a utenti specifici. Le funzionalità di social " -"networking come i profili utente e i sistemi di commenti e classificazione " -"consentono lo sviluppo di comunità intorno a ogni piattaforma per facilitare " -"l'utilizzo, la gestione e il controllo qualità dei dati contenuti " -"nell'istanza GeoNode." +"Gli strumenti di gestione dei dati integrati in GeoNode consentono la creazione integrata di " +"dati, metadati e visualizzazioni mappa. Ogni set di dati nel sistema può essere condiviso " +"pubblicamente o limitato per consentire l'accesso solo a utenti specifici. Le funzionalità " +"di social networking come i profili utente e i sistemi di commenti e classificazione " +"consentono lo sviluppo di comunità intorno a ogni piattaforma per facilitare l'utilizzo, la " +"gestione e il controllo qualità dei dati contenuti nell'istanza GeoNode." msgid "" -"It is also designed to be a flexible platform that software developers can " -"extend, modify or integrate against to meet requirements in their own " -"applications." +"It is also designed to be a flexible platform that software developers can extend, modify or " +"integrate against to meet requirements in their own applications." msgstr "" -"È inoltre progettato per essere una piattaforma flessibile che gli " -"sviluppatori di software possono estendere, modificare o integrare per " -"soddisfare i requisiti nelle proprie applicazioni." +"È inoltre progettato per essere una piattaforma flessibile che gli sviluppatori di software " +"possono estendere, modificare o integrare per soddisfare i requisiti nelle proprie " +"applicazioni." msgid "Account Inactive" msgstr "Account non attivo" @@ -4907,13 +4616,11 @@ msgstr "Account in attesa di approvazione" #, python-format msgid "" -"We have sent the administrators a notice to approve your account associated " -"with %(email)s. If the account is approved, you will receive a " -"confirmation notice." +"We have sent the administrators a notice to approve your account associated with " +"%(email)s. If the account is approved, you will receive a confirmation notice." msgstr "" -"Abbiamo inviato agli amministratori un avviso per approvare il tuo account " -"associato a %(email)s. Se l'account viene approvato, riceverai un " -"avviso di conferma." +"Abbiamo inviato agli amministratori un avviso per approvare il tuo account associato a " +"%(email)s. Se l'account viene approvato, riceverai un avviso di conferma." msgid "Account" msgstr "Account" @@ -4943,12 +4650,12 @@ msgid "Warning:" msgstr "Avviso:" msgid "" -"You currently do not have any e-mail address set up. You should really add " -"an e-mail address so you can receive notifications, reset your password, etc." +"You currently do not have any e-mail address set up. You should really add an e-mail address " +"so you can receive notifications, reset your password, etc." msgstr "" -"Al momento non è stato impostato alcun indirizzo di posta elettronica. Si " -"dovrebbe davvero aggiungere un indirizzo e-mail in modo da poter ricevere " -"notifiche, reimpostare la password, ecc." +"Al momento non è stato impostato alcun indirizzo di posta elettronica. Si dovrebbe davvero " +"aggiungere un indirizzo e-mail in modo da poter ricevere notifiche, reimpostare la password, " +"ecc." msgid "Add E-mail Address" msgstr "Aggiungi indirizzo e-mail" @@ -4963,15 +4670,15 @@ msgstr "Vuoi davvero rimuovere l'indirizzo e-mail selezionato?" msgid "" "Hello from %(site_name)s!\n" "\n" -"You're receiving this e-mail because user %(user_display)s has given yours " -"as an e-mail address to connect their account.\n" +"You're receiving this e-mail because user %(user_display)s has given yours as an e-mail " +"address to connect their account.\n" "\n" "To confirm this is correct, go to %(activate_url)s\n" msgstr "" "Ciao da %(site_name)s!\n" "\n" -"Ricevi questa e-mail perché utente %(user_display)s ha dato la tua come un " -"indirizzo di posta elettronica per collegare il proprio account.\n" +"Ricevi questa e-mail perché utente %(user_display)s ha dato la tua come un indirizzo di " +"posta elettronica per collegare il proprio account.\n" "\n" "Per confermare che questo è corretto, andare al s %(activate_url)s\n" @@ -4995,8 +4702,7 @@ msgstr "Sei stato invitato a registrarti all'indirizzo %(site_name)s." msgid "Create an account on %(site_name)s" msgstr "Creare un account su %(site_name)s" -msgid "" -"This is the email notification to confirm your password has been changed on" +msgid "This is the email notification to confirm your password has been changed on" msgstr "Questa è la notifica e-mail per confermare che la password è stata" msgid "Change password email notification" @@ -5006,17 +4712,17 @@ msgstr "Modifica password" msgid "" "Hello from %(site_name)s!\n" "\n" -"You're receiving this e-mail because you or someone else has requested a " -"password for your user account.\n" -"It can be safely ignored if you did not request a password reset. Click the " -"link below to reset your password." +"You're receiving this e-mail because you or someone else has requested a password for your " +"user account.\n" +"It can be safely ignored if you did not request a password reset. Click the link below to " +"reset your password." msgstr "" "Ciao da %(site_name)s!\n" "\n" -"Ricevi questa e-mail perché tu o qualcun altro ha richiesto una password per " -"l'account utente.\n" -"Può essere tranquillamente ignorato se non hai richiesto la reimpostazione " -"della password. Clicca sul link qui sotto per reimpostare la password." +"Ricevi questa e-mail perché tu o qualcun altro ha richiesto una password per l'account " +"utente.\n" +"Può essere tranquillamente ignorato se non hai richiesto la reimpostazione della password. " +"Clicca sul link qui sotto per reimpostare la password." #, python-format msgid "In case you forgot, your username is %(username)s." @@ -5039,23 +4745,22 @@ msgstr "Confermare Indirizzo e-Mail" #, python-format msgid "" -"Please confirm that %(email)s is an e-mail " -"address for user %(user_display)s." +"Please confirm that %(email)s is an e-mail address for user " +"%(user_display)s." msgstr "" -"Verificare che %(email)s sia un indirizzo " -"di posta elettronica per l'utente %(user_display)s." +"Verificare che %(email)s sia un indirizzo di posta " +"elettronica per l'utente %(user_display)s." msgid "Confirm" msgstr "Conferma" #, python-format msgid "" -"This e-mail confirmation link expired or is invalid. Please issue a new e-mail confirmation request." +"This e-mail confirmation link expired or is invalid. Please issue " +"a new e-mail confirmation request." msgstr "" -"Questo link di conferma e-mail è scaduto o non è valido. Si prega di " -"emettere una nuova richiesta di conferma e-mail." +"Questo link di conferma e-mail è scaduto o non è valido. Si prega di emettere una nuova " +"richiesta di conferma e-mail." msgid "Log in" msgstr "Entra" @@ -5066,13 +4771,12 @@ msgstr "Effettua il login con un account esistente" #, python-format msgid "" "Please sign in with one\n" -" of your existing third party accounts. Or, sign up\n" +" of your existing third party accounts. Or, sign up\n" " for a %(site_name)s account and sign in below:" msgstr "" "Si prega di accedere con uno\n" -" degli account di terze parti esistenti. In alternativa, iscriviti\n" +" degli account di terze parti esistenti. In alternativa, iscriviti\n" " per un account %(site_name)s e accedere di seguito:" msgid "or" @@ -5116,32 +4820,27 @@ msgstr "Reimposta password" msgid "Forgotten your password?" msgstr "Hai dimenticato la password?" -msgid "" -"Enter your email address below, and we'll send you an email allowing you to " -"reset it." +msgid "Enter your email address below, and we'll send you an email allowing you to reset it." msgstr "" -"Inserisci sotto il tuo indirizzo email e ti spediremo un'email per " -"permetterti di cambiare la password." +"Inserisci sotto il tuo indirizzo email e ti spediremo un'email per permetterti di cambiare " +"la password." msgid "Reset my password" msgstr "Reimposta la mia password" #, python-format msgid "" -"If you have any trouble resetting your password, contact us at %(THEME_ACCOUNT_CONTACT_EMAIL)s." +"If you have any trouble resetting your password, contact us at %(THEME_ACCOUNT_CONTACT_EMAIL)s." msgstr "" -"Se hai dei problemi nel reimpostare la password, contattaci a %(THEME_ACCOUNT_CONTACT_EMAIL)s." +"Se hai dei problemi nel reimpostare la password, contattaci a %(THEME_ACCOUNT_CONTACT_EMAIL)s." msgid "" -"We have sent you an e-mail. Please contact us if you do not receive it " -"within a few minutes." +"We have sent you an e-mail. Please contact us if you do not receive it within a few minutes." msgstr "" -"Vi abbiamo inviato un'e-mail. Vi preghiamo di contattarci se non lo ricevete " -"entro pochi minuti." +"Vi abbiamo inviato un'e-mail. Vi preghiamo di contattarci se non lo ricevete entro pochi " +"minuti." msgid "Change Password" msgstr "Cambia Password" @@ -5151,13 +4850,12 @@ msgstr "Token non valido" #, python-format msgid "" -"The password reset link was invalid, possibly because it has already been " -"used. Please request a new password reset." +"The password reset link was invalid, possibly because it has already been used. Please " +"request a new password reset." msgstr "" -"Il collegamento per la reimpostazione della password non è valido, " -"probabilmente perché è già stato utilizzato. Si prega di richiedere una nuova reimpostazione della password." +"Il collegamento per la reimpostazione della password non è valido, probabilmente perché è " +"già stato utilizzato. Si prega di richiedere una nuova " +"reimpostazione della password." msgid "change password" msgstr "modifica password" @@ -5208,13 +4906,11 @@ msgstr "Verificare l'indirizzo di posta elettronica" msgid "" "We have sent an email to you for verification. Follow the\n" -" link provided to finalize the signup process. Please contact us if you " -"do\n" +" link provided to finalize the signup process. Please contact us if you do\n" " not receive it within a few minutes" msgstr "" "We have sent an email to you for verification. Follow the\n" -" link provided to finalize the signup process. Please contact us if you " -"do\n" +" link provided to finalize the signup process. Please contact us if you do\n" " not receive it within a few minutes" msgid "Verify Your E-mail Address" @@ -5235,17 +4931,16 @@ msgid "" "contact us if you do not receive it within a few minutes." msgstr "" "Vi abbiamo inviato un'e-mail per\n" -"verifica. Si prega di fare clic sul link all'interno di questa e-mail. per " -"favore\n" +"verifica. Si prega di fare clic sul link all'interno di questa e-mail. per favore\n" "contattarci se non lo ricevi entro pochi minuti." #, python-format msgid "" -"Note: you can still change your e-" -"mail address." +"Note: you can still change your e-mail address." msgstr "" -"Nota: è comunque possibile modificare l'indirizzo e-mail." +"Nota: è comunque possibile modificare l'indirizzo " +"e-mail." msgid "My Activity feed" msgstr "Il mio feed attività" @@ -5292,13 +4987,11 @@ msgstr "Pubblicato da" #, python-format msgid "" "\n" -" Published from %(publish_start)s to %(publish_end)s.\n" +" Published from %(publish_start)s to %(publish_end)s.\n" " " msgstr "" "\n" -" Pubblicato da %(publish_start)s per %(publish_end)s.\n" +" Pubblicato da %(publish_start)s per %(publish_end)s.\n" " " #, python-format @@ -5328,14 +5021,13 @@ msgstr "Si prega di selezionare gli avatar che si desidera eliminare." #, python-format msgid "" -"You have no avatars to delete. Please upload one now." +"You have no avatars to delete. Please upload one now." msgstr "" -"Non ci sono gli avatar da eliminare. Si prega di caricare un ora." +"Non ci sono gli avatar da eliminare. Si prega di caricare " +"un ora." msgid "Delete These" -msgstr "Cancella questi" +msgstr "Elimina questi" msgid "GeoNode Search" msgstr "Ricerca GeoNode" @@ -5361,10 +5053,8 @@ msgstr "Aggiornamento Miniatura..." msgid "Message. Do you want to proceed?" msgstr "Sei sicuro di voler procedere?" -#, fuzzy -#| msgid "Harvesting resources..." msgid "Processing Resource..." -msgstr "Inserimento risorse..." +msgstr "Processamento risorse..." msgid "Information for Developers" msgstr "Informazioni per gli sviluppatori" @@ -5375,160 +5065,141 @@ msgstr "Informazioni utili per gli sviluppatori interessati a GeoNode." #, fuzzy, python-format #| msgid "" #| "\n" -#| "

GeoNode is an open service " -#| "built on open source software. We encourage you to build new applications " -#| "using the components and resources it provides. This page is a starting " -#| "point for developers interesting in taking full advantage of GeoNode. It " -#| "also includes links to the project's source code so anyone can build and " -#| "customize their own GeoNode.

\n" +#| "

GeoNode is an open service built on open " +#| "source software. We encourage you to build new applications using the components and " +#| "resources it provides. This page is a starting point for developers interesting in taking " +#| "full advantage of GeoNode. It also includes links to the project's source code so anyone " +#| "can build and customize their own GeoNode.

\n" #| "\n" #| "

GeoNode Software

\n" #| "\n" -#| "

All the code that runs GeoNode is open source. The code is " -#| "available at http://github." -#| "com/GeoNode/geonode/. The issue tracker for the project is at http://github.com/GeoNode/" -#| "geonode/issues.

\n" +#| "

All the code that runs GeoNode is open source. The code is available at http://github.com/GeoNode/geonode/. The " +#| "issue tracker for the project is at http://github.com/GeoNode/geonode/issues.

\n" #| "\n" -#| "

GeoNode is built using several open source projects, each with its " -#| "own community. If you are interested in contributing new features to the " -#| "GeoNode, we encourage you to do so by contributing to one of the projects " -#| "on which it is built:

\n" +#| "

GeoNode is built using several open source projects, each with its own community. " +#| "If you are interested in contributing new features to the GeoNode, we encourage you to do " +#| "so by contributing to one of the projects on which it is built:

\n" #| "
    \n" -#| "
  • GeoServer - Standards " -#| "based server for geospatial information
  • \n" -#| "
  • GeoWebCache - Cache " -#| "engine for WMS Tiles
  • OpenLayers - Pure JavaScript library powering the maps of GeoExt\n" -#| "
  • pycsw - CSW, OpenSearch and " -#| "OAI-PMH metadata catalogue server
  • \n" +#| "
  • GeoServer - Standards based server for " +#| "geospatial information
  • \n" +#| "
  • GeoWebCache - Cache engine for WMS " +#| "Tiles
  • OpenLayers - Pure JavaScript library " +#| "powering the maps of GeoExt
  • \n" +#| "
  • pycsw - CSW, OpenSearch and OAI-PMH metadata " +#| "catalogue server
  • \n" #| "
\n" #| "\n" #| "

What are OGC Services?

\n" -#| "

The data in this application is served using open standards " -#| "endorsed by ISO and the Open " -#| "Geospatial Consortium; in particular, WMS (Web Map Service) is used " -#| "for accessing maps, WFS (Web Feature Service) is used for accessing " -#| "vector data, and WCS (Web Coverage Service) is used for accessing raster " -#| "data. WMC (Web Map Context Documents) is used for sharing maps. You can " -#| "use these services in your own applications using libraries such as " -#| "OpenLayers, GeoTools, and OGR (all of which are open-source software and " -#| "available at zero cost). Additionally, CSW (Catalog Service for the Web) " -#| "supports access to collections of descriptive information (metadata) " -#| "about data and services.

\n" +#| "

The data in this application is served using open standards endorsed by ISO and " +#| "the Open Geospatial Consortium; in particular, " +#| "WMS (Web Map Service) is used for accessing maps, WFS (Web Feature Service) is used for " +#| "accessing vector data, and WCS (Web Coverage Service) is used for accessing raster data. " +#| "WMC (Web Map Context Documents) is used for sharing maps. You can use these services in " +#| "your own applications using libraries such as OpenLayers, GeoTools, and OGR (all of which " +#| "are open-source software and available at zero cost). Additionally, CSW (Catalog Service " +#| "for the Web) supports access to collections of descriptive information (metadata) about " +#| "data and services.

\n" #| "\n" #| "

What is GeoWebCache?

\n" -#| "

GeoWebCache provides mapping tiles that are compatible with a " -#| "number of mapping engines, including Google Maps, Bing Maps and " -#| "OpenLayers. All the data hosted by GeoNode is also available through " -#| "GeoWebCache. GeoWebCache improves on WMS by caching data and providing " -#| "more responsive maps.

\n" +#| "

GeoWebCache provides mapping tiles that are compatible with a number of mapping " +#| "engines, including Google Maps, Bing Maps and OpenLayers. All the data hosted by GeoNode " +#| "is also available through GeoWebCache. GeoWebCache improves on WMS by caching data and " +#| "providing more responsive maps.

\n" #| "\n" #| "

CSW Example Code

\n" -#| "

To interact with GeoNode's CSW you can use any CSW client (QGIS " -#| "MetaSearch, GRASS, etc.). The following example illustrates a simple " -#| "invocation using the OWSLib Python package:

\n" +#| "

To interact with GeoNode's CSW you can use any CSW client (QGIS MetaSearch, GRASS, " +#| "etc.). The following example illustrates a simple invocation using the OWSLib Python " +#| "package:

\n" #| "

from owslib.csw import CatalogueServiceWeb

\n" #| "

from owslib.fes import PropertyIsLike

\n" -#| "

csw = CatalogueServiceWeb('%(CATALOGUE_BASE_URL)s')\n" -#| "

anytext = PropertyIsLike('csw:AnyText', 'birds')')\n" +#| "

csw = CatalogueServiceWeb('%(CATALOGUE_BASE_URL)s')

\n" +#| "

anytext = PropertyIsLike('csw:AnyText', 'birds')')

\n" #| "

csw.getrecords2(constraints=[anytext])

\n" #| "

print csw.results

\n" #| "

print csw.records

\n" #| "\n" #| "

OpenLayers Example Code

\n" #| "\n" -#| "

To include a GeoNode map layer in an OpenLayers map, first find " -#| "the name for that layer. This is found in the layer's name " -#| "field (not title) of the layer list. For this example, we " -#| "will use the Nicaraguan political boundaries background layer, whose name " -#| "is risk:nicaragua_admin. Then, create an instance of " -#| "OpenLayers.Layer.WMS:

\n" -#| "

var geonodeLayer = new OpenLayers.Layer.WMS(\"GeoNode Risk " -#| "Data\", \"http://demo.geonode.org/geoserver/wms\",{ layers: \"risk:" -#| "nicaragua_admin\" });

\n" +#| "

To include a GeoNode map layer in an OpenLayers map, first find the name for that " +#| "layer. This is found in the layer's name field (not title) of " +#| "the layer list. For this example, we will use the Nicaraguan political boundaries " +#| "background layer, whose name is risk:nicaragua_admin. Then, create an " +#| "instance of OpenLayers.Layer.WMS:

\n" +#| "

var geonodeLayer = new OpenLayers.Layer.WMS(\"GeoNode Risk Data\", \"http://" +#| "demo.geonode.org/geoserver/wms\",{ layers: \"risk:nicaragua_admin\" });

\n" #| "\n" #| "

Google Maps Example Code

\n" -#| "

To include a GeoNode map layer in a Google Map, include the layer " -#| "namein the URL template.

\n" -#| "

var tilelayer = new GTileLayer(null, null, null, " -#| "{tileUrlTemplate: 'http://demo.geonode.org/geoserver/gwc/service/gmaps?" -#| "layers=risk:nicaragua_admin&zoom={Z}&x={X}&y={Y}', isPng:" -#| "true, opacity:0.5 } );

\n" +#| "

To include a GeoNode map layer in a Google Map, include the layer namein the URL " +#| "template.

\n" +#| "

var tilelayer = new GTileLayer(null, null, null, {tileUrlTemplate: 'http://" +#| "demo.geonode.org/geoserver/gwc/service/gmaps?layers=risk:nicaragua_admin&zoom={Z}&" +#| "x={X}&y={Y}', isPng:true, opacity:0.5 } );

\n" #| "\n" #| "

Shapefile/GeoJSON/GML Output

\n" -#| "

To get data from the GeoNode web services use the WFS protocol. " -#| "For example, to get the full Nicaraguan admin boundaries use:

\n" -#| "

http://demo.geonode.org/geoserver/wfs?request=GetFeature&" -#| "typeName=risk:nicaragua_admin&outputformat=SHAPE-ZIP

\n" -#| "

Changing output format to json, GML2, " -#| "GML3, or csv will get data in those formats. " -#| "The WFS protocol also can handle more precise queries, specifying a " -#| "bounding box or various spatial and non-spatial filters based on the " -#| "attributes of the data.

\n" +#| "

To get data from the GeoNode web services use the WFS protocol. For example, to " +#| "get the full Nicaraguan admin boundaries use:

\n" +#| "

http://demo.geonode.org/geoserver/wfs?request=GetFeature&typeName=risk:" +#| "nicaragua_admin&outputformat=SHAPE-ZIP

\n" +#| "

Changing output format to json, GML2, GML3, " +#| "or csv will get data in those formats. The WFS protocol also can handle more " +#| "precise queries, specifying a bounding box or various spatial and non-spatial filters " +#| "based on the attributes of the data.

\n" #| "\n" #| "

GeoTools Example Code

\n" -#| "

Create a DataStore and extract a FeatureType from it, then run a " -#| "Query. It is all documented on the wiki at http://geotools.org/.

\n" +#| "

Create a DataStore and extract a FeatureType from it, then run a Query. It is all " +#| "documented on the wiki at http://geotools.org/.

\n" #| " " msgid "" "\n" -"

GeoNode is an open service built " -"on open source software. We encourage you to build new applications using " -"the components and resources it provides. This page is a starting point for " -"developers interesting in taking full advantage of GeoNode. It also includes " -"links to the project's source code so anyone can build and customize their " -"own GeoNode.

\n" +"

GeoNode is an open service built on open source " +"software. We encourage you to build new applications using the components and resources it " +"provides. This page is a starting point for developers interesting in taking full advantage " +"of GeoNode. It also includes links to the project's source code so anyone can build and " +"customize their own GeoNode.

\n" "\n" "

GeoNode Software

\n" "\n" -"

All the code that runs GeoNode is open source. The code is available " -"at http://github.com/GeoNode/" -"geonode/. The issue tracker for the project is at http://github.com/GeoNode/geonode/" -"issues.

\n" +"

All the code that runs GeoNode is open source. The code is available at http://github.com/GeoNode/geonode/. The issue " +"tracker for the project is at http://" +"github.com/GeoNode/geonode/issues.

\n" "\n" -"

GeoNode is built using several open source projects, each with its " -"own community. If you are interested in contributing new features to the " -"GeoNode, we encourage you to do so by contributing to one of the projects on " -"which it is built:

\n" +"

GeoNode is built using several open source projects, each with its own community. If " +"you are interested in contributing new features to the GeoNode, we encourage you to do so by " +"contributing to one of the projects on which it is built:

\n" "
    \n" -"
  • GeoServer - Standards based " -"server for geospatial information
  • \n" -"
  • GeoWebCache - Cache engine " -"for WMS Tiles
  • OpenLayers - " -"Pure JavaScript library powering the maps of GeoExt
  • \n" -"
  • pycsw - CSW, OpenSearch and OAI-" -"PMH metadata catalogue server
  • \n" +"
  • GeoServer - Standards based server for " +"geospatial information
  • \n" +"
  • GeoWebCache - Cache engine for WMS Tiles
  • OpenLayers - Pure JavaScript library powering " +"the maps of GeoExt
  • \n" +"
  • pycsw - CSW, OpenSearch and OAI-PMH metadata " +"catalogue server
  • \n" "
\n" "\n" "

What are OGC Services?

\n" -"

The data in this application is served using open standards endorsed " -"by ISO and the Open Geospatial " -"Consortium; in particular, WMS (Web Map Service) is used for accessing " -"maps, WFS (Web Feature Service) is used for accessing vector data, and WCS " -"(Web Coverage Service) is used for accessing raster data. WMC (Web Map " -"Context Documents) is used for sharing maps. You can use these services in " -"your own applications using libraries such as OpenLayers, GeoTools, and OGR " -"(all of which are open-source software and available at zero cost). " -"Additionally, CSW (Catalog Service for the Web) supports access to " -"collections of descriptive information (metadata) about data and services.\n" +"

The data in this application is served using open standards endorsed by ISO and the " +"Open Geospatial Consortium; in particular, WMS " +"(Web Map Service) is used for accessing maps, WFS (Web Feature Service) is used for " +"accessing vector data, and WCS (Web Coverage Service) is used for accessing raster data. " +"WMC (Web Map Context Documents) is used for sharing maps. You can use these services in your " +"own applications using libraries such as OpenLayers, GeoTools, and OGR (all of which are " +"open-source software and available at zero cost). Additionally, CSW (Catalog Service for the " +"Web) supports access to collections of descriptive information (metadata) about data and " +"services.

\n" "\n" "

What is GeoWebCache?

\n" -"

GeoWebCache provides mapping tiles that are compatible with a number " -"of mapping engines, including Google Maps, Bing Maps and OpenLayers. All the " -"data hosted by GeoNode is also available through GeoWebCache. GeoWebCache " -"improves on WMS by caching data and providing more responsive maps.

\n" +"

GeoWebCache provides mapping tiles that are compatible with a number of mapping " +"engines, including Google Maps, Bing Maps and OpenLayers. All the data hosted by GeoNode is " +"also available through GeoWebCache. GeoWebCache improves on WMS by caching data and " +"providing more responsive maps.

\n" "\n" "

CSW Example Code

\n" -"

To interact with GeoNode's CSW you can use any CSW client (QGIS " -"MetaSearch, GRASS, etc.). The following example illustrates a simple " -"invocation using the OWSLib Python package:

\n" +"

To interact with GeoNode's CSW you can use any CSW client (QGIS MetaSearch, GRASS, " +"etc.). The following example illustrates a simple invocation using the OWSLib Python " +"package:

\n" "

from owslib.csw import CatalogueServiceWeb

\n" "

from owslib.fes import PropertyIsLike

\n" "

csw = CatalogueServiceWeb('%(CATALOGUE_BASE_URL)s')

\n" @@ -5539,95 +5210,84 @@ msgid "" "\n" "

OpenLayers Example Code

\n" "\n" -"

To include a GeoNode map layer in an OpenLayers map, first find the " -"name for that layer. This is found in the layer's name field " -"(not title) of the layer list. For this example, we will use " -"the Nicaraguan political boundaries background layer, whose name is " -"risk:nicaragua_admin. Then, create an instance of OpenLayers." -"Layer.WMS:

\n" -"

var geonodeLayer = new OpenLayers.Layer.WMS(\"GeoNode Risk Data" -"\", \"http://demo.geonode.org/geoserver/wms\",{ layers: \"risk:" -"nicaragua_admin\" });

\n" +"

To include a GeoNode map layer in an OpenLayers map, first find the name for that " +"layer. This is found in the layer's name field (not title) of the " +"layer list. For this example, we will use the Nicaraguan political boundaries background " +"layer, whose name is risk:nicaragua_admin. Then, create an instance of " +"OpenLayers.Layer.WMS:

\n" +"

var geonodeLayer = new OpenLayers.Layer.WMS(\"GeoNode Risk Data\", \"http://" +"demo.geonode.org/geoserver/wms\",{ layers: \"risk:nicaragua_admin\" });

\n" "\n" "

Google Maps Example Code

\n" -"

To include a GeoNode map layer in a Google Map, include the dataset " -"name in the URL template.

\n" -"

var tilelayer = new GTileLayer(null, null, null, " -"{tileUrlTemplate: 'http://demo.geonode.org/geoserver/gwc/service/gmaps?" -"layers=risk:nicaragua_admin&zoom={Z}&x={X}&y={Y}', isPng:true, " -"opacity:0.5 } );

\n" +"

To include a GeoNode map layer in a Google Map, include the dataset name in the URL " +"template.

\n" +"

var tilelayer = new GTileLayer(null, null, null, {tileUrlTemplate: 'http://" +"demo.geonode.org/geoserver/gwc/service/gmaps?layers=risk:nicaragua_admin&zoom={Z}&" +"x={X}&y={Y}', isPng:true, opacity:0.5 } );

\n" "\n" "

Shapefile/GeoJSON/GML Output

\n" -"

To get data from the GeoNode web services use the WFS protocol. For " -"example, to get the full Nicaraguan admin boundaries use:

\n" -"

http://demo.geonode.org/geoserver/wfs?request=GetFeature&" -"typeName=risk:nicaragua_admin&outputformat=SHAPE-ZIP

\n" -"

Changing output format to json, GML2, " -"GML3, or csv will get data in those formats. The " -"WFS protocol also can handle more precise queries, specifying a bounding box " -"or various spatial and non-spatial filters based on the attributes of the " -"data.

\n" +"

To get data from the GeoNode web services use the WFS protocol. For example, to get " +"the full Nicaraguan admin boundaries use:

\n" +"

http://demo.geonode.org/geoserver/wfs?request=GetFeature&typeName=risk:" +"nicaragua_admin&outputformat=SHAPE-ZIP

\n" +"

Changing output format to json, GML2, GML3, or " +"csv will get data in those formats. The WFS protocol also can handle more " +"precise queries, specifying a bounding box or various spatial and non-spatial filters based " +"on the attributes of the data.

\n" "\n" "

GeoTools Example Code

\n" -"

Create a DataStore and extract a FeatureType from it, then run a " -"Query. It is all documented on the wiki at http://geotools.org/." -"

\n" +"

Create a DataStore and extract a FeatureType from it, then run a Query. It is all " +"documented on the wiki at http://geotools.org/.

\n" " " msgstr "" "\n" -"

GeoNode è un servizio aperto " -"costruito su software open source. Vi incoraggiamo a costruire nuove " -"applicazioni utilizzando i componenti e le risorse che fornisce. Questa " -"pagina è un punto di partenza per gli sviluppatori interessati a trarre il " -"massimo vantaggio da GeoNode. Include anche link al codice sorgente del " -"progetto in modo che chiunque possa costruire e personalizzare il proprio " -"GeoNode.

\n" +"

GeoNode è un servizio aperto costruito su " +"software open source. Vi incoraggiamo a costruire nuove applicazioni utilizzando i " +"componenti e le risorse che fornisce. Questa pagina è un punto di partenza per gli " +"sviluppatori interessati a trarre il massimo vantaggio da GeoNode. Include anche link al " +"codice sorgente del progetto in modo che chiunque possa costruire e personalizzare il " +"proprio GeoNode.

\n" "\n" "

GeoNode Software

\n" "\n" -"

Tutto il codice che esegue GeoNode è open source. Il codice è " -"disponibile all'indirizzo http://github.com/GeoNode/geonode/. Il issue tracker per il progetto " -"è disponibile all'indirizzo http://github.com/GeoNode/geonode/issues.

\n" -"

GeoNode è costruito utilizzando diversi progetti open source, ognuno " -"con la propria comunità. Se siete interessati a contribuire con nuove " -"funzionalità al GeoNode, vi invitiamo a farlo contribuendo ad uno dei " -"progetti su cui è costruito:

\n" +"

Tutto il codice che esegue GeoNode è open source. Il codice è disponibile " +"all'indirizzo http://github.com/GeoNode/" +"geonode/. Il issue tracker per il progetto è disponibile all'indirizzo http://github.com/GeoNode/geonode/issues.

\n" +"

GeoNode è costruito utilizzando diversi progetti open source, ognuno con la propria " +"comunità. Se siete interessati a contribuire con nuove funzionalità al GeoNode, vi invitiamo " +"a farlo contribuendo ad uno dei progetti su cui è costruito:

\n" "
    \n" -"
  • GeoServer - Server basato su " -"standard per informazioni geospaziali
  • \n" -"
  • GeoWebCache - Motore di " -"cache per WMS Tiles
  • OpenLayers " -"- Pura libreria JavaScript che alimenta le mappe di GeoExt
  • \n" -"
  • pycsw - CSW, OpenSearch e OAI-PMH " -"metadata catalogue server
  • \n" +"
  • GeoServer - Server basato su standard per " +"informazioni geospaziali
  • \n" +"
  • GeoWebCache - Motore di cache per WMS " +"Tiles
  • OpenLayers - Pura libreria JavaScript " +"che alimenta le mappe di GeoExt
  • \n" +"
  • pycsw - CSW, OpenSearch e OAI-PMH metadata " +"catalogue server
  • \n" "
\n" "\n" "

Che cosa sono i servizi OGC?

\n" -"

I dati in questa applicazione sono serviti utilizzando standard " -"aperti approvati dall'ISO e dal Open " -"Geospatial Consortium; in particolare, il WMS (Web Map Service) è " -"utilizzato per l'accesso alle mappe, il WFS (Web Feature Service) è " -"utilizzato per l'accesso ai dati vettoriali, e il WCS (Web Coverage Service) " -"è utilizzato per l'accesso ai dati raster. Il WMC (Web Map Context " -"Documents) viene utilizzato per la condivisione delle mappe. Questi servizi " -"possono essere utilizzati nelle proprie applicazioni utilizzando librerie " -"come OpenLayers, GeoTools e OGR (tutti software open-source e disponibili a " -"costo zero). Inoltre, CSW (Catalog Service for the Web) supporta l'accesso a " -"raccolte di informazioni descrittive (metadati) su dati e servizi.

\n" +"

I dati in questa applicazione sono serviti utilizzando standard aperti approvati " +"dall'ISO e dal Open Geospatial Consortium; in " +"particolare, il WMS (Web Map Service) è utilizzato per l'accesso alle mappe, il WFS (Web " +"Feature Service) è utilizzato per l'accesso ai dati vettoriali, e il WCS (Web Coverage " +"Service) è utilizzato per l'accesso ai dati raster. Il WMC (Web Map Context Documents) " +"viene utilizzato per la condivisione delle mappe. Questi servizi possono essere utilizzati " +"nelle proprie applicazioni utilizzando librerie come OpenLayers, GeoTools e OGR (tutti " +"software open-source e disponibili a costo zero). Inoltre, CSW (Catalog Service for the Web) " +"supporta l'accesso a raccolte di informazioni descrittive (metadati) su dati e servizi.

\n" "\n" "

Che cos'è GeoWebCache?

\n" -"

GeoWebCache fornisce tiles di mappatura compatibili con una serie di " -"motori di mappatura, tra cui Google Maps, Bing Maps e OpenLayers. Tutti i " -"dati ospitati da GeoNode sono disponibili anche attraverso GeoWebCache. " -"GeoWebCache migliora il WMS mettendo in cache i dati e fornendo mappe più " -"reattive.

\n" +"

GeoWebCache fornisce tiles di mappatura compatibili con una serie di motori di " +"mappatura, tra cui Google Maps, Bing Maps e OpenLayers. Tutti i dati ospitati da GeoNode " +"sono disponibili anche attraverso GeoWebCache. GeoWebCache migliora il WMS mettendo in cache " +"i dati e fornendo mappe più reattive.

\n" "\n" "

CSW Codice d'esempio

\n" -"

Per interagire con il CSW di GeoNode è possibile utilizzare qualsiasi " -"client CSW (QGIS MetaSearch, GRASS, ecc.). Il seguente esempio illustra una " -"semplice invocazione utilizzando il pacchetto Python di OWSLib:

\n" +"

Per interagire con il CSW di GeoNode è possibile utilizzare qualsiasi client CSW " +"(QGIS MetaSearch, GRASS, ecc.). Il seguente esempio illustra una semplice invocazione " +"utilizzando il pacchetto Python di OWSLib:

\n" "

from owslib.csw import CatalogueServiceWeb

\n" "

from owslib.fes import PropertyIsLike

\n" "

csw = CatalogueServiceWeb('%(CATALOGUE_BASE_URL)s')

\n" @@ -5638,39 +5298,33 @@ msgstr "" "\n" "

Codice d'esempio OpenLayers

\n" "\n" -"

Per includere un livello di mappa GeoNode in una mappa OpenLayers, " -"trovare prima il nome di quel livello. Questo si trova nel campo nome del livello (non titolo) della lista dei livelli. Per " -"questo esempio, useremo il livello di sfondo dei confini politici del " -"Nicaragua, il cui nome è risk:nicaragua_admin. Quindi, creare " -"un'istanza di OpenLayers.Layer.WMS:

\n" -"

var geonodeLayer = new OpenLayers.Layer.WMS(\"GeoNode Risk Data" -"\", \"http://demo.geonode.org/geoserver/wms\",{ strati: \"risk:" -"nicaragua_admin\" });

\n" +"

Per includere un livello di mappa GeoNode in una mappa OpenLayers, trovare prima il " +"nome di quel livello. Questo si trova nel campo nome del livello (non " +"titolo) della lista dei livelli. Per questo esempio, useremo il livello di " +"sfondo dei confini politici del Nicaragua, il cui nome è risk:nicaragua_admin. " +"Quindi, creare un'istanza di OpenLayers.Layer.WMS:

\n" +"

var geonodeLayer = new OpenLayers.Layer.WMS(\"GeoNode Risk Data\", \"http://" +"demo.geonode.org/geoserver/wms\",{ strati: \"risk:nicaragua_admin\" });

\n" "\n" "

Codice di esempio di Google Maps

\n" -"

Per includere un livello di mappa GeoNode in una Google Map, " -"includere il nome del livello nel modello URL.

\n" -"

var tilelayer = new GTileLayer(null, null, null, null, " -"{tileUrlTemplate: 'http://demo.geonode.org/geoserver/gwc/service/gmaps?" -"layers=risk:nicaragua_admin&zoom={Z}&x={X}&y={Y}', isPng:true, " -"opacity:0.5 }', isPng:true, opacity:0.5 }. );

\n" +"

Per includere un livello di mappa GeoNode in una Google Map, includere il nome del " +"livello nel modello URL.

\n" +"

var tilelayer = new GTileLayer(null, null, null, null, {tileUrlTemplate: " +"'http://demo.geonode.org/geoserver/gwc/service/gmaps?layers=risk:nicaragua_admin&zoom={Z}" +"&x={X}&y={Y}', isPng:true, opacity:0.5 }', isPng:true, opacity:0.5 }. );

\n" "\n" "

Formafile/GeoJSON/GML Output

\n" -"

Per ottenere dati dai servizi web GeoNode utilizzare il protocollo " -"WFS. Per esempio, per ottenere i confini amministrativi completi del " -"Nicaragua usare:

\n" -"

http://demo.geonode.org/geoserver/wfs?request=GetFeature&" -"typeName=rischio:nicaragua_admin&outputformat=SHAPE-ZIP

\n" -"

Cambiando il formato di uscita in json, GML2, GML3, o csv si ottengono dati in questi " -"formati. Il protocollo WFS può anche gestire query più precise, specificando " -"un bounding box o vari filtri spaziali e non spaziali in base agli attributi " -"dei dati.

\n" +"

Per ottenere dati dai servizi web GeoNode utilizzare il protocollo WFS. Per esempio, " +"per ottenere i confini amministrativi completi del Nicaragua usare:

\n" +"

http://demo.geonode.org/geoserver/wfs?request=GetFeature&typeName=rischio:" +"nicaragua_admin&outputformat=SHAPE-ZIP

\n" +"

Cambiando il formato di uscita in json, GML2, GML3, o csv si ottengono dati in questi formati. Il protocollo WFS può anche " +"gestire query più precise, specificando un bounding box o vari filtri spaziali e non " +"spaziali in base agli attributi dei dati.

\n" "

Codice d'esempio di GeoTools

\n" -"

Creare un DataStore ed estrarre un FeatureType da esso, quindi " -"eseguire una query. È tutto documentato sul wiki all'indirizzo http://" -"geotools.org/.

\n" +"

Creare un DataStore ed estrarre un FeatureType da esso, quindi eseguire una query. È " +"tutto documentato sul wiki all'indirizzo http://geotools.org/.

\n" " " msgid "GeoNode's Web Services" @@ -5707,86 +5361,79 @@ msgid "GeoNode Help" msgstr "Aiuto su GeoNode" msgid "" -"This page provides helpful information about how to use the GeoNode. You can " -"use the sidebar links to navigate to what you want to know." +"This page provides helpful information about how to use the GeoNode. You can use the sidebar " +"links to navigate to what you want to know." msgstr "" -"Questa pagina fornisce informazioni di aiuto sull'utilizzo di GeoNode. Puoi " -"utilizzare i collegamenti nella barra laterale per ricercare quello che ti " -"interessa." +"Questa pagina fornisce informazioni di aiuto sull'utilizzo di GeoNode. Puoi utilizzare i " +"collegamenti nella barra laterale per ricercare quello che ti interessa." msgid "" "\n" -"

The GeoNode provides access to data sets and a map editing " -"application allows users to browse existing maps and contribute their own.\n" +"

The GeoNode provides access to data sets and a map editing application allows users " +"to browse existing maps and contribute their own.

\n" "\n" "

Browsing Layers

\n" "

The Layers tab allows you to browse data " "uploaded to this GeoNode.

\n" -"

All data can be downloaded in a variety of formats, for use in other " -"applications.

\n" +"

All data can be downloaded in a variety of formats, for use in other applications.\n" "\n" "

Developer Access

\n" -"

The Developer page is the place for " -"developers to get started building applications against the GeoNode. It " -"includes instructions on using the web services, links to the source code of " -"the GeoNode, and information about the open source projects used to create " -"it.

\n" +"

The Developer page is the place for developers " +"to get started building applications against the GeoNode. It includes instructions on using " +"the web services, links to the source code of the GeoNode, and information about the open " +"source projects used to create it.

\n" "\n" "

Browsing Maps

\n" -"

The GeoNode allows users to create and share maps with one another.\n" +"

The GeoNode allows users to create and share maps with one another.

\n" "

The Maps tab is a gateway to map exploration on " -"GeoNode. From here you can search for a map or " -"create a map, which will open the Map " -"Composer.

\n" +"GeoNode. From here you can search for a map or create a map, which will open the Map Composer.

\n" "
Google Earth Mode
\n" -"

Any map viewed in the interactive map editor can be seen in 3D mode " -"with the Google Earth plugin. To switch to 3D mode select the Google Earth " -"globe logo, the rightmost button on top toolbar. If you do not have the " -"Google Earth plugin installed you will be prompted to install it.

\n" +"

Any map viewed in the interactive map editor can be seen in 3D mode with the Google " +"Earth plugin. To switch to 3D mode select the Google Earth globe logo, the rightmost button " +"on top toolbar. If you do not have the Google Earth plugin installed you will be prompted to " +"install it.

\n" "\n" "

Creating a Map

\n" "

To create a new map go to the Contributed Maps " "tab and click the create your own map link.

\n" -"

This will take you to the Map Composer with " -"a base layer loaded.

\n" -"

To add data layers from the GeoNode click on the green plus button " -"located below the layers tab on the left hand side of the screen. This will " -"open a dialog listing all the layers available on the GeoNode.

\n" -"

To add layers to your map select them and hit the Add Layers button. When finished you may hit Done to close the " -"dialog and go back to the map.

\n" +"

This will take you to the Map Composer with a base " +"layer loaded.

\n" +"

To add data layers from the GeoNode click on the green plus button located below the " +"layers tab on the left hand side of the screen. This will open a dialog listing all the " +"layers available on the GeoNode.

\n" +"

To add layers to your map select them and hit the Add Layers button. " +"When finished you may hit Done to close the dialog and go back to the map.\n" "\n" "

Reordering and Removing Layers
\n" -"

Change the display order of the layers listed in the data tab by " -"simply dragging and dropping their names. The order in the map will be " -"updated to reflect that. To turn a layer's visibility off simply uncheck it, " -"and to remove it entirely select it and hit the red minus button.

\n" +"

Change the display order of the layers listed in the data tab by simply dragging and " +"dropping their names. The order in the map will be updated to reflect that. To turn a " +"layer's visibility off simply uncheck it, and to remove it entirely select it and hit the " +"red minus button.

\n" "\n" "
Saving your map
\n" -"

Once a suitable set of layers and zoom level has been found it's time " -"to save it so others can see it. Click the Save button--the left most icon " -"on the top toolbar, an image of a map with a disk--on the top menu and fill " -"out the title and abstract of the map.

\n" +"

Once a suitable set of layers and zoom level has been found it's time to save it so " +"others can see it. Click the Save button--the left most icon on the top toolbar, an image " +"of a map with a disk--on the top menu and fill out the title and abstract of the map.

\n" "\n" "

Publishing a Map

\n" -"

Any map from the GeoNode can be embedded for use in another site or " -"blog. To publish a map:

\n" +"

Any map from the GeoNode can be embedded for use in another site or blog. To publish " +"a map:

\n" "
    \n" -"
  1. Select the from the list of maps on the community or map page and " -"then hit the 'Publish map' button.
  2. \n" -"
  3. Choose your desired height and width for the widget in the wizard." -"
  4. \n" -"
  5. Copy the HTML snippet provided in the wizard to any HTML page or " -"iFrame-supporting blog post.
  6. \n" +"
  7. Select the from the list of maps on the community or map page and then hit the " +"'Publish map' button.
  8. \n" +"
  9. Choose your desired height and width for the widget in the wizard.
  10. \n" +"
  11. Copy the HTML snippet provided in the wizard to any HTML page or iFrame-supporting " +"blog post.
  12. \n" "
\n" -"

This will put an interactive widget showing you map in your web page " -"or blog post.

\n" -"

Note that the Map Composer also has a " -"button to publish the map. Just be sure to save the map before publishing if " -"there are changes that you want others to see. It publishes the last saved " -"version, not the last viewed version.

\n" +"

This will put an interactive widget showing you map in your web page or blog post.\n" +"

Note that the Map Composer also has a button to " +"publish the map. Just be sure to save the map before publishing if there are changes that " +"you want others to see. It publishes the last saved version, not the last viewed version.\n" "\n" "

Remixing a Map

\n" "

Any map available can serve as a starting point for a new map.

\n" @@ -5794,86 +5441,80 @@ msgid "" "
  • Open the map in the Map Composer.
  • \n" "
  • Add and remove layers as you like.
  • \n" "
  • Pan and zoom to highlight the area of interest.
  • \n" -"
  • IMPORTANT: Update the title, abstract, contact " -"info and tags to reflect the new map.
  • \n" +"
  • IMPORTANT: Update the title, abstract, contact info and tags to " +"reflect the new map.
  • \n" "
  • Save the map.
  • \n" " \n" -"

    You will be able to see your new map when you search for it from the " -"Maps tab.

    \n" +"

    You will be able to see your new map when you search for it from the Maps tab.

    \n" " " msgstr "" "\n" -"

    The GeoNode provides access to data sets and a map editing " -"application allows users to browse existing maps and contribute their own.\n" +"

    The GeoNode provides access to data sets and a map editing application allows users " +"to browse existing maps and contribute their own.

    \n" "\n" "

    Browsing Layers

    \n" "

    The Layers tab allows you to browse data " "uploaded to this GeoNode.

    \n" -"

    All data can be downloaded in a variety of formats, for use in other " -"applications.

    \n" +"

    All data can be downloaded in a variety of formats, for use in other applications.\n" "\n" "

    Developer Access

    \n" -"

    The Developer page is the place for " -"developers to get started building applications against the GeoNode. It " -"includes instructions on using the web services, links to the source code of " -"the GeoNode, and information about the open source projects used to create " -"it.

    \n" +"

    The Developer page is the place for developers " +"to get started building applications against the GeoNode. It includes instructions on using " +"the web services, links to the source code of the GeoNode, and information about the open " +"source projects used to create it.

    \n" "\n" "

    Browsing Maps

    \n" -"

    The GeoNode allows users to create and share maps with one another.\n" +"

    The GeoNode allows users to create and share maps with one another.

    \n" "

    The Maps tab is a gateway to map exploration on " -"GeoNode. From here you can search for a map or " -"create a map, which will open the Map " -"Composer.

    \n" +"GeoNode. From here you can search for a map or create a map, which will open the Map Composer.

    \n" "
    Google Earth Mode
    \n" -"

    Any map viewed in the interactive map editor can be seen in 3D mode " -"with the Google Earth plugin. To switch to 3D mode select the Google Earth " -"globe logo, the rightmost button on top toolbar. If you do not have the " -"Google Earth plugin installed you will be prompted to install it.

    \n" +"

    Any map viewed in the interactive map editor can be seen in 3D mode with the Google " +"Earth plugin. To switch to 3D mode select the Google Earth globe logo, the rightmost button " +"on top toolbar. If you do not have the Google Earth plugin installed you will be prompted to " +"install it.

    \n" "\n" "

    Creating a Map

    \n" "

    To create a new map go to the Contributed Maps " "tab and click the create your own map link.

    \n" -"

    This will take you to the Map Composer with " -"a base layer loaded.

    \n" -"

    To add data layers from the GeoNode click on the green plus button " -"located below the layers tab on the left hand side of the screen. This will " -"open a dialog listing all the layers available on the GeoNode.

    \n" -"

    To add layers to your map select them and hit the Add Layers button. When finished you may hit Done to close the " -"dialog and go back to the map.

    \n" +"

    This will take you to the Map Composer with a base " +"layer loaded.

    \n" +"

    To add data layers from the GeoNode click on the green plus button located below the " +"layers tab on the left hand side of the screen. This will open a dialog listing all the " +"layers available on the GeoNode.

    \n" +"

    To add layers to your map select them and hit the Add Layers button. " +"When finished you may hit Done to close the dialog and go back to the map.\n" "\n" "

    Reordering and Removing Layers
    \n" -"

    Change the display order of the layers listed in the data tab by " -"simply dragging and dropping their names. The order in the map will be " -"updated to reflect that. To turn a layer's visibility off simply uncheck it, " -"and to remove it entirely select it and hit the red minus button.

    \n" +"

    Change the display order of the layers listed in the data tab by simply dragging and " +"dropping their names. The order in the map will be updated to reflect that. To turn a " +"layer's visibility off simply uncheck it, and to remove it entirely select it and hit the " +"red minus button.

    \n" "\n" "
    Saving your map
    \n" -"

    Once a suitable set of layers and zoom level has been found it's time " -"to save it so others can see it. Click the Save button--the left most icon " -"on the top toolbar, an image of a map with a disk--on the top menu and fill " -"out the title and abstract of the map.

    \n" +"

    Once a suitable set of layers and zoom level has been found it's time to save it so " +"others can see it. Click the Save button--the left most icon on the top toolbar, an image " +"of a map with a disk--on the top menu and fill out the title and abstract of the map.

    \n" "\n" "

    Publishing a Map

    \n" -"

    Any map from the GeoNode can be embedded for use in another site or " -"blog. To publish a map:

    \n" +"

    Any map from the GeoNode can be embedded for use in another site or blog. To publish " +"a map:

    \n" "
      \n" -"
    1. Select the from the list of maps on the community or map page and " -"then hit the 'Publish map' button.
    2. \n" -"
    3. Choose your desired height and width for the widget in the wizard." -"
    4. \n" -"
    5. Copy the HTML snippet provided in the wizard to any HTML page or " -"iFrame-supporting blog post.
    6. \n" +"
    7. Select the from the list of maps on the community or map page and then hit the " +"'Publish map' button.
    8. \n" +"
    9. Choose your desired height and width for the widget in the wizard.
    10. \n" +"
    11. Copy the HTML snippet provided in the wizard to any HTML page or iFrame-supporting " +"blog post.
    12. \n" "
    \n" -"

    This will put an interactive widget showing you map in your web page " -"or blog post.

    \n" -"

    Note that the Map Composer also has a " -"button to publish the map. Just be sure to save the map before publishing if " -"there are changes that you want others to see. It publishes the last saved " -"version, not the last viewed version.

    \n" +"

    This will put an interactive widget showing you map in your web page or blog post.\n" +"

    Note that the Map Composer also has a button to " +"publish the map. Just be sure to save the map before publishing if there are changes that " +"you want others to see. It publishes the last saved version, not the last viewed version.\n" "\n" "

    Remixing a Map

    \n" "

    Any map available can serve as a starting point for a new map.

    \n" @@ -5881,12 +5522,12 @@ msgstr "" "
  • Open the map in the Map Composer.
  • \n" "
  • Add and remove layers as you like.
  • \n" "
  • Pan and zoom to highlight the area of interest.
  • \n" -"
  • IMPORTANT: Update the title, abstract, contact " -"info and tags to reflect the new map.
  • \n" +"
  • IMPORTANT: Update the title, abstract, contact info and tags to " +"reflect the new map.
  • \n" "
  • Save the map.
  • \n" " \n" -"

    You will be able to see your new map when you search for it from the " -"Maps tab.

    \n" +"

    You will be able to see your new map when you search for it from the Maps tab.

    \n" " " msgid "Sections" @@ -5923,30 +5564,25 @@ msgid "Advanced Search" msgstr "Ricerca Avanzata" msgid "" -"Click to search for geospatial data published by other users, organizations " -"and public sources. Download data in standard formats." +"Click to search for geospatial data published by other users, organizations and public " +"sources. Download data in standard formats." msgstr "" -"Clicca per la ricerca di dati geospaziali pubblicati da altri utenti, " -"organizzazioni e fonti pubbliche. Scaricare i dati in formati standard." +"Clicca per la ricerca di dati geospaziali pubblicati da altri utenti, organizzazioni e fonti " +"pubbliche. Scaricare i dati in formati standard." -#, fuzzy -#| msgid "dataset" msgid "Add datasets" -msgstr "dataset" +msgstr "Aggiungi dataset" -#, fuzzy -#| msgid "Explore Layers" msgid "Explore datasetss" -msgstr "Esplora livelli" +msgstr "Esplora i dataset" msgid "" -"Data is available for browsing, aggregating and styling to generate maps " -"which can be saved, downloaded, shared publicly or restricted to specify " -"users only." +"Data is available for browsing, aggregating and styling to generate maps which can be saved, " +"downloaded, shared publicly or restricted to specify users only." msgstr "" -"I dati sono disponibili per la navigazione, l'aggregazione e lo styling per " -"generare mappe che possono essere salvate, scaricate, condivise " -"pubblicamente o limitate per specificare solo gli utenti." +"I dati sono disponibili per la navigazione, l'aggregazione e lo styling per generare mappe " +"che possono essere salvate, scaricate, condivise pubblicamente o limitate per specificare " +"solo gli utenti." msgid "Create maps" msgstr "Creare mappe" @@ -5955,11 +5591,11 @@ msgid "Explore maps" msgstr "Esplora mappe" msgid "" -"As for the layers and maps GeoNode allows to publish tabular and text data, " -"manage their metadata and associated documents." +"As for the layers and maps GeoNode allows to publish tabular and text data, manage their " +"metadata and associated documents." msgstr "" -"Per quanto riguarda i livelli e le mappe, GeoNode consente di pubblicare " -"dati tabulari e di testo, gestirne i metadati e i documenti associati." +"Per quanto riguarda i livelli e le mappe, GeoNode consente di pubblicare dati tabulari e di " +"testo, gestirne i metadati e i documenti associati." msgid "Add documents" msgstr "Aggiungi documenti" @@ -5968,11 +5604,11 @@ msgid "Explore documents" msgstr "Esplora documenti" msgid "" -"Geonode allows registered users to easily upload geospatial data and various " -"documents in several formats." +"Geonode allows registered users to easily upload geospatial data and various documents in " +"several formats." msgstr "" -"GeoNode permette agli utenti registrati di caricare facilmente i dati " -"geospaziali e vari documenti in diversi formati." +"GeoNode permette agli utenti registrati di caricare facilmente i dati geospaziali e vari " +"documenti in diversi formati." msgid "See users" msgstr "Vedi utenti" @@ -5997,19 +5633,13 @@ msgid "is inviting you to join (%(site_name)s)." msgstr "ti sta invitando a registrati su (%(site_name)s)." #, python-format -msgid "" -"To do so, please register at %(site_name)s " -"Registration." +msgid "To do so, please register at %(site_name)s Registration." msgstr "" -"A tale scopo, registrarsi all'indirizzo " -"%(site_name)s Registrazione." +"A tale scopo, registrarsi all'indirizzo %(site_name)s " +"Registrazione." -msgid "" -"Once you receive the confirmation that your account is activated, you can " -"notify" -msgstr "" -"Una volta ricevuta la conferma dell'attivazione dell'account, è possibile " -"notificare" +msgid "Once you receive the confirmation that your account is activated, you can notify" +msgstr "Una volta ricevuta la conferma dell'attivazione dell'account, è possibile notificare" msgid "that you wish to join her/his group(s) through" msgstr "che si desidera unirsi al suo gruppo/i attraverso" @@ -6018,8 +5648,7 @@ msgid "this link" msgstr "questo link" #, python-format -msgid "" -"%(inviter_name)s is a member of the following group(s):" +msgid "%(inviter_name)s is a member of the following group(s):" msgstr "%(inviter_name)s è un membro dei seguenti gruppi:" msgid "We look forward to seeing you on the platform," @@ -6136,11 +5765,9 @@ msgstr "Il tuo account è ora attivo" msgid "has requested access to the site." msgstr "ha richiesto l'accesso a questo sito." -msgid "" -"You can enable access by setting the user as active on the admin section" +msgid "You can enable access by setting the user as active on the admin section" msgstr "" -"Puoi abilitare l'accesso all'utente impostandolo come attivo nella sezione " -"di Amministrazione" +"Puoi abilitare l'accesso all'utente impostandolo come attivo nella sezione di Amministrazione" msgid "A user has requested access to the site" msgstr "Un utente ha richiesto l'accesso al sito" @@ -6176,10 +5803,10 @@ msgid "A document has been uploaded" msgstr "Un documento è stato caricato" msgid "The following document was deleted" -msgstr "Il seguente documento è stato cancellato" +msgstr "Il seguente documento è stato eliminato" msgid "A document has been deleted" -msgstr "Un documento è stato cancellato" +msgstr "Un documento è stato eliminato" msgid "The following document was published" msgstr "Il seguente documento è stato pubblicato" @@ -6314,17 +5941,16 @@ msgid "" "\n" "

    \n" " Note:\n" -" You do not have a verified email address to which notices can be " -"sent. Add one now.\n" +" You do not have a verified email address to which notices can be sent. Add one now.\n" "

    \n" " " msgstr "" "\n" "

    \n" " Nota:\n" -" Non si dispone di un indirizzo e-mail verificato a cui gli " -"avvisi possono essere inviati. Aggiungerne uno " -"now.\n" +" Non si dispone di un indirizzo e-mail verificato a cui gli avvisi possono essere " +"inviati. Aggiungerne uno now.\n" "

    \n" " " @@ -6334,11 +5960,9 @@ msgstr "Tipo di notifica" msgid "requested you to download this resource" msgstr "ti ha richiesto di scaricare questa risorsa" -msgid "" -"Please go to resource page and assign the download permissions if you wish" +msgid "Please go to resource page and assign the download permissions if you wish" msgstr "" -"Per favore vai alla pagina delle risorse ed assegna i permessi di " -"scaricamento se desideri" +"Per favore vai alla pagina delle risorse ed assegna i permessi di scaricamento se desideri" msgid "A resource's download has been requested" msgstr "Richiesto lo scaricamento di una risorsa" @@ -6419,16 +6043,13 @@ msgstr "Selezione" msgid "No list items selected. Use the selection fields to add." msgstr "" -"Nessun elemento selezionato. Usare i pulsanti di selezione per aggiungere " -"gli elementi." +"Nessun elemento selezionato. Usare i pulsanti di selezione per aggiungere gli elementi." msgid "Filters" msgstr "Filtri" -#, fuzzy -#| msgid "Layers found" msgid "Datasets found" -msgstr "Livelli trovati" +msgstr "Dataset trovati" msgid "Maps found" msgstr "Mappe trovate" @@ -6466,10 +6087,8 @@ msgstr "Più popolari" msgid "Text" msgstr "Testo" -#, fuzzy -#| msgid "Share This" msgid "Share This Dataset" -msgstr "Condividi" +msgstr "Condividi questo dataset" msgid "Share This" msgstr "Condividi" @@ -6477,11 +6096,10 @@ msgstr "Condividi" msgid "Social Network Login Failure" msgstr "Errore di accesso al social network" -msgid "" -"An error occurred while attempting to login via your social network account." +msgid "An error occurred while attempting to login via your social network account." msgstr "" -"Si è verificato un errore durante il tentativo di accesso tramite l'account " -"del social network." +"Si è verificato un errore durante il tentativo di accesso tramite l'account del social " +"network." msgid "Code" msgstr "Codice" @@ -6493,16 +6111,14 @@ msgid "Account Connections" msgstr "Connessioni account" msgid "" -"You can sign in to your account using any of the following already connected " -"third party accounts:" +"You can sign in to your account using any of the following already connected third party " +"accounts:" msgstr "" -"Puoi accedere al tuo account utilizzando uno dei seguenti account di terze " -"parti già connessi:" +"Puoi accedere al tuo account utilizzando uno dei seguenti account di terze parti già " +"connessi:" -msgid "" -"You currently have no social network accounts connected to this account." -msgstr "" -"Al momento non hai account di social network connessi a questo account." +msgid "You currently have no social network accounts connected to this account." +msgstr "Al momento non hai account di social network connessi a questo account." msgid "Add a 3rd Party Account" msgstr "Aggiungere un account di terze parti" @@ -6512,13 +6128,12 @@ msgstr "Accesso annullato" #, python-format msgid "" -"You decided to cancel logging in to our site using one of your existing " -"accounts. If this was a mistake, please proceed to sign in." +"You decided to cancel logging in to our site using one of your existing accounts. If this " +"was a mistake, please proceed to sign in." msgstr "" -"Hai deciso di annullare l'accesso al nostro sito utilizzando uno dei tuoi " -"account esistenti. Se questo è stato un errore, si prega di procedere per accedere." +"Hai deciso di annullare l'accesso al nostro sito utilizzando uno dei tuoi account esistenti. " +"Se questo è stato un errore, si prega di procedere per accedere." msgid "Signup" msgstr "Registrati" @@ -6598,9 +6213,7 @@ msgid "Check this if the jumbotron background image already contains text" msgstr "Verificare se l'immagine di sfondo jumbotron contiene già testo" msgid "Disabling this slide will hide it from the slide show" -msgstr "" -"Se si disattiva questa diapositiva, questa verrà nascondeta dalla " -"presentazione" +msgstr "Se si disattiva questa diapositiva, questa verrà nascondeta dalla presentazione" msgid "Choose between using jumbotron background and slide show" msgstr "Scegliere tra l'utilizzo dello sfondo jumbotron e della presentazione" @@ -6611,19 +6224,11 @@ msgstr "Contenuto" msgid "Could not access to uploaded data." msgstr "Impossibile accedere ai dati caricati." -msgid "" -"One or more XML files was provided, but no matching files were found for " -"them." -msgstr "" -"È stato fornito uno o più file XML, ma non sono stati trovati file " -"corrispondenti." +msgid "One or more XML files was provided, but no matching files were found for them." +msgstr "È stato fornito uno o più file XML, ma non sono stati trovati file corrispondenti." -msgid "" -"One or more SLD files was provided, but no matching files were found for " -"them." -msgstr "" -"È stato fornito uno o più file SLD, ma non sono stati trovati file " -"corrispondenti." +msgid "One or more SLD files was provided, but no matching files were found for them." +msgstr "È stato fornito uno o più file SLD, ma non sono stati trovati file corrispondenti." msgid "Layer already exists" msgstr "Il livello esiste già" @@ -6644,8 +6249,7 @@ msgstr "Previsto per trovare il livello denominato '{name}' nel geoserver" msgid "Exception occurred while parsing the provided Metadata file." msgstr "Eccezione durante l'analisi del file di metadati fornito." -msgid "" -"The UUID identifier from the XML Metadata is already in use in this system." +msgid "The UUID identifier from the XML Metadata is already in use in this system." msgstr "L'identificatore UUID dei metadati XML è già in uso in questo sistema." msgid "Error configuring Layer" @@ -6655,9 +6259,7 @@ msgid "Invalid zip file detected" msgstr "Rilevato file zip non valido" msgid "Could not find any valid spatial file inside the uploaded zip" -msgstr "" -"Impossibile trovare alcun file spaziale valido all'interno del file zip " -"caricato" +msgstr "Impossibile trovare alcun file spaziale valido all'interno del file zip caricato" msgid "Invalid kmz file detected" msgstr "Rilevato file kmz non valido" @@ -6680,12 +6282,8 @@ msgstr "È consentito un solo file kml per file zip" msgid "Only one kml file per kmz is allowed" msgstr "È consentito un solo file kml per kmz" -msgid "" -"You are trying to upload multiple GeoTIFFs without a valid 'indexer." -"properties' file." -msgstr "" -"Si sta tentando di caricare più GeoTIFF senza un file 'indexer.properties' " -"valido." +msgid "You are trying to upload multiple GeoTIFFs without a valid 'indexer.properties' file." +msgstr "Si sta tentando di caricare più GeoTIFF senza un file 'indexer.properties' valido." msgid "Only one raster file per ZIP is allowed" msgstr "È consentito un solo file raster per file zip" @@ -6693,11 +6291,9 @@ msgstr "È consentito un solo file raster per file zip" msgid "No multiple rasters allowed" msgstr "Non sono consentiti più raster" -msgid "" -"To support the time step, you must enable the OGC_SERVER DATASTORE option" +msgid "To support the time step, you must enable the OGC_SERVER DATASTORE option" msgstr "" -"Per supportare il passaggio temporale, è necessario attivare l'opzione " -"OGC_SERVER DATASTORE" +"Per supportare il passaggio temporale, è necessario attivare l'opzione OGC_SERVER DATASTORE" #, python-brace-format msgid "Unsupported file type: {e.message}" @@ -6757,9 +6353,7 @@ msgstr "Permesso negato" #~ msgstr "Carica livello" #~ msgid "You are attempting to replace a vector layer with an unknown format." -#~ msgstr "" -#~ "Stai tentando di sostituire un livello vettoriale con un formato " -#~ "sconosiuto." +#~ msgstr "Stai tentando di sostituire un livello vettoriale con un formato sconosiuto." #~ msgid "Failed to upload the layer" #~ msgstr "Impossibile caricare il livello" diff --git a/geonode/maps/admin.py b/geonode/maps/admin.py index 6be72dd9982..da6f8bf74a1 100644 --- a/geonode/maps/admin.py +++ b/geonode/maps/admin.py @@ -44,6 +44,7 @@ class MapAdmin(TabbedTranslationAdmin): inlines = [ MapLayerInline, ] + exclude = ("ll_bbox_polygon", "bbox_polygon", "srid") list_display_links = ("title",) list_display = ( "id", @@ -78,6 +79,7 @@ class MapAdmin(TabbedTranslationAdmin): "is_approved", "is_published", ) + readonly_fields = ("geographic_bounding_box",) form = MapAdminForm actions = [metadata_batch_edit] diff --git a/geonode/maps/api/serializers.py b/geonode/maps/api/serializers.py index 2d9c88330e3..382bdd20d0b 100644 --- a/geonode/maps/api/serializers.py +++ b/geonode/maps/api/serializers.py @@ -138,6 +138,9 @@ class Meta: "current_style", "dataset", "name", + "order", + "visibility", + "opacity", ) @@ -145,12 +148,7 @@ class SimpleMapLayerSerializer(serializers.ModelSerializer): class Meta: model = MapLayer name = "maplayer" - fields = ( - "pk", - "name", - "extra_params", - "current_style", - ) + fields = ("pk", "name", "extra_params", "current_style", "order", "visibility", "opacity") class MapSerializer(ResourceBaseSerializer): diff --git a/geonode/maps/api/tests.py b/geonode/maps/api/tests.py index d5a15323a43..8d21f79bc2b 100644 --- a/geonode/maps/api/tests.py +++ b/geonode/maps/api/tests.py @@ -111,6 +111,10 @@ def test_maps(self): self.assertTrue(len(response.data["map"]["data"]["map"]["layers"]) == 7) self.assertEqual(response.data["map"]["maplayers"][0]["extra_params"], {"foo": "bar"}) self.assertIsNotNone(response.data["map"]["maplayers"][0]["dataset"]) + self.assertEqual(response.data["map"]["maplayers"][0]["extra_params"], {"foo": "bar"}) + self.assertEqual(response.data["map"]["maplayers"][0]["visibility"], 1) + self.assertEqual(response.data["map"]["maplayers"][0]["order"], 0) + self.assertEqual(response.data["map"]["maplayers"][0]["opacity"], 1.0) def test_extra_metadata_included_with_param(self): resource = Map.objects.first() @@ -150,6 +154,36 @@ def test_patch_map(self): self.assertEqual(response_maplayer["current_style"], "some-style-first-layer") self.assertIsNotNone(response_maplayer["dataset"]) + def test_patch_map_with_extra_maplayer_info(self): + """ + Patch to maps// + """ + # Get Layers List (backgrounds) + resource = Map.objects.first() + url = reverse("maps-detail", kwargs={"pk": resource.pk}) + + data = { + "title": f"{resource.title}-edited", + "abstract": resource.abstract, + "data": DUMMY_MAPDATA, + "id": resource.id, + "maplayers": DUMMY_MAPLAYERS_DATA_WITH_EXTRA_INFO, + } + self.client.login(username="admin", password="admin") + response = self.client.patch(f"{url}?include[]=data", data=data, format="json") + + self.assertEqual(response.status_code, 200) + self.assertTrue(len(response.data) > 0) + self.assertTrue("data" in response.data["map"]) + self.assertTrue(len(response.data["map"]["data"]["map"]["layers"]) == 7) + response_maplayer = response.data["map"]["maplayers"][0] + self.assertEqual(response_maplayer["extra_params"], {"msId": "Stamen.Watercolor__0"}) + self.assertEqual(response_maplayer["current_style"], "some-style-first-layer") + self.assertEqual(response_maplayer["visibility"], False) + self.assertEqual(response_maplayer["order"], 99) + self.assertEqual(response_maplayer["opacity"], 1.3) + self.assertIsNotNone(response_maplayer["dataset"]) + @patch("geonode.maps.api.views.resolve_object") def test_patch_map_raise_exception(self, mocked_obj): """ @@ -204,6 +238,34 @@ def test_create_map(self): self.assertIsNotNone(response_maplayer["dataset"]) self.assertIsNotNone(response.data["map"]["thumbnail_url"]) + def test_create_map_with_extra_maplayer_info(self): + """ + Post to maps/ + """ + # Get Layers List (backgrounds) + url = reverse("maps-list") + + data = { + "title": "Some created map", + "data": DUMMY_MAPDATA, + "maplayers": DUMMY_MAPLAYERS_DATA_WITH_EXTRA_INFO, + } + self.client.login(username="admin", password="admin") + response = self.client.post(f"{url}?include[]=data", data=data, format="json") + + self.assertEqual(response.status_code, 201) + self.assertTrue(len(response.data) > 0) + self.assertTrue("data" in response.data["map"]) + self.assertTrue(len(response.data["map"]["data"]["map"]["layers"]) == 7) + response_maplayer = response.data["map"]["maplayers"][0] + self.assertEqual(response_maplayer["extra_params"], {"msId": "Stamen.Watercolor__0"}) + self.assertEqual(response_maplayer["current_style"], "some-style-first-layer") + self.assertIsNotNone(response_maplayer["dataset"]) + self.assertEqual(response_maplayer["visibility"], False) + self.assertEqual(response_maplayer["order"], 99) + self.assertEqual(response_maplayer["opacity"], 1.3) + self.assertIsNotNone(response.data["map"]["thumbnail_url"]) + DUMMY_MAPDATA = { "map": { @@ -365,3 +427,14 @@ def test_create_map(self): "name": "geonode:CA", } ] + +DUMMY_MAPLAYERS_DATA_WITH_EXTRA_INFO = [ + { + "extra_params": {"msId": "Stamen.Watercolor__0"}, + "current_style": "some-style-first-layer", + "name": "geonode:CA", + "opacity": 1.3, + "visibility": False, + "order": 99, + } +] diff --git a/geonode/maps/migrations/0043_auto_20230807_1234.py b/geonode/maps/migrations/0043_auto_20230807_1234.py new file mode 100644 index 00000000000..894c1e4ea5c --- /dev/null +++ b/geonode/maps/migrations/0043_auto_20230807_1234.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.19 on 2023-08-07 12:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('maps', '0042_remove_maplayer_styles'), + ] + + operations = [ + migrations.AddField( + model_name='maplayer', + name='opacity', + field=models.FloatField(default=1.0), + ), + migrations.AddField( + model_name='maplayer', + name='order', + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name='maplayer', + name='visibility', + field=models.BooleanField(default=True), + ), + ] diff --git a/geonode/maps/models.py b/geonode/maps/models.py index 650417aebd8..90b18bc98ab 100644 --- a/geonode/maps/models.py +++ b/geonode/maps/models.py @@ -262,6 +262,11 @@ class MapLayer(models.Model): local = models.BooleanField(default=False, blank=True) # True if this layer is served by the local geoserver + # Extend MapLayer model with visualization properties #11251 + order = models.IntegerField(default=0) + visibility = models.BooleanField(default=True) + opacity = models.FloatField(default=1.0) + @property def dataset_title(self): """ diff --git a/geonode/messaging/notifications.py b/geonode/messaging/notifications.py index 763cf05371e..00d3135c1a1 100644 --- a/geonode/messaging/notifications.py +++ b/geonode/messaging/notifications.py @@ -38,13 +38,14 @@ def message_received_notification(**kwargs): recipients = _get_user_to_notify(message) # Enable email notifications for reciepients - for user in recipients: - notifications.models.NoticeSetting.objects.get_or_create( - notice_type=notifications.models.NoticeType.objects.get(label=notice_type_label), - send=True, - user=user, - medium=0, - ) + if notifications: + for user in recipients: + notifications.models.NoticeSetting.objects.get_or_create( + notice_type=notifications.models.NoticeType.objects.get(label=notice_type_label), + send=True, + user=user, + medium=0, + ) ctx = { "message": message.content, diff --git a/geonode/monitoring/frontend/yarn.lock b/geonode/monitoring/frontend/yarn.lock index d8f15f3051a..d0fab64321f 100644 --- a/geonode/monitoring/frontend/yarn.lock +++ b/geonode/monitoring/frontend/yarn.lock @@ -2337,11 +2337,16 @@ core-js@^1.0.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" integrity sha512-ZiPp9pZlgxpWRu0M+YWbm6+aQ84XEfH1JRXvfOc/fILWI0VKhLC2LX13X1NYq4fULzLMq7Hfh43CSo2/aIaUPA== -core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.1: +core-js@^2.4.0, core-js@^2.5.0: version "2.6.12" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== +core-js@^3.4.2: + version "3.31.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.31.1.tgz#f2b0eea9be9da0def2c5fece71064a7e5d687653" + integrity sha512-2sKLtfq1eFST7l7v62zaqXacPc7uG8ZAya8ogijLhTtaKNcpzpB4TMoTw2Si+8GYKRwFPMMtUT0263QFWFfqyQ== + core-util-is@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -6574,7 +6579,7 @@ react-simple-maps@0.12.1: d3-geo-projection "1.2.2" topojson-client "2.1.0" -react-smooth@^1.0.0: +react-smooth@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-1.0.6.tgz#18b964f123f7bca099e078324338cd8739346d0a" integrity sha512-B2vL4trGpNSMSOzFiAul9kFAsxTukL9Wyy9EXtkQy3GJr6sZqW9e1nShdVOJ3hRYamPZ94O17r3Q0bjSw3UYtg== @@ -6706,20 +6711,20 @@ recharts-scale@^0.4.2: dependencies: decimal.js-light "^2.4.1" -recharts@1.6.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/recharts/-/recharts-1.6.2.tgz#4ced884f04b680e8dac5d3e109f99b0e7cfb9b0f" - integrity sha512-NqVN8Hq5wrrBthTxQB+iCnZjup1dc+AYRIB6Q9ck9UjdSJTt4PbLepGpudQEYJEN5iIpP/I2vThC4uiTJa7xUQ== +recharts@1.8.6: + version "1.8.6" + resolved "https://registry.yarnpkg.com/recharts/-/recharts-1.8.6.tgz#ba651a4610dbac936da5001f8cb556b36a170410" + integrity sha512-UlfSEOnZRAxxaH33Fc86yHEcqN+IRauPP31NfVvlGudtwVZEIb2RFI5b1J3npQo7XyoSnkUodg3Ha6EupkV+SQ== dependencies: classnames "^2.2.5" - core-js "^2.5.1" + core-js "^3.4.2" d3-interpolate "^1.3.0" d3-scale "^2.1.0" d3-shape "^1.2.0" lodash "^4.17.5" prop-types "^15.6.0" react-resize-detector "^2.3.0" - react-smooth "^1.0.0" + react-smooth "^1.0.5" recharts-scale "^0.4.2" reduce-css-calc "^1.3.0" @@ -8464,9 +8469,9 @@ wide-align@^1.1.0: string-width "^1.0.2 || 2 || 3 || 4" word-wrap@~1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + version "1.2.4" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.4.tgz#cb4b50ec9aca570abd1f52f33cd45b6c61739a9f" + integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA== wordwrap@~0.0.2: version "0.0.3" diff --git a/geonode/monitoring/models.py b/geonode/monitoring/models.py index bbef043b228..18f302c73dc 100644 --- a/geonode/monitoring/models.py +++ b/geonode/monitoring/models.py @@ -701,39 +701,6 @@ def from_geonode(cls, service, request, response): sensitive_data = cls._get_user_data_gn(request) event_type = cls._get_event_type(request) - # ua = request.META.get('HTTP_USER_AGENT') or '' - # ua_family = cls._get_ua_family(ua) - # - # ip, is_routable = get_client_ip(request) - # lat = lon = None - # country = region = city = None - # if ip and is_routable: - # ip = ip.split(':')[0] - # if settings.TEST and ip == 'testserver': - # ip = '127.0.0.1' - # try: - # ip = gethostbyname(ip) - # except Exception as err: - # pass - # - # geoip = get_geoip() - # try: - # client_loc = geoip.city(ip) - # except Exception as err: - # log.warning("Cannot resolve %s: %s", ip, err) - # client_loc = None - # - # if client_loc: - # lat, lon = client_loc['latitude'], client_loc['longitude'], - # country = client_loc.get( - # 'country_code3') or client_loc['country_code'] - # if len(country) == 2: - # _c = pycountry.countries.get(alpha_2=country) - # country = _c.alpha_3 - # - # region = client_loc['region'] - # city = client_loc['city'] - data = { "received": received, "created": created, diff --git a/geonode/people/adapters.py b/geonode/people/adapters.py index bb290e11feb..63ac7e36659 100644 --- a/geonode/people/adapters.py +++ b/geonode/people/adapters.py @@ -25,12 +25,15 @@ """ import logging +import jwt +import requests from allauth.account.adapter import DefaultAccountAdapter from allauth.account.utils import user_field from allauth.account.utils import user_email from allauth.account.utils import user_username from allauth.socialaccount.adapter import DefaultSocialAccountAdapter +from allauth.socialaccount.providers.oauth2.views import OAuth2Adapter, OAuth2Error from invitations.adapters import BaseInvitationsAdapter @@ -41,6 +44,7 @@ from django.core.exceptions import ValidationError from django.utils.module_loading import import_string +from geonode.utils import import_class_module from geonode.groups.models import GroupProfile logger = logging.getLogger(__name__) @@ -64,6 +68,18 @@ def get_data_extractor(provider_id): return extractor +def get_group_role_mapper(provider_id): + group_role_mapper_class = import_class_module( + getattr(settings, "SOCIALACCOUNT_PROVIDERS", {}) + .get(PROVIDER_ID, {}) + .get( + "GROUP_ROLE_MAPPER_CLASS", + "geonode.people.profileextractors.OpenIDGroupRoleMapper", + ) + ) + return group_role_mapper_class() + + def update_profile(sociallogin): """Update a people.models.Profile object with info from the sociallogin""" user = sociallogin.user @@ -222,3 +238,79 @@ def _site_allows_signup(django_request): def _respond_inactive_user(user): return HttpResponseRedirect(reverse("moderator_contacted", kwargs={"inactive_user": user.id})) + + +PROVIDER_ID = getattr(settings, "SOCIALACCOUNT_OIDC_PROVIDER", "geonode_openid_connect") + +ACCESS_TOKEN_URL = getattr(settings, "SOCIALACCOUNT_PROVIDERS", {}).get(PROVIDER_ID, {}).get("ACCESS_TOKEN_URL", "") + +AUTHORIZE_URL = getattr(settings, "SOCIALACCOUNT_PROVIDERS", {}).get(PROVIDER_ID, {}).get("AUTHORIZE_URL", "") + +PROFILE_URL = getattr(settings, "SOCIALACCOUNT_PROVIDERS", {}).get(PROVIDER_ID, {}).get("PROFILE_URL", "") + +ID_TOKEN_ISSUER = getattr(settings, "SOCIALACCOUNT_PROVIDERS", {}).get(PROVIDER_ID, {}).get("ID_TOKEN_ISSUER", "") + + +class GenericOpenIDConnectAdapter(OAuth2Adapter, SocialAccountAdapter): + provider_id = PROVIDER_ID + access_token_url = ACCESS_TOKEN_URL + authorize_url = AUTHORIZE_URL + profile_url = PROFILE_URL + id_token_issuer = ID_TOKEN_ISSUER + + def complete_login(self, request, app, token, response, **kwargs): + extra_data = {} + if self.profile_url: + try: + headers = {"Authorization": f"Bearer {token.token}"} + resp = requests.get(self.profile_url, headers=headers) + profile_data = resp.json() + extra_data.update(profile_data) + except Exception: + logger.exception(OAuth2Error("Invalid profile_url, falling back to id_token checks...")) + if not extra_data and "id_token" in response: + try: + extra_data = jwt.decode( + response["id_token"], + # Since the token was received by direct communication + # protected by TLS between this library and Google, we + # are allowed to skip checking the token signature + # according to the OpenID Connect Core 1.0 + # specification. + # https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation + options={ + "verify_signature": False, + "verify_iss": True, + "verify_aud": True, + "verify_exp": True, + }, + issuer=self.id_token_issuer, + audience=app.client_id, + ) + except jwt.PyJWTError as e: + raise OAuth2Error("Invalid id_token") from e + login = self.get_provider().sociallogin_from_response(request, extra_data) + return login + + def save_user(self, request, sociallogin, form=None): + user = super(SocialAccountAdapter, self).save_user(request, sociallogin, form=form) + extractor = get_data_extractor(sociallogin.account.provider) + group_role_mapper = get_group_role_mapper(sociallogin.account.provider) + try: + groups = extractor.extract_groups(sociallogin.account.extra_data) or extractor.extract_roles( + sociallogin.account.extra_data + ) + + # check here if user is member already of other groups and remove it form the ones that are not declared here... + for groupprofile in user.group_list_all(): + groupprofile.leave(user) + for group_role_name in groups: + group_name, role_name = group_role_mapper.parse_group_and_role(group_role_name) + groupprofile = GroupProfile.objects.filter(slug=group_name).first() + if groupprofile: + groupprofile.join(user) + if group_role_mapper.is_manager(role_name): + groupprofile.promote() + except (AttributeError, NotImplementedError): + pass # extractor doesn't define a method for extracting field + return user diff --git a/geonode/people/models.py b/geonode/people/models.py index 37d4c21631c..c07b6f5e81f 100644 --- a/geonode/people/models.py +++ b/geonode/people/models.py @@ -246,6 +246,7 @@ def _notify_account_activated(self): "site_name": current_site.name, "email": self.email, "inviter": self, + "LOGIN_URL": settings.LOGIN_URL, } email_template = "pinax/notifications/account_active/account_active" diff --git a/geonode/people/profileextractors.py b/geonode/people/profileextractors.py index e6411c3d267..7b9cdebefec 100644 --- a/geonode/people/profileextractors.py +++ b/geonode/people/profileextractors.py @@ -127,6 +127,9 @@ def _extract_field(self, name, data): return result +PROVIDER_ID = getattr(settings, "SOCIALACCOUNT_OIDC_PROVIDER", "geonode_openid_connect") + + class OpenIDExtractor(BaseExtractor): def extract_email(self, data): return data.get("email", "") @@ -182,16 +185,29 @@ def extract_organization(self, data): def extract_voice(self, data): return data.get("phone", "") + def extract_keywords(self, data): + return data.get("keywords", "") + def extract_groups(self, data): return data.get("groups", "") def extract_roles(self, data): return data.get("roles", "") - def extract_keywords(self, data): - return data.get("keywords", "") - def _get_latest_position(data): all_positions = data.get("positions", {"values": []})["values"] return all_positions[0] if any(all_positions) else None + + +class OpenIDGroupRoleMapper: + """GeoNode will look for names like: ["GroupProfile1.Role", "GroupProfile2.Role", ..., "GroupProfileN.Role"]""" + + def parse_group_and_role(self, group_role_name): + _group_role_name = group_role_name if "." in group_role_name else f"{group_role_name}.None" + group_name, role_name = _group_role_name.rsplit(".", 1) + return (group_name, role_name) + + def is_manager(role_name): + _role_name = role_name or "" + return "manager" in _role_name.lower() diff --git a/geonode/people/socialaccount/__init__.py b/geonode/people/socialaccount/__init__.py new file mode 100644 index 00000000000..da86ef5219a --- /dev/null +++ b/geonode/people/socialaccount/__init__.py @@ -0,0 +1,18 @@ +######################################################################### +# +# Copyright (C) 2023 OSGeo +# +# 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 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### diff --git a/geonode/people/socialaccount/providers/__init__.py b/geonode/people/socialaccount/providers/__init__.py new file mode 100644 index 00000000000..da86ef5219a --- /dev/null +++ b/geonode/people/socialaccount/providers/__init__.py @@ -0,0 +1,18 @@ +######################################################################### +# +# Copyright (C) 2023 OSGeo +# +# 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 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### diff --git a/geonode/people/socialaccount/providers/geonode_openid_connect/__init__.py b/geonode/people/socialaccount/providers/geonode_openid_connect/__init__.py new file mode 100644 index 00000000000..da86ef5219a --- /dev/null +++ b/geonode/people/socialaccount/providers/geonode_openid_connect/__init__.py @@ -0,0 +1,18 @@ +######################################################################### +# +# Copyright (C) 2023 OSGeo +# +# 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 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### diff --git a/geonode/people/socialaccount/providers/geonode_openid_connect/apps.py b/geonode/people/socialaccount/providers/geonode_openid_connect/apps.py new file mode 100644 index 00000000000..ce9d3da1ad3 --- /dev/null +++ b/geonode/people/socialaccount/providers/geonode_openid_connect/apps.py @@ -0,0 +1,24 @@ +######################################################################### +# +# Copyright (C) 2023 OSGeo +# +# 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 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### +from django.apps import AppConfig + + +class GeoNodeOpenIdConnectAppConfig(AppConfig): + name = "geonode.people.socialaccount.providers.geonode_openid_connect" + verbose_name = "GeoNode OpenId Connect" diff --git a/geonode/people/socialaccount/providers/geonode_openid_connect/provider.py b/geonode/people/socialaccount/providers/geonode_openid_connect/provider.py new file mode 100644 index 00000000000..196be1e17a2 --- /dev/null +++ b/geonode/people/socialaccount/providers/geonode_openid_connect/provider.py @@ -0,0 +1,98 @@ +######################################################################### +# +# Copyright (C) 2023 OSGeo +# +# 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 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### + +"""Custom account providers for django-allauth. + +These are used in order to extend the default authorization provided by +django-allauth. + +""" +from django.conf import settings + +from geonode.utils import import_class_module + +from allauth.account.models import EmailAddress +from allauth.socialaccount.providers.base import AuthAction, ProviderAccount +from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider + +PROVIDER_ID = getattr(settings, "SOCIALACCOUNT_OIDC_PROVIDER", "geonode_openid_connect") + + +class GenericOpenIDConnectProviderAccount(ProviderAccount): + def to_str(self): + dflt = super(GenericOpenIDConnectProviderAccount, self).to_str() + return self.account.extra_data.get("name", dflt) + + +class GenericOpenIDConnectProvider(OAuth2Provider): + id = "geonode_openid_connect" + name = getattr(settings, "SOCIALACCOUNT_PROVIDERS", {}).get(PROVIDER_ID, {}).get("NAME", "GeoNode OpenIDConnect") + account_class = import_class_module( + getattr(settings, "SOCIALACCOUNT_PROVIDERS", {}) + .get(PROVIDER_ID, {}) + .get( + "ACCOUNT_CLASS", + "geonode.people.socialaccount.providers.geonode_openid_connect.provider.GenericOpenIDConnectProviderAccount", + ) + ) + + def get_default_scope(self): + scope = getattr(settings, "SOCIALACCOUNT_PROVIDERS", {}).get(PROVIDER_ID, {}).get("SCOPE", "") + return scope + + def get_auth_params(self, request, action): + ret = super(GenericOpenIDConnectProvider, self).get_auth_params(request, action) + if action == AuthAction.REAUTHENTICATE: + ret["prompt"] = ( + getattr(settings, "SOCIALACCOUNT_PROVIDERS", {}) + .get(PROVIDER_ID, {}) + .get("AUTH_PARAMS", {}) + .get("prompt", "") + ) + return ret + + def extract_uid(self, data): + _uid_field = getattr(settings, "SOCIALACCOUNT_PROVIDERS", {}).get(PROVIDER_ID, {}).get("UID_FIELD", None) + if _uid_field: + return data.get(_uid_field) + else: + return data.get("uid", data.get("sub", data.get("id"))) + + def extract_common_fields(self, data): + _common_fields = getattr(settings, "SOCIALACCOUNT_PROVIDERS", {}).get(PROVIDER_ID, {}).get("COMMON_FIELDS", {}) + __common_fields_data = {} + for _common_field in _common_fields: + __common_fields_data[_common_field] = data.get(_common_fields.get(_common_field), "") + return __common_fields_data + + def extract_email_addresses(self, data): + addresses = [] + email = data.get("email") + if email: + addresses.append( + EmailAddress( + email=email, + verified=data.get("email_verified", False), + primary=True, + ) + ) + return addresses + + +provider_classes = [GenericOpenIDConnectProvider] diff --git a/geonode/people/socialaccount/providers/geonode_openid_connect/tests.py b/geonode/people/socialaccount/providers/geonode_openid_connect/tests.py new file mode 100644 index 00000000000..04be234c6c3 --- /dev/null +++ b/geonode/people/socialaccount/providers/geonode_openid_connect/tests.py @@ -0,0 +1,229 @@ +######################################################################### +# +# Copyright (C) 2023 OSGeo +# +# 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 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### +from __future__ import absolute_import, unicode_literals + +import json +from datetime import datetime, timedelta +from importlib import import_module + +from django.conf import settings +from django.core import mail +from django.test.client import RequestFactory +from django.test.utils import override_settings +from django.urls import reverse +from django.contrib.auth import get_user_model + +from allauth.account import app_settings as account_settings +from allauth.account.adapter import get_adapter +from allauth.account.models import EmailAddress, EmailConfirmation +from allauth.account.signals import user_signed_up + +# from allauth.socialaccount.models import SocialAccount +from allauth.socialaccount.providers.apple.client import jwt_encode +from allauth.socialaccount.tests import OAuth2TestsMixin +from allauth.tests import TestCase + + +@override_settings( + SOCIALACCOUNT_OIDC_PROVIDER_ENABLED=True, + SOCIALACCOUNT_AUTO_SIGNUP=True, + ACCOUNT_SIGNUP_FORM_CLASS=None, + ACCOUNT_EMAIL_VERIFICATION=account_settings.EmailVerificationMethod.MANDATORY, +) +class GoogleTests(OAuth2TestsMixin, TestCase): + provider_id = "geonode_openid_connect" + + def setUp(self): + super().setUp() + self.email = "raymond.penners@example.com" + self.identity_overwrites = {} + + def get_google_id_token_payload(self): + now = datetime.utcnow() + client_id = "app123id" # Matches `setup_app` + payload = { + "iss": "https://accounts.google.com", + "azp": client_id, + "aud": client_id, + "sub": "108204268033311374519", + "hd": "example.com", + "email": self.email, + "email_verified": True, + "at_hash": "HK6E_P6Dh8Y93mRNtsDB1Q", + "name": "Raymond Penners", + "picture": "https://lh5.googleusercontent.com/photo.jpg", + "given_name": "Raymond", + "family_name": "Penners", + "locale": "en", + "iat": now, + "exp": now + timedelta(hours=1), + } + payload.update(self.identity_overwrites) + return payload + + def get_login_response_json(self, with_refresh_token=True): + data = { + "access_token": "testac", + "expires_in": 3600, + "scope": "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile openid", + "token_type": "Bearer", + "id_token": jwt_encode(self.get_google_id_token_payload(), "secret"), + } + return json.dumps(data) + + @override_settings(SOCIALACCOUNT_AUTO_SIGNUP=False) + def test_login(self): + resp = self.login(resp_mock=None) + self.assertRedirects(resp, reverse("socialaccount_signup")) + + def test_wrong_id_token_claim_values(self): + wrong_claim_values = { + "iss": "not-google", + "exp": datetime.utcnow() - timedelta(seconds=1), + "aud": "foo", + } + for key, value in wrong_claim_values.items(): + with self.subTest(key): + self.identity_overwrites = {key: value} + resp = self.login(resp_mock=None) + self.assertTemplateUsed( + resp, + "socialaccount/authentication_error.%s" % getattr(settings, "ACCOUNT_TEMPLATE_EXTENSION", "html"), + ) + + def test_username_based_on_email(self): + self.identity_overwrites = {"given_name": "明", "family_name": "小"} + self.login(resp_mock=None) + user = get_user_model().objects.get(email=self.email) + self.assertEqual(user.username, "raymond.penners") + + def test_email_verified(self): + self.identity_overwrites = {"email_verified": True} + self.login(resp_mock=None) + email_address = EmailAddress.objects.get(email=self.email, verified=True) + self.assertFalse(EmailConfirmation.objects.filter(email_address__email=self.email).exists()) + account = email_address.user.socialaccount_set.all()[0] + self.assertEqual(account.extra_data["given_name"], "Raymond") + + def test_user_signed_up_signal(self): + sent_signals = [] + + def on_signed_up(sender, request, user, **kwargs): + sociallogin = kwargs["sociallogin"] + self.assertEqual(sociallogin.account.provider, "geonode_openid_connect") + self.assertEqual(sociallogin.account.user, user) + sent_signals.append(sender) + + user_signed_up.connect(on_signed_up) + self.login(resp_mock=None) + self.assertTrue(len(sent_signals) > 0) + + @override_settings(ACCOUNT_EMAIL_CONFIRMATION_HMAC=False) + def test_email_unverified(self): + self.identity_overwrites = {"email_verified": False} + resp = self.login(resp_mock=None) + email_address = EmailAddress.objects.get(email=self.email) + self.assertFalse(email_address.verified) + self.assertTrue(EmailConfirmation.objects.filter(email_address__email=self.email).exists()) + self.assertTemplateUsed(resp, "account/email/email_confirmation_signup_subject.txt") + + def test_email_verified_stashed(self): + # http://slacy.com/blog/2012/01/how-to-set-session-variables-in-django-unit-tests/ + engine = import_module(settings.SESSION_ENGINE) + store = engine.SessionStore() + store.save() + self.client.cookies[settings.SESSION_COOKIE_NAME] = store.session_key + request = RequestFactory().get("/") + request.session = self.client.session + adapter = get_adapter(request) + adapter.stash_verified_email(request, self.email) + request.session.save() + + self.identity_overwrites = {"email_verified": False} + self.login(resp_mock=None) + email_address = EmailAddress.objects.get(email=self.email) + self.assertTrue(email_address.verified) + self.assertFalse(EmailConfirmation.objects.filter(email_address__email=self.email).exists()) + + def test_account_connect(self): + email = "user@example.com" + user = get_user_model().objects.create(username="user", is_active=True, email=email) + user.set_password("test") + user.save() + EmailAddress.objects.create(user=user, email=email, primary=True, verified=True) + self.client.login(username=user.username, password="test") + self.identity_overwrites = {"email": email, "email_verified": True} + self.login(resp_mock=None, process="connect") + # Check if we connected... + # self.assertTrue(SocialAccount.objects.filter(user=user, provider="geonode_openid_connect").exists()) + # For now, we do not pick up any new e-mail addresses on connect + self.assertEqual(EmailAddress.objects.filter(user=user).count(), 1) + self.assertEqual(EmailAddress.objects.filter(user=user, email=email).count(), 1) + + @override_settings( + ACCOUNT_EMAIL_VERIFICATION=account_settings.EmailVerificationMethod.MANDATORY, + SOCIALACCOUNT_EMAIL_VERIFICATION=account_settings.EmailVerificationMethod.NONE, + ) + def test_social_email_verification_skipped(self): + self.identity_overwrites = {"email_verified": False} + self.login(resp_mock=None) + email_address = EmailAddress.objects.get(email=self.email) + self.assertFalse(email_address.verified) + self.assertFalse(EmailConfirmation.objects.filter(email_address__email=self.email).exists()) + + @override_settings( + ACCOUNT_EMAIL_VERIFICATION=account_settings.EmailVerificationMethod.OPTIONAL, + SOCIALACCOUNT_EMAIL_VERIFICATION=account_settings.EmailVerificationMethod.OPTIONAL, + ) + def test_social_email_verification_optional(self): + self.identity_overwrites = {"email_verified": False} + self.login(resp_mock=None) + self.assertEqual(len(mail.outbox), 1) + self.login(resp_mock=None) + self.assertEqual(len(mail.outbox), 1) + + +@override_settings( + SOCIALACCOUNT_OIDC_PROVIDER_ENABLED=True, + SOCIALACCOUNT_PROVIDERS={ + "geonode_openid_connect": { + "NAME": "Google", + "SCOPE": [ + "profile", + "email", + ], + "AUTH_PARAMS": { + "access_type": "online", + "prompt": "select_account consent", + }, + "COMMON_FIELDS": {"email": "email", "last_name": "family_name", "first_name": "given_name"}, + "ACCOUNT_CLASS": "allauth.socialaccount.providers.google.provider.GoogleAccount", + "ACCESS_TOKEN_URL": "https://oauth2.googleapis.com/token", + "AUTHORIZE_URL": "https://accounts.google.com/o/oauth2/v2/auth", + "ID_TOKEN_ISSUER": "https://accounts.google.com", + "OAUTH_PKCE_ENABLED": True, + } + }, +) +class AppInSettingsTests(GoogleTests): + """ + Run the same set of tests but without having a SocialApp entry. + """ + + pass diff --git a/geonode/people/socialaccount/providers/geonode_openid_connect/urls.py b/geonode/people/socialaccount/providers/geonode_openid_connect/urls.py new file mode 100644 index 00000000000..daa5ff7d752 --- /dev/null +++ b/geonode/people/socialaccount/providers/geonode_openid_connect/urls.py @@ -0,0 +1,24 @@ +######################################################################### +# +# Copyright (C) 2023 OSGeo +# +# 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 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### +from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns + +from geonode.people.socialaccount.providers.geonode_openid_connect.provider import GenericOpenIDConnectProvider + + +urlpatterns = default_urlpatterns(GenericOpenIDConnectProvider) diff --git a/geonode/people/socialaccount/providers/geonode_openid_connect/views.py b/geonode/people/socialaccount/providers/geonode_openid_connect/views.py new file mode 100644 index 00000000000..c009c52b95c --- /dev/null +++ b/geonode/people/socialaccount/providers/geonode_openid_connect/views.py @@ -0,0 +1,30 @@ +######################################################################### +# +# Copyright (C) 2023 OSGeo +# +# 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 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### +from django.conf import settings + +from geonode.utils import import_class_module + +from allauth.socialaccount.providers.oauth2.views import ( + OAuth2CallbackView, + OAuth2LoginView, +) + + +oauth2_login = OAuth2LoginView.adapter_view(import_class_module(settings.SOCIALACCOUNT_ADAPTER)) +oauth2_callback = OAuth2CallbackView.adapter_view(import_class_module(settings.SOCIALACCOUNT_ADAPTER)) diff --git a/geonode/people/templates/people/profile_detail.html b/geonode/people/templates/people/profile_detail.html index 0d6a48269e8..1a3c09ef7f1 100644 --- a/geonode/people/templates/people/profile_detail.html +++ b/geonode/people/templates/people/profile_detail.html @@ -73,14 +73,18 @@

    {{ profile.first_name|default:profile.name_long }}

    {% endif %} {% endif %} - - {% trans 'Position' %} - {{ profile.position | default:_('Not provided.') }} - {% trans 'Organization' %} {{ profile.organization | default:_('Not provided.') }} + + {% trans 'Description' %} + {{ profile.profile | default:_('Not provided.') }} + + + {% trans 'Position Name' %} + {{ profile.position | default:_('Not provided.') }} + {% trans 'Location' %} {{ profile.location | default:_('Not provided.') }} @@ -97,10 +101,6 @@

    {{ profile.first_name|default:profile.name_long }}

    {% trans 'Fax' %} {{ profile.fax | default:_('Not provided.') }} - - {% trans 'Description' %} - {{ profile.profile | default:_('Not provided.') }} - {% trans 'Keywords' %} diff --git a/geonode/proxy/__init__.py b/geonode/proxy/__init__.py index 79177e00bdd..7b2209ac902 100644 --- a/geonode/proxy/__init__.py +++ b/geonode/proxy/__init__.py @@ -1,6 +1,6 @@ ######################################################################### # -# Copyright (C) 2016 OSGeo +# Copyright (C) 2023 OSGeo # # 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 @@ -16,3 +16,4 @@ # along with this program. If not, see . # ######################################################################### +default_app_config = "geonode.proxy.apps.GeoNodeProxyAppConfig" diff --git a/geonode/proxy/apps.py b/geonode/proxy/apps.py new file mode 100644 index 00000000000..9901d333798 --- /dev/null +++ b/geonode/proxy/apps.py @@ -0,0 +1,24 @@ +######################################################################### +# +# Copyright (C) 2023 OSGeo +# +# 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 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### +from django.apps import AppConfig + + +class GeoNodeProxyAppConfig(AppConfig): + name = "geonode.proxy" + verbose_name = "GeoNode Proxy" diff --git a/geonode/resource/download_handler.py b/geonode/resource/download_handler.py new file mode 100644 index 00000000000..ae21e4e23a9 --- /dev/null +++ b/geonode/resource/download_handler.py @@ -0,0 +1,133 @@ +######################################################################### +# +# Copyright (C) 2023 OSGeo +# +# 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 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### + +import logging +import xml.etree.ElementTree as ET + +from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse +from django.template.loader import get_template +from django.urls import reverse +from django.utils.translation import ugettext as _ +from django.conf import settings +from geonode.base.auth import get_or_create_token +from geonode.geoserver.helpers import wps_format_is_supported +from geonode.layers.views import _resolve_dataset +from geonode.proxy.views import fetch_response_headers +from geonode.utils import HttpClient + +logger = logging.getLogger("geonode.resource.download_handler") + + +class DownloadHandler: + def __str__(self): + return f"{self.__module__}.{self.__class__.__name__}" + + def __repr__(self): + return self.__str__() + + def __init__(self, request, resource_name) -> None: + self.request = request + self.resource_name = resource_name + + def get_download_response(self): + """ + Basic method. Should return the Response object + that allow the resource download + """ + resource = self.get_resource() + response = self.process_dowload(resource) + return response + + def get_resource(self): + """ + Returnt the object needed + """ + try: + return _resolve_dataset( + self.request, + self.resource_name, + "base.download_resourcebase", + _("You do not have permissions for this dataset."), + ) + except Exception as e: + raise Http404(Exception(_("Not found"), e)) + + def process_dowload(self, resource): + """ + Generate the response object + """ + if not settings.USE_GEOSERVER: + # if GeoServer is not used, we redirect to the proxy download + return HttpResponseRedirect(reverse("download", args=[resource.id])) + + download_format = self.request.GET.get("export_format") + + if download_format and not wps_format_is_supported(download_format, resource.subtype): + logger.error("The format provided is not valid for the selected resource") + return JsonResponse({"error": "The format provided is not valid for the selected resource"}, status=500) + + _format = "application/zip" if resource.is_vector() else "image/tiff" + # getting default payload + tpl = get_template("geoserver/dataset_download.xml") + ctx = {"alternate": resource.alternate, "download_format": download_format or _format} + # applying context for the payload + payload = tpl.render(ctx) + + # init of Client + client = HttpClient() + + headers = {"Content-type": "application/xml", "Accept": "application/xml"} + + # defining the URL needed fr the download + url = f"{settings.OGC_SERVER['default']['LOCATION']}ows?service=WPS&version=1.0.0&REQUEST=Execute" + if not self.request.user.is_anonymous: + # define access token for the user + access_token = get_or_create_token(self.request.user) + url += f"&access_token={access_token}" + + # request to geoserver + response, content = client.request(url=url, data=payload, method="post", headers=headers) + + if not response or response.status_code != 200: + logger.error(f"Download dataset exception: error during call with GeoServer: {content}") + return JsonResponse( + {"error": "Download dataset exception: error during call with GeoServer"}, + status=500, + ) + + # error handling + namespaces = {"ows": "http://www.opengis.net/ows/1.1", "wps": "http://www.opengis.net/wps/1.0.0"} + response_type = response.headers.get("Content-Type") + if response_type == "text/xml": + # parsing XML for get exception + content = ET.fromstring(response.text) + exc = content.find("*//ows:Exception", namespaces=namespaces) or content.find( + "ows:Exception", namespaces=namespaces + ) + if exc: + exc_text = exc.find("ows:ExceptionText", namespaces=namespaces) + logger.error(f"{exc.attrib.get('exceptionCode')} {exc_text.text}") + return JsonResponse({"error": f"{exc.attrib.get('exceptionCode')}: {exc_text.text}"}, status=500) + + return_response = fetch_response_headers( + HttpResponse(content=response.content, status=response.status_code, content_type=download_format), + response.headers, + ) + return_response.headers["Content-Type"] = download_format or _format + return return_response diff --git a/geonode/resource/manager.py b/geonode/resource/manager.py index 28d52999e5f..38ca1a0e7c3 100644 --- a/geonode/resource/manager.py +++ b/geonode/resource/manager.py @@ -944,7 +944,7 @@ def set_thumbnail( file_name = _generate_thumbnail_name(_resource.get_real_instance()) _resource.save_thumbnail(file_name, thumbnail) else: - if instance and instance.files and isinstance(instance.get_real_instance(), Document): + if instance and isinstance(instance.get_real_instance(), Document): if overwrite or not instance.thumbnail_url: create_document_thumbnail.apply((instance.id,)) self._concrete_resource_manager.set_thumbnail( diff --git a/geonode/resource/utils.py b/geonode/resource/utils.py index 70dda53f921..b8e70a362cb 100644 --- a/geonode/resource/utils.py +++ b/geonode/resource/utils.py @@ -167,13 +167,7 @@ def update_resource( if vals: for key, value in vals.items(): if key == "spatial_representation_type": - spatial_repr = SpatialRepresentationType.objects.filter(identifier=value) - if value is not None and spatial_repr.exists(): - value = SpatialRepresentationType(identifier=value) - # if the SpatialRepresentationType is not available in the DB, we just set it as None - elif value is not None and not spatial_repr.exists(): - value = None - defaults[key] = value + defaults[key] = SpatialRepresentationType.objects.filter(identifier=value).first() if value else None elif key == "topic_category": value, created = TopicCategory.objects.get_or_create( identifier=value, defaults={"description": "", "gn_description": value} @@ -483,13 +477,7 @@ def metadata_post_save(instance, *args, **kwargs): regions_to_add = [] for region in queryset: try: - srid2, wkt2 = region.geographic_bounding_box.split(";") - srid2 = re.findall(r"\d+", srid2) - - poly2 = GEOSGeometry(wkt2, srid=int(srid2[0])) - poly2.transform(4326) - - if not poly2.intersection(poly1).empty: + if region.is_assignable_to_geom(poly1): regions_to_add.append(region) if region.level == 0 and region.parent is None: global_regions.append(region) @@ -498,7 +486,7 @@ def metadata_post_save(instance, *args, **kwargs): if tb: logger.debug(tb) if regions_to_add or global_regions: - if regions_to_add and len(regions_to_add) > 0 and len(regions_to_add) <= 30: + if regions_to_add: instance.regions.add(*regions_to_add) else: instance.regions.add(*global_regions) diff --git a/geonode/security/__init__.py b/geonode/security/__init__.py index 79177e00bdd..0edec538e72 100644 --- a/geonode/security/__init__.py +++ b/geonode/security/__init__.py @@ -1,6 +1,6 @@ ######################################################################### # -# Copyright (C) 2016 OSGeo +# Copyright (C) 2023 OSGeo # # 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 @@ -16,3 +16,4 @@ # along with this program. If not, see . # ######################################################################### +default_app_config = "geonode.security.apps.GeoNodeSecurityAppConfig" diff --git a/geonode/security/apps.py b/geonode/security/apps.py new file mode 100644 index 00000000000..3475f4d05f1 --- /dev/null +++ b/geonode/security/apps.py @@ -0,0 +1,24 @@ +######################################################################### +# +# Copyright (C) 2023 OSGeo +# +# 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 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### +from django.apps import AppConfig + + +class GeoNodeSecurityAppConfig(AppConfig): + name = "geonode.security" + verbose_name = "GeoNode Security" diff --git a/geonode/services/admin.py b/geonode/services/admin.py index d447c4145f0..2e61a21ce3e 100644 --- a/geonode/services/admin.py +++ b/geonode/services/admin.py @@ -31,6 +31,7 @@ class Meta(ResourceBaseAdminForm.Meta): class ServiceAdmin(admin.ModelAdmin): + exclude = ("ll_bbox_polygon", "bbox_polygon", "srid") list_display = ("id", "name", "base_url", "type", "method") list_display_links = ("id", "name") list_filter = ("id", "name", "type", "method") diff --git a/geonode/services/templates/services/service_detail.html b/geonode/services/templates/services/service_detail.html index beb8ac84b12..08c1ef70042 100644 --- a/geonode/services/templates/services/service_detail.html +++ b/geonode/services/templates/services/service_detail.html @@ -4,7 +4,7 @@ {% block body %}
    -

    {{service.title|default:service.name}}

    +

    {{service.title|default:service.name}}

    {% trans "Type" %}: {{service.service_type}}

    {% trans "URL" %}: {{service.base_url}}

    {% trans "Abstract" %}: {{service.abstract}}

    diff --git a/geonode/services/templates/services/service_remove.html b/geonode/services/templates/services/service_remove.html index 90a2f8fa9e9..3ce551b58b6 100644 --- a/geonode/services/templates/services/service_remove.html +++ b/geonode/services/templates/services/service_remove.html @@ -1,7 +1,7 @@ {% extends "services/services_base.html" %} {% load i18n %} -{% block title %} {{ service.name }} - {{ block.super }} {% endblock %} +{% block title %} {{service.title|default:service.name}} - {{ block.super }} {% endblock %} {% block body %}
    -

    {% trans "Are you sure you want to remove" %} {{ service.name }}?

    +

    {% trans "Are you sure you want to remove" %} {{service.title|default:service.name}}?

    {% csrf_token %} diff --git a/geonode/services/templates/services/service_resources_harvest.html b/geonode/services/templates/services/service_resources_harvest.html index 4541223354e..8de1f454f43 100644 --- a/geonode/services/templates/services/service_resources_harvest.html +++ b/geonode/services/templates/services/service_resources_harvest.html @@ -8,9 +8,7 @@ {% block body %}
    {% if resources %} diff --git a/geonode/services/views.py b/geonode/services/views.py index 12edf78e71a..dc67c212c67 100644 --- a/geonode/services/views.py +++ b/geonode/services/views.py @@ -352,5 +352,5 @@ def remove_service(request, service_id): elif request.method == "POST": service.dataset_set.all().delete() service.delete() - messages.add_message(request, messages.INFO, _(f"Service {service.name} has been deleted")) + messages.add_message(request, messages.INFO, _(f"Service {service.title} has been deleted")) return HttpResponseRedirect(reverse("services")) diff --git a/geonode/settings.py b/geonode/settings.py index d408093575b..9b746b9d63e 100644 --- a/geonode/settings.py +++ b/geonode/settings.py @@ -26,7 +26,6 @@ import dj_database_url from schema import Optional from datetime import timedelta -from distutils.util import strtobool # noqa from urllib.parse import urlparse, urljoin # @@ -653,6 +652,7 @@ "glb", "pcd", "gltf", + "ifc", ] if os.getenv("ALLOWED_DOCUMENT_TYPES") is None else re.split(r" *[,|:;] *", os.getenv("ALLOWED_DOCUMENT_TYPES")) @@ -1004,9 +1004,12 @@ GEOSERVER_WEB_UI_LOCATION = os.getenv("GEOSERVER_WEB_UI_LOCATION", GEOSERVER_PUBLIC_LOCATION) -OGC_SERVER_DEFAULT_USER = os.getenv("GEOSERVER_ADMIN_USER", "admin") +GEOSERVER_ADMIN_USER = os.getenv("GEOSERVER_ADMIN_USER", "admin") -OGC_SERVER_DEFAULT_PASSWORD = os.getenv("GEOSERVER_ADMIN_PASSWORD", "geoserver") +GEOSERVER_ADMIN_PASSWORD = os.getenv("GEOSERVER_ADMIN_PASSWORD", "geoserver") + +# This is the password from Geoserver factory data-dir. It's only used at install time to perform the very first configurfation of GEOSERVER_ADMIN_PASSWORD +GEOSERVER_FACTORY_PASSWORD = os.getenv("GEOSERVER_FACTORY_PASSWORD", "geoserver") GEOFENCE_SECURITY_ENABLED = ( False if TEST and not INTEGRATION else ast.literal_eval(os.getenv("GEOFENCE_SECURITY_ENABLED", "True")) @@ -1025,8 +1028,8 @@ # the proxy won't work and the integration tests will fail # the entire block has to be overridden in the local_settings "PUBLIC_LOCATION": GEOSERVER_PUBLIC_LOCATION, - "USER": OGC_SERVER_DEFAULT_USER, - "PASSWORD": OGC_SERVER_DEFAULT_PASSWORD, + "USER": GEOSERVER_ADMIN_USER, + "PASSWORD": GEOSERVER_ADMIN_PASSWORD, "MAPFISH_PRINT_ENABLED": ast.literal_eval(os.getenv("MAPFISH_PRINT_ENABLED", "True")), "PRINT_NG_ENABLED": ast.literal_eval(os.getenv("PRINT_NG_ENABLED", "True")), "GEONODE_SECURITY_ENABLED": ast.literal_eval(os.getenv("GEONODE_SECURITY_ENABLED", "True")), @@ -1956,58 +1959,81 @@ def get_geonode_catalogue_service(): ACCOUNT_LOGIN_ATTEMPTS_LIMIT = int(os.getenv("ACCOUNT_LOGIN_ATTEMPTS_LIMIT", "3")) ACCOUNT_MAX_EMAIL_ADDRESSES = int(os.getenv("ACCOUNT_MAX_EMAIL_ADDRESSES", "2")) -SOCIALACCOUNT_ADAPTER = "geonode.people.adapters.SocialAccountAdapter" SOCIALACCOUNT_AUTO_SIGNUP = ast.literal_eval(os.environ.get("SOCIALACCOUNT_AUTO_SIGNUP", "True")) +SOCIALACCOUNT_LOGIN_ON_GET = ast.literal_eval(os.environ.get("SOCIALACCOUNT_LOGIN_ON_GET", "True")) # This will hide or show local registration form in allauth view. True will show form -SOCIALACCOUNT_WITH_GEONODE_LOCAL_SINGUP = strtobool(os.environ.get("SOCIALACCOUNT_WITH_GEONODE_LOCAL_SINGUP", "True")) +SOCIALACCOUNT_WITH_GEONODE_LOCAL_SINGUP = ast.literal_eval( + os.environ.get("SOCIALACCOUNT_WITH_GEONODE_LOCAL_SINGUP", "True") +) -# Uncomment this to enable Linkedin and Facebook login -# INSTALLED_APPS += ( -# 'allauth.socialaccount.providers.linkedin_oauth2', -# 'allauth.socialaccount.providers.facebook', -# ) +# GeoNode Default Generic OIDC Provider -SOCIALACCOUNT_PROVIDERS = { - "linkedin_oauth2": { - "SCOPE": [ - "r_emailaddress", - "r_liteprofile", - ], - "PROFILE_FIELDS": [ - "id", - "email-address", - "first-name", - "last-name", - "picture-url", - "public-profile-url", - ], +SOCIALACCOUNT_OIDC_PROVIDER = os.environ.get("SOCIALACCOUNT_OIDC_PROVIDER", "geonode_openid_connect") +SOCIALACCOUNT_OIDC_PROVIDER_ENABLED = ast.literal_eval(os.environ.get("SOCIALACCOUNT_OIDC_PROVIDER_ENABLED", "False")) +SOCIALACCOUNT_ADAPTER = os.environ.get("SOCIALACCOUNT_ADAPTER", "geonode.people.adapters.GenericOpenIDConnectAdapter") + +_SOCIALACCOUNT_PROFILE_EXTRACTOR = os.environ.get( + "SOCIALACCOUNT_PROFILE_EXTRACTOR", "geonode.people.profileextractors.OpenIDExtractor" +) +SOCIALACCOUNT_PROFILE_EXTRACTORS = { + SOCIALACCOUNT_OIDC_PROVIDER: _SOCIALACCOUNT_PROFILE_EXTRACTOR, +} + +SOCIALACCOUNT_GROUP_ROLE_MAPPER = os.environ.get( + "SOCIALACCOUNT_GROUP_ROLE_MAPPER", "geonode.people.profileextractors.OpenIDGroupRoleMapper" +) + +# Enable this in order to enable the OIDC SocialAccount Provider +if SOCIALACCOUNT_OIDC_PROVIDER_ENABLED: + INSTALLED_APPS += ("geonode.people.socialaccount.providers.geonode_openid_connect",) + +_AZURE_TENANT_ID = os.getenv("MICROSOFT_TENANT_ID", "") +_AZURE_SOCIALACCOUNT_PROVIDER = { + "NAME": "Microsoft Azure", + "SCOPE": [ + "User.Read", + "openid", + ], + "AUTH_PARAMS": { + "access_type": "online", + "prompt": "select_account", }, - "facebook": { - "METHOD": "oauth2", - "SCOPE": [ - "email", - "public_profile", - ], - "FIELDS": [ - "id", - "email", - "name", - "first_name", - "last_name", - "verified", - "locale", - "timezone", - "link", - "gender", - ], + "COMMON_FIELDS": {"email": "mail", "last_name": "surname", "first_name": "givenName"}, + "UID_FIELD": "unique_name", + "GROUP_ROLE_MAPPER_CLASS": SOCIALACCOUNT_GROUP_ROLE_MAPPER, + "ACCOUNT_CLASS": "allauth.socialaccount.providers.azure.provider.AzureAccount", + "ACCESS_TOKEN_URL": f"https://login.microsoftonline.com/{_AZURE_TENANT_ID}/oauth2/v2.0/token", + "AUTHORIZE_URL": f"https://login.microsoftonline.com/{_AZURE_TENANT_ID}/oauth2/v2.0/authorize", + "PROFILE_URL": "https://graph.microsoft.com/v1.0/me", +} + +_GOOGLE_SOCIALACCOUNT_PROVIDER = { + "NAME": "Google", + "SCOPE": [ + "profile", + "email", + ], + "AUTH_PARAMS": { + "access_type": "online", + "prompt": "select_account consent", }, + "COMMON_FIELDS": {"email": "email", "last_name": "family_name", "first_name": "given_name"}, + "GROUP_ROLE_MAPPER_CLASS": SOCIALACCOUNT_GROUP_ROLE_MAPPER, + "ACCOUNT_CLASS": "allauth.socialaccount.providers.google.provider.GoogleAccount", + "ACCESS_TOKEN_URL": "https://oauth2.googleapis.com/token", + "AUTHORIZE_URL": "https://accounts.google.com/o/oauth2/v2/auth", + "ID_TOKEN_ISSUER": "https://accounts.google.com", + "OAUTH_PKCE_ENABLED": True, } -SOCIALACCOUNT_PROFILE_EXTRACTORS = { - "facebook": "geonode.people.profileextractors.FacebookExtractor", - "linkedin_oauth2": "geonode.people.profileextractors.LinkedInExtractor", +SOCIALACCOUNT_PROVIDERS_DEFS = {"azure": _AZURE_SOCIALACCOUNT_PROVIDER, "google": _GOOGLE_SOCIALACCOUNT_PROVIDER} + +_SOCIALACCOUNT_PROVIDER = os.environ.get("SOCIALACCOUNT_PROVIDER", "google") +SOCIALACCOUNT_PROVIDERS = { + SOCIALACCOUNT_OIDC_PROVIDER: SOCIALACCOUNT_PROVIDERS_DEFS.get(_SOCIALACCOUNT_PROVIDER), } +# Invitation Adapter INVITATIONS_ADAPTER = ACCOUNT_ADAPTER INVITATIONS_CONFIRMATION_URL_NAME = "geonode.invitations:accept-invite" @@ -2117,7 +2143,7 @@ def get_geonode_catalogue_service(): GEOIP_PATH = os.getenv("GEOIP_PATH", os.path.join(PROJECT_ROOT, "GeoIPCities.dat")) # This controls if tastypie search on resourches is performed only with titles -SEARCH_RESOURCES_EXTENDED = strtobool(os.getenv("SEARCH_RESOURCES_EXTENDED", "True")) +SEARCH_RESOURCES_EXTENDED = ast.literal_eval(os.getenv("SEARCH_RESOURCES_EXTENDED", "True")) # -- END Settings for MONITORING plugin CATALOG_METADATA_TEMPLATE = os.getenv("CATALOG_METADATA_TEMPLATE", "catalogue/full_metadata.xml") @@ -2319,9 +2345,14 @@ def get_geonode_catalogue_service(): INSTALLED_APPS += ("geonode.facets",) GEONODE_APPS += ("geonode.facets",) -FACET_PROVIDERS = ( - "geonode.facets.providers.category.CategoryFacetProvider", - "geonode.facets.providers.users.OwnerFacetProvider", - "geonode.facets.providers.thesaurus.ThesaurusFacetProvider", - "geonode.facets.providers.region.RegionFacetProvider", -) +FACET_PROVIDERS = [ + {"class": "geonode.facets.providers.baseinfo.ResourceTypeFacetProvider"}, + {"class": "geonode.facets.providers.baseinfo.FeaturedFacetProvider"}, + {"class": "geonode.facets.providers.category.CategoryFacetProvider", "config": {"order": 5, "type": "select"}}, + {"class": "geonode.facets.providers.keyword.KeywordFacetProvider", "config": {"order": 6, "type": "select"}}, + {"class": "geonode.facets.providers.region.RegionFacetProvider", "config": {"order": 7, "type": "select"}}, + {"class": "geonode.facets.providers.users.OwnerFacetProvider", "config": {"order": 8, "type": "select"}}, + {"class": "geonode.facets.providers.thesaurus.ThesaurusFacetProvider", "config": {"type": "select"}}, +] + +DATASET_DOWNLOAD_HANDLER = os.getenv("DATASET_DOWNLOAD_HANDLER", "geonode.resource.download_handler.DownloadHandler") diff --git a/geonode/storage/manager.py b/geonode/storage/manager.py index f4adf983d2a..421ce6946e3 100644 --- a/geonode/storage/manager.py +++ b/geonode/storage/manager.py @@ -249,7 +249,7 @@ def clone_remote_files(self) -> Mapping: """ Using the data retriever object clone the remote path into a local temporary storage """ - self.data_retriever.get_paths(allow_transfer=True) + return self.data_retriever.get_paths(allow_transfer=True) def get_retrieved_paths(self) -> Mapping: """ diff --git a/geonode/tasks/__init__.py b/geonode/tasks/__init__.py index f3b257d5e12..41405557613 100644 --- a/geonode/tasks/__init__.py +++ b/geonode/tasks/__init__.py @@ -16,3 +16,4 @@ # along with this program. If not, see . # ######################################################################### +default_app_config = "geonode.tasks.apps.GeoNodeTasksAppConfig" diff --git a/geonode/tasks/apps.py b/geonode/tasks/apps.py new file mode 100644 index 00000000000..9b49856a418 --- /dev/null +++ b/geonode/tasks/apps.py @@ -0,0 +1,24 @@ +######################################################################### +# +# Copyright (C) 2023 OSGeo +# +# 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 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### +from django.apps import AppConfig + + +class GeoNodeTasksAppConfig(AppConfig): + name = "geonode.tasks" + verbose_name = "GeoNode Tasks" diff --git a/geonode/templates/pinax/notifications/account_active/account_active_message.txt b/geonode/templates/pinax/notifications/account_active/account_active_message.txt index 02237b540e6..f9ddbc0c091 100644 --- a/geonode/templates/pinax/notifications/account_active/account_active_message.txt +++ b/geonode/templates/pinax/notifications/account_active/account_active_message.txt @@ -1,3 +1,3 @@ {% load i18n %} {% trans "Your account has been approved and is now active." %} ({{ username }})
    -{% trans "You can use the login form at" %}: http://{{ current_site.name }} +{% trans "You can use the login form at" %}: {{ LOGIN_URL }} diff --git a/geonode/templates/pinax/notifications/account_active/full.txt b/geonode/templates/pinax/notifications/account_active/full.txt index aac4b9cc507..8344ed13d4b 100644 --- a/geonode/templates/pinax/notifications/account_active/full.txt +++ b/geonode/templates/pinax/notifications/account_active/full.txt @@ -1,3 +1,3 @@ {% load i18n %} {% trans "Your account has been approved and is now active." %}
    -{% trans "You can use the login form at" %}: http://{{ current_site }} +{% trans "You can use the login form at" %}: {{ LOGIN_URL }} diff --git a/geonode/tests/smoke.py b/geonode/tests/smoke.py index 7ae4a37c1b9..bda90ec0e16 100644 --- a/geonode/tests/smoke.py +++ b/geonode/tests/smoke.py @@ -18,7 +18,10 @@ ######################################################################### from unittest import TestCase + +from django.http import HttpResponse from geonode.base.populate_test_data import create_single_dataset +from geonode.resource.download_handler import DownloadHandler from geonode.resource.utils import metadata_storers from geonode.tests.base import GeoNodeBaseTestSupport @@ -333,3 +336,21 @@ def dummy_metadata_storer2(dataset, custom): if custom.get("second-stage", None): for key, value in custom["second-stage"].items(): setattr(dataset, key, value) + + +class DummyDownloadManager(DownloadHandler): + def get_download_response(self): + return HttpResponse(content=b"abcsfd2") + + +class TestDownloadManager(GeoNodeBaseTestSupport): + def setUp(self): + self.sut = DownloadHandler + + @override_settings(DATASET_DOWNLOAD_HANDLER="geonode.tests.smoke.DummyDownloadManager") + def test_download_handler(self): + dataset = create_single_dataset("test_dataset") + url = reverse("dataset_download", args=[dataset.alternate]) + response = self.client.get(url) + self.assertTrue(response.status_code == 200) + self.assertEqual(response.content, b"abcsfd2") diff --git a/geonode/upload/tests/test_settings.py b/geonode/upload/tests/test_settings.py index 9e25f52c835..3ff3368c88b 100644 --- a/geonode/upload/tests/test_settings.py +++ b/geonode/upload/tests/test_settings.py @@ -100,9 +100,11 @@ GEOSERVER_PUBLIC_LOCATION = os.getenv("GEOSERVER_PUBLIC_LOCATION", _default_public_location) -OGC_SERVER_DEFAULT_USER = os.getenv("GEOSERVER_ADMIN_USER", "admin") +GEOSERVER_ADMIN_USER = os.getenv("GEOSERVER_ADMIN_USER", "admin") -OGC_SERVER_DEFAULT_PASSWORD = os.getenv("GEOSERVER_ADMIN_PASSWORD", "geoserver") +GEOSERVER_ADMIN_PASSWORD = os.getenv("GEOSERVER_ADMIN_PASSWORD", "geoserver") + +GEOSERVER_FACTORY_PASSWORD = os.getenv("GEOSERVER_FACTORY_PASSWORD", "geoserver") # OGC (WMS/WFS/WCS) Server Settings OGC_SERVER = { @@ -116,8 +118,8 @@ # the proxy won't work and the integration tests will fail # the entire block has to be overridden in the local_settings "PUBLIC_LOCATION": GEOSERVER_PUBLIC_LOCATION, - "USER": OGC_SERVER_DEFAULT_USER, - "PASSWORD": OGC_SERVER_DEFAULT_PASSWORD, + "USER": GEOSERVER_ADMIN_USER, + "PASSWORD": GEOSERVER_ADMIN_PASSWORD, "MAPFISH_PRINT_ENABLED": True, "PRINT_NG_ENABLED": True, "GEONODE_SECURITY_ENABLED": True, diff --git a/geonode/utils.py b/geonode/utils.py index 3c83c824db8..5abab4b2411 100755 --- a/geonode/utils.py +++ b/geonode/utils.py @@ -17,7 +17,6 @@ # ######################################################################### -import itertools import os import gc import re @@ -34,6 +33,8 @@ import datetime import requests import tempfile +import importlib +import itertools import traceback import subprocess @@ -1412,6 +1413,8 @@ def set_resource_default_links(instance, layer, prune=False, **kwargs): from geonode.base.models import Link from django.urls import reverse from django.utils.translation import ugettext + from geonode.layers.models import Dataset + from geonode.documents.models import Document # Prune old links if prune: @@ -1481,9 +1484,15 @@ def set_resource_default_links(instance, layer, prune=False, **kwargs): # Create Raw Data download link if settings.DISPLAY_ORIGINAL_DATASET_LINK: logger.debug(" -- Resource Links[Create Raw Data download link]...") - download_url = urljoin(settings.SITEURL, reverse("download", args=[instance.id])) - while Link.objects.filter(resource=instance.resourcebase_ptr, url=download_url).count() > 1: - Link.objects.filter(resource=instance.resourcebase_ptr, url=download_url).first().delete() + if isinstance(instance, Dataset): + download_url = build_absolute_uri(reverse("dataset_download", args=(instance.alternate,))) + elif isinstance(instance, Document): + download_url = build_absolute_uri(reverse("document_download", args=(instance.id,))) + else: + download_url = None + + while Link.objects.filter(resource=instance.resourcebase_ptr, link_type="original").exists(): + Link.objects.filter(resource=instance.resourcebase_ptr, link_type="original").delete() Link.objects.update_or_create( resource=instance.resourcebase_ptr, url=download_url, @@ -1955,7 +1964,24 @@ def get_supported_datasets_file_types(): supported_types[default_types_id.index(_type.get("id"))] = _type else: supported_types.extend([_type]) - return supported_types + + # Order the formats (to support their visualization) + formats_order = [("vector", 0), ("raster", 1), ("archive", 2)] + ordered_payload = ( + (weight[1], resource_type) + for resource_type in supported_types + for weight in formats_order + if resource_type.get("format") in weight[0] + ) + + # Flatten the list + ordered_resource_types = [x[1] for x in sorted(ordered_payload, key=lambda x: x[0])] + other_resource_types = [ + resource_type + for resource_type in supported_types + if resource_type.get("format") is None or resource_type.get("format") not in [f[0] for f in formats_order] + ] + return ordered_resource_types + other_resource_types def get_allowed_extensions(): @@ -1980,3 +2006,19 @@ def safe_path_leaf(path): f"The provided path '{path}' is not safe. The file is outside the MEDIA_ROOT '{base_path}' base path!" ) return fullpath + + +def import_class_module(full_class_string): + """ + Dynamically load a class from a string + + >>> klass = import_class_module("module.submodule.ClassName") + >>> klass2 = import_class_module("myfile.Class2") + """ + try: + module_path, class_name = full_class_string.rsplit(".", 1) + module = importlib.import_module(module_path) + class_obj = getattr(module, class_name) + return class_obj + except Exception: + return None diff --git a/package/support/geonode.local_settings b/package/support/geonode.local_settings index 5525ee0aa20..3e485a5d021 100644 --- a/package/support/geonode.local_settings +++ b/package/support/geonode.local_settings @@ -98,14 +98,18 @@ GEOSERVER_PUBLIC_LOCATION = os.getenv( 'GEOSERVER_PUBLIC_LOCATION', '{}gs/'.format(SITEURL) ) -OGC_SERVER_DEFAULT_USER = os.getenv( +GEOSERVER_ADMIN_USER = os.getenv( 'GEOSERVER_ADMIN_USER', 'admin' ) -OGC_SERVER_DEFAULT_PASSWORD = os.getenv( +GEOSERVER_ADMIN_PASSWORD = os.getenv( 'GEOSERVER_ADMIN_PASSWORD', 'geoserver' ) +GEOSERVER_FACTORY_PASSWORD = os.getenv( + 'GEOSERVER_FACTORY_PASSWORD', 'geoserver' +) + # OGC (WMS/WFS/WCS) Server Settings OGC_SERVER = { 'default': { @@ -117,8 +121,8 @@ OGC_SERVER = { # the proxy won't work and the integration tests will fail # the entire block has to be overridden in the local_settings 'PUBLIC_LOCATION': GEOSERVER_PUBLIC_LOCATION, - 'USER': OGC_SERVER_DEFAULT_USER, - 'PASSWORD': OGC_SERVER_DEFAULT_PASSWORD, + 'USER': GEOSERVER_ADMIN_USER, + 'PASSWORD': GEOSERVER_ADMIN_PASSWORD, 'MAPFISH_PRINT_ENABLED': True, 'PRINT_NG_ENABLED': True, 'GEONODE_SECURITY_ENABLED': True, diff --git a/pavement.py b/pavement.py index 8ef01a09492..3c59b99a4eb 100644 --- a/pavement.py +++ b/pavement.py @@ -261,10 +261,6 @@ def static(options): ) def setup(options): """Get dependencies and prepare a GeoNode development environment.""" - - if MONITORING_ENABLED: - updategeoip(options) - info( "GeoNode development environment successfully set up." "If you have not set up an administrative account," @@ -334,20 +330,6 @@ def upgradedb(options): print(f"Upgrades from version {version} are not yet supported.") -@task -@cmdopts([("settings=", "s", "Specify custom DJANGO_SETTINGS_MODULE")]) -def updategeoip(options): - """ - Update geoip db - """ - if MONITORING_ENABLED: - settings = options.get("settings", "") - if settings and "DJANGO_SETTINGS_MODULE" not in settings: - settings = f"DJANGO_SETTINGS_MODULE={settings}" - - sh(f"{settings} python -W ignore manage.py updategeoip -o") - - @task @cmdopts([("settings=", "s", "Specify custom DJANGO_SETTINGS_MODULE")]) def sync(options): @@ -521,7 +503,7 @@ def start_django(options): bind = options.get("bind", "0.0.0.0:8000") port = bind.split(":")[1] foreground = "" if options.get("foreground", False) else "&" - sh(f"{settings} python -W ignore manage.py runserver --nostatic {bind} {foreground}") + sh(f"{settings} python -W ignore manage.py runserver {bind} {foreground}") if ASYNC_SIGNALS: sh( @@ -783,9 +765,7 @@ def test_integration(options): bind = options.get("bind", "0.0.0.0:8000") foreground = "" if options.get("foreground", False) else "&" sh(f"DJANGO_SETTINGS_MODULE={settings} python -W ignore manage.py runmessaging {foreground}") - sh( - f"DJANGO_SETTINGS_MODULE={settings} python -W ignore manage.py runserver --nostatic {bind} {foreground}" - ) + sh(f"DJANGO_SETTINGS_MODULE={settings} python -W ignore manage.py runserver {bind} {foreground}") sh("sleep 30") settings = f"REUSE_DB=1 DJANGO_SETTINGS_MODULE={settings}" else: diff --git a/requirements.txt b/requirements.txt index a911b3db9e2..05db52f8a43 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ # native dependencies -Pillow==9.5.0 -lxml==4.9.2 -psycopg2==2.9.6 -Django==3.2.19 +Pillow==10.0.0 +lxml==4.9.3 +psycopg2==2.9.7 +Django==3.2.20 # Other amqp==5.1.1 @@ -14,19 +14,19 @@ urllib3==1.26.15 Paver==1.3.4 python-slugify==8.0.1 decorator==5.1.1 -celery==5.2.7 -kombu==5.2.4 +celery==5.3.1 +kombu==5.3.1 vine==5.0.0 -tqdm==4.65.0 -Deprecated==1.2.13 +tqdm==4.66.1 +Deprecated==1.2.14 wrapt==1.15.0 -jsonschema==4.17.3 +jsonschema==4.19.0 zipstream-new==1.1.8 schema==0.7.5 rdflib==6.3.2 smart_open==6.3.0 -PyMuPDF==1.22.3 -pathvalidate==2.5.2 +PyMuPDF==1.22.5 +pathvalidate==3.1.0 # Django Apps django-allauth==0.54.0 @@ -44,30 +44,30 @@ django-downloadview==2.3.0 django-polymorphic==3.1.0 django-tastypie<0.15.0 django-tinymce==3.6.1 -django-grappelli==3.0.6 +django-grappelli==3.0.7 django-uuid-upload-path==1.0.0 django-widget-tweaks==1.4.12 -django-sequences==2.7 +django-sequences==2.8 oauthlib==3.2.2 -pyopenssl==23.1.1 -pyjwt==2.7.0 +pyopenssl==23.2.0 +pyjwt==2.8.0 # geopython dependencies -pyproj<3.6.0 +pyproj<3.7.0 OWSLib==0.29.2 pycsw==2.6.1 -SQLAlchemy==2.0.15 # required by PyCSW +SQLAlchemy==2.0.20 # required by PyCSW Shapely==1.8.5.post1 mercantile==1.2.1 geoip2==4.7.0 -numpy==1.24.* +numpy==1.25.* # # Apps with packages provided in GeoNode's PPA on Launchpad. # Django Apps -dj-database-url==2.0.0 +dj-database-url==2.1.0 dj-pagination==2.5.0 -django-select2==8.1.1 +django-select2==8.1.2 django-floppyforms<1.10.0 django-forms-bootstrap<=3.1.0 django-autocomplete-light==3.5.1 @@ -80,9 +80,9 @@ djangorestframework-gis==1.0 djangorestframework-guardian==0.3.0 drf-extensions==0.7.1 drf-writable-nested==0.7.0 -drf-spectacular==0.26.2 +drf-spectacular==0.26.4 dynamic-rest==2.1.2 -Markdown==3.4.3 +Markdown==3.4.4 pinax-notifications==6.0.0 pinax-ratings==4.0.0 @@ -91,12 +91,12 @@ pinax-ratings==4.0.0 # django-geonode-mapstore-client==4.0.5 -e git+https://github.com/GeoNode/geonode-mapstore-client.git@master#egg=django_geonode_mapstore_client -e git+https://github.com/GeoNode/geonode-importer.git@master#egg=geonode-importer -geonode-avatar==5.0.8 +django-avatar==7.1.1 geonode-oauth-toolkit==2.2.2 geonode-user-messages==2.0.2 geonode-announcements==2.0.2 geonode-django-activity-stream==0.10.0 -gn-arcrest==10.5.5 +gn-arcrest==10.5.6 geoserver-restconfig==2.0.9 gn-gsimporter==2.0.4 gisdata==0.5.4 @@ -110,14 +110,14 @@ django-bootstrap3-datetimepicker-2==2.8.3 # storage manager dependencies django-storages==1.13.2 -dropbox==11.36.0 -google-cloud-storage==2.9.0 -google-cloud-core==2.3.2 -boto3==1.26.137 +dropbox==11.36.2 +google-cloud-storage==2.10.0 +google-cloud-core==2.3.3 +boto3==1.28.32 # Django Caches python-memcached<=1.59 -whitenoise==6.4.0 +whitenoise==6.5.0 Brotli==1.0.9 # Contribs @@ -134,7 +134,7 @@ sherlock==0.4.1 # required by monitoring psutil==5.9.5 -django-cors-headers==4.0.0 +django-cors-headers==4.2.0 user-agents django-user-agents xmljson @@ -143,36 +143,37 @@ django-ipware<5.1 pycountry # production -uWSGI==2.0.21 -gunicorn==20.1.0 -ipython==8.13.2 -docker==6.1.2 -invoke==2.1.2 +uWSGI==2.0.22 +gunicorn==21.2.0 +ipython==8.14.0 +docker==6.1.3 +invoke==2.2.0 # tests -coverage==7.2.5 +coverage==7.3.0 requests-toolbelt==1.0.0 -flake8==6.0.0 -black==23.3.0 -pytest==7.3.1 +flake8==6.1.0 +black==23.7.0 +pytest==7.4.0 pytest-bdd==6.1.1 splinter==0.19.0 pytest-splinter==3.3.2 pytest-django==4.5.2 -setuptools>=59.1.1,<67.9.0 -pip==23.1.2 +setuptools>=59.1.1,<68.2.0 +pip==23.2.1 Twisted==22.10.0 pixelmatch==0.3.0 -factory-boy==3.2.1 +factory-boy==3.3.0 flaky==3.7.0 selenium>=4.1.0,<5.0.0 selenium-requests==2.0.3 -webdriver_manager==3.8.6 +webdriver_manager==4.0.0 # Security and audit -mistune==2.0.5 -wandb==0.15.3 +mistune==3.0.1 protobuf==3.20.3 mako==1.2.4 +paramiko==3.3.1 # not directly required, fixes Blowfish deprecation warning certifi>=2022.12.7 # not directly required, pinned by Snyk to avoid a vulnerability jwcrypto>=1.4 # not directly required, pinned by Snyk to avoid a vulnerability +cryptography>=41.0.0 # not directly required, pinned by Snyk to avoid a vulnerability diff --git a/scripts/docker/base/ubuntu/Dockerfile b/scripts/docker/base/ubuntu/Dockerfile new file mode 100644 index 00000000000..13ca38d20c4 --- /dev/null +++ b/scripts/docker/base/ubuntu/Dockerfile @@ -0,0 +1,40 @@ +FROM ubuntu:22.10 + +RUN mkdir -p /usr/src/geonode + +## Enable postgresql-client-15 +RUN apt-get update -y && apt-get install curl wget unzip gnupg2 -y +RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - +# will install python3.10 +RUN apt-get install lsb-core -y +RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" |tee /etc/apt/sources.list.d/pgdg.list + +# Prepraing dependencies +RUN apt-get install -y \ + libgdal-dev libpq-dev libxml2-dev \ + libxml2 libxslt1-dev zlib1g-dev libjpeg-dev \ + libmemcached-dev libldap2-dev libsasl2-dev libffi-dev + +RUN apt-get update -y && apt-get install -y --no-install-recommends \ + gcc vim zip gettext geoip-bin cron \ + postgresql-client-15 \ + python3-all-dev python3-dev \ + python3-gdal python3-psycopg2 python3-ldap \ + python3-pip python3-pil python3-lxml \ + uwsgi uwsgi-plugin-python3 python3-gdbm python-is-python3 gdal-bin + +RUN apt-get install -y devscripts build-essential debhelper pkg-kde-tools sharutils +# RUN git clone https://salsa.debian.org/debian-gis-team/proj.git /tmp/proj +# RUN cd /tmp/proj && debuild -i -us -uc -b && dpkg -i ../*.deb + +# Install pip packages +RUN pip3 install uwsgi \ + && pip install pip --upgrade \ + && pip install pygdal==$(gdal-config --version).* flower==0.9.4 + +# Activate "memcached" +RUN apt-get install -y memcached +RUN pip install sherlock + +# Cleanup apt update lists +RUN rm -rf /var/lib/apt/lists/* diff --git a/scripts/docker/letsencrypt/Dockerfile b/scripts/docker/letsencrypt/Dockerfile index a77f3b0219c..1480342e8f1 100644 --- a/scripts/docker/letsencrypt/Dockerfile +++ b/scripts/docker/letsencrypt/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.16 +FROM alpine:latest RUN apk add --no-cache certbot diff --git a/scripts/docker/nginx/Dockerfile b/scripts/docker/nginx/Dockerfile index 92652f376c6..2e3a8fea7cd 100644 --- a/scripts/docker/nginx/Dockerfile +++ b/scripts/docker/nginx/Dockerfile @@ -1,9 +1,12 @@ -FROM nginx:1-alpine +FROM nginx:1.25.1-alpine -RUN apk add --no-cache openssl inotify-tools +RUN apk add --no-cache openssl inotify-tools vim WORKDIR /etc/nginx/ +RUN mkdir -p /etc/nginx/html +RUN touch /etc/nginx/html/index.html + ADD nginx.conf.envsubst nginx.https.available.conf.envsubst ./ ADD geonode.conf.envsubst ./sites-enabled/ diff --git a/scripts/docker/nginx/docker-entrypoint.sh b/scripts/docker/nginx/docker-entrypoint.sh index 9afb7cffedb..e6bec7a1db2 100644 --- a/scripts/docker/nginx/docker-entrypoint.sh +++ b/scripts/docker/nginx/docker-entrypoint.sh @@ -15,7 +15,7 @@ echo "Creating autoissued certificates for HTTP host" if [ ! -f "/geonode-certificates/autoissued/privkey.pem" ] || [[ $(find /geonode-certificates/autoissued/privkey.pem -mtime +365 -print) ]]; then echo "Autoissued certificate does not exist or is too old, we generate one" mkdir -p "/geonode-certificates/autoissued/" - openssl req -x509 -nodes -days 1825 -newkey rsa:2048 -keyout "/geonode-certificates/autoissued/privkey.pem" -out "/geonode-certificates/autoissued/fullchain.pem" -subj "/CN=${HTTP_HOST:-null}" + openssl req -x509 -nodes -days 1825 -newkey rsa:2048 -keyout "/geonode-certificates/autoissued/privkey.pem" -out "/geonode-certificates/autoissued/fullchain.pem" -subj "/CN=${HTTP_HOST:-HTTPS_HOST}" else echo "Autoissued certificate already exists" fi @@ -32,18 +32,25 @@ else ln -sf "/geonode-certificates/autoissued" /certificate_symlink fi -echo "Sanity checks on http/s ports configuration" -if [ -z "${JENKINS_HTTP_PORT}" ]; then - JENKINS_HTTP_PORT=9080 +if [ -z "${HTTPS_HOST}" ]; then + HTTP_SCHEME="http" +else + HTTP_SCHEME="https" fi +export HTTP_SCHEME=${HTTP_SCHEME:-http} +export GEONODE_LB_HOST_IP=${GEONODE_LB_HOST_IP:-django} +export GEONODE_LB_PORT=${GEONODE_LB_PORT:-8000} +export GEOSERVER_LB_HOST_IP=${GEOSERVER_LB_HOST_IP:-geoserver} +export GEOSERVER_LB_PORT=${GEOSERVER_LB_PORT:-8080} + echo "Replacing environement variables" -envsubst '\$HTTP_HOST \$HTTPS_HOST \$RESOLVER' < /etc/nginx/nginx.conf.envsubst > /etc/nginx/nginx.conf -envsubst '\$HTTP_HOST \$HTTPS_HOST \$RESOLVER' < /etc/nginx/nginx.https.available.conf.envsubst > /etc/nginx/nginx.https.available.conf -envsubst '\$HTTP_HOST \$HTTPS_HOST \$JENKINS_HTTP_PORT' < /etc/nginx/sites-enabled/geonode.conf.envsubst > /etc/nginx/sites-enabled/geonode.conf +envsubst '\$HTTP_HOST \$HTTPS_HOST \$HTTP_SCHEME \$GEONODE_LB_HOST_IP \$GEONODE_LB_PORT \$GEOSERVER_LB_HOST_IP \$GEOSERVER_LB_PORT \$RESOLVER' < /etc/nginx/nginx.conf.envsubst > /etc/nginx/nginx.conf +envsubst '\$HTTP_HOST \$HTTPS_HOST \$HTTP_SCHEME \$GEONODE_LB_HOST_IP \$GEONODE_LB_PORT \$GEOSERVER_LB_HOST_IP \$GEOSERVER_LB_PORT \$RESOLVER' < /etc/nginx/nginx.https.available.conf.envsubst > /etc/nginx/nginx.https.available.conf +envsubst '\$HTTP_HOST \$HTTPS_HOST \$HTTP_SCHEME \$GEONODE_LB_HOST_IP \$GEONODE_LB_PORT \$GEOSERVER_LB_HOST_IP \$GEOSERVER_LB_PORT' < /etc/nginx/sites-enabled/geonode.conf.envsubst > /etc/nginx/sites-enabled/geonode.conf echo "Enabling or not https configuration" -if [ -z "${HTTPS_HOST}" ]; then +if [ -z "${HTTPS_HOST}" ]; then echo "" > /etc/nginx/nginx.https.enabled.conf else ln -sf /etc/nginx/nginx.https.available.conf /etc/nginx/nginx.https.enabled.conf diff --git a/scripts/docker/nginx/geonode.conf.envsubst b/scripts/docker/nginx/geonode.conf.envsubst index 9291ded95bc..1176ce2cc2b 100644 --- a/scripts/docker/nginx/geonode.conf.envsubst +++ b/scripts/docker/nginx/geonode.conf.envsubst @@ -6,8 +6,14 @@ charset utf-8; # max upload size client_max_body_size 100G; client_body_buffer_size 256K; +client_body_timeout 600s; large_client_header_buffers 4 64k; -proxy_read_timeout 600s; + +proxy_connect_timeout 600; +proxy_send_timeout 600; +proxy_read_timeout 600; +uwsgi_read_timeout 600; +send_timeout 600; fastcgi_hide_header Set-Cookie; @@ -35,33 +41,23 @@ gzip_types # GeoServer location /geoserver { - - # Using a variable is a trick to let Nginx start even if upstream host is not up yet - # (see https://sandro-keil.de/blog/2017/07/24/let-nginx-start-if-upstream-host-is-unavailable-or-down/) - set $upstream geoserver:8080; - - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - proxy_pass http://$upstream; -} - -# Jenkins -location /jenkins { - - # Using a variable is a trick to let Nginx start even if upstream host is not up yet - # (see https://sandro-keil.de/blog/2017/07/24/let-nginx-start-if-upstream-host-is-unavailable-or-down/) - set $upstream jenkins:$JENKINS_HTTP_PORT; - # set $upstream $HTTP_HOST$HTTPS_HOST:$JENKINS_HTTP_PORT; - - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - proxy_pass http://$upstream; + # Using a variable is a trick to let Nginx start even if upstream host is not up yet + # (see https://sandro-keil.de/blog/2017/07/24/let-nginx-start-if-upstream-host-is-unavailable-or-down/) + set $upstream $GEOSERVER_LB_HOST_IP:$GEOSERVER_LB_PORT; + + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Server $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $HTTP_SCHEME; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_hide_header X-Frame-Options; + proxy_pass http://$upstream; + proxy_http_version 1.1; + proxy_redirect http://$upstream $HTTP_SCHEME://$HTTP_HOST; + proxy_request_buffering off; + client_max_body_size 0; } # GeoNode @@ -89,31 +85,10 @@ location /uploaded/ { } } -location ~ ^/celery-monitor/? { - - # Using a variable is a trick to let Nginx start even if upstream host is not up yet - # (see https://sandro-keil.de/blog/2017/07/24/let-nginx-start-if-upstream-host-is-unavailable-or-down/) - set $upstream celerymonitor:5555; - - rewrite ^/celery-monitor/?(.*)$ /$1 break; - - sub_filter '="/' '="/celery-monitor/'; - sub_filter_last_modified on; - sub_filter_once off; - - # proxy_pass http://unix:/tmp/flower.sock:/; - proxy_pass http://$upstream; - proxy_redirect off; - proxy_set_header Host $host; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_http_version 1.1; -} - location / { # Using a variable is a trick to let Nginx start even if upstream host is not up yet # (see https://sandro-keil.de/blog/2017/07/24/let-nginx-start-if-upstream-host-is-unavailable-or-down/) - set $upstream django:8000; + set $upstream $GEONODE_LB_HOST_IP:$GEONODE_LB_PORT; if ($request_method = OPTIONS) { add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, OPTIONS"; @@ -128,27 +103,26 @@ location / { add_header Access-Control-Allow-Credentials false; add_header Access-Control-Allow-Headers "Content-Type, Accept, Authorization, Origin, User-Agent"; add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, OPTIONS"; - - proxy_connect_timeout 600; - proxy_send_timeout 600; - proxy_read_timeout 600; - send_timeout 600; + proxy_redirect off; proxy_set_header Host $host; + proxy_set_header Origin $HTTP_SCHEME://$host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Host $server_name; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $HTTP_SCHEME; proxy_hide_header X-Frame-Options; + proxy_request_buffering off; # uwsgi_params include /etc/nginx/uwsgi_params; - # proxy_pass http://$upstream; - uwsgi_pass $upstream; + proxy_pass http://$upstream; + # uwsgi_pass $upstream; # when a client closes the connection then keep the channel to uwsgi open. Otherwise uwsgi throws an IOError uwsgi_ignore_client_abort on; + uwsgi_request_buffering off; location ~* \.(?:js|jpg|jpeg|gif|png|tgz|gz|rar|bz2|doc|pdf|ppt|tar|wav|bmp|ttf|rtf|swf|ico|flv|woff|woff2|svg|xml)$ { gzip_static always; diff --git a/scripts/docker/nginx/nginx.conf.envsubst b/scripts/docker/nginx/nginx.conf.envsubst index 4a486226a36..b6065209d51 100644 --- a/scripts/docker/nginx/nginx.conf.envsubst +++ b/scripts/docker/nginx/nginx.conf.envsubst @@ -23,7 +23,7 @@ http { # TODO : do not use unencrypted connection even on LAN, but is it possible to have browser not complaining about unknown authority ? server { listen 80; - server_name $HTTP_HOST 127.0.0.1 geonode; + server_name $HTTP_HOST 127.0.0.1; include sites-enabled/*.conf; } diff --git a/setup.cfg b/setup.cfg index 46cefd95511..41a528efbc4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,21 +14,21 @@ classifiers = Intended Audience :: Developers Operating System :: OS Independent Topic :: Internet :: WWW/HTTP - Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.10 [options] zip_safe = False include_package_data = True -python_requires = >= 3.7 +python_requires = >= 3.10 packages = find: setup_requires = setuptools install_requires = # native dependencies - Pillow==9.5.0 - lxml==4.9.2 - psycopg2==2.9.6 - Django==3.2.19 + Pillow==10.0.0 + lxml==4.9.3 + psycopg2==2.9.7 + Django==3.2.20 # Other amqp==5.1.1 @@ -40,19 +40,19 @@ install_requires = Paver==1.3.4 python-slugify==8.0.1 decorator==5.1.1 - celery==5.2.7 - kombu==5.2.4 + celery==5.3.1 + kombu==5.3.1 vine==5.0.0 - tqdm==4.65.0 - Deprecated==1.2.13 + tqdm==4.66.1 + Deprecated==1.2.14 wrapt==1.15.0 - jsonschema==4.17.3 + jsonschema==4.19.0 zipstream-new==1.1.8 schema==0.7.5 rdflib==6.3.2 smart_open==6.3.0 - PyMuPDF==1.22.3 - pathvalidate==2.5.2 + PyMuPDF==1.22.5 + pathvalidate==3.1.0 # Django Apps django-allauth==0.54.0 @@ -70,30 +70,30 @@ install_requires = django-polymorphic==3.1.0 django-tastypie<0.15.0 django-tinymce==3.6.1 - django-grappelli==3.0.6 + django-grappelli==3.0.7 django-uuid-upload-path==1.0.0 django-widget-tweaks==1.4.12 - django-sequences==2.7 + django-sequences==2.8 oauthlib==3.2.2 - pyopenssl==23.1.1 - pyjwt==2.7.0 + pyopenssl==23.2.0 + pyjwt==2.8.0 # geopython dependencies - pyproj<3.6.0 + pyproj<3.7.0 OWSLib==0.29.2 pycsw==2.6.1 - SQLAlchemy==2.0.15 # required by PyCSW + SQLAlchemy==2.0.20 # required by PyCSW Shapely==1.8.5.post1 mercantile==1.2.1 geoip2==4.7.0 - numpy==1.24.* + numpy==1.25.* # # Apps with packages provided in GeoNode's PPA on Launchpad. # Django Apps - dj-database-url==2.0.0 + dj-database-url==2.1.0 dj-pagination==2.5.0 - django-select2==8.1.1 + django-select2==8.1.2 django-floppyforms<1.10.0 django-forms-bootstrap<=3.1.0 django-autocomplete-light==3.5.1 @@ -106,9 +106,9 @@ install_requires = djangorestframework-guardian==0.3.0 drf-extensions==0.7.1 drf-writable-nested==0.7.0 - drf-spectacular==0.26.2 + drf-spectacular==0.26.4 dynamic-rest==2.1.2 - Markdown==3.4.3 + Markdown==3.4.4 pinax-notifications==6.0.0 pinax-ratings==4.0.0 @@ -116,12 +116,12 @@ install_requires = # GeoNode org maintained apps. django-geonode-mapstore-client>=4.0.5,<5.0.0 geonode-importer>=1.0.2 - geonode-avatar==5.0.8 + django-avatar==7.1.1 geonode-oauth-toolkit==2.2.2 geonode-user-messages==2.0.2 geonode-announcements==2.0.2 geonode-django-activity-stream==0.10.0 - gn-arcrest==10.5.5 + gn-arcrest==10.5.6 geoserver-restconfig==2.0.9 gn-gsimporter==2.0.4 gisdata==0.5.4 @@ -135,14 +135,14 @@ install_requires = # storage manager dependencies django-storages==1.13.2 - dropbox==11.36.0 - google-cloud-storage==2.9.0 - google-cloud-core==2.3.2 - boto3==1.26.137 + dropbox==11.36.2 + google-cloud-storage==2.10.0 + google-cloud-core==2.3.3 + boto3==1.28.32 # Django Caches python-memcached<=1.59 - whitenoise==6.4.0 + whitenoise==6.5.0 Brotli==1.0.9 # Contribs @@ -159,7 +159,7 @@ install_requires = # required by monitoring psutil==5.9.5 - django-cors-headers==4.0.0 + django-cors-headers==4.2.0 user-agents django-user-agents xmljson @@ -168,39 +168,40 @@ install_requires = pycountry # production - uWSGI==2.0.21 - gunicorn==20.1.0 - ipython==8.13.2 - docker==6.1.2 - invoke==2.1.2 + uWSGI==2.0.22 + gunicorn==21.2.0 + ipython==8.14.0 + docker==6.1.3 + invoke==2.2.0 # tests - coverage==7.2.5 + coverage==7.3.0 requests-toolbelt==1.0.0 - flake8==6.0.0 - black==23.3.0 - pytest==7.3.1 + flake8==6.1.0 + black==23.7.0 + pytest==7.4.0 pytest-bdd==6.1.1 splinter==0.19.0 pytest-splinter==3.3.2 pytest-django==4.5.2 - setuptools>=59.1.1,<67.9.0 - pip==23.1.2 + setuptools>=59.1.1,<68.2.0 + pip==23.2.1 Twisted==22.10.0 pixelmatch==0.3.0 - factory-boy==3.2.1 + factory-boy==3.3.0 flaky==3.7.0 selenium>=4.1.0,<5.0.0 selenium-requests==2.0.3 - webdriver_manager==3.8.6 + webdriver_manager==4.0.0 # Security and audit - mistune==2.0.5 - wandb==0.15.3 + mistune==3.0.1 protobuf==3.20.3 mako==1.2.4 + paramiko==3.3.1 # not directly required, fixes Blowfish deprecation warning certifi>=2022.12.7 # not directly required, pinned by Snyk to avoid a vulnerability jwcrypto>=1.4 # not directly required, pinned by Snyk to avoid a vulnerability + cryptography>=41.0.0 # not directly required, pinned by Snyk to avoid a vulnerability [options.packages.find] exclude = tests diff --git a/start_django_async.sh b/start_django_async.sh index 78930d0377b..6b2d25eb84e 100755 --- a/start_django_async.sh +++ b/start_django_async.sh @@ -4,7 +4,7 @@ set -e export RESOURCE_PUBLISHING=True export ADMIN_MODERATE_UPLOADS=True export NOTIFICATION_ENABLED=True -export MONITORING_ENABLED=True +export MONITORING_ENABLED=False export EMAIL_ENABLED=True export BROKER_URL=amqp://guest:guest@localhost:5672/ export ASYNC_SIGNALS=True diff --git a/tasks.py b/tasks.py index f3d72a0fff2..967c08af386 100755 --- a/tasks.py +++ b/tasks.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- ######################################################################### # # Copyright (C) 2016 OSGeo @@ -23,10 +24,11 @@ import time import docker import socket +import ipaddress import logging import datetime -from urllib.parse import urlparse +from urllib.parse import urlparse, urlunparse from invoke import task BOOTSTRAP_IMAGE_CHEIP = "codenvy/che-ip:nightly" @@ -41,20 +43,12 @@ def waitfordbs(ctx): ctx.run(f"/usr/bin/wait-for-databases {db_host}", pty=True) -@task -def waitforgeoserver(ctx): - print("****************************geoserver********************************") - while not _gs_service_availability(f"{os.environ['GEOSERVER_LOCATION']}ows"): - print("Wait for GeoServer API availability...") - print("GeoServer is available for HTTP calls!") - - @task def update(ctx): print("***************************setting env*********************************") ctx.run("env", pty=True) - pub_ip = _geonode_public_host_ip() - print(f"Public Hostname or IP is {pub_ip}") + pub_host = _geonode_public_host() + print(f"Public Hostname is {pub_host}") pub_port = _geonode_public_port() print(f"Public PORT is {pub_port}") pub_protocol = "https" if pub_port == "443" else "http" @@ -62,13 +56,14 @@ def update(ctx): pub_port = None db_url = _update_db_connstring() geodb_url = _update_geodb_connstring() - service_ready = False - while not service_ready: + geonode_docker_host = None + for _cnt in range(1, 29): try: - socket.gethostbyname("geonode") - service_ready = True + geonode_docker_host = str(socket.gethostbyname("geonode")) + break except Exception: - time.sleep(10) + print(f"...waiting for NGINX to pop-up...{_cnt}") + time.sleep(1) override_env = "$HOME/.override_env" if os.path.exists(override_env): @@ -77,24 +72,24 @@ def update(ctx): print(f"Can not delete the {override_env} file as it doesn't exists") if pub_port: - siteurl = f"{pub_protocol}://{pub_ip}:{pub_port}/" - gs_pub_loc = f"http://{pub_ip}:{pub_port}/geoserver/" + siteurl = f"{pub_protocol}://{pub_host}:{pub_port}/" + gs_pub_loc = f"http://{pub_host}:{pub_port}/geoserver/" else: - siteurl = f"{pub_protocol}://{pub_ip}/" - gs_pub_loc = f"http://{pub_ip}/geoserver/" + siteurl = f"{pub_protocol}://{pub_host}/" + gs_pub_loc = f"http://{pub_host}/geoserver/" envs = { "local_settings": str(_localsettings()), "siteurl": os.environ.get("SITEURL", siteurl), - "geonode_docker_host": str(socket.gethostbyname("geonode")), + "geonode_docker_host": geonode_docker_host, "public_protocol": pub_protocol, - "public_fqdn": str(pub_ip) + str(f":{pub_port}" if pub_port else ""), - "public_host": str(pub_ip), + "public_fqdn": str(pub_host) + str(f":{pub_port}" if pub_port else ""), + "public_host": str(pub_host), "dburl": os.environ.get("DATABASE_URL", db_url), "geodburl": os.environ.get("GEODATABASE_URL", geodb_url), "static_root": os.environ.get("STATIC_ROOT", "/mnt/volumes/statics/static/"), "media_root": os.environ.get("MEDIA_ROOT", "/mnt/volumes/statics/uploaded/"), "geoip_path": os.environ.get("GEOIP_PATH", "/mnt/volumes/statics/geoip.db"), - "monitoring": os.environ.get("MONITORING_ENABLED", True), + "monitoring": os.environ.get("MONITORING_ENABLED", False), "monitoring_host_name": os.environ.get("MONITORING_HOST_NAME", "geonode"), "monitoring_service_name": os.environ.get("MONITORING_SERVICE_NAME", "local-geonode"), "monitoring_data_ttl": os.environ.get("MONITORING_DATA_TTL", 7), @@ -118,7 +113,7 @@ def update(ctx): ) except ValueError: current_allowed = [] - current_allowed.extend([str(pub_ip), f"{pub_ip}:{pub_port}"]) + current_allowed.extend([str(pub_host), f"{pub_host}:{pub_port}"]) allowed_hosts = [str(c) for c in current_allowed] + ['"geonode"', '"django"'] ctx.run( @@ -326,9 +321,15 @@ def update(ctx): def migrations(ctx): print("**************************migrations*******************************") ctx.run(f"python manage.py migrate --noinput --settings={_localsettings()}", pty=True) - ctx.run(f"python manage.py migrate --noinput --settings={_localsettings()} --database=datastore", pty=True) + ctx.run( + f"python manage.py migrate --noinput --settings={_localsettings()} --database=datastore", + pty=True, + ) try: - ctx.run(f"python manage.py rebuild_index --noinput --settings={_localsettings()}", pty=True) + ctx.run( + f"python manage.py rebuild_index --noinput --settings={_localsettings()}", + pty=True, + ) except Exception: pass @@ -338,7 +339,10 @@ def statics(ctx): print("**************************statics*******************************") try: ctx.run("mkdir -p /mnt/volumes/statics/{static,uploads}") - ctx.run(f"python manage.py collectstatic --noinput --settings={_localsettings()}", pty=True) + ctx.run( + f"python manage.py collectstatic --noinput --settings={_localsettings()}", + pty=True, + ) except Exception: import traceback @@ -352,28 +356,6 @@ def prepare(ctx): _prepare_oauth_fixture() ctx.run("rm -rf /tmp/default_site.json", pty=True) _prepare_site_fixture() - # Updating OAuth2 Service Config - new_ext_ip = os.environ["SITEURL"] - client_id = os.environ["OAUTH2_CLIENT_ID"] - client_secret = os.environ["OAUTH2_CLIENT_SECRET"] - oauth_config = "/geoserver_data/data/security/filter/geonode-oauth2/config.xml" - ctx.run(f'sed -i "s|.*|{client_id}|g" {oauth_config}', pty=True) - ctx.run( - f'sed -i "s|.*|{client_secret}|g" {oauth_config}', - pty=True, - ) - ctx.run( - f'sed -i "s|.*|{new_ext_ip}o/authorize/|g" {oauth_config}', # noqa - pty=True, - ) - ctx.run( - f'sed -i "s|.*|{new_ext_ip}geoserver/index.html|g" {oauth_config}', # noqa - pty=True, - ) - ctx.run( - f'sed -i "s|.*|{new_ext_ip}account/logout/|g" {oauth_config}', - pty=True, - ) @task @@ -400,38 +382,36 @@ def fixtures(ctx): def collectstatic(ctx): print("************************static artifacts******************************") ctx.run( - f"django-admin.py collectstatic --noinput \ + f"django-admin collectstatic --noinput \ --settings={_localsettings()}", pty=True, ) -@task -def geoserverfixture(ctx): - print("********************geoserver fixture********************************") - _geoserver_info_provision(f"{os.environ['GEOSERVER_LOCATION']}rest/") - - @task def monitoringfixture(ctx): - print("*******************monitoring fixture********************************") - ctx.run("rm -rf /tmp/default_monitoring_apps_docker.json", pty=True) - _prepare_monitoring_fixture() - try: - ctx.run( - f"django-admin.py loaddata /tmp/default_monitoring_apps_docker.json \ ---settings={_localsettings()}", - pty=True, - ) - except Exception as e: - logger.error(f"ERROR installing monitoring fixture: {str(e)}") - - -@task -def updategeoip(ctx): - print("**************************update geoip*******************************") if ast.literal_eval(os.environ.get("MONITORING_ENABLED", "False")): - ctx.run(f"django-admin.py updategeoip --settings={_localsettings()}", pty=True) + print("*******************monitoring fixture********************************") + ctx.run("rm -rf /tmp/default_monitoring_apps_docker.json", pty=True) + _prepare_monitoring_fixture() + try: + ctx.run( + f"django-admin loaddata geonode/monitoring/fixtures/metric_data.json \ + --settings={_localsettings()}", + pty=True, + ) + ctx.run( + f"django-admin loaddata geonode/monitoring/fixtures/notifications.json \ + --settings={_localsettings()}", + pty=True, + ) + ctx.run( + f"django-admin loaddata /tmp/default_monitoring_apps_docker.json \ + --settings={_localsettings()}", + pty=True, + ) + except Exception as e: + logger.error(f"ERROR installing monitoring fixture: {str(e)}") @task @@ -439,10 +419,11 @@ def updateadmin(ctx): print("***********************update admin details**************************") ctx.run("rm -rf /tmp/django_admin_docker.json", pty=True) _prepare_admin_fixture( - os.environ.get("ADMIN_PASSWORD", "admin"), os.environ.get("ADMIN_EMAIL", "admin@example.org") + os.environ.get("ADMIN_PASSWORD", "admin"), + os.environ.get("ADMIN_EMAIL", "admin@example.org"), ) ctx.run( - f"django-admin.py loaddata /tmp/django_admin_docker.json \ + f"django-admin loaddata /tmp/django_admin_docker.json \ --settings={_localsettings()}", pty=True, ) @@ -489,6 +470,14 @@ def _docker_host_ip(): return ip_list[0] +def _is_valid_ip(ip): + try: + ipaddress.IPv4Address(ip) + return True + except Exception as e: + return False + + def _container_exposed_port(component, instname): port = "80" try: @@ -497,7 +486,10 @@ def _container_exposed_port(component, instname): [ c.attrs["Config"]["ExposedPorts"] for c in client.containers.list( - filters={"label": f"org.geonode.component={component}", "status": "running"} + filters={ + "label": f"org.geonode.component={component}", + "status": "running", + } ) if str(instname) in c.name ][0] @@ -536,26 +528,16 @@ def _localsettings(): return settings -def _gs_service_availability(url): - import requests - - try: - r = requests.request("get", url, verify=False) - r.raise_for_status() # Raises a HTTPError if the status is 4xx, 5xxx - except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e: - logger.error(f"GeoServer connection error is {e}") - return False - except requests.exceptions.HTTPError as er: - logger.error(f"GeoServer HTTP error is {er}") - return False - else: - logger.info("GeoServer API are available!") - return True +def _geonode_public_host(): + gn_pub_hostip = os.getenv("GEONODE_LB_HOST_IP", None) + if not gn_pub_hostip: + gn_pub_hostip = _docker_host_ip() + return gn_pub_hostip def _geonode_public_host_ip(): gn_pub_hostip = os.getenv("GEONODE_LB_HOST_IP", None) - if not gn_pub_hostip: + if not gn_pub_hostip or not _is_valid_ip(gn_pub_hostip): gn_pub_hostip = _docker_host_ip() return gn_pub_hostip @@ -569,33 +551,8 @@ def _geonode_public_port(): return gn_pub_port -def _geoserver_info_provision(url): - from django.conf import settings - from geoserver.catalog import Catalog - - print("Setting GeoServer Admin Password...") - cat = Catalog(url, username=settings.OGC_SERVER_DEFAULT_USER, password=settings.OGC_SERVER_DEFAULT_PASSWORD) - headers = {"Content-type": "application/xml", "Accept": "application/xml"} - data = f""" - - {(os.getenv('GEOSERVER_ADMIN_PASSWORD', 'geoserver'))} -""" - - response = cat.http_request(f"{cat.service_url}/security/self/password", method="PUT", data=data, headers=headers) - print(f"Response Code: {response.status_code}") - if response.status_code == 200: - print("GeoServer admin password updated SUCCESSFULLY!") - else: - logger.warning(f"WARNING: GeoServer admin password *NOT* updated: code [{response.status_code}]") - - def _prepare_oauth_fixture(): upurl = urlparse(os.environ["SITEURL"]) - net_scheme = upurl.scheme - pub_ip = _geonode_public_host_ip() - print(f"Public Hostname or IP is {pub_ip}") - pub_port = _geonode_public_port() - print(f"Public PORT is {pub_port}") default_fixture = [ { "model": "oauth2_provider.application", @@ -605,9 +562,7 @@ def _prepare_oauth_fixture(): "created": "2018-05-31T10:00:31.661Z", "updated": "2018-05-31T11:30:31.245Z", "algorithm": "RS256", - "redirect_uris": f"{net_scheme}://{pub_ip}:{pub_port}/geoserver/index.html" - if pub_port - else f"{net_scheme}://{pub_ip}/geoserver/index.html", + "redirect_uris": f"{urlunparse(upurl)}geoserver/index.html", "name": "GeoServer", "authorization_grant_type": "authorization-code", "client_type": "confidential", @@ -624,7 +579,11 @@ def _prepare_oauth_fixture(): def _prepare_site_fixture(): upurl = urlparse(os.environ["SITEURL"]) default_fixture = [ - {"model": "sites.site", "pk": 1, "fields": {"domain": str(upurl.hostname), "name": str(upurl.hostname)}} + { + "model": "sites.site", + "pk": 1, + "fields": {"domain": str(upurl.hostname), "name": str(upurl.hostname)}, + } ] with open("/tmp/default_site.json", "w") as fixturefile: json.dump(default_fixture, fixturefile) @@ -649,11 +608,19 @@ def _prepare_monitoring_fixture(): d = "1970-01-01 00:00:00" default_fixture = [ { - "fields": {"active": True, "ip": str(geonode_ip), "name": str(os.environ["MONITORING_HOST_NAME"])}, + "fields": { + "active": True, + "ip": str(geonode_ip), + "name": str(os.environ["MONITORING_HOST_NAME"]), + }, "model": "monitoring.host", "pk": 1, }, - {"fields": {"active": True, "ip": str(geoserver_ip), "name": "geoserver"}, "model": "monitoring.host", "pk": 2}, + { + "fields": {"active": True, "ip": str(geoserver_ip), "name": "geoserver"}, + "model": "monitoring.host", + "pk": 2, + }, { "fields": { "name": str(os.environ["MONITORING_SERVICE_NAME"]), diff --git a/uwsgi.ini b/uwsgi.ini index a0f786b7058..c519637152e 100644 --- a/uwsgi.ini +++ b/uwsgi.ini @@ -1,6 +1,7 @@ [uwsgi] -uwsgi-socket = 0.0.0.0:8000 -http-socket = 0.0.0.0:8001 +# uwsgi-socket = 0.0.0.0:8000 +http-socket = 0.0.0.0:8000 +logto = /var/log/geonode.log # pidfile = /tmp/geonode.pid chdir = /usr/src/geonode/ @@ -13,15 +14,12 @@ vacuum = true ; Delete sockets during shutdown single-interpreter = true die-on-term = true ; Shutdown when receiving SIGTERM (default is respawn) need-app = true - -# logging -# path to where uwsgi logs will be saved -logto = /var/log/geonode.log +thunder-lock = true touch-reload = /usr/src/geonode/geonode/wsgi.py buffer-size = 32768 -harakiri = 60 ; forcefully kill workers after 60 seconds +harakiri = 600 ; forcefully kill workers after 600 seconds py-callos-afterfork = true ; allow workers to trap signals max-requests = 1000 ; Restart workers after this many requests @@ -43,4 +41,4 @@ cheaper-busyness-backlog-alert = 16 ; Spawn emergency workers if more than this cheaper-busyness-backlog-step = 2 ; How many emergency workers to create if there are too many requests in the queue # daemonize = /var/log/uwsgi/geonode.log -# cron = -1 -1 -1 -1 -1 /usr/local/bin/python /usr/src/{{project_name}}/manage.py collect_metrics -n +# cron = -1 -1 -1 -1 -1 /usr/local/bin/python /usr/src/geonode/manage.py collect_metrics -n